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 01/16] 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 2d7cf5185c50..7638ffdd3f10 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 f5c56cf83323..8a16d311b140 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 613dff2182bd..a69fdedba460 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 fae8b48128be..3c073211aee9 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 957daab64663..51d5282fa07f 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 3b07bba713a8..fecb0cd5e9a2 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 6ad2f71b5c2d..ac9e64d774e3 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 9b1918ed16b4..6273e11fc5ba 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 033f65c9c06c..29b8b91c89ec 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 62d2dc0aa234..81c2746819e2 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 02/16] 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 cbd3336d266f..9f42610641ae 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 03/16] 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 dd673190aa74..a2fab2fd5093 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 4158f14cb1dd..7e8befb8b7e6 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 800dcd0d50a7..849a90fd10a4 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 9f4acda23699..40df1cd62434 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 000000000000..df5994f159ff --- /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 a13330dcd18a..bfc502c0cbea 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 bdb9b0c9dd82..4f5f0383e9aa 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 946ea11e7527..c45dc667afcd 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 30d31b4d7569..458d671cff6d 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 e4ca6a71987f..a9c2858303aa 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 000000000000..785eeb5838ea --- /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 2a213ae51524..3869382d2236 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 6590ca667cf5..101bedde0278 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 04/16] 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 3c073211aee9..f963ad55efd3 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 51d5282fa07f..0f082a70dfa8 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 9bf4280b07e5..0045bce4c932 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 21d25bd90a6a..796829d34ef2 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 fecb0cd5e9a2..1e0287fe2799 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 d44638d48bbc..57770b2b1aed 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 641cc1fd9ffa..5786118f50d9 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 05/16] 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 06/16] 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 e72e7bba6208..f565318b8b37 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 d4343f8c680f..ea477c997583 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 79fd4bd2ff77..67a5f7296f99 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 4d3ddf038346..9a857db85c9b 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 b5878fc93004..a185c42b51ea 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 07/16] 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 4409381bc508..c9cefb68db9e 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 08/16] 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 201681bc039c..90332b7c692d 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 09/16] 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 10a1e872df98..2dac35224e68 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 e620422bcb77..6a2f4deaff06 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 2e0b7600a594..4055029d22a1 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 a95b9c2e9946..daab7c3091eb 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 84648f45ebf4..6f791f742b12 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 5b188a041e24..6ac3547e7b2f 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 13c865693c3c..3b0cfa1eed48 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 6a11315cc404..6bf88e775262 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 ccd8be9171aa..bf030eb222bc 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 dfc9004620fb..41374831defb 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 7b0c954a73bb..29f6a675d4fd 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 d1d0abca0254..45c280ef7317 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 c9cefb68db9e..12cd477926c2 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 01668c395422..decb35c1e177 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 ee7fc578514f..a4a3af82e7c6 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 867cd796d381..572f5e1f5bec 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 f225538faf9e..00dce7295edb 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 000000000000..a5d521e35305 --- /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 000000000000..ec3d62ce079c --- /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 47e96bd5e58f..db3035f8c0f0 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 db5c1dd3f850..45bc7b990a12 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 1c5d2fed61d1..56718397f483 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 1bd91a48b5d4..2f79f5113b6b 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 9abae63b0ad7..ab79db1861ff 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 f958c890fe4d..0ef0969ab902 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 80c55042db7c..e4b2ae65bd61 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 10/16] 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 fb85a53ab7d4..a03ec9f0601e 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 11/16] 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 fbe175eed94d..045d33d1c177 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 12/16] 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 ca06636469fb..c14148370051 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 13/16] [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 51480b7e62ba..25241d603359 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 14/16] 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 a9e540bc65d6..b776848a5628 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 e7a66de23f72..2ecebcd7c01b 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}}

- -