From 36c77dae2654c24a40c42a3a5e9376388309d209 Mon Sep 17 00:00:00 2001 From: a1012112796 <1012112796@qq.com> Date: Sun, 7 Feb 2021 16:15:02 +0800 Subject: [PATCH 01/32] feature: add agit flow support ref: https://git-repo.info/en/2020/03/agit-flow-and-git-repo/ example: ```Bash git checkout -b test echo "test" >> README.md git commit -m "test" git push origin HEAD:refs/for/master -o topic=test ``` Signed-off-by: a1012112796 <1012112796@qq.com> --- cmd/hook.go | 271 ++++++++++++++++++++++++++++++++ cmd/hook_test.go | 41 +++++ cmd/serv.go | 13 ++ models/migrations/migrations.go | 2 + models/migrations/v169.go | 25 +++ models/pull.go | 45 +++++- models/pull_list.go | 4 +- modules/git/git.go | 61 +++++++ modules/git/repo_branch.go | 8 + modules/private/hook.go | 43 +++++ modules/repository/hooks.go | 9 ++ routers/private/hook.go | 247 +++++++++++++++++++++++++++++ routers/private/internal.go | 1 + routers/repo/http.go | 5 + routers/repo/pull.go | 18 ++- routers/routes/web.go | 16 ++ services/pull/commit_status.go | 14 +- services/pull/pull.go | 45 +++++- services/pull/temp_repo.go | 11 +- services/pull/update.go | 7 + 20 files changed, 864 insertions(+), 22 deletions(-) create mode 100644 cmd/hook_test.go create mode 100644 models/migrations/v169.go diff --git a/cmd/hook.go b/cmd/hook.go index 1fcc0a18c316..8312c1a291b8 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -38,6 +38,7 @@ var ( subcmdHookPreReceive, subcmdHookUpdate, subcmdHookPostReceive, + subcmdHookProcReceive, }, } @@ -74,6 +75,18 @@ var ( }, }, } + // Note: new hook since git 2.29 + subcmdHookProcReceive = cli.Command{ + Name: "proc-receive", + Usage: "Delegate proc-receive Git hook", + Description: "This command should only be called by Git", + Action: runHookProcReceive, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "debug", + }, + }, + } ) type delayWriter struct { @@ -460,3 +473,261 @@ func pushOptions() map[string]string { } return opts } + +func runHookProcReceive(c *cli.Context) error { + setup("hooks/proc-receive.log", c.Bool("debug")) + + if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 { + if setting.OnlyAllowPushIfGiteaEnvironmentSet { + fail(`Rejecting changes as Gitea environment not set. +If you are pushing over SSH you must push with a key managed by +Gitea or set your environment appropriately.`, "") + } else { + return nil + } + } + + lf, err := os.OpenFile("test.log", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0777) + if err != nil { + fail("Internal Server Error", "open log file failed: %v", err) + } + defer lf.Close() + + if git.CheckGitVersionAtLeast("2.29") != nil { + fail("Internal Server Error", "git not support proc-receive.") + } + + reader := bufio.NewReader(os.Stdin) + repoUser := os.Getenv(models.EnvRepoUsername) + repoName := os.Getenv(models.EnvRepoName) + pusherID, _ := strconv.ParseInt(os.Getenv(models.EnvPusherID), 10, 64) + pusherName := os.Getenv(models.EnvPusherName) + + // 1. Version and features negotiation. + // S: PKT-LINE(version=1\0push-options atomic...) + // S: flush-pkt + // H: PKT-LINE(version=1\0push-options...) + // H: flush-pkt + + rs, err := readPktLine(reader) + if err != nil { + fail("Internal Server Error", "Pkt-Line format is wrong :%v", err) + } + if rs.Type != pktLineTypeData { + fail("Internal Server Error", "Pkt-Line format is wrong. get %v", rs) + } + + const VersionHead string = "version=1" + + if !strings.HasPrefix(rs.Data, VersionHead) { + fail("Internal Server Error", "Pkt-Line format is wrong. get %v", rs) + } + + hasPushOptions := false + response := []byte(VersionHead) + if strings.Contains(rs.Data, "push-options") { + response = append(response, byte(0)) + response = append(response, []byte("push-options")...) + hasPushOptions = true + } + response = append(response, []byte("\n")...) + + rs, err = readPktLine(reader) + if err != nil { + fail("Internal Server Error", "Pkt-Line format is wrong :%v", err) + } + if rs.Type != pktLineTypeFlush { + fail("Internal Server Error", "Pkt-Line format is wrong. get %v", rs) + } + + err = writePktLine(os.Stdout, pktLineTypeData, response) + if err != nil { + fail("Internal Server Error", "Pkt-Line response failed: %v", err) + } + + err = writePktLine(os.Stdout, pktLineTypeFlush, nil) + if err != nil { + fail("Internal Server Error", "Pkt-Line response failed: %v", err) + } + + // 2. receive commands from server. + // S: PKT-LINE( ) + // S: ... ... + // S: flush-pkt + // # receive push-options + // S: PKT-LINE(push-option) + // S: ... ... + // S: flush-pkt + hookOptions := private.HookOptions{ + UserName: pusherName, + UserID: pusherID, + } + hookOptions.OldCommitIDs = make([]string, 0, hookBatchSize) + hookOptions.NewCommitIDs = make([]string, 0, hookBatchSize) + hookOptions.RefFullNames = make([]string, 0, hookBatchSize) + + for { + rs, err = readPktLine(reader) + if err != nil { + fail("Internal Server Error", "Pkt-Line format is wrong :%v", err) + } + if rs.Type == pktLineTypeFlush { + break + } + t := strings.SplitN(rs.Data, " ", 3) + if len(t) != 3 { + continue + } + hookOptions.OldCommitIDs = append(hookOptions.OldCommitIDs, t[0]) + hookOptions.NewCommitIDs = append(hookOptions.NewCommitIDs, t[1]) + hookOptions.RefFullNames = append(hookOptions.RefFullNames, t[2]) + } + + hookOptions.GitPushOptions = make(map[string]string) + + if hasPushOptions { + for { + rs, err = readPktLine(reader) + if err != nil { + fail("Internal Server Error", "Pkt-Line format is wrong :%v", err) + } + if rs.Type == pktLineTypeFlush { + break + } + + kv := strings.SplitN(rs.Data, "=", 2) + if len(kv) == 2 { + hookOptions.GitPushOptions[kv[0]] = kv[1] + } + } + } + + // run hook + resp, err := private.HookProcReceive(repoUser, repoName, hookOptions) + if err != nil { + fail("Internal Server Error", "run proc-receive hook failed :%v", err) + } + + // 3 response result to service. + // # OK, but has an alternate reference. The alternate reference name + // # and other status can be given in option directives. + // H: PKT-LINE(ok ) + // H: PKT-LINE(option refname ) + // H: PKT-LINE(option old-oid ) + // H: PKT-LINE(option new-oid ) + // H: PKT-LINE(option forced-update) + // H: ... ... + // H: flush-pkt + for _, rs := range resp.Results { + err = writePktLine(os.Stdout, pktLineTypeData, []byte("ok "+rs.OrignRef)) + if err != nil { + fail("Internal Server Error", "Pkt-Line response failed: %v", err) + } + err = writePktLine(os.Stdout, pktLineTypeData, []byte("option refname "+rs.Ref)) + if err != nil { + fail("Internal Server Error", "Pkt-Line response failed: %v", err) + } + err = writePktLine(os.Stdout, pktLineTypeData, []byte("option old-oid "+rs.OldOID)) + if err != nil { + fail("Internal Server Error", "Pkt-Line response failed: %v", err) + } + err = writePktLine(os.Stdout, pktLineTypeData, []byte("option new-oid "+rs.NewOID)) + if err != nil { + fail("Internal Server Error", "Pkt-Line response failed: %v", err) + } + } + err = writePktLine(os.Stdout, pktLineTypeFlush, nil) + if err != nil { + fail("Internal Server Error", "Pkt-Line response failed: %v", err) + } + return nil +} + +// git PKT-Line api +// pktLineType message type of pkt-line +type pktLineType int64 + +const ( + // UnKnow type + pktLineTypeUnknow pktLineType = 0 + // flush-pkt "0000" + pktLineTypeFlush pktLineType = iota + // data line + pktLineTypeData +) + +// gitPktLine pkt-line api +type gitPktLine struct { + Type pktLineType + Length int64 + Data string +} + +func readPktLine(in *bufio.Reader) (r *gitPktLine, err error) { + // read prefix + lengthBytes := make([]byte, 4) + for i := 0; i < 4; i++ { + lengthBytes[i], err = in.ReadByte() + if err != nil { + return nil, err + } + } + r = new(gitPktLine) + r.Length, err = strconv.ParseInt(string(lengthBytes), 16, 64) + if err != nil { + return nil, err + } + + if r.Length == 0 { + r.Type = pktLineTypeFlush + return r, nil + } + + if r.Length <= 4 || r.Length > 65520 { + r.Type = pktLineTypeUnknow + return r, nil + } + + tmp := make([]byte, r.Length-4) + for i := range tmp { + tmp[i], err = in.ReadByte() + if err != nil { + return nil, err + } + } + + r.Type = pktLineTypeData + r.Data = string(tmp) + + return r, nil +} + +func writePktLine(out io.Writer, typ pktLineType, data []byte) error { + if typ == pktLineTypeFlush { + l, err := out.Write([]byte("0000")) + if err != nil { + return err + } + if l != 4 { + return fmt.Errorf("real write length is different with request, want %v, real %v", 4, l) + } + } + + if typ != pktLineTypeData { + return nil + } + + l := len(data) + 4 + tmp := []byte(fmt.Sprintf("%04x", l)) + tmp = append(tmp, data...) + + lr, err := out.Write(tmp) + if err != nil { + return err + } + if l != lr { + return fmt.Errorf("real write length is different with request, want %v, real %v", l, lr) + } + + return nil +} diff --git a/cmd/hook_test.go b/cmd/hook_test.go new file mode 100644 index 000000000000..b249a4706a55 --- /dev/null +++ b/cmd/hook_test.go @@ -0,0 +1,41 @@ +// Copyright 2021 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 cmd + +import ( + "bufio" + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPktLine(t *testing.T) { + // test read + s := strings.NewReader("0000") + r := bufio.NewReader(s) + result, err := readPktLine(r) + assert.NoError(t, err) + assert.Equal(t, pktLineTypeFlush, result.Type) + + s = strings.NewReader("0006a\n") + r = bufio.NewReader(s) + result, err = readPktLine(r) + assert.NoError(t, err) + assert.Equal(t, pktLineTypeData, result.Type) + assert.Equal(t, "a\n", result.Data) + + // test write + w := bytes.NewBuffer([]byte{}) + err = writePktLine(w, pktLineTypeFlush, nil) + assert.NoError(t, err) + assert.Equal(t, []byte("0000"), w.Bytes()) + + w.Reset() + err = writePktLine(w, pktLineTypeData, []byte("a\nb")) + assert.NoError(t, err) + assert.Equal(t, []byte("0007a\nb"), w.Bytes()) +} diff --git a/cmd/serv.go b/cmd/serv.go index 1e66cb511100..3491a57636d5 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -18,6 +18,7 @@ import ( "time" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/pprof" @@ -135,6 +136,13 @@ func runServ(c *cli.Context) error { } if len(words) < 2 { + if git.CheckGitVersionAtLeast("2.29") == nil { + // for AGit Flow + if cmd == "ssh_info" { + fmt.Print(`{"type":"gitea","version":1}`) + return nil + } + } fail("Too few arguments", "Too few arguments in cmd: %s", cmd) } @@ -203,6 +211,11 @@ func runServ(c *cli.Context) error { } } + // Because of special ref "refs/for" .. , need delay write permission check + if git.CheckGitVersionAtLeast("2.29") == nil { + requestedMode = models.AccessModeRead + } + results, err := private.ServCommand(keyID, username, reponame, requestedMode, verb, lfsVerb) if err != nil { if private.IsErrServCommand(err) { diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index c1a3b186cf87..e8179d297119 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -284,6 +284,8 @@ var migrations = []Migration{ NewMigration("Add user redirect", addUserRedirect), // v168 -> v169 NewMigration("Recreate user table to fix default values", recreateUserTableToFixDefaultValues), + // v169 -> v170 + NewMigration("Add agit style pull request support", addAgitStylePullRequest), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v169.go b/models/migrations/v169.go new file mode 100644 index 000000000000..8b08001a6a2b --- /dev/null +++ b/models/migrations/v169.go @@ -0,0 +1,25 @@ +// Copyright 2021 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 ( + "fmt" + + "xorm.io/xorm" +) + +func addAgitStylePullRequest(x *xorm.Engine) error { + type PullRequestStyle int + + type PullRequest struct { + TopicBranch string + Style PullRequestStyle + } + + if err := x.Sync2(new(PullRequest)); err != nil { + return fmt.Errorf("Sync2: %v", err) + } + return nil +} diff --git a/models/pull.go b/models/pull.go index 9b6f0830d7df..923c4989e82a 100644 --- a/models/pull.go +++ b/models/pull.go @@ -36,6 +36,18 @@ const ( PullRequestStatusError ) +// PullRequestStyle the style of pull request +type PullRequestStyle int + +const ( + // PullRequestStyleGithub github style from head branch to base branch + PullRequestStyleGithub PullRequestStyle = iota + // PullRequestStyleAGit Agit flow style pull request, head branch is not exist + PullRequestStyleAGit + // TODO Gerrit style + // PullRequestStyleGerrit +) + // PullRequest represents relation between pull request and repositories. type PullRequest struct { ID int64 `xorm:"pk autoincr"` @@ -57,6 +69,7 @@ type PullRequest struct { BaseRepo *Repository `xorm:"-"` HeadBranch string BaseBranch string + TopicBranch string // use for agit style pr ProtectedBranch *ProtectedBranch `xorm:"-"` MergeBase string `xorm:"VARCHAR(40)"` @@ -67,6 +80,8 @@ type PullRequest struct { MergedUnix timeutil.TimeStamp `xorm:"updated INDEX"` isHeadRepoLoaded bool `xorm:"-"` + + Style PullRequestStyle `xorm:"NOT NULL DEFAULT 0"` } // MustHeadUserName returns the HeadRepo's username if failed return blank @@ -467,8 +482,8 @@ func newPullRequestAttempt(repo *Repository, pull *Issue, labelIDs []int64, uuid func GetUnmergedPullRequest(headRepoID, baseRepoID int64, headBranch, baseBranch string) (*PullRequest, error) { pr := new(PullRequest) has, err := x. - Where("head_repo_id=? AND head_branch=? AND base_repo_id=? AND base_branch=? AND has_merged=? AND issue.is_closed=?", - headRepoID, headBranch, baseRepoID, baseBranch, false, false). + Where("head_repo_id=? AND head_branch=? AND base_repo_id=? AND base_branch=? AND has_merged=? AND style = ? AND issue.is_closed=?", + headRepoID, headBranch, baseRepoID, baseBranch, false, PullRequestStyleGithub, false). Join("INNER", "issue", "issue.id=pull_request.issue_id"). Get(pr) if err != nil { @@ -480,12 +495,36 @@ func GetUnmergedPullRequest(headRepoID, baseRepoID int64, headBranch, baseBranch return pr, nil } +// GetUnmergedAGitStylePullRequest get unmerged agit style pull request +func GetUnmergedAGitStylePullRequest(repoID int64, baseBranch, userName, topicBranch string) (*PullRequest, error) { + pr := new(PullRequest) + + headBranch := topicBranch + userName = strings.ToLower(userName) + if !strings.HasPrefix(topicBranch, userName+"/") { + headBranch = userName + "/" + topicBranch + } + + has, err := x. + Where("head_repo_id=? AND topic_branch=? AND base_repo_id=? AND base_branch=? AND has_merged=? AND style = ? AND issue.is_closed=?", + repoID, headBranch, repoID, baseBranch, false, PullRequestStyleAGit, false). + Join("INNER", "issue", "issue.id=pull_request.issue_id"). + Get(pr) + if err != nil { + return nil, err + } else if !has { + return nil, ErrPullRequestNotExist{0, 0, repoID, repoID, headBranch, baseBranch} + } + + return pr, nil +} + // GetLatestPullRequestByHeadInfo returns the latest pull request (regardless of its status) // by given head information (repo and branch). func GetLatestPullRequestByHeadInfo(repoID int64, branch string) (*PullRequest, error) { pr := new(PullRequest) has, err := x. - Where("head_repo_id = ? AND head_branch = ?", repoID, branch). + Where("head_repo_id = ? AND head_branch = ? AND style = ?", repoID, branch, PullRequestStyleGithub). OrderBy("id DESC"). Get(pr) if !has { diff --git a/models/pull_list.go b/models/pull_list.go index 989de46891e2..5c3a4ee70b9f 100644 --- a/models/pull_list.go +++ b/models/pull_list.go @@ -51,8 +51,8 @@ func listPullRequestStatement(baseRepoID int64, opts *PullRequestsOptions) (*xor func GetUnmergedPullRequestsByHeadInfo(repoID int64, branch string) ([]*PullRequest, error) { prs := make([]*PullRequest, 0, 2) return prs, x. - Where("head_repo_id = ? AND head_branch = ? AND has_merged = ? AND issue.is_closed = ?", - repoID, branch, false, false). + Where("head_repo_id = ? AND head_branch = ? AND has_merged = ? AND issue.is_closed = ? AND style = ?", + repoID, branch, false, false, PullRequestStyleGithub). Join("INNER", "issue", "issue.id = pull_request.issue_id"). Find(&prs) } diff --git a/modules/git/git.go b/modules/git/git.go index 6b15138a5c74..902b2ecea435 100644 --- a/modules/git/git.go +++ b/modules/git/git.go @@ -39,6 +39,9 @@ var ( // will be checked on Init goVersionLessThan115 = true + + // SupportProcReceive version >= 2.29.0 + SupportProcReceive bool ) func log(format string, args ...interface{}) { @@ -166,6 +169,19 @@ func Init(ctx context.Context) error { } } + if CheckGitVersionAtLeast("2.29") == nil { + // set support for AGit flow + if err := checkAndAddConfig("receive.procReceiveRefs", "refs/for"); err != nil { + return err + } + SupportProcReceive = true + } else { + if err := checkAndRemoveConfig("receive.procReceiveRefs", "refs/for"); err != nil { + return err + } + SupportProcReceive = false + } + if runtime.GOOS == "windows" { if err := checkAndSetConfig("core.longpaths", "true", true); err != nil { return err @@ -215,6 +231,51 @@ func checkAndSetConfig(key, defaultValue string, forceToDefault bool) error { return nil } +func checkAndAddConfig(key, value string) error { + _, stderr, err := process.GetManager().Exec("git.Init(get setting)", GitExecutable, "config", "--get", key, value) + if err != nil { + perr, ok := err.(*process.Error) + if !ok { + return fmt.Errorf("Failed to get git %s(%v) errType %T: %s", key, err, err, stderr) + } + eerr, ok := perr.Err.(*exec.ExitError) + if !ok || eerr.ExitCode() != 1 { + return fmt.Errorf("Failed to get git %s(%v) errType %T: %s", key, err, err, stderr) + } + if eerr.ExitCode() == 1 { + if _, stderr, err = process.GetManager().Exec(fmt.Sprintf("git.Init(set %s)", key), "git", "config", "--global", "--add", key, value); err != nil { + return fmt.Errorf("Failed to set git %s(%s): %s", key, err, stderr) + } + return nil + } + } + + return nil +} + +func checkAndRemoveConfig(key, value string) error { + _, stderr, err := process.GetManager().Exec("git.Init(get setting)", GitExecutable, "config", "--get", key, value) + if err != nil { + perr, ok := err.(*process.Error) + if !ok { + return fmt.Errorf("Failed to get git %s(%v) errType %T: %s", key, err, err, stderr) + } + eerr, ok := perr.Err.(*exec.ExitError) + if !ok || eerr.ExitCode() != 1 { + return fmt.Errorf("Failed to get git %s(%v) errType %T: %s", key, err, err, stderr) + } + if eerr.ExitCode() == 1 { + return nil + } + } + + if _, stderr, err = process.GetManager().Exec(fmt.Sprintf("git.Init(set %s)", key), "git", "config", "--global", "--unset-all", key, value); err != nil { + return fmt.Errorf("Failed to set git %s(%s): %s", key, err, stderr) + } + + return nil +} + // Fsck verifies the connectivity and validity of the objects in the database func Fsck(ctx context.Context, repoPath string, timeout time.Duration, args ...string) error { // Make sure timeout makes sense. diff --git a/modules/git/repo_branch.go b/modules/git/repo_branch.go index 58781eb1c71a..45795dc80f53 100644 --- a/modules/git/repo_branch.go +++ b/modules/git/repo_branch.go @@ -13,6 +13,14 @@ import ( // BranchPrefix base dir of the branch information file store on git const BranchPrefix = "refs/heads/" +// AGit Flow + +// PullRequestPrefix sepcial ref to create a pull request: refs/for/{targe-branch} +// Note: Different with origin AGit Flow, use push option to set head-branch(topic) +const PullRequestPrefix = "refs/for/" + +// TODO: /refs/for-review for suggest change interface + // IsReferenceExist returns true if given reference exists in the repository. func IsReferenceExist(repoPath, name string) bool { _, err := NewCommand("show-ref", "--verify", "--", name).RunInDir(repoPath) diff --git a/modules/private/hook.go b/modules/private/hook.go index 84d66943ba25..12c2a164297b 100644 --- a/modules/private/hook.go +++ b/modules/private/hook.go @@ -6,6 +6,7 @@ package private import ( "encoding/json" + "errors" "fmt" "net/http" "net/url" @@ -55,6 +56,7 @@ type HookOptions struct { GitPushOptions GitPushOptions ProtectedBranchID int64 IsDeployKey bool + IsWiki bool } // HookPostReceiveResult represents an individual result from PostReceive @@ -72,6 +74,20 @@ type HookPostReceiveBranchResult struct { URL string } +// HockProcReceiveResult represents an individual result from ProcReceive +type HockProcReceiveResult struct { + Results []HockProcReceiveRefResult + Err string +} + +// HockProcReceiveRefResult represents an individual result from ProcReceive +type HockProcReceiveRefResult struct { + OldOID string + NewOID string + Ref string + OrignRef string +} + // HookPreReceive check whether the provided commits are allowed func HookPreReceive(ownerName, repoName string, opts HookOptions) (int, string) { reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/pre-receive/%s/%s", @@ -123,6 +139,33 @@ func HookPostReceive(ownerName, repoName string, opts HookOptions) (*HookPostRec return res, "" } +// HookProcReceive proc-receive hook +func HookProcReceive(ownerName, repoName string, opts HookOptions) (*HockProcReceiveResult, error) { + reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/proc-receive/%s/%s", + url.PathEscape(ownerName), + url.PathEscape(repoName), + ) + + req := newInternalRequest(reqURL, "POST") + req = req.Header("Content-Type", "application/json") + req.SetTimeout(60*time.Second, time.Duration(60+len(opts.OldCommitIDs))*time.Second) + jsonBytes, _ := json.Marshal(opts) + req.Body(jsonBytes) + resp, err := req.Response() + if err != nil { + return nil, fmt.Errorf("Unable to contact gitea: %v", err.Error()) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, errors.New(decodeJSONError(resp).Err) + } + res := &HockProcReceiveResult{} + _ = json.NewDecoder(resp.Body).Decode(res) + + return res, nil +} + // SetDefaultBranch will set the default branch to the provided branch for the provided repository func SetDefaultBranch(ownerName, repoName, branch string) error { reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/set-default-branch/%s/%s/%s", diff --git a/modules/repository/hooks.go b/modules/repository/hooks.go index aba5db6719b2..608a83d8a873 100644 --- a/modules/repository/hooks.go +++ b/modules/repository/hooks.go @@ -12,6 +12,7 @@ import ( "path/filepath" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" @@ -31,6 +32,14 @@ func getHookTemplates() (hookNames, hookTpls, giteaHookTpls []string) { fmt.Sprintf("#!/usr/bin/env %s\n%s hook --config=%s update $1 $2 $3\n", setting.ScriptType, util.ShellEscape(setting.AppPath), util.ShellEscape(setting.CustomConf)), fmt.Sprintf("#!/usr/bin/env %s\n%s hook --config=%s post-receive\n", setting.ScriptType, util.ShellEscape(setting.AppPath), util.ShellEscape(setting.CustomConf)), } + + if git.SupportProcReceive { + hookNames = append(hookNames, "proc-receive") + hookTpls = append(hookTpls, + fmt.Sprintf("#!/usr/bin/env %s\n%s hook --config=%s proc-receive\n", setting.ScriptType, util.ShellEscape(setting.AppPath), util.ShellEscape(setting.CustomConf))) + giteaHookTpls = append(giteaHookTpls, "") + } + return } diff --git a/routers/private/hook.go b/routers/private/hook.go index 853d3069ec91..7ec94d4fa2dd 100644 --- a/routers/private/hook.go +++ b/routers/private/hook.go @@ -18,6 +18,7 @@ import ( gitea_context "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/notification" "code.gitea.io/gitea/modules/private" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" @@ -155,6 +156,42 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) { private.GitQuarantinePath+"="+opts.GitQuarantinePath) } + if git.SupportProcReceive { + // if need check write permission + for _, refFullName := range opts.RefFullNames { + if !strings.HasPrefix(refFullName, git.PullRequestPrefix) { + pusher, err := models.GetUserByID(opts.UserID) + if err != nil { + log.Error("models.GetUserByID:%v", err) + ctx.Error(http.StatusInternalServerError, "") + return + } + + perm, err := models.GetUserRepoPermission(repo, pusher) + if err != nil { + log.Error("models.GetUserRepoPermission:%v", err) + ctx.Error(http.StatusInternalServerError, "") + return + } + + if !perm.CanWrite(models.UnitTypeCode) { + ctx.JSON(http.StatusForbidden, map[string]interface{}{ + "err": "User permission denied.", + }) + return + } + + break + } else if opts.IsWiki { + // TODO: maybe can do it ... + ctx.JSON(http.StatusForbidden, map[string]interface{}{ + "err": "not support send pull request to wiki.", + }) + return + } + } + } + // Iterate across the provided old commit IDs for i := range opts.OldCommitIDs { oldCommitID := opts.OldCommitIDs[i] @@ -541,6 +578,216 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) { }) } +// HookProcReceive proc-receive hook +func HookProcReceive(ctx *gitea_context.PrivateContext) { + opts := web.GetForm(ctx).(*private.HookOptions) + if !git.SupportProcReceive { + ctx.Status(404) + return + } + + ownerName := ctx.Params(":owner") + repoName := ctx.Params(":repo") + repo, err := models.GetRepositoryByOwnerAndName(ownerName, repoName) + if err != nil { + log.Error("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "Err": fmt.Sprintf("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err), + }) + return + } + if repo.OwnerName == "" { + repo.OwnerName = ownerName + } + + gitRepo, err := git.OpenRepository(repo.RepoPath()) + if err != nil { + log.Error("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "Err": fmt.Sprintf("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err), + }) + return + } + defer gitRepo.Close() + + results := make([]private.HockProcReceiveRefResult, 0, len(opts.OldCommitIDs)) + // TODO: Add more options? + var ( + topicBranch string + title string + description string + ) + + for i := range opts.OldCommitIDs { + if opts.NewCommitIDs[i] == git.EmptySHA { + ctx.JSON(http.StatusForbidden, map[string]interface{}{ + "err": "Can't delete not exist branch", + }) + return + } + + baseBranchName := opts.RefFullNames[i][len(git.PullRequestPrefix):] + if !gitRepo.IsBranchExist(baseBranchName) { + ctx.JSON(http.StatusNotFound, map[string]interface{}{ + "Err": fmt.Sprintf("target branch %s is not exist in %s/%s", + baseBranchName, ownerName, repoName), + }) + return + } + + if len(topicBranch) == 0 { + has := false + topicBranch, has = opts.GitPushOptions["topic"] + if !has || len(topicBranch) == 0 { + ctx.JSON(http.StatusForbidden, map[string]interface{}{ + "err": "push option 'topic' is requested", + }) + return + } + } + + pr, err := models.GetUnmergedAGitStylePullRequest(repo.ID, baseBranchName, opts.UserName, topicBranch) + if err != nil { + if !models.IsErrPullRequestNotExist(err) { + log.Error("Failed to get unmerged agit style pull request in repository: %s/%s Error: %v", ownerName, repoName, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "Err": fmt.Sprintf("Failed to get unmerged agit style pull request in repository: %s/%s Error: %v", ownerName, repoName, err), + }) + return + } + + // create a new pull request + if len(title) == 0 { + has := false + title, has = opts.GitPushOptions["title"] + if !has || len(title) == 0 { + commit, err := gitRepo.GetCommit(opts.NewCommitIDs[i]) + if err != nil { + log.Error("Failed to get commit %s in repository: %s/%s Error: %v", opts.NewCommitIDs[i], ownerName, repoName, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "Err": fmt.Sprintf("Failed to get commit %s in repository: %s/%s Error: %v", opts.NewCommitIDs[i], ownerName, repoName, err), + }) + return + } + title = strings.Split(commit.CommitMessage, "\n")[0] + } + description = opts.GitPushOptions["description"] + } + + pusher, err := models.GetUserByID(opts.UserID) + if err != nil { + log.Error("Failed to get user. Error: %v", err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "Err": fmt.Sprintf("Failed to get user. Error: %v", err), + }) + return + } + + prIssue := &models.Issue{ + RepoID: repo.ID, + Title: title, + PosterID: pusher.ID, + Poster: pusher, + IsPull: true, + Content: description, + } + + headBranch := "" + userName := strings.ToLower(opts.UserName) + if !strings.HasPrefix(topicBranch, userName+"/") { + headBranch = userName + "/" + topicBranch + } else { + headBranch = topicBranch + } + + pr := &models.PullRequest{ + HeadRepoID: repo.ID, + BaseRepoID: repo.ID, + HeadBranch: opts.NewCommitIDs[i], + BaseBranch: baseBranchName, + TopicBranch: headBranch, + HeadRepo: repo, + BaseRepo: repo, + MergeBase: "", + Type: models.PullRequestGitea, + Style: models.PullRequestStyleAGit, + } + + if err := pull_service.NewPullRequest(repo, prIssue, []int64{}, []string{}, pr, []int64{}); err != nil { + if models.IsErrUserDoesNotHaveAccessToRepo(err) { + ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error()) + return + } + ctx.Error(http.StatusInternalServerError, "NewPullRequest", err.Error()) + return + } + + log.Trace("Pull request created: %d/%d", repo.ID, prIssue.ID) + + results = append(results, private.HockProcReceiveRefResult{ + Ref: pr.GetGitRefName(), + OrignRef: opts.RefFullNames[i], + OldOID: git.EmptySHA, + NewOID: opts.NewCommitIDs[i], + }) + continue + } + + // update exist pull request + old := pr.HeadBranch + pr.HeadBranch = opts.NewCommitIDs[i] + if err = pull_service.UpdateRef(pr); err != nil { + log.Error("Failed to update pull ref. Error: %v", err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "Err": fmt.Sprintf("Failed to update pull ref. Error: %v", err), + }) + return + } + + err = pr.UpdateCols("head_branch") + if err != nil { + log.Error("Failed to update head commit. Error: %v", err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "Err": fmt.Sprintf("Failed to update head commit. Error: %v", err), + }) + return + } + pull_service.AddToTaskQueue(pr) + pusher, err := models.GetUserByID(opts.UserID) + if err != nil { + log.Error("Failed to get user. Error: %v", err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "Err": fmt.Sprintf("Failed to get user. Error: %v", err), + }) + return + } + err = pr.LoadIssue() + if err != nil { + log.Error("Failed to load pull issue. Error: %v", err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "Err": fmt.Sprintf("Failed to load pull issue. Error: %v", err), + }) + return + } + comment, err := models.CreatePushPullComment(pusher, pr, old, opts.NewCommitIDs[i]) + if err == nil && comment != nil { + notification.NotifyPullRequestPushCommits(pusher, pr, comment) + } + notification.NotifyPullRequestSynchronized(pusher, pr) + + results = append(results, private.HockProcReceiveRefResult{ + OldOID: old, + NewOID: opts.NewCommitIDs[i], + Ref: pr.GetGitRefName(), + OrignRef: opts.RefFullNames[i], + }) + } + + ctx.JSON(http.StatusOK, private.HockProcReceiveResult{ + Results: results, + }) +} + // SetDefaultBranch updates the default branch func SetDefaultBranch(ctx *gitea_context.PrivateContext) { ownerName := ctx.Params(":owner") diff --git a/routers/private/internal.go b/routers/private/internal.go index e541591a3840..c1481abb8aaa 100644 --- a/routers/private/internal.go +++ b/routers/private/internal.go @@ -57,6 +57,7 @@ func Routes() *web.Route { r.Post("/ssh/{id}/update/{repoid}", UpdatePublicKeyInRepo) r.Post("/hook/pre-receive/{owner}/{repo}", bind(private.HookOptions{}), HookPreReceive) r.Post("/hook/post-receive/{owner}/{repo}", bind(private.HookOptions{}), HookPostReceive) + r.Post("/hook/proc-receive/{owner}/{repo}", bind(private.HookOptions{}), HookProcReceive) r.Post("/hook/set-default-branch/{owner}/{repo}/{branch}", SetDefaultBranch) r.Get("/serv/none/{keyid}", ServNoCommand) r.Get("/serv/command/{keyid}/{owner}/{repo}", ServCommand) diff --git a/routers/repo/http.go b/routers/repo/http.go index 0377979e8bb5..7ae896b35020 100644 --- a/routers/repo/http.go +++ b/routers/repo/http.go @@ -277,6 +277,11 @@ func httpBase(ctx *context.Context) (h *serviceHandler) { return } + // Because of special ref "refs/for" .. , need delay write permission check + if git.SupportProcReceive { + accessMode = models.AccessModeRead + } + if !perm.CanAccess(accessMode, unitType) { ctx.HandleText(http.StatusForbidden, "User permission denied") return diff --git a/routers/repo/pull.go b/routers/repo/pull.go index 69860021025b..ef2146ee1387 100644 --- a/routers/repo/pull.go +++ b/routers/repo/pull.go @@ -424,13 +424,21 @@ func PrepareViewPullInfo(ctx *context.Context, issue *models.Issue) *git.Compare } defer headGitRepo.Close() - headBranchExist = headGitRepo.IsBranchExist(pull.HeadBranch) + if pull.Style == models.PullRequestStyleGithub { + headBranchExist = headGitRepo.IsBranchExist(pull.HeadBranch) + } else { + headBranchExist = headGitRepo.IsCommitExist(pull.HeadBranch) + } if headBranchExist { - headBranchSha, err = headGitRepo.GetBranchCommitID(pull.HeadBranch) - if err != nil { - ctx.ServerError("GetBranchCommitID", err) - return nil + if pull.Style != models.PullRequestStyleGithub { + headBranchSha = pull.HeadBranch + } else { + headBranchSha, err = headGitRepo.GetBranchCommitID(pull.HeadBranch) + if err != nil { + ctx.ServerError("GetBranchCommitID", err) + return nil + } } } } diff --git a/routers/routes/web.go b/routers/routes/web.go index 1f860a623949..256b290afa09 100644 --- a/routers/routes/web.go +++ b/routers/routes/web.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" auth "code.gitea.io/gitea/modules/forms" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/httpcache" "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/log" @@ -194,6 +195,21 @@ func WebRoutes() *web.Route { }) } + r.Get("/ssh_info", func(rw http.ResponseWriter, req *http.Request) { + if !git.SupportProcReceive { + rw.WriteHeader(404) + return + } + rw.Header().Set("content-type", "text/json;charset=UTF-8") + _, err := rw.Write([]byte(`{"type":"gitea","version":1}`)) + if err != nil { + log.Error("fail to write result: err: %v", err) + rw.WriteHeader(500) + return + } + rw.WriteHeader(200) + }) + r.Get("/apple-touch-icon.png", func(w http.ResponseWriter, req *http.Request) { http.Redirect(w, req, path.Join(setting.StaticURLPrefix, "img/apple-touch-icon.png"), 301) }) diff --git a/services/pull/commit_status.go b/services/pull/commit_status.go index b8fb109440be..cfd3fbad6765 100644 --- a/services/pull/commit_status.go +++ b/services/pull/commit_status.go @@ -108,13 +108,19 @@ func GetPullRequestCommitStatusState(pr *models.PullRequest) (structs.CommitStat } defer headGitRepo.Close() - if !headGitRepo.IsBranchExist(pr.HeadBranch) { + if pr.Style == models.PullRequestStyleGithub && !headGitRepo.IsBranchExist(pr.HeadBranch) { + return "", errors.New("Head branch does not exist, can not merge") + } + if pr.Style != models.PullRequestStyleGithub && !headGitRepo.IsCommitExist(pr.HeadBranch) { return "", errors.New("Head branch does not exist, can not merge") } - sha, err := headGitRepo.GetBranchCommitID(pr.HeadBranch) - if err != nil { - return "", errors.Wrap(err, "GetBranchCommitID") + sha := pr.HeadBranch + if pr.Style == models.PullRequestStyleGithub { + sha, err = headGitRepo.GetBranchCommitID(pr.HeadBranch) + if err != nil { + return "", errors.Wrap(err, "GetBranchCommitID") + } } if err := pr.LoadBaseRepo(); err != nil { diff --git a/services/pull/pull.go b/services/pull/pull.go index 4f742f5a1a6a..b4a0c6cb802d 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -49,7 +49,12 @@ func NewPullRequest(repo *models.Repository, pull *models.Issue, labelIDs []int6 pr.Issue = pull pull.PullRequest = pr - if err := PushToBaseRepo(pr); err != nil { + if pr.Style == models.PullRequestStyleGithub { + err = PushToBaseRepo(pr) + } else { + UpdateRef(pr) + } + if err != nil { return err } @@ -281,8 +286,12 @@ func AddTestPullRequestTask(doer *models.User, repoID int64, branch string, isSy for _, pr := range prs { log.Trace("Updating PR[%d]: composing new test task", pr.ID) - if err := PushToBaseRepo(pr); err != nil { - log.Error("PushToBaseRepo: %v", err) + if pr.Style == models.PullRequestStyleGithub { + if err := PushToBaseRepo(pr); err != nil { + log.Error("PushToBaseRepo: %v", err) + continue + } + } else { continue } @@ -435,6 +444,22 @@ func PushToBaseRepo(pr *models.PullRequest) (err error) { return nil } +// UpdateRef update refs/pull/id/head directly for agit style pull request +func UpdateRef(pr *models.PullRequest) (err error) { + log.Trace("UpdateRef[%d]: upgate pull request ref in base repo '%s'", pr.ID, pr.GetGitRefName()) + if err := pr.LoadBaseRepo(); err != nil { + log.Error("Unable to load base repository for PR[%d] Error: %v", pr.ID, err) + return err + } + + _, err = git.NewCommand("update-ref", pr.GetGitRefName(), pr.HeadBranch).RunInDir(pr.BaseRepo.RepoPath()) + if err != nil { + log.Error("Unable to update ref in base repository for PR[%d] Error: %v", pr.ID, err) + } + + return err +} + type errlist []error func (errs errlist) Error() string { @@ -539,7 +564,12 @@ func GetSquashMergeCommitMessages(pr *models.PullRequest) string { } defer gitRepo.Close() - headCommit, err := gitRepo.GetBranchCommit(pr.HeadBranch) + var headCommit *git.Commit + if pr.Style == models.PullRequestStyleGithub { + headCommit, err = gitRepo.GetBranchCommit(pr.HeadBranch) + } else { + headCommit, err = gitRepo.GetCommit(pr.HeadBranch) + } if err != nil { log.Error("Unable to get head commit: %s Error: %v", pr.HeadBranch, err) return "" @@ -692,7 +722,12 @@ func IsHeadEqualWithBranch(pr *models.PullRequest, branchName string) (bool, err } defer headGitRepo.Close() - headCommit, err := headGitRepo.GetBranchCommit(pr.HeadBranch) + var headCommit *git.Commit + if pr.Style == models.PullRequestStyleGithub { + headCommit, err = headGitRepo.GetBranchCommit(pr.HeadBranch) + } else { + headCommit, err = headGitRepo.GetCommit(pr.HeadBranch) + } if err != nil { return false, err } diff --git a/services/pull/temp_repo.go b/services/pull/temp_repo.go index 45cd10b65bbe..24d96689feed 100644 --- a/services/pull/temp_repo.go +++ b/services/pull/temp_repo.go @@ -140,12 +140,17 @@ func createTemporaryRepo(pr *models.PullRequest) (string, error) { trackingBranch := "tracking" // Fetch head branch - if err := git.NewCommand("fetch", "--no-tags", remoteRepoName, git.BranchPrefix+pr.HeadBranch+":"+trackingBranch).RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil { - log.Error("Unable to fetch head_repo head branch [%s:%s -> tracking in %s]: %v:\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, tmpBasePath, err, outbuf.String(), errbuf.String()) + // Note: for agit style pull request, the head branch is an commit id + headbanch := pr.HeadBranch + if pr.Style == models.PullRequestStyleGithub { + headbanch = git.BranchPrefix + headbanch + } + if err := git.NewCommand("fetch", "--no-tags", remoteRepoName, headbanch+":"+trackingBranch).RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil { + log.Error("Unable to fetch head_repo head branch [%s:%s -> tracking in %s]: %v:\n%s\n%s", pr.HeadRepo.FullName(), headbanch, tmpBasePath, err, outbuf.String(), errbuf.String()) if err := models.RemoveTemporaryPath(tmpBasePath); err != nil { log.Error("CreateTempRepo: RemoveTemporaryPath: %s", err) } - return "", fmt.Errorf("Unable to fetch head_repo head branch [%s:%s -> tracking in tmpBasePath]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, err, outbuf.String(), errbuf.String()) + return "", fmt.Errorf("Unable to fetch head_repo head branch [%s:%s -> tracking in tmpBasePath]: %v\n%s\n%s", pr.HeadRepo.FullName(), headbanch, err, outbuf.String(), errbuf.String()) } outbuf.Reset() errbuf.Reset() diff --git a/services/pull/update.go b/services/pull/update.go index 0829b1c5d6b4..6daf96fe948d 100644 --- a/services/pull/update.go +++ b/services/pull/update.go @@ -48,6 +48,13 @@ func Update(pull *models.PullRequest, doer *models.User, message string) error { // IsUserAllowedToUpdate check if user is allowed to update PR with given permissions and branch protections func IsUserAllowedToUpdate(pull *models.PullRequest, user *models.User) (bool, error) { + if pull.Style != models.PullRequestStyleGithub { + if err := pull.LoadIssue(); err != nil { + return false, err + } + return user != nil && user.ID == pull.Issue.PosterID, nil + } + headRepoPerm, err := models.GetUserRepoPermission(pull.HeadRepo, user) if err != nil { return false, err From a757ed521b6f180193429261f0896fea66267305 Mon Sep 17 00:00:00 2001 From: a1012112796 <1012112796@qq.com> Date: Sun, 7 Feb 2021 16:32:30 +0800 Subject: [PATCH 02/32] fix lint --- services/pull/pull.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/pull/pull.go b/services/pull/pull.go index b4a0c6cb802d..fed766291c93 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -52,7 +52,7 @@ func NewPullRequest(repo *models.Repository, pull *models.Issue, labelIDs []int6 if pr.Style == models.PullRequestStyleGithub { err = PushToBaseRepo(pr) } else { - UpdateRef(pr) + err = UpdateRef(pr) } if err != nil { return err From f96e9e72eac2fdaad84ca68b07e0b210303e6cd0 Mon Sep 17 00:00:00 2001 From: a1012112796 <1012112796@qq.com> Date: Mon, 8 Feb 2021 23:19:15 +0800 Subject: [PATCH 03/32] simplify code add fix some nits --- cmd/hook.go | 6 ---- models/migrations/v169.go | 3 +- models/pull.go | 30 ++-------------- models/pull_test.go | 4 +-- modules/git/repo_branch.go | 13 +++++++ routers/api/v1/repo/pull.go | 2 +- routers/private/hook.go | 64 +++++++++++++++++++--------------- routers/repo/compare.go | 2 +- routers/repo/issue.go | 3 +- routers/repo/pull.go | 12 +++---- services/pull/commit_status.go | 12 ++++--- services/pull/pull.go | 11 ++++-- services/pull/temp_repo.go | 15 ++++---- services/pull/update.go | 12 ++++--- 14 files changed, 96 insertions(+), 93 deletions(-) diff --git a/cmd/hook.go b/cmd/hook.go index 8312c1a291b8..dd345581460b 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -487,12 +487,6 @@ Gitea or set your environment appropriately.`, "") } } - lf, err := os.OpenFile("test.log", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0777) - if err != nil { - fail("Internal Server Error", "open log file failed: %v", err) - } - defer lf.Close() - if git.CheckGitVersionAtLeast("2.29") != nil { fail("Internal Server Error", "git not support proc-receive.") } diff --git a/models/migrations/v169.go b/models/migrations/v169.go index 8b08001a6a2b..097de705a471 100644 --- a/models/migrations/v169.go +++ b/models/migrations/v169.go @@ -14,8 +14,7 @@ func addAgitStylePullRequest(x *xorm.Engine) error { type PullRequestStyle int type PullRequest struct { - TopicBranch string - Style PullRequestStyle + Style PullRequestStyle } if err := x.Sync2(new(PullRequest)); err != nil { diff --git a/models/pull.go b/models/pull.go index 923c4989e82a..928416b09212 100644 --- a/models/pull.go +++ b/models/pull.go @@ -68,8 +68,8 @@ type PullRequest struct { BaseRepoID int64 `xorm:"INDEX"` BaseRepo *Repository `xorm:"-"` HeadBranch string + HeadCommitID string `xorm:"-"` BaseBranch string - TopicBranch string // use for agit style pr ProtectedBranch *ProtectedBranch `xorm:"-"` MergeBase string `xorm:"VARCHAR(40)"` @@ -479,11 +479,11 @@ func newPullRequestAttempt(repo *Repository, pull *Issue, labelIDs []int64, uuid // GetUnmergedPullRequest returns a pull request that is open and has not been merged // by given head/base and repo/branch. -func GetUnmergedPullRequest(headRepoID, baseRepoID int64, headBranch, baseBranch string) (*PullRequest, error) { +func GetUnmergedPullRequest(headRepoID, baseRepoID int64, headBranch, baseBranch string, style PullRequestStyle) (*PullRequest, error) { pr := new(PullRequest) has, err := x. Where("head_repo_id=? AND head_branch=? AND base_repo_id=? AND base_branch=? AND has_merged=? AND style = ? AND issue.is_closed=?", - headRepoID, headBranch, baseRepoID, baseBranch, false, PullRequestStyleGithub, false). + headRepoID, headBranch, baseRepoID, baseBranch, false, style, false). Join("INNER", "issue", "issue.id=pull_request.issue_id"). Get(pr) if err != nil { @@ -495,30 +495,6 @@ func GetUnmergedPullRequest(headRepoID, baseRepoID int64, headBranch, baseBranch return pr, nil } -// GetUnmergedAGitStylePullRequest get unmerged agit style pull request -func GetUnmergedAGitStylePullRequest(repoID int64, baseBranch, userName, topicBranch string) (*PullRequest, error) { - pr := new(PullRequest) - - headBranch := topicBranch - userName = strings.ToLower(userName) - if !strings.HasPrefix(topicBranch, userName+"/") { - headBranch = userName + "/" + topicBranch - } - - has, err := x. - Where("head_repo_id=? AND topic_branch=? AND base_repo_id=? AND base_branch=? AND has_merged=? AND style = ? AND issue.is_closed=?", - repoID, headBranch, repoID, baseBranch, false, PullRequestStyleAGit, false). - Join("INNER", "issue", "issue.id=pull_request.issue_id"). - Get(pr) - if err != nil { - return nil, err - } else if !has { - return nil, ErrPullRequestNotExist{0, 0, repoID, repoID, headBranch, baseBranch} - } - - return pr, nil -} - // GetLatestPullRequestByHeadInfo returns the latest pull request (regardless of its status) // by given head information (repo and branch). func GetLatestPullRequestByHeadInfo(repoID int64, branch string) (*PullRequest, error) { diff --git a/models/pull_test.go b/models/pull_test.go index 3cc6abfec7e2..3479c97fc66b 100644 --- a/models/pull_test.go +++ b/models/pull_test.go @@ -92,11 +92,11 @@ func TestPullRequestsOldest(t *testing.T) { func TestGetUnmergedPullRequest(t *testing.T) { assert.NoError(t, PrepareTestDatabase()) - pr, err := GetUnmergedPullRequest(1, 1, "branch2", "master") + pr, err := GetUnmergedPullRequest(1, 1, "branch2", "master", PullRequestStyleGithub) assert.NoError(t, err) assert.Equal(t, int64(2), pr.ID) - _, err = GetUnmergedPullRequest(1, 9223372036854775807, "branch1", "master") + _, err = GetUnmergedPullRequest(1, 9223372036854775807, "branch1", "master", PullRequestStyleGithub) assert.Error(t, err) assert.True(t, IsErrPullRequestNotExist(err)) } diff --git a/modules/git/repo_branch.go b/modules/git/repo_branch.go index 45795dc80f53..61d7ab756549 100644 --- a/modules/git/repo_branch.go +++ b/modules/git/repo_branch.go @@ -40,6 +40,19 @@ type Branch struct { gitRepo *Repository } +// GetRefCommitID returns the last commit ID string of given reference (branch or tag). +func GetRefCommitID(repoPath, name string) (string, error) { + stdout, err := NewCommand("show-ref", "--verify", "--hash", name).RunInDir(repoPath) + if err != nil { + if strings.Contains(err.Error(), "not a valid ref") { + return "", ErrNotExist{name, ""} + } + return "", err + } + + return strings.TrimSpace(stdout), nil +} + // GetHEADBranch returns corresponding branch of HEAD. func (repo *Repository) GetHEADBranch() (*Branch, error) { if repo == nil { diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 38dac3655335..1206e73315e5 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -307,7 +307,7 @@ func CreatePullRequest(ctx *context.APIContext) { defer headGitRepo.Close() // Check if another PR exists with the same targets - existingPr, err := models.GetUnmergedPullRequest(headRepo.ID, ctx.Repo.Repository.ID, headBranch, baseBranch) + existingPr, err := models.GetUnmergedPullRequest(headRepo.ID, ctx.Repo.Repository.ID, headBranch, baseBranch, models.PullRequestStyleGithub) if err != nil { if !models.IsErrPullRequestNotExist(err) { ctx.Error(http.StatusInternalServerError, "GetUnmergedPullRequest", err) diff --git a/routers/private/hook.go b/routers/private/hook.go index 7ec94d4fa2dd..1f789a30ca06 100644 --- a/routers/private/hook.go +++ b/routers/private/hook.go @@ -541,7 +541,7 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) { continue } - pr, err := models.GetUnmergedPullRequest(repo.ID, baseRepo.ID, branch, baseRepo.DefaultBranch) + pr, err := models.GetUnmergedPullRequest(repo.ID, baseRepo.ID, branch, baseRepo.DefaultBranch, models.PullRequestStyleGithub) if err != nil && !models.IsErrPullRequestNotExist(err) { log.Error("Failed to get active PR in: %-v Branch: %s to: %-v Branch: %s Error: %v", repo, branch, baseRepo, baseRepo.DefaultBranch, err) ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{ @@ -646,7 +646,15 @@ func HookProcReceive(ctx *gitea_context.PrivateContext) { } } - pr, err := models.GetUnmergedAGitStylePullRequest(repo.ID, baseBranchName, opts.UserName, topicBranch) + headBranch := "" + userName := strings.ToLower(opts.UserName) + if !strings.HasPrefix(topicBranch, userName+"/") { + headBranch = userName + "/" + topicBranch + } else { + headBranch = topicBranch + } + + pr, err := models.GetUnmergedPullRequest(repo.ID, repo.ID, headBranch, baseBranchName, models.PullRequestStyleAGit) if err != nil { if !models.IsErrPullRequestNotExist(err) { log.Error("Failed to get unmerged agit style pull request in repository: %s/%s Error: %v", ownerName, repoName, err) @@ -692,25 +700,17 @@ func HookProcReceive(ctx *gitea_context.PrivateContext) { Content: description, } - headBranch := "" - userName := strings.ToLower(opts.UserName) - if !strings.HasPrefix(topicBranch, userName+"/") { - headBranch = userName + "/" + topicBranch - } else { - headBranch = topicBranch - } - pr := &models.PullRequest{ - HeadRepoID: repo.ID, - BaseRepoID: repo.ID, - HeadBranch: opts.NewCommitIDs[i], - BaseBranch: baseBranchName, - TopicBranch: headBranch, - HeadRepo: repo, - BaseRepo: repo, - MergeBase: "", - Type: models.PullRequestGitea, - Style: models.PullRequestStyleAGit, + HeadRepoID: repo.ID, + BaseRepoID: repo.ID, + HeadBranch: headBranch, + HeadCommitID: opts.NewCommitIDs[i], + BaseBranch: baseBranchName, + HeadRepo: repo, + BaseRepo: repo, + MergeBase: "", + Type: models.PullRequestGitea, + Style: models.PullRequestStyleAGit, } if err := pull_service.NewPullRequest(repo, prIssue, []int64{}, []string{}, pr, []int64{}); err != nil { @@ -734,24 +734,32 @@ func HookProcReceive(ctx *gitea_context.PrivateContext) { } // update exist pull request - old := pr.HeadBranch - pr.HeadBranch = opts.NewCommitIDs[i] - if err = pull_service.UpdateRef(pr); err != nil { - log.Error("Failed to update pull ref. Error: %v", err) + if err := pr.LoadBaseRepo(); err != nil { + log.Error("Unable to load base repository for PR[%d] Error: %v", pr.ID, err) ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "Err": fmt.Sprintf("Failed to update pull ref. Error: %v", err), + "Err": fmt.Sprintf("Unable to load base repository for PR[%d] Error: %v", pr.ID, err), }) return } - err = pr.UpdateCols("head_branch") + old, err := git.GetRefCommitID(pr.BaseRepo.RepoPath(), pr.GetGitRefName()) if err != nil { - log.Error("Failed to update head commit. Error: %v", err) + log.Error("Unable to get ref commit id in base repository for PR[%d] Error: %v", pr.ID, err) ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "Err": fmt.Sprintf("Failed to update head commit. Error: %v", err), + "Err": fmt.Sprintf("Unable to get ref commit id in base repository for PR[%d] Error: %v", pr.ID, err), }) return } + + pr.HeadCommitID = opts.NewCommitIDs[i] + if err = pull_service.UpdateRef(pr); err != nil { + log.Error("Failed to update pull ref. Error: %v", err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "Err": fmt.Sprintf("Failed to update pull ref. Error: %v", err), + }) + return + } + pull_service.AddToTaskQueue(pr) pusher, err := models.GetUserByID(opts.UserID) if err != nil { diff --git a/routers/repo/compare.go b/routers/repo/compare.go index 218f71246953..d0a5866851ad 100644 --- a/routers/repo/compare.go +++ b/routers/repo/compare.go @@ -548,7 +548,7 @@ func CompareDiff(ctx *context.Context) { } ctx.Data["HeadBranches"] = headBranches - pr, err := models.GetUnmergedPullRequest(headRepo.ID, ctx.Repo.Repository.ID, headBranch, baseBranch) + pr, err := models.GetUnmergedPullRequest(headRepo.ID, ctx.Repo.Repository.ID, headBranch, baseBranch, models.PullRequestStyleGithub) if err != nil { if !models.IsErrPullRequestNotExist(err) { ctx.ServerError("GetUnmergedPullRequest", err) diff --git a/routers/repo/issue.go b/routers/repo/issue.go index 3bc20839aa44..7cb362136251 100644 --- a/routers/repo/issue.go +++ b/routers/repo/issue.go @@ -1987,7 +1987,7 @@ func NewComment(ctx *context.Context) { if form.Status == "reopen" && issue.IsPull { pull := issue.PullRequest var err error - pr, err = models.GetUnmergedPullRequest(pull.HeadRepoID, pull.BaseRepoID, pull.HeadBranch, pull.BaseBranch) + pr, err = models.GetUnmergedPullRequest(pull.HeadRepoID, pull.BaseRepoID, pull.HeadBranch, pull.BaseBranch, pull.Style) if err != nil { if !models.IsErrPullRequestNotExist(err) { ctx.ServerError("GetUnmergedPullRequest", err) @@ -1997,6 +1997,7 @@ func NewComment(ctx *context.Context) { // Regenerate patch and test conflict. if pr == nil { + issue.PullRequest.HeadCommitID = "" pull_service.AddToTaskQueue(issue.PullRequest) } } diff --git a/routers/repo/pull.go b/routers/repo/pull.go index ef2146ee1387..10e766eeda0f 100644 --- a/routers/repo/pull.go +++ b/routers/repo/pull.go @@ -427,18 +427,18 @@ func PrepareViewPullInfo(ctx *context.Context, issue *models.Issue) *git.Compare if pull.Style == models.PullRequestStyleGithub { headBranchExist = headGitRepo.IsBranchExist(pull.HeadBranch) } else { - headBranchExist = headGitRepo.IsCommitExist(pull.HeadBranch) + headBranchExist = git.IsReferenceExist(baseGitRepo.Path, pull.GetGitRefName()) } if headBranchExist { if pull.Style != models.PullRequestStyleGithub { - headBranchSha = pull.HeadBranch + headBranchSha, err = baseGitRepo.GetRefCommitID(pull.GetGitRefName()) } else { headBranchSha, err = headGitRepo.GetBranchCommitID(pull.HeadBranch) - if err != nil { - ctx.ServerError("GetBranchCommitID", err) - return nil - } + } + if err != nil { + ctx.ServerError("GetBranchCommitID", err) + return nil } } } diff --git a/services/pull/commit_status.go b/services/pull/commit_status.go index cfd3fbad6765..569693e9366d 100644 --- a/services/pull/commit_status.go +++ b/services/pull/commit_status.go @@ -111,16 +111,18 @@ func GetPullRequestCommitStatusState(pr *models.PullRequest) (structs.CommitStat if pr.Style == models.PullRequestStyleGithub && !headGitRepo.IsBranchExist(pr.HeadBranch) { return "", errors.New("Head branch does not exist, can not merge") } - if pr.Style != models.PullRequestStyleGithub && !headGitRepo.IsCommitExist(pr.HeadBranch) { + if pr.Style != models.PullRequestStyleGithub && !git.IsReferenceExist(headGitRepo.Path, pr.GetGitRefName()) { return "", errors.New("Head branch does not exist, can not merge") } - sha := pr.HeadBranch + sha := "" if pr.Style == models.PullRequestStyleGithub { sha, err = headGitRepo.GetBranchCommitID(pr.HeadBranch) - if err != nil { - return "", errors.Wrap(err, "GetBranchCommitID") - } + } else { + sha, err = headGitRepo.GetRefCommitID(pr.GetGitRefName()) + } + if err != nil { + return "", err } if err := pr.LoadBaseRepo(); err != nil { diff --git a/services/pull/pull.go b/services/pull/pull.go index fed766291c93..87d149c90968 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -150,7 +150,7 @@ func ChangeTargetBranch(pr *models.PullRequest, doer *models.User, targetBranch } // Check if pull request for the new target branch already exists - existingPr, err := models.GetUnmergedPullRequest(pr.HeadRepoID, pr.BaseRepoID, pr.HeadBranch, targetBranch) + existingPr, err := models.GetUnmergedPullRequest(pr.HeadRepoID, pr.BaseRepoID, pr.HeadBranch, targetBranch, models.PullRequestStyleGithub) if existingPr != nil { return models.ErrPullRequestAlreadyExists{ ID: existingPr.ID, @@ -452,7 +452,7 @@ func UpdateRef(pr *models.PullRequest) (err error) { return err } - _, err = git.NewCommand("update-ref", pr.GetGitRefName(), pr.HeadBranch).RunInDir(pr.BaseRepo.RepoPath()) + _, err = git.NewCommand("update-ref", pr.GetGitRefName(), pr.HeadCommitID).RunInDir(pr.BaseRepo.RepoPath()) if err != nil { log.Error("Unable to update ref in base repository for PR[%d] Error: %v", pr.ID, err) } @@ -568,7 +568,12 @@ func GetSquashMergeCommitMessages(pr *models.PullRequest) string { if pr.Style == models.PullRequestStyleGithub { headCommit, err = gitRepo.GetBranchCommit(pr.HeadBranch) } else { - headCommit, err = gitRepo.GetCommit(pr.HeadBranch) + pr.HeadCommitID, err = git.GetRefCommitID(gitRepo.Path, pr.GetGitRefName()) + if err != nil { + log.Error("Unable to get head commit: %s Error: %v", pr.GetGitRefName(), err) + return "" + } + headCommit, err = gitRepo.GetCommit(pr.HeadCommitID) } if err != nil { log.Error("Unable to get head commit: %s Error: %v", pr.HeadBranch, err) diff --git a/services/pull/temp_repo.go b/services/pull/temp_repo.go index 24d96689feed..f43e23776580 100644 --- a/services/pull/temp_repo.go +++ b/services/pull/temp_repo.go @@ -140,17 +140,20 @@ func createTemporaryRepo(pr *models.PullRequest) (string, error) { trackingBranch := "tracking" // Fetch head branch - // Note: for agit style pull request, the head branch is an commit id - headbanch := pr.HeadBranch + var headBranch string if pr.Style == models.PullRequestStyleGithub { - headbanch = git.BranchPrefix + headbanch + headBranch = git.BranchPrefix + pr.HeadBranch + } else if len(pr.HeadCommitID) == 40 { // for not created pull request + headBranch = pr.HeadCommitID + } else { + headBranch = pr.GetGitRefName() } - if err := git.NewCommand("fetch", "--no-tags", remoteRepoName, headbanch+":"+trackingBranch).RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil { - log.Error("Unable to fetch head_repo head branch [%s:%s -> tracking in %s]: %v:\n%s\n%s", pr.HeadRepo.FullName(), headbanch, tmpBasePath, err, outbuf.String(), errbuf.String()) + if err := git.NewCommand("fetch", "--no-tags", remoteRepoName, headBranch+":"+trackingBranch).RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil { + log.Error("Unable to fetch head_repo head branch [%s:%s -> tracking in %s]: %v:\n%s\n%s", pr.HeadRepo.FullName(), headBranch, tmpBasePath, err, outbuf.String(), errbuf.String()) if err := models.RemoveTemporaryPath(tmpBasePath); err != nil { log.Error("CreateTempRepo: RemoveTemporaryPath: %s", err) } - return "", fmt.Errorf("Unable to fetch head_repo head branch [%s:%s -> tracking in tmpBasePath]: %v\n%s\n%s", pr.HeadRepo.FullName(), headbanch, err, outbuf.String(), errbuf.String()) + return "", fmt.Errorf("Unable to fetch head_repo head branch [%s:%s -> tracking in tmpBasePath]: %v\n%s\n%s", pr.HeadRepo.FullName(), headBranch, err, outbuf.String(), errbuf.String()) } outbuf.Reset() errbuf.Reset() diff --git a/services/pull/update.go b/services/pull/update.go index 6daf96fe948d..16f24731ff06 100644 --- a/services/pull/update.go +++ b/services/pull/update.go @@ -22,6 +22,11 @@ func Update(pull *models.PullRequest, doer *models.User, message string) error { BaseBranch: pull.HeadBranch, } + if pull.Style == models.PullRequestStyleAGit { + // TODO: Not support update agit style pull request's head branch + return fmt.Errorf("Not support update agit style pull request's head branch") + } + if err := pr.LoadHeadRepo(); err != nil { log.Error("LoadHeadRepo: %v", err) return fmt.Errorf("LoadHeadRepo: %v", err) @@ -48,11 +53,8 @@ func Update(pull *models.PullRequest, doer *models.User, message string) error { // IsUserAllowedToUpdate check if user is allowed to update PR with given permissions and branch protections func IsUserAllowedToUpdate(pull *models.PullRequest, user *models.User) (bool, error) { - if pull.Style != models.PullRequestStyleGithub { - if err := pull.LoadIssue(); err != nil { - return false, err - } - return user != nil && user.ID == pull.Issue.PosterID, nil + if pull.Style == models.PullRequestStyleAGit { + return false, nil } headRepoPerm, err := models.GetUserRepoPermission(pull.HeadRepo, user) From 7294bfa8f91ce657b98e5bc8a877f24f843bec0e Mon Sep 17 00:00:00 2001 From: a1012112796 <1012112796@qq.com> Date: Mon, 8 Feb 2021 23:56:10 +0800 Subject: [PATCH 04/32] update merge help message --- templates/repo/issue/view_content/pull.tmpl | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/templates/repo/issue/view_content/pull.tmpl b/templates/repo/issue/view_content/pull.tmpl index 34eaa83eb2d5..87e7080f9702 100644 --- a/templates/repo/issue/view_content/pull.tmpl +++ b/templates/repo/issue/view_content/pull.tmpl @@ -363,13 +363,17 @@ {{end}} -
{{$.i18n.Tr "repo.pulls.merge_instruction_hint" | Safe}}
+
{{$.i18n.Tr "repo.pulls.merge_instruction_hint" | Safe}}