loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request '[gitea] week 2024-32 cherry pick (gitea/main -> forgejo)' (#4801) from earl-warren/wcp/2024-32 into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/4801
Reviewed-by: Gusted <gusted@noreply.codeberg.org>

+660 -178
-4
.deadcode-out
··· 74 74 75 75 code.gitea.io/gitea/models/repo 76 76 DeleteAttachmentsByIssue 77 - releaseSorter.Len 78 - releaseSorter.Less 79 - releaseSorter.Swap 80 - SortReleases 81 77 FindReposMapByIDs 82 78 IsErrTopicNotExist 83 79 ErrTopicNotExist.Error
+3 -1
custom/conf/app.example.ini
··· 2774 2774 ;ENABLED = true 2775 2775 ;; Default address to get action plugins, e.g. the default value means downloading from "https://code.forgejo.org/actions/checkout" for "uses: actions/checkout@v3" 2776 2776 ;DEFAULT_ACTIONS_URL = https://code.forgejo.org 2777 - ;; Default artifact retention time in days, default is 90 days 2777 + ;; Logs retention time in days. Old logs will be deleted after this period. 2778 + ;LOG_RETENTION_DAYS = 365 2779 + ;; Default artifact retention time in days. Artifacts could have their own retention periods by setting the `retention-days` option in `actions/upload-artifact` step. 2778 2780 ;ARTIFACT_RETENTION_DAYS = 90 2779 2781 ;; Timeout to stop the task which have running status, but haven't been updated for a long time 2780 2782 ;ZOMBIE_TASK_TIMEOUT = 10m
+20 -5
models/actions/runner.go
··· 26 26 ) 27 27 28 28 // ActionRunner represents runner machines 29 + // 30 + // It can be: 31 + // 1. global runner, OwnerID is 0 and RepoID is 0 32 + // 2. org/user level runner, OwnerID is org/user ID and RepoID is 0 33 + // 3. repo level runner, OwnerID is 0 and RepoID is repo ID 34 + // 35 + // Please note that it's not acceptable to have both OwnerID and RepoID to be non-zero, 36 + // or it will be complicated to find runners belonging to a specific owner. 37 + // For example, conditions like `OwnerID = 1` will also return runner {OwnerID: 1, RepoID: 1}, 38 + // but it's a repo level runner, not an org/user level runner. 39 + // To avoid this, make it clear with {OwnerID: 0, RepoID: 1} for repo level runners. 29 40 type ActionRunner struct { 30 41 ID int64 31 42 UUID string `xorm:"CHAR(36) UNIQUE"` 32 43 Name string `xorm:"VARCHAR(255)"` 33 44 Version string `xorm:"VARCHAR(64)"` 34 - OwnerID int64 `xorm:"index"` // org level runner, 0 means system 45 + OwnerID int64 `xorm:"index"` 35 46 Owner *user_model.User `xorm:"-"` 36 - RepoID int64 `xorm:"index"` // repo level runner, if OwnerID also is zero, then it's a global 47 + RepoID int64 `xorm:"index"` 37 48 Repo *repo_model.Repository `xorm:"-"` 38 49 Description string `xorm:"TEXT"` 39 50 Base int // 0 native 1 docker 2 virtual machine ··· 176 187 type FindRunnerOptions struct { 177 188 db.ListOptions 178 189 RepoID int64 179 - OwnerID int64 190 + OwnerID int64 // it will be ignored if RepoID is set 180 191 Sort string 181 192 Filter string 182 193 IsOnline optional.Option[bool] ··· 193 204 c = c.Or(builder.Eq{"repo_id": 0, "owner_id": 0}) 194 205 } 195 206 cond = cond.And(c) 196 - } 197 - if opts.OwnerID > 0 { 207 + } else if opts.OwnerID > 0 { // OwnerID is ignored if RepoID is set 198 208 c := builder.NewCond().And(builder.Eq{"owner_id": opts.OwnerID}) 199 209 if opts.WithAvailable { 200 210 c = c.Or(builder.Eq{"repo_id": 0, "owner_id": 0}) ··· 297 307 298 308 // CreateRunner creates new runner. 299 309 func CreateRunner(ctx context.Context, t *ActionRunner) error { 310 + if t.OwnerID != 0 && t.RepoID != 0 { 311 + // It's trying to create a runner that belongs to a repository, but OwnerID has been set accidentally. 312 + // Remove OwnerID to avoid confusion; it's not worth returning an error here. 313 + t.OwnerID = 0 314 + } 300 315 return db.Insert(ctx, t) 301 316 } 302 317
+26 -2
models/actions/runner_token.go
··· 15 15 ) 16 16 17 17 // ActionRunnerToken represents runner tokens 18 + // 19 + // It can be: 20 + // 1. global token, OwnerID is 0 and RepoID is 0 21 + // 2. org/user level token, OwnerID is org/user ID and RepoID is 0 22 + // 3. repo level token, OwnerID is 0 and RepoID is repo ID 23 + // 24 + // Please note that it's not acceptable to have both OwnerID and RepoID to be non-zero, 25 + // or it will be complicated to find tokens belonging to a specific owner. 26 + // For example, conditions like `OwnerID = 1` will also return token {OwnerID: 1, RepoID: 1}, 27 + // but it's a repo level token, not an org/user level token. 28 + // To avoid this, make it clear with {OwnerID: 0, RepoID: 1} for repo level tokens. 18 29 type ActionRunnerToken struct { 19 30 ID int64 20 31 Token string `xorm:"UNIQUE"` 21 - OwnerID int64 `xorm:"index"` // org level runner, 0 means system 32 + OwnerID int64 `xorm:"index"` 22 33 Owner *user_model.User `xorm:"-"` 23 - RepoID int64 `xorm:"index"` // repo level runner, if orgid also is zero, then it's a global 34 + RepoID int64 `xorm:"index"` 24 35 Repo *repo_model.Repository `xorm:"-"` 25 36 IsActive bool // true means it can be used 26 37 ··· 58 69 } 59 70 60 71 // NewRunnerToken creates a new active runner token and invalidate all old tokens 72 + // ownerID will be ignored and treated as 0 if repoID is non-zero. 61 73 func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) { 74 + if ownerID != 0 && repoID != 0 { 75 + // It's trying to create a runner token that belongs to a repository, but OwnerID has been set accidentally. 76 + // Remove OwnerID to avoid confusion; it's not worth returning an error here. 77 + ownerID = 0 78 + } 79 + 62 80 token, err := util.CryptoRandomString(40) 63 81 if err != nil { 64 82 return nil, err ··· 84 102 85 103 // GetLatestRunnerToken returns the latest runner token 86 104 func GetLatestRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) { 105 + if ownerID != 0 && repoID != 0 { 106 + // It's trying to get a runner token that belongs to a repository, but OwnerID has been set accidentally. 107 + // Remove OwnerID to avoid confusion; it's not worth returning an error here. 108 + ownerID = 0 109 + } 110 + 87 111 var runnerToken ActionRunnerToken 88 112 has, err := db.GetEngine(ctx).Where("owner_id=? AND repo_id=?", ownerID, repoID). 89 113 OrderBy("id DESC").Get(&runnerToken)
+9 -11
models/actions/schedule.go
··· 13 13 user_model "code.gitea.io/gitea/models/user" 14 14 "code.gitea.io/gitea/modules/timeutil" 15 15 webhook_module "code.gitea.io/gitea/modules/webhook" 16 - 17 - "github.com/robfig/cron/v3" 18 16 ) 19 17 20 18 // ActionSchedule represents a schedule of a workflow file ··· 53 51 return repos, db.GetEngine(ctx).In("id", ids).Find(&repos) 54 52 } 55 53 56 - var cronParser = cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor) 57 - 58 54 // CreateScheduleTask creates new schedule task. 59 55 func CreateScheduleTask(ctx context.Context, rows []*ActionSchedule) error { 60 56 // Return early if there are no rows to insert ··· 80 76 now := time.Now() 81 77 82 78 for _, spec := range row.Specs { 79 + specRow := &ActionScheduleSpec{ 80 + RepoID: row.RepoID, 81 + ScheduleID: row.ID, 82 + Spec: spec, 83 + } 83 84 // Parse the spec and check for errors 84 - schedule, err := cronParser.Parse(spec) 85 + schedule, err := specRow.Parse() 85 86 if err != nil { 86 87 continue // skip to the next spec if there's an error 87 88 } 88 89 90 + specRow.Next = timeutil.TimeStamp(schedule.Next(now).Unix()) 91 + 89 92 // Insert the new schedule spec row 90 - if err = db.Insert(ctx, &ActionScheduleSpec{ 91 - RepoID: row.RepoID, 92 - ScheduleID: row.ID, 93 - Spec: spec, 94 - Next: timeutil.TimeStamp(schedule.Next(now).Unix()), 95 - }); err != nil { 93 + if err = db.Insert(ctx, specRow); err != nil { 96 94 return err 97 95 } 98 96 }
+24 -1
models/actions/schedule_spec.go
··· 5 5 6 6 import ( 7 7 "context" 8 + "strings" 9 + "time" 8 10 9 11 "code.gitea.io/gitea/models/db" 10 12 repo_model "code.gitea.io/gitea/models/repo" ··· 32 34 Updated timeutil.TimeStamp `xorm:"updated"` 33 35 } 34 36 37 + // Parse parses the spec and returns a cron.Schedule 38 + // Unlike the default cron parser, Parse uses UTC timezone as the default if none is specified. 35 39 func (s *ActionScheduleSpec) Parse() (cron.Schedule, error) { 36 - return cronParser.Parse(s.Spec) 40 + parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor) 41 + schedule, err := parser.Parse(s.Spec) 42 + if err != nil { 43 + return nil, err 44 + } 45 + 46 + // If the spec has specified a timezone, use it 47 + if strings.HasPrefix(s.Spec, "TZ=") || strings.HasPrefix(s.Spec, "CRON_TZ=") { 48 + return schedule, nil 49 + } 50 + 51 + specSchedule, ok := schedule.(*cron.SpecSchedule) 52 + // If it's not a spec schedule, like "@every 5m", timezone is not relevant 53 + if !ok { 54 + return schedule, nil 55 + } 56 + 57 + // Set the timezone to UTC 58 + specSchedule.Location = time.UTC 59 + return specSchedule, nil 37 60 } 38 61 39 62 func init() {
+71
models/actions/schedule_spec_test.go
··· 1 + // Copyright 2024 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package actions 5 + 6 + import ( 7 + "testing" 8 + "time" 9 + 10 + "github.com/stretchr/testify/assert" 11 + "github.com/stretchr/testify/require" 12 + ) 13 + 14 + func TestActionScheduleSpec_Parse(t *testing.T) { 15 + // Mock the local timezone is not UTC 16 + local := time.Local 17 + tz, err := time.LoadLocation("Asia/Shanghai") 18 + require.NoError(t, err) 19 + defer func() { 20 + time.Local = local 21 + }() 22 + time.Local = tz 23 + 24 + now, err := time.Parse(time.RFC3339, "2024-07-31T15:47:55+08:00") 25 + require.NoError(t, err) 26 + 27 + tests := []struct { 28 + name string 29 + spec string 30 + want string 31 + wantErr assert.ErrorAssertionFunc 32 + }{ 33 + { 34 + name: "regular", 35 + spec: "0 10 * * *", 36 + want: "2024-07-31T10:00:00Z", 37 + wantErr: assert.NoError, 38 + }, 39 + { 40 + name: "invalid", 41 + spec: "0 10 * *", 42 + want: "", 43 + wantErr: assert.Error, 44 + }, 45 + { 46 + name: "with timezone", 47 + spec: "TZ=America/New_York 0 10 * * *", 48 + want: "2024-07-31T14:00:00Z", 49 + wantErr: assert.NoError, 50 + }, 51 + { 52 + name: "timezone irrelevant", 53 + spec: "@every 5m", 54 + want: "2024-07-31T07:52:55Z", 55 + wantErr: assert.NoError, 56 + }, 57 + } 58 + for _, tt := range tests { 59 + t.Run(tt.name, func(t *testing.T) { 60 + s := &ActionScheduleSpec{ 61 + Spec: tt.spec, 62 + } 63 + got, err := s.Parse() 64 + tt.wantErr(t, err) 65 + 66 + if err == nil { 67 + assert.Equal(t, tt.want, got.Next(now).UTC().Format(time.RFC3339)) 68 + } 69 + }) 70 + } 71 + }
+13 -3
models/actions/task.go
··· 35 35 RunnerID int64 `xorm:"index"` 36 36 Status Status `xorm:"index"` 37 37 Started timeutil.TimeStamp `xorm:"index"` 38 - Stopped timeutil.TimeStamp 38 + Stopped timeutil.TimeStamp `xorm:"index(stopped_log_expired)"` 39 39 40 40 RepoID int64 `xorm:"index"` 41 41 OwnerID int64 `xorm:"index"` ··· 51 51 LogInStorage bool // read log from database or from storage 52 52 LogLength int64 // lines count 53 53 LogSize int64 // blob size 54 - LogIndexes LogIndexes `xorm:"LONGBLOB"` // line number to offset 55 - LogExpired bool // files that are too old will be deleted 54 + LogIndexes LogIndexes `xorm:"LONGBLOB"` // line number to offset 55 + LogExpired bool `xorm:"index(stopped_log_expired)"` // files that are too old will be deleted 56 56 57 57 Created timeutil.TimeStamp `xorm:"created"` 58 58 Updated timeutil.TimeStamp `xorm:"updated index"` ··· 468 468 } 469 469 470 470 return nil 471 + } 472 + 473 + func FindOldTasksToExpire(ctx context.Context, olderThan timeutil.TimeStamp, limit int) ([]*ActionTask, error) { 474 + e := db.GetEngine(ctx) 475 + 476 + tasks := make([]*ActionTask, 0, limit) 477 + // Check "stopped > 0" to avoid deleting tasks that are still running 478 + return tasks, e.Where("stopped > 0 AND stopped < ? AND log_expired = ?", olderThan, false). 479 + Limit(limit). 480 + Find(&tasks) 471 481 } 472 482 473 483 func isSubset(set, subset []string) bool {
+24 -12
models/actions/variable.go
··· 5 5 6 6 import ( 7 7 "context" 8 - "errors" 9 8 "strings" 10 9 11 10 "code.gitea.io/gitea/models/db" ··· 15 14 "xorm.io/builder" 16 15 ) 17 16 17 + // ActionVariable represents a variable that can be used in actions 18 + // 19 + // It can be: 20 + // 1. global variable, OwnerID is 0 and RepoID is 0 21 + // 2. org/user level variable, OwnerID is org/user ID and RepoID is 0 22 + // 3. repo level variable, OwnerID is 0 and RepoID is repo ID 23 + // 24 + // Please note that it's not acceptable to have both OwnerID and RepoID to be non-zero, 25 + // or it will be complicated to find variables belonging to a specific owner. 26 + // For example, conditions like `OwnerID = 1` will also return variable {OwnerID: 1, RepoID: 1}, 27 + // but it's a repo level variable, not an org/user level variable. 28 + // To avoid this, make it clear with {OwnerID: 0, RepoID: 1} for repo level variables. 18 29 type ActionVariable struct { 19 30 ID int64 `xorm:"pk autoincr"` 20 31 OwnerID int64 `xorm:"UNIQUE(owner_repo_name)"` ··· 29 40 db.RegisterModel(new(ActionVariable)) 30 41 } 31 42 32 - func (v *ActionVariable) Validate() error { 33 - if v.OwnerID != 0 && v.RepoID != 0 { 34 - return errors.New("a variable should not be bound to an owner and a repository at the same time") 43 + func InsertVariable(ctx context.Context, ownerID, repoID int64, name, data string) (*ActionVariable, error) { 44 + if ownerID != 0 && repoID != 0 { 45 + // It's trying to create a variable that belongs to a repository, but OwnerID has been set accidentally. 46 + // Remove OwnerID to avoid confusion; it's not worth returning an error here. 47 + ownerID = 0 35 48 } 36 - return nil 37 - } 38 49 39 - func InsertVariable(ctx context.Context, ownerID, repoID int64, name, data string) (*ActionVariable, error) { 40 50 variable := &ActionVariable{ 41 51 OwnerID: ownerID, 42 52 RepoID: repoID, 43 53 Name: strings.ToUpper(name), 44 54 Data: data, 45 - } 46 - if err := variable.Validate(); err != nil { 47 - return variable, err 48 55 } 49 56 return variable, db.Insert(ctx, variable) 50 57 } 51 58 52 59 type FindVariablesOpts struct { 53 60 db.ListOptions 54 - OwnerID int64 55 61 RepoID int64 62 + OwnerID int64 // it will be ignored if RepoID is set 56 63 Name string 57 64 } 58 65 ··· 60 67 cond := builder.NewCond() 61 68 // Since we now support instance-level variables, 62 69 // there is no need to check for null values for `owner_id` and `repo_id` 63 - cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) 64 70 cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) 71 + if opts.RepoID != 0 { // if RepoID is set 72 + // ignore OwnerID and treat it as 0 73 + cond = cond.And(builder.Eq{"owner_id": 0}) 74 + } else { 75 + cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) 76 + } 65 77 66 78 if opts.Name != "" { 67 79 cond = cond.And(builder.Eq{"name": strings.ToUpper(opts.Name)})
+7
models/git/branch.go
··· 385 385 return err 386 386 } 387 387 388 + // 4.1 Update all not merged pull request head branch name 389 + if _, err = sess.Table("pull_request").Where("head_repo_id=? AND head_branch=? AND has_merged=?", 390 + repo.ID, from, false). 391 + Update(map[string]any{"head_branch": to}); err != nil { 392 + return err 393 + } 394 + 388 395 // 5. insert renamed branch record 389 396 renamedBranch := &RenamedBranch{ 390 397 RepoID: repo.ID,
+47 -1
models/git/commit_status.go
··· 141 141 return newIdx, nil 142 142 } 143 143 144 - func (status *CommitStatus) loadAttributes(ctx context.Context) (err error) { 144 + func (status *CommitStatus) loadRepository(ctx context.Context) (err error) { 145 145 if status.Repo == nil { 146 146 status.Repo, err = repo_model.GetRepositoryByID(ctx, status.RepoID) 147 147 if err != nil { 148 148 return fmt.Errorf("getRepositoryByID [%d]: %w", status.RepoID, err) 149 149 } 150 150 } 151 + return nil 152 + } 153 + 154 + func (status *CommitStatus) loadCreator(ctx context.Context) (err error) { 151 155 if status.Creator == nil && status.CreatorID > 0 { 152 156 status.Creator, err = user_model.GetUserByID(ctx, status.CreatorID) 153 157 if err != nil { ··· 157 161 return nil 158 162 } 159 163 164 + func (status *CommitStatus) loadAttributes(ctx context.Context) (err error) { 165 + if err := status.loadRepository(ctx); err != nil { 166 + return err 167 + } 168 + return status.loadCreator(ctx) 169 + } 170 + 160 171 // APIURL returns the absolute APIURL to this commit-status. 161 172 func (status *CommitStatus) APIURL(ctx context.Context) string { 162 173 _ = status.loadAttributes(ctx) ··· 166 177 // LocaleString returns the locale string name of the Status 167 178 func (status *CommitStatus) LocaleString(lang translation.Locale) string { 168 179 return lang.TrString("repo.commitstatus." + status.State.String()) 180 + } 181 + 182 + // HideActionsURL set `TargetURL` to an empty string if the status comes from Gitea Actions 183 + func (status *CommitStatus) HideActionsURL(ctx context.Context) { 184 + if status.RepoID == 0 { 185 + return 186 + } 187 + 188 + if status.Repo == nil { 189 + if err := status.loadRepository(ctx); err != nil { 190 + log.Error("loadRepository: %v", err) 191 + return 192 + } 193 + } 194 + 195 + prefix := fmt.Sprintf("%s/actions", status.Repo.Link()) 196 + if strings.HasPrefix(status.TargetURL, prefix) { 197 + status.TargetURL = "" 198 + } 169 199 } 170 200 171 201 // CalcCommitStatus returns commit status state via some status, the commit statues should order by id desc ··· 471 501 repo, 472 502 ) 473 503 } 504 + 505 + // CommitStatusesHideActionsURL hide Gitea Actions urls 506 + func CommitStatusesHideActionsURL(ctx context.Context, statuses []*CommitStatus) { 507 + idToRepos := make(map[int64]*repo_model.Repository) 508 + for _, status := range statuses { 509 + if status == nil { 510 + continue 511 + } 512 + 513 + if status.Repo == nil { 514 + status.Repo = idToRepos[status.RepoID] 515 + } 516 + status.HideActionsURL(ctx) 517 + idToRepos[status.RepoID] = status.Repo 518 + } 519 + }
+25
models/git/commit_status_test.go
··· 4 4 package git_test 5 5 6 6 import ( 7 + "fmt" 7 8 "testing" 8 9 "time" 9 10 11 + actions_model "code.gitea.io/gitea/models/actions" 10 12 "code.gitea.io/gitea/models/db" 11 13 git_model "code.gitea.io/gitea/models/git" 12 14 repo_model "code.gitea.io/gitea/models/repo" ··· 240 242 assert.Equal(t, "compliance/lint-backend", contexts[0]) 241 243 } 242 244 } 245 + 246 + func TestCommitStatusesHideActionsURL(t *testing.T) { 247 + require.NoError(t, unittest.PrepareTestDatabase()) 248 + 249 + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) 250 + run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 791, RepoID: repo.ID}) 251 + require.NoError(t, run.LoadAttributes(db.DefaultContext)) 252 + 253 + statuses := []*git_model.CommitStatus{ 254 + { 255 + RepoID: repo.ID, 256 + TargetURL: fmt.Sprintf("%s/jobs/%d", run.Link(), run.Index), 257 + }, 258 + { 259 + RepoID: repo.ID, 260 + TargetURL: "https://mycicd.org/1", 261 + }, 262 + } 263 + 264 + git_model.CommitStatusesHideActionsURL(db.DefaultContext, statuses) 265 + assert.Empty(t, statuses[0].TargetURL) 266 + assert.Equal(t, "https://mycicd.org/1", statuses[1].TargetURL) 267 + }
+6
models/migrations/migrations.go
··· 593 593 594 594 // v299 -> v300 595 595 NewMigration("Add content version to issue and comment table", v1_23.AddContentVersionToIssueAndComment), 596 + // v300 -> v301 597 + NewMigration("Add force-push branch protection support", v1_23.AddForcePushBranchProtection), 598 + // v301 -> v302 599 + NewMigration("Add skip_secondary_authorization option to oauth2 application table", v1_23.AddSkipSecondaryAuthColumnToOAuth2ApplicationTable), 600 + // v302 -> v303 601 + NewMigration("Add index to action_task stopped log_expired", v1_23.AddIndexToActionTaskStoppedLogExpired), 596 602 } 597 603 598 604 // GetCurrentDBVersion returns the current db version
+17
models/migrations/v1_23/v300.go
··· 1 + // Copyright 2024 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package v1_23 //nolint 5 + 6 + import "xorm.io/xorm" 7 + 8 + func AddForcePushBranchProtection(x *xorm.Engine) error { 9 + type ProtectedBranch struct { 10 + CanForcePush bool `xorm:"NOT NULL DEFAULT false"` 11 + EnableForcePushAllowlist bool `xorm:"NOT NULL DEFAULT false"` 12 + ForcePushAllowlistUserIDs []int64 `xorm:"JSON TEXT"` 13 + ForcePushAllowlistTeamIDs []int64 `xorm:"JSON TEXT"` 14 + ForcePushAllowlistDeployKeys bool `xorm:"NOT NULL DEFAULT false"` 15 + } 16 + return x.Sync(new(ProtectedBranch)) 17 + }
+14
models/migrations/v1_23/v301.go
··· 1 + // Copyright 2024 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package v1_23 //nolint 5 + 6 + import "xorm.io/xorm" 7 + 8 + // AddSkipSeconderyAuthToOAuth2ApplicationTable: add SkipSecondaryAuthorization column, setting existing rows to false 9 + func AddSkipSecondaryAuthColumnToOAuth2ApplicationTable(x *xorm.Engine) error { 10 + type oauth2Application struct { 11 + SkipSecondaryAuthorization bool `xorm:"NOT NULL DEFAULT FALSE"` 12 + } 13 + return x.Sync(new(oauth2Application)) 14 + }
+18
models/migrations/v1_23/v302.go
··· 1 + // Copyright 2024 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package v1_23 //nolint 5 + 6 + import ( 7 + "code.gitea.io/gitea/modules/timeutil" 8 + 9 + "xorm.io/xorm" 10 + ) 11 + 12 + func AddIndexToActionTaskStoppedLogExpired(x *xorm.Engine) error { 13 + type ActionTask struct { 14 + Stopped timeutil.TimeStamp `xorm:"index(stopped_log_expired)"` 15 + LogExpired bool `xorm:"index(stopped_log_expired)"` 16 + } 17 + return x.Sync(new(ActionTask)) 18 + }
+7
models/project/project.go
··· 103 103 ClosedDateUnix timeutil.TimeStamp 104 104 } 105 105 106 + // Ghost Project is a project which has been deleted 107 + const GhostProjectID = -1 108 + 109 + func (p *Project) IsGhost() bool { 110 + return p.ID == GhostProjectID 111 + } 112 + 106 113 func (p *Project) LoadOwner(ctx context.Context) (err error) { 107 114 if p.Owner != nil { 108 115 return nil
-26
models/repo/release.go
··· 413 413 return err 414 414 } 415 415 416 - type releaseSorter struct { 417 - rels []*Release 418 - } 419 - 420 - func (rs *releaseSorter) Len() int { 421 - return len(rs.rels) 422 - } 423 - 424 - func (rs *releaseSorter) Less(i, j int) bool { 425 - diffNum := rs.rels[i].NumCommits - rs.rels[j].NumCommits 426 - if diffNum != 0 { 427 - return diffNum > 0 428 - } 429 - return rs.rels[i].CreatedUnix > rs.rels[j].CreatedUnix 430 - } 431 - 432 - func (rs *releaseSorter) Swap(i, j int) { 433 - rs.rels[i], rs.rels[j] = rs.rels[j], rs.rels[i] 434 - } 435 - 436 - // SortReleases sorts releases by number of commits and created time. 437 - func SortReleases(rels []*Release) { 438 - sorter := &releaseSorter{rels: rels} 439 - sort.Sort(sorter) 440 - } 441 - 442 416 // UpdateReleasesMigrationsByType updates all migrated repositories' releases from gitServiceType to replace originalAuthorID to posterID 443 417 func UpdateReleasesMigrationsByType(ctx context.Context, gitServiceType structs.GitServiceType, originalAuthorID string, posterID int64) error { 444 418 _, err := db.GetEngine(ctx).Table("release").
+7 -6
models/repo/repo.go
··· 766 766 767 767 // GetRepositoryByName returns the repository by given name under user if exists. 768 768 func GetRepositoryByName(ctx context.Context, ownerID int64, name string) (*Repository, error) { 769 - repo := &Repository{ 770 - OwnerID: ownerID, 771 - LowerName: strings.ToLower(name), 772 - } 773 - has, err := db.GetEngine(ctx).Get(repo) 769 + var repo Repository 770 + has, err := db.GetEngine(ctx). 771 + Where("`owner_id`=?", ownerID). 772 + And("`lower_name`=?", strings.ToLower(name)). 773 + NoAutoCondition(). 774 + Get(&repo) 774 775 if err != nil { 775 776 return nil, err 776 777 } else if !has { 777 778 return nil, ErrRepoNotExist{0, ownerID, "", name} 778 779 } 779 - return repo, err 780 + return &repo, err 780 781 } 781 782 782 783 // getRepositoryURLPathSegments returns segments (owner, reponame) extracted from a url
+30 -16
models/secret/secret.go
··· 5 5 6 6 import ( 7 7 "context" 8 - "errors" 9 8 "fmt" 10 9 "strings" 11 10 ··· 22 21 ) 23 22 24 23 // Secret represents a secret 24 + // 25 + // It can be: 26 + // 1. org/user level secret, OwnerID is org/user ID and RepoID is 0 27 + // 2. repo level secret, OwnerID is 0 and RepoID is repo ID 28 + // 29 + // Please note that it's not acceptable to have both OwnerID and RepoID to be non-zero, 30 + // or it will be complicated to find secrets belonging to a specific owner. 31 + // For example, conditions like `OwnerID = 1` will also return secret {OwnerID: 1, RepoID: 1}, 32 + // but it's a repo level secret, not an org/user level secret. 33 + // To avoid this, make it clear with {OwnerID: 0, RepoID: 1} for repo level secrets. 34 + // 35 + // Please note that it's not acceptable to have both OwnerID and RepoID to zero, global secrets are not supported. 36 + // It's for security reasons, admin may be not aware of that the secrets could be stolen by any user when setting them as global. 25 37 type Secret struct { 26 38 ID int64 27 39 OwnerID int64 `xorm:"INDEX UNIQUE(owner_repo_name) NOT NULL"` ··· 46 58 47 59 // InsertEncryptedSecret Creates, encrypts, and validates a new secret with yet unencrypted data and insert into database 48 60 func InsertEncryptedSecret(ctx context.Context, ownerID, repoID int64, name, data string) (*Secret, error) { 61 + if ownerID != 0 && repoID != 0 { 62 + // It's trying to create a secret that belongs to a repository, but OwnerID has been set accidentally. 63 + // Remove OwnerID to avoid confusion; it's not worth returning an error here. 64 + ownerID = 0 65 + } 66 + if ownerID == 0 && repoID == 0 { 67 + return nil, fmt.Errorf("%w: ownerID and repoID cannot be both zero, global secrets are not supported", util.ErrInvalidArgument) 68 + } 69 + 49 70 encrypted, err := secret_module.EncryptSecret(setting.SecretKey, data) 50 71 if err != nil { 51 72 return nil, err ··· 56 77 Name: strings.ToUpper(name), 57 78 Data: encrypted, 58 79 } 59 - if err := secret.Validate(); err != nil { 60 - return secret, err 61 - } 62 80 return secret, db.Insert(ctx, secret) 63 81 } 64 82 ··· 66 84 db.RegisterModel(new(Secret)) 67 85 } 68 86 69 - func (s *Secret) Validate() error { 70 - if s.OwnerID == 0 && s.RepoID == 0 { 71 - return errors.New("the secret is not bound to any scope") 72 - } 73 - return nil 74 - } 75 - 76 87 type FindSecretsOptions struct { 77 88 db.ListOptions 78 - OwnerID int64 79 89 RepoID int64 90 + OwnerID int64 // it will be ignored if RepoID is set 80 91 SecretID int64 81 92 Name string 82 93 } 83 94 84 95 func (opts FindSecretsOptions) ToConds() builder.Cond { 85 96 cond := builder.NewCond() 86 - if opts.OwnerID > 0 { 97 + 98 + cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) 99 + if opts.RepoID != 0 { // if RepoID is set 100 + // ignore OwnerID and treat it as 0 101 + cond = cond.And(builder.Eq{"owner_id": 0}) 102 + } else { 87 103 cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) 88 104 } 89 - if opts.RepoID > 0 { 90 - cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) 91 - } 105 + 92 106 if opts.SecretID != 0 { 93 107 cond = cond.And(builder.Eq{"id": opts.SecretID}) 94 108 }
+3 -4
modules/lfs/http_client.go
··· 136 136 137 137 for _, object := range result.Objects { 138 138 if object.Error != nil { 139 - objectError := errors.New(object.Error.Message) 140 - log.Trace("Error on object %v: %v", object.Pointer, objectError) 139 + log.Trace("Error on object %v: %v", object.Pointer, object.Error) 141 140 if uc != nil { 142 - if _, err := uc(object.Pointer, objectError); err != nil { 141 + if _, err := uc(object.Pointer, object.Error); err != nil { 143 142 return err 144 143 } 145 144 } else { 146 - if err := dc(object.Pointer, nil, objectError); err != nil { 145 + if err := dc(object.Pointer, nil, object.Error); err != nil { 147 146 return err 148 147 } 149 148 }
+37
modules/lfs/shared.go
··· 4 4 package lfs 5 5 6 6 import ( 7 + "errors" 8 + "fmt" 7 9 "time" 10 + 11 + "code.gitea.io/gitea/modules/util" 8 12 ) 9 13 10 14 const ( ··· 62 66 type ObjectError struct { 63 67 Code int `json:"code"` 64 68 Message string `json:"message"` 69 + } 70 + 71 + var ( 72 + // See https://github.com/git-lfs/git-lfs/blob/main/docs/api/batch.md#successful-responses 73 + // LFS object error codes should match HTTP status codes where possible: 74 + // 404 - The object does not exist on the server. 75 + // 409 - The specified hash algorithm disagrees with the server's acceptable options. 76 + // 410 - The object was removed by the owner. 77 + // 422 - Validation error. 78 + 79 + ErrObjectNotExist = util.ErrNotExist // the object does not exist on the server 80 + ErrObjectHashMismatch = errors.New("the specified hash algorithm disagrees with the server's acceptable options") 81 + ErrObjectRemoved = errors.New("the object was removed by the owner") 82 + ErrObjectValidation = errors.New("validation error") 83 + ) 84 + 85 + func (e *ObjectError) Error() string { 86 + return fmt.Sprintf("[%d] %s", e.Code, e.Message) 87 + } 88 + 89 + func (e *ObjectError) Unwrap() error { 90 + switch e.Code { 91 + case 404: 92 + return ErrObjectNotExist 93 + case 409: 94 + return ErrObjectHashMismatch 95 + case 410: 96 + return ErrObjectRemoved 97 + case 422: 98 + return ErrObjectValidation 99 + default: 100 + return errors.New(e.Message) 101 + } 65 102 } 66 103 67 104 // PointerBlob associates a Git blob with a Pointer.
+5
modules/repository/repo.go
··· 6 6 7 7 import ( 8 8 "context" 9 + "errors" 9 10 "fmt" 10 11 "io" 11 12 "strings" ··· 182 183 downloadObjects := func(pointers []lfs.Pointer) error { 183 184 err := lfsClient.Download(ctx, pointers, func(p lfs.Pointer, content io.ReadCloser, objectError error) error { 184 185 if objectError != nil { 186 + if errors.Is(objectError, lfs.ErrObjectNotExist) { 187 + log.Warn("Repo[%-v]: Ignore missing LFS object %-v: %v", repo, p, objectError) 188 + return nil 189 + } 185 190 return objectError 186 191 } 187 192
+12 -4
modules/setting/actions.go
··· 12 12 // Actions settings 13 13 var ( 14 14 Actions = struct { 15 - LogStorage *Storage // how the created logs should be stored 16 - ArtifactStorage *Storage // how the created artifacts should be stored 17 - ArtifactRetentionDays int64 `ini:"ARTIFACT_RETENTION_DAYS"` 18 15 Enabled bool 16 + LogStorage *Storage // how the created logs should be stored 17 + LogRetentionDays int64 `ini:"LOG_RETENTION_DAYS"` 18 + ArtifactStorage *Storage // how the created artifacts should be stored 19 + ArtifactRetentionDays int64 `ini:"ARTIFACT_RETENTION_DAYS"` 19 20 DefaultActionsURL defaultActionsURL `ini:"DEFAULT_ACTIONS_URL"` 20 21 ZombieTaskTimeout time.Duration `ini:"ZOMBIE_TASK_TIMEOUT"` 21 22 EndlessTaskTimeout time.Duration `ini:"ENDLESS_TASK_TIMEOUT"` ··· 61 62 if err != nil { 62 63 return err 63 64 } 65 + // default to 1 year 66 + if Actions.LogRetentionDays <= 0 { 67 + Actions.LogRetentionDays = 365 68 + } 64 69 65 70 actionsSec, _ := rootCfg.GetSection("actions.artifacts") 66 71 67 72 Actions.ArtifactStorage, err = getStorage(rootCfg, "actions_artifacts", "", actionsSec) 73 + if err != nil { 74 + return err 75 + } 68 76 69 77 // default to 90 days in Github Actions 70 78 if Actions.ArtifactRetentionDays <= 0 { ··· 75 83 Actions.EndlessTaskTimeout = sec.Key("ENDLESS_TASK_TIMEOUT").MustDuration(3 * time.Hour) 76 84 Actions.AbandonedJobTimeout = sec.Key("ABANDONED_JOB_TIMEOUT").MustDuration(24 * time.Hour) 77 85 78 - return err 86 + return nil 79 87 }
+1
modules/structs/repo_collaborator.go
··· 5 5 6 6 // AddCollaboratorOption options when adding a user as a collaborator of a repository 7 7 type AddCollaboratorOption struct { 8 + // enum: read,write,admin 8 9 Permission *string `json:"permission"` 9 10 } 10 11
+6 -4
options/locale/locale_en-US.ini
··· 2997 2997 dashboard.update_checker = Update checker 2998 2998 dashboard.delete_old_system_notices = Delete all old system notices from database 2999 2999 dashboard.gc_lfs = Garbage collect LFS meta objects 3000 - dashboard.stop_zombie_tasks = Stop zombie tasks 3001 - dashboard.stop_endless_tasks = Stop endless tasks 3002 - dashboard.cancel_abandoned_jobs = Cancel abandoned jobs 3003 - dashboard.start_schedule_tasks = Start schedule tasks 3000 + dashboard.stop_zombie_tasks = Stop zombie actions tasks 3001 + dashboard.stop_endless_tasks = Stop endless actions tasks 3002 + dashboard.cancel_abandoned_jobs = Cancel abandoned actions jobs 3003 + dashboard.start_schedule_tasks = Start schedule actions tasks 3004 3004 dashboard.sync_branch.started = Branch sync started 3005 3005 dashboard.sync_tag.started = Tag sync started 3006 3006 dashboard.rebuild_issue_indexer = Rebuild issue indexer ··· 3803 3803 runs.no_workflows.documentation = For more information on Forgejo Actions, see <a target="_blank" rel="noopener noreferrer" href="%s">the documentation</a>. 3804 3804 runs.no_runs = The workflow has no runs yet. 3805 3805 runs.empty_commit_message = (empty commit message) 3806 + runs.expire_log_message = Logs have been purged because they were too old. 3806 3807 3807 3808 workflow.disable = Disable workflow 3808 3809 workflow.disable_success = Workflow "%s" disabled successfully. ··· 3836 3837 variables.update.success = The variable has been edited. 3837 3838 3838 3839 [projects] 3840 + deleted.display_name = Deleted Project 3839 3841 type-1.display_name = Individual project 3840 3842 type-2.display_name = Repository project 3841 3843 type-3.display_name = Organization project
+9
release-notes/4801.md
··· 1 + fix: [commit](https://codeberg.org/forgejo/forgejo/commit/0dbc6230286e113accbc6d5e829ce8dae1d1f5d4) Hide the "Details" link of commit status when the user cannot access actions. 2 + fix: [commit](https://codeberg.org/forgejo/forgejo/commit/6e63afe31f43eaf5ff7c8595ddeaf8515c2dc0c0) The API endpoint to get the actions registration token is GET /repos/{owner}/{repo}/actions/runners/registration-token and not GET /repos/{owner}/{repo}/runners/registration-token. 3 + fix: [commit](https://codeberg.org/forgejo/forgejo/commit/6e63afe31f43eaf5ff7c8595ddeaf8515c2dc0c0) Runner registration token via API is broken for repo level runners. 4 + fix: [commit](https://codeberg.org/forgejo/forgejo/commit/c784a5874066ca1a1fd518408d5767b4eb57bd69) Deleted projects causes bad popover text on issues. 5 + fix: [commit](https://codeberg.org/forgejo/forgejo/commit/42bb51af9b8283071e15ac6470ada9824d87cd40) Distinguish LFS object errors to ignore missing objects during migration. 6 + feat: [commit](https://codeberg.org/forgejo/forgejo/commit/11b6253e7532ba11dee8bc31d4c262b102674a4d) Use UTC as a timezone when running scheduled actions tasks. 7 + feat: [commit](https://codeberg.org/forgejo/forgejo/commit/feb43b2584b7f64ec7f9952af2b50b2210e6e6cf) The actions logs older than `[actions].LOG_RETENTION_DAYS` days are removed (the default is 365). 8 + fix: [commit](https://codeberg.org/forgejo/forgejo/commit/6328f648decc2754ef10ee5ca6ca9785a156614c) When viewing the revision history of wiki pages, the pagination links are broken: instead of org/repo/wiki/Page?action=_revision&page=2, the link is only org/repo/wiki/Page?page=2, thus bringing the user back to the wiki page. 9 + fix: [commit](https://codeberg.org/forgejo/forgejo/commit/2310556158d70bf1dbfca96dc928e1be3d3f41be) Also rename the head branch of open pull requests when renaming a branch.
+4 -6
routers/api/v1/repo/action.go
··· 117 117 // "404": 118 118 // "$ref": "#/responses/notFound" 119 119 120 - owner := ctx.Repo.Owner 121 120 repo := ctx.Repo.Repository 122 121 123 122 opt := web.GetForm(ctx).(*api.CreateOrUpdateSecretOption) 124 123 125 - _, created, err := secret_service.CreateOrUpdateSecret(ctx, owner.ID, repo.ID, ctx.Params("secretname"), opt.Data) 124 + _, created, err := secret_service.CreateOrUpdateSecret(ctx, 0, repo.ID, ctx.Params("secretname"), opt.Data) 126 125 if err != nil { 127 126 if errors.Is(err, util.ErrInvalidArgument) { 128 127 ctx.Error(http.StatusBadRequest, "CreateOrUpdateSecret", err) ··· 174 173 // "404": 175 174 // "$ref": "#/responses/notFound" 176 175 177 - owner := ctx.Repo.Owner 178 176 repo := ctx.Repo.Repository 179 177 180 - err := secret_service.DeleteSecretByName(ctx, owner.ID, repo.ID, ctx.Params("secretname")) 178 + err := secret_service.DeleteSecretByName(ctx, 0, repo.ID, ctx.Params("secretname")) 181 179 if err != nil { 182 180 if errors.Is(err, util.ErrInvalidArgument) { 183 181 ctx.Error(http.StatusBadRequest, "DeleteSecret", err) ··· 486 484 487 485 // GetRegistrationToken returns the token to register repo runners 488 486 func (Action) GetRegistrationToken(ctx *context.APIContext) { 489 - // swagger:operation GET /repos/{owner}/{repo}/runners/registration-token repository repoGetRunnerRegistrationToken 487 + // swagger:operation GET /repos/{owner}/{repo}/actions/runners/registration-token repository repoGetRunnerRegistrationToken 490 488 // --- 491 489 // summary: Get a repository's actions runner registration token 492 490 // produces: ··· 506 504 // "200": 507 505 // "$ref": "#/responses/RegistrationToken" 508 506 509 - shared.GetRegistrationToken(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID) 507 + shared.GetRegistrationToken(ctx, 0, ctx.Repo.Repository.ID) 510 508 } 511 509 512 510 var _ actions_service.API = new(Action)
+21
routers/web/repo/actions/view.go
··· 271 271 272 272 step := steps[cursor.Step] 273 273 274 + // if task log is expired, return a consistent log line 275 + if task.LogExpired { 276 + if cursor.Cursor == 0 { 277 + resp.Logs.StepsLog = append(resp.Logs.StepsLog, &ViewStepLog{ 278 + Step: cursor.Step, 279 + Cursor: 1, 280 + Lines: []*ViewStepLogLine{ 281 + { 282 + Index: 1, 283 + Message: ctx.Locale.TrString("actions.runs.expire_log_message"), 284 + // Timestamp doesn't mean anything when the log is expired. 285 + // Set it to the task's updated time since it's probably the time when the log has expired. 286 + Timestamp: float64(task.Updated.AsTime().UnixNano()) / float64(time.Second), 287 + }, 288 + }, 289 + Started: int64(step.Started), 290 + }) 291 + } 292 + continue 293 + } 294 + 274 295 logLines := make([]*ViewStepLogLine, 0) // marshal to '[]' instead of 'null' in json 275 296 276 297 index := step.LogIndex + cursor.Cursor
+5
routers/web/repo/branch.go
··· 70 70 ctx.ServerError("LoadBranches", err) 71 71 return 72 72 } 73 + if !ctx.Repo.CanRead(unit.TypeActions) { 74 + for key := range commitStatuses { 75 + git_model.CommitStatusesHideActionsURL(ctx, commitStatuses[key]) 76 + } 77 + } 73 78 74 79 commitStatus := make(map[string]*git_model.CommitStatus) 75 80 for commitID, cs := range commitStatuses {
+21 -3
routers/web/repo/commit.go
··· 16 16 "code.gitea.io/gitea/models/db" 17 17 git_model "code.gitea.io/gitea/models/git" 18 18 repo_model "code.gitea.io/gitea/models/repo" 19 + unit_model "code.gitea.io/gitea/models/unit" 19 20 user_model "code.gitea.io/gitea/models/user" 20 21 "code.gitea.io/gitea/modules/base" 21 22 "code.gitea.io/gitea/modules/charset" ··· 81 82 ctx.ServerError("CommitsByRange", err) 82 83 return 83 84 } 84 - ctx.Data["Commits"] = git_model.ConvertFromGitCommit(ctx, commits, ctx.Repo.Repository) 85 + ctx.Data["Commits"] = processGitCommits(ctx, commits) 85 86 86 87 ctx.Data["Username"] = ctx.Repo.Owner.Name 87 88 ctx.Data["Reponame"] = ctx.Repo.Repository.Name ··· 199 200 return 200 201 } 201 202 ctx.Data["CommitCount"] = len(commits) 202 - ctx.Data["Commits"] = git_model.ConvertFromGitCommit(ctx, commits, ctx.Repo.Repository) 203 + ctx.Data["Commits"] = processGitCommits(ctx, commits) 203 204 204 205 ctx.Data["Keyword"] = query 205 206 if all { ··· 264 265 } 265 266 } 266 267 267 - ctx.Data["Commits"] = git_model.ConvertFromGitCommit(ctx, commits, ctx.Repo.Repository) 268 + ctx.Data["Commits"] = processGitCommits(ctx, commits) 268 269 269 270 ctx.Data["Username"] = ctx.Repo.Owner.Name 270 271 ctx.Data["Reponame"] = ctx.Repo.Repository.Name ··· 375 376 if err != nil { 376 377 log.Error("GetLatestCommitStatus: %v", err) 377 378 } 379 + if !ctx.Repo.CanRead(unit_model.TypeActions) { 380 + git_model.CommitStatusesHideActionsURL(ctx, statuses) 381 + } 378 382 379 383 ctx.Data["CommitStatus"] = git_model.CalcCommitStatus(statuses) 380 384 ctx.Data["CommitStatuses"] = statuses ··· 454 458 return 455 459 } 456 460 } 461 + 462 + func processGitCommits(ctx *context.Context, gitCommits []*git.Commit) []*git_model.SignCommitWithStatuses { 463 + commits := git_model.ConvertFromGitCommit(ctx, gitCommits, ctx.Repo.Repository) 464 + if !ctx.Repo.CanRead(unit_model.TypeActions) { 465 + for _, commit := range commits { 466 + if commit.Status == nil { 467 + continue 468 + } 469 + commit.Status.HideActionsURL(ctx) 470 + git_model.CommitStatusesHideActionsURL(ctx, commit.Statuses) 471 + } 472 + } 473 + return commits 474 + }
+1 -1
routers/web/repo/compare.go
··· 643 643 return false 644 644 } 645 645 646 - commits := git_model.ConvertFromGitCommit(ctx, ci.CompareInfo.Commits, ci.HeadRepo) 646 + commits := processGitCommits(ctx, ci.CompareInfo.Commits) 647 647 ctx.Data["Commits"] = commits 648 648 ctx.Data["CommitCount"] = len(commits) 649 649
+15 -1
routers/web/repo/issue.go
··· 346 346 ctx.ServerError("GetIssuesAllCommitStatus", err) 347 347 return 348 348 } 349 + if !ctx.Repo.CanRead(unit.TypeActions) { 350 + for key := range commitStatuses { 351 + git_model.CommitStatusesHideActionsURL(ctx, commitStatuses[key]) 352 + } 353 + } 349 354 350 355 if err := issues.LoadAttributes(ctx); err != nil { 351 356 ctx.ServerError("issues.LoadAttributes", err) ··· 1692 1697 } 1693 1698 1694 1699 ghostProject := &project_model.Project{ 1695 - ID: -1, 1700 + ID: project_model.GhostProjectID, 1696 1701 Title: ctx.Locale.TrString("repo.issues.deleted_project"), 1697 1702 } 1698 1703 ··· 1776 1781 if err = comment.LoadPushCommits(ctx); err != nil { 1777 1782 ctx.ServerError("LoadPushCommits", err) 1778 1783 return 1784 + } 1785 + if !ctx.Repo.CanRead(unit.TypeActions) { 1786 + for _, commit := range comment.Commits { 1787 + if commit.Status == nil { 1788 + continue 1789 + } 1790 + commit.Status.HideActionsURL(ctx) 1791 + git_model.CommitStatusesHideActionsURL(ctx, commit.Statuses) 1792 + } 1779 1793 } 1780 1794 } else if comment.Type == issues_model.CommentTypeAddTimeManual || 1781 1795 comment.Type == issues_model.CommentTypeStopTracking ||
+13 -1
routers/web/repo/pull.go
··· 515 515 ctx.ServerError("GetLatestCommitStatus", err) 516 516 return nil 517 517 } 518 + if !ctx.Repo.CanRead(unit.TypeActions) { 519 + git_model.CommitStatusesHideActionsURL(ctx, commitStatuses) 520 + } 521 + 518 522 if len(commitStatuses) != 0 { 519 523 ctx.Data["LatestCommitStatuses"] = commitStatuses 520 524 ctx.Data["LatestCommitStatus"] = git_model.CalcCommitStatus(commitStatuses) ··· 577 581 ctx.ServerError("GetLatestCommitStatus", err) 578 582 return nil 579 583 } 584 + if !ctx.Repo.CanRead(unit.TypeActions) { 585 + git_model.CommitStatusesHideActionsURL(ctx, commitStatuses) 586 + } 587 + 580 588 if len(commitStatuses) > 0 { 581 589 ctx.Data["LatestCommitStatuses"] = commitStatuses 582 590 ctx.Data["LatestCommitStatus"] = git_model.CalcCommitStatus(commitStatuses) ··· 669 677 ctx.ServerError("GetLatestCommitStatus", err) 670 678 return nil 671 679 } 680 + if !ctx.Repo.CanRead(unit.TypeActions) { 681 + git_model.CommitStatusesHideActionsURL(ctx, commitStatuses) 682 + } 683 + 672 684 if len(commitStatuses) > 0 { 673 685 ctx.Data["LatestCommitStatuses"] = commitStatuses 674 686 ctx.Data["LatestCommitStatus"] = git_model.CalcCommitStatus(commitStatuses) ··· 835 847 ctx.Data["Username"] = ctx.Repo.Owner.Name 836 848 ctx.Data["Reponame"] = ctx.Repo.Repository.Name 837 849 838 - commits := git_model.ConvertFromGitCommit(ctx, prInfo.Commits, ctx.Repo.Repository) 850 + commits := processGitCommits(ctx, prInfo.Commits) 839 851 ctx.Data["Commits"] = commits 840 852 ctx.Data["CommitCount"] = len(commits) 841 853
+3
routers/web/repo/repo.go
··· 683 683 ctx.JSON(http.StatusInternalServerError, nil) 684 684 return 685 685 } 686 + if !ctx.Repo.CanRead(unit.TypeActions) { 687 + git_model.CommitStatusesHideActionsURL(ctx, latestCommitStatuses) 688 + } 686 689 687 690 results := make([]*repo_service.WebSearchRepository, len(repos)) 688 691 for i, repo := range repos {
+3
routers/web/repo/view.go
··· 368 368 if err != nil { 369 369 log.Error("GetLatestCommitStatus: %v", err) 370 370 } 371 + if !ctx.Repo.CanRead(unit_model.TypeActions) { 372 + git_model.CommitStatusesHideActionsURL(ctx, statuses) 373 + } 371 374 372 375 ctx.Data["LatestCommitStatus"] = git_model.CalcCommitStatus(statuses) 373 376 ctx.Data["LatestCommitStatuses"] = statuses
+1
routers/web/repo/wiki.go
··· 396 396 397 397 pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5) 398 398 pager.SetDefaultParams(ctx) 399 + pager.AddParamString("action", "_revision") 399 400 ctx.Data["Page"] = pager 400 401 401 402 return wikiRepo, entry
+6
routers/web/user/home.go
··· 17 17 activities_model "code.gitea.io/gitea/models/activities" 18 18 asymkey_model "code.gitea.io/gitea/models/asymkey" 19 19 "code.gitea.io/gitea/models/db" 20 + git_model "code.gitea.io/gitea/models/git" 20 21 issues_model "code.gitea.io/gitea/models/issues" 21 22 "code.gitea.io/gitea/models/organization" 22 23 repo_model "code.gitea.io/gitea/models/repo" ··· 596 597 if err != nil { 597 598 ctx.ServerError("GetIssuesLastCommitStatus", err) 598 599 return 600 + } 601 + if !ctx.Repo.CanRead(unit.TypeActions) { 602 + for key := range commitStatuses { 603 + git_model.CommitStatusesHideActionsURL(ctx, commitStatuses[key]) 604 + } 599 605 } 600 606 601 607 // -------------------------------
+7
routers/web/user/notification.go
··· 13 13 14 14 activities_model "code.gitea.io/gitea/models/activities" 15 15 "code.gitea.io/gitea/models/db" 16 + git_model "code.gitea.io/gitea/models/git" 16 17 issues_model "code.gitea.io/gitea/models/issues" 17 18 repo_model "code.gitea.io/gitea/models/repo" 19 + "code.gitea.io/gitea/models/unit" 18 20 "code.gitea.io/gitea/modules/base" 19 21 "code.gitea.io/gitea/modules/log" 20 22 "code.gitea.io/gitea/modules/optional" ··· 302 304 if err != nil { 303 305 ctx.ServerError("GetIssuesAllCommitStatus", err) 304 306 return 307 + } 308 + if !ctx.Repo.CanRead(unit.TypeActions) { 309 + for key := range commitStatuses { 310 + git_model.CommitStatusesHideActionsURL(ctx, commitStatuses[key]) 311 + } 305 312 } 306 313 ctx.Data["CommitLastStatus"] = lastStatus 307 314 ctx.Data["CommitStatuses"] = commitStatuses
+57 -9
services/actions/cleanup.go
··· 5 5 6 6 import ( 7 7 "context" 8 + "fmt" 8 9 "time" 9 10 10 - "code.gitea.io/gitea/models/actions" 11 + actions_model "code.gitea.io/gitea/models/actions" 12 + actions_module "code.gitea.io/gitea/modules/actions" 11 13 "code.gitea.io/gitea/modules/log" 14 + "code.gitea.io/gitea/modules/setting" 12 15 "code.gitea.io/gitea/modules/storage" 16 + "code.gitea.io/gitea/modules/timeutil" 13 17 ) 14 18 15 19 // Cleanup removes expired actions logs, data and artifacts 16 - func Cleanup(taskCtx context.Context, olderThan time.Duration) error { 17 - // TODO: clean up expired actions logs 20 + func Cleanup(ctx context.Context) error { 21 + // clean up expired artifacts 22 + if err := CleanupArtifacts(ctx); err != nil { 23 + return fmt.Errorf("cleanup artifacts: %w", err) 24 + } 25 + 26 + // clean up old logs 27 + if err := CleanupLogs(ctx); err != nil { 28 + return fmt.Errorf("cleanup logs: %w", err) 29 + } 18 30 19 - // clean up expired artifacts 20 - return CleanupArtifacts(taskCtx) 31 + return nil 21 32 } 22 33 23 34 // CleanupArtifacts removes expired add need-deleted artifacts and set records expired status ··· 29 40 } 30 41 31 42 func cleanExpiredArtifacts(taskCtx context.Context) error { 32 - artifacts, err := actions.ListNeedExpiredArtifacts(taskCtx) 43 + artifacts, err := actions_model.ListNeedExpiredArtifacts(taskCtx) 33 44 if err != nil { 34 45 return err 35 46 } 36 47 log.Info("Found %d expired artifacts", len(artifacts)) 37 48 for _, artifact := range artifacts { 38 - if err := actions.SetArtifactExpired(taskCtx, artifact.ID); err != nil { 49 + if err := actions_model.SetArtifactExpired(taskCtx, artifact.ID); err != nil { 39 50 log.Error("Cannot set artifact %d expired: %v", artifact.ID, err) 40 51 continue 41 52 } ··· 53 64 54 65 func cleanNeedDeleteArtifacts(taskCtx context.Context) error { 55 66 for { 56 - artifacts, err := actions.ListPendingDeleteArtifacts(taskCtx, deleteArtifactBatchSize) 67 + artifacts, err := actions_model.ListPendingDeleteArtifacts(taskCtx, deleteArtifactBatchSize) 57 68 if err != nil { 58 69 return err 59 70 } 60 71 log.Info("Found %d artifacts pending deletion", len(artifacts)) 61 72 for _, artifact := range artifacts { 62 - if err := actions.SetArtifactDeleted(taskCtx, artifact.ID); err != nil { 73 + if err := actions_model.SetArtifactDeleted(taskCtx, artifact.ID); err != nil { 63 74 log.Error("Cannot set artifact %d deleted: %v", artifact.ID, err) 64 75 continue 65 76 } ··· 76 87 } 77 88 return nil 78 89 } 90 + 91 + const deleteLogBatchSize = 100 92 + 93 + // CleanupLogs removes logs which are older than the configured retention time 94 + func CleanupLogs(ctx context.Context) error { 95 + olderThan := timeutil.TimeStampNow().AddDuration(-time.Duration(setting.Actions.LogRetentionDays) * 24 * time.Hour) 96 + 97 + count := 0 98 + for { 99 + tasks, err := actions_model.FindOldTasksToExpire(ctx, olderThan, deleteLogBatchSize) 100 + if err != nil { 101 + return fmt.Errorf("find old tasks: %w", err) 102 + } 103 + for _, task := range tasks { 104 + if err := actions_module.RemoveLogs(ctx, task.LogInStorage, task.LogFilename); err != nil { 105 + log.Error("Failed to remove log %s (in storage %v) of task %v: %v", task.LogFilename, task.LogInStorage, task.ID, err) 106 + // do not return error here, continue to next task 107 + continue 108 + } 109 + task.LogIndexes = nil // clear log indexes since it's a heavy field 110 + task.LogExpired = true 111 + if err := actions_model.UpdateTask(ctx, task, "log_indexes", "log_expired"); err != nil { 112 + log.Error("Failed to update task %v: %v", task.ID, err) 113 + // do not return error here, continue to next task 114 + continue 115 + } 116 + count++ 117 + log.Trace("Removed log %s of task %v", task.LogFilename, task.ID) 118 + } 119 + if len(tasks) < deleteLogBatchSize { 120 + break 121 + } 122 + } 123 + 124 + log.Info("Removed %d logs", count) 125 + return nil 126 + }
+11
services/cron/tasks_actions.go
··· 19 19 registerStopEndlessTasks() 20 20 registerCancelAbandonedJobs() 21 21 registerScheduleTasks() 22 + registerActionsCleanup() 22 23 } 23 24 24 25 func registerStopZombieTasks() { ··· 63 64 return actions_service.StartScheduleTasks(ctx) 64 65 }) 65 66 } 67 + 68 + func registerActionsCleanup() { 69 + RegisterTaskFatal("cleanup_actions", &BaseConfig{ 70 + Enabled: true, 71 + RunAtStart: false, 72 + Schedule: "@midnight", 73 + }, func(ctx context.Context, _ *user_model.User, _ Config) error { 74 + return actions_service.Cleanup(ctx) 75 + }) 76 + }
-18
services/cron/tasks_basic.go
··· 13 13 "code.gitea.io/gitea/models/webhook" 14 14 "code.gitea.io/gitea/modules/git" 15 15 "code.gitea.io/gitea/modules/setting" 16 - "code.gitea.io/gitea/services/actions" 17 16 "code.gitea.io/gitea/services/auth" 18 17 "code.gitea.io/gitea/services/migrations" 19 18 mirror_service "code.gitea.io/gitea/services/mirror" ··· 157 156 }) 158 157 } 159 158 160 - func registerActionsCleanup() { 161 - RegisterTaskFatal("cleanup_actions", &OlderThanConfig{ 162 - BaseConfig: BaseConfig{ 163 - Enabled: true, 164 - RunAtStart: true, 165 - Schedule: "@midnight", 166 - }, 167 - OlderThan: 24 * time.Hour, 168 - }, func(ctx context.Context, _ *user_model.User, config Config) error { 169 - realConfig := config.(*OlderThanConfig) 170 - return actions.Cleanup(ctx, realConfig.OlderThan) 171 - }) 172 - } 173 - 174 159 func initBasicTasks() { 175 160 if setting.Mirror.Enabled { 176 161 registerUpdateMirrorTask() ··· 186 171 registerCleanupHookTaskTable() 187 172 if setting.Packages.Enabled { 188 173 registerCleanupPackages() 189 - } 190 - if setting.Actions.Enabled { 191 - registerActionsCleanup() 192 174 } 193 175 }
+1
services/repository/migrate.go
··· 172 172 lfsClient := lfs.NewClient(endpoint, httpTransport) 173 173 if err = repo_module.StoreMissingLfsObjectsInRepository(ctx, repo, gitRepo, lfsClient); err != nil { 174 174 log.Error("Failed to store missing LFS objects for repository: %v", err) 175 + return repo, fmt.Errorf("StoreMissingLfsObjectsInRepository: %w", err) 175 176 } 176 177 } 177 178 }
+10 -4
templates/repo/issue/view_content/comments.tmpl
··· 582 582 {{template "shared/user/authorlink" .Poster}} 583 583 {{$oldProjectDisplayHtml := "Unknown Project"}} 584 584 {{if .OldProject}} 585 - {{$trKey := printf "projects.type-%d.display_name" .OldProject.Type}} 586 - {{$oldProjectDisplayHtml = HTMLFormat `<span data-tooltip-content="%s">%s</span>` (ctx.Locale.Tr $trKey) .OldProject.Title}} 585 + {{$tooltip := ctx.Locale.Tr "projects.deleted.display_name"}} 586 + {{if not .OldProject.IsGhost}} 587 + {{$tooltip = ctx.Locale.Tr (printf "projects.type-%d.display_name" .OldProject.Type)}} 588 + {{end}} 589 + {{$oldProjectDisplayHtml = HTMLFormat `<span data-tooltip-content="%s">%s</span>` $tooltip .OldProject.Title}} 587 590 {{end}} 588 591 {{$newProjectDisplayHtml := "Unknown Project"}} 589 592 {{if .Project}} 590 - {{$trKey := printf "projects.type-%d.display_name" .Project.Type}} 591 - {{$newProjectDisplayHtml = HTMLFormat `<span data-tooltip-content="%s">%s</span>` (ctx.Locale.Tr $trKey) .Project.Title}} 593 + {{$tooltip := ctx.Locale.Tr "projects.deleted.display_name"}} 594 + {{if not .Project.IsGhost}} 595 + {{$tooltip = ctx.Locale.Tr (printf "projects.type-%d.display_name" .Project.Type)}} 596 + {{end}} 597 + {{$newProjectDisplayHtml = HTMLFormat `<span data-tooltip-content="%s">%s</span>` $tooltip .Project.Title}} 592 598 {{end}} 593 599 {{if and (gt .OldProjectID 0) (gt .ProjectID 0)}} 594 600 {{ctx.Locale.Tr "repo.issues.change_project_at" $oldProjectDisplayHtml $newProjectDisplayHtml $createdStr}}
+38 -33
templates/swagger/v1_json.tmpl
··· 4598 4598 } 4599 4599 } 4600 4600 }, 4601 + "/repos/{owner}/{repo}/actions/runners/registration-token": { 4602 + "get": { 4603 + "produces": [ 4604 + "application/json" 4605 + ], 4606 + "tags": [ 4607 + "repository" 4608 + ], 4609 + "summary": "Get a repository's actions runner registration token", 4610 + "operationId": "repoGetRunnerRegistrationToken", 4611 + "parameters": [ 4612 + { 4613 + "type": "string", 4614 + "description": "owner of the repo", 4615 + "name": "owner", 4616 + "in": "path", 4617 + "required": true 4618 + }, 4619 + { 4620 + "type": "string", 4621 + "description": "name of the repo", 4622 + "name": "repo", 4623 + "in": "path", 4624 + "required": true 4625 + } 4626 + ], 4627 + "responses": { 4628 + "200": { 4629 + "$ref": "#/responses/RegistrationToken" 4630 + } 4631 + } 4632 + } 4633 + }, 4601 4634 "/repos/{owner}/{repo}/actions/secrets": { 4602 4635 "get": { 4603 4636 "produces": [ ··· 14739 14772 } 14740 14773 } 14741 14774 }, 14742 - "/repos/{owner}/{repo}/runners/registration-token": { 14743 - "get": { 14744 - "produces": [ 14745 - "application/json" 14746 - ], 14747 - "tags": [ 14748 - "repository" 14749 - ], 14750 - "summary": "Get a repository's actions runner registration token", 14751 - "operationId": "repoGetRunnerRegistrationToken", 14752 - "parameters": [ 14753 - { 14754 - "type": "string", 14755 - "description": "owner of the repo", 14756 - "name": "owner", 14757 - "in": "path", 14758 - "required": true 14759 - }, 14760 - { 14761 - "type": "string", 14762 - "description": "name of the repo", 14763 - "name": "repo", 14764 - "in": "path", 14765 - "required": true 14766 - } 14767 - ], 14768 - "responses": { 14769 - "200": { 14770 - "$ref": "#/responses/RegistrationToken" 14771 - } 14772 - } 14773 - } 14774 - }, 14775 14775 "/repos/{owner}/{repo}/signing-key.gpg": { 14776 14776 "get": { 14777 14777 "produces": [ ··· 19959 19959 "properties": { 19960 19960 "permission": { 19961 19961 "type": "string", 19962 + "enum": [ 19963 + "read", 19964 + "write", 19965 + "admin" 19966 + ], 19962 19967 "x-go-name": "Permission" 19963 19968 } 19964 19969 },
+2 -2
tests/integration/api_user_variables_test.go
··· 19 19 session := loginUser(t, "user1") 20 20 token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser) 21 21 22 - t.Run("CreateRepoVariable", func(t *testing.T) { 22 + t.Run("CreateUserVariable", func(t *testing.T) { 23 23 cases := []struct { 24 24 Name string 25 25 ExpectedStatus int ··· 70 70 } 71 71 }) 72 72 73 - t.Run("UpdateRepoVariable", func(t *testing.T) { 73 + t.Run("UpdateUserVariable", func(t *testing.T) { 74 74 variableName := "test_update_var" 75 75 url := fmt.Sprintf("/api/v1/user/actions/variables/%s", variableName) 76 76 req := NewRequestWithJSON(t, "POST", url, api.CreateVariableOption{