diff --git a/cmd/serv.go b/cmd/serv.go index 2bfd111061..b3b9cac0c5 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -181,7 +181,8 @@ func runServ(c *cli.Context) error { if git.DefaultFeatures().SupportProcReceive { // for AGit Flow if cmd == "ssh_info" { - fmt.Print(`{"type":"gitea","version":1}`) + data := private.GetSshInfo(ctx) + fmt.Print(data) return nil } } diff --git a/modules/git/git.go b/modules/git/git.go index 05ca260855..b2bfea88ac 100644 --- a/modules/git/git.go +++ b/modules/git/git.go @@ -259,10 +259,16 @@ func syncGitConfig() (err error) { if err := configAddNonExist("receive.procReceiveRefs", "refs/for"); err != nil { return err } + if err := configAddNonExist("receive.procReceiveRefs", "refs/for-review"); err != nil { + return err + } } else { if err := configUnsetAll("receive.procReceiveRefs", "refs/for"); err != nil { return err } + if err := configUnsetAll("receive.procReceiveRefs", "refs/for-review"); err != nil { + return err + } } // Due to CVE-2022-24765, git now denies access to git directories which are not owned by current user. diff --git a/modules/git/ref.go b/modules/git/ref.go index 2db630e2ea..3ebcdfe359 100644 --- a/modules/git/ref.go +++ b/modules/git/ref.go @@ -67,7 +67,8 @@ func (ref *Reference) RefGroup() string { // or refs/for/ -o topic='' const ForPrefix = "refs/for/" -// TODO: /refs/for-review for suggest change interface +// ForReviewPrefix special ref to update a pull request: refs/for-review/ +const ForReviewPrefix = "refs/for-review/" // RefName represents a full git reference name type RefName string @@ -104,6 +105,12 @@ func (ref RefName) IsFor() bool { return strings.HasPrefix(string(ref), ForPrefix) } +var forReviewPattern = regexp.MustCompile(ForReviewPrefix + `[1-9][0-9]*`) + +func (ref RefName) IsForReview() bool { + return forReviewPattern.MatchString(string(ref)) +} + func (ref RefName) nameWithoutPrefix(prefix string) string { if strings.HasPrefix(string(ref), prefix) { return strings.TrimPrefix(string(ref), prefix) diff --git a/modules/private/manager.go b/modules/private/manager.go index 6055e553bd..66709acea0 100644 --- a/modules/private/manager.go +++ b/modules/private/manager.go @@ -36,6 +36,29 @@ func ReloadTemplates(ctx context.Context) ResponseExtra { return requestJSONClientMsg(req, "Reloaded") } +// Shutdown calls the internal shutdown function +func GetSshInfo(ctx context.Context) string { + reqURL := setting.LocalURL + "ssh_info" + req := newInternalRequest(ctx, reqURL, "GET") + + resp, err := req.Response() + if err != nil { + return "" + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "" + } + + content, err := io.ReadAll(resp.Body) + if err != nil { + return "" + } + + return string(content) +} + // FlushOptions represents the options for the flush call type FlushOptions struct { Timeout time.Duration diff --git a/routers/private/hook_pre_receive.go b/routers/private/hook_pre_receive.go index 0a3c8e2559..7ed8de6497 100644 --- a/routers/private/hook_pre_receive.go +++ b/routers/private/hook_pre_receive.go @@ -7,6 +7,8 @@ import ( "fmt" "net/http" "os" + "strconv" + "strings" "code.gitea.io/gitea/models" asymkey_model "code.gitea.io/gitea/models/asymkey" @@ -124,6 +126,8 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) { preReceiveTag(ourCtx, refFullName) case git.DefaultFeatures().SupportProcReceive && refFullName.IsFor(): preReceiveFor(ourCtx, refFullName) + case git.DefaultFeatures().SupportProcReceive && refFullName.IsForReview(): + preReceiveForReview(ourCtx, refFullName) default: ourCtx.AssertCanWriteCode() } @@ -447,6 +451,77 @@ func preReceiveFor(ctx *preReceiveContext, refFullName git.RefName) { } } +func canUpdateAgitPull(ctx *preReceiveContext, pull *issues_model.PullRequest) bool { + if pull.Flow != issues_model.PullRequestFlowAGit { + return false + } + + if ctx.opts.UserID == pull.Issue.PosterID { + return true + } + + if !pull.AllowMaintainerEdit { + return false + } + + if !ctx.loadPusherAndPermission() { + return false + } + + return ctx.userPerm.CanWrite(unit.TypeCode) +} + +func preReceiveForReview(ctx *preReceiveContext, refFullName git.RefName) { + if !ctx.AssertCreatePullRequest() { + return + } + + if ctx.Repo.Repository.IsEmpty { + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: "Can't create pull request for an empty repository.", + }) + return + } + + if ctx.opts.IsWiki { + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: "Pull requests are not supported on the wiki.", + }) + return + } + + pullIndex, err := strconv.ParseInt(strings.TrimPrefix(string(refFullName), git.ForReviewPrefix), 10, 64) + if err != nil { + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: "Unknow pull request index.", + }) + return + } + pull, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, pullIndex) + if err != nil { + log.Error("preReceiveForReview: GetPullRequestByIndex: err: %v", err) + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: "Unknow pull request index.", + }) + return + } + err = pull.LoadIssue(ctx) + if err != nil { + log.Error("preReceiveForReview: pull.LoadIssue: err: %v", err) + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: "Unknow pull request.", + }) + return + } + + if !canUpdateAgitPull(ctx, pull) { + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: "Unknow pull request.", + }) + return + } +} + func generateGitEnv(opts *private.HookOptions) (env []string) { env = os.Environ() if opts.GitAlternativeObjectDirectories != "" { diff --git a/routers/web/misc/misc.go b/routers/web/misc/misc.go index caaca7f521..8cfbb078d6 100644 --- a/routers/web/misc/misc.go +++ b/routers/web/misc/misc.go @@ -20,7 +20,7 @@ func SSHInfo(rw http.ResponseWriter, req *http.Request) { return } rw.Header().Set("content-type", "text/json;charset=UTF-8") - _, err := rw.Write([]byte(`{"type":"gitea","version":1}`)) + _, err := rw.Write([]byte(`{"type":"gitea","version":2}`)) if err != nil { log.Error("fail to write result: err: %v", err) rw.WriteHeader(http.StatusInternalServerError) diff --git a/services/agit/agit.go b/services/agit/agit.go index 52a70469e0..6e6d5157c0 100644 --- a/services/agit/agit.go +++ b/services/agit/agit.go @@ -36,6 +36,75 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git. return nil, fmt.Errorf("failed to get user. Error: %w", err) } + updateExistPull := func(pr *issues_model.PullRequest, i int) error { + // update exist pull request + if err := pr.LoadBaseRepo(ctx); err != nil { + return fmt.Errorf("unable to load base repository for PR[%d] Error: %w", pr.ID, err) + } + + oldCommitID, err := gitRepo.GetRefCommitID(pr.GetGitRefName()) + if err != nil { + return fmt.Errorf("unable to get ref commit id in base repository for PR[%d] Error: %w", pr.ID, err) + } + + if oldCommitID == opts.NewCommitIDs[i] { + results = append(results, private.HookProcReceiveRefResult{ + OriginalRef: opts.RefFullNames[i], + OldOID: opts.OldCommitIDs[i], + NewOID: opts.NewCommitIDs[i], + Err: "new commit is same with old commit", + }) + return nil + } + + if !forcePush { + output, _, err := git.NewCommand(ctx, "rev-list", "--max-count=1"). + AddDynamicArguments(oldCommitID, "^"+opts.NewCommitIDs[i]). + RunStdString(&git.RunOpts{Dir: repo.RepoPath(), Env: os.Environ()}) + if err != nil { + return fmt.Errorf("failed to detect force push: %w", err) + } else if len(output) > 0 { + results = append(results, private.HookProcReceiveRefResult{ + OriginalRef: opts.RefFullNames[i], + OldOID: opts.OldCommitIDs[i], + NewOID: opts.NewCommitIDs[i], + Err: "request `force-push` push option", + }) + return nil + } + } + + pr.HeadCommitID = opts.NewCommitIDs[i] + if err = pull_service.UpdateRef(ctx, pr); err != nil { + return fmt.Errorf("failed to update pull ref. Error: %w", err) + } + + pull_service.AddToTaskQueue(ctx, pr) + err = pr.LoadIssue(ctx) + if err != nil { + return fmt.Errorf("failed to load pull issue. Error: %w", err) + } + comment, err := pull_service.CreatePushPullComment(ctx, pusher, pr, oldCommitID, opts.NewCommitIDs[i]) + if err == nil && comment != nil { + notify_service.PullRequestPushCommits(ctx, pusher, pr, comment) + } + notify_service.PullRequestSynchronized(ctx, pusher, pr) + isForcePush := comment != nil && comment.IsForcePush + + results = append(results, private.HookProcReceiveRefResult{ + OldOID: oldCommitID, + NewOID: opts.NewCommitIDs[i], + Ref: pr.GetGitRefName(), + OriginalRef: opts.RefFullNames[i], + IsForcePush: isForcePush, + IsCreatePR: false, + URL: fmt.Sprintf("%s/pulls/%d", repo.HTMLURL(), pr.Index), + ShouldShowMessage: setting.Git.PullRequestPushMessage && repo.AllowsPulls(ctx), + }) + + return nil + } + for i := range opts.OldCommitIDs { if opts.NewCommitIDs[i] == objectFormat.EmptyObjectID().String() { results = append(results, private.HookProcReceiveRefResult{ @@ -47,6 +116,37 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git. continue } + if opts.RefFullNames[i].IsForReview() { + // try match refs/for-review/ + pullIndex, err := strconv.ParseInt(strings.TrimPrefix(string(opts.RefFullNames[i]), git.ForReviewPrefix), 10, 64) + if err != nil { + results = append(results, private.HookProcReceiveRefResult{ + OriginalRef: opts.RefFullNames[i], + OldOID: opts.OldCommitIDs[i], + NewOID: opts.NewCommitIDs[i], + Err: "Unknow pull request index", + }) + continue + } + pull, err := issues_model.GetPullRequestByIndex(ctx, repo.ID, pullIndex) + if err != nil { + results = append(results, private.HookProcReceiveRefResult{ + OriginalRef: opts.RefFullNames[i], + OldOID: opts.OldCommitIDs[i], + NewOID: opts.NewCommitIDs[i], + Err: "Unknow pull request index", + }) + continue + } + + err = updateExistPull(pull, i) + if err != nil { + return nil, err + } + + continue + } + if !opts.RefFullNames[i].IsFor() { results = append(results, private.HookProcReceiveRefResult{ IsNotMatched: true, @@ -158,70 +258,10 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git. continue } - // update exist pull request - if err := pr.LoadBaseRepo(ctx); err != nil { - return nil, fmt.Errorf("unable to load base repository for PR[%d] Error: %w", pr.ID, err) - } - - oldCommitID, err := gitRepo.GetRefCommitID(pr.GetGitRefName()) + err = updateExistPull(pr, i) if err != nil { - return nil, fmt.Errorf("unable to get ref commit id in base repository for PR[%d] Error: %w", pr.ID, err) + return nil, err } - - if oldCommitID == opts.NewCommitIDs[i] { - results = append(results, private.HookProcReceiveRefResult{ - OriginalRef: opts.RefFullNames[i], - OldOID: opts.OldCommitIDs[i], - NewOID: opts.NewCommitIDs[i], - Err: "new commit is same with old commit", - }) - continue - } - - if !forcePush { - output, _, err := git.NewCommand(ctx, "rev-list", "--max-count=1"). - AddDynamicArguments(oldCommitID, "^"+opts.NewCommitIDs[i]). - RunStdString(&git.RunOpts{Dir: repo.RepoPath(), Env: os.Environ()}) - if err != nil { - return nil, fmt.Errorf("failed to detect force push: %w", err) - } else if len(output) > 0 { - results = append(results, private.HookProcReceiveRefResult{ - OriginalRef: opts.RefFullNames[i], - OldOID: opts.OldCommitIDs[i], - NewOID: opts.NewCommitIDs[i], - Err: "request `force-push` push option", - }) - continue - } - } - - pr.HeadCommitID = opts.NewCommitIDs[i] - if err = pull_service.UpdateRef(ctx, pr); err != nil { - return nil, fmt.Errorf("failed to update pull ref. Error: %w", err) - } - - pull_service.AddToTaskQueue(ctx, pr) - err = pr.LoadIssue(ctx) - if err != nil { - return nil, fmt.Errorf("failed to load pull issue. Error: %w", err) - } - comment, err := pull_service.CreatePushPullComment(ctx, pusher, pr, oldCommitID, opts.NewCommitIDs[i]) - if err == nil && comment != nil { - notify_service.PullRequestPushCommits(ctx, pusher, pr, comment) - } - notify_service.PullRequestSynchronized(ctx, pusher, pr) - isForcePush := comment != nil && comment.IsForcePush - - results = append(results, private.HookProcReceiveRefResult{ - OldOID: oldCommitID, - NewOID: opts.NewCommitIDs[i], - Ref: pr.GetGitRefName(), - OriginalRef: opts.RefFullNames[i], - IsForcePush: isForcePush, - IsCreatePR: false, - URL: fmt.Sprintf("%s/pulls/%d", repo.HTMLURL(), pr.Index), - ShouldShowMessage: setting.Git.PullRequestPushMessage && repo.AllowsPulls(ctx), - }) } return results, nil diff --git a/services/pull/edits.go b/services/pull/edits.go index c7550dcb07..368092b5fa 100644 --- a/services/pull/edits.go +++ b/services/pull/edits.go @@ -22,6 +22,11 @@ func SetAllowEdits(ctx context.Context, doer *user_model.User, pr *issues_model. return ErrUserHasNoPermissionForAction } + if doer.ID == pr.Issue.PosterID { + pr.AllowMaintainerEdit = allow + return issues_model.UpdateAllowEdits(ctx, pr) + } + if err := pr.LoadHeadRepo(ctx); err != nil { return err } diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl index bb0bb2cff3..60621730da 100644 --- a/templates/repo/issue/view_content/sidebar.tmpl +++ b/templates/repo/issue/view_content/sidebar.tmpl @@ -674,7 +674,7 @@ {{end}} {{if and .Issue.IsPull .IsIssuePoster (not .Issue.IsClosed) .Issue.PullRequest.HeadRepo}} - {{if and (not (eq .Issue.PullRequest.HeadRepo.FullName .Issue.PullRequest.BaseRepo.FullName)) .CanWriteToHeadRepo}} + {{if or (eq .SignedUserID .Issue.PosterID) (and (not (eq .Issue.PullRequest.HeadRepo.FullName .Issue.PullRequest.BaseRepo.FullName)) .CanWriteToHeadRepo)}}