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-20 cherry pick (gitea-github/main -> forgejo)' (#3729) from earl-warren/wcp/2024-20 into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/3729
Reviewed-by: twenty-panda <twenty-panda@noreply.codeberg.org>

+803 -325
-3
.deadcode-out
··· 186 186 func (ErrExecTimeout).Error 187 187 func (ErrUnsupportedVersion).Error 188 188 func SetUpdateHook 189 - func GetObjectFormatOfRepo 190 189 func openRepositoryWithDefaultContext 191 190 func IsTagExist 192 191 func ToEntryMode ··· 325 324 326 325 package "code.gitea.io/gitea/routers/web/org" 327 326 func MustEnableProjects 328 - func getActionIssues 329 - func UpdateIssueProject 330 327 331 328 package "code.gitea.io/gitea/services/context" 332 329 func GetPrivateContext
+3
cmd/hook.go
··· 366 366 isWiki, _ := strconv.ParseBool(os.Getenv(repo_module.EnvRepoIsWiki)) 367 367 repoName := os.Getenv(repo_module.EnvRepoName) 368 368 pusherID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64) 369 + prID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPRID), 10, 64) 369 370 pusherName := os.Getenv(repo_module.EnvPusherName) 370 371 371 372 hookOptions := private.HookOptions{ ··· 375 376 GitObjectDirectory: os.Getenv(private.GitObjectDirectory), 376 377 GitQuarantinePath: os.Getenv(private.GitQuarantinePath), 377 378 GitPushOptions: pushOptions(), 379 + PullRequestID: prID, 380 + PushTrigger: repo_module.PushTrigger(os.Getenv(repo_module.EnvPushTrigger)), 378 381 } 379 382 oldCommitIDs := make([]string, hookBatchSize) 380 383 newCommitIDs := make([]string, hookBatchSize)
+16 -8
cmd/migrate_storage.go
··· 34 34 Name: "type", 35 35 Aliases: []string{"t"}, 36 36 Value: "", 37 - Usage: "Type of stored files to copy. Allowed types: 'attachments', 'lfs', 'avatars', 'repo-avatars', 'repo-archivers', 'packages', 'actions-log'", 37 + Usage: "Type of stored files to copy. Allowed types: 'attachments', 'lfs', 'avatars', 'repo-avatars', 'repo-archivers', 'packages', 'actions-log', 'actions-artifacts", 38 38 }, 39 39 &cli.StringFlag{ 40 40 Name: "storage", ··· 160 160 }) 161 161 } 162 162 163 + func migrateActionsArtifacts(ctx context.Context, dstStorage storage.ObjectStorage) error { 164 + return db.Iterate(ctx, nil, func(ctx context.Context, artifact *actions_model.ActionArtifact) error { 165 + _, err := storage.Copy(dstStorage, artifact.ArtifactPath, storage.ActionsArtifacts, artifact.ArtifactPath) 166 + return err 167 + }) 168 + } 169 + 163 170 func runMigrateStorage(ctx *cli.Context) error { 164 171 stdCtx, cancel := installSignals() 165 172 defer cancel() ··· 223 230 } 224 231 225 232 migratedMethods := map[string]func(context.Context, storage.ObjectStorage) error{ 226 - "attachments": migrateAttachments, 227 - "lfs": migrateLFS, 228 - "avatars": migrateAvatars, 229 - "repo-avatars": migrateRepoAvatars, 230 - "repo-archivers": migrateRepoArchivers, 231 - "packages": migratePackages, 232 - "actions-log": migrateActionsLog, 233 + "attachments": migrateAttachments, 234 + "lfs": migrateLFS, 235 + "avatars": migrateAvatars, 236 + "repo-avatars": migrateRepoAvatars, 237 + "repo-archivers": migrateRepoArchivers, 238 + "packages": migratePackages, 239 + "actions-log": migrateActionsLog, 240 + "actions-artifacts": migrateActionsArtifacts, 233 241 } 234 242 235 243 tp := strings.ToLower(ctx.String("type"))
+1
models/db/engine.go
··· 58 58 SumInt(bean any, columnName string) (res int64, err error) 59 59 Sync(...any) error 60 60 Select(string) *xorm.Session 61 + SetExpr(string, any) *xorm.Session 61 62 NotIn(string, ...any) *xorm.Session 62 63 OrderBy(any, ...any) *xorm.Session 63 64 Exist(...any) (bool, error)
+60 -45
models/issues/issue_project.go
··· 5 5 6 6 import ( 7 7 "context" 8 - "fmt" 9 8 10 9 "code.gitea.io/gitea/models/db" 11 10 project_model "code.gitea.io/gitea/models/project" 12 11 user_model "code.gitea.io/gitea/models/user" 12 + "code.gitea.io/gitea/modules/util" 13 13 ) 14 14 15 15 // LoadProject load the project the issue was assigned to ··· 90 90 return issuesMap, nil 91 91 } 92 92 93 - // ChangeProjectAssign changes the project associated with an issue 94 - func ChangeProjectAssign(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID int64) error { 95 - ctx, committer, err := db.TxContext(ctx) 96 - if err != nil { 97 - return err 98 - } 99 - defer committer.Close() 93 + // IssueAssignOrRemoveProject changes the project associated with an issue 94 + // If newProjectID is 0, the issue is removed from the project 95 + func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID, newColumnID int64) error { 96 + return db.WithTx(ctx, func(ctx context.Context) error { 97 + oldProjectID := issue.projectID(ctx) 100 98 101 - if err := addUpdateIssueProject(ctx, issue, doer, newProjectID); err != nil { 102 - return err 103 - } 99 + if err := issue.LoadRepo(ctx); err != nil { 100 + return err 101 + } 104 102 105 - return committer.Commit() 106 - } 103 + // Only check if we add a new project and not remove it. 104 + if newProjectID > 0 { 105 + newProject, err := project_model.GetProjectByID(ctx, newProjectID) 106 + if err != nil { 107 + return err 108 + } 109 + if !newProject.CanBeAccessedByOwnerRepo(issue.Repo.OwnerID, issue.Repo) { 110 + return util.NewPermissionDeniedErrorf("issue %d can't be accessed by project %d", issue.ID, newProject.ID) 111 + } 112 + if newColumnID == 0 { 113 + newDefaultColumn, err := newProject.GetDefaultBoard(ctx) 114 + if err != nil { 115 + return err 116 + } 117 + newColumnID = newDefaultColumn.ID 118 + } 119 + } 107 120 108 - func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID int64) error { 109 - oldProjectID := issue.projectID(ctx) 121 + if _, err := db.GetEngine(ctx).Where("project_issue.issue_id=?", issue.ID).Delete(&project_model.ProjectIssue{}); err != nil { 122 + return err 123 + } 110 124 111 - if err := issue.LoadRepo(ctx); err != nil { 112 - return err 113 - } 114 - 115 - // Only check if we add a new project and not remove it. 116 - if newProjectID > 0 { 117 - newProject, err := project_model.GetProjectByID(ctx, newProjectID) 118 - if err != nil { 119 - return err 125 + if oldProjectID > 0 || newProjectID > 0 { 126 + if _, err := CreateComment(ctx, &CreateCommentOptions{ 127 + Type: CommentTypeProject, 128 + Doer: doer, 129 + Repo: issue.Repo, 130 + Issue: issue, 131 + OldProjectID: oldProjectID, 132 + ProjectID: newProjectID, 133 + }); err != nil { 134 + return err 135 + } 120 136 } 121 - if newProject.RepoID != issue.RepoID && newProject.OwnerID != issue.Repo.OwnerID { 122 - return fmt.Errorf("issue's repository is not the same as project's repository") 137 + if newProjectID == 0 { 138 + return nil 139 + } 140 + if newColumnID == 0 { 141 + panic("newColumnID must not be zero") // shouldn't happen 123 142 } 124 - } 125 143 126 - if _, err := db.GetEngine(ctx).Where("project_issue.issue_id=?", issue.ID).Delete(&project_model.ProjectIssue{}); err != nil { 127 - return err 128 - } 129 - 130 - if oldProjectID > 0 || newProjectID > 0 { 131 - if _, err := CreateComment(ctx, &CreateCommentOptions{ 132 - Type: CommentTypeProject, 133 - Doer: doer, 134 - Repo: issue.Repo, 135 - Issue: issue, 136 - OldProjectID: oldProjectID, 137 - ProjectID: newProjectID, 138 - }); err != nil { 144 + res := struct { 145 + MaxSorting int64 146 + IssueCount int64 147 + }{} 148 + if _, err := db.GetEngine(ctx).Select("max(sorting) as max_sorting, count(*) as issue_count").Table("project_issue"). 149 + Where("project_id=?", newProjectID). 150 + And("project_board_id=?", newColumnID). 151 + Get(&res); err != nil { 139 152 return err 140 153 } 141 - } 142 - 143 - return db.Insert(ctx, &project_model.ProjectIssue{ 144 - IssueID: issue.ID, 145 - ProjectID: newProjectID, 154 + newSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0) 155 + return db.Insert(ctx, &project_model.ProjectIssue{ 156 + IssueID: issue.ID, 157 + ProjectID: newProjectID, 158 + ProjectBoardID: newColumnID, 159 + Sorting: newSorting, 160 + }) 146 161 }) 147 162 }
+2
models/migrations/fixtures/Test_RepositoryFormat/review_state.yml
··· 1 1 - 2 2 id: 1 3 + user_id: 1 4 + pull_id: 1 3 5 commit_sha: 19fe5caf872476db265596eaac1dc35ad1c6422d
+7 -7
models/migrations/v1_22/v286_test.go
··· 19 19 20 20 type CommitStatus struct { 21 21 ID int64 22 - ContextHash string 22 + ContextHash string `xorm:"char(40) index"` 23 23 } 24 24 25 25 type RepoArchiver struct { 26 26 ID int64 27 - RepoID int64 28 - Type int 29 - CommitID string 27 + RepoID int64 `xorm:"index unique(s)"` 28 + Type int `xorm:"unique(s)"` 29 + CommitID string `xorm:"VARCHAR(40) unique(s)"` 30 30 } 31 31 32 32 type ReviewState struct { 33 33 ID int64 34 - CommitSHA string 35 - UserID int64 36 - PullID int64 34 + UserID int64 `xorm:"NOT NULL UNIQUE(pull_commit_user)"` 35 + PullID int64 `xorm:"NOT NULL INDEX UNIQUE(pull_commit_user) DEFAULT 0"` 36 + CommitSHA string `xorm:"NOT NULL VARCHAR(40) UNIQUE(pull_commit_user)"` 37 37 } 38 38 39 39 type Comment struct {
+83 -12
models/project/board.go
··· 5 5 6 6 import ( 7 7 "context" 8 + "errors" 8 9 "fmt" 9 10 "regexp" 10 11 11 12 "code.gitea.io/gitea/models/db" 12 13 "code.gitea.io/gitea/modules/setting" 13 14 "code.gitea.io/gitea/modules/timeutil" 15 + "code.gitea.io/gitea/modules/util" 14 16 15 17 "xorm.io/builder" 16 18 ) ··· 82 84 return int(c) 83 85 } 84 86 87 + func (b *Board) GetIssues(ctx context.Context) ([]*ProjectIssue, error) { 88 + issues := make([]*ProjectIssue, 0, 5) 89 + if err := db.GetEngine(ctx).Where("project_id=?", b.ProjectID). 90 + And("project_board_id=?", b.ID). 91 + OrderBy("sorting, id"). 92 + Find(&issues); err != nil { 93 + return nil, err 94 + } 95 + return issues, nil 96 + } 97 + 85 98 func init() { 86 99 db.RegisterModel(new(Board)) 87 100 } ··· 150 163 return db.Insert(ctx, boards) 151 164 } 152 165 166 + // maxProjectColumns max columns allowed in a project, this should not bigger than 127 167 + // because sorting is int8 in database 168 + const maxProjectColumns = 20 169 + 153 170 // NewBoard adds a new project board to a given project 154 171 func NewBoard(ctx context.Context, board *Board) error { 155 172 if len(board.Color) != 0 && !BoardColorPattern.MatchString(board.Color) { 156 173 return fmt.Errorf("bad color code: %s", board.Color) 157 174 } 158 - 175 + res := struct { 176 + MaxSorting int64 177 + ColumnCount int64 178 + }{} 179 + if _, err := db.GetEngine(ctx).Select("max(sorting) as max_sorting, count(*) as column_count").Table("project_board"). 180 + Where("project_id=?", board.ProjectID).Get(&res); err != nil { 181 + return err 182 + } 183 + if res.ColumnCount >= maxProjectColumns { 184 + return fmt.Errorf("NewBoard: maximum number of columns reached") 185 + } 186 + board.Sorting = int8(util.Iif(res.ColumnCount > 0, res.MaxSorting+1, 0)) 159 187 _, err := db.GetEngine(ctx).Insert(board) 160 188 return err 161 189 } ··· 189 217 return fmt.Errorf("deleteBoardByID: cannot delete default board") 190 218 } 191 219 192 - if err = board.removeIssues(ctx); err != nil { 220 + // move all issues to the default column 221 + project, err := GetProjectByID(ctx, board.ProjectID) 222 + if err != nil { 223 + return err 224 + } 225 + defaultColumn, err := project.GetDefaultBoard(ctx) 226 + if err != nil { 227 + return err 228 + } 229 + 230 + if err = board.moveIssuesToAnotherColumn(ctx, defaultColumn); err != nil { 193 231 return err 194 232 } 195 233 ··· 242 280 // GetBoards fetches all boards related to a project 243 281 func (p *Project) GetBoards(ctx context.Context) (BoardList, error) { 244 282 boards := make([]*Board, 0, 5) 245 - 246 - if err := db.GetEngine(ctx).Where("project_id=? AND `default`=?", p.ID, false).OrderBy("sorting").Find(&boards); err != nil { 283 + if err := db.GetEngine(ctx).Where("project_id=?", p.ID).OrderBy("sorting, id").Find(&boards); err != nil { 247 284 return nil, err 248 285 } 249 286 250 - defaultB, err := p.getDefaultBoard(ctx) 251 - if err != nil { 252 - return nil, err 253 - } 254 - 255 - return append([]*Board{defaultB}, boards...), nil 287 + return boards, nil 256 288 } 257 289 258 - // getDefaultBoard return default board and ensure only one exists 259 - func (p *Project) getDefaultBoard(ctx context.Context) (*Board, error) { 290 + // GetDefaultBoard return default board and ensure only one exists 291 + func (p *Project) GetDefaultBoard(ctx context.Context) (*Board, error) { 260 292 var board Board 261 293 has, err := db.GetEngine(ctx). 262 294 Where("project_id=? AND `default` = ?", p.ID, true). ··· 316 348 return nil 317 349 }) 318 350 } 351 + 352 + func GetColumnsByIDs(ctx context.Context, projectID int64, columnsIDs []int64) (BoardList, error) { 353 + columns := make([]*Board, 0, 5) 354 + if err := db.GetEngine(ctx). 355 + Where("project_id =?", projectID). 356 + In("id", columnsIDs). 357 + OrderBy("sorting").Find(&columns); err != nil { 358 + return nil, err 359 + } 360 + return columns, nil 361 + } 362 + 363 + // MoveColumnsOnProject sorts columns in a project 364 + func MoveColumnsOnProject(ctx context.Context, project *Project, sortedColumnIDs map[int64]int64) error { 365 + return db.WithTx(ctx, func(ctx context.Context) error { 366 + sess := db.GetEngine(ctx) 367 + columnIDs := util.ValuesOfMap(sortedColumnIDs) 368 + movedColumns, err := GetColumnsByIDs(ctx, project.ID, columnIDs) 369 + if err != nil { 370 + return err 371 + } 372 + if len(movedColumns) != len(sortedColumnIDs) { 373 + return errors.New("some columns do not exist") 374 + } 375 + 376 + for _, column := range movedColumns { 377 + if column.ProjectID != project.ID { 378 + return fmt.Errorf("column[%d]'s projectID is not equal to project's ID [%d]", column.ProjectID, project.ID) 379 + } 380 + } 381 + 382 + for sorting, columnID := range sortedColumnIDs { 383 + if _, err := sess.Exec("UPDATE `project_board` SET sorting=? WHERE id=?", sorting, columnID); err != nil { 384 + return err 385 + } 386 + } 387 + return nil 388 + }) 389 + }
+85 -2
models/project/board_test.go
··· 4 4 package project 5 5 6 6 import ( 7 + "fmt" 8 + "strings" 7 9 "testing" 8 10 9 11 "code.gitea.io/gitea/models/db" ··· 19 21 assert.NoError(t, err) 20 22 21 23 // check if default board was added 22 - board, err := projectWithoutDefault.getDefaultBoard(db.DefaultContext) 24 + board, err := projectWithoutDefault.GetDefaultBoard(db.DefaultContext) 23 25 assert.NoError(t, err) 24 26 assert.Equal(t, int64(5), board.ProjectID) 25 27 assert.Equal(t, "Uncategorized", board.Title) ··· 28 30 assert.NoError(t, err) 29 31 30 32 // check if multiple defaults were removed 31 - board, err = projectWithMultipleDefaults.getDefaultBoard(db.DefaultContext) 33 + board, err = projectWithMultipleDefaults.GetDefaultBoard(db.DefaultContext) 32 34 assert.NoError(t, err) 33 35 assert.Equal(t, int64(6), board.ProjectID) 34 36 assert.Equal(t, int64(9), board.ID) ··· 42 44 assert.Equal(t, int64(6), board.ProjectID) 43 45 assert.False(t, board.Default) 44 46 } 47 + 48 + func Test_moveIssuesToAnotherColumn(t *testing.T) { 49 + assert.NoError(t, unittest.PrepareTestDatabase()) 50 + 51 + column1 := unittest.AssertExistsAndLoadBean(t, &Board{ID: 1, ProjectID: 1}) 52 + 53 + issues, err := column1.GetIssues(db.DefaultContext) 54 + assert.NoError(t, err) 55 + assert.Len(t, issues, 1) 56 + assert.EqualValues(t, 1, issues[0].ID) 57 + 58 + column2 := unittest.AssertExistsAndLoadBean(t, &Board{ID: 2, ProjectID: 1}) 59 + issues, err = column2.GetIssues(db.DefaultContext) 60 + assert.NoError(t, err) 61 + assert.Len(t, issues, 1) 62 + assert.EqualValues(t, 3, issues[0].ID) 63 + 64 + err = column1.moveIssuesToAnotherColumn(db.DefaultContext, column2) 65 + assert.NoError(t, err) 66 + 67 + issues, err = column1.GetIssues(db.DefaultContext) 68 + assert.NoError(t, err) 69 + assert.Len(t, issues, 0) 70 + 71 + issues, err = column2.GetIssues(db.DefaultContext) 72 + assert.NoError(t, err) 73 + assert.Len(t, issues, 2) 74 + assert.EqualValues(t, 3, issues[0].ID) 75 + assert.EqualValues(t, 0, issues[0].Sorting) 76 + assert.EqualValues(t, 1, issues[1].ID) 77 + assert.EqualValues(t, 1, issues[1].Sorting) 78 + } 79 + 80 + func Test_MoveColumnsOnProject(t *testing.T) { 81 + assert.NoError(t, unittest.PrepareTestDatabase()) 82 + 83 + project1 := unittest.AssertExistsAndLoadBean(t, &Project{ID: 1}) 84 + columns, err := project1.GetBoards(db.DefaultContext) 85 + assert.NoError(t, err) 86 + assert.Len(t, columns, 3) 87 + assert.EqualValues(t, 0, columns[0].Sorting) // even if there is no default sorting, the code should also work 88 + assert.EqualValues(t, 0, columns[1].Sorting) 89 + assert.EqualValues(t, 0, columns[2].Sorting) 90 + 91 + err = MoveColumnsOnProject(db.DefaultContext, project1, map[int64]int64{ 92 + 0: columns[1].ID, 93 + 1: columns[2].ID, 94 + 2: columns[0].ID, 95 + }) 96 + assert.NoError(t, err) 97 + 98 + columnsAfter, err := project1.GetBoards(db.DefaultContext) 99 + assert.NoError(t, err) 100 + assert.Len(t, columnsAfter, 3) 101 + assert.EqualValues(t, columns[1].ID, columnsAfter[0].ID) 102 + assert.EqualValues(t, columns[2].ID, columnsAfter[1].ID) 103 + assert.EqualValues(t, columns[0].ID, columnsAfter[2].ID) 104 + } 105 + 106 + func Test_NewBoard(t *testing.T) { 107 + assert.NoError(t, unittest.PrepareTestDatabase()) 108 + 109 + project1 := unittest.AssertExistsAndLoadBean(t, &Project{ID: 1}) 110 + columns, err := project1.GetBoards(db.DefaultContext) 111 + assert.NoError(t, err) 112 + assert.Len(t, columns, 3) 113 + 114 + for i := 0; i < maxProjectColumns-3; i++ { 115 + err := NewBoard(db.DefaultContext, &Board{ 116 + Title: fmt.Sprintf("board-%d", i+4), 117 + ProjectID: project1.ID, 118 + }) 119 + assert.NoError(t, err) 120 + } 121 + err = NewBoard(db.DefaultContext, &Board{ 122 + Title: "board-21", 123 + ProjectID: project1.ID, 124 + }) 125 + assert.Error(t, err) 126 + assert.True(t, strings.Contains(err.Error(), "maximum number of columns reached")) 127 + }
+43 -8
models/project/issue.go
··· 9 9 10 10 "code.gitea.io/gitea/models/db" 11 11 "code.gitea.io/gitea/modules/log" 12 + "code.gitea.io/gitea/modules/util" 12 13 ) 13 14 14 15 // ProjectIssue saves relation from issue to a project ··· 17 18 IssueID int64 `xorm:"INDEX"` 18 19 ProjectID int64 `xorm:"INDEX"` 19 20 20 - // If 0, then it has not been added to a specific board in the project 21 + // ProjectBoardID should not be zero since 1.22. If it's zero, the issue will not be displayed on UI and it might result in errors. 21 22 ProjectBoardID int64 `xorm:"INDEX"` 22 23 23 24 // the sorting order on the board ··· 79 80 func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, sortedIssueIDs map[int64]int64) error { 80 81 return db.WithTx(ctx, func(ctx context.Context) error { 81 82 sess := db.GetEngine(ctx) 83 + issueIDs := util.ValuesOfMap(sortedIssueIDs) 82 84 83 - issueIDs := make([]int64, 0, len(sortedIssueIDs)) 84 - for _, issueID := range sortedIssueIDs { 85 - issueIDs = append(issueIDs, issueID) 86 - } 87 85 count, err := sess.Table(new(ProjectIssue)).Where("project_id=?", board.ProjectID).In("issue_id", issueIDs).Count() 88 86 if err != nil { 89 87 return err ··· 102 100 }) 103 101 } 104 102 105 - func (b *Board) removeIssues(ctx context.Context) error { 106 - _, err := db.GetEngine(ctx).Exec("UPDATE `project_issue` SET project_board_id = 0 WHERE project_board_id = ? ", b.ID) 107 - return err 103 + func (b *Board) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Board) error { 104 + if b.ProjectID != newColumn.ProjectID { 105 + return fmt.Errorf("columns have to be in the same project") 106 + } 107 + 108 + if b.ID == newColumn.ID { 109 + return nil 110 + } 111 + 112 + res := struct { 113 + MaxSorting int64 114 + IssueCount int64 115 + }{} 116 + if _, err := db.GetEngine(ctx).Select("max(sorting) as max_sorting, count(*) as issue_count"). 117 + Table("project_issue"). 118 + Where("project_id=?", newColumn.ProjectID). 119 + And("project_board_id=?", newColumn.ID). 120 + Get(&res); err != nil { 121 + return err 122 + } 123 + 124 + issues, err := b.GetIssues(ctx) 125 + if err != nil { 126 + return err 127 + } 128 + if len(issues) == 0 { 129 + return nil 130 + } 131 + 132 + nextSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0) 133 + return db.WithTx(ctx, func(ctx context.Context) error { 134 + for i, issue := range issues { 135 + issue.ProjectBoardID = newColumn.ID 136 + issue.Sorting = nextSorting + int64(i) 137 + if _, err := db.GetEngine(ctx).ID(issue.ID).Cols("project_board_id", "sorting").Update(issue); err != nil { 138 + return err 139 + } 140 + } 141 + return nil 142 + }) 108 143 }
+7
models/project/project.go
··· 161 161 return p.Type == TypeRepository 162 162 } 163 163 164 + func (p *Project) CanBeAccessedByOwnerRepo(ownerID int64, repo *repo_model.Repository) bool { 165 + if p.Type == TypeRepository { 166 + return repo != nil && p.RepoID == repo.ID // if a project belongs to a repository, then its OwnerID is 0 and can be ignored 167 + } 168 + return p.OwnerID == ownerID && p.RepoID == 0 169 + } 170 + 164 171 func init() { 165 172 db.RegisterModel(new(Project)) 166 173 }
+2 -2
models/repo/search.go
··· 8 8 // SearchOrderByMap represents all possible search order 9 9 var SearchOrderByMap = map[string]map[string]db.SearchOrderBy{ 10 10 "asc": { 11 - "alpha": db.SearchOrderByAlphabetically, 11 + "alpha": "owner_name ASC, name ASC", 12 12 "created": db.SearchOrderByOldest, 13 13 "updated": db.SearchOrderByLeastUpdated, 14 14 "size": db.SearchOrderBySize, 15 15 "id": db.SearchOrderByID, 16 16 }, 17 17 "desc": { 18 - "alpha": db.SearchOrderByAlphabeticallyReverse, 18 + "alpha": "owner_name DESC, name DESC", 19 19 "created": db.SearchOrderByNewest, 20 20 "updated": db.SearchOrderByRecentUpdated, 21 21 "size": db.SearchOrderBySizeReverse,
-27
modules/git/repo.go
··· 7 7 import ( 8 8 "bytes" 9 9 "context" 10 - "errors" 11 10 "fmt" 12 11 "io" 13 12 "net/url" ··· 61 60 func IsRepoURLAccessible(ctx context.Context, url string) bool { 62 61 _, _, err := NewCommand(ctx, "ls-remote", "-q", "-h").AddDynamicArguments(url, "HEAD").RunStdString(nil) 63 62 return err == nil 64 - } 65 - 66 - // GetObjectFormatOfRepo returns the hash type of repository at a given path 67 - func GetObjectFormatOfRepo(ctx context.Context, repoPath string) (ObjectFormat, error) { 68 - var stdout, stderr strings.Builder 69 - 70 - err := NewCommand(ctx, "hash-object", "--stdin").Run(&RunOpts{ 71 - Dir: repoPath, 72 - Stdout: &stdout, 73 - Stderr: &stderr, 74 - Stdin: &strings.Reader{}, 75 - }) 76 - if err != nil { 77 - return nil, err 78 - } 79 - 80 - if stderr.Len() > 0 { 81 - return nil, errors.New(stderr.String()) 82 - } 83 - 84 - h, err := NewIDFromString(strings.TrimRight(stdout.String(), "\n")) 85 - if err != nil { 86 - return nil, err 87 - } 88 - 89 - return h.Type(), nil 90 63 } 91 64 92 65 // InitRepository initializes a new Git repository.
+2
modules/private/hook.go
··· 11 11 "time" 12 12 13 13 "code.gitea.io/gitea/modules/git" 14 + "code.gitea.io/gitea/modules/repository" 14 15 "code.gitea.io/gitea/modules/setting" 15 16 ) 16 17 ··· 53 54 GitQuarantinePath string 54 55 GitPushOptions GitPushOptions 55 56 PullRequestID int64 57 + PushTrigger repository.PushTrigger 56 58 DeployKeyID int64 // if the pusher is a DeployKey, then UserID is the repo's org user. 57 59 IsWiki bool 58 60 ActionPerm int
+10
modules/repository/branch.go
··· 5 5 6 6 import ( 7 7 "context" 8 + "fmt" 8 9 9 10 "code.gitea.io/gitea/models/db" 10 11 git_model "code.gitea.io/gitea/models/git" ··· 36 37 } 37 38 38 39 func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, doerID int64) (int64, error) { 40 + objFmt, err := gitRepo.GetObjectFormat() 41 + if err != nil { 42 + return 0, fmt.Errorf("GetObjectFormat: %w", err) 43 + } 44 + _, err = db.GetEngine(ctx).ID(repo.ID).Update(&repo_model.Repository{ObjectFormatName: objFmt.Name()}) 45 + if err != nil { 46 + return 0, fmt.Errorf("UpdateRepository: %w", err) 47 + } 48 + 39 49 allBranches := container.Set[string]{} 40 50 { 41 51 branches, _, err := gitRepo.GetBranchNames(0, 0)
+31
modules/repository/branch_test.go
··· 1 + // Copyright 2024 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package repository 5 + 6 + import ( 7 + "testing" 8 + 9 + "code.gitea.io/gitea/models/db" 10 + git_model "code.gitea.io/gitea/models/git" 11 + repo_model "code.gitea.io/gitea/models/repo" 12 + "code.gitea.io/gitea/models/unittest" 13 + 14 + "github.com/stretchr/testify/assert" 15 + ) 16 + 17 + func TestSyncRepoBranches(t *testing.T) { 18 + assert.NoError(t, unittest.PrepareTestDatabase()) 19 + _, err := db.GetEngine(db.DefaultContext).ID(1).Update(&repo_model.Repository{ObjectFormatName: "bad-fmt"}) 20 + assert.NoError(t, db.TruncateBeans(db.DefaultContext, &git_model.Branch{})) 21 + assert.NoError(t, err) 22 + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) 23 + assert.Equal(t, "bad-fmt", repo.ObjectFormatName) 24 + _, err = SyncRepoBranches(db.DefaultContext, 1, 0) 25 + assert.NoError(t, err) 26 + repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) 27 + assert.Equal(t, "sha1", repo.ObjectFormatName) 28 + branch, err := git_model.GetBranch(db.DefaultContext, 1, "master") 29 + assert.NoError(t, err) 30 + assert.EqualValues(t, "master", branch.Name) 31 + }
+8
modules/repository/env.go
··· 25 25 EnvKeyID = "GITEA_KEY_ID" // public key ID 26 26 EnvDeployKeyID = "GITEA_DEPLOY_KEY_ID" 27 27 EnvPRID = "GITEA_PR_ID" 28 + EnvPushTrigger = "GITEA_PUSH_TRIGGER" 28 29 EnvIsInternal = "GITEA_INTERNAL_PUSH" 29 30 EnvAppURL = "GITEA_ROOT_URL" 30 31 EnvActionPerm = "GITEA_ACTION_PERM" 32 + ) 33 + 34 + type PushTrigger string 35 + 36 + const ( 37 + PushTriggerPRMergeToBase PushTrigger = "pr-merge-to-base" 38 + PushTriggerPRUpdateWithBase PushTrigger = "pr-update-with-base" 31 39 ) 32 40 33 41 // InternalPushingEnvironment returns an os environment to switch off hooks on push
+1
release-notes/8.0.0/feat/3729.md
··· 1 + - [PR](https://github.com/go-gitea/gitea/pull/30874): add actions-artifacts to the [storage migrate CLI](https://forgejo.org/docs/v8.0/admin/command-line/#migrate).
+2
release-notes/8.0.0/fix/3729.md
··· 1 + - [PR](https://github.com/go-gitea/gitea/pull/30912): when adopting a repository, the default branch is not taken into account 2 + - [PR](https://github.com/go-gitea/gitea/pull/30715): pull request search shows closed pull requests in the open tab
-13
routers/api/actions/runner/interceptor.go
··· 23 23 const ( 24 24 uuidHeaderKey = "x-runner-uuid" 25 25 tokenHeaderKey = "x-runner-token" 26 - // Deprecated: will be removed after Gitea 1.20 released. 27 - versionHeaderKey = "x-runner-version" 28 26 ) 29 27 30 28 var withRunner = connect.WithInterceptors(connect.UnaryInterceptorFunc(func(unaryFunc connect.UnaryFunc) connect.UnaryFunc { ··· 35 33 } 36 34 uuid := request.Header().Get(uuidHeaderKey) 37 35 token := request.Header().Get(tokenHeaderKey) 38 - // TODO: version will be removed from request header after Gitea 1.20 released. 39 - // And Gitea will not try to read version from request header 40 - version := request.Header().Get(versionHeaderKey) 41 36 42 37 runner, err := actions_model.GetRunnerByUUID(ctx, uuid) 43 38 if err != nil { ··· 51 46 } 52 47 53 48 cols := []string{"last_online"} 54 - 55 - // TODO: version will be removed from request header after Gitea 1.20 released. 56 - // And Gitea will not try to read version from request header 57 - version, _ = util.SplitStringAtByteN(version, 64) 58 - if !util.IsEmptyString(version) && runner.Version != version { 59 - runner.Version = version 60 - cols = append(cols, "version") 61 - } 62 49 runner.LastOnline = timeutil.TimeStampNow() 63 50 if methodName == "UpdateTask" || methodName == "UpdateLog" { 64 51 runner.LastActive = timeutil.TimeStampNow()
-6
routers/api/actions/runner/runner.go
··· 69 69 } 70 70 71 71 labels := req.Msg.Labels 72 - // TODO: agent_labels should be removed from pb after Gitea 1.20 released. 73 - // Old version runner's agent_labels slice is not empty and labels slice is empty. 74 - // And due to compatibility with older versions, it is temporarily marked as Deprecated in pb, so use `//nolint` here. 75 - if len(req.Msg.AgentLabels) > 0 && len(req.Msg.Labels) == 0 { //nolint:staticcheck 76 - labels = req.Msg.AgentLabels //nolint:staticcheck 77 - } 78 72 79 73 // create new runner 80 74 name, _ := util.SplitStringAtByteN(req.Msg.Name, 255)
+2 -2
routers/api/v1/repo/pull.go
··· 878 878 } 879 879 880 880 // start with merging by checking 881 - if err := pull_service.CheckPullMergable(ctx, ctx.Doer, &ctx.Repo.Permission, pr, mergeCheckType, form.ForceMerge); err != nil { 881 + if err := pull_service.CheckPullMergeable(ctx, ctx.Doer, &ctx.Repo.Permission, pr, mergeCheckType, form.ForceMerge); err != nil { 882 882 if errors.Is(err, pull_service.ErrIsClosed) { 883 883 ctx.NotFound() 884 884 } else if errors.Is(err, pull_service.ErrUserNotAllowedToMerge) { ··· 887 887 ctx.Error(http.StatusMethodNotAllowed, "PR already merged", "") 888 888 } else if errors.Is(err, pull_service.ErrIsWorkInProgress) { 889 889 ctx.Error(http.StatusMethodNotAllowed, "PR is a work in progress", "Work in progress PRs cannot be merged") 890 - } else if errors.Is(err, pull_service.ErrNotMergableState) { 890 + } else if errors.Is(err, pull_service.ErrNotMergeableState) { 891 891 ctx.Error(http.StatusMethodNotAllowed, "PR not in mergeable state", "Please try again later") 892 892 } else if models.IsErrDisallowedToMerge(err) { 893 893 ctx.Error(http.StatusMethodNotAllowed, "PR is not ready to be merged", err)
+63
routers/private/hook_post_receive.go
··· 4 4 package private 5 5 6 6 import ( 7 + "context" 7 8 "fmt" 8 9 "net/http" 9 10 "strconv" 10 11 "time" 11 12 13 + "code.gitea.io/gitea/models/db" 12 14 git_model "code.gitea.io/gitea/models/git" 13 15 issues_model "code.gitea.io/gitea/models/issues" 16 + pull_model "code.gitea.io/gitea/models/pull" 14 17 repo_model "code.gitea.io/gitea/models/repo" 18 + user_model "code.gitea.io/gitea/models/user" 19 + "code.gitea.io/gitea/modules/cache" 15 20 "code.gitea.io/gitea/modules/git" 16 21 "code.gitea.io/gitea/modules/gitrepo" 17 22 "code.gitea.io/gitea/modules/log" 18 23 "code.gitea.io/gitea/modules/private" 19 24 repo_module "code.gitea.io/gitea/modules/repository" 20 25 "code.gitea.io/gitea/modules/setting" 26 + timeutil "code.gitea.io/gitea/modules/timeutil" 21 27 "code.gitea.io/gitea/modules/util" 22 28 "code.gitea.io/gitea/modules/web" 23 29 gitea_context "code.gitea.io/gitea/services/context" ··· 155 161 } 156 162 } 157 163 164 + // handle pull request merging, a pull request action should push at least 1 commit 165 + if opts.PushTrigger == repo_module.PushTriggerPRMergeToBase { 166 + handlePullRequestMerging(ctx, opts, ownerName, repoName, updates) 167 + if ctx.Written() { 168 + return 169 + } 170 + } 171 + 158 172 // Handle Push Options 159 173 if len(opts.GitPushOptions) > 0 { 160 174 // load the repository ··· 302 316 RepoWasEmpty: wasEmpty, 303 317 }) 304 318 } 319 + 320 + func loadContextCacheUser(ctx context.Context, id int64) (*user_model.User, error) { 321 + return cache.GetWithContextCache(ctx, "hook_post_receive_user", id, func() (*user_model.User, error) { 322 + return user_model.GetUserByID(ctx, id) 323 + }) 324 + } 325 + 326 + // handlePullRequestMerging handle pull request merging, a pull request action should push at least 1 commit 327 + func handlePullRequestMerging(ctx *gitea_context.PrivateContext, opts *private.HookOptions, ownerName, repoName string, updates []*repo_module.PushUpdateOptions) { 328 + if len(updates) == 0 { 329 + ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{ 330 + Err: fmt.Sprintf("Pushing a merged PR (pr:%d) no commits pushed ", opts.PullRequestID), 331 + }) 332 + return 333 + } 334 + 335 + pr, err := issues_model.GetPullRequestByID(ctx, opts.PullRequestID) 336 + if err != nil { 337 + log.Error("GetPullRequestByID[%d]: %v", opts.PullRequestID, err) 338 + ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "GetPullRequestByID failed"}) 339 + return 340 + } 341 + 342 + pusher, err := loadContextCacheUser(ctx, opts.UserID) 343 + if err != nil { 344 + log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err) 345 + ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "Load pusher user failed"}) 346 + return 347 + } 348 + 349 + pr.MergedCommitID = updates[len(updates)-1].NewCommitID 350 + pr.MergedUnix = timeutil.TimeStampNow() 351 + pr.Merger = pusher 352 + pr.MergerID = pusher.ID 353 + err = db.WithTx(ctx, func(ctx context.Context) error { 354 + // Removing an auto merge pull and ignore if not exist 355 + if err := pull_model.DeleteScheduledAutoMerge(ctx, pr.ID); err != nil && !db.IsErrNotExist(err) { 356 + return fmt.Errorf("DeleteScheduledAutoMerge[%d]: %v", opts.PullRequestID, err) 357 + } 358 + if _, err := pr.SetMerged(ctx); err != nil { 359 + return fmt.Errorf("SetMerged failed: %s/%s Error: %v", ownerName, repoName, err) 360 + } 361 + return nil 362 + }) 363 + if err != nil { 364 + log.Error("Failed to update PR to merged: %v", err) 365 + ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "Failed to update PR to merged"}) 366 + } 367 + }
+49
routers/private/hook_post_receive_test.go
··· 1 + // Copyright 2024 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package private 5 + 6 + import ( 7 + "testing" 8 + 9 + "code.gitea.io/gitea/models/db" 10 + issues_model "code.gitea.io/gitea/models/issues" 11 + pull_model "code.gitea.io/gitea/models/pull" 12 + repo_model "code.gitea.io/gitea/models/repo" 13 + "code.gitea.io/gitea/models/unittest" 14 + user_model "code.gitea.io/gitea/models/user" 15 + "code.gitea.io/gitea/modules/private" 16 + repo_module "code.gitea.io/gitea/modules/repository" 17 + "code.gitea.io/gitea/services/contexttest" 18 + 19 + "github.com/stretchr/testify/assert" 20 + ) 21 + 22 + func TestHandlePullRequestMerging(t *testing.T) { 23 + assert.NoError(t, unittest.PrepareTestDatabase()) 24 + pr, err := issues_model.GetUnmergedPullRequest(db.DefaultContext, 1, 1, "branch2", "master", issues_model.PullRequestFlowGithub) 25 + assert.NoError(t, err) 26 + assert.NoError(t, pr.LoadBaseRepo(db.DefaultContext)) 27 + 28 + user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) 29 + 30 + err = pull_model.ScheduleAutoMerge(db.DefaultContext, user1, pr.ID, repo_model.MergeStyleSquash, "squash merge a pr") 31 + assert.NoError(t, err) 32 + 33 + autoMerge := unittest.AssertExistsAndLoadBean(t, &pull_model.AutoMerge{PullID: pr.ID}) 34 + 35 + ctx, resp := contexttest.MockPrivateContext(t, "/") 36 + handlePullRequestMerging(ctx, &private.HookOptions{ 37 + PullRequestID: pr.ID, 38 + UserID: 2, 39 + }, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, []*repo_module.PushUpdateOptions{ 40 + {NewCommitID: "01234567"}, 41 + }) 42 + assert.Equal(t, 0, len(resp.Body.String())) 43 + pr, err = issues_model.GetPullRequestByID(db.DefaultContext, pr.ID) 44 + assert.NoError(t, err) 45 + assert.True(t, pr.HasMerged) 46 + assert.EqualValues(t, "01234567", pr.MergedCommitID) 47 + 48 + unittest.AssertNotExistsBean(t, &pull_model.AutoMerge{ID: autoMerge.ID}) 49 + }
-69
routers/web/org/projects.go
··· 7 7 "errors" 8 8 "fmt" 9 9 "net/http" 10 - "strconv" 11 10 "strings" 12 11 13 12 "code.gitea.io/gitea/models/db" ··· 388 387 } 389 388 390 389 ctx.HTML(http.StatusOK, tplProjectsView) 391 - } 392 - 393 - func getActionIssues(ctx *context.Context) issues_model.IssueList { 394 - commaSeparatedIssueIDs := ctx.FormString("issue_ids") 395 - if len(commaSeparatedIssueIDs) == 0 { 396 - return nil 397 - } 398 - issueIDs := make([]int64, 0, 10) 399 - for _, stringIssueID := range strings.Split(commaSeparatedIssueIDs, ",") { 400 - issueID, err := strconv.ParseInt(stringIssueID, 10, 64) 401 - if err != nil { 402 - ctx.ServerError("ParseInt", err) 403 - return nil 404 - } 405 - issueIDs = append(issueIDs, issueID) 406 - } 407 - issues, err := issues_model.GetIssuesByIDs(ctx, issueIDs) 408 - if err != nil { 409 - ctx.ServerError("GetIssuesByIDs", err) 410 - return nil 411 - } 412 - // Check access rights for all issues 413 - issueUnitEnabled := ctx.Repo.CanRead(unit.TypeIssues) 414 - prUnitEnabled := ctx.Repo.CanRead(unit.TypePullRequests) 415 - for _, issue := range issues { 416 - if issue.RepoID != ctx.Repo.Repository.ID { 417 - ctx.NotFound("some issue's RepoID is incorrect", errors.New("some issue's RepoID is incorrect")) 418 - return nil 419 - } 420 - if issue.IsPull && !prUnitEnabled || !issue.IsPull && !issueUnitEnabled { 421 - ctx.NotFound("IssueOrPullRequestUnitNotAllowed", nil) 422 - return nil 423 - } 424 - if err = issue.LoadAttributes(ctx); err != nil { 425 - ctx.ServerError("LoadAttributes", err) 426 - return nil 427 - } 428 - } 429 - return issues 430 - } 431 - 432 - // UpdateIssueProject change an issue's project 433 - func UpdateIssueProject(ctx *context.Context) { 434 - issues := getActionIssues(ctx) 435 - if ctx.Written() { 436 - return 437 - } 438 - 439 - if err := issues.LoadProjects(ctx); err != nil { 440 - ctx.ServerError("LoadProjects", err) 441 - return 442 - } 443 - 444 - projectID := ctx.FormInt64("id") 445 - for _, issue := range issues { 446 - if issue.Project != nil { 447 - if issue.Project.ID == projectID { 448 - continue 449 - } 450 - } 451 - 452 - if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID); err != nil { 453 - ctx.ServerError("ChangeProjectAssign", err) 454 - return 455 - } 456 - } 457 - 458 - ctx.JSONOK() 459 390 } 460 391 461 392 // DeleteProjectBoard allows for the deletion of a project board
+2 -2
routers/web/repo/issue.go
··· 1266 1266 ctx.Error(http.StatusBadRequest, "user hasn't permissions to read projects") 1267 1267 return 1268 1268 } 1269 - if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID); err != nil { 1270 - ctx.ServerError("ChangeProjectAssign", err) 1269 + if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, ctx.Doer, projectID, 0); err != nil { 1270 + ctx.ServerError("IssueAssignOrRemoveProject", err) 1271 1271 return 1272 1272 } 1273 1273 }
+11 -6
routers/web/repo/projects.go
··· 21 21 "code.gitea.io/gitea/modules/markup/markdown" 22 22 "code.gitea.io/gitea/modules/optional" 23 23 "code.gitea.io/gitea/modules/setting" 24 + "code.gitea.io/gitea/modules/util" 24 25 "code.gitea.io/gitea/modules/web" 25 26 "code.gitea.io/gitea/services/context" 26 27 "code.gitea.io/gitea/services/forms" ··· 382 383 ctx.ServerError("LoadProjects", err) 383 384 return 384 385 } 386 + if _, err := issues.LoadRepositories(ctx); err != nil { 387 + ctx.ServerError("LoadProjects", err) 388 + return 389 + } 385 390 386 391 projectID := ctx.FormInt64("id") 387 392 for _, issue := range issues { 388 - if issue.Project != nil { 389 - if issue.Project.ID == projectID { 393 + if issue.Project != nil && issue.Project.ID == projectID { 394 + continue 395 + } 396 + if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, ctx.Doer, projectID, 0); err != nil { 397 + if errors.Is(err, util.ErrPermissionDenied) { 390 398 continue 391 399 } 392 - } 393 - 394 - if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID); err != nil { 395 - ctx.ServerError("ChangeProjectAssign", err) 400 + ctx.ServerError("IssueAssignOrRemoveProject", err) 396 401 return 397 402 } 398 403 }
+8 -10
routers/web/repo/pull.go
··· 1218 1218 } 1219 1219 1220 1220 // start with merging by checking 1221 - if err := pull_service.CheckPullMergable(ctx, ctx.Doer, &ctx.Repo.Permission, pr, mergeCheckType, form.ForceMerge); err != nil { 1221 + if err := pull_service.CheckPullMergeable(ctx, ctx.Doer, &ctx.Repo.Permission, pr, mergeCheckType, form.ForceMerge); err != nil { 1222 1222 switch { 1223 1223 case errors.Is(err, pull_service.ErrIsClosed): 1224 1224 if issue.IsPull { ··· 1232 1232 ctx.JSONError(ctx.Tr("repo.pulls.has_merged")) 1233 1233 case errors.Is(err, pull_service.ErrIsWorkInProgress): 1234 1234 ctx.JSONError(ctx.Tr("repo.pulls.no_merge_wip")) 1235 - case errors.Is(err, pull_service.ErrNotMergableState): 1235 + case errors.Is(err, pull_service.ErrNotMergeableState): 1236 1236 ctx.JSONError(ctx.Tr("repo.pulls.no_merge_not_ready")) 1237 1237 case models.IsErrDisallowedToMerge(err): 1238 1238 ctx.JSONError(ctx.Tr("repo.pulls.no_merge_not_ready")) ··· 1537 1537 return 1538 1538 } 1539 1539 1540 - if projectID > 0 { 1541 - if !ctx.Repo.CanWrite(unit.TypeProjects) { 1542 - ctx.Error(http.StatusBadRequest, "user hasn't the permission to write to projects") 1543 - return 1544 - } 1545 - if err := issues_model.ChangeProjectAssign(ctx, pullIssue, ctx.Doer, projectID); err != nil { 1546 - ctx.ServerError("ChangeProjectAssign", err) 1547 - return 1540 + if projectID > 0 && ctx.Repo.CanWrite(unit.TypeProjects) { 1541 + if err := issues_model.IssueAssignOrRemoveProject(ctx, pullIssue, ctx.Doer, projectID, 0); err != nil { 1542 + if !errors.Is(err, util.ErrPermissionDenied) { 1543 + ctx.ServerError("IssueAssignOrRemoveProject", err) 1544 + return 1545 + } 1548 1546 } 1549 1547 } 1550 1548
+8 -2
routers/web/repo/setting/setting.go
··· 798 798 ctx.Repo.GitRepo = nil 799 799 } 800 800 801 + oldFullname := repo.FullName() 801 802 if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, repo, nil); err != nil { 802 803 if errors.Is(err, user_model.ErrBlockedByUser) { 803 804 ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_blocked_doer"), tplSettingsOptions, nil) ··· 812 813 return 813 814 } 814 815 815 - log.Trace("Repository transfer process was started: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newOwner) 816 - ctx.Flash.Success(ctx.Tr("repo.settings.transfer_started", newOwner.DisplayName())) 816 + if ctx.Repo.Repository.Status == repo_model.RepositoryPendingTransfer { 817 + log.Trace("Repository transfer process was started: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newOwner) 818 + ctx.Flash.Success(ctx.Tr("repo.settings.transfer_started", newOwner.DisplayName())) 819 + } else { 820 + log.Trace("Repository transferred: %s -> %s", oldFullname, ctx.Repo.Repository.FullName()) 821 + ctx.Flash.Success(ctx.Tr("repo.settings.transfer_succeed")) 822 + } 817 823 ctx.Redirect(repo.Link() + "/settings") 818 824 819 825 case "cancel_transfer":
+48
routers/web/shared/project/column.go
··· 1 + // Copyright 2024 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package project 5 + 6 + import ( 7 + project_model "code.gitea.io/gitea/models/project" 8 + "code.gitea.io/gitea/modules/json" 9 + "code.gitea.io/gitea/services/context" 10 + ) 11 + 12 + // MoveColumns moves or keeps columns in a project and sorts them inside that project 13 + func MoveColumns(ctx *context.Context) { 14 + project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) 15 + if err != nil { 16 + ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) 17 + return 18 + } 19 + if !project.CanBeAccessedByOwnerRepo(ctx.ContextUser.ID, ctx.Repo.Repository) { 20 + ctx.NotFound("CanBeAccessedByOwnerRepo", nil) 21 + return 22 + } 23 + 24 + type movedColumnsForm struct { 25 + Columns []struct { 26 + ColumnID int64 `json:"columnID"` 27 + Sorting int64 `json:"sorting"` 28 + } `json:"columns"` 29 + } 30 + 31 + form := &movedColumnsForm{} 32 + if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil { 33 + ctx.ServerError("DecodeMovedColumnsForm", err) 34 + return 35 + } 36 + 37 + sortedColumnIDs := make(map[int64]int64) 38 + for _, column := range form.Columns { 39 + sortedColumnIDs[column.Sorting] = column.ColumnID 40 + } 41 + 42 + if err = project_model.MoveColumnsOnProject(ctx, project, sortedColumnIDs); err != nil { 43 + ctx.ServerError("MoveColumnsOnProject", err) 44 + return 45 + } 46 + 47 + ctx.JSONOK() 48 + }
+9 -6
routers/web/web.go
··· 39 39 "code.gitea.io/gitea/routers/web/repo/badges" 40 40 repo_flags "code.gitea.io/gitea/routers/web/repo/flags" 41 41 repo_setting "code.gitea.io/gitea/routers/web/repo/setting" 42 + "code.gitea.io/gitea/routers/web/shared/project" 42 43 "code.gitea.io/gitea/routers/web/user" 43 44 user_setting "code.gitea.io/gitea/routers/web/user/setting" 44 45 "code.gitea.io/gitea/routers/web/user/setting/security" ··· 97 98 // The Session plugin is expected to be executed second, in order to skip authentication 98 99 // for users that have already signed in. 99 100 func buildAuthGroup() *auth_service.Group { 100 - group := auth_service.NewGroup( 101 - &auth_service.OAuth2{}, // FIXME: this should be removed and only applied in download and oauth related routers 102 - &auth_service.Basic{}, // FIXME: this should be removed and only applied in download and git/lfs routers 103 - &auth_service.Session{}, 104 - ) 101 + group := auth_service.NewGroup() 102 + group.Add(&auth_service.OAuth2{}) // FIXME: this should be removed and only applied in download and oauth related routers 103 + group.Add(&auth_service.Basic{}) // FIXME: this should be removed and only applied in download and git/lfs routers 104 + 105 105 if setting.Service.EnableReverseProxyAuth { 106 - group.Add(&auth_service.ReverseProxy{}) 106 + group.Add(&auth_service.ReverseProxy{}) // reverseproxy should before Session, otherwise the header will be ignored if user has login 107 107 } 108 + group.Add(&auth_service.Session{}) 108 109 109 110 if setting.IsWindows && auth_model.IsSSPIEnabled(db.DefaultContext) { 110 111 group.Add(&auth_service.SSPI{}) // it MUST be the last, see the comment of SSPI ··· 976 977 m.Post("/new", web.Bind(forms.CreateProjectForm{}), org.NewProjectPost) 977 978 m.Group("/{id}", func() { 978 979 m.Post("", web.Bind(forms.EditProjectBoardForm{}), org.AddBoardToProjectPost) 980 + m.Post("/move", project.MoveColumns) 979 981 m.Post("/delete", org.DeleteProject) 980 982 981 983 m.Get("/edit", org.RenderEditProject) ··· 1349 1351 m.Post("/new", web.Bind(forms.CreateProjectForm{}), repo.NewProjectPost) 1350 1352 m.Group("/{id}", func() { 1351 1353 m.Post("", web.Bind(forms.EditProjectBoardForm{}), repo.AddBoardToProjectPost) 1354 + m.Post("/move", project.MoveColumns) 1352 1355 m.Post("/delete", repo.DeleteProject) 1353 1356 1354 1357 m.Get("/edit", repo.RenderEditProject)
+2 -2
services/automerge/automerge.go
··· 229 229 return 230 230 } 231 231 232 - if err := pull_service.CheckPullMergable(ctx, doer, &perm, pr, pull_service.MergeCheckTypeGeneral, false); err != nil { 232 + if err := pull_service.CheckPullMergeable(ctx, doer, &perm, pr, pull_service.MergeCheckTypeGeneral, false); err != nil { 233 233 if errors.Is(pull_service.ErrUserNotAllowedToMerge, err) { 234 234 log.Info("%-v was scheduled to automerge by an unauthorized user", pr) 235 235 return 236 236 } 237 - log.Error("%-v CheckPullMergable: %v", pr, err) 237 + log.Error("%-v CheckPullMergeable: %v", pr, err) 238 238 return 239 239 } 240 240
+13
services/contexttest/context_tests.go
··· 86 86 return ctx, resp 87 87 } 88 88 89 + func MockPrivateContext(t *testing.T, reqPath string) (*context.PrivateContext, *httptest.ResponseRecorder) { 90 + resp := httptest.NewRecorder() 91 + req := mockRequest(t, reqPath) 92 + base, baseCleanUp := context.NewBaseContext(resp, req) 93 + base.Data = middleware.GetContextData(req.Context()) 94 + base.Locale = &translation.MockLocale{} 95 + ctx := &context.PrivateContext{Base: base} 96 + _ = baseCleanUp // during test, it doesn't need to do clean up. TODO: this can be improved later 97 + chiCtx := chi.NewRouteContext() 98 + ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx) 99 + return ctx, resp 100 + } 101 + 89 102 // LoadRepo load a repo into a test context. 90 103 func LoadRepo(t *testing.T, ctx gocontext.Context, repoID int64) { 91 104 var doer *user_model.User
+16
services/indexer/notify.go
··· 152 152 func (r *indexerNotifier) IssueClearLabels(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) { 153 153 issue_indexer.UpdateIssueIndexer(ctx, issue.ID) 154 154 } 155 + 156 + func (r *indexerNotifier) MergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) { 157 + if err := pr.LoadIssue(ctx); err != nil { 158 + log.Error("LoadIssue: %v", err) 159 + return 160 + } 161 + issue_indexer.UpdateIssueIndexer(ctx, pr.Issue.ID) 162 + } 163 + 164 + func (r *indexerNotifier) AutoMergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) { 165 + if err := pr.LoadIssue(ctx); err != nil { 166 + log.Error("LoadIssue: %v", err) 167 + return 168 + } 169 + issue_indexer.UpdateIssueIndexer(ctx, pr.Issue.ID) 170 + }
+4 -4
services/pull/check.go
··· 39 39 ErrHasMerged = errors.New("has already been merged") 40 40 ErrIsWorkInProgress = errors.New("work in progress PRs cannot be merged") 41 41 ErrIsChecking = errors.New("cannot merge while conflict checking is in progress") 42 - ErrNotMergableState = errors.New("not in mergeable state") 42 + ErrNotMergeableState = errors.New("not in mergeable state") 43 43 ErrDependenciesLeft = errors.New("is blocked by an open dependency") 44 44 ) 45 45 ··· 66 66 MergeCheckTypeAuto // Auto Merge (Scheduled Merge) After Checks Succeed 67 67 ) 68 68 69 - // CheckPullMergable check if the pull mergeable based on all conditions (branch protection, merge options, ...) 70 - func CheckPullMergable(stdCtx context.Context, doer *user_model.User, perm *access_model.Permission, pr *issues_model.PullRequest, mergeCheckType MergeCheckType, adminSkipProtectionCheck bool) error { 69 + // CheckPullMergeable check if the pull mergeable based on all conditions (branch protection, merge options, ...) 70 + func CheckPullMergeable(stdCtx context.Context, doer *user_model.User, perm *access_model.Permission, pr *issues_model.PullRequest, mergeCheckType MergeCheckType, adminSkipProtectionCheck bool) error { 71 71 return db.WithTx(stdCtx, func(ctx context.Context) error { 72 72 if pr.HasMerged { 73 73 return ErrHasMerged ··· 97 97 } 98 98 99 99 if !pr.CanAutoMerge() && !pr.IsEmpty() { 100 - return ErrNotMergableState 100 + return ErrNotMergeableState 101 101 } 102 102 103 103 if pr.IsChecking() {
+10 -17
services/pull/merge.go
··· 18 18 git_model "code.gitea.io/gitea/models/git" 19 19 issues_model "code.gitea.io/gitea/models/issues" 20 20 access_model "code.gitea.io/gitea/models/perm/access" 21 - pull_model "code.gitea.io/gitea/models/pull" 22 21 repo_model "code.gitea.io/gitea/models/repo" 23 22 "code.gitea.io/gitea/models/unit" 24 23 user_model "code.gitea.io/gitea/models/user" ··· 168 167 pullWorkingPool.CheckIn(fmt.Sprint(pr.ID)) 169 168 defer pullWorkingPool.CheckOut(fmt.Sprint(pr.ID)) 170 169 171 - // Removing an auto merge pull and ignore if not exist 172 - // FIXME: is this the correct point to do this? Shouldn't this be after IsMergeStyleAllowed? 173 - if err := pull_model.DeleteScheduledAutoMerge(ctx, pr.ID); err != nil && !db.IsErrNotExist(err) { 174 - return err 175 - } 176 - 177 170 prUnit, err := pr.BaseRepo.GetUnit(ctx, unit.TypePullRequests) 178 171 if err != nil { 179 172 log.Error("pr.BaseRepo.GetUnit(unit.TypePullRequests): %v", err) ··· 190 183 AddTestPullRequestTask(ctx, doer, pr.BaseRepo.ID, pr.BaseBranch, false, "", "", 0) 191 184 }() 192 185 193 - pr.MergedCommitID, err = doMergeAndPush(ctx, pr, doer, mergeStyle, expectedHeadCommitID, message) 186 + _, err = doMergeAndPush(ctx, pr, doer, mergeStyle, expectedHeadCommitID, message, repo_module.PushTriggerPRMergeToBase) 194 187 if err != nil { 195 188 return err 196 189 } 197 190 198 - pr.MergedUnix = timeutil.TimeStampNow() 199 - pr.Merger = doer 200 - pr.MergerID = doer.ID 201 - 202 - if _, err := pr.SetMerged(ctx); err != nil { 203 - log.Error("SetMerged %-v: %v", pr, err) 191 + // reload pull request because it has been updated by post receive hook 192 + pr, err = issues_model.GetPullRequestByID(ctx, pr.ID) 193 + if err != nil { 194 + return err 204 195 } 205 196 206 197 if err := pr.LoadIssue(ctx); err != nil { ··· 251 242 } 252 243 253 244 // doMergeAndPush performs the merge operation without changing any pull information in database and pushes it up to the base repository 254 - func doMergeAndPush(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string) (string, error) { 245 + func doMergeAndPush(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string, pushTrigger repo_module.PushTrigger) (string, error) { 255 246 // Clone base repo. 256 247 mergeCtx, cancel, err := createTemporaryRepoForMerge(ctx, pr, doer, expectedHeadCommitID) 257 248 if err != nil { ··· 324 315 pr.BaseRepo.Name, 325 316 pr.ID, 326 317 ) 318 + 319 + mergeCtx.env = append(mergeCtx.env, repo_module.EnvPushTrigger+"="+string(pushTrigger)) 327 320 pushCmd := git.NewCommand(ctx, "push", "origin").AddDynamicArguments(baseBranch + ":" + git.BranchPrefix + pr.BaseBranch) 328 321 329 322 // Push back to upstream. 330 - // TODO: this cause an api call to "/api/internal/hook/post-receive/...", 331 - // that prevents us from doint the whole merge in one db transaction 323 + // This cause an api call to "/api/internal/hook/post-receive/...", 324 + // If it's merge, all db transaction and operations should be there but not here to prevent deadlock. 332 325 if err := pushCmd.Run(mergeCtx.RunOpts()); err != nil { 333 326 if strings.Contains(mergeCtx.errbuf.String(), "non-fast-forward") { 334 327 return "", &git.ErrPushOutOfDate{
+2 -1
services/pull/update.go
··· 15 15 user_model "code.gitea.io/gitea/models/user" 16 16 "code.gitea.io/gitea/modules/git" 17 17 "code.gitea.io/gitea/modules/log" 18 + "code.gitea.io/gitea/modules/repository" 18 19 ) 19 20 20 21 // Update updates pull request with base branch. ··· 72 73 BaseBranch: pr.HeadBranch, 73 74 } 74 75 75 - _, err = doMergeAndPush(ctx, reversePR, doer, repo_model.MergeStyleMerge, "", message) 76 + _, err = doMergeAndPush(ctx, reversePR, doer, repo_model.MergeStyleMerge, "", message, repository.PushTriggerPRUpdateWithBase) 76 77 77 78 defer func() { 78 79 AddTestPullRequestTask(ctx, doer, reversePR.HeadRepo.ID, reversePR.HeadBranch, false, "", "", 0)
+16 -17
services/repository/adopt.go
··· 36 36 } 37 37 } 38 38 39 - if len(opts.DefaultBranch) == 0 { 40 - opts.DefaultBranch = setting.Repository.DefaultBranch 41 - } 42 - 43 39 repo := &repo_model.Repository{ 44 40 OwnerID: u.ID, 45 41 Owner: u, ··· 81 77 } 82 78 83 79 if err := adoptRepository(ctx, repoPath, repo, opts.DefaultBranch); err != nil { 84 - return fmt.Errorf("createDelegateHooks: %w", err) 80 + return fmt.Errorf("adoptRepository: %w", err) 85 81 } 86 82 87 83 if err := repo_module.CheckDaemonExportOK(ctx, repo); err != nil { ··· 143 139 } 144 140 } 145 141 142 + // Don't bother looking this repo in the context it won't be there 143 + gitRepo, err := gitrepo.OpenRepository(ctx, repo) 144 + if err != nil { 145 + return fmt.Errorf("openRepository: %w", err) 146 + } 147 + defer gitRepo.Close() 148 + 149 + if _, err = repo_module.SyncRepoBranchesWithRepo(ctx, repo, gitRepo, 0); err != nil { 150 + return fmt.Errorf("SyncRepoBranchesWithRepo: %w", err) 151 + } 152 + 153 + if err = repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil { 154 + return fmt.Errorf("SyncReleasesWithTags: %w", err) 155 + } 156 + 146 157 branches, _ := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{ 147 158 RepoID: repo.ID, 148 159 ListOptions: db.ListOptionsAll, ··· 183 194 return fmt.Errorf("setDefaultBranch: %w", err) 184 195 } 185 196 } 186 - 187 197 if err = repo_module.UpdateRepository(ctx, repo, false); err != nil { 188 198 return fmt.Errorf("updateRepository: %w", err) 189 - } 190 - 191 - // Don't bother looking this repo in the context it won't be there 192 - gitRepo, err := gitrepo.OpenRepository(ctx, repo) 193 - if err != nil { 194 - return fmt.Errorf("openRepository: %w", err) 195 - } 196 - defer gitRepo.Close() 197 - 198 - if err = repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil { 199 - return fmt.Errorf("SyncReleasesWithTags: %w", err) 200 199 } 201 200 202 201 return nil
+1 -1
stylelint.config.js
··· 150 150 'declaration-property-unit-allowed-list': null, 151 151 'declaration-property-unit-disallowed-list': {'line-height': ['em']}, 152 152 'declaration-property-value-allowed-list': null, 153 - 'declaration-property-value-disallowed-list': null, 153 + 'declaration-property-value-disallowed-list': {'word-break': ['break-word']}, 154 154 'declaration-property-value-no-unknown': true, 155 155 'font-family-name-quotes': 'always-where-recommended', 156 156 'font-family-no-duplicate-names': true,
+1 -1
templates/projects/view.tmpl
··· 64 64 </div> 65 65 66 66 <div id="project-board"> 67 - <div class="board {{if .CanWriteProjects}}sortable{{end}}"> 67 + <div class="board {{if .CanWriteProjects}}sortable{{end}}"{{if .CanWriteProjects}} data-url="{{$.Link}}/move"{{end}}> 68 68 {{range .Columns}} 69 69 <div class="ui segment project-column"{{if .Color}} style="background: {{.Color}} !important; color: {{ContrastColor .Color}} !important"{{end}} data-id="{{.ID}}" data-sorting="{{.Sorting}}" data-url="{{$.Link}}/{{.ID}}"> 70 70 <div class="project-column-header{{if $canWriteProject}} tw-cursor-grab{{end}}">
+9 -5
templates/repo/commits_list_small.tmpl
··· 13 13 14 14 {{$commitLink:= printf "%s/commit/%s" $.comment.Issue.PullRequest.BaseRepo.Link (PathEscape .ID.String)}} 15 15 16 - <span class="tw-flex-1 gt-ellipsis tw-font-mono{{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{RenderCommitMessageLinkSubject $.root.Context .Message $commitLink ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx)}}</span> 16 + <span class="tw-flex-1 tw-font-mono gt-ellipsis" title="{{.Summary}}"> 17 + {{- RenderCommitMessageLinkSubject $.root.Context .Message $commitLink ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx) -}} 18 + </span> 17 19 18 20 {{if IsMultilineCommitMessage .Message}} 19 - <button class="ui button js-toggle-commit-body ellipsis-button" aria-expanded="false">...</button> 20 - {{end}} 21 - {{if IsMultilineCommitMessage .Message}} 22 - <pre class="commit-body tw-hidden">{{RenderCommitBody $.root.Context .Message ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx)}}</pre> 21 + <button class="ui button ellipsis-button show-panel toggle" data-panel="[data-singular-commit-body-for='{{$tag}}']">...</button> 23 22 {{end}} 24 23 25 24 <span class="shabox tw-flex tw-items-center"> ··· 47 46 </a> 48 47 </span> 49 48 </div> 49 + {{if IsMultilineCommitMessage .Message}} 50 + <pre class="commit-body tw-ml-[33px] tw-hidden" data-singular-commit-body-for="{{$tag}}"> 51 + {{- RenderCommitBody $.root.Context .Message ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx) -}} 52 + </pre> 53 + {{end}} 50 54 {{end}} 51 55 </div>
+3 -3
tests/integration/org_project_test.go
··· 5 5 6 6 import ( 7 7 "net/http" 8 + "slices" 8 9 "testing" 9 10 10 11 unit_model "code.gitea.io/gitea/models/unit" 12 + "code.gitea.io/gitea/modules/test" 11 13 "code.gitea.io/gitea/tests" 12 14 ) 13 15 14 16 func TestOrgProjectAccess(t *testing.T) { 15 17 defer tests.PrepareTestEnv(t)() 16 - 17 - // disable repo project unit 18 - unit_model.DisabledRepoUnits = []unit_model.Type{unit_model.TypeProjects} 18 + defer test.MockVariableValue(&unit_model.DisabledRepoUnits, append(slices.Clone(unit_model.DisabledRepoUnits), unit_model.TypeProjects))() 19 19 20 20 // repo project, 404 21 21 req := NewRequest(t, "GET", "/user2/repo1/projects")
+60
tests/integration/project_test.go
··· 4 4 package integration 5 5 6 6 import ( 7 + "fmt" 7 8 "net/http" 8 9 "testing" 9 10 11 + "code.gitea.io/gitea/models/db" 12 + project_model "code.gitea.io/gitea/models/project" 13 + repo_model "code.gitea.io/gitea/models/repo" 14 + "code.gitea.io/gitea/models/unittest" 10 15 "code.gitea.io/gitea/tests" 16 + 17 + "github.com/stretchr/testify/assert" 11 18 ) 12 19 13 20 func TestPrivateRepoProject(t *testing.T) { ··· 21 28 req = NewRequest(t, "GET", "/user31/-/projects") 22 29 sess.MakeRequest(t, req, http.StatusOK) 23 30 } 31 + 32 + func TestMoveRepoProjectColumns(t *testing.T) { 33 + defer tests.PrepareTestEnv(t)() 34 + 35 + repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) 36 + 37 + project1 := project_model.Project{ 38 + Title: "new created project", 39 + RepoID: repo2.ID, 40 + Type: project_model.TypeRepository, 41 + BoardType: project_model.BoardTypeNone, 42 + } 43 + err := project_model.NewProject(db.DefaultContext, &project1) 44 + assert.NoError(t, err) 45 + 46 + for i := 0; i < 3; i++ { 47 + err = project_model.NewBoard(db.DefaultContext, &project_model.Board{ 48 + Title: fmt.Sprintf("column %d", i+1), 49 + ProjectID: project1.ID, 50 + }) 51 + assert.NoError(t, err) 52 + } 53 + 54 + columns, err := project1.GetBoards(db.DefaultContext) 55 + assert.NoError(t, err) 56 + assert.Len(t, columns, 3) 57 + assert.EqualValues(t, 0, columns[0].Sorting) 58 + assert.EqualValues(t, 1, columns[1].Sorting) 59 + assert.EqualValues(t, 2, columns[2].Sorting) 60 + 61 + sess := loginUser(t, "user1") 62 + req := NewRequest(t, "GET", fmt.Sprintf("/%s/projects/%d", repo2.FullName(), project1.ID)) 63 + resp := sess.MakeRequest(t, req, http.StatusOK) 64 + htmlDoc := NewHTMLParser(t, resp.Body) 65 + 66 + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s/projects/%d/move?_csrf="+htmlDoc.GetCSRF(), repo2.FullName(), project1.ID), map[string]any{ 67 + "columns": []map[string]any{ 68 + {"columnID": columns[1].ID, "sorting": 0}, 69 + {"columnID": columns[2].ID, "sorting": 1}, 70 + {"columnID": columns[0].ID, "sorting": 2}, 71 + }, 72 + }) 73 + sess.MakeRequest(t, req, http.StatusOK) 74 + 75 + columnsAfter, err := project1.GetBoards(db.DefaultContext) 76 + assert.NoError(t, err) 77 + assert.Len(t, columns, 3) 78 + assert.EqualValues(t, columns[1].ID, columnsAfter[0].ID) 79 + assert.EqualValues(t, columns[2].ID, columnsAfter[1].ID) 80 + assert.EqualValues(t, columns[0].ID, columnsAfter[2].ID) 81 + 82 + assert.NoError(t, project_model.DeleteProjectByID(db.DefaultContext, project1.ID)) 83 + }
+61
tests/integration/pull_merge_test.go
··· 27 27 "code.gitea.io/gitea/modules/git" 28 28 "code.gitea.io/gitea/modules/gitrepo" 29 29 "code.gitea.io/gitea/modules/hostmatcher" 30 + "code.gitea.io/gitea/modules/queue" 30 31 "code.gitea.io/gitea/modules/setting" 31 32 api "code.gitea.io/gitea/modules/structs" 32 33 "code.gitea.io/gitea/modules/test" ··· 600 601 assert.EqualValues(t, "Closed", prStatus) 601 602 }) 602 603 } 604 + 605 + func TestPullMergeIndexerNotifier(t *testing.T) { 606 + onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { 607 + // create a pull request 608 + session := loginUser(t, "user1") 609 + testRepoFork(t, session, "user2", "repo1", "user1", "repo1") 610 + testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n") 611 + createPullResp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "Indexer notifier test pull") 612 + 613 + assert.NoError(t, queue.GetManager().FlushAll(context.Background(), 0)) 614 + time.Sleep(time.Second) 615 + 616 + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ 617 + OwnerName: "user2", 618 + Name: "repo1", 619 + }) 620 + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ 621 + RepoID: repo1.ID, 622 + Title: "Indexer notifier test pull", 623 + IsPull: true, 624 + IsClosed: false, 625 + }) 626 + 627 + // build the request for searching issues 628 + link, _ := url.Parse("/api/v1/repos/issues/search") 629 + query := url.Values{} 630 + query.Add("state", "closed") 631 + query.Add("type", "pulls") 632 + query.Add("q", "notifier") 633 + link.RawQuery = query.Encode() 634 + 635 + // search issues 636 + searchIssuesResp := session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK) 637 + var apiIssuesBefore []*api.Issue 638 + DecodeJSON(t, searchIssuesResp, &apiIssuesBefore) 639 + assert.Len(t, apiIssuesBefore, 0) 640 + 641 + // merge the pull request 642 + elem := strings.Split(test.RedirectURL(createPullResp), "/") 643 + assert.EqualValues(t, "pulls", elem[3]) 644 + testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge, false) 645 + 646 + // check if the issue is closed 647 + issue = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ 648 + ID: issue.ID, 649 + }) 650 + assert.True(t, issue.IsClosed) 651 + 652 + assert.NoError(t, queue.GetManager().FlushAll(context.Background(), 0)) 653 + time.Sleep(time.Second) 654 + 655 + // search issues again 656 + searchIssuesResp = session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK) 657 + var apiIssuesAfter []*api.Issue 658 + DecodeJSON(t, searchIssuesResp, &apiIssuesAfter) 659 + if assert.Len(t, apiIssuesAfter, 1) { 660 + assert.Equal(t, issue.ID, apiIssuesAfter[0].ID) 661 + } 662 + }) 663 + }
+1
web_src/css/base.css
··· 898 898 font-weight: var(--font-weight-normal); 899 899 margin: 0 6px; 900 900 padding: 5px 10px; 901 + flex-shrink: 0; 901 902 } 902 903 903 904 .ui .sha.label .shortsha {
+1 -2
web_src/css/features/console.css
··· 5 5 color: var(--color-console-fg); 6 6 font-family: var(--fonts-monospace); 7 7 border-radius: var(--border-radius); 8 - word-break: break-word; 9 - overflow-wrap: break-word; 8 + overflow-wrap: anywhere; 10 9 } 11 10 12 11 .console img { max-width: 100%; }
-1
web_src/css/helpers.css
··· 5 5 6 6 .gt-word-break { 7 7 word-wrap: break-word !important; 8 - word-break: break-word; /* compat: Safari */ 9 8 overflow-wrap: anywhere; 10 9 } 11 10
+2 -6
web_src/css/repo.css
··· 418 418 } 419 419 420 420 .repository.file.list .non-diff-file-content .plain-text pre { 421 - word-break: break-word; 421 + overflow-wrap: anywhere; 422 422 white-space: pre-wrap; 423 423 } 424 424 ··· 2487 2487 .commit-body { 2488 2488 margin: 0.25em 0; 2489 2489 white-space: pre-wrap; 2490 + overflow-wrap: anywhere; 2490 2491 line-height: initial; 2491 - } 2492 - 2493 - /* PR-comment */ 2494 - .repository .timeline-item .commit-body { 2495 - margin-left: 45px; 2496 2492 } 2497 2493 2498 2494 .git-notes.top {
+2 -2
web_src/css/shared/flex-list.css
··· 59 59 color: var(--color-text); 60 60 font-size: 16px; 61 61 font-weight: var(--font-weight-semibold); 62 - word-break: break-word; 62 + overflow-wrap: anywhere; 63 63 min-width: 0; 64 64 } 65 65 ··· 74 74 flex-wrap: wrap; 75 75 gap: .25rem; 76 76 color: var(--color-text-light-2); 77 - word-break: break-word; 77 + overflow-wrap: anywhere; 78 78 } 79 79 80 80 .flex-item .flex-item-body a {
+1 -1
web_src/js/components/RepoCodeFrequency.vue
··· 67 67 const weekValues = Object.values(this.data); 68 68 const start = weekValues[0].week; 69 69 const end = firstStartDateAfterDate(new Date()); 70 - const startDays = startDaysBetween(new Date(start), new Date(end)); 70 + const startDays = startDaysBetween(start, end); 71 71 this.data = fillEmptyStartDaysWithZeroes(startDays, this.data); 72 72 this.errorText = ''; 73 73 } else {
+1 -1
web_src/js/components/RepoContributors.vue
··· 114 114 const weekValues = Object.values(total.weeks); 115 115 this.xAxisStart = weekValues[0].week; 116 116 this.xAxisEnd = firstStartDateAfterDate(new Date()); 117 - const startDays = startDaysBetween(new Date(this.xAxisStart), new Date(this.xAxisEnd)); 117 + const startDays = startDaysBetween(this.xAxisStart, this.xAxisEnd); 118 118 total.weeks = fillEmptyStartDaysWithZeroes(startDays, total.weeks); 119 119 this.xAxisMin = this.xAxisStart; 120 120 this.xAxisMax = this.xAxisEnd;
+1 -1
web_src/js/components/RepoRecentCommits.vue
··· 62 62 const data = await response.json(); 63 63 const start = Object.values(data)[0].week; 64 64 const end = firstStartDateAfterDate(new Date()); 65 - const startDays = startDaysBetween(new Date(start), new Date(end)); 65 + const startDays = startDaysBetween(start, end); 66 66 this.data = fillEmptyStartDaysWithZeroes(startDays, data).slice(-52); 67 67 this.errorText = ''; 68 68 } else {
-4
web_src/js/features/codeeditor.js
··· 101 101 }, 102 102 }); 103 103 104 - // Quick fix: https://github.com/microsoft/monaco-editor/issues/2962 105 - monaco.languages.register({id: 'vs.editor.nullLanguage'}); 106 - monaco.languages.setLanguageConfiguration('vs.editor.nullLanguage', {}); 107 - 108 104 const editor = monaco.editor.create(container, { 109 105 value: textarea.value, 110 106 theme: 'gitea',
+14 -12
web_src/js/features/repo-projects.js
··· 2 2 import {contrastColor} from '../utils/color.js'; 3 3 import {createSortable} from '../modules/sortable.js'; 4 4 import {POST, DELETE, PUT} from '../modules/fetch.js'; 5 - import tinycolor from 'tinycolor2'; 6 5 7 6 function updateIssueCount(cards) { 8 7 const parent = cards.parentElement; ··· 63 62 delay: 500, 64 63 onSort: async () => { 65 64 boardColumns = mainBoard.getElementsByClassName('project-column'); 66 - for (let i = 0; i < boardColumns.length; i++) { 67 - const column = boardColumns[i]; 68 - if (parseInt(column.getAttribute('data-sorting')) !== i) { 69 - try { 70 - const bgColor = column.style.backgroundColor; // will be rgb() string 71 - const color = bgColor ? tinycolor(bgColor).toHexString() : ''; 72 - await PUT(column.getAttribute('data-url'), {data: {sorting: i, color}}); 73 - } catch (error) { 74 - console.error(error); 75 - } 76 - } 65 + 66 + const columnSorting = { 67 + columns: Array.from(boardColumns, (column, i) => ({ 68 + columnID: parseInt(column.getAttribute('data-id')), 69 + sorting: i, 70 + })), 71 + }; 72 + 73 + try { 74 + await POST(mainBoard.getAttribute('data-url'), { 75 + data: columnSorting, 76 + }); 77 + } catch (error) { 78 + console.error(error); 77 79 } 78 80 }, 79 81 });
+19 -14
web_src/js/utils/time.js
··· 1 1 import dayjs from 'dayjs'; 2 + import utc from 'dayjs/plugin/utc.js'; 2 3 import {getCurrentLocale} from '../utils.js'; 3 4 4 - // Returns an array of millisecond-timestamps of start-of-week days (Sundays) 5 + dayjs.extend(utc); 6 + 7 + /** 8 + * Returns an array of millisecond-timestamps of start-of-week days (Sundays) 9 + * 10 + * @param startConfig The start date. Can take any type that `Date` accepts. 11 + * @param endConfig The end date. Can take any type that `Date` accepts. 12 + */ 5 13 export function startDaysBetween(startDate, endDate) { 14 + const start = dayjs.utc(startDate); 15 + const end = dayjs.utc(endDate); 16 + 17 + let current = start; 18 + 6 19 // Ensure the start date is a Sunday 7 - while (startDate.getDay() !== 0) { 8 - startDate.setDate(startDate.getDate() + 1); 20 + while (current.day() !== 0) { 21 + current = current.add(1, 'day'); 9 22 } 10 23 11 - const start = dayjs(startDate); 12 - const end = dayjs(endDate); 13 24 const startDays = []; 14 - 15 - let current = start; 16 25 while (current.isBefore(end)) { 17 26 startDays.push(current.valueOf()); 18 - // we are adding 7 * 24 hours instead of 1 week because we don't want 19 - // date library to use local time zone to calculate 1 week from now. 20 - // local time zone is problematic because of daylight saving time (dst) 21 - // used on some countries 22 - current = current.add(7 * 24, 'hour'); 27 + current = current.add(1, 'week'); 23 28 } 24 29 25 30 return startDays; ··· 29 34 if (!(inputDate instanceof Date)) { 30 35 throw new Error('Invalid date'); 31 36 } 32 - const dayOfWeek = inputDate.getDay(); 37 + const dayOfWeek = inputDate.getUTCDay(); 33 38 const daysUntilSunday = 7 - dayOfWeek; 34 39 const resultDate = new Date(inputDate.getTime()); 35 - resultDate.setDate(resultDate.getDate() + daysUntilSunday); 40 + resultDate.setUTCDate(resultDate.getUTCDate() + daysUntilSunday); 36 41 return resultDate.valueOf(); 37 42 } 38 43