diff --git a/.deadcode-out b/.deadcode-out index f22a9df101..0f1b12c257 100644 --- a/.deadcode-out +++ b/.deadcode-out @@ -186,7 +186,6 @@ package "code.gitea.io/gitea/modules/git" func (ErrExecTimeout).Error func (ErrUnsupportedVersion).Error func SetUpdateHook - func GetObjectFormatOfRepo func openRepositoryWithDefaultContext func IsTagExist func ToEntryMode @@ -325,8 +324,6 @@ package "code.gitea.io/gitea/routers/web" package "code.gitea.io/gitea/routers/web/org" func MustEnableProjects - func getActionIssues - func UpdateIssueProject package "code.gitea.io/gitea/services/context" func GetPrivateContext diff --git a/cmd/hook.go b/cmd/hook.go index ff3059f9df..4f73f8e2bc 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -366,6 +366,7 @@ Forgejo or set your environment appropriately.`, "") isWiki, _ := strconv.ParseBool(os.Getenv(repo_module.EnvRepoIsWiki)) repoName := os.Getenv(repo_module.EnvRepoName) pusherID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64) + prID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPRID), 10, 64) pusherName := os.Getenv(repo_module.EnvPusherName) hookOptions := private.HookOptions{ @@ -375,6 +376,8 @@ Forgejo or set your environment appropriately.`, "") GitObjectDirectory: os.Getenv(private.GitObjectDirectory), GitQuarantinePath: os.Getenv(private.GitQuarantinePath), GitPushOptions: pushOptions(), + PullRequestID: prID, + PushTrigger: repo_module.PushTrigger(os.Getenv(repo_module.EnvPushTrigger)), } oldCommitIDs := make([]string, hookBatchSize) newCommitIDs := make([]string, hookBatchSize) diff --git a/cmd/migrate_storage.go b/cmd/migrate_storage.go index aa49445a89..357416fc33 100644 --- a/cmd/migrate_storage.go +++ b/cmd/migrate_storage.go @@ -34,7 +34,7 @@ var CmdMigrateStorage = &cli.Command{ Name: "type", Aliases: []string{"t"}, Value: "", - Usage: "Type of stored files to copy. Allowed types: 'attachments', 'lfs', 'avatars', 'repo-avatars', 'repo-archivers', 'packages', 'actions-log'", + Usage: "Type of stored files to copy. Allowed types: 'attachments', 'lfs', 'avatars', 'repo-avatars', 'repo-archivers', 'packages', 'actions-log', 'actions-artifacts", }, &cli.StringFlag{ Name: "storage", @@ -160,6 +160,13 @@ func migrateActionsLog(ctx context.Context, dstStorage storage.ObjectStorage) er }) } +func migrateActionsArtifacts(ctx context.Context, dstStorage storage.ObjectStorage) error { + return db.Iterate(ctx, nil, func(ctx context.Context, artifact *actions_model.ActionArtifact) error { + _, err := storage.Copy(dstStorage, artifact.ArtifactPath, storage.ActionsArtifacts, artifact.ArtifactPath) + return err + }) +} + func runMigrateStorage(ctx *cli.Context) error { stdCtx, cancel := installSignals() defer cancel() @@ -223,13 +230,14 @@ func runMigrateStorage(ctx *cli.Context) error { } migratedMethods := map[string]func(context.Context, storage.ObjectStorage) error{ - "attachments": migrateAttachments, - "lfs": migrateLFS, - "avatars": migrateAvatars, - "repo-avatars": migrateRepoAvatars, - "repo-archivers": migrateRepoArchivers, - "packages": migratePackages, - "actions-log": migrateActionsLog, + "attachments": migrateAttachments, + "lfs": migrateLFS, + "avatars": migrateAvatars, + "repo-avatars": migrateRepoAvatars, + "repo-archivers": migrateRepoArchivers, + "packages": migratePackages, + "actions-log": migrateActionsLog, + "actions-artifacts": migrateActionsArtifacts, } tp := strings.ToLower(ctx.String("type")) diff --git a/models/db/engine.go b/models/db/engine.go index b7146e87f2..61649592e7 100755 --- a/models/db/engine.go +++ b/models/db/engine.go @@ -58,6 +58,7 @@ type Engine interface { SumInt(bean any, columnName string) (res int64, err error) Sync(...any) error Select(string) *xorm.Session + SetExpr(string, any) *xorm.Session NotIn(string, ...any) *xorm.Session OrderBy(any, ...any) *xorm.Session Exist(...any) (bool, error) diff --git a/models/issues/issue_project.go b/models/issues/issue_project.go index 907a5a17b9..e31d2ef151 100644 --- a/models/issues/issue_project.go +++ b/models/issues/issue_project.go @@ -5,11 +5,11 @@ package issues import ( "context" - "fmt" "code.gitea.io/gitea/models/db" project_model "code.gitea.io/gitea/models/project" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/util" ) // LoadProject load the project the issue was assigned to @@ -90,58 +90,73 @@ func LoadIssuesFromBoardList(ctx context.Context, bs project_model.BoardList) (m return issuesMap, nil } -// ChangeProjectAssign changes the project associated with an issue -func ChangeProjectAssign(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID int64) error { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() +// IssueAssignOrRemoveProject changes the project associated with an issue +// If newProjectID is 0, the issue is removed from the project +func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID, newColumnID int64) error { + return db.WithTx(ctx, func(ctx context.Context) error { + oldProjectID := issue.projectID(ctx) - if err := addUpdateIssueProject(ctx, issue, doer, newProjectID); err != nil { - return err - } - - return committer.Commit() -} - -func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID int64) error { - oldProjectID := issue.projectID(ctx) - - if err := issue.LoadRepo(ctx); err != nil { - return err - } - - // Only check if we add a new project and not remove it. - if newProjectID > 0 { - newProject, err := project_model.GetProjectByID(ctx, newProjectID) - if err != nil { + if err := issue.LoadRepo(ctx); err != nil { return err } - if newProject.RepoID != issue.RepoID && newProject.OwnerID != issue.Repo.OwnerID { - return fmt.Errorf("issue's repository is not the same as project's repository") + + // Only check if we add a new project and not remove it. + if newProjectID > 0 { + newProject, err := project_model.GetProjectByID(ctx, newProjectID) + if err != nil { + return err + } + if !newProject.CanBeAccessedByOwnerRepo(issue.Repo.OwnerID, issue.Repo) { + return util.NewPermissionDeniedErrorf("issue %d can't be accessed by project %d", issue.ID, newProject.ID) + } + if newColumnID == 0 { + newDefaultColumn, err := newProject.GetDefaultBoard(ctx) + if err != nil { + return err + } + newColumnID = newDefaultColumn.ID + } } - } - if _, err := db.GetEngine(ctx).Where("project_issue.issue_id=?", issue.ID).Delete(&project_model.ProjectIssue{}); err != nil { - return err - } - - if oldProjectID > 0 || newProjectID > 0 { - if _, err := CreateComment(ctx, &CreateCommentOptions{ - Type: CommentTypeProject, - Doer: doer, - Repo: issue.Repo, - Issue: issue, - OldProjectID: oldProjectID, - ProjectID: newProjectID, - }); err != nil { + if _, err := db.GetEngine(ctx).Where("project_issue.issue_id=?", issue.ID).Delete(&project_model.ProjectIssue{}); err != nil { return err } - } - return db.Insert(ctx, &project_model.ProjectIssue{ - IssueID: issue.ID, - ProjectID: newProjectID, + if oldProjectID > 0 || newProjectID > 0 { + if _, err := CreateComment(ctx, &CreateCommentOptions{ + Type: CommentTypeProject, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + OldProjectID: oldProjectID, + ProjectID: newProjectID, + }); err != nil { + return err + } + } + if newProjectID == 0 { + return nil + } + if newColumnID == 0 { + panic("newColumnID must not be zero") // shouldn't happen + } + + res := struct { + MaxSorting int64 + IssueCount int64 + }{} + if _, err := db.GetEngine(ctx).Select("max(sorting) as max_sorting, count(*) as issue_count").Table("project_issue"). + Where("project_id=?", newProjectID). + And("project_board_id=?", newColumnID). + Get(&res); err != nil { + return err + } + newSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0) + return db.Insert(ctx, &project_model.ProjectIssue{ + IssueID: issue.ID, + ProjectID: newProjectID, + ProjectBoardID: newColumnID, + Sorting: newSorting, + }) }) } diff --git a/models/migrations/fixtures/Test_RepositoryFormat/review_state.yml b/models/migrations/fixtures/Test_RepositoryFormat/review_state.yml index 1197b086e3..dd64980916 100644 --- a/models/migrations/fixtures/Test_RepositoryFormat/review_state.yml +++ b/models/migrations/fixtures/Test_RepositoryFormat/review_state.yml @@ -1,3 +1,5 @@ - id: 1 + user_id: 1 + pull_id: 1 commit_sha: 19fe5caf872476db265596eaac1dc35ad1c6422d diff --git a/models/migrations/v1_22/v286_test.go b/models/migrations/v1_22/v286_test.go index 7c353747e3..a19c9396e2 100644 --- a/models/migrations/v1_22/v286_test.go +++ b/models/migrations/v1_22/v286_test.go @@ -19,21 +19,21 @@ func PrepareOldRepository(t *testing.T) (*xorm.Engine, func()) { type CommitStatus struct { ID int64 - ContextHash string + ContextHash string `xorm:"char(40) index"` } type RepoArchiver struct { ID int64 - RepoID int64 - Type int - CommitID string + RepoID int64 `xorm:"index unique(s)"` + Type int `xorm:"unique(s)"` + CommitID string `xorm:"VARCHAR(40) unique(s)"` } type ReviewState struct { ID int64 - CommitSHA string - UserID int64 - PullID int64 + UserID int64 `xorm:"NOT NULL UNIQUE(pull_commit_user)"` + PullID int64 `xorm:"NOT NULL INDEX UNIQUE(pull_commit_user) DEFAULT 0"` + CommitSHA string `xorm:"NOT NULL VARCHAR(40) UNIQUE(pull_commit_user)"` } type Comment struct { diff --git a/models/project/board.go b/models/project/board.go index 7faabc52c5..a52baa0c18 100644 --- a/models/project/board.go +++ b/models/project/board.go @@ -5,12 +5,14 @@ package project import ( "context" + "errors" "fmt" "regexp" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" "xorm.io/builder" ) @@ -82,6 +84,17 @@ func (b *Board) NumIssues(ctx context.Context) int { return int(c) } +func (b *Board) GetIssues(ctx context.Context) ([]*ProjectIssue, error) { + issues := make([]*ProjectIssue, 0, 5) + if err := db.GetEngine(ctx).Where("project_id=?", b.ProjectID). + And("project_board_id=?", b.ID). + OrderBy("sorting, id"). + Find(&issues); err != nil { + return nil, err + } + return issues, nil +} + func init() { db.RegisterModel(new(Board)) } @@ -150,12 +163,27 @@ func createBoardsForProjectsType(ctx context.Context, project *Project) error { return db.Insert(ctx, boards) } +// maxProjectColumns max columns allowed in a project, this should not bigger than 127 +// because sorting is int8 in database +const maxProjectColumns = 20 + // NewBoard adds a new project board to a given project func NewBoard(ctx context.Context, board *Board) error { if len(board.Color) != 0 && !BoardColorPattern.MatchString(board.Color) { return fmt.Errorf("bad color code: %s", board.Color) } - + res := struct { + MaxSorting int64 + ColumnCount int64 + }{} + if _, err := db.GetEngine(ctx).Select("max(sorting) as max_sorting, count(*) as column_count").Table("project_board"). + Where("project_id=?", board.ProjectID).Get(&res); err != nil { + return err + } + if res.ColumnCount >= maxProjectColumns { + return fmt.Errorf("NewBoard: maximum number of columns reached") + } + board.Sorting = int8(util.Iif(res.ColumnCount > 0, res.MaxSorting+1, 0)) _, err := db.GetEngine(ctx).Insert(board) return err } @@ -189,7 +217,17 @@ func deleteBoardByID(ctx context.Context, boardID int64) error { return fmt.Errorf("deleteBoardByID: cannot delete default board") } - if err = board.removeIssues(ctx); err != nil { + // move all issues to the default column + project, err := GetProjectByID(ctx, board.ProjectID) + if err != nil { + return err + } + defaultColumn, err := project.GetDefaultBoard(ctx) + if err != nil { + return err + } + + if err = board.moveIssuesToAnotherColumn(ctx, defaultColumn); err != nil { return err } @@ -242,21 +280,15 @@ func UpdateBoard(ctx context.Context, board *Board) error { // GetBoards fetches all boards related to a project func (p *Project) GetBoards(ctx context.Context) (BoardList, error) { boards := make([]*Board, 0, 5) - - if err := db.GetEngine(ctx).Where("project_id=? AND `default`=?", p.ID, false).OrderBy("sorting").Find(&boards); err != nil { + if err := db.GetEngine(ctx).Where("project_id=?", p.ID).OrderBy("sorting, id").Find(&boards); err != nil { return nil, err } - defaultB, err := p.getDefaultBoard(ctx) - if err != nil { - return nil, err - } - - return append([]*Board{defaultB}, boards...), nil + return boards, nil } -// getDefaultBoard return default board and ensure only one exists -func (p *Project) getDefaultBoard(ctx context.Context) (*Board, error) { +// GetDefaultBoard return default board and ensure only one exists +func (p *Project) GetDefaultBoard(ctx context.Context) (*Board, error) { var board Board has, err := db.GetEngine(ctx). Where("project_id=? AND `default` = ?", p.ID, true). @@ -316,3 +348,42 @@ func UpdateBoardSorting(ctx context.Context, bs BoardList) error { return nil }) } + +func GetColumnsByIDs(ctx context.Context, projectID int64, columnsIDs []int64) (BoardList, error) { + columns := make([]*Board, 0, 5) + if err := db.GetEngine(ctx). + Where("project_id =?", projectID). + In("id", columnsIDs). + OrderBy("sorting").Find(&columns); err != nil { + return nil, err + } + return columns, nil +} + +// MoveColumnsOnProject sorts columns in a project +func MoveColumnsOnProject(ctx context.Context, project *Project, sortedColumnIDs map[int64]int64) error { + return db.WithTx(ctx, func(ctx context.Context) error { + sess := db.GetEngine(ctx) + columnIDs := util.ValuesOfMap(sortedColumnIDs) + movedColumns, err := GetColumnsByIDs(ctx, project.ID, columnIDs) + if err != nil { + return err + } + if len(movedColumns) != len(sortedColumnIDs) { + return errors.New("some columns do not exist") + } + + for _, column := range movedColumns { + if column.ProjectID != project.ID { + return fmt.Errorf("column[%d]'s projectID is not equal to project's ID [%d]", column.ProjectID, project.ID) + } + } + + for sorting, columnID := range sortedColumnIDs { + if _, err := sess.Exec("UPDATE `project_board` SET sorting=? WHERE id=?", sorting, columnID); err != nil { + return err + } + } + return nil + }) +} diff --git a/models/project/board_test.go b/models/project/board_test.go index 71ba29a589..da922ff7ad 100644 --- a/models/project/board_test.go +++ b/models/project/board_test.go @@ -4,6 +4,8 @@ package project import ( + "fmt" + "strings" "testing" "code.gitea.io/gitea/models/db" @@ -19,7 +21,7 @@ func TestGetDefaultBoard(t *testing.T) { assert.NoError(t, err) // check if default board was added - board, err := projectWithoutDefault.getDefaultBoard(db.DefaultContext) + board, err := projectWithoutDefault.GetDefaultBoard(db.DefaultContext) assert.NoError(t, err) assert.Equal(t, int64(5), board.ProjectID) assert.Equal(t, "Uncategorized", board.Title) @@ -28,7 +30,7 @@ func TestGetDefaultBoard(t *testing.T) { assert.NoError(t, err) // check if multiple defaults were removed - board, err = projectWithMultipleDefaults.getDefaultBoard(db.DefaultContext) + board, err = projectWithMultipleDefaults.GetDefaultBoard(db.DefaultContext) assert.NoError(t, err) assert.Equal(t, int64(6), board.ProjectID) assert.Equal(t, int64(9), board.ID) @@ -42,3 +44,84 @@ func TestGetDefaultBoard(t *testing.T) { assert.Equal(t, int64(6), board.ProjectID) assert.False(t, board.Default) } + +func Test_moveIssuesToAnotherColumn(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + column1 := unittest.AssertExistsAndLoadBean(t, &Board{ID: 1, ProjectID: 1}) + + issues, err := column1.GetIssues(db.DefaultContext) + assert.NoError(t, err) + assert.Len(t, issues, 1) + assert.EqualValues(t, 1, issues[0].ID) + + column2 := unittest.AssertExistsAndLoadBean(t, &Board{ID: 2, ProjectID: 1}) + issues, err = column2.GetIssues(db.DefaultContext) + assert.NoError(t, err) + assert.Len(t, issues, 1) + assert.EqualValues(t, 3, issues[0].ID) + + err = column1.moveIssuesToAnotherColumn(db.DefaultContext, column2) + assert.NoError(t, err) + + issues, err = column1.GetIssues(db.DefaultContext) + assert.NoError(t, err) + assert.Len(t, issues, 0) + + issues, err = column2.GetIssues(db.DefaultContext) + assert.NoError(t, err) + assert.Len(t, issues, 2) + assert.EqualValues(t, 3, issues[0].ID) + assert.EqualValues(t, 0, issues[0].Sorting) + assert.EqualValues(t, 1, issues[1].ID) + assert.EqualValues(t, 1, issues[1].Sorting) +} + +func Test_MoveColumnsOnProject(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + project1 := unittest.AssertExistsAndLoadBean(t, &Project{ID: 1}) + columns, err := project1.GetBoards(db.DefaultContext) + assert.NoError(t, err) + assert.Len(t, columns, 3) + assert.EqualValues(t, 0, columns[0].Sorting) // even if there is no default sorting, the code should also work + assert.EqualValues(t, 0, columns[1].Sorting) + assert.EqualValues(t, 0, columns[2].Sorting) + + err = MoveColumnsOnProject(db.DefaultContext, project1, map[int64]int64{ + 0: columns[1].ID, + 1: columns[2].ID, + 2: columns[0].ID, + }) + assert.NoError(t, err) + + columnsAfter, err := project1.GetBoards(db.DefaultContext) + assert.NoError(t, err) + assert.Len(t, columnsAfter, 3) + assert.EqualValues(t, columns[1].ID, columnsAfter[0].ID) + assert.EqualValues(t, columns[2].ID, columnsAfter[1].ID) + assert.EqualValues(t, columns[0].ID, columnsAfter[2].ID) +} + +func Test_NewBoard(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + project1 := unittest.AssertExistsAndLoadBean(t, &Project{ID: 1}) + columns, err := project1.GetBoards(db.DefaultContext) + assert.NoError(t, err) + assert.Len(t, columns, 3) + + for i := 0; i < maxProjectColumns-3; i++ { + err := NewBoard(db.DefaultContext, &Board{ + Title: fmt.Sprintf("board-%d", i+4), + ProjectID: project1.ID, + }) + assert.NoError(t, err) + } + err = NewBoard(db.DefaultContext, &Board{ + Title: "board-21", + ProjectID: project1.ID, + }) + assert.Error(t, err) + assert.True(t, strings.Contains(err.Error(), "maximum number of columns reached")) +} diff --git a/models/project/issue.go b/models/project/issue.go index ebc9719de5..32e72e909d 100644 --- a/models/project/issue.go +++ b/models/project/issue.go @@ -9,6 +9,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/util" ) // ProjectIssue saves relation from issue to a project @@ -17,7 +18,7 @@ type ProjectIssue struct { //revive:disable-line:exported IssueID int64 `xorm:"INDEX"` ProjectID int64 `xorm:"INDEX"` - // If 0, then it has not been added to a specific board in the project + // 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. ProjectBoardID int64 `xorm:"INDEX"` // the sorting order on the board @@ -79,11 +80,8 @@ func (p *Project) NumOpenIssues(ctx context.Context) int { func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, sortedIssueIDs map[int64]int64) error { return db.WithTx(ctx, func(ctx context.Context) error { sess := db.GetEngine(ctx) + issueIDs := util.ValuesOfMap(sortedIssueIDs) - issueIDs := make([]int64, 0, len(sortedIssueIDs)) - for _, issueID := range sortedIssueIDs { - issueIDs = append(issueIDs, issueID) - } count, err := sess.Table(new(ProjectIssue)).Where("project_id=?", board.ProjectID).In("issue_id", issueIDs).Count() if err != nil { return err @@ -102,7 +100,44 @@ func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, sortedIssueIDs }) } -func (b *Board) removeIssues(ctx context.Context) error { - _, err := db.GetEngine(ctx).Exec("UPDATE `project_issue` SET project_board_id = 0 WHERE project_board_id = ? ", b.ID) - return err +func (b *Board) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Board) error { + if b.ProjectID != newColumn.ProjectID { + return fmt.Errorf("columns have to be in the same project") + } + + if b.ID == newColumn.ID { + return nil + } + + res := struct { + MaxSorting int64 + IssueCount int64 + }{} + if _, err := db.GetEngine(ctx).Select("max(sorting) as max_sorting, count(*) as issue_count"). + Table("project_issue"). + Where("project_id=?", newColumn.ProjectID). + And("project_board_id=?", newColumn.ID). + Get(&res); err != nil { + return err + } + + issues, err := b.GetIssues(ctx) + if err != nil { + return err + } + if len(issues) == 0 { + return nil + } + + nextSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0) + return db.WithTx(ctx, func(ctx context.Context) error { + for i, issue := range issues { + issue.ProjectBoardID = newColumn.ID + issue.Sorting = nextSorting + int64(i) + if _, err := db.GetEngine(ctx).ID(issue.ID).Cols("project_board_id", "sorting").Update(issue); err != nil { + return err + } + } + return nil + }) } diff --git a/models/project/project.go b/models/project/project.go index 8f9ee2a99e..8be38694c5 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -161,6 +161,13 @@ func (p *Project) IsRepositoryProject() bool { return p.Type == TypeRepository } +func (p *Project) CanBeAccessedByOwnerRepo(ownerID int64, repo *repo_model.Repository) bool { + if p.Type == TypeRepository { + return repo != nil && p.RepoID == repo.ID // if a project belongs to a repository, then its OwnerID is 0 and can be ignored + } + return p.OwnerID == ownerID && p.RepoID == 0 +} + func init() { db.RegisterModel(new(Project)) } diff --git a/models/repo/search.go b/models/repo/search.go index 4d64acf8cf..54d6dcfb44 100644 --- a/models/repo/search.go +++ b/models/repo/search.go @@ -8,14 +8,14 @@ import "code.gitea.io/gitea/models/db" // SearchOrderByMap represents all possible search order var SearchOrderByMap = map[string]map[string]db.SearchOrderBy{ "asc": { - "alpha": db.SearchOrderByAlphabetically, + "alpha": "owner_name ASC, name ASC", "created": db.SearchOrderByOldest, "updated": db.SearchOrderByLeastUpdated, "size": db.SearchOrderBySize, "id": db.SearchOrderByID, }, "desc": { - "alpha": db.SearchOrderByAlphabeticallyReverse, + "alpha": "owner_name DESC, name DESC", "created": db.SearchOrderByNewest, "updated": db.SearchOrderByRecentUpdated, "size": db.SearchOrderBySizeReverse, diff --git a/modules/git/repo.go b/modules/git/repo.go index e8a7016d99..857424fcd4 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -7,7 +7,6 @@ package git import ( "bytes" "context" - "errors" "fmt" "io" "net/url" @@ -63,32 +62,6 @@ func IsRepoURLAccessible(ctx context.Context, url string) bool { return err == nil } -// GetObjectFormatOfRepo returns the hash type of repository at a given path -func GetObjectFormatOfRepo(ctx context.Context, repoPath string) (ObjectFormat, error) { - var stdout, stderr strings.Builder - - err := NewCommand(ctx, "hash-object", "--stdin").Run(&RunOpts{ - Dir: repoPath, - Stdout: &stdout, - Stderr: &stderr, - Stdin: &strings.Reader{}, - }) - if err != nil { - return nil, err - } - - if stderr.Len() > 0 { - return nil, errors.New(stderr.String()) - } - - h, err := NewIDFromString(strings.TrimRight(stdout.String(), "\n")) - if err != nil { - return nil, err - } - - return h.Type(), nil -} - // InitRepository initializes a new Git repository. func InitRepository(ctx context.Context, repoPath string, bare bool, objectFormatName string) error { err := os.MkdirAll(repoPath, os.ModePerm) diff --git a/modules/private/hook.go b/modules/private/hook.go index cab8c81224..1d0ef4e3a9 100644 --- a/modules/private/hook.go +++ b/modules/private/hook.go @@ -11,6 +11,7 @@ import ( "time" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" ) @@ -53,6 +54,7 @@ type HookOptions struct { GitQuarantinePath string GitPushOptions GitPushOptions PullRequestID int64 + PushTrigger repository.PushTrigger DeployKeyID int64 // if the pusher is a DeployKey, then UserID is the repo's org user. IsWiki bool ActionPerm int diff --git a/modules/repository/branch.go b/modules/repository/branch.go index e448490f4a..a3fca7c7ce 100644 --- a/modules/repository/branch.go +++ b/modules/repository/branch.go @@ -5,6 +5,7 @@ package repository import ( "context" + "fmt" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" @@ -36,6 +37,15 @@ func SyncRepoBranches(ctx context.Context, repoID, doerID int64) (int64, error) } func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, doerID int64) (int64, error) { + objFmt, err := gitRepo.GetObjectFormat() + if err != nil { + return 0, fmt.Errorf("GetObjectFormat: %w", err) + } + _, err = db.GetEngine(ctx).ID(repo.ID).Update(&repo_model.Repository{ObjectFormatName: objFmt.Name()}) + if err != nil { + return 0, fmt.Errorf("UpdateRepository: %w", err) + } + allBranches := container.Set[string]{} { branches, _, err := gitRepo.GetBranchNames(0, 0) diff --git a/modules/repository/branch_test.go b/modules/repository/branch_test.go new file mode 100644 index 0000000000..acf75a1ac0 --- /dev/null +++ b/modules/repository/branch_test.go @@ -0,0 +1,31 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repository + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + git_model "code.gitea.io/gitea/models/git" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" +) + +func TestSyncRepoBranches(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + _, err := db.GetEngine(db.DefaultContext).ID(1).Update(&repo_model.Repository{ObjectFormatName: "bad-fmt"}) + assert.NoError(t, db.TruncateBeans(db.DefaultContext, &git_model.Branch{})) + assert.NoError(t, err) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + assert.Equal(t, "bad-fmt", repo.ObjectFormatName) + _, err = SyncRepoBranches(db.DefaultContext, 1, 0) + assert.NoError(t, err) + repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + assert.Equal(t, "sha1", repo.ObjectFormatName) + branch, err := git_model.GetBranch(db.DefaultContext, 1, "master") + assert.NoError(t, err) + assert.EqualValues(t, "master", branch.Name) +} diff --git a/modules/repository/env.go b/modules/repository/env.go index 30edd1c9e3..e4f32092fc 100644 --- a/modules/repository/env.go +++ b/modules/repository/env.go @@ -25,11 +25,19 @@ const ( EnvKeyID = "GITEA_KEY_ID" // public key ID EnvDeployKeyID = "GITEA_DEPLOY_KEY_ID" EnvPRID = "GITEA_PR_ID" + EnvPushTrigger = "GITEA_PUSH_TRIGGER" EnvIsInternal = "GITEA_INTERNAL_PUSH" EnvAppURL = "GITEA_ROOT_URL" EnvActionPerm = "GITEA_ACTION_PERM" ) +type PushTrigger string + +const ( + PushTriggerPRMergeToBase PushTrigger = "pr-merge-to-base" + PushTriggerPRUpdateWithBase PushTrigger = "pr-update-with-base" +) + // InternalPushingEnvironment returns an os environment to switch off hooks on push // It is recommended to avoid using this unless you are pushing within a transaction // or if you absolutely are sure that post-receive and pre-receive will do nothing diff --git a/release-notes/8.0.0/feat/3729.md b/release-notes/8.0.0/feat/3729.md new file mode 100644 index 0000000000..bc76e24bd5 --- /dev/null +++ b/release-notes/8.0.0/feat/3729.md @@ -0,0 +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). diff --git a/release-notes/8.0.0/fix/3729.md b/release-notes/8.0.0/fix/3729.md new file mode 100644 index 0000000000..27d83e0e55 --- /dev/null +++ b/release-notes/8.0.0/fix/3729.md @@ -0,0 +1,2 @@ +- [PR](https://github.com/go-gitea/gitea/pull/30912): when adopting a repository, the default branch is not taken into account +- [PR](https://github.com/go-gitea/gitea/pull/30715): pull request search shows closed pull requests in the open tab diff --git a/routers/api/actions/runner/interceptor.go b/routers/api/actions/runner/interceptor.go index 0e99f3deda..521ba910e3 100644 --- a/routers/api/actions/runner/interceptor.go +++ b/routers/api/actions/runner/interceptor.go @@ -23,8 +23,6 @@ import ( const ( uuidHeaderKey = "x-runner-uuid" tokenHeaderKey = "x-runner-token" - // Deprecated: will be removed after Gitea 1.20 released. - versionHeaderKey = "x-runner-version" ) var withRunner = connect.WithInterceptors(connect.UnaryInterceptorFunc(func(unaryFunc connect.UnaryFunc) connect.UnaryFunc { @@ -35,9 +33,6 @@ var withRunner = connect.WithInterceptors(connect.UnaryInterceptorFunc(func(unar } uuid := request.Header().Get(uuidHeaderKey) token := request.Header().Get(tokenHeaderKey) - // TODO: version will be removed from request header after Gitea 1.20 released. - // And Gitea will not try to read version from request header - version := request.Header().Get(versionHeaderKey) runner, err := actions_model.GetRunnerByUUID(ctx, uuid) if err != nil { @@ -51,14 +46,6 @@ var withRunner = connect.WithInterceptors(connect.UnaryInterceptorFunc(func(unar } cols := []string{"last_online"} - - // TODO: version will be removed from request header after Gitea 1.20 released. - // And Gitea will not try to read version from request header - version, _ = util.SplitStringAtByteN(version, 64) - if !util.IsEmptyString(version) && runner.Version != version { - runner.Version = version - cols = append(cols, "version") - } runner.LastOnline = timeutil.TimeStampNow() if methodName == "UpdateTask" || methodName == "UpdateLog" { runner.LastActive = timeutil.TimeStampNow() diff --git a/routers/api/actions/runner/runner.go b/routers/api/actions/runner/runner.go index bbffa9acfb..017bdf6324 100644 --- a/routers/api/actions/runner/runner.go +++ b/routers/api/actions/runner/runner.go @@ -69,12 +69,6 @@ func (s *Service) Register( } labels := req.Msg.Labels - // TODO: agent_labels should be removed from pb after Gitea 1.20 released. - // Old version runner's agent_labels slice is not empty and labels slice is empty. - // And due to compatibility with older versions, it is temporarily marked as Deprecated in pb, so use `//nolint` here. - if len(req.Msg.AgentLabels) > 0 && len(req.Msg.Labels) == 0 { //nolint:staticcheck - labels = req.Msg.AgentLabels //nolint:staticcheck - } // create new runner name, _ := util.SplitStringAtByteN(req.Msg.Name, 255) diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index dbb7de6e66..e3d3665ebe 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -878,7 +878,7 @@ func MergePullRequest(ctx *context.APIContext) { } // start with merging by checking - if err := pull_service.CheckPullMergable(ctx, ctx.Doer, &ctx.Repo.Permission, pr, mergeCheckType, form.ForceMerge); err != nil { + if err := pull_service.CheckPullMergeable(ctx, ctx.Doer, &ctx.Repo.Permission, pr, mergeCheckType, form.ForceMerge); err != nil { if errors.Is(err, pull_service.ErrIsClosed) { ctx.NotFound() } else if errors.Is(err, pull_service.ErrUserNotAllowedToMerge) { @@ -887,7 +887,7 @@ func MergePullRequest(ctx *context.APIContext) { ctx.Error(http.StatusMethodNotAllowed, "PR already merged", "") } else if errors.Is(err, pull_service.ErrIsWorkInProgress) { ctx.Error(http.StatusMethodNotAllowed, "PR is a work in progress", "Work in progress PRs cannot be merged") - } else if errors.Is(err, pull_service.ErrNotMergableState) { + } else if errors.Is(err, pull_service.ErrNotMergeableState) { ctx.Error(http.StatusMethodNotAllowed, "PR not in mergeable state", "Please try again later") } else if models.IsErrDisallowedToMerge(err) { ctx.Error(http.StatusMethodNotAllowed, "PR is not ready to be merged", err) diff --git a/routers/private/hook_post_receive.go b/routers/private/hook_post_receive.go index 381e3c6c77..10b300f3df 100644 --- a/routers/private/hook_post_receive.go +++ b/routers/private/hook_post_receive.go @@ -4,20 +4,26 @@ package private import ( + "context" "fmt" "net/http" "strconv" "time" + "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" + pull_model "code.gitea.io/gitea/models/pull" repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/private" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" + timeutil "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" gitea_context "code.gitea.io/gitea/services/context" @@ -155,6 +161,14 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) { } } + // handle pull request merging, a pull request action should push at least 1 commit + if opts.PushTrigger == repo_module.PushTriggerPRMergeToBase { + handlePullRequestMerging(ctx, opts, ownerName, repoName, updates) + if ctx.Written() { + return + } + } + // Handle Push Options if len(opts.GitPushOptions) > 0 { // load the repository @@ -302,3 +316,52 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) { RepoWasEmpty: wasEmpty, }) } + +func loadContextCacheUser(ctx context.Context, id int64) (*user_model.User, error) { + return cache.GetWithContextCache(ctx, "hook_post_receive_user", id, func() (*user_model.User, error) { + return user_model.GetUserByID(ctx, id) + }) +} + +// handlePullRequestMerging handle pull request merging, a pull request action should push at least 1 commit +func handlePullRequestMerging(ctx *gitea_context.PrivateContext, opts *private.HookOptions, ownerName, repoName string, updates []*repo_module.PushUpdateOptions) { + if len(updates) == 0 { + ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{ + Err: fmt.Sprintf("Pushing a merged PR (pr:%d) no commits pushed ", opts.PullRequestID), + }) + return + } + + pr, err := issues_model.GetPullRequestByID(ctx, opts.PullRequestID) + if err != nil { + log.Error("GetPullRequestByID[%d]: %v", opts.PullRequestID, err) + ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "GetPullRequestByID failed"}) + return + } + + pusher, err := loadContextCacheUser(ctx, opts.UserID) + if err != nil { + log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err) + ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "Load pusher user failed"}) + return + } + + pr.MergedCommitID = updates[len(updates)-1].NewCommitID + pr.MergedUnix = timeutil.TimeStampNow() + pr.Merger = pusher + pr.MergerID = pusher.ID + err = db.WithTx(ctx, func(ctx context.Context) error { + // Removing an auto merge pull and ignore if not exist + if err := pull_model.DeleteScheduledAutoMerge(ctx, pr.ID); err != nil && !db.IsErrNotExist(err) { + return fmt.Errorf("DeleteScheduledAutoMerge[%d]: %v", opts.PullRequestID, err) + } + if _, err := pr.SetMerged(ctx); err != nil { + return fmt.Errorf("SetMerged failed: %s/%s Error: %v", ownerName, repoName, err) + } + return nil + }) + if err != nil { + log.Error("Failed to update PR to merged: %v", err) + ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "Failed to update PR to merged"}) + } +} diff --git a/routers/private/hook_post_receive_test.go b/routers/private/hook_post_receive_test.go new file mode 100644 index 0000000000..658557d3cf --- /dev/null +++ b/routers/private/hook_post_receive_test.go @@ -0,0 +1,49 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package private + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" + pull_model "code.gitea.io/gitea/models/pull" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/private" + repo_module "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/services/contexttest" + + "github.com/stretchr/testify/assert" +) + +func TestHandlePullRequestMerging(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + pr, err := issues_model.GetUnmergedPullRequest(db.DefaultContext, 1, 1, "branch2", "master", issues_model.PullRequestFlowGithub) + assert.NoError(t, err) + assert.NoError(t, pr.LoadBaseRepo(db.DefaultContext)) + + user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + + err = pull_model.ScheduleAutoMerge(db.DefaultContext, user1, pr.ID, repo_model.MergeStyleSquash, "squash merge a pr") + assert.NoError(t, err) + + autoMerge := unittest.AssertExistsAndLoadBean(t, &pull_model.AutoMerge{PullID: pr.ID}) + + ctx, resp := contexttest.MockPrivateContext(t, "/") + handlePullRequestMerging(ctx, &private.HookOptions{ + PullRequestID: pr.ID, + UserID: 2, + }, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, []*repo_module.PushUpdateOptions{ + {NewCommitID: "01234567"}, + }) + assert.Equal(t, 0, len(resp.Body.String())) + pr, err = issues_model.GetPullRequestByID(db.DefaultContext, pr.ID) + assert.NoError(t, err) + assert.True(t, pr.HasMerged) + assert.EqualValues(t, "01234567", pr.MergedCommitID) + + unittest.AssertNotExistsBean(t, &pull_model.AutoMerge{ID: autoMerge.ID}) +} diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index 821228b347..e6f3a19625 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -7,7 +7,6 @@ import ( "errors" "fmt" "net/http" - "strconv" "strings" "code.gitea.io/gitea/models/db" @@ -390,74 +389,6 @@ func ViewProject(ctx *context.Context) { ctx.HTML(http.StatusOK, tplProjectsView) } -func getActionIssues(ctx *context.Context) issues_model.IssueList { - commaSeparatedIssueIDs := ctx.FormString("issue_ids") - if len(commaSeparatedIssueIDs) == 0 { - return nil - } - issueIDs := make([]int64, 0, 10) - for _, stringIssueID := range strings.Split(commaSeparatedIssueIDs, ",") { - issueID, err := strconv.ParseInt(stringIssueID, 10, 64) - if err != nil { - ctx.ServerError("ParseInt", err) - return nil - } - issueIDs = append(issueIDs, issueID) - } - issues, err := issues_model.GetIssuesByIDs(ctx, issueIDs) - if err != nil { - ctx.ServerError("GetIssuesByIDs", err) - return nil - } - // Check access rights for all issues - issueUnitEnabled := ctx.Repo.CanRead(unit.TypeIssues) - prUnitEnabled := ctx.Repo.CanRead(unit.TypePullRequests) - for _, issue := range issues { - if issue.RepoID != ctx.Repo.Repository.ID { - ctx.NotFound("some issue's RepoID is incorrect", errors.New("some issue's RepoID is incorrect")) - return nil - } - if issue.IsPull && !prUnitEnabled || !issue.IsPull && !issueUnitEnabled { - ctx.NotFound("IssueOrPullRequestUnitNotAllowed", nil) - return nil - } - if err = issue.LoadAttributes(ctx); err != nil { - ctx.ServerError("LoadAttributes", err) - return nil - } - } - return issues -} - -// UpdateIssueProject change an issue's project -func UpdateIssueProject(ctx *context.Context) { - issues := getActionIssues(ctx) - if ctx.Written() { - return - } - - if err := issues.LoadProjects(ctx); err != nil { - ctx.ServerError("LoadProjects", err) - return - } - - projectID := ctx.FormInt64("id") - for _, issue := range issues { - if issue.Project != nil { - if issue.Project.ID == projectID { - continue - } - } - - if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID); err != nil { - ctx.ServerError("ChangeProjectAssign", err) - return - } - } - - ctx.JSONOK() -} - // DeleteProjectBoard allows for the deletion of a project board func DeleteProjectBoard(ctx *context.Context) { if ctx.Doer == nil { diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 9f4979cb67..350e577ede 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -1266,8 +1266,8 @@ func NewIssuePost(ctx *context.Context) { ctx.Error(http.StatusBadRequest, "user hasn't permissions to read projects") return } - if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID); err != nil { - ctx.ServerError("ChangeProjectAssign", err) + if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, ctx.Doer, projectID, 0); err != nil { + ctx.ServerError("IssueAssignOrRemoveProject", err) return } } diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index a0c044fd64..934cf8873b 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -21,6 +21,7 @@ import ( "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" @@ -382,17 +383,21 @@ func UpdateIssueProject(ctx *context.Context) { ctx.ServerError("LoadProjects", err) return } + if _, err := issues.LoadRepositories(ctx); err != nil { + ctx.ServerError("LoadProjects", err) + return + } projectID := ctx.FormInt64("id") for _, issue := range issues { - if issue.Project != nil { - if issue.Project.ID == projectID { + if issue.Project != nil && issue.Project.ID == projectID { + continue + } + if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, ctx.Doer, projectID, 0); err != nil { + if errors.Is(err, util.ErrPermissionDenied) { continue } - } - - if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID); err != nil { - ctx.ServerError("ChangeProjectAssign", err) + ctx.ServerError("IssueAssignOrRemoveProject", err) return } } diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index a0dd36927f..be6511afaa 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -1218,7 +1218,7 @@ func MergePullRequest(ctx *context.Context) { } // start with merging by checking - if err := pull_service.CheckPullMergable(ctx, ctx.Doer, &ctx.Repo.Permission, pr, mergeCheckType, form.ForceMerge); err != nil { + if err := pull_service.CheckPullMergeable(ctx, ctx.Doer, &ctx.Repo.Permission, pr, mergeCheckType, form.ForceMerge); err != nil { switch { case errors.Is(err, pull_service.ErrIsClosed): if issue.IsPull { @@ -1232,7 +1232,7 @@ func MergePullRequest(ctx *context.Context) { ctx.JSONError(ctx.Tr("repo.pulls.has_merged")) case errors.Is(err, pull_service.ErrIsWorkInProgress): ctx.JSONError(ctx.Tr("repo.pulls.no_merge_wip")) - case errors.Is(err, pull_service.ErrNotMergableState): + case errors.Is(err, pull_service.ErrNotMergeableState): ctx.JSONError(ctx.Tr("repo.pulls.no_merge_not_ready")) case models.IsErrDisallowedToMerge(err): ctx.JSONError(ctx.Tr("repo.pulls.no_merge_not_ready")) @@ -1537,14 +1537,12 @@ func CompareAndPullRequestPost(ctx *context.Context) { return } - if projectID > 0 { - if !ctx.Repo.CanWrite(unit.TypeProjects) { - ctx.Error(http.StatusBadRequest, "user hasn't the permission to write to projects") - return - } - if err := issues_model.ChangeProjectAssign(ctx, pullIssue, ctx.Doer, projectID); err != nil { - ctx.ServerError("ChangeProjectAssign", err) - return + if projectID > 0 && ctx.Repo.CanWrite(unit.TypeProjects) { + if err := issues_model.IssueAssignOrRemoveProject(ctx, pullIssue, ctx.Doer, projectID, 0); err != nil { + if !errors.Is(err, util.ErrPermissionDenied) { + ctx.ServerError("IssueAssignOrRemoveProject", err) + return + } } } diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index e2c6a9902e..a7d4e75ff6 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -798,6 +798,7 @@ func SettingsPost(ctx *context.Context) { ctx.Repo.GitRepo = nil } + oldFullname := repo.FullName() if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, repo, nil); err != nil { if errors.Is(err, user_model.ErrBlockedByUser) { ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_blocked_doer"), tplSettingsOptions, nil) @@ -812,8 +813,13 @@ func SettingsPost(ctx *context.Context) { return } - log.Trace("Repository transfer process was started: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newOwner) - ctx.Flash.Success(ctx.Tr("repo.settings.transfer_started", newOwner.DisplayName())) + if ctx.Repo.Repository.Status == repo_model.RepositoryPendingTransfer { + log.Trace("Repository transfer process was started: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newOwner) + ctx.Flash.Success(ctx.Tr("repo.settings.transfer_started", newOwner.DisplayName())) + } else { + log.Trace("Repository transferred: %s -> %s", oldFullname, ctx.Repo.Repository.FullName()) + ctx.Flash.Success(ctx.Tr("repo.settings.transfer_succeed")) + } ctx.Redirect(repo.Link() + "/settings") case "cancel_transfer": diff --git a/routers/web/shared/project/column.go b/routers/web/shared/project/column.go new file mode 100644 index 0000000000..599842ea9e --- /dev/null +++ b/routers/web/shared/project/column.go @@ -0,0 +1,48 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package project + +import ( + project_model "code.gitea.io/gitea/models/project" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/services/context" +) + +// MoveColumns moves or keeps columns in a project and sorts them inside that project +func MoveColumns(ctx *context.Context) { + project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) + if err != nil { + ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) + return + } + if !project.CanBeAccessedByOwnerRepo(ctx.ContextUser.ID, ctx.Repo.Repository) { + ctx.NotFound("CanBeAccessedByOwnerRepo", nil) + return + } + + type movedColumnsForm struct { + Columns []struct { + ColumnID int64 `json:"columnID"` + Sorting int64 `json:"sorting"` + } `json:"columns"` + } + + form := &movedColumnsForm{} + if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil { + ctx.ServerError("DecodeMovedColumnsForm", err) + return + } + + sortedColumnIDs := make(map[int64]int64) + for _, column := range form.Columns { + sortedColumnIDs[column.Sorting] = column.ColumnID + } + + if err = project_model.MoveColumnsOnProject(ctx, project, sortedColumnIDs); err != nil { + ctx.ServerError("MoveColumnsOnProject", err) + return + } + + ctx.JSONOK() +} diff --git a/routers/web/web.go b/routers/web/web.go index 8faedca178..48f9102749 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -39,6 +39,7 @@ import ( "code.gitea.io/gitea/routers/web/repo/badges" repo_flags "code.gitea.io/gitea/routers/web/repo/flags" repo_setting "code.gitea.io/gitea/routers/web/repo/setting" + "code.gitea.io/gitea/routers/web/shared/project" "code.gitea.io/gitea/routers/web/user" user_setting "code.gitea.io/gitea/routers/web/user/setting" "code.gitea.io/gitea/routers/web/user/setting/security" @@ -97,14 +98,14 @@ func optionsCorsHandler() func(next http.Handler) http.Handler { // The Session plugin is expected to be executed second, in order to skip authentication // for users that have already signed in. func buildAuthGroup() *auth_service.Group { - group := auth_service.NewGroup( - &auth_service.OAuth2{}, // FIXME: this should be removed and only applied in download and oauth related routers - &auth_service.Basic{}, // FIXME: this should be removed and only applied in download and git/lfs routers - &auth_service.Session{}, - ) + group := auth_service.NewGroup() + group.Add(&auth_service.OAuth2{}) // FIXME: this should be removed and only applied in download and oauth related routers + group.Add(&auth_service.Basic{}) // FIXME: this should be removed and only applied in download and git/lfs routers + if setting.Service.EnableReverseProxyAuth { - group.Add(&auth_service.ReverseProxy{}) + group.Add(&auth_service.ReverseProxy{}) // reverseproxy should before Session, otherwise the header will be ignored if user has login } + group.Add(&auth_service.Session{}) if setting.IsWindows && auth_model.IsSSPIEnabled(db.DefaultContext) { group.Add(&auth_service.SSPI{}) // it MUST be the last, see the comment of SSPI @@ -976,6 +977,7 @@ func registerRoutes(m *web.Route) { m.Post("/new", web.Bind(forms.CreateProjectForm{}), org.NewProjectPost) m.Group("/{id}", func() { m.Post("", web.Bind(forms.EditProjectBoardForm{}), org.AddBoardToProjectPost) + m.Post("/move", project.MoveColumns) m.Post("/delete", org.DeleteProject) m.Get("/edit", org.RenderEditProject) @@ -1349,6 +1351,7 @@ func registerRoutes(m *web.Route) { m.Post("/new", web.Bind(forms.CreateProjectForm{}), repo.NewProjectPost) m.Group("/{id}", func() { m.Post("", web.Bind(forms.EditProjectBoardForm{}), repo.AddBoardToProjectPost) + m.Post("/move", project.MoveColumns) m.Post("/delete", repo.DeleteProject) m.Get("/edit", repo.RenderEditProject) diff --git a/services/automerge/automerge.go b/services/automerge/automerge.go index bd427bef9f..bd1317c7f4 100644 --- a/services/automerge/automerge.go +++ b/services/automerge/automerge.go @@ -229,12 +229,12 @@ func handlePull(pullID int64, sha string) { return } - if err := pull_service.CheckPullMergable(ctx, doer, &perm, pr, pull_service.MergeCheckTypeGeneral, false); err != nil { + if err := pull_service.CheckPullMergeable(ctx, doer, &perm, pr, pull_service.MergeCheckTypeGeneral, false); err != nil { if errors.Is(pull_service.ErrUserNotAllowedToMerge, err) { log.Info("%-v was scheduled to automerge by an unauthorized user", pr) return } - log.Error("%-v CheckPullMergable: %v", pr, err) + log.Error("%-v CheckPullMergeable: %v", pr, err) return } diff --git a/services/contexttest/context_tests.go b/services/contexttest/context_tests.go index 7bfab2ed16..073af213a2 100644 --- a/services/contexttest/context_tests.go +++ b/services/contexttest/context_tests.go @@ -86,6 +86,19 @@ func MockAPIContext(t *testing.T, reqPath string) (*context.APIContext, *httptes return ctx, resp } +func MockPrivateContext(t *testing.T, reqPath string) (*context.PrivateContext, *httptest.ResponseRecorder) { + resp := httptest.NewRecorder() + req := mockRequest(t, reqPath) + base, baseCleanUp := context.NewBaseContext(resp, req) + base.Data = middleware.GetContextData(req.Context()) + base.Locale = &translation.MockLocale{} + ctx := &context.PrivateContext{Base: base} + _ = baseCleanUp // during test, it doesn't need to do clean up. TODO: this can be improved later + chiCtx := chi.NewRouteContext() + ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx) + return ctx, resp +} + // LoadRepo load a repo into a test context. func LoadRepo(t *testing.T, ctx gocontext.Context, repoID int64) { var doer *user_model.User diff --git a/services/indexer/notify.go b/services/indexer/notify.go index f1e21a2d40..e2cfe477d3 100644 --- a/services/indexer/notify.go +++ b/services/indexer/notify.go @@ -152,3 +152,19 @@ func (r *indexerNotifier) IssueChangeLabels(ctx context.Context, doer *user_mode func (r *indexerNotifier) IssueClearLabels(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) { issue_indexer.UpdateIssueIndexer(ctx, issue.ID) } + +func (r *indexerNotifier) MergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) { + if err := pr.LoadIssue(ctx); err != nil { + log.Error("LoadIssue: %v", err) + return + } + issue_indexer.UpdateIssueIndexer(ctx, pr.Issue.ID) +} + +func (r *indexerNotifier) AutoMergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) { + if err := pr.LoadIssue(ctx); err != nil { + log.Error("LoadIssue: %v", err) + return + } + issue_indexer.UpdateIssueIndexer(ctx, pr.Issue.ID) +} diff --git a/services/pull/check.go b/services/pull/check.go index 5cca0e1f28..765f7580cb 100644 --- a/services/pull/check.go +++ b/services/pull/check.go @@ -39,7 +39,7 @@ var ( ErrHasMerged = errors.New("has already been merged") ErrIsWorkInProgress = errors.New("work in progress PRs cannot be merged") ErrIsChecking = errors.New("cannot merge while conflict checking is in progress") - ErrNotMergableState = errors.New("not in mergeable state") + ErrNotMergeableState = errors.New("not in mergeable state") ErrDependenciesLeft = errors.New("is blocked by an open dependency") ) @@ -66,8 +66,8 @@ const ( MergeCheckTypeAuto // Auto Merge (Scheduled Merge) After Checks Succeed ) -// CheckPullMergable check if the pull mergeable based on all conditions (branch protection, merge options, ...) -func CheckPullMergable(stdCtx context.Context, doer *user_model.User, perm *access_model.Permission, pr *issues_model.PullRequest, mergeCheckType MergeCheckType, adminSkipProtectionCheck bool) error { +// CheckPullMergeable check if the pull mergeable based on all conditions (branch protection, merge options, ...) +func CheckPullMergeable(stdCtx context.Context, doer *user_model.User, perm *access_model.Permission, pr *issues_model.PullRequest, mergeCheckType MergeCheckType, adminSkipProtectionCheck bool) error { return db.WithTx(stdCtx, func(ctx context.Context) error { if pr.HasMerged { return ErrHasMerged @@ -97,7 +97,7 @@ func CheckPullMergable(stdCtx context.Context, doer *user_model.User, perm *acce } if !pr.CanAutoMerge() && !pr.IsEmpty() { - return ErrNotMergableState + return ErrNotMergeableState } if pr.IsChecking() { diff --git a/services/pull/merge.go b/services/pull/merge.go index 1d6431ab66..525146833e 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -18,7 +18,6 @@ import ( git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" access_model "code.gitea.io/gitea/models/perm/access" - pull_model "code.gitea.io/gitea/models/pull" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" @@ -168,12 +167,6 @@ func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.U pullWorkingPool.CheckIn(fmt.Sprint(pr.ID)) defer pullWorkingPool.CheckOut(fmt.Sprint(pr.ID)) - // Removing an auto merge pull and ignore if not exist - // FIXME: is this the correct point to do this? Shouldn't this be after IsMergeStyleAllowed? - if err := pull_model.DeleteScheduledAutoMerge(ctx, pr.ID); err != nil && !db.IsErrNotExist(err) { - return err - } - prUnit, err := pr.BaseRepo.GetUnit(ctx, unit.TypePullRequests) if err != nil { log.Error("pr.BaseRepo.GetUnit(unit.TypePullRequests): %v", err) @@ -190,17 +183,15 @@ func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.U AddTestPullRequestTask(ctx, doer, pr.BaseRepo.ID, pr.BaseBranch, false, "", "", 0) }() - pr.MergedCommitID, err = doMergeAndPush(ctx, pr, doer, mergeStyle, expectedHeadCommitID, message) + _, err = doMergeAndPush(ctx, pr, doer, mergeStyle, expectedHeadCommitID, message, repo_module.PushTriggerPRMergeToBase) if err != nil { return err } - pr.MergedUnix = timeutil.TimeStampNow() - pr.Merger = doer - pr.MergerID = doer.ID - - if _, err := pr.SetMerged(ctx); err != nil { - log.Error("SetMerged %-v: %v", pr, err) + // reload pull request because it has been updated by post receive hook + pr, err = issues_model.GetPullRequestByID(ctx, pr.ID) + if err != nil { + return err } if err := pr.LoadIssue(ctx); err != nil { @@ -251,7 +242,7 @@ func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.U } // doMergeAndPush performs the merge operation without changing any pull information in database and pushes it up to the base repository -func doMergeAndPush(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string) (string, error) { +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) { // Clone base repo. mergeCtx, cancel, err := createTemporaryRepoForMerge(ctx, pr, doer, expectedHeadCommitID) if err != nil { @@ -324,11 +315,13 @@ func doMergeAndPush(ctx context.Context, pr *issues_model.PullRequest, doer *use pr.BaseRepo.Name, pr.ID, ) + + mergeCtx.env = append(mergeCtx.env, repo_module.EnvPushTrigger+"="+string(pushTrigger)) pushCmd := git.NewCommand(ctx, "push", "origin").AddDynamicArguments(baseBranch + ":" + git.BranchPrefix + pr.BaseBranch) // Push back to upstream. - // TODO: this cause an api call to "/api/internal/hook/post-receive/...", - // that prevents us from doint the whole merge in one db transaction + // This cause an api call to "/api/internal/hook/post-receive/...", + // If it's merge, all db transaction and operations should be there but not here to prevent deadlock. if err := pushCmd.Run(mergeCtx.RunOpts()); err != nil { if strings.Contains(mergeCtx.errbuf.String(), "non-fast-forward") { return "", &git.ErrPushOutOfDate{ diff --git a/services/pull/update.go b/services/pull/update.go index 1bba396880..dbc1b711e2 100644 --- a/services/pull/update.go +++ b/services/pull/update.go @@ -15,6 +15,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/repository" ) // Update updates pull request with base branch. @@ -72,7 +73,7 @@ func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model. BaseBranch: pr.HeadBranch, } - _, err = doMergeAndPush(ctx, reversePR, doer, repo_model.MergeStyleMerge, "", message) + _, err = doMergeAndPush(ctx, reversePR, doer, repo_model.MergeStyleMerge, "", message, repository.PushTriggerPRUpdateWithBase) defer func() { AddTestPullRequestTask(ctx, doer, reversePR.HeadRepo.ID, reversePR.HeadBranch, false, "", "", 0) diff --git a/services/repository/adopt.go b/services/repository/adopt.go index 914cd9047b..3d6fe71a09 100644 --- a/services/repository/adopt.go +++ b/services/repository/adopt.go @@ -36,10 +36,6 @@ func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts CreateR } } - if len(opts.DefaultBranch) == 0 { - opts.DefaultBranch = setting.Repository.DefaultBranch - } - repo := &repo_model.Repository{ OwnerID: u.ID, Owner: u, @@ -81,7 +77,7 @@ func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts CreateR } if err := adoptRepository(ctx, repoPath, repo, opts.DefaultBranch); err != nil { - return fmt.Errorf("createDelegateHooks: %w", err) + return fmt.Errorf("adoptRepository: %w", err) } if err := repo_module.CheckDaemonExportOK(ctx, repo); err != nil { @@ -143,6 +139,21 @@ func adoptRepository(ctx context.Context, repoPath string, repo *repo_model.Repo } } + // Don't bother looking this repo in the context it won't be there + gitRepo, err := gitrepo.OpenRepository(ctx, repo) + if err != nil { + return fmt.Errorf("openRepository: %w", err) + } + defer gitRepo.Close() + + if _, err = repo_module.SyncRepoBranchesWithRepo(ctx, repo, gitRepo, 0); err != nil { + return fmt.Errorf("SyncRepoBranchesWithRepo: %w", err) + } + + if err = repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil { + return fmt.Errorf("SyncReleasesWithTags: %w", err) + } + branches, _ := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{ RepoID: repo.ID, ListOptions: db.ListOptionsAll, @@ -183,22 +194,10 @@ func adoptRepository(ctx context.Context, repoPath string, repo *repo_model.Repo return fmt.Errorf("setDefaultBranch: %w", err) } } - if err = repo_module.UpdateRepository(ctx, repo, false); err != nil { return fmt.Errorf("updateRepository: %w", err) } - // Don't bother looking this repo in the context it won't be there - gitRepo, err := gitrepo.OpenRepository(ctx, repo) - if err != nil { - return fmt.Errorf("openRepository: %w", err) - } - defer gitRepo.Close() - - if err = repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil { - return fmt.Errorf("SyncReleasesWithTags: %w", err) - } - return nil } diff --git a/stylelint.config.js b/stylelint.config.js index 523b18841e..22c7feb485 100644 --- a/stylelint.config.js +++ b/stylelint.config.js @@ -150,7 +150,7 @@ export default { 'declaration-property-unit-allowed-list': null, 'declaration-property-unit-disallowed-list': {'line-height': ['em']}, 'declaration-property-value-allowed-list': null, - 'declaration-property-value-disallowed-list': null, + 'declaration-property-value-disallowed-list': {'word-break': ['break-word']}, 'declaration-property-value-no-unknown': true, 'font-family-name-quotes': 'always-where-recommended', 'font-family-no-duplicate-names': true, diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index 3e000660e2..47f214a44e 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -64,7 +64,7 @@
-
+
{{range .Columns}}
diff --git a/templates/repo/commits_list_small.tmpl b/templates/repo/commits_list_small.tmpl index 6ca6dd5cdc..4c67319b8c 100644 --- a/templates/repo/commits_list_small.tmpl +++ b/templates/repo/commits_list_small.tmpl @@ -13,13 +13,12 @@ {{$commitLink:= printf "%s/commit/%s" $.comment.Issue.PullRequest.BaseRepo.Link (PathEscape .ID.String)}} - {{RenderCommitMessageLinkSubject $.root.Context .Message $commitLink ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx)}} + + {{- RenderCommitMessageLinkSubject $.root.Context .Message $commitLink ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx) -}} + {{if IsMultilineCommitMessage .Message}} - - {{end}} - {{if IsMultilineCommitMessage .Message}} -
{{RenderCommitBody $.root.Context .Message ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx)}}
+ {{end}} @@ -47,5 +46,10 @@
+ {{if IsMultilineCommitMessage .Message}} +
+		{{- RenderCommitBody $.root.Context .Message ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx) -}}
+	
+ {{end}} {{end}}
diff --git a/tests/integration/org_project_test.go b/tests/integration/org_project_test.go index a14004f6b0..ca39cf5130 100644 --- a/tests/integration/org_project_test.go +++ b/tests/integration/org_project_test.go @@ -5,17 +5,17 @@ package integration import ( "net/http" + "slices" "testing" unit_model "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/tests" ) func TestOrgProjectAccess(t *testing.T) { defer tests.PrepareTestEnv(t)() - - // disable repo project unit - unit_model.DisabledRepoUnits = []unit_model.Type{unit_model.TypeProjects} + defer test.MockVariableValue(&unit_model.DisabledRepoUnits, append(slices.Clone(unit_model.DisabledRepoUnits), unit_model.TypeProjects))() // repo project, 404 req := NewRequest(t, "GET", "/user2/repo1/projects") diff --git a/tests/integration/project_test.go b/tests/integration/project_test.go index 45061c5b24..5ddea4314a 100644 --- a/tests/integration/project_test.go +++ b/tests/integration/project_test.go @@ -4,10 +4,17 @@ package integration import ( + "fmt" "net/http" "testing" + "code.gitea.io/gitea/models/db" + project_model "code.gitea.io/gitea/models/project" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" ) func TestPrivateRepoProject(t *testing.T) { @@ -21,3 +28,56 @@ func TestPrivateRepoProject(t *testing.T) { req = NewRequest(t, "GET", "/user31/-/projects") sess.MakeRequest(t, req, http.StatusOK) } + +func TestMoveRepoProjectColumns(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) + + project1 := project_model.Project{ + Title: "new created project", + RepoID: repo2.ID, + Type: project_model.TypeRepository, + BoardType: project_model.BoardTypeNone, + } + err := project_model.NewProject(db.DefaultContext, &project1) + assert.NoError(t, err) + + for i := 0; i < 3; i++ { + err = project_model.NewBoard(db.DefaultContext, &project_model.Board{ + Title: fmt.Sprintf("column %d", i+1), + ProjectID: project1.ID, + }) + assert.NoError(t, err) + } + + columns, err := project1.GetBoards(db.DefaultContext) + assert.NoError(t, err) + assert.Len(t, columns, 3) + assert.EqualValues(t, 0, columns[0].Sorting) + assert.EqualValues(t, 1, columns[1].Sorting) + assert.EqualValues(t, 2, columns[2].Sorting) + + sess := loginUser(t, "user1") + req := NewRequest(t, "GET", fmt.Sprintf("/%s/projects/%d", repo2.FullName(), project1.ID)) + resp := sess.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s/projects/%d/move?_csrf="+htmlDoc.GetCSRF(), repo2.FullName(), project1.ID), map[string]any{ + "columns": []map[string]any{ + {"columnID": columns[1].ID, "sorting": 0}, + {"columnID": columns[2].ID, "sorting": 1}, + {"columnID": columns[0].ID, "sorting": 2}, + }, + }) + sess.MakeRequest(t, req, http.StatusOK) + + columnsAfter, err := project1.GetBoards(db.DefaultContext) + assert.NoError(t, err) + assert.Len(t, columns, 3) + assert.EqualValues(t, columns[1].ID, columnsAfter[0].ID) + assert.EqualValues(t, columns[2].ID, columnsAfter[1].ID) + assert.EqualValues(t, columns[0].ID, columnsAfter[2].ID) + + assert.NoError(t, project_model.DeleteProjectByID(db.DefaultContext, project1.ID)) +} diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go index 3bd4f57410..6dddb84bcd 100644 --- a/tests/integration/pull_merge_test.go +++ b/tests/integration/pull_merge_test.go @@ -27,6 +27,7 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/hostmatcher" + "code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/test" @@ -600,3 +601,63 @@ func TestPullDontRetargetChildOnWrongRepo(t *testing.T) { assert.EqualValues(t, "Closed", prStatus) }) } + +func TestPullMergeIndexerNotifier(t *testing.T) { + onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + // create a pull request + session := loginUser(t, "user1") + testRepoFork(t, session, "user2", "repo1", "user1", "repo1") + testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n") + createPullResp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "Indexer notifier test pull") + + assert.NoError(t, queue.GetManager().FlushAll(context.Background(), 0)) + time.Sleep(time.Second) + + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ + OwnerName: "user2", + Name: "repo1", + }) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ + RepoID: repo1.ID, + Title: "Indexer notifier test pull", + IsPull: true, + IsClosed: false, + }) + + // build the request for searching issues + link, _ := url.Parse("/api/v1/repos/issues/search") + query := url.Values{} + query.Add("state", "closed") + query.Add("type", "pulls") + query.Add("q", "notifier") + link.RawQuery = query.Encode() + + // search issues + searchIssuesResp := session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK) + var apiIssuesBefore []*api.Issue + DecodeJSON(t, searchIssuesResp, &apiIssuesBefore) + assert.Len(t, apiIssuesBefore, 0) + + // merge the pull request + elem := strings.Split(test.RedirectURL(createPullResp), "/") + assert.EqualValues(t, "pulls", elem[3]) + testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge, false) + + // check if the issue is closed + issue = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ + ID: issue.ID, + }) + assert.True(t, issue.IsClosed) + + assert.NoError(t, queue.GetManager().FlushAll(context.Background(), 0)) + time.Sleep(time.Second) + + // search issues again + searchIssuesResp = session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK) + var apiIssuesAfter []*api.Issue + DecodeJSON(t, searchIssuesResp, &apiIssuesAfter) + if assert.Len(t, apiIssuesAfter, 1) { + assert.Equal(t, issue.ID, apiIssuesAfter[0].ID) + } + }) +} diff --git a/web_src/css/base.css b/web_src/css/base.css index a43ba031e0..deef2a866f 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -898,6 +898,7 @@ input:-webkit-autofill:active, font-weight: var(--font-weight-normal); margin: 0 6px; padding: 5px 10px; + flex-shrink: 0; } .ui .sha.label .shortsha { diff --git a/web_src/css/features/console.css b/web_src/css/features/console.css index 99fb25dae5..e2d3327cfa 100644 --- a/web_src/css/features/console.css +++ b/web_src/css/features/console.css @@ -5,8 +5,7 @@ color: var(--color-console-fg); font-family: var(--fonts-monospace); border-radius: var(--border-radius); - word-break: break-word; - overflow-wrap: break-word; + overflow-wrap: anywhere; } .console img { max-width: 100%; } diff --git a/web_src/css/helpers.css b/web_src/css/helpers.css index 64621ac906..60ecd7db72 100644 --- a/web_src/css/helpers.css +++ b/web_src/css/helpers.css @@ -5,7 +5,6 @@ Gitea's private styles use `g-` prefix. .gt-word-break { word-wrap: break-word !important; - word-break: break-word; /* compat: Safari */ overflow-wrap: anywhere; } diff --git a/web_src/css/repo.css b/web_src/css/repo.css index a29e9a0986..e58525af0f 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -418,7 +418,7 @@ td .commit-summary { } .repository.file.list .non-diff-file-content .plain-text pre { - word-break: break-word; + overflow-wrap: anywhere; white-space: pre-wrap; } @@ -2487,14 +2487,10 @@ tbody.commit-list { .commit-body { margin: 0.25em 0; white-space: pre-wrap; + overflow-wrap: anywhere; line-height: initial; } -/* PR-comment */ -.repository .timeline-item .commit-body { - margin-left: 45px; -} - .git-notes.top { text-align: left; } diff --git a/web_src/css/shared/flex-list.css b/web_src/css/shared/flex-list.css index 6217b45300..0f54779252 100644 --- a/web_src/css/shared/flex-list.css +++ b/web_src/css/shared/flex-list.css @@ -59,7 +59,7 @@ color: var(--color-text); font-size: 16px; font-weight: var(--font-weight-semibold); - word-break: break-word; + overflow-wrap: anywhere; min-width: 0; } @@ -74,7 +74,7 @@ flex-wrap: wrap; gap: .25rem; color: var(--color-text-light-2); - word-break: break-word; + overflow-wrap: anywhere; } .flex-item .flex-item-body a { diff --git a/web_src/js/components/RepoCodeFrequency.vue b/web_src/js/components/RepoCodeFrequency.vue index adce431264..1d40d6d417 100644 --- a/web_src/js/components/RepoCodeFrequency.vue +++ b/web_src/js/components/RepoCodeFrequency.vue @@ -67,7 +67,7 @@ export default { const weekValues = Object.values(this.data); const start = weekValues[0].week; const end = firstStartDateAfterDate(new Date()); - const startDays = startDaysBetween(new Date(start), new Date(end)); + const startDays = startDaysBetween(start, end); this.data = fillEmptyStartDaysWithZeroes(startDays, this.data); this.errorText = ''; } else { diff --git a/web_src/js/components/RepoContributors.vue b/web_src/js/components/RepoContributors.vue index 2347c41ae4..f7b05831e0 100644 --- a/web_src/js/components/RepoContributors.vue +++ b/web_src/js/components/RepoContributors.vue @@ -114,7 +114,7 @@ export default { const weekValues = Object.values(total.weeks); this.xAxisStart = weekValues[0].week; this.xAxisEnd = firstStartDateAfterDate(new Date()); - const startDays = startDaysBetween(new Date(this.xAxisStart), new Date(this.xAxisEnd)); + const startDays = startDaysBetween(this.xAxisStart, this.xAxisEnd); total.weeks = fillEmptyStartDaysWithZeroes(startDays, total.weeks); this.xAxisMin = this.xAxisStart; this.xAxisMax = this.xAxisEnd; diff --git a/web_src/js/components/RepoRecentCommits.vue b/web_src/js/components/RepoRecentCommits.vue index 502af533da..8759978e78 100644 --- a/web_src/js/components/RepoRecentCommits.vue +++ b/web_src/js/components/RepoRecentCommits.vue @@ -62,7 +62,7 @@ export default { const data = await response.json(); const start = Object.values(data)[0].week; const end = firstStartDateAfterDate(new Date()); - const startDays = startDaysBetween(new Date(start), new Date(end)); + const startDays = startDaysBetween(start, end); this.data = fillEmptyStartDaysWithZeroes(startDays, data).slice(-52); this.errorText = ''; } else { diff --git a/web_src/js/features/codeeditor.js b/web_src/js/features/codeeditor.js index f5e4e74dc6..07a686f459 100644 --- a/web_src/js/features/codeeditor.js +++ b/web_src/js/features/codeeditor.js @@ -101,10 +101,6 @@ export async function createMonaco(textarea, filename, editorOpts) { }, }); - // Quick fix: https://github.com/microsoft/monaco-editor/issues/2962 - monaco.languages.register({id: 'vs.editor.nullLanguage'}); - monaco.languages.setLanguageConfiguration('vs.editor.nullLanguage', {}); - const editor = monaco.editor.create(container, { value: textarea.value, theme: 'gitea', diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js index a869c24c82..a1cc4b346b 100644 --- a/web_src/js/features/repo-projects.js +++ b/web_src/js/features/repo-projects.js @@ -2,7 +2,6 @@ import $ from 'jquery'; import {contrastColor} from '../utils/color.js'; import {createSortable} from '../modules/sortable.js'; import {POST, DELETE, PUT} from '../modules/fetch.js'; -import tinycolor from 'tinycolor2'; function updateIssueCount(cards) { const parent = cards.parentElement; @@ -63,17 +62,20 @@ async function initRepoProjectSortable() { delay: 500, onSort: async () => { boardColumns = mainBoard.getElementsByClassName('project-column'); - for (let i = 0; i < boardColumns.length; i++) { - const column = boardColumns[i]; - if (parseInt(column.getAttribute('data-sorting')) !== i) { - try { - const bgColor = column.style.backgroundColor; // will be rgb() string - const color = bgColor ? tinycolor(bgColor).toHexString() : ''; - await PUT(column.getAttribute('data-url'), {data: {sorting: i, color}}); - } catch (error) { - console.error(error); - } - } + + const columnSorting = { + columns: Array.from(boardColumns, (column, i) => ({ + columnID: parseInt(column.getAttribute('data-id')), + sorting: i, + })), + }; + + try { + await POST(mainBoard.getAttribute('data-url'), { + data: columnSorting, + }); + } catch (error) { + console.error(error); } }, }); diff --git a/web_src/js/utils/time.js b/web_src/js/utils/time.js index 1848792c98..7c7eabd1a3 100644 --- a/web_src/js/utils/time.js +++ b/web_src/js/utils/time.js @@ -1,25 +1,30 @@ import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc.js'; import {getCurrentLocale} from '../utils.js'; -// Returns an array of millisecond-timestamps of start-of-week days (Sundays) -export function startDaysBetween(startDate, endDate) { - // Ensure the start date is a Sunday - while (startDate.getDay() !== 0) { - startDate.setDate(startDate.getDate() + 1); - } +dayjs.extend(utc); - const start = dayjs(startDate); - const end = dayjs(endDate); - const startDays = []; +/** + * Returns an array of millisecond-timestamps of start-of-week days (Sundays) + * + * @param startConfig The start date. Can take any type that `Date` accepts. + * @param endConfig The end date. Can take any type that `Date` accepts. + */ +export function startDaysBetween(startDate, endDate) { + const start = dayjs.utc(startDate); + const end = dayjs.utc(endDate); let current = start; + + // Ensure the start date is a Sunday + while (current.day() !== 0) { + current = current.add(1, 'day'); + } + + const startDays = []; while (current.isBefore(end)) { startDays.push(current.valueOf()); - // we are adding 7 * 24 hours instead of 1 week because we don't want - // date library to use local time zone to calculate 1 week from now. - // local time zone is problematic because of daylight saving time (dst) - // used on some countries - current = current.add(7 * 24, 'hour'); + current = current.add(1, 'week'); } return startDays; @@ -29,10 +34,10 @@ export function firstStartDateAfterDate(inputDate) { if (!(inputDate instanceof Date)) { throw new Error('Invalid date'); } - const dayOfWeek = inputDate.getDay(); + const dayOfWeek = inputDate.getUTCDay(); const daysUntilSunday = 7 - dayOfWeek; const resultDate = new Date(inputDate.getTime()); - resultDate.setDate(resultDate.getDate() + daysUntilSunday); + resultDate.setUTCDate(resultDate.getUTCDate() + daysUntilSunday); return resultDate.valueOf(); }