diff --git a/.drone.yml b/.drone.yml index b3b1682619a3f..ce922f8c60f66 100644 --- a/.drone.yml +++ b/.drone.yml @@ -235,6 +235,7 @@ steps: image: docker:git pull: always commands: + - git config --global --add safe.directory /drone/src - git fetch --tags --force when: event: @@ -427,6 +428,7 @@ steps: image: docker:git pull: always commands: + - git config --global --add safe.directory /drone/src - git fetch --tags --force when: event: @@ -628,6 +630,7 @@ steps: image: docker:git pull: always commands: + - git config --global --add safe.directory /drone/src - git fetch --tags --force - name: deps-frontend @@ -746,6 +749,7 @@ steps: image: docker:git pull: always commands: + - git config --global --add safe.directory /drone/src - git fetch --tags --force - name: deps-frontend @@ -891,6 +895,7 @@ steps: image: docker:git pull: always commands: + - git config --global --add safe.directory /drone/src - git fetch --tags --force - name: publish @@ -954,6 +959,7 @@ steps: image: docker:git pull: always commands: + - git config --global --add safe.directory /drone/src - git fetch --tags --force - name: publish @@ -1016,6 +1022,7 @@ steps: image: docker:git pull: always commands: + - git config --global --add safe.directory /drone/src - git fetch --tags --force - name: publish @@ -1112,6 +1119,7 @@ steps: image: docker:git pull: always commands: + - git config --global --add safe.directory /drone/src - git fetch --tags --force - name: publish @@ -1175,6 +1183,7 @@ steps: image: docker:git pull: always commands: + - git config --global --add safe.directory /drone/src - git fetch --tags --force - name: publish @@ -1237,6 +1246,7 @@ steps: image: docker:git pull: always commands: + - git config --global --add safe.directory /drone/src - git fetch --tags --force - name: publish diff --git a/CHANGELOG.md b/CHANGELOG.md index c5ffc6d610a34..16499cb916375 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,29 @@ This changelog goes through all the changes that have been made in each release without substantial changes to our git log; to see the highlights of what has been added to each release, please refer to the [blog](https://blog.gitea.io). +## [1.16.7](https://github.com/go-gitea/gitea/releases/tag/v1.16.7) - 2022-05-02 + +* SECURITY + * Escape git fetch remote (#19487) (#19490) +* BUGFIXES + * Don't overwrite err with nil (#19572) (#19574) + * On Migrations, only write commit-graph if wiki clone was successful (#19563) (#19568) + * Respect DefaultUserIsRestricted system default when creating new user (#19310) (#19560) + * Don't error when branch's commit doesn't exist (#19547) (#19548) + * Support `hostname:port` to pass host matcher's check (#19543) (#19544) + * Prevent intermittent race in attribute reader close (#19537) (#19539) + * Fix 64-bit atomic operations on 32-bit machines (#19531) (#19532) + * Prevent dangling archiver goroutine (#19516) (#19526) + * Fix migrate release from github (#19510) (#19523) + * When view _Siderbar or _Footer, just display once (#19501) (#19522) + * Fix blame page select range error and some typos (#19503) + * Fix name of doctor fix "authorized-keys" in hints (#19464) (#19484) + * User specific repoID or xorm builder conditions for issue search (#19475) (#19476) + * Prevent dangling cat-file calls (goroutine alternative) (#19454) (#19466) + * RepoAssignment ensure to close before overwrite (#19449) (#19460) + * Set correct PR status on 3way on conflict checking (#19457) (#19458) + * Mark TemplateLoading error as "UnprocessableEntity" (#19445) (#19446) + ## [1.16.6](https://github.com/go-gitea/gitea/releases/tag/v1.16.6) - 2022-04-20 * ENHANCEMENTS diff --git a/docs/config.yaml b/docs/config.yaml index 93961f32c0bd7..41a1f7f02232b 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -18,7 +18,7 @@ params: description: Git with a cup of tea author: The Gitea Authors website: https://docs.gitea.io - version: 1.16.6 + version: 1.16.7 minGoVersion: 1.17 goVersion: 1.18 minNodeVersion: 14 diff --git a/docs/content/doc/usage/issue-pull-request-templates.en-us.md b/docs/content/doc/usage/issue-pull-request-templates.en-us.md index 218b8a36427b7..65af260c3100d 100644 --- a/docs/content/doc/usage/issue-pull-request-templates.en-us.md +++ b/docs/content/doc/usage/issue-pull-request-templates.en-us.md @@ -43,6 +43,39 @@ Possible file names for PR templates: - `.github/PULL_REQUEST_TEMPLATE.md` - `.github/pull_request_template.md` +Possible file names for PR default merge message templates: + +- `.gitea/default_merge_message/MERGE_TEMPLATE.md` +- `.gitea/default_merge_message/REBASE_TEMPLATE.md` +- `.gitea/default_merge_message/REBASE-MERGE_TEMPLATE.md` +- `.gitea/default_merge_message/SQUASH_TEMPLATE.md` +- `.gitea/default_merge_message/MANUALLY-MERGED_TEMPLATE.md` +- `.gitea/default_merge_message/REBASE-UPDATE-ONLY_TEMPLATE.md` + +Possible file names for PR default merge message templates: + +- `.gitea/default_merge_message/MERGE_TEMPLATE.md` +- `.gitea/default_merge_message/REBASE_TEMPLATE.md` +- `.gitea/default_merge_message/REBASE-MERGE_TEMPLATE.md` +- `.gitea/default_merge_message/SQUASH_TEMPLATE.md` +- `.gitea/default_merge_message/MANUALLY-MERGED_TEMPLATE.md` +- `.gitea/default_merge_message/REBASE-UPDATE-ONLY_TEMPLATE.md` + +You can use the following variables enclosed in `${}` inside these templates which follow [os.Expand](https://pkg.go.dev/os#Expand) syntax: + +- BaseRepoOwnerName: Base repository owner name of this pull request +- BaseRepoName: Base repository name of this pull request +- BaseBranch: Base repository target branch name of this pull request +- HeadRepoOwnerName: Head repository owner name of this pull request +- HeadRepoName: Head repository name of this pull request +- HeadBranch: Head repository branch name of this pull request +- PullRequestTitle: Pull request's title +- PullRequestDescription: Pull request's description +- PullRequestPosterName: Pull request's poster name +- PullRequestIndex: Pull request's index number +- PullRequestReference: Pull request's reference char with index number. i.e. #1, !2 +- ClosingIssues: return a string contains all issues which will be closed by this pull request i.e. `close #1, close #2` + Additionally, the New Issue page URL can be suffixed with `?title=Issue+Title&body=Issue+Text` and the form will be populated with those strings. Those strings will be used instead of the template if there is one. ## Issue Template Directory diff --git a/integrations/api_helper_for_declarative_test.go b/integrations/api_helper_for_declarative_test.go index 5da72b7fb15a6..181a6469467cd 100644 --- a/integrations/api_helper_for_declarative_test.go +++ b/integrations/api_helper_for_declarative_test.go @@ -314,6 +314,37 @@ func doAPIManuallyMergePullRequest(ctx APITestContext, owner, repo, commitID str } } +func doAPIAutoMergePullRequest(ctx APITestContext, owner, repo string, index int64) func(*testing.T) { + return func(t *testing.T) { + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge?token=%s", + owner, repo, index, ctx.Token) + req := NewRequestWithJSON(t, http.MethodPost, urlStr, &forms.MergePullRequestForm{ + MergeMessageField: "doAPIMergePullRequest Merge", + Do: string(repo_model.MergeStyleMerge), + MergeWhenChecksSucceed: true, + }) + + if ctx.ExpectedCode != 0 { + ctx.Session.MakeRequest(t, req, ctx.ExpectedCode) + return + } + ctx.Session.MakeRequest(t, req, 200) + } +} + +func doAPICancelAutoMergePullRequest(ctx APITestContext, owner, repo string, index int64) func(*testing.T) { + return func(t *testing.T) { + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge?token=%s", + owner, repo, index, ctx.Token) + req := NewRequest(t, http.MethodDelete, urlStr) + if ctx.ExpectedCode != 0 { + ctx.Session.MakeRequest(t, req, ctx.ExpectedCode) + return + } + ctx.Session.MakeRequest(t, req, 204) + } +} + func doAPIGetBranch(ctx APITestContext, branch string, callback ...func(*testing.T, api.Branch)) func(*testing.T) { return func(t *testing.T) { req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/branches/%s?token=%s", ctx.Username, ctx.Reponame, branch, ctx.Token) diff --git a/integrations/api_packages_test.go b/integrations/api_packages_test.go index b3f6e88d9f20a..1f24807060df7 100644 --- a/integrations/api_packages_test.go +++ b/integrations/api_packages_test.go @@ -71,6 +71,44 @@ func TestPackageAPI(t *testing.T) { assert.Equal(t, packageVersion, p.Version) assert.NotNil(t, p.Creator) assert.Equal(t, user.Name, p.Creator.UserName) + + t.Run("RepositoryLink", func(t *testing.T) { + defer PrintCurrentTest(t)() + + p, err := packages_model.GetPackageByName(db.DefaultContext, user.ID, packages_model.TypeGeneric, packageName) + assert.NoError(t, err) + + // no repository link + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/%s?token=%s", user.Name, packageName, packageVersion, token)) + resp := MakeRequest(t, req, http.StatusOK) + + var ap1 *api.Package + DecodeJSON(t, resp, &ap1) + assert.Nil(t, ap1.Repository) + + // link to public repository + assert.NoError(t, packages_model.SetRepositoryLink(db.DefaultContext, p.ID, 1)) + + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/%s?token=%s", user.Name, packageName, packageVersion, token)) + resp = MakeRequest(t, req, http.StatusOK) + + var ap2 *api.Package + DecodeJSON(t, resp, &ap2) + assert.NotNil(t, ap2.Repository) + assert.EqualValues(t, 1, ap2.Repository.ID) + + // link to private repository + assert.NoError(t, packages_model.SetRepositoryLink(db.DefaultContext, p.ID, 2)) + + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/%s?token=%s", user.Name, packageName, packageVersion, token)) + resp = MakeRequest(t, req, http.StatusOK) + + var ap3 *api.Package + DecodeJSON(t, resp, &ap3) + assert.Nil(t, ap3.Repository) + + assert.NoError(t, packages_model.UnlinkRepositoryFromAllPackages(db.DefaultContext, 2)) + }) }) t.Run("ListPackageFiles", func(t *testing.T) { diff --git a/integrations/git_test.go b/integrations/git_test.go index 85f08606ee4b4..04cdf633bd0ed 100644 --- a/integrations/git_test.go +++ b/integrations/git_test.go @@ -82,6 +82,7 @@ func testGit(t *testing.T, u *url.URL) { t.Run("CreateAgitFlowPull", doCreateAgitFlowPull(dstPath, &httpContext, "master", "test/head")) t.Run("BranchProtectMerge", doBranchProtectPRMerge(&httpContext, dstPath)) + t.Run("AutoMerge", doAutoPRMerge(&httpContext, dstPath)) t.Run("CreatePRAndSetManuallyMerged", doCreatePRAndSetManuallyMerged(httpContext, httpContext, dstPath, "master", "test-manually-merge")) t.Run("MergeFork", func(t *testing.T) { defer PrintCurrentTest(t)() @@ -615,6 +616,88 @@ func doBranchDelete(ctx APITestContext, owner, repo, branch string) func(*testin } } +func doAutoPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) { + return func(t *testing.T) { + defer PrintCurrentTest(t)() + + ctx := NewAPITestContext(t, baseCtx.Username, baseCtx.Reponame) + + t.Run("CheckoutProtected", doGitCheckoutBranch(dstPath, "protected")) + t.Run("PullProtected", doGitPull(dstPath, "origin", "protected")) + t.Run("GenerateCommit", func(t *testing.T) { + _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-") + assert.NoError(t, err) + }) + t.Run("PushToUnprotectedBranch", doGitPushTestRepository(dstPath, "origin", "protected:unprotected3")) + var pr api.PullRequest + var err error + t.Run("CreatePullRequest", func(t *testing.T) { + pr, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, "protected", "unprotected3")(t) + assert.NoError(t, err) + }) + + // Request repository commits page + req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d/commits", baseCtx.Username, baseCtx.Reponame, pr.Index)) + resp := ctx.Session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + + // Get first commit URL + commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Last().Attr("href") + assert.True(t, exists) + assert.NotEmpty(t, commitURL) + + commitID := path.Base(commitURL) + + // Call API to add Pending status for commit + t.Run("CreateStatus", doAPICreateCommitStatus(ctx, commitID, api.CommitStatusPending)) + + // Cancel not existing auto merge + ctx.ExpectedCode = http.StatusNotFound + t.Run("CancelAutoMergePR", doAPICancelAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)) + + // Add auto merge request + ctx.ExpectedCode = http.StatusCreated + t.Run("AutoMergePR", doAPIAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)) + + // Can not create schedule twice + ctx.ExpectedCode = http.StatusConflict + t.Run("AutoMergePRTwice", doAPIAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)) + + // Cancel auto merge request + ctx.ExpectedCode = http.StatusNoContent + t.Run("CancelAutoMergePR", doAPICancelAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)) + + // Add auto merge request + ctx.ExpectedCode = http.StatusCreated + t.Run("AutoMergePR", doAPIAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)) + + // Check pr status + ctx.ExpectedCode = 0 + pr, err = doAPIGetPullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)(t) + assert.NoError(t, err) + assert.False(t, pr.HasMerged) + + // Call API to add Failure status for commit + t.Run("CreateStatus", doAPICreateCommitStatus(ctx, commitID, api.CommitStatusFailure)) + + // Check pr status + pr, err = doAPIGetPullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)(t) + assert.NoError(t, err) + assert.False(t, pr.HasMerged) + + // Call API to add Success status for commit + t.Run("CreateStatus", doAPICreateCommitStatus(ctx, commitID, api.CommitStatusSuccess)) + + // wait to let gitea merge stuff + time.Sleep(time.Second) + + // test pr status + pr, err = doAPIGetPullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)(t) + assert.NoError(t, err) + assert.True(t, pr.HasMerged) + } +} + func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, baseBranch, headBranch string) func(t *testing.T) { return func(t *testing.T) { defer PrintCurrentTest(t)() diff --git a/integrations/pull_merge_test.go b/integrations/pull_merge_test.go index 476f7ab52f9f8..c50913383c08b 100644 --- a/integrations/pull_merge_test.go +++ b/integrations/pull_merge_test.go @@ -6,6 +6,7 @@ package integrations import ( "bytes" + "context" "fmt" "net/http" "net/http/httptest" @@ -243,11 +244,11 @@ func TestCantMergeConflict(t *testing.T) { gitRepo, err := git.OpenRepository(git.DefaultContext, repo_model.RepoPath(user1.Name, repo1.Name)) assert.NoError(t, err) - err = pull.Merge(pr, user1, gitRepo, repo_model.MergeStyleMerge, "", "CONFLICT") + err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleMerge, "", "CONFLICT") assert.Error(t, err, "Merge should return an error due to conflict") assert.True(t, models.IsErrMergeConflicts(err), "Merge error is not a conflict error") - err = pull.Merge(pr, user1, gitRepo, repo_model.MergeStyleRebase, "", "CONFLICT") + err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleRebase, "", "CONFLICT") assert.Error(t, err, "Merge should return an error due to conflict") assert.True(t, models.IsErrRebaseConflicts(err), "Merge error is not a conflict error") gitRepo.Close() @@ -342,7 +343,7 @@ func TestCantMergeUnrelated(t *testing.T) { BaseBranch: "base", }).(*models.PullRequest) - err = pull.Merge(pr, user1, gitRepo, repo_model.MergeStyleMerge, "", "UNRELATED") + err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleMerge, "", "UNRELATED") assert.Error(t, err, "Merge should return an error due to unrelated") assert.True(t, models.IsErrMergeUnrelatedHistories(err), "Merge error is not a unrelated histories error") gitRepo.Close() diff --git a/integrations/pull_status_test.go b/integrations/pull_status_test.go index 07c73ceac6826..a5247f56ec5f3 100644 --- a/integrations/pull_status_test.go +++ b/integrations/pull_status_test.go @@ -63,20 +63,13 @@ func TestPullCreate_CommitStatus(t *testing.T) { api.CommitStatusWarning: "warning sign icon yellow", } + testCtx := NewAPITestContext(t, "user1", "repo1") + // Update commit status, and check if icon is updated as well for _, status := range statusList { // Call API to add status for commit - token := getTokenForLoggedInUser(t, session) - req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/user1/repo1/statuses/%s?token=%s", commitID, token), - api.CreateStatusOption{ - State: status, - TargetURL: "http://test.ci/", - Description: "", - Context: "testci", - }, - ) - session.MakeRequest(t, req, http.StatusCreated) + t.Run("CreateStatus", doAPICreateCommitStatus(testCtx, commitID, status)) req = NewRequestf(t, "GET", "/user1/repo1/pulls/1/commits") resp = session.MakeRequest(t, req, http.StatusOK) @@ -94,6 +87,24 @@ func TestPullCreate_CommitStatus(t *testing.T) { }) } +func doAPICreateCommitStatus(ctx APITestContext, commitID string, status api.CommitStatusState) func(*testing.T) { + return func(t *testing.T) { + req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/statuses/%s?token=%s", ctx.Username, ctx.Reponame, commitID, ctx.Token), + api.CreateStatusOption{ + State: status, + TargetURL: "http://test.ci/", + Description: "", + Context: "testci", + }, + ) + if ctx.ExpectedCode != 0 { + ctx.Session.MakeRequest(t, req, ctx.ExpectedCode) + return + } + ctx.Session.MakeRequest(t, req, http.StatusCreated) + } +} + func TestPullCreate_EmptyChangesWithCommits(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user1") diff --git a/integrations/repo_commits_test.go b/integrations/repo_commits_test.go index b53d988c58b7e..7107f43b0fed3 100644 --- a/integrations/repo_commits_test.go +++ b/integrations/repo_commits_test.go @@ -36,7 +36,6 @@ func doTestRepoCommitWithStatus(t *testing.T, state string, classes ...string) { defer prepareTestEnv(t)() session := loginUser(t, "user2") - token := getTokenForLoggedInUser(t, session) // Request repository commits page req := NewRequest(t, "GET", "/user2/repo1/commits/branch/master") @@ -49,16 +48,7 @@ func doTestRepoCommitWithStatus(t *testing.T, state string, classes ...string) { assert.NotEmpty(t, commitURL) // Call API to add status for commit - req = NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/statuses/"+path.Base(commitURL)+"?token="+token, - api.CreateStatusOption{ - State: api.CommitStatusState(state), - TargetURL: "http://test.ci/", - Description: "", - Context: "testci", - }, - ) - - resp = session.MakeRequest(t, req, http.StatusCreated) + t.Run("CreateStatus", doAPICreateCommitStatus(NewAPITestContext(t, "user2", "repo1"), path.Base(commitURL), api.CommitStatusState(state))) req = NewRequest(t, "GET", "/user2/repo1/commits/branch/master") resp = session.MakeRequest(t, req, http.StatusOK) diff --git a/models/db/error.go b/models/db/error.go index f20cc9b4cb71e..65572299436a6 100644 --- a/models/db/error.go +++ b/models/db/error.go @@ -42,3 +42,18 @@ func IsErrSSHDisabled(err error) bool { func (err ErrSSHDisabled) Error() string { return "SSH is disabled" } + +// ErrNotExist represents a non-exist error. +type ErrNotExist struct { + ID int64 +} + +// IsErrNotExist checks if an error is an ErrNotExist +func IsErrNotExist(err error) bool { + _, ok := err.(ErrNotExist) + return ok +} + +func (err ErrNotExist) Error() string { + return fmt.Sprintf("record does not exist [id: %d]", err.ID) +} diff --git a/models/error.go b/models/error.go index c29c818589fb4..0dc14c3e318c5 100644 --- a/models/error.go +++ b/models/error.go @@ -13,21 +13,6 @@ import ( "code.gitea.io/gitea/modules/git" ) -// ErrNotExist represents a non-exist error. -type ErrNotExist struct { - ID int64 -} - -// IsErrNotExist checks if an error is an ErrNotExist -func IsErrNotExist(err error) bool { - _, ok := err.(ErrNotExist) - return ok -} - -func (err ErrNotExist) Error() string { - return fmt.Sprintf("record does not exist [id: %d]", err.ID) -} - // ErrUserOwnRepos represents a "UserOwnRepos" kind of error. type ErrUserOwnRepos struct { UID int64 diff --git a/models/issue_comment.go b/models/issue_comment.go index ceea878662815..2cf3d5a61d50d 100644 --- a/models/issue_comment.go +++ b/models/issue_comment.go @@ -110,6 +110,10 @@ const ( CommentTypeDismissReview // 33 Change issue ref CommentTypeChangeIssueRef + // 34 pr was scheduled to auto merge when checks succeed + CommentTypePRScheduledToAutoMerge + // 35 pr was un scheduled to auto merge when checks succeed + CommentTypePRUnScheduledToAutoMerge ) var commentStrings = []string{ @@ -147,6 +151,8 @@ var commentStrings = []string{ "project_board", "dismiss_review", "change_issue_ref", + "pull_scheduled_merge", + "pull_cancel_scheduled_merge", } func (t CommentType) String() string { @@ -1354,6 +1360,28 @@ func CreatePushPullComment(ctx context.Context, pusher *user_model.User, pr *Pul return } +// CreateAutoMergeComment is a internal function, only use it for CommentTypePRScheduledToAutoMerge and CommentTypePRUnScheduledToAutoMerge CommentTypes +func CreateAutoMergeComment(ctx context.Context, typ CommentType, pr *PullRequest, doer *user_model.User) (comment *Comment, err error) { + if typ != CommentTypePRScheduledToAutoMerge && typ != CommentTypePRUnScheduledToAutoMerge { + return nil, fmt.Errorf("comment type %d cannot be used to create an auto merge comment", typ) + } + if err = pr.LoadIssueCtx(ctx); err != nil { + return + } + + if err = pr.LoadBaseRepoCtx(ctx); err != nil { + return + } + + comment, err = CreateCommentCtx(ctx, &CreateCommentOptions{ + Type: typ, + Doer: doer, + Repo: pr.BaseRepo, + Issue: pr.Issue, + }) + return +} + // getCommitsFromRepo get commit IDs from repo in between oldCommitID and newCommitID // isForcePush will be true if oldCommit isn't on the branch // Commit on baseBranch will skip diff --git a/models/issue_tracked_time.go b/models/issue_tracked_time.go index e675c7919378f..76ff874c59f21 100644 --- a/models/issue_tracked_time.go +++ b/models/issue_tracked_time.go @@ -251,7 +251,7 @@ func DeleteIssueUserTimes(issue *Issue, user *user_model.User) error { return err } if removedTime == 0 { - return ErrNotExist{} + return db.ErrNotExist{} } if err := issue.LoadRepo(ctx); err != nil { @@ -311,7 +311,7 @@ func deleteTimes(e db.Engine, opts FindTrackedTimesOptions) (removedTime int64, func deleteTime(e db.Engine, t *TrackedTime) error { if t.Deleted { - return ErrNotExist{ID: t.ID} + return db.ErrNotExist{ID: t.ID} } t.Deleted = true _, err := e.ID(t.ID).Cols("deleted").Update(t) @@ -325,7 +325,7 @@ func GetTrackedTimeByID(id int64) (*TrackedTime, error) { if err != nil { return nil, err } else if !has { - return nil, ErrNotExist{ID: id} + return nil, db.ErrNotExist{ID: id} } return time, nil } diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 9e46791ec607b..5a2297ac0d3c2 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -383,6 +383,10 @@ var migrations = []Migration{ NewMigration("Add package tables", addPackageTables), // v213 -> v214 NewMigration("Add allow edits from maintainers to PullRequest table", addAllowMaintainerEdit), + // v214 -> v215 + NewMigration("Add auto merge table", addAutoMergeTable), + // v215 -> v216 + NewMigration("allow to view files in PRs", addReviewViewedFiles), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v214.go b/models/migrations/v214.go new file mode 100644 index 0000000000000..dfe5d776a0f29 --- /dev/null +++ b/models/migrations/v214.go @@ -0,0 +1,23 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "xorm.io/xorm" +) + +func addAutoMergeTable(x *xorm.Engine) error { + type MergeStyle string + type PullAutoMerge struct { + ID int64 `xorm:"pk autoincr"` + PullID int64 `xorm:"UNIQUE"` + DoerID int64 `xorm:"NOT NULL"` + MergeStyle MergeStyle `xorm:"varchar(30)"` + Message string `xorm:"LONGTEXT"` + CreatedUnix int64 `xorm:"created"` + } + + return x.Sync2(&PullAutoMerge{}) +} diff --git a/models/migrations/v215.go b/models/migrations/v215.go new file mode 100644 index 0000000000000..d65488a18126f --- /dev/null +++ b/models/migrations/v215.go @@ -0,0 +1,25 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "code.gitea.io/gitea/models/pull" + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" +) + +func addReviewViewedFiles(x *xorm.Engine) error { + type ReviewState struct { + ID int64 `xorm:"pk autoincr"` + 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)"` + UpdatedFiles map[string]pull.ViewedState `xorm:"NOT NULL LONGTEXT JSON"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated"` + } + + return x.Sync2(new(ReviewState)) +} diff --git a/models/notification.go b/models/notification.go index a1248c240b948..d0b7852cd2481 100644 --- a/models/notification.go +++ b/models/notification.go @@ -825,7 +825,7 @@ func getNotificationByID(e db.Engine, notificationID int64) (*Notification, erro } if !ok { - return nil, ErrNotExist{ID: notificationID} + return nil, db.ErrNotExist{ID: notificationID} } return notification, nil diff --git a/models/project/issue.go b/models/project/issue.go index 0976185c495ad..6bde91668fae8 100644 --- a/models/project/issue.go +++ b/models/project/issue.go @@ -19,6 +19,9 @@ type ProjectIssue struct { //revive:disable-line:exported // If 0, then it has not been added to a specific board in the project ProjectBoardID int64 `xorm:"INDEX"` + + // the sorting order on the board + Sorting int64 `xorm:"NOT NULL DEFAULT 0"` } func init() { diff --git a/models/pull.go b/models/pull.go index d056888130e6b..8eab7569cd847 100644 --- a/models/pull.go +++ b/models/pull.go @@ -12,14 +12,16 @@ import ( "strings" "code.gitea.io/gitea/models/db" + 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" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" + + "xorm.io/builder" ) // PullRequestType defines pull request type @@ -95,6 +97,25 @@ func init() { db.RegisterModel(new(PullRequest)) } +func deletePullsByBaseRepoID(sess db.Engine, repoID int64) error { + deleteCond := builder.Select("id").From("pull_request").Where(builder.Eq{"pull_request.base_repo_id": repoID}) + + // Delete scheduled auto merges + if _, err := sess.In("pull_id", deleteCond). + Delete(&pull_model.AutoMerge{}); err != nil { + return err + } + + // Delete review states + if _, err := sess.In("pull_id", deleteCond). + Delete(&pull_model.ReviewState{}); err != nil { + return err + } + + _, err := sess.Delete(&PullRequest{BaseRepoID: repoID}) + return err +} + // MustHeadUserName returns the HeadRepo's username if failed return blank func (pr *PullRequest) MustHeadUserName() string { if err := pr.LoadHeadRepo(); err != nil { @@ -226,34 +247,6 @@ func (pr *PullRequest) LoadProtectedBranchCtx(ctx context.Context) (err error) { return } -// GetDefaultMergeMessage returns default message used when merging pull request -func (pr *PullRequest) GetDefaultMergeMessage(ctx context.Context) (string, error) { - if pr.HeadRepo == nil { - var err error - pr.HeadRepo, err = repo_model.GetRepositoryByIDCtx(ctx, pr.HeadRepoID) - if err != nil { - return "", fmt.Errorf("GetRepositoryById[%d]: %v", pr.HeadRepoID, err) - } - } - if err := pr.LoadIssueCtx(ctx); err != nil { - return "", fmt.Errorf("Cannot load issue %d for PR id %d: Error: %v", pr.IssueID, pr.ID, err) - } - if err := pr.LoadBaseRepoCtx(ctx); err != nil { - return "", fmt.Errorf("LoadBaseRepo: %v", err) - } - - issueReference := "#" - if pr.BaseRepo.UnitEnabledCtx(ctx, unit.TypeExternalTracker) { - issueReference = "!" - } - - if pr.BaseRepoID == pr.HeadRepoID { - return fmt.Sprintf("Merge pull request '%s' (%s%d) from %s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadBranch, pr.BaseBranch), nil - } - - return fmt.Sprintf("Merge pull request '%s' (%s%d) from %s:%s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseBranch), nil -} - // ReviewCount represents a count of Reviews type ReviewCount struct { IssueID int64 @@ -336,20 +329,6 @@ func (pr *PullRequest) getReviewedByLines(writer io.Writer) error { return committer.Commit() } -// GetDefaultSquashMessage returns default message used when squash and merging pull request -func (pr *PullRequest) GetDefaultSquashMessage(ctx context.Context) (string, error) { - if err := pr.LoadIssueCtx(ctx); err != nil { - return "", fmt.Errorf("LoadIssue: %v", err) - } - if err := pr.LoadBaseRepoCtx(ctx); err != nil { - return "", fmt.Errorf("LoadBaseRepo: %v", err) - } - if pr.BaseRepo.UnitEnabledCtx(ctx, unit.TypeExternalTracker) { - return fmt.Sprintf("%s (!%d)", pr.Issue.Title, pr.Issue.Index), nil - } - return fmt.Sprintf("%s (#%d)", pr.Issue.Title, pr.Issue.Index), nil -} - // GetGitRefName returns git ref for hidden pull request branch func (pr *PullRequest) GetGitRefName() string { return fmt.Sprintf("%s%d/head", git.PullPrefix, pr.Index) @@ -675,6 +654,18 @@ func (pr *PullRequest) IsSameRepo() bool { return pr.BaseRepoID == pr.HeadRepoID } +// GetPullRequestsByHeadBranch returns all prs by head branch +// Since there could be multiple prs with the same head branch, this function returns a slice of prs +func GetPullRequestsByHeadBranch(ctx context.Context, headBranch string, headRepoID int64) ([]*PullRequest, error) { + log.Trace("GetPullRequestsByHeadBranch: headBranch: '%s', headRepoID: '%d'", headBranch, headRepoID) + prs := make([]*PullRequest, 0, 2) + if err := db.GetEngine(ctx).Where(builder.Eq{"head_branch": headBranch, "head_repo_id": headRepoID}). + Find(&prs); err != nil { + return nil, err + } + return prs, nil +} + // GetBaseBranchHTMLURL returns the HTML URL of the base branch func (pr *PullRequest) GetBaseBranchHTMLURL() string { if err := pr.LoadBaseRepo(); err != nil { diff --git a/models/pull/automerge.go b/models/pull/automerge.go new file mode 100644 index 0000000000000..d0aca2e85f976 --- /dev/null +++ b/models/pull/automerge.go @@ -0,0 +1,98 @@ +// Copyright 2022 Gitea. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package pull + +import ( + "context" + "fmt" + + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/timeutil" +) + +// AutoMerge represents a pull request scheduled for merging when checks succeed +type AutoMerge struct { + ID int64 `xorm:"pk autoincr"` + PullID int64 `xorm:"UNIQUE"` + DoerID int64 `xorm:"NOT NULL"` + Doer *user_model.User `xorm:"-"` + MergeStyle repo_model.MergeStyle `xorm:"varchar(30)"` + Message string `xorm:"LONGTEXT"` + CreatedUnix timeutil.TimeStamp `xorm:"created"` +} + +// TableName return database table name for xorm +func (AutoMerge) TableName() string { + return "pull_auto_merge" +} + +func init() { + db.RegisterModel(new(AutoMerge)) +} + +// ErrAlreadyScheduledToAutoMerge represents a "PullRequestHasMerged"-error +type ErrAlreadyScheduledToAutoMerge struct { + PullID int64 +} + +func (err ErrAlreadyScheduledToAutoMerge) Error() string { + return fmt.Sprintf("pull request is already scheduled to auto merge when checks succeed [pull_id: %d]", err.PullID) +} + +// IsErrAlreadyScheduledToAutoMerge checks if an error is a ErrAlreadyScheduledToAutoMerge. +func IsErrAlreadyScheduledToAutoMerge(err error) bool { + _, ok := err.(ErrAlreadyScheduledToAutoMerge) + return ok +} + +// ScheduleAutoMerge schedules a pull request to be merged when all checks succeed +func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pullID int64, style repo_model.MergeStyle, message string) error { + // Check if we already have a merge scheduled for that pull request + if exists, _, err := GetScheduledMergeByPullID(ctx, pullID); err != nil { + return err + } else if exists { + return ErrAlreadyScheduledToAutoMerge{PullID: pullID} + } + + _, err := db.GetEngine(ctx).Insert(&AutoMerge{ + DoerID: doer.ID, + PullID: pullID, + MergeStyle: style, + Message: message, + }) + return err +} + +// GetScheduledMergeByPullID gets a scheduled pull request merge by pull request id +func GetScheduledMergeByPullID(ctx context.Context, pullID int64) (bool, *AutoMerge, error) { + scheduledPRM := &AutoMerge{} + exists, err := db.GetEngine(ctx).Where("pull_id = ?", pullID).Get(scheduledPRM) + if err != nil || !exists { + return false, nil, err + } + + doer, err := user_model.GetUserByIDCtx(ctx, scheduledPRM.DoerID) + if err != nil { + return false, nil, err + } + + scheduledPRM.Doer = doer + return true, scheduledPRM, nil +} + +// DeleteScheduledAutoMerge delete a scheduled pull request +func DeleteScheduledAutoMerge(ctx context.Context, pullID int64) error { + exist, scheduledPRM, err := GetScheduledMergeByPullID(ctx, pullID) + if err != nil { + return err + } else if !exist { + return db.ErrNotExist{ID: pullID} + } + + _, err = db.GetEngine(ctx).ID(scheduledPRM.ID).Delete(&AutoMerge{}) + return err +} diff --git a/models/pull/review_state.go b/models/pull/review_state.go new file mode 100644 index 0000000000000..1c465bf766772 --- /dev/null +++ b/models/pull/review_state.go @@ -0,0 +1,139 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +package pull + +import ( + "context" + "fmt" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/timeutil" +) + +// ViewedState stores for a file in which state it is currently viewed +type ViewedState uint8 + +const ( + Unviewed ViewedState = iota + HasChanged // cannot be set from the UI/ API, only internally + Viewed +) + +func (viewedState ViewedState) String() string { + switch viewedState { + case Unviewed: + return "unviewed" + case HasChanged: + return "has-changed" + case Viewed: + return "viewed" + default: + return fmt.Sprintf("unknown(value=%d)", viewedState) + } +} + +// ReviewState stores for a user-PR-commit combination which files the user has already viewed +type ReviewState struct { + ID int64 `xorm:"pk autoincr"` + UserID int64 `xorm:"NOT NULL UNIQUE(pull_commit_user)"` + PullID int64 `xorm:"NOT NULL INDEX UNIQUE(pull_commit_user) DEFAULT 0"` // Which PR was the review on? + CommitSHA string `xorm:"NOT NULL VARCHAR(40) UNIQUE(pull_commit_user)"` // Which commit was the head commit for the review? + UpdatedFiles map[string]ViewedState `xorm:"NOT NULL LONGTEXT JSON"` // Stores for each of the changed files of a PR whether they have been viewed, changed since last viewed, or not viewed + UpdatedUnix timeutil.TimeStamp `xorm:"updated"` // Is an accurate indicator of the order of commits as we do not expect it to be possible to make reviews on previous commits +} + +func init() { + db.RegisterModel(new(ReviewState)) +} + +// GetReviewState returns the ReviewState with all given values prefilled, whether or not it exists in the database. +// If the review didn't exist before in the database, it won't afterwards either. +// The returned boolean shows whether the review exists in the database +func GetReviewState(ctx context.Context, userID, pullID int64, commitSHA string) (*ReviewState, bool, error) { + review := &ReviewState{UserID: userID, PullID: pullID, CommitSHA: commitSHA} + has, err := db.GetEngine(ctx).Get(review) + return review, has, err +} + +// UpdateReviewState updates the given review inside the database, regardless of whether it existed before or not +// The given map of files with their viewed state will be merged with the previous review, if present +func UpdateReviewState(ctx context.Context, userID, pullID int64, commitSHA string, updatedFiles map[string]ViewedState) error { + log.Trace("Updating review for user %d, repo %d, commit %s with the updated files %v.", userID, pullID, commitSHA, updatedFiles) + + review, exists, err := GetReviewState(ctx, userID, pullID, commitSHA) + if err != nil { + return err + } + + if exists { + review.UpdatedFiles = mergeFiles(review.UpdatedFiles, updatedFiles) + } else if previousReview, err := getNewestReviewStateApartFrom(ctx, userID, pullID, commitSHA); err != nil { + return err + + // Overwrite the viewed files of the previous review if present + } else if previousReview != nil { + review.UpdatedFiles = mergeFiles(previousReview.UpdatedFiles, updatedFiles) + } else { + review.UpdatedFiles = updatedFiles + } + + // Insert or Update review + engine := db.GetEngine(ctx) + if !exists { + log.Trace("Inserting new review for user %d, repo %d, commit %s with the updated files %v.", userID, pullID, commitSHA, review.UpdatedFiles) + _, err := engine.Insert(review) + return err + } + log.Trace("Updating already existing review with ID %d (user %d, repo %d, commit %s) with the updated files %v.", review.ID, userID, pullID, commitSHA, review.UpdatedFiles) + _, err = engine.ID(review.ID).Update(&ReviewState{UpdatedFiles: review.UpdatedFiles}) + return err +} + +// mergeFiles merges the given maps of files with their viewing state into one map. +// Values from oldFiles will be overridden with values from newFiles +func mergeFiles(oldFiles, newFiles map[string]ViewedState) map[string]ViewedState { + if oldFiles == nil { + return newFiles + } else if newFiles == nil { + return oldFiles + } + + for file, viewed := range newFiles { + oldFiles[file] = viewed + } + return oldFiles +} + +// GetNewestReviewState gets the newest review of the current user in the current PR. +// The returned PR Review will be nil if the user has not yet reviewed this PR. +func GetNewestReviewState(ctx context.Context, userID, pullID int64) (*ReviewState, error) { + var review ReviewState + has, err := db.GetEngine(ctx).Where("user_id = ?", userID).And("pull_id = ?", pullID).OrderBy("updated_unix DESC").Get(&review) + if err != nil || !has { + return nil, err + } + return &review, err +} + +// getNewestReviewStateApartFrom is like GetNewestReview, except that the second newest review will be returned if the newest review points at the given commit. +// The returned PR Review will be nil if the user has not yet reviewed this PR. +func getNewestReviewStateApartFrom(ctx context.Context, userID, pullID int64, commitSHA string) (*ReviewState, error) { + var reviews []ReviewState + err := db.GetEngine(ctx).Where("user_id = ?", userID).And("pull_id = ?", pullID).OrderBy("updated_unix DESC").Limit(2).Find(&reviews) + // It would also be possible to use ".And("commit_sha != ?", commitSHA)" instead of the error handling below + // However, benchmarks show drastically improved performance by not doing that + + // Error cases in which no review should be returned + if err != nil || len(reviews) == 0 || (len(reviews) == 1 && reviews[0].CommitSHA == commitSHA) { + return nil, err + + // The first review points at the commit to exclude, hence skip to the second review + } else if len(reviews) >= 2 && reviews[0].CommitSHA == commitSHA { + return &reviews[1], nil + } + + // As we have no error cases left, the result must be the first element in the list + return &reviews[0], nil +} diff --git a/models/pull_test.go b/models/pull_test.go index 92cf9a6524171..6119bca692769 100644 --- a/models/pull_test.go +++ b/models/pull_test.go @@ -8,10 +8,7 @@ import ( "testing" "code.gitea.io/gitea/models/db" - repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unittest" - user_model "code.gitea.io/gitea/models/user" "github.com/stretchr/testify/assert" ) @@ -256,53 +253,3 @@ func TestPullRequest_GetWorkInProgressPrefixWorkInProgress(t *testing.T) { pr.Issue.Title = "[wip] " + original assert.Equal(t, "[wip]", pr.GetWorkInProgressPrefix()) } - -func TestPullRequest_GetDefaultMergeMessage_InternalTracker(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - pr := unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: 2}).(*PullRequest) - - msg, err := pr.GetDefaultMergeMessage(db.DefaultContext) - assert.NoError(t, err) - assert.Equal(t, "Merge pull request 'issue3' (#3) from branch2 into master", msg) - - pr.BaseRepoID = 1 - pr.HeadRepoID = 2 - msg, err = pr.GetDefaultMergeMessage(db.DefaultContext) - assert.NoError(t, err) - assert.Equal(t, "Merge pull request 'issue3' (#3) from user2/repo1:branch2 into master", msg) -} - -func TestPullRequest_GetDefaultMergeMessage_ExternalTracker(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - externalTracker := repo_model.RepoUnit{ - Type: unit.TypeExternalTracker, - Config: &repo_model.ExternalTrackerConfig{ - ExternalTrackerFormat: "https://someurl.com/{user}/{repo}/{issue}", - }, - } - baseRepo := &repo_model.Repository{Name: "testRepo", ID: 1} - baseRepo.Owner = &user_model.User{Name: "testOwner"} - baseRepo.Units = []*repo_model.RepoUnit{&externalTracker} - - pr := unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: 2, BaseRepo: baseRepo}).(*PullRequest) - - msg, err := pr.GetDefaultMergeMessage(db.DefaultContext) - assert.NoError(t, err) - assert.Equal(t, "Merge pull request 'issue3' (!3) from branch2 into master", msg) - - pr.BaseRepoID = 1 - pr.HeadRepoID = 2 - msg, err = pr.GetDefaultMergeMessage(db.DefaultContext) - assert.NoError(t, err) - assert.Equal(t, "Merge pull request 'issue3' (!3) from user2/repo1:branch2 into master", msg) -} - -func TestPullRequest_GetDefaultSquashMessage(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - pr := unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: 2}).(*PullRequest) - - msg, err := pr.GetDefaultSquashMessage(db.DefaultContext) - assert.NoError(t, err) - assert.Equal(t, "issue3 (#3)", msg) -} diff --git a/models/repo.go b/models/repo.go index fbc766850d954..e20bf90d97f37 100644 --- a/models/repo.go +++ b/models/repo.go @@ -704,7 +704,6 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error { &Notification{RepoID: repoID}, &ProtectedBranch{RepoID: repoID}, &ProtectedTag{RepoID: repoID}, - &PullRequest{BaseRepoID: repoID}, &repo_model.PushMirror{RepoID: repoID}, &Release{RepoID: repoID}, &repo_model.RepoIndexerStatus{RepoID: repoID}, @@ -723,6 +722,11 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error { return err } + // Delete Pulls and related objects + if err := deletePullsByBaseRepoID(sess, repoID); err != nil { + return err + } + // Delete Issues and related objects var attachmentPaths []string if attachmentPaths, err = deleteIssuesByRepoID(sess, repoID); err != nil { diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index 37f1c70545060..4c87f017b36a8 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -173,8 +173,6 @@ func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) { switch colName { case "type": switch unit.Type(db.Cell2Int64(val)) { - case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypeProjects: - r.Config = new(UnitConfig) case unit.TypeExternalWiki: r.Config = new(ExternalWikiConfig) case unit.TypeExternalTracker: @@ -183,8 +181,10 @@ func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) { r.Config = new(PullRequestsConfig) case unit.TypeIssues: r.Config = new(IssuesConfig) + case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypeProjects: + fallthrough default: - panic(fmt.Sprintf("unrecognized repo unit type: %v", *val)) + r.Config = new(UnitConfig) } } } diff --git a/models/user.go b/models/user.go index e8307796293f8..11234a881df05 100644 --- a/models/user.go +++ b/models/user.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" + 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/setting" @@ -82,6 +83,8 @@ func DeleteUser(ctx context.Context, u *user_model.User) (err error) { &Collaboration{UserID: u.ID}, &Stopwatch{UserID: u.ID}, &user_model.Setting{UserID: u.ID}, + &pull_model.AutoMerge{DoerID: u.ID}, + &pull_model.ReviewState{UserID: u.ID}, ); err != nil { return fmt.Errorf("deleteBeans: %v", err) } diff --git a/modules/context/org.go b/modules/context/org.go index 427e4910c0de9..9f4ce485e5ee7 100644 --- a/modules/context/org.go +++ b/modules/context/org.go @@ -72,12 +72,6 @@ func HandleOrgAssignment(ctx *Context, args ...bool) { ctx.ContextUser = org.AsUser() ctx.Data["Org"] = org - teams, err := org.LoadTeams() - if err != nil { - ctx.ServerError("LoadTeams", err) - } - ctx.Data["OrgTeams"] = teams - // Admin has super access. if ctx.IsSigned && ctx.Doer.IsAdmin { ctx.Org.IsOwner = true diff --git a/modules/convert/package.go b/modules/convert/package.go index 681219ca1a413..a4ea41d522e77 100644 --- a/modules/convert/package.go +++ b/modules/convert/package.go @@ -5,28 +5,38 @@ package convert import ( + "context" + + "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/packages" - "code.gitea.io/gitea/models/perm" + user_model "code.gitea.io/gitea/models/user" api "code.gitea.io/gitea/modules/structs" ) // ToPackage convert a packages.PackageDescriptor to api.Package -func ToPackage(pd *packages.PackageDescriptor) *api.Package { +func ToPackage(ctx context.Context, pd *packages.PackageDescriptor, doer *user_model.User) (*api.Package, error) { var repo *api.Repository if pd.Repository != nil { - repo = ToRepo(pd.Repository, perm.AccessModeNone) + permission, err := models.GetUserRepoPermission(ctx, pd.Repository, doer) + if err != nil { + return nil, err + } + + if permission.HasAccess() { + repo = ToRepo(pd.Repository, permission.AccessMode) + } } return &api.Package{ ID: pd.Version.ID, - Owner: ToUser(pd.Owner, nil), + Owner: ToUser(pd.Owner, doer), Repository: repo, - Creator: ToUser(pd.Creator, nil), + Creator: ToUser(pd.Creator, doer), Type: string(pd.Package.Type), Name: pd.Package.Name, Version: pd.Version.Version, CreatedAt: pd.Version.CreatedUnix.AsTime(), - } + }, nil } // ToPackageFile converts packages.PackageFileDescriptor to api.PackageFile diff --git a/modules/git/commit.go b/modules/git/commit.go index 8337e54fef27d..8c194ef502946 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -17,6 +17,7 @@ import ( "strings" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/util" ) // Commit represents a git commit. @@ -306,6 +307,35 @@ func (c *Commit) HasFile(filename string) (bool, error) { return true, nil } +// GetFileContent reads a file content as a string or returns false if this was not possible +func (c *Commit) GetFileContent(filename string, limit int) (string, error) { + entry, err := c.GetTreeEntryByPath(filename) + if err != nil { + return "", err + } + + r, err := entry.Blob().DataAsync() + if err != nil { + return "", err + } + defer r.Close() + + if limit > 0 { + bs := make([]byte, limit) + n, err := util.ReadAtMost(r, bs) + if err != nil { + return "", err + } + return string(bs[:n]), nil + } + + bytes, err := io.ReadAll(r) + if err != nil { + return "", err + } + return string(bytes), nil +} + // GetSubModules get all the sub modules of current revision git tree func (c *Commit) GetSubModules() (*ObjectCache, error) { if c.submoduleCache != nil { diff --git a/modules/git/repo_branch_gogit.go b/modules/git/repo_branch_gogit.go index ecedb56686d93..dc295765629bc 100644 --- a/modules/git/repo_branch_gogit.go +++ b/modules/git/repo_branch_gogit.go @@ -144,3 +144,19 @@ func (repo *Repository) WalkReferences(arg ObjectType, skip, limit int, walkfn f }) return i, err } + +// GetRefsBySha returns all references filtered with prefix that belong to a sha commit hash +func (repo *Repository) GetRefsBySha(sha, prefix string) ([]string, error) { + var revList []string + iter, err := repo.gogitRepo.References() + if err != nil { + return nil, err + } + err = iter.ForEach(func(ref *plumbing.Reference) error { + if ref.Hash().String() == sha && strings.HasPrefix(string(ref.Name()), prefix) { + revList = append(revList, string(ref.Name())) + } + return nil + }) + return revList, err +} diff --git a/modules/git/repo_branch_nogogit.go b/modules/git/repo_branch_nogogit.go index 3aed4abdf35b0..bc58991085b78 100644 --- a/modules/git/repo_branch_nogogit.go +++ b/modules/git/repo_branch_nogogit.go @@ -190,3 +190,15 @@ func walkShowRef(ctx context.Context, repoPath, arg string, skip, limit int, wal } return i, nil } + +// GetRefsBySha returns all references filtered with prefix that belong to a sha commit hash +func (repo *Repository) GetRefsBySha(sha, prefix string) ([]string, error) { + var revList []string + _, err := walkShowRef(repo.Ctx, repo.Path, "", 0, 0, func(walkSha, refname string) error { + if walkSha == sha && strings.HasPrefix(refname, prefix) { + revList = append(revList, refname) + } + return nil + }) + return revList, err +} diff --git a/modules/git/repo_branch_test.go b/modules/git/repo_branch_test.go index add04cb4a78d0..56f7387097df7 100644 --- a/modules/git/repo_branch_test.go +++ b/modules/git/repo_branch_test.go @@ -54,3 +54,44 @@ func BenchmarkRepository_GetBranches(b *testing.B) { } } } + +func TestGetRefsBySha(t *testing.T) { + bareRepo5Path := filepath.Join(testReposDir, "repo5_pulls") + bareRepo5, err := OpenRepository(DefaultContext, bareRepo5Path) + if err != nil { + t.Fatal(err) + } + defer bareRepo5.Close() + + // do not exist + branches, err := bareRepo5.GetRefsBySha("8006ff9adbf0cb94da7dad9e537e53817f9fa5c0", "") + assert.NoError(t, err) + assert.Len(t, branches, 0) + + // refs/pull/1/head + branches, err = bareRepo5.GetRefsBySha("c83380d7056593c51a699d12b9c00627bd5743e9", PullPrefix) + assert.NoError(t, err) + assert.EqualValues(t, []string{"refs/pull/1/head"}, branches) + + branches, err = bareRepo5.GetRefsBySha("d8e0bbb45f200e67d9a784ce55bd90821af45ebd", BranchPrefix) + assert.NoError(t, err) + assert.EqualValues(t, []string{"refs/heads/master", "refs/heads/master-clone"}, branches) + + branches, err = bareRepo5.GetRefsBySha("58a4bcc53ac13e7ff76127e0fb518b5262bf09af", BranchPrefix) + assert.NoError(t, err) + assert.EqualValues(t, []string{"refs/heads/test-patch-1"}, branches) +} + +func BenchmarkGetRefsBySha(b *testing.B) { + bareRepo5Path := filepath.Join(testReposDir, "repo5_pulls") + bareRepo5, err := OpenRepository(DefaultContext, bareRepo5Path) + if err != nil { + b.Fatal(err) + } + defer bareRepo5.Close() + + _, _ = bareRepo5.GetRefsBySha("8006ff9adbf0cb94da7dad9e537e53817f9fa5c0", "") + _, _ = bareRepo5.GetRefsBySha("d8e0bbb45f200e67d9a784ce55bd90821af45ebd", "") + _, _ = bareRepo5.GetRefsBySha("c83380d7056593c51a699d12b9c00627bd5743e9", "") + _, _ = bareRepo5.GetRefsBySha("58a4bcc53ac13e7ff76127e0fb518b5262bf09af", "") +} diff --git a/modules/git/repo_compare.go b/modules/git/repo_compare.go index f6b4f77645380..4b0cc8536b673 100644 --- a/modules/git/repo_compare.go +++ b/modules/git/repo_compare.go @@ -286,6 +286,15 @@ func (repo *Repository) GetPatch(base, head string, w io.Writer) error { return err } +// GetFilesChangedBetween returns a list of all files that have been changed between the given commits +func (repo *Repository) GetFilesChangedBetween(base, head string) ([]string, error) { + stdout, _, err := NewCommand(repo.Ctx, "diff", "--name-only", base+".."+head).RunStdString(&RunOpts{Dir: repo.Path}) + if err != nil { + return nil, err + } + return strings.Split(stdout, "\n"), err +} + // GetDiffFromMergeBase generates and return patch data from merge base to head func (repo *Repository) GetDiffFromMergeBase(base, head string, w io.Writer) error { stderr := new(bytes.Buffer) diff --git a/modules/git/tests/repos/repo5_pulls/HEAD b/modules/git/tests/repos/repo5_pulls/HEAD new file mode 100644 index 0000000000000..cb089cd89a7d7 --- /dev/null +++ b/modules/git/tests/repos/repo5_pulls/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/modules/git/tests/repos/repo5_pulls/config b/modules/git/tests/repos/repo5_pulls/config new file mode 100644 index 0000000000000..0a0ad6d9fe21d --- /dev/null +++ b/modules/git/tests/repos/repo5_pulls/config @@ -0,0 +1,6 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = true +[receive] + advertisePushOptions = true diff --git a/modules/git/tests/repos/repo5_pulls/description b/modules/git/tests/repos/repo5_pulls/description new file mode 100644 index 0000000000000..498b267a8c781 --- /dev/null +++ b/modules/git/tests/repos/repo5_pulls/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/modules/git/tests/repos/repo5_pulls/info/exclude b/modules/git/tests/repos/repo5_pulls/info/exclude new file mode 100644 index 0000000000000..a5196d1be8fb5 --- /dev/null +++ b/modules/git/tests/repos/repo5_pulls/info/exclude @@ -0,0 +1,6 @@ +# git ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +# *~ diff --git a/modules/git/tests/repos/repo5_pulls/objects/1a/2959532d2d18daa87bbd9f9d16051bef7b51df b/modules/git/tests/repos/repo5_pulls/objects/1a/2959532d2d18daa87bbd9f9d16051bef7b51df new file mode 100644 index 0000000000000..90464be0785ad Binary files /dev/null and b/modules/git/tests/repos/repo5_pulls/objects/1a/2959532d2d18daa87bbd9f9d16051bef7b51df differ diff --git a/modules/git/tests/repos/repo5_pulls/objects/56/51a1c4a48c47484a7a00a967ba4b6dde070bbf b/modules/git/tests/repos/repo5_pulls/objects/56/51a1c4a48c47484a7a00a967ba4b6dde070bbf new file mode 100644 index 0000000000000..cf9d59f7aed4e Binary files /dev/null and b/modules/git/tests/repos/repo5_pulls/objects/56/51a1c4a48c47484a7a00a967ba4b6dde070bbf differ diff --git a/modules/git/tests/repos/repo5_pulls/objects/58/a4bcc53ac13e7ff76127e0fb518b5262bf09af b/modules/git/tests/repos/repo5_pulls/objects/58/a4bcc53ac13e7ff76127e0fb518b5262bf09af new file mode 100644 index 0000000000000..efc69b12e66d4 --- /dev/null +++ b/modules/git/tests/repos/repo5_pulls/objects/58/a4bcc53ac13e7ff76127e0fb518b5262bf09af @@ -0,0 +1 @@ +x%��n�0 �;�)�0H��1 P�]��(�F2�T��k�7|�wu]�{O�қ�H��p��8�$A�1�"\��a�Rf��f�4� ��#ZL:J�\-��#fO2s��N���6��ӯ��N�;�v��#�� 3p��׺5���p�y^���y��L)x�ۼs_�n�1]�ާa�_�)@X \ No newline at end of file diff --git a/modules/git/tests/repos/repo5_pulls/objects/6d/0b4cca434953833618fcd3dd7acff42c800df1 b/modules/git/tests/repos/repo5_pulls/objects/6d/0b4cca434953833618fcd3dd7acff42c800df1 new file mode 100644 index 0000000000000..74e848ffcce76 Binary files /dev/null and b/modules/git/tests/repos/repo5_pulls/objects/6d/0b4cca434953833618fcd3dd7acff42c800df1 differ diff --git a/modules/git/tests/repos/repo5_pulls/objects/a5/2ca5af1b0277638ce20797f80bb1a2997470ab b/modules/git/tests/repos/repo5_pulls/objects/a5/2ca5af1b0277638ce20797f80bb1a2997470ab new file mode 100644 index 0000000000000..d6e616d902249 Binary files /dev/null and b/modules/git/tests/repos/repo5_pulls/objects/a5/2ca5af1b0277638ce20797f80bb1a2997470ab differ diff --git a/modules/git/tests/repos/repo5_pulls/objects/bf/4dc0709be60f043821351ff4bb2b17e5cabbb2 b/modules/git/tests/repos/repo5_pulls/objects/bf/4dc0709be60f043821351ff4bb2b17e5cabbb2 new file mode 100644 index 0000000000000..271cffb983313 --- /dev/null +++ b/modules/git/tests/repos/repo5_pulls/objects/bf/4dc0709be60f043821351ff4bb2b17e5cabbb2 @@ -0,0 +1,2 @@ +x��MN�0 �Y��l��'��� ��i%��4ܟ� �<=�}~��2Mcc�M�"���h֬z���)q(��CRI�O��tk�27Ƚ1=�GrL&]�Y�BFt�'&o��?^�/�u������Ѿ��*�L���ݛ�ů6,�\ǵ�O���� +�5�ؤ��#xj��吇C�A9�VyB�����c��i�ޤ^R�s�<�mo>�8��.kly���C�i� \ No newline at end of file diff --git a/modules/git/tests/repos/repo5_pulls/objects/d8/e0bbb45f200e67d9a784ce55bd90821af45ebd b/modules/git/tests/repos/repo5_pulls/objects/d8/e0bbb45f200e67d9a784ce55bd90821af45ebd new file mode 100644 index 0000000000000..0e2dc872fa9f7 --- /dev/null +++ b/modules/git/tests/repos/repo5_pulls/objects/d8/e0bbb45f200e67d9a784ce55bd90821af45ebd @@ -0,0 +1,2 @@ +x��AJAE]�)��"�VwW�t E�čz�NU5�$�T�9���&�$'1�+�y|�����f�6=^XS�NpE̅"�R�1v>W�(���gD���J��@%W�PKZ +�c�2����D2)�r����m�`��Yy�f����h�:j�\��)�۩�=����."�>�W�~6��5w<|>>�/�����| �mp?�X� \ No newline at end of file diff --git a/modules/git/tests/repos/repo5_pulls/objects/ed/5119b3c1f45547b6785bc03eac7f87570fa17f b/modules/git/tests/repos/repo5_pulls/objects/ed/5119b3c1f45547b6785bc03eac7f87570fa17f new file mode 100644 index 0000000000000..33d2a219e2731 Binary files /dev/null and b/modules/git/tests/repos/repo5_pulls/objects/ed/5119b3c1f45547b6785bc03eac7f87570fa17f differ diff --git a/modules/git/tests/repos/repo5_pulls/objects/ed/8f4d2fa5b2420706580d191f5dd50c4e491f3f b/modules/git/tests/repos/repo5_pulls/objects/ed/8f4d2fa5b2420706580d191f5dd50c4e491f3f new file mode 100644 index 0000000000000..d64847cf20d39 --- /dev/null +++ b/modules/git/tests/repos/repo5_pulls/objects/ed/8f4d2fa5b2420706580d191f5dd50c4e491f3f @@ -0,0 +1,3 @@ +x��AJAE]�)��!V��tM��"Y�F=@uw5�$���D\yo��h� +n��?����l��xbMd�,�T���C7f%��uĔ�P��3Jr;i:ԎJ,�`��5�P)�a�̔��1ƞ +9y��m�9����U��.n�Ig��Y����O���l�G,��:�=�q�s$D��M�����w�����a�_�S�6�o9X� \ No newline at end of file diff --git a/modules/git/tests/repos/repo5_pulls/objects/ee/469963e76ae1bb7ee83d7510df2864e6c8c640 b/modules/git/tests/repos/repo5_pulls/objects/ee/469963e76ae1bb7ee83d7510df2864e6c8c640 new file mode 100644 index 0000000000000..9cd9d008e1e17 Binary files /dev/null and b/modules/git/tests/repos/repo5_pulls/objects/ee/469963e76ae1bb7ee83d7510df2864e6c8c640 differ diff --git a/modules/git/tests/repos/repo5_pulls/objects/info/packs b/modules/git/tests/repos/repo5_pulls/objects/info/packs new file mode 100644 index 0000000000000..8bbc84872442d --- /dev/null +++ b/modules/git/tests/repos/repo5_pulls/objects/info/packs @@ -0,0 +1,2 @@ +P pack-81423f591973f5d9dab89cc45afa1c544448133e.pack + diff --git a/modules/git/tests/repos/repo5_pulls/objects/pack/pack-81423f591973f5d9dab89cc45afa1c544448133e.idx b/modules/git/tests/repos/repo5_pulls/objects/pack/pack-81423f591973f5d9dab89cc45afa1c544448133e.idx new file mode 100644 index 0000000000000..b66df23164695 Binary files /dev/null and b/modules/git/tests/repos/repo5_pulls/objects/pack/pack-81423f591973f5d9dab89cc45afa1c544448133e.idx differ diff --git a/modules/git/tests/repos/repo5_pulls/objects/pack/pack-81423f591973f5d9dab89cc45afa1c544448133e.pack b/modules/git/tests/repos/repo5_pulls/objects/pack/pack-81423f591973f5d9dab89cc45afa1c544448133e.pack new file mode 100644 index 0000000000000..a5dfc5ebde869 Binary files /dev/null and b/modules/git/tests/repos/repo5_pulls/objects/pack/pack-81423f591973f5d9dab89cc45afa1c544448133e.pack differ diff --git a/modules/git/tests/repos/repo5_pulls/packed-refs b/modules/git/tests/repos/repo5_pulls/packed-refs new file mode 100644 index 0000000000000..d0012b5441811 --- /dev/null +++ b/modules/git/tests/repos/repo5_pulls/packed-refs @@ -0,0 +1,5 @@ +# pack-refs with: peeled fully-peeled sorted +c83380d7056593c51a699d12b9c00627bd5743e9 refs/heads/test-patch-1 +c83380d7056593c51a699d12b9c00627bd5743e9 refs/pull/1/head +111cac04bd7d20301964e27a93698aabb5781b80 refs/pull/1/merge +72866af952e98d02a73003501836074b286a78f6 refs/tags/v0.9.99 diff --git a/modules/git/tests/repos/repo5_pulls/refs/heads/master b/modules/git/tests/repos/repo5_pulls/refs/heads/master new file mode 100644 index 0000000000000..9a8e3b2a34251 --- /dev/null +++ b/modules/git/tests/repos/repo5_pulls/refs/heads/master @@ -0,0 +1 @@ +d8e0bbb45f200e67d9a784ce55bd90821af45ebd diff --git a/modules/git/tests/repos/repo5_pulls/refs/heads/master-clone b/modules/git/tests/repos/repo5_pulls/refs/heads/master-clone new file mode 100644 index 0000000000000..9a8e3b2a34251 --- /dev/null +++ b/modules/git/tests/repos/repo5_pulls/refs/heads/master-clone @@ -0,0 +1 @@ +d8e0bbb45f200e67d9a784ce55bd90821af45ebd diff --git a/modules/git/tests/repos/repo5_pulls/refs/heads/test-patch-1 b/modules/git/tests/repos/repo5_pulls/refs/heads/test-patch-1 new file mode 100644 index 0000000000000..d8b26cb03776e --- /dev/null +++ b/modules/git/tests/repos/repo5_pulls/refs/heads/test-patch-1 @@ -0,0 +1 @@ +58a4bcc53ac13e7ff76127e0fb518b5262bf09af diff --git a/modules/git/tests/repos/repo5_pulls/refs/pull/4/head b/modules/git/tests/repos/repo5_pulls/refs/pull/4/head new file mode 100644 index 0000000000000..d8b26cb03776e --- /dev/null +++ b/modules/git/tests/repos/repo5_pulls/refs/pull/4/head @@ -0,0 +1 @@ +58a4bcc53ac13e7ff76127e0fb518b5262bf09af diff --git a/modules/notification/webhook/webhook.go b/modules/notification/webhook/webhook.go index d24440d585c79..c59e972ed6e6c 100644 --- a/modules/notification/webhook/webhook.go +++ b/modules/notification/webhook/webhook.go @@ -872,17 +872,19 @@ func notifyPackage(sender *user_model.User, pd *packages_model.PackageDescriptor return } - org := pd.Owner - if !org.IsOrganization() { - org = nil + ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("webhook.notifyPackage Package: %s[%d]", pd.Package.Name, pd.Package.ID)) + defer finished() + + apiPackage, err := convert.ToPackage(ctx, pd, sender) + if err != nil { + log.Error("Error converting package: %v", err) + return } if err := webhook_services.PrepareWebhooks(pd.Repository, webhook.HookEventPackage, &api.PackagePayload{ - Action: action, - Repository: convert.ToRepo(pd.Repository, perm.AccessModeNone), - Package: convert.ToPackage(pd), - Organization: convert.ToUser(org, nil), - Sender: convert.ToUser(sender, nil), + Action: action, + Package: apiPackage, + Sender: convert.ToUser(sender, nil), }); err != nil { log.Error("PrepareWebhooks: %v", err) } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index c040386ca70c4..8c7269703fe94 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1493,6 +1493,9 @@ pulls.allow_edits_from_maintainers = Allow edits from maintainers pulls.allow_edits_from_maintainers_desc = Users with write access to the base branch can also push to this branch pulls.allow_edits_from_maintainers_err = Updating failed pulls.compare_changes_desc = Select the branch to merge into and the branch to pull from. +pulls.has_viewed_file = Viewed +pulls.has_changed_since_last_review = Changed since your last review +pulls.viewed_files_label = %[1]d / %[2]d files viewed pulls.compare_base = merge into pulls.compare_compare = pull from pulls.switch_comparison_type = Switch comparison type @@ -1560,6 +1563,14 @@ pulls.squash_merge_pull_request = Create squash commit pulls.merge_manually = Manually merged pulls.merge_commit_id = The merge commit ID pulls.require_signed_wont_sign = The branch requires signed commits but this merge will not be signed +pulls.merge_pull_request_now = Merge Pull Request Now +pulls.rebase_merge_pull_request_now = Rebase and Merge Now +pulls.rebase_merge_commit_pull_request_now = Rebase and Merge Now (--no-ff) +pulls.squash_merge_pull_request_now = Squash and Merge Now +pulls.merge_pull_request_on_status_success = Merge Pull Request When All Checks Succeed +pulls.rebase_merge_pull_request_on_status_success = Rebase and Merge When All Checks Succeed +pulls.rebase_merge_commit_pull_request_on_status_success = Rebase and Merge (--no-ff) When All Checks Succeed +pulls.squash_merge_pull_request_on_status_success = Squash and Merge When All Checks Succeed pulls.invalid_merge_option = You cannot use this merge option for this pull request. pulls.merge_conflict = Merge Failed: There was a conflict whilst merging. Hint: Try a different strategy pulls.merge_conflict_summary = Error Message @@ -1588,9 +1599,16 @@ pulls.outdated_with_base_branch = This branch is out-of-date with the base branc pulls.closed_at = `closed this pull request %[2]s` pulls.reopened_at = `reopened this pull request %[2]s` pulls.merge_instruction_hint = `You can also view command line instructions.` - pulls.merge_instruction_step1_desc = From your project repository, check out a new branch and test the changes. pulls.merge_instruction_step2_desc = Merge the changes and update on Gitea. +pulls.merge_on_status_success = The pull request was scheduled to merge when all checks succeed. +pulls.merge_on_status_success_already_scheduled = This pull request is already scheduled to merge when all checks succeed. +pulls.pr_has_pending_merge_on_success = %[1]s scheduled this pull request to auto merge when all checks succeed %[2]s. +pulls.merge_pull_on_success_cancel = Cancel auto merge +pulls.pull_request_not_scheduled = This pull request is not scheduled to auto merge. +pulls.pull_request_schedule_canceled = The auto merge was canceled for this pull request. +pulls.pull_request_scheduled_auto_merge = `scheduled this pull request to auto merge when all checks succeed %[1]s` +pulls.pull_request_canceled_scheduled_auto_merge = `canceled auto merging this pull request when all checks succeed %[1]s` milestones.new = New Milestone milestones.open_tab = %d Open diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index 0a5b55324c548..44c0539da1a54 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -1479,6 +1479,7 @@ issues.content_history.created=criado issues.content_history.delete_from_history=Excluir do histórico issues.content_history.delete_from_history_confirm=Excluir do histórico? issues.content_history.options=Opções +issues.reference_link=Referência: %s compare.compare_base=base compare.compare_head=comparar diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index d8801f7517b98..41a3decceaea4 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -1478,6 +1478,7 @@ issues.content_history.created=criado issues.content_history.delete_from_history=Eliminar do histórico issues.content_history.delete_from_history_confirm=Eliminar do histórico? issues.content_history.options=Opções +issues.reference_link=Referência: %s compare.compare_base=base compare.compare_head=comparar diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 6587037ea3f8b..8fa9a0ed65378 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -984,7 +984,8 @@ func Routes() *web.Route { m.Post("/update", reqToken(), repo.UpdatePullRequest) m.Get("/commits", repo.GetPullRequestCommits) m.Combo("/merge").Get(repo.IsPullRequestMerged). - Post(reqToken(), mustNotBeArchived, bind(forms.MergePullRequestForm{}), repo.MergePullRequest) + Post(reqToken(), mustNotBeArchived, bind(forms.MergePullRequestForm{}), repo.MergePullRequest). + Delete(reqToken(), mustNotBeArchived, repo.CancelScheduledAutoMerge) m.Group("/reviews", func() { m.Combo(""). Get(repo.ListPullReviews). diff --git a/routers/api/v1/notify/threads.go b/routers/api/v1/notify/threads.go index fe89304dc878e..4effd6b3e02e0 100644 --- a/routers/api/v1/notify/threads.go +++ b/routers/api/v1/notify/threads.go @@ -9,6 +9,7 @@ import ( "net/http" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/convert" ) @@ -102,7 +103,7 @@ func ReadThread(ctx *context.APIContext) { func getThread(ctx *context.APIContext) *models.Notification { n, err := models.GetNotificationByID(ctx.ParamsInt64(":id")) if err != nil { - if models.IsErrNotExist(err) { + if db.IsErrNotExist(err) { ctx.Error(http.StatusNotFound, "GetNotificationByID", err) } else { ctx.InternalServerError(err) diff --git a/routers/api/v1/packages/package.go b/routers/api/v1/packages/package.go index f3aa19c3192e7..038924737ac7c 100644 --- a/routers/api/v1/packages/package.go +++ b/routers/api/v1/packages/package.go @@ -73,7 +73,12 @@ func ListPackages(ctx *context.APIContext) { apiPackages := make([]*api.Package, 0, len(pds)) for _, pd := range pds { - apiPackages = append(apiPackages, convert.ToPackage(pd)) + apiPackage, err := convert.ToPackage(ctx, pd, ctx.Doer) + if err != nil { + ctx.Error(http.StatusInternalServerError, "Error converting package for api", err) + return + } + apiPackages = append(apiPackages, apiPackage) } ctx.SetLinkHeader(int(count), listOptions.PageSize) @@ -115,7 +120,13 @@ func GetPackage(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - ctx.JSON(http.StatusOK, convert.ToPackage(ctx.Package.Descriptor)) + apiPackage, err := convert.ToPackage(ctx, ctx.Package.Descriptor, ctx.Doer) + if err != nil { + ctx.Error(http.StatusInternalServerError, "Error converting package for api", err) + return + } + + ctx.JSON(http.StatusOK, apiPackage) } // DeletePackage deletes a package diff --git a/routers/api/v1/repo/issue_tracked_time.go b/routers/api/v1/repo/issue_tracked_time.go index e42dc60a94c81..8ccad87838c56 100644 --- a/routers/api/v1/repo/issue_tracked_time.go +++ b/routers/api/v1/repo/issue_tracked_time.go @@ -10,6 +10,7 @@ import ( "time" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/context" @@ -281,7 +282,7 @@ func ResetIssueTime(ctx *context.APIContext) { err = models.DeleteIssueUserTimes(issue, ctx.Doer) if err != nil { - if models.IsErrNotExist(err) { + if db.IsErrNotExist(err) { ctx.Error(http.StatusNotFound, "DeleteIssueUserTimes", err) } else { ctx.Error(http.StatusInternalServerError, "DeleteIssueUserTimes", err) @@ -352,7 +353,7 @@ func DeleteTime(ctx *context.APIContext) { time, err := models.GetTrackedTimeByID(ctx.ParamsInt64(":id")) if err != nil { - if models.IsErrNotExist(err) { + if db.IsErrNotExist(err) { ctx.NotFound(err) return } diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index d6f349e332c16..f95bc6b16b16c 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/models" 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/unit" user_model "code.gitea.io/gitea/models/user" @@ -28,6 +29,7 @@ import ( "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/utils" asymkey_service "code.gitea.io/gitea/services/asymkey" + "code.gitea.io/gitea/services/automerge" "code.gitea.io/gitea/services/forms" issue_service "code.gitea.io/gitea/services/issue" pull_service "code.gitea.io/gitea/services/pull" @@ -799,13 +801,41 @@ func MergePullRequest(ctx *context.APIContext) { return } - // set defaults to propagate needed fields - if err := form.SetDefaults(ctx, pr); err != nil { - ctx.ServerError("SetDefaults", fmt.Errorf("SetDefaults: %v", err)) - return + if len(form.Do) == 0 { + form.Do = string(repo_model.MergeStyleMerge) + } + + message := strings.TrimSpace(form.MergeTitleField) + if len(message) == 0 { + message, err = pull_service.GetDefaultMergeMessage(ctx.Repo.GitRepo, pr, repo_model.MergeStyle(form.Do)) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetDefaultMergeMessage", err) + return + } + } + + form.MergeMessageField = strings.TrimSpace(form.MergeMessageField) + if len(form.MergeMessageField) > 0 { + message += "\n\n" + form.MergeMessageField } - if err := pull_service.Merge(pr, ctx.Doer, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), form.HeadCommitID, form.MergeTitleField); err != nil { + if form.MergeWhenChecksSucceed { + scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), message) + if err != nil { + if pull_model.IsErrAlreadyScheduledToAutoMerge(err) { + ctx.Error(http.StatusConflict, "ScheduleAutoMerge", err) + return + } + ctx.Error(http.StatusInternalServerError, "ScheduleAutoMerge", err) + return + } else if scheduled { + // nothing more to do ... + ctx.Status(http.StatusCreated) + return + } + } + + if err := pull_service.Merge(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), form.HeadCommitID, message); err != nil { if models.IsErrInvalidMergeStyle(err) { ctx.Error(http.StatusMethodNotAllowed, "Invalid merge style", fmt.Errorf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do))) } else if models.IsErrMergeConflicts(err) { @@ -1113,6 +1143,78 @@ func UpdatePullRequest(ctx *context.APIContext) { ctx.Status(http.StatusOK) } +// MergePullRequest cancel an auto merge scheduled for a given PullRequest by index +func CancelScheduledAutoMerge(ctx *context.APIContext) { + // swagger:operation DELETE /repos/{owner}/{repo}/pulls/{index}/merge repository repoCancelScheduledAutoMerge + // --- + // summary: Cancel the scheduled auto merge for the given pull request + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: index + // in: path + // description: index of the pull request to merge + // type: integer + // format: int64 + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + + pullIndex := ctx.ParamsInt64(":index") + pull, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, pullIndex) + if err != nil { + if models.IsErrPullRequestNotExist(err) { + ctx.NotFound() + return + } + ctx.InternalServerError(err) + return + } + + exist, autoMerge, err := pull_model.GetScheduledMergeByPullID(ctx, pull.ID) + if err != nil { + ctx.InternalServerError(err) + return + } + if !exist { + ctx.NotFound() + return + } + + if ctx.Doer.ID != autoMerge.DoerID { + allowed, err := models.IsUserRepoAdminCtx(ctx, ctx.Repo.Repository, ctx.Doer) + if err != nil { + ctx.InternalServerError(err) + return + } + if !allowed { + ctx.Error(http.StatusForbidden, "No permission to cancel", "user has no permission to cancel the scheduled auto merge") + return + } + } + + if err := automerge.RemoveScheduledAutoMerge(ctx, ctx.Doer, pull); err != nil { + ctx.InternalServerError(err) + } else { + ctx.Status(http.StatusNoContent) + } +} + // GetPullRequestCommits gets all commits associated with a given PR func GetPullRequestCommits(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/pulls/{index}/commits repository repoGetPullRequestCommits diff --git a/routers/init.go b/routers/init.go index 403fab00cd3b2..2e7fec86db8a9 100644 --- a/routers/init.go +++ b/routers/init.go @@ -39,6 +39,7 @@ import ( web_routers "code.gitea.io/gitea/routers/web" "code.gitea.io/gitea/services/auth" "code.gitea.io/gitea/services/auth/source/oauth2" + "code.gitea.io/gitea/services/automerge" "code.gitea.io/gitea/services/cron" "code.gitea.io/gitea/services/mailer" repo_migrations "code.gitea.io/gitea/services/migrations" @@ -147,6 +148,7 @@ func GlobalInitInstalled(ctx context.Context) { mirror_service.InitSyncMirrors() mustInit(webhook.Init) mustInit(pull_service.Init) + mustInit(automerge.Init) mustInit(task.Init) mustInit(repo_migrations.Init) eventsource.GetManager().Init() diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index d905c075e3cf3..7ddeb05f719cf 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -24,6 +24,7 @@ import ( issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" project_model "code.gitea.io/gitea/models/project" + 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" @@ -711,8 +712,6 @@ func RetrieveRepoMetas(ctx *context.Context, repo *repo_model.Repository, isPull } func getFileContentFromDefaultBranch(ctx *context.Context, filename string) (string, bool) { - var bytes []byte - if ctx.Repo.Commit == nil { var err error ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) @@ -733,7 +732,7 @@ func getFileContentFromDefaultBranch(ctx *context.Context, filename string) (str return "", false } defer r.Close() - bytes, err = io.ReadAll(r) + bytes, err := io.ReadAll(r) if err != nil { return "", false } @@ -1573,26 +1572,42 @@ func ViewIssue(ctx *context.Context) { } prConfig := prUnit.PullRequestsConfig() + var mergeStyle repo_model.MergeStyle // Check correct values and select default if ms, ok := ctx.Data["MergeStyle"].(repo_model.MergeStyle); !ok || !prConfig.IsMergeStyleAllowed(ms) { defaultMergeStyle := prConfig.GetDefaultMergeStyle() if prConfig.IsMergeStyleAllowed(defaultMergeStyle) && !ok { - ctx.Data["MergeStyle"] = defaultMergeStyle + mergeStyle = defaultMergeStyle } else if prConfig.AllowMerge { - ctx.Data["MergeStyle"] = repo_model.MergeStyleMerge + mergeStyle = repo_model.MergeStyleMerge } else if prConfig.AllowRebase { - ctx.Data["MergeStyle"] = repo_model.MergeStyleRebase + mergeStyle = repo_model.MergeStyleRebase } else if prConfig.AllowRebaseMerge { - ctx.Data["MergeStyle"] = repo_model.MergeStyleRebaseMerge + mergeStyle = repo_model.MergeStyleRebaseMerge } else if prConfig.AllowSquash { - ctx.Data["MergeStyle"] = repo_model.MergeStyleSquash + mergeStyle = repo_model.MergeStyleSquash } else if prConfig.AllowManualMerge { - ctx.Data["MergeStyle"] = repo_model.MergeStyleManuallyMerged - } else { - ctx.Data["MergeStyle"] = "" + mergeStyle = repo_model.MergeStyleManuallyMerged } } + + ctx.Data["MergeStyle"] = mergeStyle + + defaultMergeMessage, err := pull_service.GetDefaultMergeMessage(ctx.Repo.GitRepo, pull, mergeStyle) + if err != nil { + ctx.ServerError("GetDefaultMergeMessage", err) + return + } + ctx.Data["DefaultMergeMessage"] = defaultMergeMessage + + defaultSquashMergeMessage, err := pull_service.GetDefaultMergeMessage(ctx.Repo.GitRepo, pull, repo_model.MergeStyleSquash) + if err != nil { + ctx.ServerError("GetDefaultSquashMergeMessage", err) + return + } + ctx.Data["DefaultSquashMergeMessage"] = defaultSquashMergeMessage + if err = pull.LoadProtectedBranch(); err != nil { ctx.ServerError("LoadProtectedBranch", err) return @@ -1662,6 +1677,13 @@ func ViewIssue(ctx *context.Context) { } ctx.Data["StillCanManualMerge"] = stillCanManualMerge() + + // Check if there is a pending pr merge + ctx.Data["HasPendingPullRequestMerge"], ctx.Data["PendingPullRequestMerge"], err = pull_model.GetScheduledMergeByPullID(ctx, pull.ID) + if err != nil { + ctx.ServerError("GetScheduledMergeByPullID", err) + return + } } // Get Dependencies diff --git a/routers/web/repo/issue_timetrack.go b/routers/web/repo/issue_timetrack.go index 0809acc2e417f..28274a7f7b88f 100644 --- a/routers/web/repo/issue_timetrack.go +++ b/routers/web/repo/issue_timetrack.go @@ -9,6 +9,7 @@ import ( "time" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" @@ -63,7 +64,7 @@ func DeleteTime(c *context.Context) { t, err := models.GetTrackedTimeByID(c.ParamsInt64(":timeid")) if err != nil { - if models.IsErrNotExist(err) { + if db.IsErrNotExist(err) { c.NotFound("time not found", err) return } diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go index b4db2d5787bf9..03ea4fc5f4f46 100644 --- a/routers/web/repo/packages.go +++ b/routers/web/repo/packages.go @@ -62,6 +62,7 @@ func Packages(ctx *context.Context) { ctx.Data["HasPackages"] = hasPackages ctx.Data["PackageDescriptors"] = pds ctx.Data["Total"] = total + ctx.Data["RepositoryAccessMap"] = map[int64]bool{ctx.Repo.Repository.ID: true} // There is only the current repository pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5) pager.AddParam(ctx, "q", "Query") diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 74028c316cdc9..27b61309a5756 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -685,22 +685,35 @@ func ViewPullFiles(ctx *context.Context) { if fileOnly && (len(files) == 2 || len(files) == 1) { maxLines, maxFiles = -1, -1 } - - diff, err := gitdiff.GetDiff(gitRepo, - &gitdiff.DiffOptions{ - BeforeCommitID: startCommitID, - AfterCommitID: endCommitID, - SkipTo: ctx.FormString("skip-to"), - MaxLines: maxLines, - MaxLineCharacters: setting.Git.MaxGitDiffLineCharacters, - MaxFiles: maxFiles, - WhitespaceBehavior: gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)), - }, ctx.FormStrings("files")...) + diffOptions := &gitdiff.DiffOptions{ + BeforeCommitID: startCommitID, + AfterCommitID: endCommitID, + SkipTo: ctx.FormString("skip-to"), + MaxLines: maxLines, + MaxLineCharacters: setting.Git.MaxGitDiffLineCharacters, + MaxFiles: maxFiles, + WhitespaceBehavior: gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)), + } + + var methodWithError string + var diff *gitdiff.Diff + if !ctx.IsSigned { + diff, err = gitdiff.GetDiff(gitRepo, diffOptions, files...) + methodWithError = "GetDiff" + } else { + diff, err = gitdiff.SyncAndGetUserSpecificDiff(ctx, ctx.Doer.ID, pull, gitRepo, diffOptions, files...) + methodWithError = "SyncAndGetUserSpecificDiff" + } if err != nil { - ctx.ServerError("GetDiffRangeWithWhitespaceBehavior", err) + ctx.ServerError(methodWithError, err) return } + ctx.PageData["prReview"] = map[string]interface{}{ + "numberOfFiles": diff.NumFiles, + "numberOfViewedFiles": diff.NumViewedFiles, + } + if err = diff.LoadComments(ctx, issue, ctx.Doer); err != nil { ctx.ServerError("LoadComments", err) return @@ -752,11 +765,27 @@ func ViewPullFiles(ctx *context.Context) { if ctx.Written() { return } - ctx.Data["CurrentReview"], err = models.GetCurrentReview(ctx.Doer, issue) + + currentReview, err := models.GetCurrentReview(ctx.Doer, issue) if err != nil && !models.IsErrReviewNotExist(err) { ctx.ServerError("GetCurrentReview", err) return } + numPendingCodeComments := int64(0) + if currentReview != nil { + numPendingCodeComments, err = models.CountComments(&models.FindCommentsOptions{ + Type: models.CommentTypeCode, + ReviewID: currentReview.ID, + IssueID: issue.ID, + }) + if err != nil { + ctx.ServerError("CountComments", err) + return + } + } + ctx.Data["CurrentReview"] = currentReview + ctx.Data["PendingCodeCommentNumber"] = numPendingCodeComments + getBranchData(ctx, issue) ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.Doer.ID) ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) @@ -921,13 +950,22 @@ func MergePullRequest(ctx *context.Context) { return } - // set defaults to propagate needed fields - if err := form.SetDefaults(ctx, pr); err != nil { - ctx.ServerError("SetDefaults", fmt.Errorf("SetDefaults: %v", err)) - return + message := strings.TrimSpace(form.MergeTitleField) + if len(message) == 0 { + var err error + message, err = pull_service.GetDefaultMergeMessage(ctx.Repo.GitRepo, pr, repo_model.MergeStyle(form.Do)) + if err != nil { + ctx.ServerError("GetDefaultMergeMessage", err) + return + } + } + + form.MergeMessageField = strings.TrimSpace(form.MergeMessageField) + if len(form.MergeMessageField) > 0 { + message += "\n\n" + form.MergeMessageField } - if err := pull_service.Merge(pr, ctx.Doer, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), form.HeadCommitID, form.MergeTitleField); err != nil { + if err := pull_service.Merge(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), form.HeadCommitID, message); err != nil { if models.IsErrInvalidMergeStyle(err) { ctx.Flash.Error(ctx.Tr("repo.pulls.invalid_merge_option")) ctx.Redirect(issue.Link()) diff --git a/routers/web/repo/pull_review.go b/routers/web/repo/pull_review.go index 939b0037a09e0..98272ed48d8f8 100644 --- a/routers/web/repo/pull_review.go +++ b/routers/web/repo/pull_review.go @@ -9,8 +9,10 @@ import ( "net/http" "code.gitea.io/gitea/models" + pull_model "code.gitea.io/gitea/models/pull" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web" @@ -242,3 +244,47 @@ func DismissReview(ctx *context.Context) { ctx.Redirect(fmt.Sprintf("%s/pulls/%d#%s", ctx.Repo.RepoLink, comm.Issue.Index, comm.HashTag())) } + +// viewedFilesUpdate Struct to parse the body of a request to update the reviewed files of a PR +// If you want to implement an API to update the review, simply move this struct into modules. +type viewedFilesUpdate struct { + Files map[string]bool `json:"files"` + HeadCommitSHA string `json:"headCommitSHA"` +} + +func UpdateViewedFiles(ctx *context.Context) { + // Find corresponding PR + issue := checkPullInfo(ctx) + if ctx.Written() { + return + } + pull := issue.PullRequest + + var data *viewedFilesUpdate + err := json.NewDecoder(ctx.Req.Body).Decode(&data) + if err != nil { + log.Warn("Attempted to update a review but could not parse request body: %v", err) + ctx.Resp.WriteHeader(http.StatusBadRequest) + return + } + + // Expect the review to have been now if no head commit was supplied + if data.HeadCommitSHA == "" { + data.HeadCommitSHA = pull.HeadCommitID + } + + updatedFiles := make(map[string]pull_model.ViewedState, len(data.Files)) + for file, viewed := range data.Files { + + // Only unviewed and viewed are possible, has-changed can not be set from the outside + state := pull_model.Unviewed + if viewed { + state = pull_model.Viewed + } + updatedFiles[file] = state + } + + if err := pull_model.UpdateReviewState(ctx, ctx.Doer.ID, pull.ID, data.HeadCommitSHA, updatedFiles); err != nil { + ctx.ServerError("UpdateReview", err) + } +} diff --git a/routers/web/user/package.go b/routers/web/user/package.go index 04b4e1e8ecd6c..1c33998db90ba 100644 --- a/routers/web/user/package.go +++ b/routers/web/user/package.go @@ -58,6 +58,23 @@ func ListPackages(ctx *context.Context) { return } + repositoryAccessMap := make(map[int64]bool) + for _, pd := range pds { + if pd.Repository == nil { + continue + } + if _, has := repositoryAccessMap[pd.Repository.ID]; has { + continue + } + + permission, err := models.GetUserRepoPermission(ctx, pd.Repository, ctx.Doer) + if err != nil { + ctx.ServerError("GetUserRepoPermission", err) + return + } + repositoryAccessMap[pd.Repository.ID] = permission.HasAccess() + } + hasPackages, err := packages_model.HasOwnerPackages(ctx, ctx.ContextUser.ID) if err != nil { ctx.ServerError("HasOwnerPackages", err) @@ -72,6 +89,7 @@ func ListPackages(ctx *context.Context) { ctx.Data["HasPackages"] = hasPackages ctx.Data["PackageDescriptors"] = pds ctx.Data["Total"] = total + ctx.Data["RepositoryAccessMap"] = repositoryAccessMap pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5) pager.AddParam(ctx, "q", "Query") @@ -157,6 +175,17 @@ func ViewPackageVersion(ctx *context.Context) { ctx.Data["CanWritePackages"] = ctx.Package.AccessMode >= perm.AccessModeWrite || ctx.IsUserSiteAdmin() + hasRepositoryAccess := false + if pd.Repository != nil { + permission, err := models.GetUserRepoPermission(ctx, pd.Repository, ctx.Doer) + if err != nil { + ctx.ServerError("GetUserRepoPermission", err) + return + } + hasRepositoryAccess = permission.HasAccess() + } + ctx.Data["HasRepositoryAccess"] = hasRepositoryAccess + ctx.HTML(http.StatusOK, tplPackagesView) } diff --git a/routers/web/web.go b/routers/web/web.go index dcaad3d2bde1d..38754025eee81 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -849,6 +849,7 @@ func RegisterRoutes(m *web.Route) { m.Post("/deadline", bindIgnErr(structs.EditDeadlineOption{}), repo.UpdateIssueDeadline) m.Post("/watch", repo.IssueWatch) m.Post("/ref", repo.UpdateIssueRef) + m.Post("/viewed-files", repo.UpdateViewedFiles) m.Group("/dependency", func() { m.Post("/add", repo.AddDependency) m.Post("/delete", repo.RemoveDependency) diff --git a/services/automerge/automerge.go b/services/automerge/automerge.go new file mode 100644 index 0000000000000..85af2659c606f --- /dev/null +++ b/services/automerge/automerge.go @@ -0,0 +1,263 @@ +// Copyright 2021 Gitea. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package automerge + +import ( + "context" + "errors" + "fmt" + "strconv" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/models/db" + 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/git" + "code.gitea.io/gitea/modules/graceful" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/process" + "code.gitea.io/gitea/modules/queue" + pull_service "code.gitea.io/gitea/services/pull" +) + +// prAutoMergeQueue represents a queue to handle update pull request tests +var prAutoMergeQueue queue.UniqueQueue + +// Init runs the task queue to that handles auto merges +func Init() error { + prAutoMergeQueue = queue.CreateUniqueQueue("pr_auto_merge", handle, "") + if prAutoMergeQueue == nil { + return fmt.Errorf("Unable to create pr_auto_merge Queue") + } + go graceful.GetManager().RunWithShutdownFns(prAutoMergeQueue.Run) + return nil +} + +// handle passed PR IDs and test the PRs +func handle(data ...queue.Data) []queue.Data { + for _, d := range data { + var id int64 + var sha string + if _, err := fmt.Sscanf(d.(string), "%d_%s", &id, &sha); err != nil { + log.Error("could not parse data from pr_auto_merge queue (%v): %v", d, err) + continue + } + handlePull(id, sha) + } + return nil +} + +func addToQueue(pr *models.PullRequest, sha string) { + if err := prAutoMergeQueue.PushFunc(fmt.Sprintf("%d_%s", pr.ID, sha), func() error { + log.Trace("Adding pullID: %d to the pull requests patch checking queue with sha %s", pr.ID, sha) + return nil + }); err != nil { + log.Error("Error adding pullID: %d to the pull requests patch checking queue %v", pr.ID, err) + } +} + +// ScheduleAutoMerge if schedule is false and no error, pull can be merged directly +func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pull *models.PullRequest, style repo_model.MergeStyle, message string) (scheduled bool, err error) { + err = db.WithTx(func(ctx context.Context) error { + lastCommitStatus, err := pull_service.GetPullRequestCommitStatusState(ctx, pull) + if err != nil { + return err + } + + // we don't need to schedule + if lastCommitStatus.IsSuccess() { + return nil + } + + if err := pull_model.ScheduleAutoMerge(ctx, doer, pull.ID, style, message); err != nil { + return err + } + scheduled = true + + _, err = models.CreateAutoMergeComment(ctx, models.CommentTypePRScheduledToAutoMerge, pull, doer) + return err + }, ctx) + return +} + +// RemoveScheduledAutoMerge cancels a previously scheduled pull request +func RemoveScheduledAutoMerge(ctx context.Context, doer *user_model.User, pull *models.PullRequest) error { + return db.WithTx(func(ctx context.Context) error { + if err := pull_model.DeleteScheduledAutoMerge(ctx, pull.ID); err != nil { + return err + } + + _, err := models.CreateAutoMergeComment(ctx, models.CommentTypePRUnScheduledToAutoMerge, pull, doer) + return err + }, ctx) +} + +// MergeScheduledPullRequest merges a previously scheduled pull request when all checks succeeded +func MergeScheduledPullRequest(ctx context.Context, sha string, repo *repo_model.Repository) error { + pulls, err := getPullRequestsByHeadSHA(ctx, sha, repo, func(pr *models.PullRequest) bool { + return !pr.HasMerged && pr.CanAutoMerge() + }) + if err != nil { + return err + } + + for _, pr := range pulls { + addToQueue(pr, sha) + } + + return nil +} + +func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model.Repository, filter func(*models.PullRequest) bool) (map[int64]*models.PullRequest, error) { + gitRepo, err := git.OpenRepository(ctx, repo.RepoPath()) + if err != nil { + return nil, err + } + defer gitRepo.Close() + + refs, err := gitRepo.GetRefsBySha(sha, "") + if err != nil { + return nil, err + } + + pulls := make(map[int64]*models.PullRequest) + + for _, ref := range refs { + // Each pull branch starts with refs/pull/ we then go from there to find the index of the pr and then + // use that to get the pr. + if strings.HasPrefix(ref, git.PullPrefix) { + parts := strings.Split(ref[len(git.PullPrefix):], "/") + + // e.g. 'refs/pull/1/head' would be []string{"1", "head"} + if len(parts) != 2 { + log.Error("getPullRequestsByHeadSHA found broken pull ref [%s] on repo [%-v]", ref, repo) + continue + } + + prIndex, err := strconv.ParseInt(parts[0], 10, 64) + if err != nil { + log.Error("getPullRequestsByHeadSHA found broken pull ref [%s] on repo [%-v]", ref, repo) + continue + } + + p, err := models.GetPullRequestByIndexCtx(ctx, repo.ID, prIndex) + if err != nil { + // If there is no pull request for this branch, we don't try to merge it. + if models.IsErrPullRequestNotExist(err) { + continue + } + return nil, err + } + + if filter(p) { + pulls[p.ID] = p + } + } + } + + return pulls, nil +} + +func handlePull(pullID int64, sha string) { + ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), + fmt.Sprintf("Handle AutoMerge of pull[%d] with sha[%s]", pullID, sha)) + defer finished() + + pr, err := models.GetPullRequestByID(ctx, pullID) + if err != nil { + log.Error("GetPullRequestByID[%d]: %v", pullID, err) + return + } + + // Check if there is a scheduled pr in the db + exists, scheduledPRM, err := pull_model.GetScheduledMergeByPullID(ctx, pr.ID) + if err != nil { + log.Error("pull[%d] GetScheduledMergeByPullID: %v", pr.ID, err) + return + } + if !exists { + return + } + + // Get all checks for this pr + // We get the latest sha commit hash again to handle the case where the check of a previous push + // did not succeed or was not finished yet. + + if err = pr.LoadHeadRepoCtx(ctx); err != nil { + log.Error("pull[%d] LoadHeadRepoCtx: %v", pr.ID, err) + return + } + + headGitRepo, err := git.OpenRepository(ctx, pr.HeadRepo.RepoPath()) + if err != nil { + log.Error("OpenRepository: %v", err) + return + } + defer headGitRepo.Close() + + headBranchExist := headGitRepo.IsBranchExist(pr.HeadBranch) + + if pr.HeadRepo == nil || !headBranchExist { + log.Warn("Head branch of auto merge pr does not exist [HeadRepoID: %d, Branch: %s, PR ID: %d]", pr.HeadRepoID, pr.HeadBranch, pr.ID) + return + } + + // Check if all checks succeeded + pass, err := pull_service.IsPullCommitStatusPass(ctx, pr) + if err != nil { + log.Error("IsPullCommitStatusPass: %v", err) + return + } + if !pass { + log.Info("Scheduled auto merge pr has unsuccessful status checks [PullID: %d]", pr.ID) + return + } + + // Merge if all checks succeeded + doer, err := user_model.GetUserByIDCtx(ctx, scheduledPRM.DoerID) + if err != nil { + log.Error("GetUserByIDCtx: %v", err) + return + } + + perm, err := models.GetUserRepoPermission(ctx, pr.HeadRepo, doer) + if err != nil { + log.Error("GetUserRepoPermission: %v", err) + return + } + + if err := pull_service.CheckPullMergable(ctx, doer, &perm, pr, false, false); err != nil { + if errors.Is(pull_service.ErrUserNotAllowedToMerge, err) { + log.Info("PR %d was scheduled to automerge by an unauthorized user", pr.ID) + return + } + log.Error("pull[%d] CheckPullMergable: %v", pr.ID, err) + return + } + + var baseGitRepo *git.Repository + if pr.BaseRepoID == pr.HeadRepoID { + baseGitRepo = headGitRepo + } else { + if err = pr.LoadBaseRepoCtx(ctx); err != nil { + log.Error("LoadBaseRepoCtx: %v", err) + return + } + + baseGitRepo, err = git.OpenRepository(ctx, pr.BaseRepo.RepoPath()) + if err != nil { + log.Error("OpenRepository: %v", err) + return + } + defer baseGitRepo.Close() + } + + if err := pull_service.Merge(ctx, pr, doer, baseGitRepo, scheduledPRM.MergeStyle, "", scheduledPRM.Message); err != nil { + log.Error("pull_service.Merge: %v", err) + return + } +} diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 5c3adc1cd3ebe..6c61ed00ecb8d 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -6,7 +6,6 @@ package forms import ( - stdContext "context" "net/http" "net/url" "strings" @@ -592,6 +591,7 @@ type MergePullRequestForm struct { MergeCommitID string // only used for manually-merged HeadCommitID string `json:"head_commit_id,omitempty"` ForceMerge *bool `json:"force_merge,omitempty"` + MergeWhenChecksSucceed bool `json:"merge_when_checks_succeed,omitempty"` DeleteBranchAfterMerge bool `json:"delete_branch_after_merge,omitempty"` } @@ -601,31 +601,6 @@ func (f *MergePullRequestForm) Validate(req *http.Request, errs binding.Errors) return middleware.Validate(errs, ctx.Data, f, ctx.Locale) } -// SetDefaults if not provided for mergestyle and commit message -func (f *MergePullRequestForm) SetDefaults(ctx stdContext.Context, pr *models.PullRequest) (err error) { - if f.Do == "" { - f.Do = "merge" - } - - f.MergeTitleField = strings.TrimSpace(f.MergeTitleField) - if len(f.MergeTitleField) == 0 { - switch f.Do { - case "merge", "rebase-merge": - f.MergeTitleField, err = pr.GetDefaultMergeMessage(ctx) - case "squash": - f.MergeTitleField, err = pr.GetDefaultSquashMessage(ctx) - } - } - - f.MergeMessageField = strings.TrimSpace(f.MergeMessageField) - if len(f.MergeMessageField) > 0 { - f.MergeTitleField += "\n\n" + f.MergeMessageField - f.MergeMessageField = "" - } - - return -} - // CodeCommentForm form for adding code comments for PRs type CodeCommentForm struct { Origin string `binding:"Required;In(timeline,diff)"` diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index 1df16e50167cf..7fe056a4818b8 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -22,6 +22,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" + pull_model "code.gitea.io/gitea/models/pull" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/analyze" "code.gitea.io/gitea/modules/charset" @@ -602,25 +603,27 @@ func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine) Dif // DiffFile represents a file diff. type DiffFile struct { - Name string - OldName string - Index int - Addition, Deletion int - Type DiffFileType - IsCreated bool - IsDeleted bool - IsBin bool - IsLFSFile bool - IsRenamed bool - IsAmbiguous bool - IsSubmodule bool - Sections []*DiffSection - IsIncomplete bool - IsIncompleteLineTooLong bool - IsProtected bool - IsGenerated bool - IsVendored bool - Language string + Name string + OldName string + Index int + Addition, Deletion int + Type DiffFileType + IsCreated bool + IsDeleted bool + IsBin bool + IsLFSFile bool + IsRenamed bool + IsAmbiguous bool + IsSubmodule bool + Sections []*DiffSection + IsIncomplete bool + IsIncompleteLineTooLong bool + IsProtected bool + IsGenerated bool + IsVendored bool + IsViewed bool // User specific + HasChangedSinceLastReview bool // User specific + Language string } // GetType returns type of diff file. @@ -663,6 +666,18 @@ func (diffFile *DiffFile) GetTailSection(gitRepo *git.Repository, leftCommitID, return tailSection } +// GetDiffFileName returns the name of the diff file, or its old name in case it was deleted +func (diffFile *DiffFile) GetDiffFileName() string { + if diffFile.Name == "" { + return diffFile.OldName + } + return diffFile.Name +} + +func (diffFile *DiffFile) ShouldBeHidden() bool { + return diffFile.IsGenerated || diffFile.IsViewed +} + func getCommitFileLineCount(commit *git.Commit, filePath string) int { blob, err := commit.GetBlobByPath(filePath) if err != nil { @@ -677,10 +692,12 @@ func getCommitFileLineCount(commit *git.Commit, filePath string) int { // Diff represents a difference between two git trees. type Diff struct { - Start, End string - NumFiles, TotalAddition, TotalDeletion int - Files []*DiffFile - IsIncomplete bool + Start, End string + NumFiles int + TotalAddition, TotalDeletion int + Files []*DiffFile + IsIncomplete bool + NumViewedFiles int // user-specific } // LoadComments loads comments into each line @@ -1497,6 +1514,70 @@ func GetDiff(gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff return diff, nil } +// SyncAndGetUserSpecificDiff is like GetDiff, except that user specific data such as which files the given user has already viewed on the given PR will also be set +// Additionally, the database asynchronously is updated if files have changed since the last review +func SyncAndGetUserSpecificDiff(ctx context.Context, userID int64, pull *models.PullRequest, gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff, error) { + diff, err := GetDiff(gitRepo, opts, files...) + if err != nil { + return nil, err + } + review, err := pull_model.GetNewestReviewState(ctx, userID, pull.ID) + if err != nil || review == nil || review.UpdatedFiles == nil { + return diff, err + } + + latestCommit := opts.AfterCommitID + if latestCommit == "" { + latestCommit = pull.HeadBranch // opts.AfterCommitID is preferred because it handles PRs from forks correctly and the branch name doesn't + } + + changedFiles, err := gitRepo.GetFilesChangedBetween(review.CommitSHA, latestCommit) + if err != nil { + return diff, err + } + + filesChangedSinceLastDiff := make(map[string]pull_model.ViewedState) +outer: + for _, diffFile := range diff.Files { + fileViewedState := review.UpdatedFiles[diffFile.GetDiffFileName()] + + // Check whether it was previously detected that the file has changed since the last review + if fileViewedState == pull_model.HasChanged { + diffFile.HasChangedSinceLastReview = true + continue + } + + filename := diffFile.GetDiffFileName() + + // Check explicitly whether the file has changed since the last review + for _, changedFile := range changedFiles { + diffFile.HasChangedSinceLastReview = filename == changedFile + if diffFile.HasChangedSinceLastReview { + filesChangedSinceLastDiff[filename] = pull_model.HasChanged + continue outer // We don't want to check if the file is viewed here as that would fold the file, which is in this case unwanted + } + } + // Check whether the file has already been viewed + if fileViewedState == pull_model.Viewed { + diffFile.IsViewed = true + diff.NumViewedFiles++ + } + } + + // Explicitly store files that have changed in the database, if any is present at all. + // This has the benefit that the "Has Changed" attribute will be present as long as the user does not explicitly mark this file as viewed, so it will even survive a page reload after marking another file as viewed. + // On the other hand, this means that even if a commit reverting an unseen change is committed, the file will still be seen as changed. + if len(filesChangedSinceLastDiff) > 0 { + err := pull_model.UpdateReviewState(ctx, review.UserID, review.PullID, review.CommitSHA, filesChangedSinceLastDiff) + if err != nil { + log.Warn("Could not update review for user %d, pull %d, commit %s and the changed files %v: %v", review.UserID, review.PullID, review.CommitSHA, filesChangedSinceLastDiff, err) + return nil, err + } + } + + return diff, err +} + // CommentAsDiff returns c.Patch as *Diff func CommentAsDiff(c *models.Comment) (*Diff, error) { diff, err := ParsePatch(setting.Git.MaxGitDiffLines, diff --git a/services/pull/commit_status.go b/services/pull/commit_status.go index 143f3d50d0980..ec4cc2aa07844 100644 --- a/services/pull/commit_status.go +++ b/services/pull/commit_status.go @@ -137,5 +137,13 @@ func GetPullRequestCommitStatusState(ctx context.Context, pr *models.PullRequest return "", errors.Wrap(err, "GetLatestCommitStatus") } - return MergeRequiredContextsCommitStatus(commitStatuses, pr.ProtectedBranch.StatusCheckContexts), nil + if err := pr.LoadProtectedBranchCtx(ctx); err != nil { + return "", errors.Wrap(err, "LoadProtectedBranch") + } + var requiredContexts []string + if pr.ProtectedBranch != nil { + requiredContexts = pr.ProtectedBranch.StatusCheckContexts + } + + return MergeRequiredContextsCommitStatus(commitStatuses, requiredContexts), nil } diff --git a/services/pull/merge.go b/services/pull/merge.go index fe295cbe03ac2..e054df716b055 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -13,11 +13,13 @@ import ( "os" "path/filepath" "regexp" + "strconv" "strings" "time" "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" + 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" @@ -32,9 +34,101 @@ import ( issue_service "code.gitea.io/gitea/services/issue" ) +// GetDefaultMergeMessage returns default message used when merging pull request +func GetDefaultMergeMessage(baseGitRepo *git.Repository, pr *models.PullRequest, mergeStyle repo_model.MergeStyle) (string, error) { + if err := pr.LoadHeadRepo(); err != nil { + return "", err + } + if err := pr.LoadBaseRepo(); err != nil { + return "", err + } + if pr.BaseRepo == nil { + return "", repo_model.ErrRepoNotExist{ID: pr.BaseRepoID} + } + + if err := pr.LoadIssue(); err != nil { + return "", err + } + + isExternalTracker := pr.BaseRepo.UnitEnabled(unit.TypeExternalTracker) + issueReference := "#" + if isExternalTracker { + issueReference = "!" + } + + if mergeStyle != "" { + templateFilepath := fmt.Sprintf(".gitea/default_merge_message/%s_TEMPLATE.md", strings.ToUpper(string(mergeStyle))) + commit, err := baseGitRepo.GetBranchCommit(pr.BaseRepo.DefaultBranch) + if err != nil { + return "", err + } + templateContent, err := commit.GetFileContent(templateFilepath, setting.Repository.PullRequest.DefaultMergeMessageSize) + if err != nil { + if !git.IsErrNotExist(err) { + return "", err + } + } else { + vars := map[string]string{ + "BaseRepoOwnerName": pr.BaseRepo.OwnerName, + "BaseRepoName": pr.BaseRepo.Name, + "BaseBranch": pr.BaseBranch, + "HeadRepoOwnerName": "", + "HeadRepoName": "", + "HeadBranch": pr.HeadBranch, + "PullRequestTitle": pr.Issue.Title, + "PullRequestDescription": pr.Issue.Content, + "PullRequestPosterName": pr.Issue.Poster.Name, + "PullRequestIndex": strconv.FormatInt(pr.Index, 10), + "PullRequestReference": fmt.Sprintf("%s%d", issueReference, pr.Index), + } + if pr.HeadRepo != nil { + vars["HeadRepoOwnerName"] = pr.HeadRepo.OwnerName + vars["HeadRepoName"] = pr.HeadRepo.Name + } + refs, err := pr.ResolveCrossReferences(baseGitRepo.Ctx) + if err == nil { + closeIssueIndexes := make([]string, 0, len(refs)) + closeWord := "close" + if len(setting.Repository.PullRequest.CloseKeywords) > 0 { + closeWord = setting.Repository.PullRequest.CloseKeywords[0] + } + for _, ref := range refs { + if ref.RefAction == references.XRefActionCloses { + closeIssueIndexes = append(closeIssueIndexes, fmt.Sprintf("%s %s%d", closeWord, issueReference, ref.Issue.Index)) + } + } + if len(closeIssueIndexes) > 0 { + vars["ClosingIssues"] = strings.Join(closeIssueIndexes, ", ") + } else { + vars["ClosingIssues"] = "" + } + } + + return os.Expand(templateContent, func(s string) string { + return vars[s] + }), nil + } + } + + // Squash merge has a different from other styles. + if mergeStyle == repo_model.MergeStyleSquash { + return fmt.Sprintf("%s (%s%d)", pr.Issue.Title, issueReference, pr.Issue.Index), nil + } + + if pr.BaseRepoID == pr.HeadRepoID { + return fmt.Sprintf("Merge pull request '%s' (%s%d) from %s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadBranch, pr.BaseBranch), nil + } + + if pr.HeadRepo == nil { + return fmt.Sprintf("Merge pull request '%s' (%s%d) from :%s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadBranch, pr.BaseBranch), nil + } + + return fmt.Sprintf("Merge pull request '%s' (%s%d) from %s:%s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseBranch), nil +} + // Merge merges pull request to base repository. // Caller should check PR is ready to be merged (review and status checks) -func Merge(pr *models.PullRequest, doer *user_model.User, baseGitRepo *git.Repository, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string) error { +func Merge(ctx context.Context, pr *models.PullRequest, doer *user_model.User, baseGitRepo *git.Repository, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string) error { if err := pr.LoadHeadRepo(); err != nil { log.Error("LoadHeadRepo: %v", err) return fmt.Errorf("LoadHeadRepo: %v", err) @@ -46,6 +140,11 @@ func Merge(pr *models.PullRequest, doer *user_model.User, baseGitRepo *git.Repos pullWorkingPool.CheckIn(fmt.Sprint(pr.ID)) defer pullWorkingPool.CheckOut(fmt.Sprint(pr.ID)) + // Removing an auto merge pull and ignore if not exist + if err := pull_model.DeleteScheduledAutoMerge(db.DefaultContext, pr.ID); err != nil && !db.IsErrNotExist(err) { + return err + } + prUnit, err := pr.BaseRepo.GetUnit(unit.TypePullRequests) if err != nil { log.Error("pr.BaseRepo.GetUnit(unit.TypePullRequests): %v", err) @@ -73,18 +172,18 @@ func Merge(pr *models.PullRequest, doer *user_model.User, baseGitRepo *git.Repos pr.Merger = doer pr.MergerID = doer.ID - if _, err := pr.SetMerged(db.DefaultContext); err != nil { + if _, err := pr.SetMerged(ctx); err != nil { log.Error("setMerged [%d]: %v", pr.ID, err) } - if err := pr.LoadIssueCtx(db.DefaultContext); err != nil { + if err := pr.LoadIssueCtx(ctx); err != nil { log.Error("loadIssue [%d]: %v", pr.ID, err) } - if err := pr.Issue.LoadRepo(db.DefaultContext); err != nil { + if err := pr.Issue.LoadRepo(ctx); err != nil { log.Error("loadRepo for issue [%d]: %v", pr.ID, err) } - if err := pr.Issue.Repo.GetOwner(db.DefaultContext); err != nil { + if err := pr.Issue.Repo.GetOwner(ctx); err != nil { log.Error("GetOwner for issue repo [%d]: %v", pr.ID, err) } @@ -94,17 +193,17 @@ func Merge(pr *models.PullRequest, doer *user_model.User, baseGitRepo *git.Repos cache.Remove(pr.Issue.Repo.GetCommitsCountCacheKey(pr.BaseBranch, true)) // Resolve cross references - refs, err := pr.ResolveCrossReferences(db.DefaultContext) + refs, err := pr.ResolveCrossReferences(ctx) if err != nil { log.Error("ResolveCrossReferences: %v", err) return nil } for _, ref := range refs { - if err = ref.LoadIssueCtx(db.DefaultContext); err != nil { + if err = ref.LoadIssueCtx(ctx); err != nil { return err } - if err = ref.Issue.LoadRepo(db.DefaultContext); err != nil { + if err = ref.Issue.LoadRepo(ctx); err != nil { return err } close := ref.RefAction == references.XRefActionCloses diff --git a/services/pull/pull.go b/services/pull/pull.go index 5cef3c356f049..d226c60ec229e 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -253,7 +253,7 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string, graceful.GetManager().RunWithShutdownContext(func(ctx context.Context) { // There is no sensible way to shut this down ":-(" // If you don't let it run all the way then you will lose data - // FIXME: graceful: AddTestPullRequestTask needs to become a queue! + // TODO: graceful: AddTestPullRequestTask needs to become a queue! prs, err := models.GetUnmergedPullRequestsByHeadInfo(repoID, branch) if err != nil { diff --git a/services/pull/pull_test.go b/services/pull/pull_test.go index 81627ebb77bcf..09bae97780929 100644 --- a/services/pull/pull_test.go +++ b/services/pull/pull_test.go @@ -8,6 +8,12 @@ package pull import ( "testing" + "code.gitea.io/gitea/models" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/git" + "github.com/stretchr/testify/assert" ) @@ -29,3 +35,57 @@ func TestPullRequest_CommitMessageTrailersPattern(t *testing.T) { assert.True(t, commitMessageTrailersPattern.MatchString("Additional whitespace is accepted.\n\nSigned-off-by \t : \tBob ")) assert.True(t, commitMessageTrailersPattern.MatchString("Folded value.\n\nFolded-trailer: This is\n a folded\n trailer value\nOther-Trailer: Value")) } + +func TestPullRequest_GetDefaultMergeMessage_InternalTracker(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + pr := unittest.AssertExistsAndLoadBean(t, &models.PullRequest{ID: 2}).(*models.PullRequest) + + assert.NoError(t, pr.LoadBaseRepo()) + gitRepo, err := git.OpenRepository(git.DefaultContext, pr.BaseRepo.RepoPath()) + assert.NoError(t, err) + defer gitRepo.Close() + + mergeMessage, err := GetDefaultMergeMessage(gitRepo, pr, "") + assert.NoError(t, err) + assert.Equal(t, "Merge pull request 'issue3' (#3) from branch2 into master", mergeMessage) + + pr.BaseRepoID = 1 + pr.HeadRepoID = 2 + mergeMessage, err = GetDefaultMergeMessage(gitRepo, pr, "") + assert.NoError(t, err) + assert.Equal(t, "Merge pull request 'issue3' (#3) from user2/repo1:branch2 into master", mergeMessage) +} + +func TestPullRequest_GetDefaultMergeMessage_ExternalTracker(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + externalTracker := repo_model.RepoUnit{ + Type: unit.TypeExternalTracker, + Config: &repo_model.ExternalTrackerConfig{ + ExternalTrackerFormat: "https://someurl.com/{user}/{repo}/{issue}", + }, + } + baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) + baseRepo.Units = []*repo_model.RepoUnit{&externalTracker} + + pr := unittest.AssertExistsAndLoadBean(t, &models.PullRequest{ID: 2, BaseRepo: baseRepo}).(*models.PullRequest) + + assert.NoError(t, pr.LoadBaseRepo()) + gitRepo, err := git.OpenRepository(git.DefaultContext, pr.BaseRepo.RepoPath()) + assert.NoError(t, err) + defer gitRepo.Close() + + mergeMessage, err := GetDefaultMergeMessage(gitRepo, pr, "") + assert.NoError(t, err) + + assert.Equal(t, "Merge pull request 'issue3' (!3) from branch2 into master", mergeMessage) + + pr.BaseRepoID = 1 + pr.HeadRepoID = 2 + pr.BaseRepo = nil + pr.HeadRepo = nil + mergeMessage, err = GetDefaultMergeMessage(gitRepo, pr, "") + assert.NoError(t, err) + + assert.Equal(t, "Merge pull request 'issue3' (#3) from user2/repo2:branch2 into master", mergeMessage) +} diff --git a/services/repository/files/commit.go b/services/repository/files/commit.go index e7604e3f924d7..6ecabb4020bb6 100644 --- a/services/repository/files/commit.go +++ b/services/repository/files/commit.go @@ -14,6 +14,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/services/automerge" ) // CreateCommitStatus creates a new CommitStatus given a bunch of parameters @@ -44,6 +45,12 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creato return fmt.Errorf("NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %v", repo.ID, creator.ID, sha, err) } + if status.State.IsSuccess() { + if err := automerge.MergeScheduledPullRequest(ctx, sha, repo); err != nil { + return fmt.Errorf("MergeScheduledPullRequest[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err) + } + } + return nil } diff --git a/templates/package/shared/list.tmpl b/templates/package/shared/list.tmpl index 0b0f71283b155..9e6bf5ce9e7c2 100644 --- a/templates/package/shared/list.tmpl +++ b/templates/package/shared/list.tmpl @@ -30,7 +30,11 @@
{{$timeStr := TimeSinceUnix .Version.CreatedUnix $.i18n.Lang}} + {{$hasRepositoryAccess := false}} {{if .Repository}} + {{$hasRepositoryAccess = index $.RepositoryAccessMap .Repository.ID}} + {{end}} + {{if $hasRepositoryAccess}} {{$.i18n.Tr "packages.published_by_in" $timeStr .Creator.HomeLink (.Creator.GetDisplayName | Escape) .Repository.HTMLURL (.Repository.FullName | Escape) | Safe}} {{else}} {{$.i18n.Tr "packages.published_by" $timeStr .Creator.HomeLink (.Creator.GetDisplayName | Escape) | Safe}} diff --git a/templates/package/view.tmpl b/templates/package/view.tmpl index bb96da3410669..efad9f9b8fe9a 100644 --- a/templates/package/view.tmpl +++ b/templates/package/view.tmpl @@ -10,7 +10,7 @@
{{$timeStr := TimeSinceUnix .PackageDescriptor.Version.CreatedUnix $.i18n.Lang}} - {{if .PackageDescriptor.Repository}} + {{if .HasRepositoryAccess}} {{.i18n.Tr "packages.published_by_in" $timeStr .PackageDescriptor.Creator.HomeLink (.PackageDescriptor.Creator.GetDisplayName | Escape) .PackageDescriptor.Repository.HTMLURL (.PackageDescriptor.Repository.FullName | Escape) | Safe}} {{else}} {{.i18n.Tr "packages.published_by" $timeStr .PackageDescriptor.Creator.HomeLink (.PackageDescriptor.Creator.GetDisplayName | Escape) | Safe}} @@ -35,7 +35,7 @@ {{.i18n.Tr "packages.details"}}
{{svg .PackageDescriptor.Package.Type.SVGName 16 "mr-3"}} {{.PackageDescriptor.Package.Type.Name}}
- {{if .PackageDescriptor.Repository}} + {{if .HasRepositoryAccess}}
{{svg "octicon-repo" 16 "mr-3"}} {{.PackageDescriptor.Repository.FullName}}
{{end}}
{{svg "octicon-calendar" 16 "mr-3"}} {{.PackageDescriptor.Version.CreatedUnix.FormatDate}}
@@ -76,10 +76,10 @@ {{end}}
{{end}} - {{if or .CanWritePackages .PackageDescriptor.Repository}} + {{if or .CanWritePackages .HasRepositoryAccess}}
- {{if .PackageDescriptor.Repository}} + {{if .HasRepositoryAccess}}
{{svg "octicon-issue-opened" 16 "mr-3"}} {{.i18n.Tr "repo.issues"}}
{{end}} {{if .CanWritePackages}} diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl index 6f67f36422ec9..d6ad169821c2e 100644 --- a/templates/repo/diff/box.tmpl +++ b/templates/repo/diff/box.tmpl @@ -18,6 +18,12 @@ {{svg "octicon-diff" 16 "mr-2"}}{{.i18n.Tr "repo.diff.stats_desc" .Diff.NumFiles .Diff.TotalAddition .Diff.TotalDeletion | Str2html}}
+ {{if and .PageIsPullFiles $.SignedUserID (not .IsArchived)}} + + + {{end}} {{template "repo/diff/whitespace_dropdown" .}} {{template "repo/diff/options_dropdown" .}} {{if and .PageIsPullFiles $.SignedUserID (not .IsArchived)}} @@ -58,11 +64,11 @@ {{$isCsv := (call $.IsCsvFile $file)}} {{$showFileViewToggle := or $isImage (and (not $file.IsIncomplete) $isCsv)}} {{$nameHash := Sha1 $file.Name}} -
+

- {{if $file.IsGenerated}} + {{if $file.ShouldBeHidden}} {{svg "octicon-chevron-right" 18}} {{else}} {{svg "octicon-chevron-down" 18}} @@ -106,9 +112,18 @@ {{$.i18n.Tr "repo.diff.view_file"}} {{end}} {{end}} + {{if and $.IsSigned $.PageIsPullFiles (not $.IsArchived)}} + {{if $file.HasChangedSinceLastReview}} + {{$.i18n.Tr "repo.pulls.has_changed_since_last_review"}} + {{end}} +
+ + +
+ {{end}}

-
+
{{if or $file.IsIncomplete $file.IsBin}}
diff --git a/templates/repo/diff/comments.tmpl b/templates/repo/diff/comments.tmpl index 3b8f1c2a9c59d..863e295862854 100644 --- a/templates/repo/diff/comments.tmpl +++ b/templates/repo/diff/comments.tmpl @@ -37,7 +37,7 @@
{{if and .Review}} {{if eq .Review.Type 0}} -
+
{{$.root.i18n.Tr "repo.issues.review.pending"}}
{{else}} diff --git a/templates/repo/diff/new_review.tmpl b/templates/repo/diff/new_review.tmpl index cbaabe255e21b..e4110b50ed022 100644 --- a/templates/repo/diff/new_review.tmpl +++ b/templates/repo/diff/new_review.tmpl @@ -1,6 +1,7 @@ @@ -172,7 +172,7 @@
{{end}} {{end}} -
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index 7ff7f247fca6c..235f4c8fc2662 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -10,7 +10,8 @@ 22 = REVIEW, 23 = ISSUE_LOCKED, 24 = ISSUE_UNLOCKED, 25 = TARGET_BRANCH_CHANGED, 26 = DELETE_TIME_MANUAL, 27 = REVIEW_REQUEST, 28 = MERGE_PULL_REQUEST, 29 = PULL_PUSH_EVENT, 30 = PROJECT_CHANGED, 31 = PROJECT_BOARD_CHANGED - 32 = DISMISSED_REVIEW --> + 32 = DISMISSED_REVIEW, 33 = COMMENT_TYPE_CHANGE_ISSUE_REF, 34 = PR_SCHEDULE_TO_AUTO_MERGE, + 35 = CANCEL_SCHEDULED_AUTO_MERGE_PR --> {{if eq .Type 0}}
{{if .OriginalAuthor }} @@ -837,6 +838,15 @@ {{end}}
+ {{else if or (eq .Type 34) (eq .Type 35)}} +
+ {{svg "octicon-git-merge" 16}} + + {{.Poster.GetDisplayName}} + {{if eq .Type 34}}{{$.i18n.Tr "repo.pulls.pull_request_scheduled_auto_merge" $createdStr | Safe}} + {{else}}{{$.i18n.Tr "repo.pulls.pull_request_canceled_scheduled_auto_merge" $createdStr | Safe}}{{end}} + +
{{end}} {{end}} {{end}} diff --git a/templates/repo/issue/view_content/pull.tmpl b/templates/repo/issue/view_content/pull.tmpl index 8c43ace32e134..07b2e89d2432c 100644 --- a/templates/repo/issue/view_content/pull.tmpl +++ b/templates/repo/issue/view_content/pull.tmpl @@ -329,7 +329,7 @@ {{.CsrfTokenHtml}}
- +
@@ -375,10 +375,7 @@ {{.CsrfTokenHtml}}
- -
-
- +