From 97aa749578a05b207d54dbbcd8580201b5eba359 Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 6 Sep 2023 02:02:44 +0200 Subject: [PATCH 01/52] Vendor `jquery.are-you-sure` with strict mode fixes (#26901) Extract from https://github.com/go-gitea/gitea/pull/25940 and because https://github.com/go-gitea/gitea/pull/26743 does seem to need more work. This will be required if we are to run our JS in [strict mode](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode). Previously, the two variables `$fields` and `$dirtyForms` polluted `window`: image --- package-lock.json | 12 -- package.json | 1 - web_src/js/features/common-global.js | 2 +- web_src/js/vendor/jquery.are-you-sure.js | 195 +++++++++++++++++++++++ webpack.config.js | 1 - 5 files changed, 196 insertions(+), 15 deletions(-) create mode 100644 web_src/js/vendor/jquery.are-you-sure.js diff --git a/package-lock.json b/package-lock.json index dda23bcbd4..db0480a5e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,6 @@ "escape-goat": "4.0.0", "fast-glob": "3.3.1", "jquery": "3.7.1", - "jquery.are-you-sure": "1.9.0", "katex": "0.16.8", "license-checker-webpack-plugin": "0.2.1", "lightningcss-loader": "2.1.0", @@ -6466,17 +6465,6 @@ "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" }, - "node_modules/jquery.are-you-sure": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/jquery.are-you-sure/-/jquery.are-you-sure-1.9.0.tgz", - "integrity": "sha512-2r0uFx8CyAopjeHGOdvvwpFP921TnW1+v1uJXcAWQYHYGB1tryTDhQY+5u6HsVeMwbWiRTKVZFWnLaFpDvIqZQ==", - "dependencies": { - "jquery": ">=1.4.2" - }, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/js-levenshtein-esm": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/js-levenshtein-esm/-/js-levenshtein-esm-1.2.0.tgz", diff --git a/package.json b/package.json index e4f1743feb..224a1422ec 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "escape-goat": "4.0.0", "fast-glob": "3.3.1", "jquery": "3.7.1", - "jquery.are-you-sure": "1.9.0", "katex": "0.16.8", "license-checker-webpack-plugin": "0.2.1", "lightningcss-loader": "2.1.0", diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js index d02a82a2ef..03169e6815 100644 --- a/web_src/js/features/common-global.js +++ b/web_src/js/features/common-global.js @@ -1,5 +1,5 @@ import $ from 'jquery'; -import 'jquery.are-you-sure'; +import '../vendor/jquery.are-you-sure.js'; import {clippie} from 'clippie'; import {createDropzone} from './dropzone.js'; import {initCompColorPicker} from './comp/ColorPicker.js'; diff --git a/web_src/js/vendor/jquery.are-you-sure.js b/web_src/js/vendor/jquery.are-you-sure.js new file mode 100644 index 0000000000..e06da39fc9 --- /dev/null +++ b/web_src/js/vendor/jquery.are-you-sure.js @@ -0,0 +1,195 @@ +// Fork of the upstream module. The only changes are the addition of `const` on +// lines 93 and 161 to make it strict mode compatible. + +/*! + * jQuery Plugin: Are-You-Sure (Dirty Form Detection) + * https://github.com/codedance/jquery.AreYouSure/ + * + * Copyright (c) 2012-2014, Chris Dance and PaperCut Software http://www.papercut.com/ + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * Author: chris.dance@papercut.com + * Version: 1.9.0 + * Date: 13th August 2014 + */ +(function($) { + + $.fn.areYouSure = function(options) { + + var settings = $.extend( + { + 'message' : 'You have unsaved changes!', + 'dirtyClass' : 'dirty', + 'change' : null, + 'silent' : false, + 'addRemoveFieldsMarksDirty' : false, + 'fieldEvents' : 'change keyup propertychange input', + 'fieldSelector': ":input:not(input[type=submit]):not(input[type=button])" + }, options); + + var getValue = function($field) { + if ($field.hasClass('ays-ignore') + || $field.hasClass('aysIgnore') + || $field.attr('data-ays-ignore') + || $field.attr('name') === undefined) { + return null; + } + + if ($field.is(':disabled')) { + return 'ays-disabled'; + } + + var val; + var type = $field.attr('type'); + if ($field.is('select')) { + type = 'select'; + } + + switch (type) { + case 'checkbox': + case 'radio': + val = $field.is(':checked'); + break; + case 'select': + val = ''; + $field.find('option').each(function(o) { + var $option = $(this); + if ($option.is(':selected')) { + val += $option.val(); + } + }); + break; + default: + val = $field.val(); + } + + return val; + }; + + var storeOrigValue = function($field) { + $field.data('ays-orig', getValue($field)); + }; + + var checkForm = function(evt) { + + var isFieldDirty = function($field) { + var origValue = $field.data('ays-orig'); + if (undefined === origValue) { + return false; + } + return (getValue($field) != origValue); + }; + + var $form = ($(this).is('form')) + ? $(this) + : $(this).parents('form'); + + // Test on the target first as it's the most likely to be dirty + if (isFieldDirty($(evt.target))) { + setDirtyStatus($form, true); + return; + } + + const $fields = $form.find(settings.fieldSelector); + + if (settings.addRemoveFieldsMarksDirty) { + // Check if field count has changed + var origCount = $form.data("ays-orig-field-count"); + if (origCount != $fields.length) { + setDirtyStatus($form, true); + return; + } + } + + // Brute force - check each field + var isDirty = false; + $fields.each(function() { + var $field = $(this); + if (isFieldDirty($field)) { + isDirty = true; + return false; // break + } + }); + + setDirtyStatus($form, isDirty); + }; + + var initForm = function($form) { + var fields = $form.find(settings.fieldSelector); + $(fields).each(function() { storeOrigValue($(this)); }); + $(fields).unbind(settings.fieldEvents, checkForm); + $(fields).bind(settings.fieldEvents, checkForm); + $form.data("ays-orig-field-count", $(fields).length); + setDirtyStatus($form, false); + }; + + var setDirtyStatus = function($form, isDirty) { + var changed = isDirty != $form.hasClass(settings.dirtyClass); + $form.toggleClass(settings.dirtyClass, isDirty); + + // Fire change event if required + if (changed) { + if (settings.change) settings.change.call($form, $form); + + if (isDirty) $form.trigger('dirty.areYouSure', [$form]); + if (!isDirty) $form.trigger('clean.areYouSure', [$form]); + $form.trigger('change.areYouSure', [$form]); + } + }; + + var rescan = function() { + var $form = $(this); + var fields = $form.find(settings.fieldSelector); + $(fields).each(function() { + var $field = $(this); + if (!$field.data('ays-orig')) { + storeOrigValue($field); + $field.bind(settings.fieldEvents, checkForm); + } + }); + // Check for changes while we're here + $form.trigger('checkform.areYouSure'); + }; + + var reinitialize = function() { + initForm($(this)); + } + + if (!settings.silent && !window.aysUnloadSet) { + window.aysUnloadSet = true; + $(window).bind('beforeunload', function() { + const $dirtyForms = $("form").filter('.' + settings.dirtyClass); + if ($dirtyForms.length == 0) { + return; + } + // Prevent multiple prompts - seen on Chrome and IE + if (navigator.userAgent.toLowerCase().match(/msie|chrome/)) { + if (window.aysHasPrompted) { + return; + } + window.aysHasPrompted = true; + window.setTimeout(function() {window.aysHasPrompted = false;}, 900); + } + return settings.message; + }); + } + + return this.each(function(elem) { + if (!$(this).is('form')) { + return; + } + var $form = $(this); + + $form.submit(function() { + $form.removeClass(settings.dirtyClass); + }); + $form.bind('reset', function() { setDirtyStatus($form, false); }); + // Add a custom events + $form.bind('rescan.areYouSure', rescan); + $form.bind('reinitialize.areYouSure', reinitialize); + $form.bind('checkform.areYouSure', checkForm); + initForm($form); + }); + }; +})(jQuery); diff --git a/webpack.config.js b/webpack.config.js index c6b28068f0..f95296c380 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -206,7 +206,6 @@ export default { }).join('\n'); }, override: { - 'jquery.are-you-sure@*': {licenseName: 'MIT'}, // https://github.com/codedance/jquery.AreYouSure/pull/147 'khroma@*': {licenseName: 'MIT'}, // https://github.com/fabiospampinato/khroma/pull/33 }, emitError: true, From 31c92d9695a43ff77a3eef2e8383343ef60f8ae7 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 6 Sep 2023 10:13:08 +0800 Subject: [PATCH 02/52] Add missing translation (#26926) Fix #26923 --- options/locale/locale_en-US.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 08aa320cc2..bdb9b0c9dd 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1764,6 +1764,7 @@ pulls.rebase_conflict_summary = Error Message pulls.unrelated_histories = Merge Failed: The merge head and base do not share a common history. Hint: Try a different strategy pulls.merge_out_of_date = Merge Failed: Whilst generating the merge, the base was updated. Hint: Try again. pulls.head_out_of_date = Merge Failed: Whilst generating the merge, the head was updated. Hint: Try again. +pulls.has_merged = Failed: The pull request has been merged, you cannot merge again or change the target branch. pulls.push_rejected = Merge Failed: The push was rejected. Review the Git Hooks for this repository. pulls.push_rejected_summary = Full Rejection Message pulls.push_rejected_no_message = Merge Failed: The push was rejected but there was no remote message.
Review the Git Hooks for this repository From 0850be6750b3df69e2eb5c52e7df53705c517fbc Mon Sep 17 00:00:00 2001 From: yp05327 <576951401@qq.com> Date: Wed, 6 Sep 2023 15:38:14 +0900 Subject: [PATCH 03/52] Fix the display of org level badges (#26504) Follow #24654 #22705 #24232 In some pages we still have missing badges, for example: ![image](https://github.com/go-gitea/gitea/assets/18380374/f57fae6d-95ad-4996-8881-160c9cd27768) ![image](https://github.com/go-gitea/gitea/assets/18380374/11e86d43-b715-4d14-bdf0-51bf8b5c7b01) ![image](https://github.com/go-gitea/gitea/assets/18380374/61c514e7-d8f6-4c93-a61f-60604619e3a7) --- modules/context/org.go | 1 + modules/context/repo.go | 1 + routers/web/org/home.go | 1 - routers/web/org/members.go | 1 - routers/web/org/setting.go | 1 - routers/web/org/teams.go | 1 - routers/web/repo/packages.go | 1 - routers/web/shared/user/header.go | 1 - routers/web/user/code.go | 1 - services/context/user.go | 1 + 10 files changed, 3 insertions(+), 7 deletions(-) diff --git a/modules/context/org.go b/modules/context/org.go index 2d7cf5185c..7638ffdd3f 100644 --- a/modules/context/org.go +++ b/modules/context/org.go @@ -250,6 +250,7 @@ func HandleOrgAssignment(ctx *Context, args ...bool) { return } } + ctx.Data["ContextUser"] = ctx.ContextUser ctx.Data["CanReadProjects"] = ctx.Org.CanReadUnit(ctx, unit.TypeProjects) ctx.Data["CanReadPackages"] = ctx.Org.CanReadUnit(ctx, unit.TypePackages) diff --git a/modules/context/repo.go b/modules/context/repo.go index f5c56cf833..8a16d311b1 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -471,6 +471,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc { } ctx.Repo.Owner = owner ctx.ContextUser = owner + ctx.Data["ContextUser"] = ctx.ContextUser ctx.Data["Username"] = ctx.Repo.Owner.Name // redirect link to wiki diff --git a/routers/web/org/home.go b/routers/web/org/home.go index 613dff2182..a69fdedba4 100644 --- a/routers/web/org/home.go +++ b/routers/web/org/home.go @@ -152,7 +152,6 @@ func Home(ctx *context.Context) { pager.SetDefaultParams(ctx) pager.AddParam(ctx, "language", "Language") ctx.Data["Page"] = pager - ctx.Data["ContextUser"] = ctx.ContextUser ctx.Data["ShowMemberAndTeamTab"] = ctx.Org.IsMember || len(members) > 0 diff --git a/routers/web/org/members.go b/routers/web/org/members.go index fae8b48128..3c073211ae 100644 --- a/routers/web/org/members.go +++ b/routers/web/org/members.go @@ -62,7 +62,6 @@ func Members(ctx *context.Context) { } ctx.Data["Page"] = pager ctx.Data["Members"] = members - ctx.Data["ContextUser"] = ctx.ContextUser ctx.Data["MembersIsPublicMember"] = membersIsPublic ctx.Data["MembersIsUserOrgOwner"] = organization.IsUserOrgOwner(members, org.ID) ctx.Data["MembersTwoFaStatus"] = members.GetTwoFaStatus() diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go index 957daab646..51d5282fa0 100644 --- a/routers/web/org/setting.go +++ b/routers/web/org/setting.go @@ -45,7 +45,6 @@ func Settings(ctx *context.Context) { ctx.Data["PageIsSettingsOptions"] = true ctx.Data["CurrentVisibility"] = ctx.Org.Organization.Visibility ctx.Data["RepoAdminChangeTeamAccess"] = ctx.Org.Organization.RepoAdminChangeTeamAccess - ctx.Data["ContextUser"] = ctx.ContextUser ctx.HTML(http.StatusOK, tplSettingsOptions) } diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go index 3b07bba713..fecb0cd5e9 100644 --- a/routers/web/org/teams.go +++ b/routers/web/org/teams.go @@ -56,7 +56,6 @@ func Teams(ctx *context.Context) { } } ctx.Data["Teams"] = ctx.Org.Teams - ctx.Data["ContextUser"] = ctx.ContextUser ctx.HTML(http.StatusOK, tplTeams) } diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go index 6ad2f71b5c..ac9e64d774 100644 --- a/routers/web/repo/packages.go +++ b/routers/web/repo/packages.go @@ -58,7 +58,6 @@ func Packages(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("packages.title") ctx.Data["IsPackagesPage"] = true - ctx.Data["ContextUser"] = ctx.ContextUser ctx.Data["Query"] = query ctx.Data["PackageType"] = packageType ctx.Data["AvailableTypes"] = packages.TypeList diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go index 9b1918ed16..6273e11fc5 100644 --- a/routers/web/shared/user/header.go +++ b/routers/web/shared/user/header.go @@ -22,7 +22,6 @@ import ( func prepareContextForCommonProfile(ctx *context.Context) { ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled - ctx.Data["ContextUser"] = ctx.ContextUser ctx.Data["EnableFeed"] = setting.Other.EnableFeed ctx.Data["FeedURL"] = ctx.ContextUser.HomeLink() } diff --git a/routers/web/user/code.go b/routers/web/user/code.go index 033f65c9c0..29b8b91c89 100644 --- a/routers/web/user/code.go +++ b/routers/web/user/code.go @@ -30,7 +30,6 @@ func CodeSearch(ctx *context.Context) { ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled ctx.Data["Title"] = ctx.Tr("explore.code") - ctx.Data["ContextUser"] = ctx.ContextUser language := ctx.FormTrim("l") keyword := ctx.FormTrim("q") diff --git a/services/context/user.go b/services/context/user.go index 62d2dc0aa2..81c2746819 100644 --- a/services/context/user.go +++ b/services/context/user.go @@ -27,6 +27,7 @@ func UserAssignmentWeb() func(ctx *context.Context) { } } ctx.ContextUser = userAssignment(ctx.Base, ctx.Doer, errorFn) + ctx.Data["ContextUser"] = ctx.ContextUser } } From 113eb5fc24f0890950167ca0dcc914bf858861ff Mon Sep 17 00:00:00 2001 From: Kerwin Bryant Date: Wed, 6 Sep 2023 15:00:45 +0800 Subject: [PATCH 04/52] Fix UI anomalies (#26929) --- web_src/css/base.css | 1 + 1 file changed, 1 insertion(+) diff --git a/web_src/css/base.css b/web_src/css/base.css index cbd3336d26..9f42610641 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -2024,6 +2024,7 @@ a.ui.basic.label:hover { top: 0; bottom: 0; display: flex; + align-items: center; } .ui.attached.header > .ui.right > .button, From 460a2b0edffe71d9e64633beaa1071fcf4a33369 Mon Sep 17 00:00:00 2001 From: FuXiaoHei Date: Wed, 6 Sep 2023 15:41:06 +0800 Subject: [PATCH 05/52] Artifacts retention and auto clean up (#26131) Currently, Artifact does not have an expiration and automatic cleanup mechanism, and this feature needs to be added. It contains the following key points: - [x] add global artifact retention days option in config file. Default value is 90 days. - [x] add cron task to clean up expired artifacts. It should run once a day. - [x] support custom retention period from `retention-days: 5` in `upload-artifact@v3`. - [x] artifacts link in actions view should be non-clickable text when expired. --- custom/conf/app.example.ini | 2 + .../config-cheat-sheet.en-us.md | 7 ++++ models/actions/artifact.go | 38 ++++++++++++----- models/migrations/migrations.go | 2 + models/migrations/v1_21/v274.go | 36 ++++++++++++++++ modules/setting/actions.go | 14 +++++-- options/locale/locale_en-US.ini | 1 + routers/api/actions/artifacts.go | 28 +++++++++++-- routers/api/actions/artifacts_chunks.go | 2 +- routers/web/repo/actions/view.go | 14 +++++-- services/actions/cleanup.go | 42 +++++++++++++++++++ services/cron/tasks_basic.go | 18 ++++++++ .../integration/api_actions_artifact_test.go | 42 ++++++++++++++++++- 13 files changed, 221 insertions(+), 25 deletions(-) create mode 100644 models/migrations/v1_21/v274.go create mode 100644 services/actions/cleanup.go diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index dd673190aa..a2fab2fd50 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -2564,6 +2564,8 @@ LEVEL = Info ;; ;; Default platform to get action plugins, `github` for `https://github.com`, `self` for the current Gitea instance. ;DEFAULT_ACTIONS_URL = github +;; Default artifact retention time in days, default is 90 days +;ARTIFACT_RETENTION_DAYS = 90 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md index 4158f14cb1..7e8befb8b7 100644 --- a/docs/content/administration/config-cheat-sheet.en-us.md +++ b/docs/content/administration/config-cheat-sheet.en-us.md @@ -955,6 +955,12 @@ Default templates for project boards: - `SCHEDULE`: **@midnight** : Interval as a duration between each synchronization, it will always attempt synchronization when the instance starts. - `UPDATE_EXISTING`: **true**: Create new users, update existing user data and disable users that are not in external source anymore (default) or only create new users if UPDATE_EXISTING is set to false. +## Cron - Cleanup Expired Actions Assets (`cron.cleanup_actions`) + +- `ENABLED`: **true**: Enable cleanup expired actions assets job. +- `RUN_AT_START`: **true**: Run job at start time (if ENABLED). +- `SCHEDULE`: **@midnight** : Cron syntax for the job. + ### Extended cron tasks (not enabled by default) #### Cron - Garbage collect all repositories (`cron.git_gc_repos`) @@ -1381,6 +1387,7 @@ PROXY_HOSTS = *.github.com - `DEFAULT_ACTIONS_URL`: **github**: Default platform to get action plugins, `github` for `https://github.com`, `self` for the current Gitea instance. - `STORAGE_TYPE`: **local**: Storage type for actions logs, `local` for local disk or `minio` for s3 compatible object storage service, default is `local` or other name defined with `[storage.xxx]` - `MINIO_BASE_PATH`: **actions_log/**: Minio base path on the bucket only available when STORAGE_TYPE is `minio` +- `ARTIFACT_RETENTION_DAYS`: **90**: Number of days to keep artifacts. Set to 0 to disable artifact retention. Default is 90 days if not set. `DEFAULT_ACTIONS_URL` indicates where the Gitea Actions runners should find the actions with relative path. For example, `uses: actions/checkout@v3` means `https://github.com/actions/checkout@v3` since the value of `DEFAULT_ACTIONS_URL` is `github`. diff --git a/models/actions/artifact.go b/models/actions/artifact.go index 800dcd0d50..849a90fd10 100644 --- a/models/actions/artifact.go +++ b/models/actions/artifact.go @@ -9,19 +9,21 @@ package actions import ( "context" "errors" + "time" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" ) +// ArtifactStatus is the status of an artifact, uploading, expired or need-delete +type ArtifactStatus int64 + const ( - // ArtifactStatusUploadPending is the status of an artifact upload that is pending - ArtifactStatusUploadPending = 1 - // ArtifactStatusUploadConfirmed is the status of an artifact upload that is confirmed - ArtifactStatusUploadConfirmed = 2 - // ArtifactStatusUploadError is the status of an artifact upload that is errored - ArtifactStatusUploadError = 3 + ArtifactStatusUploadPending ArtifactStatus = iota + 1 // 1, ArtifactStatusUploadPending is the status of an artifact upload that is pending + ArtifactStatusUploadConfirmed // 2, ArtifactStatusUploadConfirmed is the status of an artifact upload that is confirmed + ArtifactStatusUploadError // 3, ArtifactStatusUploadError is the status of an artifact upload that is errored + ArtifactStatusExpired // 4, ArtifactStatusExpired is the status of an artifact that is expired ) func init() { @@ -45,9 +47,10 @@ type ActionArtifact struct { Status int64 `xorm:"index"` // The status of the artifact, uploading, expired or need-delete CreatedUnix timeutil.TimeStamp `xorm:"created"` UpdatedUnix timeutil.TimeStamp `xorm:"updated index"` + ExpiredUnix timeutil.TimeStamp `xorm:"index"` // The time when the artifact will be expired } -func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPath string) (*ActionArtifact, error) { +func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPath string, expiredDays int64) (*ActionArtifact, error) { if err := t.LoadJob(ctx); err != nil { return nil, err } @@ -61,7 +64,8 @@ func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPa RepoID: t.RepoID, OwnerID: t.OwnerID, CommitSHA: t.CommitSHA, - Status: ArtifactStatusUploadPending, + Status: int64(ArtifactStatusUploadPending), + ExpiredUnix: timeutil.TimeStamp(time.Now().Unix() + 3600*24*expiredDays), } if _, err := db.GetEngine(ctx).Insert(artifact); err != nil { return nil, err @@ -126,15 +130,16 @@ func ListUploadedArtifactsByRunID(ctx context.Context, runID int64) ([]*ActionAr type ActionArtifactMeta struct { ArtifactName string FileSize int64 + Status int64 } // ListUploadedArtifactsMeta returns all uploaded artifacts meta of a run func ListUploadedArtifactsMeta(ctx context.Context, runID int64) ([]*ActionArtifactMeta, error) { arts := make([]*ActionArtifactMeta, 0, 10) return arts, db.GetEngine(ctx).Table("action_artifact"). - Where("run_id=? AND status=?", runID, ArtifactStatusUploadConfirmed). + Where("run_id=? AND (status=? OR status=?)", runID, ArtifactStatusUploadConfirmed, ArtifactStatusExpired). GroupBy("artifact_name"). - Select("artifact_name, sum(file_size) as file_size"). + Select("artifact_name, sum(file_size) as file_size, max(status) as status"). Find(&arts) } @@ -149,3 +154,16 @@ func ListArtifactsByRunIDAndName(ctx context.Context, runID int64, name string) arts := make([]*ActionArtifact, 0, 10) return arts, db.GetEngine(ctx).Where("run_id=? AND artifact_name=?", runID, name).Find(&arts) } + +// ListNeedExpiredArtifacts returns all need expired artifacts but not deleted +func ListNeedExpiredArtifacts(ctx context.Context) ([]*ActionArtifact, error) { + arts := make([]*ActionArtifact, 0, 10) + return arts, db.GetEngine(ctx). + Where("expired_unix < ? AND status = ?", timeutil.TimeStamp(time.Now().Unix()), ArtifactStatusUploadConfirmed).Find(&arts) +} + +// SetArtifactExpired sets an artifact to expired +func SetArtifactExpired(ctx context.Context, artifactID int64) error { + _, err := db.GetEngine(ctx).Where("id=? AND status = ?", artifactID, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: int64(ArtifactStatusExpired)}) + return err +} diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 9f4acda236..40df1cd624 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -528,6 +528,8 @@ var migrations = []Migration{ NewMigration("Add Version to ActionRun table", v1_21.AddVersionToActionRunTable), // v273 -> v274 NewMigration("Add Action Schedule Table", v1_21.AddActionScheduleTable), + // v274 -> v275 + NewMigration("Add Actions artifacts expiration date", v1_21.AddExpiredUnixColumnInActionArtifactTable), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v1_21/v274.go b/models/migrations/v1_21/v274.go new file mode 100644 index 0000000000..df5994f159 --- /dev/null +++ b/models/migrations/v1_21/v274.go @@ -0,0 +1,36 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_21 //nolint +import ( + "time" + + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" +) + +func AddExpiredUnixColumnInActionArtifactTable(x *xorm.Engine) error { + type ActionArtifact struct { + ExpiredUnix timeutil.TimeStamp `xorm:"index"` // time when the artifact will be expired + } + if err := x.Sync(new(ActionArtifact)); err != nil { + return err + } + return updateArtifactsExpiredUnixTo90Days(x) +} + +func updateArtifactsExpiredUnixTo90Days(x *xorm.Engine) error { + sess := x.NewSession() + defer sess.Close() + + if err := sess.Begin(); err != nil { + return err + } + expiredTime := time.Now().AddDate(0, 0, 90).Unix() + if _, err := sess.Exec(`UPDATE action_artifact SET expired_unix=? WHERE status='2' AND expired_unix is NULL`, expiredTime); err != nil { + return err + } + + return sess.Commit() +} diff --git a/modules/setting/actions.go b/modules/setting/actions.go index a13330dcd1..bfc502c0cb 100644 --- a/modules/setting/actions.go +++ b/modules/setting/actions.go @@ -13,10 +13,11 @@ import ( // Actions settings var ( Actions = struct { - LogStorage *Storage // how the created logs should be stored - ArtifactStorage *Storage // how the created artifacts should be stored - Enabled bool - DefaultActionsURL defaultActionsURL `ini:"DEFAULT_ACTIONS_URL"` + LogStorage *Storage // how the created logs should be stored + ArtifactStorage *Storage // how the created artifacts should be stored + ArtifactRetentionDays int64 `ini:"ARTIFACT_RETENTION_DAYS"` + Enabled bool + DefaultActionsURL defaultActionsURL `ini:"DEFAULT_ACTIONS_URL"` }{ Enabled: false, DefaultActionsURL: defaultActionsURLGitHub, @@ -76,5 +77,10 @@ func loadActionsFrom(rootCfg ConfigProvider) error { Actions.ArtifactStorage, err = getStorage(rootCfg, "actions_artifacts", "", actionsSec) + // default to 90 days in Github Actions + if Actions.ArtifactRetentionDays <= 0 { + Actions.ArtifactRetentionDays = 90 + } + return err } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index bdb9b0c9dd..4f5f0383e9 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -2731,6 +2731,7 @@ dashboard.reinit_missing_repos = Reinitialize all missing Git repositories for w dashboard.sync_external_users = Synchronize external user data dashboard.cleanup_hook_task_table = Cleanup hook_task table dashboard.cleanup_packages = Cleanup expired packages +dashboard.cleanup_actions = Cleanup actions expired logs and artifacts dashboard.server_uptime = Server Uptime dashboard.current_goroutine = Current Goroutines dashboard.current_memory_usage = Current Memory Usage diff --git a/routers/api/actions/artifacts.go b/routers/api/actions/artifacts.go index 946ea11e75..c45dc667af 100644 --- a/routers/api/actions/artifacts.go +++ b/routers/api/actions/artifacts.go @@ -170,8 +170,9 @@ func (ar artifactRoutes) buildArtifactURL(runID int64, artifactHash, suffix stri } type getUploadArtifactRequest struct { - Type string - Name string + Type string + Name string + RetentionDays int64 } type getUploadArtifactResponse struct { @@ -192,10 +193,16 @@ func (ar artifactRoutes) getUploadArtifactURL(ctx *ArtifactContext) { return } + // set retention days + retentionQuery := "" + if req.RetentionDays > 0 { + retentionQuery = fmt.Sprintf("?retentionDays=%d", req.RetentionDays) + } + // use md5(artifact_name) to create upload url artifactHash := fmt.Sprintf("%x", md5.Sum([]byte(req.Name))) resp := getUploadArtifactResponse{ - FileContainerResourceURL: ar.buildArtifactURL(runID, artifactHash, "upload"), + FileContainerResourceURL: ar.buildArtifactURL(runID, artifactHash, "upload"+retentionQuery), } log.Debug("[artifact] get upload url: %s", resp.FileContainerResourceURL) ctx.JSON(http.StatusOK, resp) @@ -219,8 +226,21 @@ func (ar artifactRoutes) uploadArtifact(ctx *ArtifactContext) { return } + // get artifact retention days + expiredDays := setting.Actions.ArtifactRetentionDays + if queryRetentionDays := ctx.Req.URL.Query().Get("retentionDays"); queryRetentionDays != "" { + expiredDays, err = strconv.ParseInt(queryRetentionDays, 10, 64) + if err != nil { + log.Error("Error parse retention days: %v", err) + ctx.Error(http.StatusBadRequest, "Error parse retention days") + return + } + } + log.Debug("[artifact] upload chunk, name: %s, path: %s, size: %d, retention days: %d", + artifactName, artifactPath, fileRealTotalSize, expiredDays) + // create or get artifact with name and path - artifact, err := actions.CreateArtifact(ctx, task, artifactName, artifactPath) + artifact, err := actions.CreateArtifact(ctx, task, artifactName, artifactPath, expiredDays) if err != nil { log.Error("Error create or get artifact: %v", err) ctx.Error(http.StatusInternalServerError, "Error create or get artifact") diff --git a/routers/api/actions/artifacts_chunks.go b/routers/api/actions/artifacts_chunks.go index 30d31b4d75..458d671cff 100644 --- a/routers/api/actions/artifacts_chunks.go +++ b/routers/api/actions/artifacts_chunks.go @@ -179,7 +179,7 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st // save storage path to artifact log.Debug("[artifact] merge chunks to artifact: %d, %s", artifact.ID, storagePath) artifact.StoragePath = storagePath - artifact.Status = actions.ArtifactStatusUploadConfirmed + artifact.Status = int64(actions.ArtifactStatusUploadConfirmed) if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { return fmt.Errorf("update artifact error: %v", err) } diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index e4ca6a7198..a9c2858303 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -486,8 +486,9 @@ type ArtifactsViewResponse struct { } type ArtifactsViewItem struct { - Name string `json:"name"` - Size int64 `json:"size"` + Name string `json:"name"` + Size int64 `json:"size"` + Status string `json:"status"` } func ArtifactsView(ctx *context_module.Context) { @@ -510,9 +511,14 @@ func ArtifactsView(ctx *context_module.Context) { Artifacts: make([]*ArtifactsViewItem, 0, len(artifacts)), } for _, art := range artifacts { + status := "completed" + if art.Status == int64(actions_model.ArtifactStatusExpired) { + status = "expired" + } artifactsResponse.Artifacts = append(artifactsResponse.Artifacts, &ArtifactsViewItem{ - Name: art.ArtifactName, - Size: art.FileSize, + Name: art.ArtifactName, + Size: art.FileSize, + Status: status, }) } ctx.JSON(http.StatusOK, artifactsResponse) diff --git a/services/actions/cleanup.go b/services/actions/cleanup.go new file mode 100644 index 0000000000..785eeb5838 --- /dev/null +++ b/services/actions/cleanup.go @@ -0,0 +1,42 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + "time" + + "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/storage" +) + +// Cleanup removes expired actions logs, data and artifacts +func Cleanup(taskCtx context.Context, olderThan time.Duration) error { + // TODO: clean up expired actions logs + + // clean up expired artifacts + return CleanupArtifacts(taskCtx) +} + +// CleanupArtifacts removes expired artifacts and set records expired status +func CleanupArtifacts(taskCtx context.Context) error { + artifacts, err := actions.ListNeedExpiredArtifacts(taskCtx) + if err != nil { + return err + } + log.Info("Found %d expired artifacts", len(artifacts)) + for _, artifact := range artifacts { + if err := storage.ActionsArtifacts.Delete(artifact.StoragePath); err != nil { + log.Error("Cannot delete artifact %d: %v", artifact.ID, err) + continue + } + if err := actions.SetArtifactExpired(taskCtx, artifact.ID); err != nil { + log.Error("Cannot set artifact %d expired: %v", artifact.ID, err) + continue + } + log.Info("Artifact %d set expired", artifact.ID) + } + return nil +} diff --git a/services/cron/tasks_basic.go b/services/cron/tasks_basic.go index 2a213ae515..3869382d22 100644 --- a/services/cron/tasks_basic.go +++ b/services/cron/tasks_basic.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/services/auth" "code.gitea.io/gitea/services/migrations" mirror_service "code.gitea.io/gitea/services/mirror" @@ -156,6 +157,20 @@ func registerCleanupPackages() { }) } +func registerActionsCleanup() { + RegisterTaskFatal("cleanup_actions", &OlderThanConfig{ + BaseConfig: BaseConfig{ + Enabled: true, + RunAtStart: true, + Schedule: "@midnight", + }, + OlderThan: 24 * time.Hour, + }, func(ctx context.Context, _ *user_model.User, config Config) error { + realConfig := config.(*OlderThanConfig) + return actions.Cleanup(ctx, realConfig.OlderThan) + }) +} + func initBasicTasks() { if setting.Mirror.Enabled { registerUpdateMirrorTask() @@ -172,4 +187,7 @@ func initBasicTasks() { if setting.Packages.Enabled { registerCleanupPackages() } + if setting.Actions.Enabled { + registerActionsCleanup() + } } diff --git a/tests/integration/api_actions_artifact_test.go b/tests/integration/api_actions_artifact_test.go index 6590ca667c..101bedde02 100644 --- a/tests/integration/api_actions_artifact_test.go +++ b/tests/integration/api_actions_artifact_test.go @@ -18,8 +18,9 @@ type uploadArtifactResponse struct { } type getUploadArtifactRequest struct { - Type string - Name string + Type string + Name string + RetentionDays int64 } func TestActionsArtifactUploadSingleFile(t *testing.T) { @@ -252,3 +253,40 @@ func TestActionsArtifactDownloadMultiFiles(t *testing.T) { assert.Equal(t, resp.Body.String(), body) } } + +func TestActionsArtifactUploadWithRetentionDays(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + // acquire artifact upload url + req := NewRequestWithJSON(t, "POST", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts", getUploadArtifactRequest{ + Type: "actions_storage", + Name: "artifact-retention-days", + RetentionDays: 9, + }) + req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a") + resp := MakeRequest(t, req, http.StatusOK) + var uploadResp uploadArtifactResponse + DecodeJSON(t, resp, &uploadResp) + assert.Contains(t, uploadResp.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts") + assert.Contains(t, uploadResp.FileContainerResourceURL, "?retentionDays=9") + + // get upload url + idx := strings.Index(uploadResp.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/") + url := uploadResp.FileContainerResourceURL[idx:] + "&itemPath=artifact-retention-days/abc.txt" + + // upload artifact chunk + body := strings.Repeat("A", 1024) + req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body)) + req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a") + req.Header.Add("Content-Range", "bytes 0-1023/1024") + req.Header.Add("x-tfs-filelength", "1024") + req.Header.Add("x-actions-results-md5", "1HsSe8LeLWh93ILaw1TEFQ==") // base64(md5(body)) + MakeRequest(t, req, http.StatusOK) + + t.Logf("Create artifact confirm") + + // confirm artifact upload + req = NewRequest(t, "PATCH", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts?artifactName=artifact-retention-days") + req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a") + MakeRequest(t, req, http.StatusOK) +} From 958d148043a6ace08776b1fd5ec0a5859144dea0 Mon Sep 17 00:00:00 2001 From: JakobDev Date: Wed, 6 Sep 2023 10:49:36 +0200 Subject: [PATCH 06/52] Show always repo count in header (#26842) A few pages don't load the repo count of an user/org, so it is not shown in the header. This happens mostly on org pages, but the package settings applies to the user page as well. Before: ![Screenshot 2023-08-31 at 12-45-36 Gitea Git with a cup of tea](https://github.com/go-gitea/gitea/assets/15185051/14a59998-2cf9-4771-82f4-5d1d6fcb31f4) After: ![grafik](https://github.com/go-gitea/gitea/assets/15185051/ff055aa0-7cde-49be-9522-437bf970be1d) Seen on #26826 Regression of #25928 --- routers/web/org/members.go | 7 ++++++ routers/web/org/setting.go | 28 +++++++++++++++++++++++ routers/web/org/setting_oauth2.go | 7 ++++++ routers/web/org/setting_packages.go | 25 ++++++++++++++++++++ routers/web/org/teams.go | 7 ++++++ routers/web/user/package.go | 6 +++++ routers/web/user/setting/oauth2_common.go | 10 ++++++++ 7 files changed, 90 insertions(+) diff --git a/routers/web/org/members.go b/routers/web/org/members.go index 3c073211ae..f963ad55ef 100644 --- a/routers/web/org/members.go +++ b/routers/web/org/members.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + shared_user "code.gitea.io/gitea/routers/web/shared/user" ) const ( @@ -52,6 +53,12 @@ func Members(ctx *context.Context) { return } + err = shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + pager := context.NewPagination(int(total), setting.UI.MembersPagingNum, page, 5) opts.ListOptions.Page = page opts.ListOptions.PageSize = setting.UI.MembersPagingNum diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go index 51d5282fa0..0f082a70df 100644 --- a/routers/web/org/setting.go +++ b/routers/web/org/setting.go @@ -20,6 +20,7 @@ import ( repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web" + shared_user "code.gitea.io/gitea/routers/web/shared/user" user_setting "code.gitea.io/gitea/routers/web/user/setting" "code.gitea.io/gitea/services/forms" org_service "code.gitea.io/gitea/services/org" @@ -45,6 +46,14 @@ func Settings(ctx *context.Context) { ctx.Data["PageIsSettingsOptions"] = true ctx.Data["CurrentVisibility"] = ctx.Org.Organization.Visibility ctx.Data["RepoAdminChangeTeamAccess"] = ctx.Org.Organization.RepoAdminChangeTeamAccess + ctx.Data["ContextUser"] = ctx.ContextUser + + err := shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + ctx.HTML(http.StatusOK, tplSettingsOptions) } @@ -188,6 +197,12 @@ func SettingsDelete(ctx *context.Context) { return } + err := shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + ctx.HTML(http.StatusOK, tplSettingsDelete) } @@ -206,6 +221,12 @@ func Webhooks(ctx *context.Context) { return } + err = shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + ctx.Data["Webhooks"] = ws ctx.HTML(http.StatusOK, tplSettingsHooks) } @@ -227,5 +248,12 @@ func Labels(ctx *context.Context) { ctx.Data["PageIsOrgSettings"] = true ctx.Data["PageIsOrgSettingsLabels"] = true ctx.Data["LabelTemplateFiles"] = repo_module.LabelTemplateFiles + + err := shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + ctx.HTML(http.StatusOK, tplSettingsLabels) } diff --git a/routers/web/org/setting_oauth2.go b/routers/web/org/setting_oauth2.go index 9bf4280b07..0045bce4c9 100644 --- a/routers/web/org/setting_oauth2.go +++ b/routers/web/org/setting_oauth2.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/setting" + shared_user "code.gitea.io/gitea/routers/web/shared/user" user_setting "code.gitea.io/gitea/routers/web/user/setting" ) @@ -41,6 +42,12 @@ func Applications(ctx *context.Context) { } ctx.Data["Applications"] = apps + err = shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + ctx.HTML(http.StatusOK, tplSettingsApplications) } diff --git a/routers/web/org/setting_packages.go b/routers/web/org/setting_packages.go index 21d25bd90a..796829d34e 100644 --- a/routers/web/org/setting_packages.go +++ b/routers/web/org/setting_packages.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/setting" shared "code.gitea.io/gitea/routers/web/shared/packages" + shared_user "code.gitea.io/gitea/routers/web/shared/user" ) const ( @@ -24,6 +25,12 @@ func Packages(ctx *context.Context) { ctx.Data["PageIsOrgSettings"] = true ctx.Data["PageIsSettingsPackages"] = true + err := shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + shared.SetPackagesContext(ctx, ctx.ContextUser) ctx.HTML(http.StatusOK, tplSettingsPackages) @@ -34,6 +41,12 @@ func PackagesRuleAdd(ctx *context.Context) { ctx.Data["PageIsOrgSettings"] = true ctx.Data["PageIsSettingsPackages"] = true + err := shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + shared.SetRuleAddContext(ctx) ctx.HTML(http.StatusOK, tplSettingsPackagesRuleEdit) @@ -44,6 +57,12 @@ func PackagesRuleEdit(ctx *context.Context) { ctx.Data["PageIsOrgSettings"] = true ctx.Data["PageIsSettingsPackages"] = true + err := shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + shared.SetRuleEditContext(ctx, ctx.ContextUser) ctx.HTML(http.StatusOK, tplSettingsPackagesRuleEdit) @@ -80,6 +99,12 @@ func PackagesRulePreview(ctx *context.Context) { ctx.Data["PageIsOrgSettings"] = true ctx.Data["PageIsSettingsPackages"] = true + err := shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + shared.SetRulePreviewContext(ctx, ctx.ContextUser) ctx.HTML(http.StatusOK, tplSettingsPackagesRulePreview) diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go index fecb0cd5e9..1e0287fe27 100644 --- a/routers/web/org/teams.go +++ b/routers/web/org/teams.go @@ -25,6 +25,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/utils" + shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/convert" "code.gitea.io/gitea/services/forms" org_service "code.gitea.io/gitea/services/org" @@ -57,6 +58,12 @@ func Teams(ctx *context.Context) { } ctx.Data["Teams"] = ctx.Org.Teams + err := shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + ctx.HTML(http.StatusOK, tplTeams) } diff --git a/routers/web/user/package.go b/routers/web/user/package.go index d44638d48b..57770b2b1a 100644 --- a/routers/web/user/package.go +++ b/routers/web/user/package.go @@ -390,6 +390,12 @@ func PackageSettings(ctx *context.Context) { ctx.Data["Repos"] = repos ctx.Data["CanWritePackages"] = ctx.Package.AccessMode >= perm.AccessModeWrite || ctx.IsUserSiteAdmin() + err := shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + ctx.HTML(http.StatusOK, tplPackagesSettings) } diff --git a/routers/web/user/setting/oauth2_common.go b/routers/web/user/setting/oauth2_common.go index 641cc1fd9f..5786118f50 100644 --- a/routers/web/user/setting/oauth2_common.go +++ b/routers/web/user/setting/oauth2_common.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" + shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/forms" ) @@ -25,6 +26,15 @@ type OAuth2CommonHandlers struct { func (oa *OAuth2CommonHandlers) renderEditPage(ctx *context.Context) { app := ctx.Data["App"].(*auth.OAuth2Application) ctx.Data["FormActionPath"] = fmt.Sprintf("%s/%d", oa.BasePathEditPrefix, app.ID) + + if ctx.ContextUser.IsOrganization() { + err := shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + } + ctx.HTML(http.StatusOK, oa.TplAppEdit) } From 7812ce86dcd5aa31363915c00d3d9c934b9a7191 Mon Sep 17 00:00:00 2001 From: JakobDev Date: Wed, 6 Sep 2023 11:34:26 +0200 Subject: [PATCH 07/52] Show always repo count in header (#26842) A few pages don't load the repo count of an user/org, so it is not shown in the header. This happens mostly on org pages, but the package settings applies to the user page as well. Before: ![Screenshot 2023-08-31 at 12-45-36 Gitea Git with a cup of tea](https://github.com/go-gitea/gitea/assets/15185051/14a59998-2cf9-4771-82f4-5d1d6fcb31f4) After: ![grafik](https://github.com/go-gitea/gitea/assets/15185051/ff055aa0-7cde-49be-9522-437bf970be1d) Seen on #26826 Regression of #25928 From 9b0743ae33e8342e00a7ad3a6ba8af81b4c7019e Mon Sep 17 00:00:00 2001 From: Kerwin Bryant Date: Wed, 6 Sep 2023 18:11:06 +0800 Subject: [PATCH 08/52] Extract common code to new template (#26933) Same as #26903 --- templates/admin/hook_new.tmpl | 38 ++++++------------------ templates/org/settings/hook_new.tmpl | 4 ++- templates/repo/settings/webhook/new.tmpl | 4 ++- templates/user/settings/hook_new.tmpl | 4 ++- templates/webhook/new.tmpl | 28 ++++++++--------- 5 files changed, 32 insertions(+), 46 deletions(-) diff --git a/templates/admin/hook_new.tmpl b/templates/admin/hook_new.tmpl index e72e7bba62..f565318b8b 100644 --- a/templates/admin/hook_new.tmpl +++ b/templates/admin/hook_new.tmpl @@ -1,33 +1,13 @@ {{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin settings new webhook")}}
-

- {{if .PageIsAdminDefaultHooksNew}} - {{.locale.Tr "admin.defaulthooks.add_webhook"}} - {{else if .PageIsAdminSystemHooksNew}} - {{.locale.Tr "admin.systemhooks.add_webhook"}} - {{else if .Webhook.IsSystemWebhook}} - {{.locale.Tr "admin.systemhooks.update_webhook"}} - {{else}} - {{.locale.Tr "admin.defaulthooks.update_webhook"}} - {{end}} -
- {{template "shared/webhook/icon" .}} -
-

-
- {{template "repo/settings/webhook/gitea" .}} - {{template "repo/settings/webhook/gogs" .}} - {{template "repo/settings/webhook/slack" .}} - {{template "repo/settings/webhook/discord" .}} - {{template "repo/settings/webhook/dingtalk" .}} - {{template "repo/settings/webhook/telegram" .}} - {{template "repo/settings/webhook/msteams" .}} - {{template "repo/settings/webhook/feishu" .}} - {{template "repo/settings/webhook/matrix" .}} - {{template "repo/settings/webhook/wechatwork" .}} - {{template "repo/settings/webhook/packagist" .}} -
- - {{template "repo/settings/webhook/history" .}} + {{$CustomHeaderTitle := .locale.Tr "admin.defaulthooks.update_webhook"}} + {{if .PageIsAdminDefaultHooksNew}} + {{$CustomHeaderTitle = .locale.Tr "admin.defaulthooks.add_webhook"}} + {{else if .PageIsAdminSystemHooksNew}} + {{$CustomHeaderTitle = .locale.Tr "admin.systemhooks.add_webhook"}} + {{else if .Webhook.IsSystemWebhook}} + {{$CustomHeaderTitle = .locale.Tr "admin.systemhooks.update_webhook"}} + {{end}} + {{template "webhook/new" (dict "ctxData" . "CustomHeaderTitle" $CustomHeaderTitle)}}
{{template "admin/layout_footer" .}} diff --git a/templates/org/settings/hook_new.tmpl b/templates/org/settings/hook_new.tmpl index d4343f8c68..ea477c9975 100644 --- a/templates/org/settings/hook_new.tmpl +++ b/templates/org/settings/hook_new.tmpl @@ -1,5 +1,7 @@ {{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings new webhook")}}
- {{template "webhook/new" .}} + {{$CustomHeaderTitle := .locale.Tr "repo.settings.update_webhook"}} + {{if .PageIsSettingsHooksNew}}{{$CustomHeaderTitle = .locale.Tr "repo.settings.add_webhook"}}{{end}} + {{template "webhook/new" (dict "ctxData" . "CustomHeaderTitle" $CustomHeaderTitle)}}
{{template "org/settings/layout_footer" .}} diff --git a/templates/repo/settings/webhook/new.tmpl b/templates/repo/settings/webhook/new.tmpl index 79fd4bd2ff..67a5f7296f 100644 --- a/templates/repo/settings/webhook/new.tmpl +++ b/templates/repo/settings/webhook/new.tmpl @@ -1,5 +1,7 @@ {{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings new webhook")}}
- {{template "webhook/new" .}} + {{$CustomHeaderTitle := .locale.Tr "repo.settings.update_webhook"}} + {{if .PageIsSettingsHooksNew}}{{$CustomHeaderTitle = .locale.Tr "repo.settings.add_webhook"}}{{end}} + {{template "webhook/new" (dict "ctxData" . "CustomHeaderTitle" $CustomHeaderTitle)}}
{{template "repo/settings/layout_footer" .}} diff --git a/templates/user/settings/hook_new.tmpl b/templates/user/settings/hook_new.tmpl index 4d3ddf0383..9a857db85c 100644 --- a/templates/user/settings/hook_new.tmpl +++ b/templates/user/settings/hook_new.tmpl @@ -1,5 +1,7 @@ {{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings new webhook")}}
- {{template "webhook/new" .}} + {{$CustomHeaderTitle := .locale.Tr "repo.settings.update_webhook"}} + {{if .PageIsSettingsHooksNew}}{{$CustomHeaderTitle = .locale.Tr "repo.settings.add_webhook"}}{{end}} + {{template "webhook/new" (dict "ctxData" . "CustomHeaderTitle" $CustomHeaderTitle)}}
{{template "user/settings/layout_footer" .}} diff --git a/templates/webhook/new.tmpl b/templates/webhook/new.tmpl index b5878fc930..a185c42b51 100644 --- a/templates/webhook/new.tmpl +++ b/templates/webhook/new.tmpl @@ -1,20 +1,20 @@

- {{if .PageIsSettingsHooksNew}}{{.locale.Tr "repo.settings.add_webhook"}}{{else}}{{.locale.Tr "repo.settings.update_webhook"}}{{end}} + {{.CustomHeaderTitle}}
- {{template "shared/webhook/icon" .}} + {{template "shared/webhook/icon" .ctxData}}

- {{template "repo/settings/webhook/gitea" .}} - {{template "repo/settings/webhook/gogs" .}} - {{template "repo/settings/webhook/slack" .}} - {{template "repo/settings/webhook/discord" .}} - {{template "repo/settings/webhook/dingtalk" .}} - {{template "repo/settings/webhook/telegram" .}} - {{template "repo/settings/webhook/msteams" .}} - {{template "repo/settings/webhook/feishu" .}} - {{template "repo/settings/webhook/matrix" .}} - {{template "repo/settings/webhook/wechatwork" .}} - {{template "repo/settings/webhook/packagist" .}} + {{template "repo/settings/webhook/gitea" .ctxData}} + {{template "repo/settings/webhook/gogs" .ctxData}} + {{template "repo/settings/webhook/slack" .ctxData}} + {{template "repo/settings/webhook/discord" .ctxData}} + {{template "repo/settings/webhook/dingtalk" .ctxData}} + {{template "repo/settings/webhook/telegram" .ctxData}} + {{template "repo/settings/webhook/msteams" .ctxData}} + {{template "repo/settings/webhook/feishu" .ctxData}} + {{template "repo/settings/webhook/matrix" .ctxData}} + {{template "repo/settings/webhook/wechatwork" .ctxData}} + {{template "repo/settings/webhook/packagist" .ctxData}}
-{{template "repo/settings/webhook/history" .}} +{{template "repo/settings/webhook/history" .ctxData}} From a7d9a70552410d797cefc87b177b33ca4a1a60c4 Mon Sep 17 00:00:00 2001 From: merlleu Date: Wed, 6 Sep 2023 13:06:04 +0200 Subject: [PATCH 09/52] allow "latest" to be used in release vTag when downloading file (#26748) Hello, In the discord I saw [someone](https://discord.com/channels/322538954119184384/1069795723178160168/1145061200644800514) complaining that you can't use the "latest" keyword as release tag to download a specific file: In his example: https://www.uberwald.me/gitea/public/fvtt-ecryme/releases/latest/system.json However the latest keyword works for the release page, so I think it's a good thing to implement this on the release attachment download url too. --------- Co-authored-by: wxiaoguang --- routers/web/repo/repo.go | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index 4409381bc5..c9cefb68db 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -378,10 +378,6 @@ func RedirectDownload(ctx *context.Context) { curRepo := ctx.Repo.Repository releases, err := repo_model.GetReleasesByRepoIDAndNames(ctx, curRepo.ID, tagNames) if err != nil { - if repo_model.IsErrAttachmentNotExist(err) { - ctx.Error(http.StatusNotFound) - return - } ctx.ServerError("RedirectDownload", err) return } @@ -396,6 +392,23 @@ func RedirectDownload(ctx *context.Context) { ServeAttachment(ctx, att.UUID) return } + } else if len(releases) == 0 && vTag == "latest" { + // GitHub supports the alias "latest" for the latest release + // We only fetch the latest release if the tag is "latest" and no release with the tag "latest" exists + release, err := repo_model.GetLatestReleaseByRepoID(ctx.Repo.Repository.ID) + if err != nil { + ctx.Error(http.StatusNotFound) + return + } + att, err := repo_model.GetAttachmentByReleaseIDFileName(ctx, release.ID, fileName) + if err != nil { + ctx.Error(http.StatusNotFound) + return + } + if att != nil { + ServeAttachment(ctx, att.UUID) + return + } } ctx.Error(http.StatusNotFound) } From b3d88ada01c5bafe0581dc129fabe35a6faa3a85 Mon Sep 17 00:00:00 2001 From: KazzmanK Date: Wed, 6 Sep 2023 14:14:12 +0300 Subject: [PATCH 10/52] Add a documentation note for Windows Service (#26938) Service may fail to start at boot time with timeout Resolves #26934 Co-authored-by: Nikolay Kobzarev --- docs/content/installation/windows-service.en-us.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/content/installation/windows-service.en-us.md b/docs/content/installation/windows-service.en-us.md index 201681bc03..90332b7c69 100644 --- a/docs/content/installation/windows-service.en-us.md +++ b/docs/content/installation/windows-service.en-us.md @@ -51,6 +51,15 @@ Open "Windows Services", search for the service named "gitea", right-click it an "Run". If everything is OK, Gitea will be reachable on `http://localhost:3000` (or the port that was configured). +## Service startup type + +It was observed that on loaded systems during boot Gitea service may fail to start with timeout records in Windows Event Log. +In that case change startup type to `Automatic-Delayed`. This can be done during service creation, or by running config command + +``` +sc.exe config gitea start= delayed-auto +``` + ## Adding startup dependencies To add a startup dependency to the Gitea Windows service (eg Mysql, Mariadb), as an Administrator, then run the following command: From b9df9fa2e22d0bbf66a549183749b9dfaca6bd2f Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 6 Sep 2023 20:08:51 +0800 Subject: [PATCH 11/52] Move createrepository from module to service layer (#26927) Repository creation depends on many models, so moving it to service layer is better. --- modules/repository/create.go | 137 ---------- modules/repository/create_test.go | 135 ---------- modules/repository/generate.go | 4 +- modules/repository/hooks.go | 7 +- modules/repository/init.go | 162 +---------- modules/repository/license.go | 6 +- modules/repository/license_test.go | 22 +- modules/repository/repo.go | 4 +- routers/api/v1/admin/adopt.go | 3 +- routers/api/v1/repo/migrate.go | 4 +- routers/api/v1/repo/repo.go | 2 +- routers/web/admin/repos.go | 3 +- routers/web/repo/repo.go | 2 +- routers/web/user/setting/adopt.go | 3 +- services/migrations/gitea_uploader.go | 3 +- services/packages/cargo/index.go | 4 +- services/repository/adopt.go | 2 +- services/repository/create.go | 315 ++++++++++++++++++++++ services/repository/create_test.go | 148 ++++++++++ services/repository/repository.go | 6 +- services/task/task.go | 4 +- tests/integration/actions_trigger_test.go | 3 +- tests/integration/mirror_pull_test.go | 3 +- tests/integration/mirror_push_test.go | 4 +- tests/integration/pull_merge_test.go | 3 +- tests/integration/pull_update_test.go | 3 +- 26 files changed, 510 insertions(+), 482 deletions(-) create mode 100644 services/repository/create.go create mode 100644 services/repository/create_test.go diff --git a/modules/repository/create.go b/modules/repository/create.go index 10a1e872df..2dac35224e 100644 --- a/modules/repository/create.go +++ b/modules/repository/create.go @@ -22,7 +22,6 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/models/webhook" - "code.gitea.io/gitea/modules/git" issue_indexer "code.gitea.io/gitea/modules/indexer/issues" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -156,142 +155,6 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re return nil } -// CreateRepoOptions contains the create repository options -type CreateRepoOptions struct { - Name string - Description string - OriginalURL string - GitServiceType api.GitServiceType - Gitignores string - IssueLabels string - License string - Readme string - DefaultBranch string - IsPrivate bool - IsMirror bool - IsTemplate bool - AutoInit bool - Status repo_model.RepositoryStatus - TrustModel repo_model.TrustModelType - MirrorInterval string -} - -// CreateRepository creates a repository for the user/organization. -func CreateRepository(doer, u *user_model.User, opts CreateRepoOptions) (*repo_model.Repository, error) { - if !doer.IsAdmin && !u.CanCreateRepo() { - return nil, repo_model.ErrReachLimitOfRepo{ - Limit: u.MaxRepoCreation, - } - } - - if len(opts.DefaultBranch) == 0 { - opts.DefaultBranch = setting.Repository.DefaultBranch - } - - // Check if label template exist - if len(opts.IssueLabels) > 0 { - if _, err := LoadTemplateLabelsByDisplayName(opts.IssueLabels); err != nil { - return nil, err - } - } - - repo := &repo_model.Repository{ - OwnerID: u.ID, - Owner: u, - OwnerName: u.Name, - Name: opts.Name, - LowerName: strings.ToLower(opts.Name), - Description: opts.Description, - OriginalURL: opts.OriginalURL, - OriginalServiceType: opts.GitServiceType, - IsPrivate: opts.IsPrivate, - IsFsckEnabled: !opts.IsMirror, - IsTemplate: opts.IsTemplate, - CloseIssuesViaCommitInAnyBranch: setting.Repository.DefaultCloseIssuesViaCommitsInAnyBranch, - Status: opts.Status, - IsEmpty: !opts.AutoInit, - TrustModel: opts.TrustModel, - IsMirror: opts.IsMirror, - DefaultBranch: opts.DefaultBranch, - } - - var rollbackRepo *repo_model.Repository - - if err := db.WithTx(db.DefaultContext, func(ctx context.Context) error { - if err := CreateRepositoryByExample(ctx, doer, u, repo, false, false); err != nil { - return err - } - - // No need for init mirror. - if opts.IsMirror { - return nil - } - - repoPath := repo_model.RepoPath(u.Name, repo.Name) - isExist, err := util.IsExist(repoPath) - if err != nil { - log.Error("Unable to check if %s exists. Error: %v", repoPath, err) - return err - } - if isExist { - // repo already exists - We have two or three options. - // 1. We fail stating that the directory exists - // 2. We create the db repository to go with this data and adopt the git repo - // 3. We delete it and start afresh - // - // Previously Gitea would just delete and start afresh - this was naughty. - // So we will now fail and delegate to other functionality to adopt or delete - log.Error("Files already exist in %s and we are not going to adopt or delete.", repoPath) - return repo_model.ErrRepoFilesAlreadyExist{ - Uname: u.Name, - Name: repo.Name, - } - } - - if err = initRepository(ctx, repoPath, doer, repo, opts); err != nil { - if err2 := util.RemoveAll(repoPath); err2 != nil { - log.Error("initRepository: %v", err) - return fmt.Errorf( - "delete repo directory %s/%s failed(2): %v", u.Name, repo.Name, err2) - } - return fmt.Errorf("initRepository: %w", err) - } - - // Initialize Issue Labels if selected - if len(opts.IssueLabels) > 0 { - if err = InitializeLabels(ctx, repo.ID, opts.IssueLabels, false); err != nil { - rollbackRepo = repo - rollbackRepo.OwnerID = u.ID - return fmt.Errorf("InitializeLabels: %w", err) - } - } - - if err := CheckDaemonExportOK(ctx, repo); err != nil { - return fmt.Errorf("checkDaemonExportOK: %w", err) - } - - if stdout, _, err := git.NewCommand(ctx, "update-server-info"). - SetDescription(fmt.Sprintf("CreateRepository(git update-server-info): %s", repoPath)). - RunStdString(&git.RunOpts{Dir: repoPath}); err != nil { - log.Error("CreateRepository(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err) - rollbackRepo = repo - rollbackRepo.OwnerID = u.ID - return fmt.Errorf("CreateRepository(git update-server-info): %w", err) - } - return nil - }); err != nil { - if rollbackRepo != nil { - if errDelete := models.DeleteRepository(doer, rollbackRepo.OwnerID, rollbackRepo.ID); errDelete != nil { - log.Error("Rollback deleteRepository: %v", errDelete) - } - } - - return nil, err - } - - return repo, nil -} - const notRegularFileMode = os.ModeSymlink | os.ModeNamedPipe | os.ModeSocket | os.ModeDevice | os.ModeCharDevice | os.ModeIrregular // getDirectorySize returns the disk consumption for a given path diff --git a/modules/repository/create_test.go b/modules/repository/create_test.go index e620422bcb..6a2f4deaff 100644 --- a/modules/repository/create_test.go +++ b/modules/repository/create_test.go @@ -4,151 +4,16 @@ package repository import ( - "fmt" "testing" - "code.gitea.io/gitea/models" activities_model "code.gitea.io/gitea/models/activities" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/organization" - "code.gitea.io/gitea/models/perm" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" - user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/structs" "github.com/stretchr/testify/assert" ) -func TestIncludesAllRepositoriesTeams(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - testTeamRepositories := func(teamID int64, repoIds []int64) { - team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID}) - assert.NoError(t, team.LoadRepositories(db.DefaultContext), "%s: GetRepositories", team.Name) - assert.Len(t, team.Repos, team.NumRepos, "%s: len repo", team.Name) - assert.Len(t, team.Repos, len(repoIds), "%s: repo count", team.Name) - for i, rid := range repoIds { - if rid > 0 { - assert.True(t, models.HasRepository(team, rid), "%s: HasRepository(%d) %d", rid, i) - } - } - } - - // Get an admin user. - user, err := user_model.GetUserByID(db.DefaultContext, 1) - assert.NoError(t, err, "GetUserByID") - - // Create org. - org := &organization.Organization{ - Name: "All_repo", - IsActive: true, - Type: user_model.UserTypeOrganization, - Visibility: structs.VisibleTypePublic, - } - assert.NoError(t, organization.CreateOrganization(org, user), "CreateOrganization") - - // Check Owner team. - ownerTeam, err := org.GetOwnerTeam(db.DefaultContext) - assert.NoError(t, err, "GetOwnerTeam") - assert.True(t, ownerTeam.IncludesAllRepositories, "Owner team includes all repositories") - - // Create repos. - repoIds := make([]int64, 0) - for i := 0; i < 3; i++ { - r, err := CreateRepository(user, org.AsUser(), CreateRepoOptions{Name: fmt.Sprintf("repo-%d", i)}) - assert.NoError(t, err, "CreateRepository %d", i) - if r != nil { - repoIds = append(repoIds, r.ID) - } - } - // Get fresh copy of Owner team after creating repos. - ownerTeam, err = org.GetOwnerTeam(db.DefaultContext) - assert.NoError(t, err, "GetOwnerTeam") - - // Create teams and check repositories. - teams := []*organization.Team{ - ownerTeam, - { - OrgID: org.ID, - Name: "team one", - AccessMode: perm.AccessModeRead, - IncludesAllRepositories: true, - }, - { - OrgID: org.ID, - Name: "team 2", - AccessMode: perm.AccessModeRead, - IncludesAllRepositories: false, - }, - { - OrgID: org.ID, - Name: "team three", - AccessMode: perm.AccessModeWrite, - IncludesAllRepositories: true, - }, - { - OrgID: org.ID, - Name: "team 4", - AccessMode: perm.AccessModeWrite, - IncludesAllRepositories: false, - }, - } - teamRepos := [][]int64{ - repoIds, - repoIds, - {}, - repoIds, - {}, - } - for i, team := range teams { - if i > 0 { // first team is Owner. - assert.NoError(t, models.NewTeam(team), "%s: NewTeam", team.Name) - } - testTeamRepositories(team.ID, teamRepos[i]) - } - - // Update teams and check repositories. - teams[3].IncludesAllRepositories = false - teams[4].IncludesAllRepositories = true - teamRepos[4] = repoIds - for i, team := range teams { - assert.NoError(t, models.UpdateTeam(team, false, true), "%s: UpdateTeam", team.Name) - testTeamRepositories(team.ID, teamRepos[i]) - } - - // Create repo and check teams repositories. - r, err := CreateRepository(user, org.AsUser(), CreateRepoOptions{Name: "repo-last"}) - assert.NoError(t, err, "CreateRepository last") - if r != nil { - repoIds = append(repoIds, r.ID) - } - teamRepos[0] = repoIds - teamRepos[1] = repoIds - teamRepos[4] = repoIds - for i, team := range teams { - testTeamRepositories(team.ID, teamRepos[i]) - } - - // Remove repo and check teams repositories. - assert.NoError(t, models.DeleteRepository(user, org.ID, repoIds[0]), "DeleteRepository") - teamRepos[0] = repoIds[1:] - teamRepos[1] = repoIds[1:] - teamRepos[3] = repoIds[1:3] - teamRepos[4] = repoIds[1:] - for i, team := range teams { - testTeamRepositories(team.ID, teamRepos[i]) - } - - // Wipe created items. - for i, rid := range repoIds { - if i > 0 { // first repo already deleted. - assert.NoError(t, models.DeleteRepository(user, org.ID, rid), "DeleteRepository %d", i) - } - } - assert.NoError(t, organization.DeleteOrganization(db.DefaultContext, org), "DeleteOrganization") -} - func TestUpdateRepositoryVisibilityChanged(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) diff --git a/modules/repository/generate.go b/modules/repository/generate.go index 2e0b7600a5..4055029d22 100644 --- a/modules/repository/generate.go +++ b/modules/repository/generate.go @@ -241,7 +241,7 @@ func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *r defaultBranch = templateRepo.DefaultBranch } - return initRepoCommit(ctx, tmpDir, repo, repo.Owner, defaultBranch) + return InitRepoCommit(ctx, tmpDir, repo, repo.Owner, defaultBranch) } func generateGitContent(ctx context.Context, repo, templateRepo, generateRepo *repo_model.Repository) (err error) { @@ -356,7 +356,7 @@ func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templ } } - if err = checkInitRepository(ctx, owner.Name, generateRepo.Name); err != nil { + if err = CheckInitRepository(ctx, owner.Name, generateRepo.Name); err != nil { return generateRepo, err } diff --git a/modules/repository/hooks.go b/modules/repository/hooks.go index a95b9c2e99..daab7c3091 100644 --- a/modules/repository/hooks.go +++ b/modules/repository/hooks.go @@ -108,12 +108,7 @@ done } // CreateDelegateHooks creates all the hooks scripts for the repo -func CreateDelegateHooks(repoPath string) error { - return createDelegateHooks(repoPath) -} - -// createDelegateHooks creates all the hooks scripts for the repo -func createDelegateHooks(repoPath string) (err error) { +func CreateDelegateHooks(repoPath string) (err error) { hookNames, hookTpls, giteaHookTpls := getHookTemplates() hookDir := filepath.Join(repoPath, "hooks") diff --git a/modules/repository/init.go b/modules/repository/init.go index 84648f45eb..6f791f742b 100644 --- a/modules/repository/init.go +++ b/modules/repository/init.go @@ -4,7 +4,6 @@ package repository import ( - "bytes" "context" "fmt" "os" @@ -21,7 +20,6 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/options" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates/vars" "code.gitea.io/gitea/modules/util" asymkey_service "code.gitea.io/gitea/services/asymkey" ) @@ -126,95 +124,8 @@ func LoadRepoConfig() error { return nil } -func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir, repoPath string, opts CreateRepoOptions) error { - commitTimeStr := time.Now().Format(time.RFC3339) - authorSig := repo.Owner.NewGitSig() - - // Because this may call hooks we should pass in the environment - env := append(os.Environ(), - "GIT_AUTHOR_NAME="+authorSig.Name, - "GIT_AUTHOR_EMAIL="+authorSig.Email, - "GIT_AUTHOR_DATE="+commitTimeStr, - "GIT_COMMITTER_NAME="+authorSig.Name, - "GIT_COMMITTER_EMAIL="+authorSig.Email, - "GIT_COMMITTER_DATE="+commitTimeStr, - ) - - // Clone to temporary path and do the init commit. - if stdout, _, err := git.NewCommand(ctx, "clone").AddDynamicArguments(repoPath, tmpDir). - SetDescription(fmt.Sprintf("prepareRepoCommit (git clone): %s to %s", repoPath, tmpDir)). - RunStdString(&git.RunOpts{Dir: "", Env: env}); err != nil { - log.Error("Failed to clone from %v into %s: stdout: %s\nError: %v", repo, tmpDir, stdout, err) - return fmt.Errorf("git clone: %w", err) - } - - // README - data, err := options.Readme(opts.Readme) - if err != nil { - return fmt.Errorf("GetRepoInitFile[%s]: %w", opts.Readme, err) - } - - cloneLink := repo.CloneLink() - match := map[string]string{ - "Name": repo.Name, - "Description": repo.Description, - "CloneURL.SSH": cloneLink.SSH, - "CloneURL.HTTPS": cloneLink.HTTPS, - "OwnerName": repo.OwnerName, - } - res, err := vars.Expand(string(data), match) - if err != nil { - // here we could just log the error and continue the rendering - log.Error("unable to expand template vars for repo README: %s, err: %v", opts.Readme, err) - } - if err = os.WriteFile(filepath.Join(tmpDir, "README.md"), - []byte(res), 0o644); err != nil { - return fmt.Errorf("write README.md: %w", err) - } - - // .gitignore - if len(opts.Gitignores) > 0 { - var buf bytes.Buffer - names := strings.Split(opts.Gitignores, ",") - for _, name := range names { - data, err = options.Gitignore(name) - if err != nil { - return fmt.Errorf("GetRepoInitFile[%s]: %w", name, err) - } - buf.WriteString("# ---> " + name + "\n") - buf.Write(data) - buf.WriteString("\n") - } - - if buf.Len() > 0 { - if err = os.WriteFile(filepath.Join(tmpDir, ".gitignore"), buf.Bytes(), 0o644); err != nil { - return fmt.Errorf("write .gitignore: %w", err) - } - } - } - - // LICENSE - if len(opts.License) > 0 { - data, err = getLicense(opts.License, &licenseValues{ - Owner: repo.OwnerName, - Email: authorSig.Email, - Repo: repo.Name, - Year: time.Now().Format("2006"), - }) - if err != nil { - return fmt.Errorf("getLicense[%s]: %w", opts.License, err) - } - - if err = os.WriteFile(filepath.Join(tmpDir, "LICENSE"), data, 0o644); err != nil { - return fmt.Errorf("write LICENSE: %w", err) - } - } - - return nil -} - -// initRepoCommit temporarily changes with work directory. -func initRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Repository, u *user_model.User, defaultBranch string) (err error) { +// InitRepoCommit temporarily changes with work directory. +func InitRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Repository, u *user_model.User, defaultBranch string) (err error) { commitTimeStr := time.Now().Format(time.RFC3339) sig := u.NewGitSig() @@ -277,7 +188,7 @@ func initRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Reposi return nil } -func checkInitRepository(ctx context.Context, owner, name string) (err error) { +func CheckInitRepository(ctx context.Context, owner, name string) (err error) { // Somehow the directory could exist. repoPath := repo_model.RepoPath(owner, name) isExist, err := util.IsExist(repoPath) @@ -295,77 +206,12 @@ func checkInitRepository(ctx context.Context, owner, name string) (err error) { // Init git bare new repository. if err = git.InitRepository(ctx, repoPath, true); err != nil { return fmt.Errorf("git.InitRepository: %w", err) - } else if err = createDelegateHooks(repoPath); err != nil { + } else if err = CreateDelegateHooks(repoPath); err != nil { return fmt.Errorf("createDelegateHooks: %w", err) } return nil } -// InitRepository initializes README and .gitignore if needed. -func initRepository(ctx context.Context, repoPath string, u *user_model.User, repo *repo_model.Repository, opts CreateRepoOptions) (err error) { - if err = checkInitRepository(ctx, repo.OwnerName, repo.Name); err != nil { - return err - } - - // Initialize repository according to user's choice. - if opts.AutoInit { - tmpDir, err := os.MkdirTemp(os.TempDir(), "gitea-"+repo.Name) - if err != nil { - return fmt.Errorf("Failed to create temp dir for repository %s: %w", repo.RepoPath(), err) - } - defer func() { - if err := util.RemoveAll(tmpDir); err != nil { - log.Warn("Unable to remove temporary directory: %s: Error: %v", tmpDir, err) - } - }() - - if err = prepareRepoCommit(ctx, repo, tmpDir, repoPath, opts); err != nil { - return fmt.Errorf("prepareRepoCommit: %w", err) - } - - // Apply changes and commit. - if err = initRepoCommit(ctx, tmpDir, repo, u, opts.DefaultBranch); err != nil { - return fmt.Errorf("initRepoCommit: %w", err) - } - } - - // Re-fetch the repository from database before updating it (else it would - // override changes that were done earlier with sql) - if repo, err = repo_model.GetRepositoryByID(ctx, repo.ID); err != nil { - return fmt.Errorf("getRepositoryByID: %w", err) - } - - if !opts.AutoInit { - repo.IsEmpty = true - } - - repo.DefaultBranch = setting.Repository.DefaultBranch - - if len(opts.DefaultBranch) > 0 { - repo.DefaultBranch = opts.DefaultBranch - gitRepo, err := git.OpenRepository(ctx, repo.RepoPath()) - if err != nil { - return fmt.Errorf("openRepository: %w", err) - } - defer gitRepo.Close() - if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil { - return fmt.Errorf("setDefaultBranch: %w", err) - } - - if !repo.IsEmpty { - if _, err := SyncRepoBranches(ctx, repo.ID, u.ID); err != nil { - return fmt.Errorf("SyncRepoBranches: %w", err) - } - } - } - - if err = UpdateRepository(ctx, repo, false); err != nil { - return fmt.Errorf("updateRepository: %w", err) - } - - return nil -} - // InitializeLabels adds a label set to a repository using a template func InitializeLabels(ctx context.Context, id int64, labelTemplate string, isOrg bool) error { list, err := LoadTemplateLabelsByDisplayName(labelTemplate) diff --git a/modules/repository/license.go b/modules/repository/license.go index 5b188a041e..6ac3547e7b 100644 --- a/modules/repository/license.go +++ b/modules/repository/license.go @@ -13,14 +13,14 @@ import ( "code.gitea.io/gitea/modules/options" ) -type licenseValues struct { +type LicenseValues struct { Owner string Email string Repo string Year string } -func getLicense(name string, values *licenseValues) ([]byte, error) { +func GetLicense(name string, values *LicenseValues) ([]byte, error) { data, err := options.License(name) if err != nil { return nil, fmt.Errorf("GetRepoInitFile[%s]: %w", name, err) @@ -28,7 +28,7 @@ func getLicense(name string, values *licenseValues) ([]byte, error) { return fillLicensePlaceholder(name, values, data), nil } -func fillLicensePlaceholder(name string, values *licenseValues, origin []byte) []byte { +func fillLicensePlaceholder(name string, values *LicenseValues, origin []byte) []byte { placeholder := getLicensePlaceholder(name) scanner := bufio.NewScanner(bytes.NewReader(origin)) diff --git a/modules/repository/license_test.go b/modules/repository/license_test.go index 13c865693c..3b0cfa1eed 100644 --- a/modules/repository/license_test.go +++ b/modules/repository/license_test.go @@ -13,7 +13,7 @@ import ( func Test_getLicense(t *testing.T) { type args struct { name string - values *licenseValues + values *LicenseValues } tests := []struct { name string @@ -25,7 +25,7 @@ func Test_getLicense(t *testing.T) { name: "regular", args: args{ name: "MIT", - values: &licenseValues{Owner: "Gitea", Year: "2023"}, + values: &LicenseValues{Owner: "Gitea", Year: "2023"}, }, want: `MIT License @@ -49,11 +49,11 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := getLicense(tt.args.name, tt.args.values) - if !tt.wantErr(t, err, fmt.Sprintf("getLicense(%v, %v)", tt.args.name, tt.args.values)) { + got, err := GetLicense(tt.args.name, tt.args.values) + if !tt.wantErr(t, err, fmt.Sprintf("GetLicense(%v, %v)", tt.args.name, tt.args.values)) { return } - assert.Equalf(t, tt.want, string(got), "getLicense(%v, %v)", tt.args.name, tt.args.values) + assert.Equalf(t, tt.want, string(got), "GetLicense(%v, %v)", tt.args.name, tt.args.values) }) } } @@ -61,7 +61,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI func Test_fillLicensePlaceholder(t *testing.T) { type args struct { name string - values *licenseValues + values *LicenseValues origin string } tests := []struct { @@ -73,7 +73,7 @@ func Test_fillLicensePlaceholder(t *testing.T) { name: "owner", args: args{ name: "regular", - values: &licenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"}, + values: &LicenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"}, origin: ` @@ -104,7 +104,7 @@ Gitea name: "email", args: args{ name: "regular", - values: &licenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"}, + values: &LicenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"}, origin: ` [EMAIL] `, @@ -117,7 +117,7 @@ teabot@gitea.io name: "repo", args: args{ name: "regular", - values: &licenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"}, + values: &LicenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"}, origin: ` @@ -132,7 +132,7 @@ gitea name: "year", args: args{ name: "regular", - values: &licenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"}, + values: &LicenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"}, origin: ` [YEAR] @@ -155,7 +155,7 @@ gitea name: "0BSD", args: args{ name: "0BSD", - values: &licenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"}, + values: &LicenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"}, origin: ` Copyright (C) YEAR by AUTHOR EMAIL diff --git a/modules/repository/repo.go b/modules/repository/repo.go index 6a11315cc4..6bf88e7752 100644 --- a/modules/repository/repo.go +++ b/modules/repository/repo.go @@ -256,11 +256,11 @@ func cleanUpMigrateGitConfig(ctx context.Context, repoPath string) error { // CleanUpMigrateInfo finishes migrating repository and/or wiki with things that don't need to be done for mirrors. func CleanUpMigrateInfo(ctx context.Context, repo *repo_model.Repository) (*repo_model.Repository, error) { repoPath := repo.RepoPath() - if err := createDelegateHooks(repoPath); err != nil { + if err := CreateDelegateHooks(repoPath); err != nil { return repo, fmt.Errorf("createDelegateHooks: %w", err) } if repo.HasWiki() { - if err := createDelegateHooks(repo.WikiPath()); err != nil { + if err := CreateDelegateHooks(repo.WikiPath()); err != nil { return repo, fmt.Errorf("createDelegateHooks.(wiki): %w", err) } } diff --git a/routers/api/v1/admin/adopt.go b/routers/api/v1/admin/adopt.go index ccd8be9171..bf030eb222 100644 --- a/routers/api/v1/admin/adopt.go +++ b/routers/api/v1/admin/adopt.go @@ -9,7 +9,6 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/context" - repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/api/v1/utils" repo_service "code.gitea.io/gitea/services/repository" @@ -109,7 +108,7 @@ func AdoptRepository(ctx *context.APIContext) { ctx.NotFound() return } - if _, err := repo_service.AdoptRepository(ctx, ctx.Doer, ctxUser, repo_module.CreateRepoOptions{ + if _, err := repo_service.AdoptRepository(ctx, ctx.Doer, ctxUser, repo_service.CreateRepoOptions{ Name: repoName, IsPrivate: true, }); err != nil { diff --git a/routers/api/v1/repo/migrate.go b/routers/api/v1/repo/migrate.go index dfc9004620..41374831de 100644 --- a/routers/api/v1/repo/migrate.go +++ b/routers/api/v1/repo/migrate.go @@ -22,7 +22,6 @@ import ( "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/log" base "code.gitea.io/gitea/modules/migration" - repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" @@ -31,6 +30,7 @@ import ( "code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/migrations" notify_service "code.gitea.io/gitea/services/notify" + repo_service "code.gitea.io/gitea/services/repository" ) // Migrate migrate remote git repository to gitea @@ -170,7 +170,7 @@ func Migrate(ctx *context.APIContext) { opts.Releases = false } - repo, err := repo_module.CreateRepository(ctx.Doer, repoOwner, repo_module.CreateRepoOptions{ + repo, err := repo_service.CreateRepositoryDirectly(ctx.Doer, repoOwner, repo_service.CreateRepoOptions{ Name: opts.RepoName, Description: opts.Description, OriginalURL: form.CloneAddr, diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 7b0c954a73..29f6a675d4 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -240,7 +240,7 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre return } - repo, err := repo_service.CreateRepository(ctx, ctx.Doer, owner, repo_module.CreateRepoOptions{ + repo, err := repo_service.CreateRepository(ctx, ctx.Doer, owner, repo_service.CreateRepoOptions{ Name: opt.Name, Description: opt.Description, IssueLabels: opt.IssueLabels, diff --git a/routers/web/admin/repos.go b/routers/web/admin/repos.go index d1d0abca02..45c280ef73 100644 --- a/routers/web/admin/repos.go +++ b/routers/web/admin/repos.go @@ -14,7 +14,6 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" - repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/web/explore" @@ -144,7 +143,7 @@ func AdoptOrDeleteRepository(ctx *context.Context) { if has || !isDir { // Fallthrough to failure mode } else if action == "adopt" { - if _, err := repo_service.AdoptRepository(ctx, ctx.Doer, ctxUser, repo_module.CreateRepoOptions{ + if _, err := repo_service.AdoptRepository(ctx, ctx.Doer, ctxUser, repo_service.CreateRepoOptions{ Name: dirSplit[1], IsPrivate: true, }); err != nil { diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index c9cefb68db..12cd477926 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -275,7 +275,7 @@ func CreatePost(ctx *context.Context) { return } } else { - repo, err = repo_service.CreateRepository(ctx, ctx.Doer, ctxUser, repo_module.CreateRepoOptions{ + repo, err = repo_service.CreateRepository(ctx, ctx.Doer, ctxUser, repo_service.CreateRepoOptions{ Name: form.RepoName, Description: form.Description, Gitignores: form.Gitignores, diff --git a/routers/web/user/setting/adopt.go b/routers/web/user/setting/adopt.go index 01668c3954..decb35c1e1 100644 --- a/routers/web/user/setting/adopt.go +++ b/routers/web/user/setting/adopt.go @@ -9,7 +9,6 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/context" - repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" repo_service "code.gitea.io/gitea/services/repository" @@ -45,7 +44,7 @@ func AdoptOrDeleteRepository(ctx *context.Context) { if has || !isDir { // Fallthrough to failure mode } else if action == "adopt" && allowAdopt { - if _, err := repo_service.AdoptRepository(ctx, ctxUser, ctxUser, repo_module.CreateRepoOptions{ + if _, err := repo_service.AdoptRepository(ctx, ctxUser, ctxUser, repo_service.CreateRepoOptions{ Name: dir, IsPrivate: true, }); err != nil { diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go index ee7fc57851..a4a3af82e7 100644 --- a/services/migrations/gitea_uploader.go +++ b/services/migrations/gitea_uploader.go @@ -31,6 +31,7 @@ import ( "code.gitea.io/gitea/modules/uri" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/pull" + repo_service "code.gitea.io/gitea/services/repository" "github.com/google/uuid" ) @@ -99,7 +100,7 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate var r *repo_model.Repository if opts.MigrateToRepoID <= 0 { - r, err = repo_module.CreateRepository(g.doer, owner, repo_module.CreateRepoOptions{ + r, err = repo_service.CreateRepositoryDirectly(g.doer, owner, repo_service.CreateRepoOptions{ Name: g.repoName, Description: repo.Description, OriginalURL: repo.OriginalURL, diff --git a/services/packages/cargo/index.go b/services/packages/cargo/index.go index 867cd796d3..572f5e1f5b 100644 --- a/services/packages/cargo/index.go +++ b/services/packages/cargo/index.go @@ -19,10 +19,10 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/json" cargo_module "code.gitea.io/gitea/modules/packages/cargo" - repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" + repo_service "code.gitea.io/gitea/services/repository" files_service "code.gitea.io/gitea/services/repository/files" ) @@ -206,7 +206,7 @@ func getOrCreateIndexRepository(ctx context.Context, doer, owner *user_model.Use repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner.Name, IndexRepositoryName) if err != nil { if errors.Is(err, util.ErrNotExist) { - repo, err = repo_module.CreateRepository(doer, owner, repo_module.CreateRepoOptions{ + repo, err = repo_service.CreateRepositoryDirectly(doer, owner, repo_service.CreateRepoOptions{ Name: IndexRepositoryName, }) if err != nil { diff --git a/services/repository/adopt.go b/services/repository/adopt.go index f225538faf..00dce7295e 100644 --- a/services/repository/adopt.go +++ b/services/repository/adopt.go @@ -27,7 +27,7 @@ import ( ) // AdoptRepository adopts pre-existing repository files for the user/organization. -func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts repo_module.CreateRepoOptions) (*repo_model.Repository, error) { +func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts CreateRepoOptions) (*repo_model.Repository, error) { if !doer.IsAdmin && !u.CanCreateRepo() { return nil, repo_model.ErrReachLimitOfRepo{ Limit: u.MaxRepoCreation, diff --git a/services/repository/create.go b/services/repository/create.go new file mode 100644 index 0000000000..a5d521e353 --- /dev/null +++ b/services/repository/create.go @@ -0,0 +1,315 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repository + +import ( + "bytes" + "context" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/options" + repo_module "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/templates/vars" + "code.gitea.io/gitea/modules/util" +) + +// CreateRepoOptions contains the create repository options +type CreateRepoOptions struct { + Name string + Description string + OriginalURL string + GitServiceType api.GitServiceType + Gitignores string + IssueLabels string + License string + Readme string + DefaultBranch string + IsPrivate bool + IsMirror bool + IsTemplate bool + AutoInit bool + Status repo_model.RepositoryStatus + TrustModel repo_model.TrustModelType + MirrorInterval string +} + +func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir, repoPath string, opts CreateRepoOptions) error { + commitTimeStr := time.Now().Format(time.RFC3339) + authorSig := repo.Owner.NewGitSig() + + // Because this may call hooks we should pass in the environment + env := append(os.Environ(), + "GIT_AUTHOR_NAME="+authorSig.Name, + "GIT_AUTHOR_EMAIL="+authorSig.Email, + "GIT_AUTHOR_DATE="+commitTimeStr, + "GIT_COMMITTER_NAME="+authorSig.Name, + "GIT_COMMITTER_EMAIL="+authorSig.Email, + "GIT_COMMITTER_DATE="+commitTimeStr, + ) + + // Clone to temporary path and do the init commit. + if stdout, _, err := git.NewCommand(ctx, "clone").AddDynamicArguments(repoPath, tmpDir). + SetDescription(fmt.Sprintf("prepareRepoCommit (git clone): %s to %s", repoPath, tmpDir)). + RunStdString(&git.RunOpts{Dir: "", Env: env}); err != nil { + log.Error("Failed to clone from %v into %s: stdout: %s\nError: %v", repo, tmpDir, stdout, err) + return fmt.Errorf("git clone: %w", err) + } + + // README + data, err := options.Readme(opts.Readme) + if err != nil { + return fmt.Errorf("GetRepoInitFile[%s]: %w", opts.Readme, err) + } + + cloneLink := repo.CloneLink() + match := map[string]string{ + "Name": repo.Name, + "Description": repo.Description, + "CloneURL.SSH": cloneLink.SSH, + "CloneURL.HTTPS": cloneLink.HTTPS, + "OwnerName": repo.OwnerName, + } + res, err := vars.Expand(string(data), match) + if err != nil { + // here we could just log the error and continue the rendering + log.Error("unable to expand template vars for repo README: %s, err: %v", opts.Readme, err) + } + if err = os.WriteFile(filepath.Join(tmpDir, "README.md"), + []byte(res), 0o644); err != nil { + return fmt.Errorf("write README.md: %w", err) + } + + // .gitignore + if len(opts.Gitignores) > 0 { + var buf bytes.Buffer + names := strings.Split(opts.Gitignores, ",") + for _, name := range names { + data, err = options.Gitignore(name) + if err != nil { + return fmt.Errorf("GetRepoInitFile[%s]: %w", name, err) + } + buf.WriteString("# ---> " + name + "\n") + buf.Write(data) + buf.WriteString("\n") + } + + if buf.Len() > 0 { + if err = os.WriteFile(filepath.Join(tmpDir, ".gitignore"), buf.Bytes(), 0o644); err != nil { + return fmt.Errorf("write .gitignore: %w", err) + } + } + } + + // LICENSE + if len(opts.License) > 0 { + data, err = repo_module.GetLicense(opts.License, &repo_module.LicenseValues{ + Owner: repo.OwnerName, + Email: authorSig.Email, + Repo: repo.Name, + Year: time.Now().Format("2006"), + }) + if err != nil { + return fmt.Errorf("getLicense[%s]: %w", opts.License, err) + } + + if err = os.WriteFile(filepath.Join(tmpDir, "LICENSE"), data, 0o644); err != nil { + return fmt.Errorf("write LICENSE: %w", err) + } + } + + return nil +} + +// InitRepository initializes README and .gitignore if needed. +func initRepository(ctx context.Context, repoPath string, u *user_model.User, repo *repo_model.Repository, opts CreateRepoOptions) (err error) { + if err = repo_module.CheckInitRepository(ctx, repo.OwnerName, repo.Name); err != nil { + return err + } + + // Initialize repository according to user's choice. + if opts.AutoInit { + tmpDir, err := os.MkdirTemp(os.TempDir(), "gitea-"+repo.Name) + if err != nil { + return fmt.Errorf("Failed to create temp dir for repository %s: %w", repo.RepoPath(), err) + } + defer func() { + if err := util.RemoveAll(tmpDir); err != nil { + log.Warn("Unable to remove temporary directory: %s: Error: %v", tmpDir, err) + } + }() + + if err = prepareRepoCommit(ctx, repo, tmpDir, repoPath, opts); err != nil { + return fmt.Errorf("prepareRepoCommit: %w", err) + } + + // Apply changes and commit. + if err = repo_module.InitRepoCommit(ctx, tmpDir, repo, u, opts.DefaultBranch); err != nil { + return fmt.Errorf("initRepoCommit: %w", err) + } + } + + // Re-fetch the repository from database before updating it (else it would + // override changes that were done earlier with sql) + if repo, err = repo_model.GetRepositoryByID(ctx, repo.ID); err != nil { + return fmt.Errorf("getRepositoryByID: %w", err) + } + + if !opts.AutoInit { + repo.IsEmpty = true + } + + repo.DefaultBranch = setting.Repository.DefaultBranch + + if len(opts.DefaultBranch) > 0 { + repo.DefaultBranch = opts.DefaultBranch + gitRepo, err := git.OpenRepository(ctx, repo.RepoPath()) + if err != nil { + return fmt.Errorf("openRepository: %w", err) + } + defer gitRepo.Close() + if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil { + return fmt.Errorf("setDefaultBranch: %w", err) + } + + if !repo.IsEmpty { + if _, err := repo_module.SyncRepoBranches(ctx, repo.ID, u.ID); err != nil { + return fmt.Errorf("SyncRepoBranches: %w", err) + } + } + } + + if err = UpdateRepository(ctx, repo, false); err != nil { + return fmt.Errorf("updateRepository: %w", err) + } + + return nil +} + +// CreateRepositoryDirectly creates a repository for the user/organization. +func CreateRepositoryDirectly(doer, u *user_model.User, opts CreateRepoOptions) (*repo_model.Repository, error) { + if !doer.IsAdmin && !u.CanCreateRepo() { + return nil, repo_model.ErrReachLimitOfRepo{ + Limit: u.MaxRepoCreation, + } + } + + if len(opts.DefaultBranch) == 0 { + opts.DefaultBranch = setting.Repository.DefaultBranch + } + + // Check if label template exist + if len(opts.IssueLabels) > 0 { + if _, err := repo_module.LoadTemplateLabelsByDisplayName(opts.IssueLabels); err != nil { + return nil, err + } + } + + repo := &repo_model.Repository{ + OwnerID: u.ID, + Owner: u, + OwnerName: u.Name, + Name: opts.Name, + LowerName: strings.ToLower(opts.Name), + Description: opts.Description, + OriginalURL: opts.OriginalURL, + OriginalServiceType: opts.GitServiceType, + IsPrivate: opts.IsPrivate, + IsFsckEnabled: !opts.IsMirror, + IsTemplate: opts.IsTemplate, + CloseIssuesViaCommitInAnyBranch: setting.Repository.DefaultCloseIssuesViaCommitsInAnyBranch, + Status: opts.Status, + IsEmpty: !opts.AutoInit, + TrustModel: opts.TrustModel, + IsMirror: opts.IsMirror, + DefaultBranch: opts.DefaultBranch, + } + + var rollbackRepo *repo_model.Repository + + if err := db.WithTx(db.DefaultContext, func(ctx context.Context) error { + if err := repo_module.CreateRepositoryByExample(ctx, doer, u, repo, false, false); err != nil { + return err + } + + // No need for init mirror. + if opts.IsMirror { + return nil + } + + repoPath := repo_model.RepoPath(u.Name, repo.Name) + isExist, err := util.IsExist(repoPath) + if err != nil { + log.Error("Unable to check if %s exists. Error: %v", repoPath, err) + return err + } + if isExist { + // repo already exists - We have two or three options. + // 1. We fail stating that the directory exists + // 2. We create the db repository to go with this data and adopt the git repo + // 3. We delete it and start afresh + // + // Previously Gitea would just delete and start afresh - this was naughty. + // So we will now fail and delegate to other functionality to adopt or delete + log.Error("Files already exist in %s and we are not going to adopt or delete.", repoPath) + return repo_model.ErrRepoFilesAlreadyExist{ + Uname: u.Name, + Name: repo.Name, + } + } + + if err = initRepository(ctx, repoPath, doer, repo, opts); err != nil { + if err2 := util.RemoveAll(repoPath); err2 != nil { + log.Error("initRepository: %v", err) + return fmt.Errorf( + "delete repo directory %s/%s failed(2): %v", u.Name, repo.Name, err2) + } + return fmt.Errorf("initRepository: %w", err) + } + + // Initialize Issue Labels if selected + if len(opts.IssueLabels) > 0 { + if err = repo_module.InitializeLabels(ctx, repo.ID, opts.IssueLabels, false); err != nil { + rollbackRepo = repo + rollbackRepo.OwnerID = u.ID + return fmt.Errorf("InitializeLabels: %w", err) + } + } + + if err := repo_module.CheckDaemonExportOK(ctx, repo); err != nil { + return fmt.Errorf("checkDaemonExportOK: %w", err) + } + + if stdout, _, err := git.NewCommand(ctx, "update-server-info"). + SetDescription(fmt.Sprintf("CreateRepository(git update-server-info): %s", repoPath)). + RunStdString(&git.RunOpts{Dir: repoPath}); err != nil { + log.Error("CreateRepository(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err) + rollbackRepo = repo + rollbackRepo.OwnerID = u.ID + return fmt.Errorf("CreateRepository(git update-server-info): %w", err) + } + return nil + }); err != nil { + if rollbackRepo != nil { + if errDelete := models.DeleteRepository(doer, rollbackRepo.OwnerID, rollbackRepo.ID); errDelete != nil { + log.Error("Rollback deleteRepository: %v", errDelete) + } + } + + return nil, err + } + + return repo, nil +} diff --git a/services/repository/create_test.go b/services/repository/create_test.go new file mode 100644 index 0000000000..ec3d62ce07 --- /dev/null +++ b/services/repository/create_test.go @@ -0,0 +1,148 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repository + +import ( + "fmt" + "testing" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/organization" + "code.gitea.io/gitea/models/perm" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/structs" + + "github.com/stretchr/testify/assert" +) + +func TestIncludesAllRepositoriesTeams(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + testTeamRepositories := func(teamID int64, repoIds []int64) { + team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID}) + assert.NoError(t, team.LoadRepositories(db.DefaultContext), "%s: GetRepositories", team.Name) + assert.Len(t, team.Repos, team.NumRepos, "%s: len repo", team.Name) + assert.Len(t, team.Repos, len(repoIds), "%s: repo count", team.Name) + for i, rid := range repoIds { + if rid > 0 { + assert.True(t, models.HasRepository(team, rid), "%s: HasRepository(%d) %d", rid, i) + } + } + } + + // Get an admin user. + user, err := user_model.GetUserByID(db.DefaultContext, 1) + assert.NoError(t, err, "GetUserByID") + + // Create org. + org := &organization.Organization{ + Name: "All_repo", + IsActive: true, + Type: user_model.UserTypeOrganization, + Visibility: structs.VisibleTypePublic, + } + assert.NoError(t, organization.CreateOrganization(org, user), "CreateOrganization") + + // Check Owner team. + ownerTeam, err := org.GetOwnerTeam(db.DefaultContext) + assert.NoError(t, err, "GetOwnerTeam") + assert.True(t, ownerTeam.IncludesAllRepositories, "Owner team includes all repositories") + + // Create repos. + repoIds := make([]int64, 0) + for i := 0; i < 3; i++ { + r, err := CreateRepositoryDirectly(user, org.AsUser(), CreateRepoOptions{Name: fmt.Sprintf("repo-%d", i)}) + assert.NoError(t, err, "CreateRepository %d", i) + if r != nil { + repoIds = append(repoIds, r.ID) + } + } + // Get fresh copy of Owner team after creating repos. + ownerTeam, err = org.GetOwnerTeam(db.DefaultContext) + assert.NoError(t, err, "GetOwnerTeam") + + // Create teams and check repositories. + teams := []*organization.Team{ + ownerTeam, + { + OrgID: org.ID, + Name: "team one", + AccessMode: perm.AccessModeRead, + IncludesAllRepositories: true, + }, + { + OrgID: org.ID, + Name: "team 2", + AccessMode: perm.AccessModeRead, + IncludesAllRepositories: false, + }, + { + OrgID: org.ID, + Name: "team three", + AccessMode: perm.AccessModeWrite, + IncludesAllRepositories: true, + }, + { + OrgID: org.ID, + Name: "team 4", + AccessMode: perm.AccessModeWrite, + IncludesAllRepositories: false, + }, + } + teamRepos := [][]int64{ + repoIds, + repoIds, + {}, + repoIds, + {}, + } + for i, team := range teams { + if i > 0 { // first team is Owner. + assert.NoError(t, models.NewTeam(team), "%s: NewTeam", team.Name) + } + testTeamRepositories(team.ID, teamRepos[i]) + } + + // Update teams and check repositories. + teams[3].IncludesAllRepositories = false + teams[4].IncludesAllRepositories = true + teamRepos[4] = repoIds + for i, team := range teams { + assert.NoError(t, models.UpdateTeam(team, false, true), "%s: UpdateTeam", team.Name) + testTeamRepositories(team.ID, teamRepos[i]) + } + + // Create repo and check teams repositories. + r, err := CreateRepositoryDirectly(user, org.AsUser(), CreateRepoOptions{Name: "repo-last"}) + assert.NoError(t, err, "CreateRepository last") + if r != nil { + repoIds = append(repoIds, r.ID) + } + teamRepos[0] = repoIds + teamRepos[1] = repoIds + teamRepos[4] = repoIds + for i, team := range teams { + testTeamRepositories(team.ID, teamRepos[i]) + } + + // Remove repo and check teams repositories. + assert.NoError(t, models.DeleteRepository(user, org.ID, repoIds[0]), "DeleteRepository") + teamRepos[0] = repoIds[1:] + teamRepos[1] = repoIds[1:] + teamRepos[3] = repoIds[1:3] + teamRepos[4] = repoIds[1:] + for i, team := range teams { + testTeamRepositories(team.ID, teamRepos[i]) + } + + // Wipe created items. + for i, rid := range repoIds { + if i > 0 { // first repo already deleted. + assert.NoError(t, models.DeleteRepository(user, org.ID, rid), "DeleteRepository %d", i) + } + } + assert.NoError(t, organization.DeleteOrganization(db.DefaultContext, org), "DeleteOrganization") +} diff --git a/services/repository/repository.go b/services/repository/repository.go index 47e96bd5e5..db3035f8c0 100644 --- a/services/repository/repository.go +++ b/services/repository/repository.go @@ -40,8 +40,8 @@ type WebSearchResults struct { } // CreateRepository creates a repository for the user/organization. -func CreateRepository(ctx context.Context, doer, owner *user_model.User, opts repo_module.CreateRepoOptions) (*repo_model.Repository, error) { - repo, err := repo_module.CreateRepository(doer, owner, opts) +func CreateRepository(ctx context.Context, doer, owner *user_model.User, opts CreateRepoOptions) (*repo_model.Repository, error) { + repo, err := CreateRepositoryDirectly(doer, owner, opts) if err != nil { // No need to rollback here we should do this in CreateRepository... return nil, err @@ -84,7 +84,7 @@ func PushCreateRepo(ctx context.Context, authUser, owner *user_model.User, repoN } } - repo, err := CreateRepository(ctx, authUser, owner, repo_module.CreateRepoOptions{ + repo, err := CreateRepository(ctx, authUser, owner, CreateRepoOptions{ Name: repoName, IsPrivate: setting.Repository.DefaultPushCreatePrivate, }) diff --git a/services/task/task.go b/services/task/task.go index db5c1dd3f8..45bc7b990a 100644 --- a/services/task/task.go +++ b/services/task/task.go @@ -14,12 +14,12 @@ import ( "code.gitea.io/gitea/modules/log" base "code.gitea.io/gitea/modules/migration" "code.gitea.io/gitea/modules/queue" - repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/secret" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" + repo_service "code.gitea.io/gitea/services/repository" ) // taskQueue is a global queue of tasks @@ -100,7 +100,7 @@ func CreateMigrateTask(doer, u *user_model.User, opts base.MigrateOptions) (*adm return nil, err } - repo, err := repo_module.CreateRepository(doer, u, repo_module.CreateRepoOptions{ + repo, err := repo_service.CreateRepositoryDirectly(doer, u, repo_service.CreateRepoOptions{ Name: opts.RepoName, Description: opts.Description, OriginalURL: opts.OriginalURL, diff --git a/tests/integration/actions_trigger_test.go b/tests/integration/actions_trigger_test.go index 1c5d2fed61..56718397f4 100644 --- a/tests/integration/actions_trigger_test.go +++ b/tests/integration/actions_trigger_test.go @@ -18,7 +18,6 @@ import ( user_model "code.gitea.io/gitea/models/user" actions_module "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/git" - repo_module "code.gitea.io/gitea/modules/repository" pull_service "code.gitea.io/gitea/services/pull" repo_service "code.gitea.io/gitea/services/repository" files_service "code.gitea.io/gitea/services/repository/files" @@ -32,7 +31,7 @@ func TestPullRequestTargetEvent(t *testing.T) { user3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) // owner of the forked repo // create the base repo - baseRepo, err := repo_service.CreateRepository(db.DefaultContext, user2, user2, repo_module.CreateRepoOptions{ + baseRepo, err := repo_service.CreateRepository(db.DefaultContext, user2, user2, repo_service.CreateRepoOptions{ Name: "repo-pull-request-target", Description: "test pull-request-target event", AutoInit: true, diff --git a/tests/integration/mirror_pull_test.go b/tests/integration/mirror_pull_test.go index 1bd91a48b5..2f79f5113b 100644 --- a/tests/integration/mirror_pull_test.go +++ b/tests/integration/mirror_pull_test.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/repository" mirror_service "code.gitea.io/gitea/services/mirror" release_service "code.gitea.io/gitea/services/release" + repo_service "code.gitea.io/gitea/services/repository" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" @@ -38,7 +39,7 @@ func TestMirrorPull(t *testing.T) { Releases: false, } - mirrorRepo, err := repository.CreateRepository(user, user, repository.CreateRepoOptions{ + mirrorRepo, err := repo_service.CreateRepositoryDirectly(user, user, repo_service.CreateRepoOptions{ Name: opts.RepoName, Description: opts.Description, IsPrivate: opts.Private, diff --git a/tests/integration/mirror_push_test.go b/tests/integration/mirror_push_test.go index 9abae63b0a..ab79db1861 100644 --- a/tests/integration/mirror_push_test.go +++ b/tests/integration/mirror_push_test.go @@ -17,10 +17,10 @@ import ( user_model "code.gitea.io/gitea/models/user" gitea_context "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/services/migrations" mirror_service "code.gitea.io/gitea/services/mirror" + repo_service "code.gitea.io/gitea/services/repository" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" @@ -39,7 +39,7 @@ func testMirrorPush(t *testing.T, u *url.URL) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) srcRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - mirrorRepo, err := repository.CreateRepository(user, user, repository.CreateRepoOptions{ + mirrorRepo, err := repo_service.CreateRepositoryDirectly(user, user, repo_service.CreateRepoOptions{ Name: "test-push-mirror", }) assert.NoError(t, err) diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go index f958c890fe..0ef0969ab9 100644 --- a/tests/integration/pull_merge_test.go +++ b/tests/integration/pull_merge_test.go @@ -25,7 +25,6 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/git" - repo_module "code.gitea.io/gitea/modules/repository" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/translation" @@ -356,7 +355,7 @@ func TestConflictChecking(t *testing.T) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // Create new clean repo to test conflict checking. - baseRepo, err := repo_service.CreateRepository(db.DefaultContext, user, user, repo_module.CreateRepoOptions{ + baseRepo, err := repo_service.CreateRepository(db.DefaultContext, user, user, repo_service.CreateRepoOptions{ Name: "conflict-checking", Description: "Tempo repo", AutoInit: true, diff --git a/tests/integration/pull_update_test.go b/tests/integration/pull_update_test.go index 80c55042db..e4b2ae65bd 100644 --- a/tests/integration/pull_update_test.go +++ b/tests/integration/pull_update_test.go @@ -16,7 +16,6 @@ import ( "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" - repo_module "code.gitea.io/gitea/modules/repository" pull_service "code.gitea.io/gitea/services/pull" repo_service "code.gitea.io/gitea/services/repository" files_service "code.gitea.io/gitea/services/repository/files" @@ -81,7 +80,7 @@ func TestAPIPullUpdateByRebase(t *testing.T) { } func createOutdatedPR(t *testing.T, actor, forkOrg *user_model.User) *issues_model.PullRequest { - baseRepo, err := repo_service.CreateRepository(db.DefaultContext, actor, actor, repo_module.CreateRepoOptions{ + baseRepo, err := repo_service.CreateRepository(db.DefaultContext, actor, actor, repo_service.CreateRepoOptions{ Name: "repo-pr-update", Description: "repo-tmp-pr-update description", AutoInit: true, From 2715ef6558a3a89ab1acf8cdfb642bbf849293d3 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Wed, 6 Sep 2023 20:22:38 +0800 Subject: [PATCH 12/52] Fix scoped label layout (#26932) Fix #26931 --- web_src/css/repo.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web_src/css/repo.css b/web_src/css/repo.css index fb85a53ab7..a03ec9f060 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -2507,9 +2507,10 @@ } /* Scoped labels with different colors on left and right */ -.scope-parent { +.ui.label.scope-parent { background: none !important; padding: 0 !important; + gap: 0 !important; } .ui.label.scope-left { From d1353ad55a9c075ab4f0f6f8a2df71331a2c818b Mon Sep 17 00:00:00 2001 From: "Panagiotis \"Ivory\" Vasilopoulos" Date: Wed, 6 Sep 2023 14:22:50 +0000 Subject: [PATCH 13/52] docs: Update Profile README information (#26947) Follow-up of https://github.com/go-gitea/gitea/pull/26295 --- docs/content/usage/profile-readme.en-us.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/content/usage/profile-readme.en-us.md b/docs/content/usage/profile-readme.en-us.md index fbe175eed9..045d33d1c1 100644 --- a/docs/content/usage/profile-readme.en-us.md +++ b/docs/content/usage/profile-readme.en-us.md @@ -15,6 +15,6 @@ menu: # Profile READMEs -To display a markdown file in your Gitea profile page, simply make a repository named ".profile" and edit the README.md file inside. Gitea will automatically pull this file in and display it above your repositories. +To display a Markdown file in your Gitea profile page, simply create a repository named `.profile` and add a new file called `README.md`. Gitea will automatically display the contents of the file on your profile, above your repositories. -Note. You are welcome to make this repository private. Doing so will hide your source files from public viewing and allow you to privitize certain files. However, the README.md file will be the only file present on your profile. If you wish to have an entirely private .profile repository, remove or rename the README.md file. +Making the `.profile` repository private will hide the Profile README. From e5968062178628d1baee46a382cfa4da51331500 Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 6 Sep 2023 19:49:45 +0200 Subject: [PATCH 14/52] Update nginx recommendations (#26924) - `Connection` and `Upgrade` [needed for websockets](https://www.nginx.com/blog/websocket-nginx/) - ~~`X-Real-IP` unnecessary and duplicate with `X-Forwarded-For`. [chi checks both headers](https://github.com/go-chi/chi/blob/master/middleware/realip.go), but XFF is definitely the more "standard" one.~~ --- docs/content/administration/reverse-proxies.en-us.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/content/administration/reverse-proxies.en-us.md b/docs/content/administration/reverse-proxies.en-us.md index ca06636469..c141483700 100644 --- a/docs/content/administration/reverse-proxies.en-us.md +++ b/docs/content/administration/reverse-proxies.en-us.md @@ -29,6 +29,8 @@ server { location / { client_max_body_size 512M; proxy_pass http://localhost:3000; + proxy_set_header Connection $http_connection; + proxy_set_header Upgrade $http_upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; From 9860dba56601a881d0193edc27f11b4feaab3f6b Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Thu, 7 Sep 2023 00:22:15 +0000 Subject: [PATCH 15/52] [skip ci] Updated translations via Crowdin --- options/locale/locale_ja-JP.ini | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index 51480b7e62..25241d6033 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -91,6 +91,7 @@ edit=編集 enabled=有効 disabled=無効 +locked=ロック済み copy=コピー copy_url=URLをコピー @@ -594,6 +595,8 @@ user_bio=経歴 disabled_public_activity=このユーザーはアクティビティ表示を公開していません。 email_visibility.limited=あなたのメールアドレスはすべての認証済みユーザーに表示されています email_visibility.private=あなたのメールアドレスは、あなたと管理者のみに表示されます +show_on_map=地図上にこの場所を表示 +settings=ユーザー設定 form.name_reserved=ユーザー名 "%s" は予約されています。 form.name_pattern_not_allowed=`"%s" の形式はユーザー名に使用できません。` @@ -620,6 +623,7 @@ webauthn=セキュリティキー public_profile=公開プロフィール biography_placeholder=自己紹介してください!(Markdownを使うことができます) +location_placeholder=おおよその場所を他の人と共有 profile_desc=あなたのプロフィールが他のユーザーにどのように表示されるかを制御します。あなたのプライマリメールアドレスは、通知、パスワードの回復、WebベースのGit操作に使用されます。 password_username_disabled=非ローカルユーザーのユーザー名は変更できません。詳細はサイト管理者にお問い合わせください。 full_name=フルネーム @@ -632,6 +636,8 @@ update_language_not_found=言語 "%s" は利用できません。 update_language_success=言語が更新されました。 update_profile_success=プロフィールを更新しました。 change_username=ユーザー名を変更しました。 +change_username_prompt=注意: ユーザー名を変更するとアカウントのURLも変更されます。 +change_username_redirect_prompt=古いユーザー名は、誰かが再使用するまではリダイレクトします。 continue=続行 cancel=キャンセル language=言語 @@ -656,6 +662,7 @@ comment_type_group_project=プロジェクト comment_type_group_issue_ref=イシューの参照先 saved_successfully=設定は正常に保存されました。 privacy=プライバシー +keep_activity_private=プロフィールページのアクティビティ表示を隠す keep_activity_private_popup=アクティビティを、あなたと管理者にのみ表示します lookup_avatar_by_mail=メールアドレスでアバターを見つける @@ -689,6 +696,7 @@ requires_activation=アクティベーションが必要 primary_email=プライマリーにする activate_email=アクティベーションを送信 activations_pending=アクティベーション待ち +can_not_add_email_activations_pending=保留中のアクティベーションがあります。新しいメールを追加する場合は、数分後にもう一度お試しください。 delete_email=削除 email_deletion=メールアドレスの削除 email_deletion_desc=メールアドレスと関連情報をアカウントから削除します。 このメールアドレスを使ったGitのコミットはそのまま残ります。 続行しますか? @@ -807,8 +815,10 @@ repo_and_org_access=リポジトリと組織へのアクセス permissions_public_only=公開のみ permissions_access_all=すべて (公開、プライベート、限定) select_permissions=許可の選択 -permission_no_access=アクセスなし -permission_read=既読 +permission_no_access=アクセス不可 +permission_read=読み取り +permission_write=読み取りと書き込み +access_token_desc=選択したトークン権限に応じて、関連するAPIルートのみに許可が制限されます。 詳細はドキュメントを参照してください。 at_least_one_permission=トークンを作成するには、少なくともひとつの許可を選択する必要があります permissions_list=許可: @@ -834,6 +844,7 @@ oauth2_client_secret_hint=このページから移動したりページを更新 oauth2_application_edit=編集 oauth2_application_create_description=OAuth2アプリケーションで、サードパーティアプリケーションがこのインスタンス上のユーザーアカウントにアクセスできるようになります。 oauth2_application_remove_description=OAuth2アプリケーションを削除すると、このインスタンス上の許可されたユーザーアカウントへのアクセスができなくなります。 続行しますか? +oauth2_application_locked=設定で有効にされた場合、Giteaは起動時にいくつかのOAuth2アプリケーションを事前登録します。 想定されていない動作を防ぐため、これらは編集も削除もできません。 詳細についてはOAuth2のドキュメントを参照してください。 authorized_oauth2_applications=許可済みOAuth2アプリケーション authorized_oauth2_applications_description=これらのサードパーティ アプリケーションに、あなたのGiteaアカウントへのアクセスを許可しています。 不要になったアプリケーションはアクセス権を取り消すようにしてください。 @@ -922,6 +933,7 @@ fork_from=フォーク元 already_forked=%s はフォーク済み fork_to_different_account=別のアカウントにフォークする fork_visibility_helper=フォークしたリポジトリの公開/非公開は変更できません。 +fork_no_valid_owners=このリポジトリには有効なオーナーがいないため、フォークできません。 use_template=このテンプレートを使用 clone_in_vsc=VSCodeでクローン download_zip=ZIPファイルをダウンロード @@ -2522,6 +2534,7 @@ settings.visibility.private_shortname=プライベート settings.update_settings=設定の更新 settings.update_setting_success=組織の設定を更新しました。 +settings.change_orgname_prompt=注意: 組織名を変更すると組織のURLも変更され、古い名前は解放されます。 settings.change_orgname_redirect_prompt=古い名前は、再使用されていない限りリダイレクトします。 settings.update_avatar_success=組織のアバターを更新しました。 settings.delete=組織を削除 @@ -2615,6 +2628,7 @@ monitor=モニタリング first_page=最初 last_page=最後 total=合計: %d +settings=管理設定 dashboard.new_version_hint=Gitea %s が入手可能になりました。 現在実行しているのは %s です。 詳細は ブログ を確認してください。 dashboard.statistic=サマリー @@ -3378,6 +3392,7 @@ status.waiting=待機中 status.running=実行中 status.success=成功 status.failure=失敗 +status.cancelled=キャンセル status.skipped=スキップ status.blocked=ブロックされた From 419003adb24495d804b276d2c7d59b170d0a56b7 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 7 Sep 2023 09:13:11 +0800 Subject: [PATCH 16/52] Improve SSH Key / GPG Key / Deploy Key UI (#26949) 1. In many cases, the `flex-list` has previous and next `gt-hidden` siblings, so relax the CSS selector to remove all ".segument .flex-list" paddings. 2. Make the "Add key" button can toggle 3. Move help message into the related segment(panel). Otherwise users would misread the message, eg: the SSH help seemed for GPG because they are so near 4. Move modal element into the segment element, otherwise it affects the layout --- templates/repo/settings/deploy_keys.tmpl | 4 ++-- templates/user/settings/keys_gpg.tmpl | 30 ++++++++++++------------ templates/user/settings/keys_ssh.tmpl | 30 ++++++++++++------------ web_src/css/shared/flex-list.css | 6 ++--- 4 files changed, 35 insertions(+), 35 deletions(-) diff --git a/templates/repo/settings/deploy_keys.tmpl b/templates/repo/settings/deploy_keys.tmpl index a9e540bc65..b776848a56 100644 --- a/templates/repo/settings/deploy_keys.tmpl +++ b/templates/repo/settings/deploy_keys.tmpl @@ -4,14 +4,14 @@ {{.locale.Tr "repo.settings.deploy_keys"}}
{{if not .DisableSSH}} - + {{else}} {{end}}
-
+
{{.CsrfTokenHtml}}
diff --git a/templates/user/settings/keys_gpg.tmpl b/templates/user/settings/keys_gpg.tmpl index e7a66de23f..2ecebcd7c0 100644 --- a/templates/user/settings/keys_gpg.tmpl +++ b/templates/user/settings/keys_gpg.tmpl @@ -1,11 +1,11 @@

{{.locale.Tr "settings.manage_gpg_keys"}}
- +

-
+
{{.CsrfTokenHtml}} @@ -41,7 +41,10 @@
- {{.locale.Tr "settings.gpg_desc"}} +

+ {{.locale.Tr "settings.gpg_desc"}}
+ {{.locale.Tr "settings.gpg_helper" "https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/about-commit-signature-verification#gpg-commit-signature-verification" | Str2html}} +

{{range .GPGKeys}}
@@ -107,17 +110,14 @@ {{end}} {{end}}
-
-
-

{{.locale.Tr "settings.gpg_helper" "https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/about-commit-signature-verification#gpg-commit-signature-verification" | Str2html}}

- -