diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index dd673190aa..a2fab2fd50 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -2564,6 +2564,8 @@ LEVEL = Info ;; ;; Default platform to get action plugins, `github` for `https://github.com`, `self` for the current Gitea instance. ;DEFAULT_ACTIONS_URL = github +;; Default artifact retention time in days, default is 90 days +;ARTIFACT_RETENTION_DAYS = 90 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md index 4158f14cb1..7e8befb8b7 100644 --- a/docs/content/administration/config-cheat-sheet.en-us.md +++ b/docs/content/administration/config-cheat-sheet.en-us.md @@ -955,6 +955,12 @@ Default templates for project boards: - `SCHEDULE`: **@midnight** : Interval as a duration between each synchronization, it will always attempt synchronization when the instance starts. - `UPDATE_EXISTING`: **true**: Create new users, update existing user data and disable users that are not in external source anymore (default) or only create new users if UPDATE_EXISTING is set to false. +## Cron - Cleanup Expired Actions Assets (`cron.cleanup_actions`) + +- `ENABLED`: **true**: Enable cleanup expired actions assets job. +- `RUN_AT_START`: **true**: Run job at start time (if ENABLED). +- `SCHEDULE`: **@midnight** : Cron syntax for the job. + ### Extended cron tasks (not enabled by default) #### Cron - Garbage collect all repositories (`cron.git_gc_repos`) @@ -1381,6 +1387,7 @@ PROXY_HOSTS = *.github.com - `DEFAULT_ACTIONS_URL`: **github**: Default platform to get action plugins, `github` for `https://github.com`, `self` for the current Gitea instance. - `STORAGE_TYPE`: **local**: Storage type for actions logs, `local` for local disk or `minio` for s3 compatible object storage service, default is `local` or other name defined with `[storage.xxx]` - `MINIO_BASE_PATH`: **actions_log/**: Minio base path on the bucket only available when STORAGE_TYPE is `minio` +- `ARTIFACT_RETENTION_DAYS`: **90**: Number of days to keep artifacts. Set to 0 to disable artifact retention. Default is 90 days if not set. `DEFAULT_ACTIONS_URL` indicates where the Gitea Actions runners should find the actions with relative path. For example, `uses: actions/checkout@v3` means `https://github.com/actions/checkout@v3` since the value of `DEFAULT_ACTIONS_URL` is `github`. diff --git a/docs/content/administration/reverse-proxies.en-us.md b/docs/content/administration/reverse-proxies.en-us.md index ca06636469..c141483700 100644 --- a/docs/content/administration/reverse-proxies.en-us.md +++ b/docs/content/administration/reverse-proxies.en-us.md @@ -29,6 +29,8 @@ server { location / { client_max_body_size 512M; proxy_pass http://localhost:3000; + proxy_set_header Connection $http_connection; + proxy_set_header Upgrade $http_upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; diff --git a/docs/content/installation/windows-service.en-us.md b/docs/content/installation/windows-service.en-us.md index 201681bc03..90332b7c69 100644 --- a/docs/content/installation/windows-service.en-us.md +++ b/docs/content/installation/windows-service.en-us.md @@ -51,6 +51,15 @@ Open "Windows Services", search for the service named "gitea", right-click it an "Run". If everything is OK, Gitea will be reachable on `http://localhost:3000` (or the port that was configured). +## Service startup type + +It was observed that on loaded systems during boot Gitea service may fail to start with timeout records in Windows Event Log. +In that case change startup type to `Automatic-Delayed`. This can be done during service creation, or by running config command + +``` +sc.exe config gitea start= delayed-auto +``` + ## Adding startup dependencies To add a startup dependency to the Gitea Windows service (eg Mysql, Mariadb), as an Administrator, then run the following command: diff --git a/docs/content/usage/profile-readme.en-us.md b/docs/content/usage/profile-readme.en-us.md index fbe175eed9..045d33d1c1 100644 --- a/docs/content/usage/profile-readme.en-us.md +++ b/docs/content/usage/profile-readme.en-us.md @@ -15,6 +15,6 @@ menu: # Profile READMEs -To display a markdown file in your Gitea profile page, simply make a repository named ".profile" and edit the README.md file inside. Gitea will automatically pull this file in and display it above your repositories. +To display a Markdown file in your Gitea profile page, simply create a repository named `.profile` and add a new file called `README.md`. Gitea will automatically display the contents of the file on your profile, above your repositories. -Note. You are welcome to make this repository private. Doing so will hide your source files from public viewing and allow you to privitize certain files. However, the README.md file will be the only file present on your profile. If you wish to have an entirely private .profile repository, remove or rename the README.md file. +Making the `.profile` repository private will hide the Profile README. diff --git a/models/actions/artifact.go b/models/actions/artifact.go index 800dcd0d50..849a90fd10 100644 --- a/models/actions/artifact.go +++ b/models/actions/artifact.go @@ -9,19 +9,21 @@ package actions import ( "context" "errors" + "time" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" ) +// ArtifactStatus is the status of an artifact, uploading, expired or need-delete +type ArtifactStatus int64 + const ( - // ArtifactStatusUploadPending is the status of an artifact upload that is pending - ArtifactStatusUploadPending = 1 - // ArtifactStatusUploadConfirmed is the status of an artifact upload that is confirmed - ArtifactStatusUploadConfirmed = 2 - // ArtifactStatusUploadError is the status of an artifact upload that is errored - ArtifactStatusUploadError = 3 + ArtifactStatusUploadPending ArtifactStatus = iota + 1 // 1, ArtifactStatusUploadPending is the status of an artifact upload that is pending + ArtifactStatusUploadConfirmed // 2, ArtifactStatusUploadConfirmed is the status of an artifact upload that is confirmed + ArtifactStatusUploadError // 3, ArtifactStatusUploadError is the status of an artifact upload that is errored + ArtifactStatusExpired // 4, ArtifactStatusExpired is the status of an artifact that is expired ) func init() { @@ -45,9 +47,10 @@ type ActionArtifact struct { Status int64 `xorm:"index"` // The status of the artifact, uploading, expired or need-delete CreatedUnix timeutil.TimeStamp `xorm:"created"` UpdatedUnix timeutil.TimeStamp `xorm:"updated index"` + ExpiredUnix timeutil.TimeStamp `xorm:"index"` // The time when the artifact will be expired } -func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPath string) (*ActionArtifact, error) { +func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPath string, expiredDays int64) (*ActionArtifact, error) { if err := t.LoadJob(ctx); err != nil { return nil, err } @@ -61,7 +64,8 @@ func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPa RepoID: t.RepoID, OwnerID: t.OwnerID, CommitSHA: t.CommitSHA, - Status: ArtifactStatusUploadPending, + Status: int64(ArtifactStatusUploadPending), + ExpiredUnix: timeutil.TimeStamp(time.Now().Unix() + 3600*24*expiredDays), } if _, err := db.GetEngine(ctx).Insert(artifact); err != nil { return nil, err @@ -126,15 +130,16 @@ func ListUploadedArtifactsByRunID(ctx context.Context, runID int64) ([]*ActionAr type ActionArtifactMeta struct { ArtifactName string FileSize int64 + Status int64 } // ListUploadedArtifactsMeta returns all uploaded artifacts meta of a run func ListUploadedArtifactsMeta(ctx context.Context, runID int64) ([]*ActionArtifactMeta, error) { arts := make([]*ActionArtifactMeta, 0, 10) return arts, db.GetEngine(ctx).Table("action_artifact"). - Where("run_id=? AND status=?", runID, ArtifactStatusUploadConfirmed). + Where("run_id=? AND (status=? OR status=?)", runID, ArtifactStatusUploadConfirmed, ArtifactStatusExpired). GroupBy("artifact_name"). - Select("artifact_name, sum(file_size) as file_size"). + Select("artifact_name, sum(file_size) as file_size, max(status) as status"). Find(&arts) } @@ -149,3 +154,16 @@ func ListArtifactsByRunIDAndName(ctx context.Context, runID int64, name string) arts := make([]*ActionArtifact, 0, 10) return arts, db.GetEngine(ctx).Where("run_id=? AND artifact_name=?", runID, name).Find(&arts) } + +// ListNeedExpiredArtifacts returns all need expired artifacts but not deleted +func ListNeedExpiredArtifacts(ctx context.Context) ([]*ActionArtifact, error) { + arts := make([]*ActionArtifact, 0, 10) + return arts, db.GetEngine(ctx). + Where("expired_unix < ? AND status = ?", timeutil.TimeStamp(time.Now().Unix()), ArtifactStatusUploadConfirmed).Find(&arts) +} + +// SetArtifactExpired sets an artifact to expired +func SetArtifactExpired(ctx context.Context, artifactID int64) error { + _, err := db.GetEngine(ctx).Where("id=? AND status = ?", artifactID, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: int64(ArtifactStatusExpired)}) + return err +} diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 9f4acda236..40df1cd624 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -528,6 +528,8 @@ var migrations = []Migration{ NewMigration("Add Version to ActionRun table", v1_21.AddVersionToActionRunTable), // v273 -> v274 NewMigration("Add Action Schedule Table", v1_21.AddActionScheduleTable), + // v274 -> v275 + NewMigration("Add Actions artifacts expiration date", v1_21.AddExpiredUnixColumnInActionArtifactTable), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v1_21/v274.go b/models/migrations/v1_21/v274.go new file mode 100644 index 0000000000..df5994f159 --- /dev/null +++ b/models/migrations/v1_21/v274.go @@ -0,0 +1,36 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_21 //nolint +import ( + "time" + + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" +) + +func AddExpiredUnixColumnInActionArtifactTable(x *xorm.Engine) error { + type ActionArtifact struct { + ExpiredUnix timeutil.TimeStamp `xorm:"index"` // time when the artifact will be expired + } + if err := x.Sync(new(ActionArtifact)); err != nil { + return err + } + return updateArtifactsExpiredUnixTo90Days(x) +} + +func updateArtifactsExpiredUnixTo90Days(x *xorm.Engine) error { + sess := x.NewSession() + defer sess.Close() + + if err := sess.Begin(); err != nil { + return err + } + expiredTime := time.Now().AddDate(0, 0, 90).Unix() + if _, err := sess.Exec(`UPDATE action_artifact SET expired_unix=? WHERE status='2' AND expired_unix is NULL`, expiredTime); err != nil { + return err + } + + return sess.Commit() +} diff --git a/modules/context/org.go b/modules/context/org.go index 2d7cf5185c..7638ffdd3f 100644 --- a/modules/context/org.go +++ b/modules/context/org.go @@ -250,6 +250,7 @@ func HandleOrgAssignment(ctx *Context, args ...bool) { return } } + ctx.Data["ContextUser"] = ctx.ContextUser ctx.Data["CanReadProjects"] = ctx.Org.CanReadUnit(ctx, unit.TypeProjects) ctx.Data["CanReadPackages"] = ctx.Org.CanReadUnit(ctx, unit.TypePackages) diff --git a/modules/context/repo.go b/modules/context/repo.go index f5c56cf833..8a16d311b1 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -471,6 +471,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc { } ctx.Repo.Owner = owner ctx.ContextUser = owner + ctx.Data["ContextUser"] = ctx.ContextUser ctx.Data["Username"] = ctx.Repo.Owner.Name // redirect link to wiki diff --git a/modules/repository/create.go b/modules/repository/create.go index 10a1e872df..2dac35224e 100644 --- a/modules/repository/create.go +++ b/modules/repository/create.go @@ -22,7 +22,6 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/models/webhook" - "code.gitea.io/gitea/modules/git" issue_indexer "code.gitea.io/gitea/modules/indexer/issues" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -156,142 +155,6 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re return nil } -// CreateRepoOptions contains the create repository options -type CreateRepoOptions struct { - Name string - Description string - OriginalURL string - GitServiceType api.GitServiceType - Gitignores string - IssueLabels string - License string - Readme string - DefaultBranch string - IsPrivate bool - IsMirror bool - IsTemplate bool - AutoInit bool - Status repo_model.RepositoryStatus - TrustModel repo_model.TrustModelType - MirrorInterval string -} - -// CreateRepository creates a repository for the user/organization. -func CreateRepository(doer, u *user_model.User, opts CreateRepoOptions) (*repo_model.Repository, error) { - if !doer.IsAdmin && !u.CanCreateRepo() { - return nil, repo_model.ErrReachLimitOfRepo{ - Limit: u.MaxRepoCreation, - } - } - - if len(opts.DefaultBranch) == 0 { - opts.DefaultBranch = setting.Repository.DefaultBranch - } - - // Check if label template exist - if len(opts.IssueLabels) > 0 { - if _, err := LoadTemplateLabelsByDisplayName(opts.IssueLabels); err != nil { - return nil, err - } - } - - repo := &repo_model.Repository{ - OwnerID: u.ID, - Owner: u, - OwnerName: u.Name, - Name: opts.Name, - LowerName: strings.ToLower(opts.Name), - Description: opts.Description, - OriginalURL: opts.OriginalURL, - OriginalServiceType: opts.GitServiceType, - IsPrivate: opts.IsPrivate, - IsFsckEnabled: !opts.IsMirror, - IsTemplate: opts.IsTemplate, - CloseIssuesViaCommitInAnyBranch: setting.Repository.DefaultCloseIssuesViaCommitsInAnyBranch, - Status: opts.Status, - IsEmpty: !opts.AutoInit, - TrustModel: opts.TrustModel, - IsMirror: opts.IsMirror, - DefaultBranch: opts.DefaultBranch, - } - - var rollbackRepo *repo_model.Repository - - if err := db.WithTx(db.DefaultContext, func(ctx context.Context) error { - if err := CreateRepositoryByExample(ctx, doer, u, repo, false, false); err != nil { - return err - } - - // No need for init mirror. - if opts.IsMirror { - return nil - } - - repoPath := repo_model.RepoPath(u.Name, repo.Name) - isExist, err := util.IsExist(repoPath) - if err != nil { - log.Error("Unable to check if %s exists. Error: %v", repoPath, err) - return err - } - if isExist { - // repo already exists - We have two or three options. - // 1. We fail stating that the directory exists - // 2. We create the db repository to go with this data and adopt the git repo - // 3. We delete it and start afresh - // - // Previously Gitea would just delete and start afresh - this was naughty. - // So we will now fail and delegate to other functionality to adopt or delete - log.Error("Files already exist in %s and we are not going to adopt or delete.", repoPath) - return repo_model.ErrRepoFilesAlreadyExist{ - Uname: u.Name, - Name: repo.Name, - } - } - - if err = initRepository(ctx, repoPath, doer, repo, opts); err != nil { - if err2 := util.RemoveAll(repoPath); err2 != nil { - log.Error("initRepository: %v", err) - return fmt.Errorf( - "delete repo directory %s/%s failed(2): %v", u.Name, repo.Name, err2) - } - return fmt.Errorf("initRepository: %w", err) - } - - // Initialize Issue Labels if selected - if len(opts.IssueLabels) > 0 { - if err = InitializeLabels(ctx, repo.ID, opts.IssueLabels, false); err != nil { - rollbackRepo = repo - rollbackRepo.OwnerID = u.ID - return fmt.Errorf("InitializeLabels: %w", err) - } - } - - if err := CheckDaemonExportOK(ctx, repo); err != nil { - return fmt.Errorf("checkDaemonExportOK: %w", err) - } - - if stdout, _, err := git.NewCommand(ctx, "update-server-info"). - SetDescription(fmt.Sprintf("CreateRepository(git update-server-info): %s", repoPath)). - RunStdString(&git.RunOpts{Dir: repoPath}); err != nil { - log.Error("CreateRepository(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err) - rollbackRepo = repo - rollbackRepo.OwnerID = u.ID - return fmt.Errorf("CreateRepository(git update-server-info): %w", err) - } - return nil - }); err != nil { - if rollbackRepo != nil { - if errDelete := models.DeleteRepository(doer, rollbackRepo.OwnerID, rollbackRepo.ID); errDelete != nil { - log.Error("Rollback deleteRepository: %v", errDelete) - } - } - - return nil, err - } - - return repo, nil -} - const notRegularFileMode = os.ModeSymlink | os.ModeNamedPipe | os.ModeSocket | os.ModeDevice | os.ModeCharDevice | os.ModeIrregular // getDirectorySize returns the disk consumption for a given path diff --git a/modules/repository/create_test.go b/modules/repository/create_test.go index e620422bcb..6a2f4deaff 100644 --- a/modules/repository/create_test.go +++ b/modules/repository/create_test.go @@ -4,151 +4,16 @@ package repository import ( - "fmt" "testing" - "code.gitea.io/gitea/models" activities_model "code.gitea.io/gitea/models/activities" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/organization" - "code.gitea.io/gitea/models/perm" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" - user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/structs" "github.com/stretchr/testify/assert" ) -func TestIncludesAllRepositoriesTeams(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - testTeamRepositories := func(teamID int64, repoIds []int64) { - team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID}) - assert.NoError(t, team.LoadRepositories(db.DefaultContext), "%s: GetRepositories", team.Name) - assert.Len(t, team.Repos, team.NumRepos, "%s: len repo", team.Name) - assert.Len(t, team.Repos, len(repoIds), "%s: repo count", team.Name) - for i, rid := range repoIds { - if rid > 0 { - assert.True(t, models.HasRepository(team, rid), "%s: HasRepository(%d) %d", rid, i) - } - } - } - - // Get an admin user. - user, err := user_model.GetUserByID(db.DefaultContext, 1) - assert.NoError(t, err, "GetUserByID") - - // Create org. - org := &organization.Organization{ - Name: "All_repo", - IsActive: true, - Type: user_model.UserTypeOrganization, - Visibility: structs.VisibleTypePublic, - } - assert.NoError(t, organization.CreateOrganization(org, user), "CreateOrganization") - - // Check Owner team. - ownerTeam, err := org.GetOwnerTeam(db.DefaultContext) - assert.NoError(t, err, "GetOwnerTeam") - assert.True(t, ownerTeam.IncludesAllRepositories, "Owner team includes all repositories") - - // Create repos. - repoIds := make([]int64, 0) - for i := 0; i < 3; i++ { - r, err := CreateRepository(user, org.AsUser(), CreateRepoOptions{Name: fmt.Sprintf("repo-%d", i)}) - assert.NoError(t, err, "CreateRepository %d", i) - if r != nil { - repoIds = append(repoIds, r.ID) - } - } - // Get fresh copy of Owner team after creating repos. - ownerTeam, err = org.GetOwnerTeam(db.DefaultContext) - assert.NoError(t, err, "GetOwnerTeam") - - // Create teams and check repositories. - teams := []*organization.Team{ - ownerTeam, - { - OrgID: org.ID, - Name: "team one", - AccessMode: perm.AccessModeRead, - IncludesAllRepositories: true, - }, - { - OrgID: org.ID, - Name: "team 2", - AccessMode: perm.AccessModeRead, - IncludesAllRepositories: false, - }, - { - OrgID: org.ID, - Name: "team three", - AccessMode: perm.AccessModeWrite, - IncludesAllRepositories: true, - }, - { - OrgID: org.ID, - Name: "team 4", - AccessMode: perm.AccessModeWrite, - IncludesAllRepositories: false, - }, - } - teamRepos := [][]int64{ - repoIds, - repoIds, - {}, - repoIds, - {}, - } - for i, team := range teams { - if i > 0 { // first team is Owner. - assert.NoError(t, models.NewTeam(team), "%s: NewTeam", team.Name) - } - testTeamRepositories(team.ID, teamRepos[i]) - } - - // Update teams and check repositories. - teams[3].IncludesAllRepositories = false - teams[4].IncludesAllRepositories = true - teamRepos[4] = repoIds - for i, team := range teams { - assert.NoError(t, models.UpdateTeam(team, false, true), "%s: UpdateTeam", team.Name) - testTeamRepositories(team.ID, teamRepos[i]) - } - - // Create repo and check teams repositories. - r, err := CreateRepository(user, org.AsUser(), CreateRepoOptions{Name: "repo-last"}) - assert.NoError(t, err, "CreateRepository last") - if r != nil { - repoIds = append(repoIds, r.ID) - } - teamRepos[0] = repoIds - teamRepos[1] = repoIds - teamRepos[4] = repoIds - for i, team := range teams { - testTeamRepositories(team.ID, teamRepos[i]) - } - - // Remove repo and check teams repositories. - assert.NoError(t, models.DeleteRepository(user, org.ID, repoIds[0]), "DeleteRepository") - teamRepos[0] = repoIds[1:] - teamRepos[1] = repoIds[1:] - teamRepos[3] = repoIds[1:3] - teamRepos[4] = repoIds[1:] - for i, team := range teams { - testTeamRepositories(team.ID, teamRepos[i]) - } - - // Wipe created items. - for i, rid := range repoIds { - if i > 0 { // first repo already deleted. - assert.NoError(t, models.DeleteRepository(user, org.ID, rid), "DeleteRepository %d", i) - } - } - assert.NoError(t, organization.DeleteOrganization(db.DefaultContext, org), "DeleteOrganization") -} - func TestUpdateRepositoryVisibilityChanged(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) diff --git a/modules/repository/generate.go b/modules/repository/generate.go index 2e0b7600a5..4055029d22 100644 --- a/modules/repository/generate.go +++ b/modules/repository/generate.go @@ -241,7 +241,7 @@ func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *r defaultBranch = templateRepo.DefaultBranch } - return initRepoCommit(ctx, tmpDir, repo, repo.Owner, defaultBranch) + return InitRepoCommit(ctx, tmpDir, repo, repo.Owner, defaultBranch) } func generateGitContent(ctx context.Context, repo, templateRepo, generateRepo *repo_model.Repository) (err error) { @@ -356,7 +356,7 @@ func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templ } } - if err = checkInitRepository(ctx, owner.Name, generateRepo.Name); err != nil { + if err = CheckInitRepository(ctx, owner.Name, generateRepo.Name); err != nil { return generateRepo, err } diff --git a/modules/repository/hooks.go b/modules/repository/hooks.go index a95b9c2e99..daab7c3091 100644 --- a/modules/repository/hooks.go +++ b/modules/repository/hooks.go @@ -108,12 +108,7 @@ done } // CreateDelegateHooks creates all the hooks scripts for the repo -func CreateDelegateHooks(repoPath string) error { - return createDelegateHooks(repoPath) -} - -// createDelegateHooks creates all the hooks scripts for the repo -func createDelegateHooks(repoPath string) (err error) { +func CreateDelegateHooks(repoPath string) (err error) { hookNames, hookTpls, giteaHookTpls := getHookTemplates() hookDir := filepath.Join(repoPath, "hooks") diff --git a/modules/repository/init.go b/modules/repository/init.go index 84648f45eb..6f791f742b 100644 --- a/modules/repository/init.go +++ b/modules/repository/init.go @@ -4,7 +4,6 @@ package repository import ( - "bytes" "context" "fmt" "os" @@ -21,7 +20,6 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/options" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates/vars" "code.gitea.io/gitea/modules/util" asymkey_service "code.gitea.io/gitea/services/asymkey" ) @@ -126,95 +124,8 @@ func LoadRepoConfig() error { return nil } -func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir, repoPath string, opts CreateRepoOptions) error { - commitTimeStr := time.Now().Format(time.RFC3339) - authorSig := repo.Owner.NewGitSig() - - // Because this may call hooks we should pass in the environment - env := append(os.Environ(), - "GIT_AUTHOR_NAME="+authorSig.Name, - "GIT_AUTHOR_EMAIL="+authorSig.Email, - "GIT_AUTHOR_DATE="+commitTimeStr, - "GIT_COMMITTER_NAME="+authorSig.Name, - "GIT_COMMITTER_EMAIL="+authorSig.Email, - "GIT_COMMITTER_DATE="+commitTimeStr, - ) - - // Clone to temporary path and do the init commit. - if stdout, _, err := git.NewCommand(ctx, "clone").AddDynamicArguments(repoPath, tmpDir). - SetDescription(fmt.Sprintf("prepareRepoCommit (git clone): %s to %s", repoPath, tmpDir)). - RunStdString(&git.RunOpts{Dir: "", Env: env}); err != nil { - log.Error("Failed to clone from %v into %s: stdout: %s\nError: %v", repo, tmpDir, stdout, err) - return fmt.Errorf("git clone: %w", err) - } - - // README - data, err := options.Readme(opts.Readme) - if err != nil { - return fmt.Errorf("GetRepoInitFile[%s]: %w", opts.Readme, err) - } - - cloneLink := repo.CloneLink() - match := map[string]string{ - "Name": repo.Name, - "Description": repo.Description, - "CloneURL.SSH": cloneLink.SSH, - "CloneURL.HTTPS": cloneLink.HTTPS, - "OwnerName": repo.OwnerName, - } - res, err := vars.Expand(string(data), match) - if err != nil { - // here we could just log the error and continue the rendering - log.Error("unable to expand template vars for repo README: %s, err: %v", opts.Readme, err) - } - if err = os.WriteFile(filepath.Join(tmpDir, "README.md"), - []byte(res), 0o644); err != nil { - return fmt.Errorf("write README.md: %w", err) - } - - // .gitignore - if len(opts.Gitignores) > 0 { - var buf bytes.Buffer - names := strings.Split(opts.Gitignores, ",") - for _, name := range names { - data, err = options.Gitignore(name) - if err != nil { - return fmt.Errorf("GetRepoInitFile[%s]: %w", name, err) - } - buf.WriteString("# ---> " + name + "\n") - buf.Write(data) - buf.WriteString("\n") - } - - if buf.Len() > 0 { - if err = os.WriteFile(filepath.Join(tmpDir, ".gitignore"), buf.Bytes(), 0o644); err != nil { - return fmt.Errorf("write .gitignore: %w", err) - } - } - } - - // LICENSE - if len(opts.License) > 0 { - data, err = getLicense(opts.License, &licenseValues{ - Owner: repo.OwnerName, - Email: authorSig.Email, - Repo: repo.Name, - Year: time.Now().Format("2006"), - }) - if err != nil { - return fmt.Errorf("getLicense[%s]: %w", opts.License, err) - } - - if err = os.WriteFile(filepath.Join(tmpDir, "LICENSE"), data, 0o644); err != nil { - return fmt.Errorf("write LICENSE: %w", err) - } - } - - return nil -} - -// initRepoCommit temporarily changes with work directory. -func initRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Repository, u *user_model.User, defaultBranch string) (err error) { +// InitRepoCommit temporarily changes with work directory. +func InitRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Repository, u *user_model.User, defaultBranch string) (err error) { commitTimeStr := time.Now().Format(time.RFC3339) sig := u.NewGitSig() @@ -277,7 +188,7 @@ func initRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Reposi return nil } -func checkInitRepository(ctx context.Context, owner, name string) (err error) { +func CheckInitRepository(ctx context.Context, owner, name string) (err error) { // Somehow the directory could exist. repoPath := repo_model.RepoPath(owner, name) isExist, err := util.IsExist(repoPath) @@ -295,77 +206,12 @@ func checkInitRepository(ctx context.Context, owner, name string) (err error) { // Init git bare new repository. if err = git.InitRepository(ctx, repoPath, true); err != nil { return fmt.Errorf("git.InitRepository: %w", err) - } else if err = createDelegateHooks(repoPath); err != nil { + } else if err = CreateDelegateHooks(repoPath); err != nil { return fmt.Errorf("createDelegateHooks: %w", err) } return nil } -// InitRepository initializes README and .gitignore if needed. -func initRepository(ctx context.Context, repoPath string, u *user_model.User, repo *repo_model.Repository, opts CreateRepoOptions) (err error) { - if err = checkInitRepository(ctx, repo.OwnerName, repo.Name); err != nil { - return err - } - - // Initialize repository according to user's choice. - if opts.AutoInit { - tmpDir, err := os.MkdirTemp(os.TempDir(), "gitea-"+repo.Name) - if err != nil { - return fmt.Errorf("Failed to create temp dir for repository %s: %w", repo.RepoPath(), err) - } - defer func() { - if err := util.RemoveAll(tmpDir); err != nil { - log.Warn("Unable to remove temporary directory: %s: Error: %v", tmpDir, err) - } - }() - - if err = prepareRepoCommit(ctx, repo, tmpDir, repoPath, opts); err != nil { - return fmt.Errorf("prepareRepoCommit: %w", err) - } - - // Apply changes and commit. - if err = initRepoCommit(ctx, tmpDir, repo, u, opts.DefaultBranch); err != nil { - return fmt.Errorf("initRepoCommit: %w", err) - } - } - - // Re-fetch the repository from database before updating it (else it would - // override changes that were done earlier with sql) - if repo, err = repo_model.GetRepositoryByID(ctx, repo.ID); err != nil { - return fmt.Errorf("getRepositoryByID: %w", err) - } - - if !opts.AutoInit { - repo.IsEmpty = true - } - - repo.DefaultBranch = setting.Repository.DefaultBranch - - if len(opts.DefaultBranch) > 0 { - repo.DefaultBranch = opts.DefaultBranch - gitRepo, err := git.OpenRepository(ctx, repo.RepoPath()) - if err != nil { - return fmt.Errorf("openRepository: %w", err) - } - defer gitRepo.Close() - if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil { - return fmt.Errorf("setDefaultBranch: %w", err) - } - - if !repo.IsEmpty { - if _, err := SyncRepoBranches(ctx, repo.ID, u.ID); err != nil { - return fmt.Errorf("SyncRepoBranches: %w", err) - } - } - } - - if err = UpdateRepository(ctx, repo, false); err != nil { - return fmt.Errorf("updateRepository: %w", err) - } - - return nil -} - // InitializeLabels adds a label set to a repository using a template func InitializeLabels(ctx context.Context, id int64, labelTemplate string, isOrg bool) error { list, err := LoadTemplateLabelsByDisplayName(labelTemplate) diff --git a/modules/repository/license.go b/modules/repository/license.go index 5b188a041e..6ac3547e7b 100644 --- a/modules/repository/license.go +++ b/modules/repository/license.go @@ -13,14 +13,14 @@ import ( "code.gitea.io/gitea/modules/options" ) -type licenseValues struct { +type LicenseValues struct { Owner string Email string Repo string Year string } -func getLicense(name string, values *licenseValues) ([]byte, error) { +func GetLicense(name string, values *LicenseValues) ([]byte, error) { data, err := options.License(name) if err != nil { return nil, fmt.Errorf("GetRepoInitFile[%s]: %w", name, err) @@ -28,7 +28,7 @@ func getLicense(name string, values *licenseValues) ([]byte, error) { return fillLicensePlaceholder(name, values, data), nil } -func fillLicensePlaceholder(name string, values *licenseValues, origin []byte) []byte { +func fillLicensePlaceholder(name string, values *LicenseValues, origin []byte) []byte { placeholder := getLicensePlaceholder(name) scanner := bufio.NewScanner(bytes.NewReader(origin)) diff --git a/modules/repository/license_test.go b/modules/repository/license_test.go index 13c865693c..3b0cfa1eed 100644 --- a/modules/repository/license_test.go +++ b/modules/repository/license_test.go @@ -13,7 +13,7 @@ import ( func Test_getLicense(t *testing.T) { type args struct { name string - values *licenseValues + values *LicenseValues } tests := []struct { name string @@ -25,7 +25,7 @@ func Test_getLicense(t *testing.T) { name: "regular", args: args{ name: "MIT", - values: &licenseValues{Owner: "Gitea", Year: "2023"}, + values: &LicenseValues{Owner: "Gitea", Year: "2023"}, }, want: `MIT License @@ -49,11 +49,11 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := getLicense(tt.args.name, tt.args.values) - if !tt.wantErr(t, err, fmt.Sprintf("getLicense(%v, %v)", tt.args.name, tt.args.values)) { + got, err := GetLicense(tt.args.name, tt.args.values) + if !tt.wantErr(t, err, fmt.Sprintf("GetLicense(%v, %v)", tt.args.name, tt.args.values)) { return } - assert.Equalf(t, tt.want, string(got), "getLicense(%v, %v)", tt.args.name, tt.args.values) + assert.Equalf(t, tt.want, string(got), "GetLicense(%v, %v)", tt.args.name, tt.args.values) }) } } @@ -61,7 +61,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI func Test_fillLicensePlaceholder(t *testing.T) { type args struct { name string - values *licenseValues + values *LicenseValues origin string } tests := []struct { @@ -73,7 +73,7 @@ func Test_fillLicensePlaceholder(t *testing.T) { name: "owner", args: args{ name: "regular", - values: &licenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"}, + values: &LicenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"}, origin: ` @@ -104,7 +104,7 @@ Gitea name: "email", args: args{ name: "regular", - values: &licenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"}, + values: &LicenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"}, origin: ` [EMAIL] `, @@ -117,7 +117,7 @@ teabot@gitea.io name: "repo", args: args{ name: "regular", - values: &licenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"}, + values: &LicenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"}, origin: ` @@ -132,7 +132,7 @@ gitea name: "year", args: args{ name: "regular", - values: &licenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"}, + values: &LicenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"}, origin: ` [YEAR] @@ -155,7 +155,7 @@ gitea name: "0BSD", args: args{ name: "0BSD", - values: &licenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"}, + values: &LicenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"}, origin: ` Copyright (C) YEAR by AUTHOR EMAIL diff --git a/modules/repository/repo.go b/modules/repository/repo.go index 6a11315cc4..6bf88e7752 100644 --- a/modules/repository/repo.go +++ b/modules/repository/repo.go @@ -256,11 +256,11 @@ func cleanUpMigrateGitConfig(ctx context.Context, repoPath string) error { // CleanUpMigrateInfo finishes migrating repository and/or wiki with things that don't need to be done for mirrors. func CleanUpMigrateInfo(ctx context.Context, repo *repo_model.Repository) (*repo_model.Repository, error) { repoPath := repo.RepoPath() - if err := createDelegateHooks(repoPath); err != nil { + if err := CreateDelegateHooks(repoPath); err != nil { return repo, fmt.Errorf("createDelegateHooks: %w", err) } if repo.HasWiki() { - if err := createDelegateHooks(repo.WikiPath()); err != nil { + if err := CreateDelegateHooks(repo.WikiPath()); err != nil { return repo, fmt.Errorf("createDelegateHooks.(wiki): %w", err) } } diff --git a/modules/setting/actions.go b/modules/setting/actions.go index a13330dcd1..bfc502c0cb 100644 --- a/modules/setting/actions.go +++ b/modules/setting/actions.go @@ -13,10 +13,11 @@ import ( // Actions settings var ( Actions = struct { - LogStorage *Storage // how the created logs should be stored - ArtifactStorage *Storage // how the created artifacts should be stored - Enabled bool - DefaultActionsURL defaultActionsURL `ini:"DEFAULT_ACTIONS_URL"` + LogStorage *Storage // how the created logs should be stored + ArtifactStorage *Storage // how the created artifacts should be stored + ArtifactRetentionDays int64 `ini:"ARTIFACT_RETENTION_DAYS"` + Enabled bool + DefaultActionsURL defaultActionsURL `ini:"DEFAULT_ACTIONS_URL"` }{ Enabled: false, DefaultActionsURL: defaultActionsURLGitHub, @@ -76,5 +77,10 @@ func loadActionsFrom(rootCfg ConfigProvider) error { Actions.ArtifactStorage, err = getStorage(rootCfg, "actions_artifacts", "", actionsSec) + // default to 90 days in Github Actions + if Actions.ArtifactRetentionDays <= 0 { + Actions.ArtifactRetentionDays = 90 + } + return err } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 886152cbae..fad2665f5a 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1764,6 +1764,7 @@ pulls.rebase_conflict_summary = Error Message pulls.unrelated_histories = Merge Failed: The merge head and base do not share a common history. Hint: Try a different strategy pulls.merge_out_of_date = Merge Failed: Whilst generating the merge, the base was updated. Hint: Try again. pulls.head_out_of_date = Merge Failed: Whilst generating the merge, the head was updated. Hint: Try again. +pulls.has_merged = Failed: The pull request has been merged, you cannot merge again or change the target branch. pulls.push_rejected = Merge Failed: The push was rejected. Review the Git Hooks for this repository. pulls.push_rejected_summary = Full Rejection Message pulls.push_rejected_no_message = Merge Failed: The push was rejected but there was no remote message.
Review the Git Hooks for this repository @@ -2730,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/package-lock.json b/package-lock.json index dda23bcbd4..db0480a5e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,6 @@ "escape-goat": "4.0.0", "fast-glob": "3.3.1", "jquery": "3.7.1", - "jquery.are-you-sure": "1.9.0", "katex": "0.16.8", "license-checker-webpack-plugin": "0.2.1", "lightningcss-loader": "2.1.0", @@ -6466,17 +6465,6 @@ "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" }, - "node_modules/jquery.are-you-sure": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/jquery.are-you-sure/-/jquery.are-you-sure-1.9.0.tgz", - "integrity": "sha512-2r0uFx8CyAopjeHGOdvvwpFP921TnW1+v1uJXcAWQYHYGB1tryTDhQY+5u6HsVeMwbWiRTKVZFWnLaFpDvIqZQ==", - "dependencies": { - "jquery": ">=1.4.2" - }, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/js-levenshtein-esm": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/js-levenshtein-esm/-/js-levenshtein-esm-1.2.0.tgz", diff --git a/package.json b/package.json index e4f1743feb..224a1422ec 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "escape-goat": "4.0.0", "fast-glob": "3.3.1", "jquery": "3.7.1", - "jquery.are-you-sure": "1.9.0", "katex": "0.16.8", "license-checker-webpack-plugin": "0.2.1", "lightningcss-loader": "2.1.0", diff --git a/routers/api/actions/artifacts.go b/routers/api/actions/artifacts.go index 946ea11e75..c45dc667af 100644 --- a/routers/api/actions/artifacts.go +++ b/routers/api/actions/artifacts.go @@ -170,8 +170,9 @@ func (ar artifactRoutes) buildArtifactURL(runID int64, artifactHash, suffix stri } type getUploadArtifactRequest struct { - Type string - Name string + Type string + Name string + RetentionDays int64 } type getUploadArtifactResponse struct { @@ -192,10 +193,16 @@ func (ar artifactRoutes) getUploadArtifactURL(ctx *ArtifactContext) { return } + // set retention days + retentionQuery := "" + if req.RetentionDays > 0 { + retentionQuery = fmt.Sprintf("?retentionDays=%d", req.RetentionDays) + } + // use md5(artifact_name) to create upload url artifactHash := fmt.Sprintf("%x", md5.Sum([]byte(req.Name))) resp := getUploadArtifactResponse{ - FileContainerResourceURL: ar.buildArtifactURL(runID, artifactHash, "upload"), + FileContainerResourceURL: ar.buildArtifactURL(runID, artifactHash, "upload"+retentionQuery), } log.Debug("[artifact] get upload url: %s", resp.FileContainerResourceURL) ctx.JSON(http.StatusOK, resp) @@ -219,8 +226,21 @@ func (ar artifactRoutes) uploadArtifact(ctx *ArtifactContext) { return } + // get artifact retention days + expiredDays := setting.Actions.ArtifactRetentionDays + if queryRetentionDays := ctx.Req.URL.Query().Get("retentionDays"); queryRetentionDays != "" { + expiredDays, err = strconv.ParseInt(queryRetentionDays, 10, 64) + if err != nil { + log.Error("Error parse retention days: %v", err) + ctx.Error(http.StatusBadRequest, "Error parse retention days") + return + } + } + log.Debug("[artifact] upload chunk, name: %s, path: %s, size: %d, retention days: %d", + artifactName, artifactPath, fileRealTotalSize, expiredDays) + // create or get artifact with name and path - artifact, err := actions.CreateArtifact(ctx, task, artifactName, artifactPath) + artifact, err := actions.CreateArtifact(ctx, task, artifactName, artifactPath, expiredDays) if err != nil { log.Error("Error create or get artifact: %v", err) ctx.Error(http.StatusInternalServerError, "Error create or get artifact") diff --git a/routers/api/actions/artifacts_chunks.go b/routers/api/actions/artifacts_chunks.go index 30d31b4d75..458d671cff 100644 --- a/routers/api/actions/artifacts_chunks.go +++ b/routers/api/actions/artifacts_chunks.go @@ -179,7 +179,7 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st // save storage path to artifact log.Debug("[artifact] merge chunks to artifact: %d, %s", artifact.ID, storagePath) artifact.StoragePath = storagePath - artifact.Status = actions.ArtifactStatusUploadConfirmed + artifact.Status = int64(actions.ArtifactStatusUploadConfirmed) if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { return fmt.Errorf("update artifact error: %v", err) } diff --git a/routers/api/v1/admin/adopt.go b/routers/api/v1/admin/adopt.go index ccd8be9171..bf030eb222 100644 --- a/routers/api/v1/admin/adopt.go +++ b/routers/api/v1/admin/adopt.go @@ -9,7 +9,6 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/context" - repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/api/v1/utils" repo_service "code.gitea.io/gitea/services/repository" @@ -109,7 +108,7 @@ func AdoptRepository(ctx *context.APIContext) { ctx.NotFound() return } - if _, err := repo_service.AdoptRepository(ctx, ctx.Doer, ctxUser, repo_module.CreateRepoOptions{ + if _, err := repo_service.AdoptRepository(ctx, ctx.Doer, ctxUser, repo_service.CreateRepoOptions{ Name: repoName, IsPrivate: true, }); err != nil { diff --git a/routers/api/v1/repo/migrate.go b/routers/api/v1/repo/migrate.go index dfc9004620..41374831de 100644 --- a/routers/api/v1/repo/migrate.go +++ b/routers/api/v1/repo/migrate.go @@ -22,7 +22,6 @@ import ( "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/log" base "code.gitea.io/gitea/modules/migration" - repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" @@ -31,6 +30,7 @@ import ( "code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/migrations" notify_service "code.gitea.io/gitea/services/notify" + repo_service "code.gitea.io/gitea/services/repository" ) // Migrate migrate remote git repository to gitea @@ -170,7 +170,7 @@ func Migrate(ctx *context.APIContext) { opts.Releases = false } - repo, err := repo_module.CreateRepository(ctx.Doer, repoOwner, repo_module.CreateRepoOptions{ + repo, err := repo_service.CreateRepositoryDirectly(ctx.Doer, repoOwner, repo_service.CreateRepoOptions{ Name: opts.RepoName, Description: opts.Description, OriginalURL: form.CloneAddr, diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 7b0c954a73..29f6a675d4 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -240,7 +240,7 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre return } - repo, err := repo_service.CreateRepository(ctx, ctx.Doer, owner, repo_module.CreateRepoOptions{ + repo, err := repo_service.CreateRepository(ctx, ctx.Doer, owner, repo_service.CreateRepoOptions{ Name: opt.Name, Description: opt.Description, IssueLabels: opt.IssueLabels, diff --git a/routers/web/admin/repos.go b/routers/web/admin/repos.go index d1d0abca02..45c280ef73 100644 --- a/routers/web/admin/repos.go +++ b/routers/web/admin/repos.go @@ -14,7 +14,6 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" - repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/web/explore" @@ -144,7 +143,7 @@ func AdoptOrDeleteRepository(ctx *context.Context) { if has || !isDir { // Fallthrough to failure mode } else if action == "adopt" { - if _, err := repo_service.AdoptRepository(ctx, ctx.Doer, ctxUser, repo_module.CreateRepoOptions{ + if _, err := repo_service.AdoptRepository(ctx, ctx.Doer, ctxUser, repo_service.CreateRepoOptions{ Name: dirSplit[1], IsPrivate: true, }); err != nil { diff --git a/routers/web/org/home.go b/routers/web/org/home.go index 613dff2182..a69fdedba4 100644 --- a/routers/web/org/home.go +++ b/routers/web/org/home.go @@ -152,7 +152,6 @@ func Home(ctx *context.Context) { pager.SetDefaultParams(ctx) pager.AddParam(ctx, "language", "Language") ctx.Data["Page"] = pager - ctx.Data["ContextUser"] = ctx.ContextUser ctx.Data["ShowMemberAndTeamTab"] = ctx.Org.IsMember || len(members) > 0 diff --git a/routers/web/org/members.go b/routers/web/org/members.go index fae8b48128..f963ad55ef 100644 --- a/routers/web/org/members.go +++ b/routers/web/org/members.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + shared_user "code.gitea.io/gitea/routers/web/shared/user" ) const ( @@ -52,6 +53,12 @@ func Members(ctx *context.Context) { return } + err = shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + pager := context.NewPagination(int(total), setting.UI.MembersPagingNum, page, 5) opts.ListOptions.Page = page opts.ListOptions.PageSize = setting.UI.MembersPagingNum @@ -62,7 +69,6 @@ func Members(ctx *context.Context) { } ctx.Data["Page"] = pager ctx.Data["Members"] = members - ctx.Data["ContextUser"] = ctx.ContextUser ctx.Data["MembersIsPublicMember"] = membersIsPublic ctx.Data["MembersIsUserOrgOwner"] = organization.IsUserOrgOwner(members, org.ID) ctx.Data["MembersTwoFaStatus"] = members.GetTwoFaStatus() diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go index 957daab646..0f082a70df 100644 --- a/routers/web/org/setting.go +++ b/routers/web/org/setting.go @@ -20,6 +20,7 @@ import ( repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web" + shared_user "code.gitea.io/gitea/routers/web/shared/user" user_setting "code.gitea.io/gitea/routers/web/user/setting" "code.gitea.io/gitea/services/forms" org_service "code.gitea.io/gitea/services/org" @@ -46,6 +47,13 @@ func Settings(ctx *context.Context) { 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) } @@ -189,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) } @@ -207,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) } @@ -228,5 +248,12 @@ func Labels(ctx *context.Context) { ctx.Data["PageIsOrgSettings"] = true ctx.Data["PageIsOrgSettingsLabels"] = true ctx.Data["LabelTemplateFiles"] = repo_module.LabelTemplateFiles + + err := shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + ctx.HTML(http.StatusOK, tplSettingsLabels) } diff --git a/routers/web/org/setting_oauth2.go b/routers/web/org/setting_oauth2.go index 9bf4280b07..0045bce4c9 100644 --- a/routers/web/org/setting_oauth2.go +++ b/routers/web/org/setting_oauth2.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/setting" + shared_user "code.gitea.io/gitea/routers/web/shared/user" user_setting "code.gitea.io/gitea/routers/web/user/setting" ) @@ -41,6 +42,12 @@ func Applications(ctx *context.Context) { } ctx.Data["Applications"] = apps + err = shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + ctx.HTML(http.StatusOK, tplSettingsApplications) } diff --git a/routers/web/org/setting_packages.go b/routers/web/org/setting_packages.go index 21d25bd90a..796829d34e 100644 --- a/routers/web/org/setting_packages.go +++ b/routers/web/org/setting_packages.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/setting" shared "code.gitea.io/gitea/routers/web/shared/packages" + shared_user "code.gitea.io/gitea/routers/web/shared/user" ) const ( @@ -24,6 +25,12 @@ func Packages(ctx *context.Context) { ctx.Data["PageIsOrgSettings"] = true ctx.Data["PageIsSettingsPackages"] = true + err := shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + shared.SetPackagesContext(ctx, ctx.ContextUser) ctx.HTML(http.StatusOK, tplSettingsPackages) @@ -34,6 +41,12 @@ func PackagesRuleAdd(ctx *context.Context) { ctx.Data["PageIsOrgSettings"] = true ctx.Data["PageIsSettingsPackages"] = true + err := shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + shared.SetRuleAddContext(ctx) ctx.HTML(http.StatusOK, tplSettingsPackagesRuleEdit) @@ -44,6 +57,12 @@ func PackagesRuleEdit(ctx *context.Context) { ctx.Data["PageIsOrgSettings"] = true ctx.Data["PageIsSettingsPackages"] = true + err := shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + shared.SetRuleEditContext(ctx, ctx.ContextUser) ctx.HTML(http.StatusOK, tplSettingsPackagesRuleEdit) @@ -80,6 +99,12 @@ func PackagesRulePreview(ctx *context.Context) { ctx.Data["PageIsOrgSettings"] = true ctx.Data["PageIsSettingsPackages"] = true + err := shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + shared.SetRulePreviewContext(ctx, ctx.ContextUser) ctx.HTML(http.StatusOK, tplSettingsPackagesRulePreview) diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go index 3b07bba713..1e0287fe27 100644 --- a/routers/web/org/teams.go +++ b/routers/web/org/teams.go @@ -25,6 +25,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/utils" + shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/convert" "code.gitea.io/gitea/services/forms" org_service "code.gitea.io/gitea/services/org" @@ -56,7 +57,12 @@ func Teams(ctx *context.Context) { } } ctx.Data["Teams"] = ctx.Org.Teams - ctx.Data["ContextUser"] = ctx.ContextUser + + err := shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } ctx.HTML(http.StatusOK, tplTeams) } diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index e4ca6a7198..a9c2858303 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -486,8 +486,9 @@ type ArtifactsViewResponse struct { } type ArtifactsViewItem struct { - Name string `json:"name"` - Size int64 `json:"size"` + Name string `json:"name"` + Size int64 `json:"size"` + Status string `json:"status"` } func ArtifactsView(ctx *context_module.Context) { @@ -510,9 +511,14 @@ func ArtifactsView(ctx *context_module.Context) { Artifacts: make([]*ArtifactsViewItem, 0, len(artifacts)), } for _, art := range artifacts { + status := "completed" + if art.Status == int64(actions_model.ArtifactStatusExpired) { + status = "expired" + } artifactsResponse.Artifacts = append(artifactsResponse.Artifacts, &ArtifactsViewItem{ - Name: art.ArtifactName, - Size: art.FileSize, + Name: art.ArtifactName, + Size: art.FileSize, + Status: status, }) } ctx.JSON(http.StatusOK, artifactsResponse) diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go index 6ad2f71b5c..ac9e64d774 100644 --- a/routers/web/repo/packages.go +++ b/routers/web/repo/packages.go @@ -58,7 +58,6 @@ func Packages(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("packages.title") ctx.Data["IsPackagesPage"] = true - ctx.Data["ContextUser"] = ctx.ContextUser ctx.Data["Query"] = query ctx.Data["PackageType"] = packageType ctx.Data["AvailableTypes"] = packages.TypeList diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index 4409381bc5..12cd477926 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -275,7 +275,7 @@ func CreatePost(ctx *context.Context) { return } } else { - repo, err = repo_service.CreateRepository(ctx, ctx.Doer, ctxUser, repo_module.CreateRepoOptions{ + repo, err = repo_service.CreateRepository(ctx, ctx.Doer, ctxUser, repo_service.CreateRepoOptions{ Name: form.RepoName, Description: form.Description, Gitignores: form.Gitignores, @@ -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) } diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go index 9b1918ed16..6273e11fc5 100644 --- a/routers/web/shared/user/header.go +++ b/routers/web/shared/user/header.go @@ -22,7 +22,6 @@ import ( func prepareContextForCommonProfile(ctx *context.Context) { ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled - ctx.Data["ContextUser"] = ctx.ContextUser ctx.Data["EnableFeed"] = setting.Other.EnableFeed ctx.Data["FeedURL"] = ctx.ContextUser.HomeLink() } diff --git a/routers/web/user/code.go b/routers/web/user/code.go index 033f65c9c0..29b8b91c89 100644 --- a/routers/web/user/code.go +++ b/routers/web/user/code.go @@ -30,7 +30,6 @@ func CodeSearch(ctx *context.Context) { ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled ctx.Data["Title"] = ctx.Tr("explore.code") - ctx.Data["ContextUser"] = ctx.ContextUser language := ctx.FormTrim("l") keyword := ctx.FormTrim("q") diff --git a/routers/web/user/package.go b/routers/web/user/package.go index 5c65246480..03eeac7348 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/adopt.go b/routers/web/user/setting/adopt.go index 01668c3954..decb35c1e1 100644 --- a/routers/web/user/setting/adopt.go +++ b/routers/web/user/setting/adopt.go @@ -9,7 +9,6 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/context" - repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" repo_service "code.gitea.io/gitea/services/repository" @@ -45,7 +44,7 @@ func AdoptOrDeleteRepository(ctx *context.Context) { if has || !isDir { // Fallthrough to failure mode } else if action == "adopt" && allowAdopt { - if _, err := repo_service.AdoptRepository(ctx, ctxUser, ctxUser, repo_module.CreateRepoOptions{ + if _, err := repo_service.AdoptRepository(ctx, ctxUser, ctxUser, repo_service.CreateRepoOptions{ Name: dir, IsPrivate: true, }); err != nil { diff --git a/routers/web/user/setting/oauth2_common.go b/routers/web/user/setting/oauth2_common.go index 641cc1fd9f..5786118f50 100644 --- a/routers/web/user/setting/oauth2_common.go +++ b/routers/web/user/setting/oauth2_common.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" + shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/forms" ) @@ -25,6 +26,15 @@ type OAuth2CommonHandlers struct { func (oa *OAuth2CommonHandlers) renderEditPage(ctx *context.Context) { app := ctx.Data["App"].(*auth.OAuth2Application) ctx.Data["FormActionPath"] = fmt.Sprintf("%s/%d", oa.BasePathEditPrefix, app.ID) + + if ctx.ContextUser.IsOrganization() { + err := shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + } + ctx.HTML(http.StatusOK, oa.TplAppEdit) } diff --git a/services/actions/cleanup.go b/services/actions/cleanup.go new file mode 100644 index 0000000000..785eeb5838 --- /dev/null +++ b/services/actions/cleanup.go @@ -0,0 +1,42 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + "time" + + "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/storage" +) + +// Cleanup removes expired actions logs, data and artifacts +func Cleanup(taskCtx context.Context, olderThan time.Duration) error { + // TODO: clean up expired actions logs + + // clean up expired artifacts + return CleanupArtifacts(taskCtx) +} + +// CleanupArtifacts removes expired artifacts and set records expired status +func CleanupArtifacts(taskCtx context.Context) error { + artifacts, err := actions.ListNeedExpiredArtifacts(taskCtx) + if err != nil { + return err + } + log.Info("Found %d expired artifacts", len(artifacts)) + for _, artifact := range artifacts { + if err := storage.ActionsArtifacts.Delete(artifact.StoragePath); err != nil { + log.Error("Cannot delete artifact %d: %v", artifact.ID, err) + continue + } + if err := actions.SetArtifactExpired(taskCtx, artifact.ID); err != nil { + log.Error("Cannot set artifact %d expired: %v", artifact.ID, err) + continue + } + log.Info("Artifact %d set expired", artifact.ID) + } + return nil +} diff --git a/services/context/user.go b/services/context/user.go index 62d2dc0aa2..81c2746819 100644 --- a/services/context/user.go +++ b/services/context/user.go @@ -27,6 +27,7 @@ func UserAssignmentWeb() func(ctx *context.Context) { } } ctx.ContextUser = userAssignment(ctx.Base, ctx.Doer, errorFn) + ctx.Data["ContextUser"] = ctx.ContextUser } } diff --git a/services/cron/tasks_basic.go b/services/cron/tasks_basic.go index 2a213ae515..3869382d22 100644 --- a/services/cron/tasks_basic.go +++ b/services/cron/tasks_basic.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/services/auth" "code.gitea.io/gitea/services/migrations" mirror_service "code.gitea.io/gitea/services/mirror" @@ -156,6 +157,20 @@ func registerCleanupPackages() { }) } +func registerActionsCleanup() { + RegisterTaskFatal("cleanup_actions", &OlderThanConfig{ + BaseConfig: BaseConfig{ + Enabled: true, + RunAtStart: true, + Schedule: "@midnight", + }, + OlderThan: 24 * time.Hour, + }, func(ctx context.Context, _ *user_model.User, config Config) error { + realConfig := config.(*OlderThanConfig) + return actions.Cleanup(ctx, realConfig.OlderThan) + }) +} + func initBasicTasks() { if setting.Mirror.Enabled { registerUpdateMirrorTask() @@ -172,4 +187,7 @@ func initBasicTasks() { if setting.Packages.Enabled { registerCleanupPackages() } + if setting.Actions.Enabled { + registerActionsCleanup() + } } diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go index ee7fc57851..a4a3af82e7 100644 --- a/services/migrations/gitea_uploader.go +++ b/services/migrations/gitea_uploader.go @@ -31,6 +31,7 @@ import ( "code.gitea.io/gitea/modules/uri" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/pull" + repo_service "code.gitea.io/gitea/services/repository" "github.com/google/uuid" ) @@ -99,7 +100,7 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate var r *repo_model.Repository if opts.MigrateToRepoID <= 0 { - r, err = repo_module.CreateRepository(g.doer, owner, repo_module.CreateRepoOptions{ + r, err = repo_service.CreateRepositoryDirectly(g.doer, owner, repo_service.CreateRepoOptions{ Name: g.repoName, Description: repo.Description, OriginalURL: repo.OriginalURL, diff --git a/services/packages/cargo/index.go b/services/packages/cargo/index.go index 867cd796d3..572f5e1f5b 100644 --- a/services/packages/cargo/index.go +++ b/services/packages/cargo/index.go @@ -19,10 +19,10 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/json" cargo_module "code.gitea.io/gitea/modules/packages/cargo" - repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" + repo_service "code.gitea.io/gitea/services/repository" files_service "code.gitea.io/gitea/services/repository/files" ) @@ -206,7 +206,7 @@ func getOrCreateIndexRepository(ctx context.Context, doer, owner *user_model.Use repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner.Name, IndexRepositoryName) if err != nil { if errors.Is(err, util.ErrNotExist) { - repo, err = repo_module.CreateRepository(doer, owner, repo_module.CreateRepoOptions{ + repo, err = repo_service.CreateRepositoryDirectly(doer, owner, repo_service.CreateRepoOptions{ Name: IndexRepositoryName, }) if err != nil { diff --git a/services/repository/adopt.go b/services/repository/adopt.go index f225538faf..00dce7295e 100644 --- a/services/repository/adopt.go +++ b/services/repository/adopt.go @@ -27,7 +27,7 @@ import ( ) // AdoptRepository adopts pre-existing repository files for the user/organization. -func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts repo_module.CreateRepoOptions) (*repo_model.Repository, error) { +func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts CreateRepoOptions) (*repo_model.Repository, error) { if !doer.IsAdmin && !u.CanCreateRepo() { return nil, repo_model.ErrReachLimitOfRepo{ Limit: u.MaxRepoCreation, diff --git a/services/repository/create.go b/services/repository/create.go new file mode 100644 index 0000000000..a5d521e353 --- /dev/null +++ b/services/repository/create.go @@ -0,0 +1,315 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repository + +import ( + "bytes" + "context" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/options" + repo_module "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/templates/vars" + "code.gitea.io/gitea/modules/util" +) + +// CreateRepoOptions contains the create repository options +type CreateRepoOptions struct { + Name string + Description string + OriginalURL string + GitServiceType api.GitServiceType + Gitignores string + IssueLabels string + License string + Readme string + DefaultBranch string + IsPrivate bool + IsMirror bool + IsTemplate bool + AutoInit bool + Status repo_model.RepositoryStatus + TrustModel repo_model.TrustModelType + MirrorInterval string +} + +func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir, repoPath string, opts CreateRepoOptions) error { + commitTimeStr := time.Now().Format(time.RFC3339) + authorSig := repo.Owner.NewGitSig() + + // Because this may call hooks we should pass in the environment + env := append(os.Environ(), + "GIT_AUTHOR_NAME="+authorSig.Name, + "GIT_AUTHOR_EMAIL="+authorSig.Email, + "GIT_AUTHOR_DATE="+commitTimeStr, + "GIT_COMMITTER_NAME="+authorSig.Name, + "GIT_COMMITTER_EMAIL="+authorSig.Email, + "GIT_COMMITTER_DATE="+commitTimeStr, + ) + + // Clone to temporary path and do the init commit. + if stdout, _, err := git.NewCommand(ctx, "clone").AddDynamicArguments(repoPath, tmpDir). + SetDescription(fmt.Sprintf("prepareRepoCommit (git clone): %s to %s", repoPath, tmpDir)). + RunStdString(&git.RunOpts{Dir: "", Env: env}); err != nil { + log.Error("Failed to clone from %v into %s: stdout: %s\nError: %v", repo, tmpDir, stdout, err) + return fmt.Errorf("git clone: %w", err) + } + + // README + data, err := options.Readme(opts.Readme) + if err != nil { + return fmt.Errorf("GetRepoInitFile[%s]: %w", opts.Readme, err) + } + + cloneLink := repo.CloneLink() + match := map[string]string{ + "Name": repo.Name, + "Description": repo.Description, + "CloneURL.SSH": cloneLink.SSH, + "CloneURL.HTTPS": cloneLink.HTTPS, + "OwnerName": repo.OwnerName, + } + res, err := vars.Expand(string(data), match) + if err != nil { + // here we could just log the error and continue the rendering + log.Error("unable to expand template vars for repo README: %s, err: %v", opts.Readme, err) + } + if err = os.WriteFile(filepath.Join(tmpDir, "README.md"), + []byte(res), 0o644); err != nil { + return fmt.Errorf("write README.md: %w", err) + } + + // .gitignore + if len(opts.Gitignores) > 0 { + var buf bytes.Buffer + names := strings.Split(opts.Gitignores, ",") + for _, name := range names { + data, err = options.Gitignore(name) + if err != nil { + return fmt.Errorf("GetRepoInitFile[%s]: %w", name, err) + } + buf.WriteString("# ---> " + name + "\n") + buf.Write(data) + buf.WriteString("\n") + } + + if buf.Len() > 0 { + if err = os.WriteFile(filepath.Join(tmpDir, ".gitignore"), buf.Bytes(), 0o644); err != nil { + return fmt.Errorf("write .gitignore: %w", err) + } + } + } + + // LICENSE + if len(opts.License) > 0 { + data, err = repo_module.GetLicense(opts.License, &repo_module.LicenseValues{ + Owner: repo.OwnerName, + Email: authorSig.Email, + Repo: repo.Name, + Year: time.Now().Format("2006"), + }) + if err != nil { + return fmt.Errorf("getLicense[%s]: %w", opts.License, err) + } + + if err = os.WriteFile(filepath.Join(tmpDir, "LICENSE"), data, 0o644); err != nil { + return fmt.Errorf("write LICENSE: %w", err) + } + } + + return nil +} + +// InitRepository initializes README and .gitignore if needed. +func initRepository(ctx context.Context, repoPath string, u *user_model.User, repo *repo_model.Repository, opts CreateRepoOptions) (err error) { + if err = repo_module.CheckInitRepository(ctx, repo.OwnerName, repo.Name); err != nil { + return err + } + + // Initialize repository according to user's choice. + if opts.AutoInit { + tmpDir, err := os.MkdirTemp(os.TempDir(), "gitea-"+repo.Name) + if err != nil { + return fmt.Errorf("Failed to create temp dir for repository %s: %w", repo.RepoPath(), err) + } + defer func() { + if err := util.RemoveAll(tmpDir); err != nil { + log.Warn("Unable to remove temporary directory: %s: Error: %v", tmpDir, err) + } + }() + + if err = prepareRepoCommit(ctx, repo, tmpDir, repoPath, opts); err != nil { + return fmt.Errorf("prepareRepoCommit: %w", err) + } + + // Apply changes and commit. + if err = repo_module.InitRepoCommit(ctx, tmpDir, repo, u, opts.DefaultBranch); err != nil { + return fmt.Errorf("initRepoCommit: %w", err) + } + } + + // Re-fetch the repository from database before updating it (else it would + // override changes that were done earlier with sql) + if repo, err = repo_model.GetRepositoryByID(ctx, repo.ID); err != nil { + return fmt.Errorf("getRepositoryByID: %w", err) + } + + if !opts.AutoInit { + repo.IsEmpty = true + } + + repo.DefaultBranch = setting.Repository.DefaultBranch + + if len(opts.DefaultBranch) > 0 { + repo.DefaultBranch = opts.DefaultBranch + gitRepo, err := git.OpenRepository(ctx, repo.RepoPath()) + if err != nil { + return fmt.Errorf("openRepository: %w", err) + } + defer gitRepo.Close() + if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil { + return fmt.Errorf("setDefaultBranch: %w", err) + } + + if !repo.IsEmpty { + if _, err := repo_module.SyncRepoBranches(ctx, repo.ID, u.ID); err != nil { + return fmt.Errorf("SyncRepoBranches: %w", err) + } + } + } + + if err = UpdateRepository(ctx, repo, false); err != nil { + return fmt.Errorf("updateRepository: %w", err) + } + + return nil +} + +// CreateRepositoryDirectly creates a repository for the user/organization. +func CreateRepositoryDirectly(doer, u *user_model.User, opts CreateRepoOptions) (*repo_model.Repository, error) { + if !doer.IsAdmin && !u.CanCreateRepo() { + return nil, repo_model.ErrReachLimitOfRepo{ + Limit: u.MaxRepoCreation, + } + } + + if len(opts.DefaultBranch) == 0 { + opts.DefaultBranch = setting.Repository.DefaultBranch + } + + // Check if label template exist + if len(opts.IssueLabels) > 0 { + if _, err := repo_module.LoadTemplateLabelsByDisplayName(opts.IssueLabels); err != nil { + return nil, err + } + } + + repo := &repo_model.Repository{ + OwnerID: u.ID, + Owner: u, + OwnerName: u.Name, + Name: opts.Name, + LowerName: strings.ToLower(opts.Name), + Description: opts.Description, + OriginalURL: opts.OriginalURL, + OriginalServiceType: opts.GitServiceType, + IsPrivate: opts.IsPrivate, + IsFsckEnabled: !opts.IsMirror, + IsTemplate: opts.IsTemplate, + CloseIssuesViaCommitInAnyBranch: setting.Repository.DefaultCloseIssuesViaCommitsInAnyBranch, + Status: opts.Status, + IsEmpty: !opts.AutoInit, + TrustModel: opts.TrustModel, + IsMirror: opts.IsMirror, + DefaultBranch: opts.DefaultBranch, + } + + var rollbackRepo *repo_model.Repository + + if err := db.WithTx(db.DefaultContext, func(ctx context.Context) error { + if err := repo_module.CreateRepositoryByExample(ctx, doer, u, repo, false, false); err != nil { + return err + } + + // No need for init mirror. + if opts.IsMirror { + return nil + } + + repoPath := repo_model.RepoPath(u.Name, repo.Name) + isExist, err := util.IsExist(repoPath) + if err != nil { + log.Error("Unable to check if %s exists. Error: %v", repoPath, err) + return err + } + if isExist { + // repo already exists - We have two or three options. + // 1. We fail stating that the directory exists + // 2. We create the db repository to go with this data and adopt the git repo + // 3. We delete it and start afresh + // + // Previously Gitea would just delete and start afresh - this was naughty. + // So we will now fail and delegate to other functionality to adopt or delete + log.Error("Files already exist in %s and we are not going to adopt or delete.", repoPath) + return repo_model.ErrRepoFilesAlreadyExist{ + Uname: u.Name, + Name: repo.Name, + } + } + + if err = initRepository(ctx, repoPath, doer, repo, opts); err != nil { + if err2 := util.RemoveAll(repoPath); err2 != nil { + log.Error("initRepository: %v", err) + return fmt.Errorf( + "delete repo directory %s/%s failed(2): %v", u.Name, repo.Name, err2) + } + return fmt.Errorf("initRepository: %w", err) + } + + // Initialize Issue Labels if selected + if len(opts.IssueLabels) > 0 { + if err = repo_module.InitializeLabels(ctx, repo.ID, opts.IssueLabels, false); err != nil { + rollbackRepo = repo + rollbackRepo.OwnerID = u.ID + return fmt.Errorf("InitializeLabels: %w", err) + } + } + + if err := repo_module.CheckDaemonExportOK(ctx, repo); err != nil { + return fmt.Errorf("checkDaemonExportOK: %w", err) + } + + if stdout, _, err := git.NewCommand(ctx, "update-server-info"). + SetDescription(fmt.Sprintf("CreateRepository(git update-server-info): %s", repoPath)). + RunStdString(&git.RunOpts{Dir: repoPath}); err != nil { + log.Error("CreateRepository(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err) + rollbackRepo = repo + rollbackRepo.OwnerID = u.ID + return fmt.Errorf("CreateRepository(git update-server-info): %w", err) + } + return nil + }); err != nil { + if rollbackRepo != nil { + if errDelete := models.DeleteRepository(doer, rollbackRepo.OwnerID, rollbackRepo.ID); errDelete != nil { + log.Error("Rollback deleteRepository: %v", errDelete) + } + } + + return nil, err + } + + return repo, nil +} diff --git a/services/repository/create_test.go b/services/repository/create_test.go new file mode 100644 index 0000000000..ec3d62ce07 --- /dev/null +++ b/services/repository/create_test.go @@ -0,0 +1,148 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repository + +import ( + "fmt" + "testing" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/organization" + "code.gitea.io/gitea/models/perm" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/structs" + + "github.com/stretchr/testify/assert" +) + +func TestIncludesAllRepositoriesTeams(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + testTeamRepositories := func(teamID int64, repoIds []int64) { + team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID}) + assert.NoError(t, team.LoadRepositories(db.DefaultContext), "%s: GetRepositories", team.Name) + assert.Len(t, team.Repos, team.NumRepos, "%s: len repo", team.Name) + assert.Len(t, team.Repos, len(repoIds), "%s: repo count", team.Name) + for i, rid := range repoIds { + if rid > 0 { + assert.True(t, models.HasRepository(team, rid), "%s: HasRepository(%d) %d", rid, i) + } + } + } + + // Get an admin user. + user, err := user_model.GetUserByID(db.DefaultContext, 1) + assert.NoError(t, err, "GetUserByID") + + // Create org. + org := &organization.Organization{ + Name: "All_repo", + IsActive: true, + Type: user_model.UserTypeOrganization, + Visibility: structs.VisibleTypePublic, + } + assert.NoError(t, organization.CreateOrganization(org, user), "CreateOrganization") + + // Check Owner team. + ownerTeam, err := org.GetOwnerTeam(db.DefaultContext) + assert.NoError(t, err, "GetOwnerTeam") + assert.True(t, ownerTeam.IncludesAllRepositories, "Owner team includes all repositories") + + // Create repos. + repoIds := make([]int64, 0) + for i := 0; i < 3; i++ { + r, err := CreateRepositoryDirectly(user, org.AsUser(), CreateRepoOptions{Name: fmt.Sprintf("repo-%d", i)}) + assert.NoError(t, err, "CreateRepository %d", i) + if r != nil { + repoIds = append(repoIds, r.ID) + } + } + // Get fresh copy of Owner team after creating repos. + ownerTeam, err = org.GetOwnerTeam(db.DefaultContext) + assert.NoError(t, err, "GetOwnerTeam") + + // Create teams and check repositories. + teams := []*organization.Team{ + ownerTeam, + { + OrgID: org.ID, + Name: "team one", + AccessMode: perm.AccessModeRead, + IncludesAllRepositories: true, + }, + { + OrgID: org.ID, + Name: "team 2", + AccessMode: perm.AccessModeRead, + IncludesAllRepositories: false, + }, + { + OrgID: org.ID, + Name: "team three", + AccessMode: perm.AccessModeWrite, + IncludesAllRepositories: true, + }, + { + OrgID: org.ID, + Name: "team 4", + AccessMode: perm.AccessModeWrite, + IncludesAllRepositories: false, + }, + } + teamRepos := [][]int64{ + repoIds, + repoIds, + {}, + repoIds, + {}, + } + for i, team := range teams { + if i > 0 { // first team is Owner. + assert.NoError(t, models.NewTeam(team), "%s: NewTeam", team.Name) + } + testTeamRepositories(team.ID, teamRepos[i]) + } + + // Update teams and check repositories. + teams[3].IncludesAllRepositories = false + teams[4].IncludesAllRepositories = true + teamRepos[4] = repoIds + for i, team := range teams { + assert.NoError(t, models.UpdateTeam(team, false, true), "%s: UpdateTeam", team.Name) + testTeamRepositories(team.ID, teamRepos[i]) + } + + // Create repo and check teams repositories. + r, err := CreateRepositoryDirectly(user, org.AsUser(), CreateRepoOptions{Name: "repo-last"}) + assert.NoError(t, err, "CreateRepository last") + if r != nil { + repoIds = append(repoIds, r.ID) + } + teamRepos[0] = repoIds + teamRepos[1] = repoIds + teamRepos[4] = repoIds + for i, team := range teams { + testTeamRepositories(team.ID, teamRepos[i]) + } + + // Remove repo and check teams repositories. + assert.NoError(t, models.DeleteRepository(user, org.ID, repoIds[0]), "DeleteRepository") + teamRepos[0] = repoIds[1:] + teamRepos[1] = repoIds[1:] + teamRepos[3] = repoIds[1:3] + teamRepos[4] = repoIds[1:] + for i, team := range teams { + testTeamRepositories(team.ID, teamRepos[i]) + } + + // Wipe created items. + for i, rid := range repoIds { + if i > 0 { // first repo already deleted. + assert.NoError(t, models.DeleteRepository(user, org.ID, rid), "DeleteRepository %d", i) + } + } + assert.NoError(t, organization.DeleteOrganization(db.DefaultContext, org), "DeleteOrganization") +} diff --git a/services/repository/repository.go b/services/repository/repository.go index 47e96bd5e5..db3035f8c0 100644 --- a/services/repository/repository.go +++ b/services/repository/repository.go @@ -40,8 +40,8 @@ type WebSearchResults struct { } // CreateRepository creates a repository for the user/organization. -func CreateRepository(ctx context.Context, doer, owner *user_model.User, opts repo_module.CreateRepoOptions) (*repo_model.Repository, error) { - repo, err := repo_module.CreateRepository(doer, owner, opts) +func CreateRepository(ctx context.Context, doer, owner *user_model.User, opts CreateRepoOptions) (*repo_model.Repository, error) { + repo, err := CreateRepositoryDirectly(doer, owner, opts) if err != nil { // No need to rollback here we should do this in CreateRepository... return nil, err @@ -84,7 +84,7 @@ func PushCreateRepo(ctx context.Context, authUser, owner *user_model.User, repoN } } - repo, err := CreateRepository(ctx, authUser, owner, repo_module.CreateRepoOptions{ + repo, err := CreateRepository(ctx, authUser, owner, CreateRepoOptions{ Name: repoName, IsPrivate: setting.Repository.DefaultPushCreatePrivate, }) diff --git a/services/task/task.go b/services/task/task.go index db5c1dd3f8..45bc7b990a 100644 --- a/services/task/task.go +++ b/services/task/task.go @@ -14,12 +14,12 @@ import ( "code.gitea.io/gitea/modules/log" base "code.gitea.io/gitea/modules/migration" "code.gitea.io/gitea/modules/queue" - repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/secret" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" + repo_service "code.gitea.io/gitea/services/repository" ) // taskQueue is a global queue of tasks @@ -100,7 +100,7 @@ func CreateMigrateTask(doer, u *user_model.User, opts base.MigrateOptions) (*adm return nil, err } - repo, err := repo_module.CreateRepository(doer, u, repo_module.CreateRepoOptions{ + repo, err := repo_service.CreateRepositoryDirectly(doer, u, repo_service.CreateRepoOptions{ Name: opts.RepoName, Description: opts.Description, OriginalURL: opts.OriginalURL, diff --git a/templates/admin/hook_new.tmpl b/templates/admin/hook_new.tmpl index e72e7bba62..f565318b8b 100644 --- a/templates/admin/hook_new.tmpl +++ b/templates/admin/hook_new.tmpl @@ -1,33 +1,13 @@ {{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin settings new webhook")}}
-

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

-
- {{template "repo/settings/webhook/gitea" .}} - {{template "repo/settings/webhook/gogs" .}} - {{template "repo/settings/webhook/slack" .}} - {{template "repo/settings/webhook/discord" .}} - {{template "repo/settings/webhook/dingtalk" .}} - {{template "repo/settings/webhook/telegram" .}} - {{template "repo/settings/webhook/msteams" .}} - {{template "repo/settings/webhook/feishu" .}} - {{template "repo/settings/webhook/matrix" .}} - {{template "repo/settings/webhook/wechatwork" .}} - {{template "repo/settings/webhook/packagist" .}} -
- - {{template "repo/settings/webhook/history" .}} + {{$CustomHeaderTitle := .locale.Tr "admin.defaulthooks.update_webhook"}} + {{if .PageIsAdminDefaultHooksNew}} + {{$CustomHeaderTitle = .locale.Tr "admin.defaulthooks.add_webhook"}} + {{else if .PageIsAdminSystemHooksNew}} + {{$CustomHeaderTitle = .locale.Tr "admin.systemhooks.add_webhook"}} + {{else if .Webhook.IsSystemWebhook}} + {{$CustomHeaderTitle = .locale.Tr "admin.systemhooks.update_webhook"}} + {{end}} + {{template "webhook/new" (dict "ctxData" . "CustomHeaderTitle" $CustomHeaderTitle)}}
{{template "admin/layout_footer" .}} diff --git a/templates/org/settings/hook_new.tmpl b/templates/org/settings/hook_new.tmpl index d4343f8c68..ea477c9975 100644 --- a/templates/org/settings/hook_new.tmpl +++ b/templates/org/settings/hook_new.tmpl @@ -1,5 +1,7 @@ {{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings new webhook")}}
- {{template "webhook/new" .}} + {{$CustomHeaderTitle := .locale.Tr "repo.settings.update_webhook"}} + {{if .PageIsSettingsHooksNew}}{{$CustomHeaderTitle = .locale.Tr "repo.settings.add_webhook"}}{{end}} + {{template "webhook/new" (dict "ctxData" . "CustomHeaderTitle" $CustomHeaderTitle)}}
{{template "org/settings/layout_footer" .}} diff --git a/templates/repo/settings/webhook/new.tmpl b/templates/repo/settings/webhook/new.tmpl index 79fd4bd2ff..67a5f7296f 100644 --- a/templates/repo/settings/webhook/new.tmpl +++ b/templates/repo/settings/webhook/new.tmpl @@ -1,5 +1,7 @@ {{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings new webhook")}}
- {{template "webhook/new" .}} + {{$CustomHeaderTitle := .locale.Tr "repo.settings.update_webhook"}} + {{if .PageIsSettingsHooksNew}}{{$CustomHeaderTitle = .locale.Tr "repo.settings.add_webhook"}}{{end}} + {{template "webhook/new" (dict "ctxData" . "CustomHeaderTitle" $CustomHeaderTitle)}}
{{template "repo/settings/layout_footer" .}} diff --git a/templates/shared/secrets/add_list.tmpl b/templates/shared/secrets/add_list.tmpl index a80ec314d2..6215eff369 100644 --- a/templates/shared/secrets/add_list.tmpl +++ b/templates/shared/secrets/add_list.tmpl @@ -63,7 +63,7 @@ id="secret-name" name="name" value="{{.name}}" - pattern="^[a-zA-Z_][a-zA-Z0-9_]*$" + pattern="^(?!GITEA_|GITHUB_)[a-zA-Z_][a-zA-Z0-9_]*$" placeholder="{{.locale.Tr "secrets.creation.name_placeholder"}}" > diff --git a/templates/user/settings/hook_new.tmpl b/templates/user/settings/hook_new.tmpl index 4d3ddf0383..9a857db85c 100644 --- a/templates/user/settings/hook_new.tmpl +++ b/templates/user/settings/hook_new.tmpl @@ -1,5 +1,7 @@ {{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings new webhook")}}
- {{template "webhook/new" .}} + {{$CustomHeaderTitle := .locale.Tr "repo.settings.update_webhook"}} + {{if .PageIsSettingsHooksNew}}{{$CustomHeaderTitle = .locale.Tr "repo.settings.add_webhook"}}{{end}} + {{template "webhook/new" (dict "ctxData" . "CustomHeaderTitle" $CustomHeaderTitle)}}
{{template "user/settings/layout_footer" .}} diff --git a/templates/webhook/new.tmpl b/templates/webhook/new.tmpl index b5878fc930..a185c42b51 100644 --- a/templates/webhook/new.tmpl +++ b/templates/webhook/new.tmpl @@ -1,20 +1,20 @@

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

- {{template "repo/settings/webhook/gitea" .}} - {{template "repo/settings/webhook/gogs" .}} - {{template "repo/settings/webhook/slack" .}} - {{template "repo/settings/webhook/discord" .}} - {{template "repo/settings/webhook/dingtalk" .}} - {{template "repo/settings/webhook/telegram" .}} - {{template "repo/settings/webhook/msteams" .}} - {{template "repo/settings/webhook/feishu" .}} - {{template "repo/settings/webhook/matrix" .}} - {{template "repo/settings/webhook/wechatwork" .}} - {{template "repo/settings/webhook/packagist" .}} + {{template "repo/settings/webhook/gitea" .ctxData}} + {{template "repo/settings/webhook/gogs" .ctxData}} + {{template "repo/settings/webhook/slack" .ctxData}} + {{template "repo/settings/webhook/discord" .ctxData}} + {{template "repo/settings/webhook/dingtalk" .ctxData}} + {{template "repo/settings/webhook/telegram" .ctxData}} + {{template "repo/settings/webhook/msteams" .ctxData}} + {{template "repo/settings/webhook/feishu" .ctxData}} + {{template "repo/settings/webhook/matrix" .ctxData}} + {{template "repo/settings/webhook/wechatwork" .ctxData}} + {{template "repo/settings/webhook/packagist" .ctxData}}
-{{template "repo/settings/webhook/history" .}} +{{template "repo/settings/webhook/history" .ctxData}} diff --git a/tests/integration/actions_trigger_test.go b/tests/integration/actions_trigger_test.go index 1c5d2fed61..56718397f4 100644 --- a/tests/integration/actions_trigger_test.go +++ b/tests/integration/actions_trigger_test.go @@ -18,7 +18,6 @@ import ( user_model "code.gitea.io/gitea/models/user" actions_module "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/git" - repo_module "code.gitea.io/gitea/modules/repository" pull_service "code.gitea.io/gitea/services/pull" repo_service "code.gitea.io/gitea/services/repository" files_service "code.gitea.io/gitea/services/repository/files" @@ -32,7 +31,7 @@ func TestPullRequestTargetEvent(t *testing.T) { user3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) // owner of the forked repo // create the base repo - baseRepo, err := repo_service.CreateRepository(db.DefaultContext, user2, user2, repo_module.CreateRepoOptions{ + baseRepo, err := repo_service.CreateRepository(db.DefaultContext, user2, user2, repo_service.CreateRepoOptions{ Name: "repo-pull-request-target", Description: "test pull-request-target event", AutoInit: true, diff --git a/tests/integration/api_actions_artifact_test.go b/tests/integration/api_actions_artifact_test.go index 6590ca667c..101bedde02 100644 --- a/tests/integration/api_actions_artifact_test.go +++ b/tests/integration/api_actions_artifact_test.go @@ -18,8 +18,9 @@ type uploadArtifactResponse struct { } type getUploadArtifactRequest struct { - Type string - Name string + Type string + Name string + RetentionDays int64 } func TestActionsArtifactUploadSingleFile(t *testing.T) { @@ -252,3 +253,40 @@ func TestActionsArtifactDownloadMultiFiles(t *testing.T) { assert.Equal(t, resp.Body.String(), body) } } + +func TestActionsArtifactUploadWithRetentionDays(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + // acquire artifact upload url + req := NewRequestWithJSON(t, "POST", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts", getUploadArtifactRequest{ + Type: "actions_storage", + Name: "artifact-retention-days", + RetentionDays: 9, + }) + req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a") + resp := MakeRequest(t, req, http.StatusOK) + var uploadResp uploadArtifactResponse + DecodeJSON(t, resp, &uploadResp) + assert.Contains(t, uploadResp.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts") + assert.Contains(t, uploadResp.FileContainerResourceURL, "?retentionDays=9") + + // get upload url + idx := strings.Index(uploadResp.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/") + url := uploadResp.FileContainerResourceURL[idx:] + "&itemPath=artifact-retention-days/abc.txt" + + // upload artifact chunk + body := strings.Repeat("A", 1024) + req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body)) + req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a") + req.Header.Add("Content-Range", "bytes 0-1023/1024") + req.Header.Add("x-tfs-filelength", "1024") + req.Header.Add("x-actions-results-md5", "1HsSe8LeLWh93ILaw1TEFQ==") // base64(md5(body)) + MakeRequest(t, req, http.StatusOK) + + t.Logf("Create artifact confirm") + + // confirm artifact upload + req = NewRequest(t, "PATCH", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts?artifactName=artifact-retention-days") + req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a") + MakeRequest(t, req, http.StatusOK) +} diff --git a/tests/integration/mirror_pull_test.go b/tests/integration/mirror_pull_test.go index 1bd91a48b5..2f79f5113b 100644 --- a/tests/integration/mirror_pull_test.go +++ b/tests/integration/mirror_pull_test.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/repository" mirror_service "code.gitea.io/gitea/services/mirror" release_service "code.gitea.io/gitea/services/release" + repo_service "code.gitea.io/gitea/services/repository" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" @@ -38,7 +39,7 @@ func TestMirrorPull(t *testing.T) { Releases: false, } - mirrorRepo, err := repository.CreateRepository(user, user, repository.CreateRepoOptions{ + mirrorRepo, err := repo_service.CreateRepositoryDirectly(user, user, repo_service.CreateRepoOptions{ Name: opts.RepoName, Description: opts.Description, IsPrivate: opts.Private, diff --git a/tests/integration/mirror_push_test.go b/tests/integration/mirror_push_test.go index 9abae63b0a..ab79db1861 100644 --- a/tests/integration/mirror_push_test.go +++ b/tests/integration/mirror_push_test.go @@ -17,10 +17,10 @@ import ( user_model "code.gitea.io/gitea/models/user" gitea_context "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/services/migrations" mirror_service "code.gitea.io/gitea/services/mirror" + repo_service "code.gitea.io/gitea/services/repository" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" @@ -39,7 +39,7 @@ func testMirrorPush(t *testing.T, u *url.URL) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) srcRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - mirrorRepo, err := repository.CreateRepository(user, user, repository.CreateRepoOptions{ + mirrorRepo, err := repo_service.CreateRepositoryDirectly(user, user, repo_service.CreateRepoOptions{ Name: "test-push-mirror", }) assert.NoError(t, err) diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go index f958c890fe..0ef0969ab9 100644 --- a/tests/integration/pull_merge_test.go +++ b/tests/integration/pull_merge_test.go @@ -25,7 +25,6 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/git" - repo_module "code.gitea.io/gitea/modules/repository" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/translation" @@ -356,7 +355,7 @@ func TestConflictChecking(t *testing.T) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // Create new clean repo to test conflict checking. - baseRepo, err := repo_service.CreateRepository(db.DefaultContext, user, user, repo_module.CreateRepoOptions{ + baseRepo, err := repo_service.CreateRepository(db.DefaultContext, user, user, repo_service.CreateRepoOptions{ Name: "conflict-checking", Description: "Tempo repo", AutoInit: true, diff --git a/tests/integration/pull_update_test.go b/tests/integration/pull_update_test.go index 80c55042db..e4b2ae65bd 100644 --- a/tests/integration/pull_update_test.go +++ b/tests/integration/pull_update_test.go @@ -16,7 +16,6 @@ import ( "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" - repo_module "code.gitea.io/gitea/modules/repository" pull_service "code.gitea.io/gitea/services/pull" repo_service "code.gitea.io/gitea/services/repository" files_service "code.gitea.io/gitea/services/repository/files" @@ -81,7 +80,7 @@ func TestAPIPullUpdateByRebase(t *testing.T) { } func createOutdatedPR(t *testing.T, actor, forkOrg *user_model.User) *issues_model.PullRequest { - baseRepo, err := repo_service.CreateRepository(db.DefaultContext, actor, actor, repo_module.CreateRepoOptions{ + baseRepo, err := repo_service.CreateRepository(db.DefaultContext, actor, actor, repo_service.CreateRepoOptions{ Name: "repo-pr-update", Description: "repo-tmp-pr-update description", AutoInit: true, diff --git a/web_src/css/base.css b/web_src/css/base.css index cbd3336d26..9f42610641 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -2024,6 +2024,7 @@ a.ui.basic.label:hover { top: 0; bottom: 0; display: flex; + align-items: center; } .ui.attached.header > .ui.right > .button, diff --git a/web_src/css/repo.css b/web_src/css/repo.css index fb85a53ab7..a03ec9f060 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -2507,9 +2507,10 @@ } /* Scoped labels with different colors on left and right */ -.scope-parent { +.ui.label.scope-parent { background: none !important; padding: 0 !important; + gap: 0 !important; } .ui.label.scope-left { diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js index d02a82a2ef..03169e6815 100644 --- a/web_src/js/features/common-global.js +++ b/web_src/js/features/common-global.js @@ -1,5 +1,5 @@ import $ from 'jquery'; -import 'jquery.are-you-sure'; +import '../vendor/jquery.are-you-sure.js'; import {clippie} from 'clippie'; import {createDropzone} from './dropzone.js'; import {initCompColorPicker} from './comp/ColorPicker.js'; diff --git a/web_src/js/vendor/jquery.are-you-sure.js b/web_src/js/vendor/jquery.are-you-sure.js new file mode 100644 index 0000000000..e06da39fc9 --- /dev/null +++ b/web_src/js/vendor/jquery.are-you-sure.js @@ -0,0 +1,195 @@ +// Fork of the upstream module. The only changes are the addition of `const` on +// lines 93 and 161 to make it strict mode compatible. + +/*! + * jQuery Plugin: Are-You-Sure (Dirty Form Detection) + * https://github.com/codedance/jquery.AreYouSure/ + * + * Copyright (c) 2012-2014, Chris Dance and PaperCut Software http://www.papercut.com/ + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * Author: chris.dance@papercut.com + * Version: 1.9.0 + * Date: 13th August 2014 + */ +(function($) { + + $.fn.areYouSure = function(options) { + + var settings = $.extend( + { + 'message' : 'You have unsaved changes!', + 'dirtyClass' : 'dirty', + 'change' : null, + 'silent' : false, + 'addRemoveFieldsMarksDirty' : false, + 'fieldEvents' : 'change keyup propertychange input', + 'fieldSelector': ":input:not(input[type=submit]):not(input[type=button])" + }, options); + + var getValue = function($field) { + if ($field.hasClass('ays-ignore') + || $field.hasClass('aysIgnore') + || $field.attr('data-ays-ignore') + || $field.attr('name') === undefined) { + return null; + } + + if ($field.is(':disabled')) { + return 'ays-disabled'; + } + + var val; + var type = $field.attr('type'); + if ($field.is('select')) { + type = 'select'; + } + + switch (type) { + case 'checkbox': + case 'radio': + val = $field.is(':checked'); + break; + case 'select': + val = ''; + $field.find('option').each(function(o) { + var $option = $(this); + if ($option.is(':selected')) { + val += $option.val(); + } + }); + break; + default: + val = $field.val(); + } + + return val; + }; + + var storeOrigValue = function($field) { + $field.data('ays-orig', getValue($field)); + }; + + var checkForm = function(evt) { + + var isFieldDirty = function($field) { + var origValue = $field.data('ays-orig'); + if (undefined === origValue) { + return false; + } + return (getValue($field) != origValue); + }; + + var $form = ($(this).is('form')) + ? $(this) + : $(this).parents('form'); + + // Test on the target first as it's the most likely to be dirty + if (isFieldDirty($(evt.target))) { + setDirtyStatus($form, true); + return; + } + + const $fields = $form.find(settings.fieldSelector); + + if (settings.addRemoveFieldsMarksDirty) { + // Check if field count has changed + var origCount = $form.data("ays-orig-field-count"); + if (origCount != $fields.length) { + setDirtyStatus($form, true); + return; + } + } + + // Brute force - check each field + var isDirty = false; + $fields.each(function() { + var $field = $(this); + if (isFieldDirty($field)) { + isDirty = true; + return false; // break + } + }); + + setDirtyStatus($form, isDirty); + }; + + var initForm = function($form) { + var fields = $form.find(settings.fieldSelector); + $(fields).each(function() { storeOrigValue($(this)); }); + $(fields).unbind(settings.fieldEvents, checkForm); + $(fields).bind(settings.fieldEvents, checkForm); + $form.data("ays-orig-field-count", $(fields).length); + setDirtyStatus($form, false); + }; + + var setDirtyStatus = function($form, isDirty) { + var changed = isDirty != $form.hasClass(settings.dirtyClass); + $form.toggleClass(settings.dirtyClass, isDirty); + + // Fire change event if required + if (changed) { + if (settings.change) settings.change.call($form, $form); + + if (isDirty) $form.trigger('dirty.areYouSure', [$form]); + if (!isDirty) $form.trigger('clean.areYouSure', [$form]); + $form.trigger('change.areYouSure', [$form]); + } + }; + + var rescan = function() { + var $form = $(this); + var fields = $form.find(settings.fieldSelector); + $(fields).each(function() { + var $field = $(this); + if (!$field.data('ays-orig')) { + storeOrigValue($field); + $field.bind(settings.fieldEvents, checkForm); + } + }); + // Check for changes while we're here + $form.trigger('checkform.areYouSure'); + }; + + var reinitialize = function() { + initForm($(this)); + } + + if (!settings.silent && !window.aysUnloadSet) { + window.aysUnloadSet = true; + $(window).bind('beforeunload', function() { + const $dirtyForms = $("form").filter('.' + settings.dirtyClass); + if ($dirtyForms.length == 0) { + return; + } + // Prevent multiple prompts - seen on Chrome and IE + if (navigator.userAgent.toLowerCase().match(/msie|chrome/)) { + if (window.aysHasPrompted) { + return; + } + window.aysHasPrompted = true; + window.setTimeout(function() {window.aysHasPrompted = false;}, 900); + } + return settings.message; + }); + } + + return this.each(function(elem) { + if (!$(this).is('form')) { + return; + } + var $form = $(this); + + $form.submit(function() { + $form.removeClass(settings.dirtyClass); + }); + $form.bind('reset', function() { setDirtyStatus($form, false); }); + // Add a custom events + $form.bind('rescan.areYouSure', rescan); + $form.bind('reinitialize.areYouSure', reinitialize); + $form.bind('checkform.areYouSure', checkForm); + initForm($form); + }); + }; +})(jQuery); diff --git a/webpack.config.js b/webpack.config.js index c6b28068f0..f95296c380 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -206,7 +206,6 @@ export default { }).join('\n'); }, override: { - 'jquery.are-you-sure@*': {licenseName: 'MIT'}, // https://github.com/codedance/jquery.AreYouSure/pull/147 'khroma@*': {licenseName: 'MIT'}, // https://github.com/fabiospampinato/khroma/pull/33 }, emitError: true,