summaryrefslogtreecommitdiffstats
path: root/services/agit/agit.go
diff options
context:
space:
mode:
authora1012112796 <1012112796@qq.com>2021-07-28 17:42:56 +0800
committerGitHub <noreply@github.com>2021-07-28 17:42:56 +0800
commit370516883717de0e6e2087c12d368eb1465ee3b0 (patch)
tree1dd7b0946ac2bbeb3b9f13518cf2df2f4ebca348 /services/agit/agit.go
parent5b2e2d29ca50ba4c11a44d4f1de18ffcf215ba1b (diff)
downloadgitea-370516883717de0e6e2087c12d368eb1465ee3b0.tar.gz
gitea-370516883717de0e6e2087c12d368eb1465ee3b0.zip
Add agit flow support in gitea (#14295)
* 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> * fix lint * simplify code add fix some nits * update merge help message * Apply suggestions from code review. Thanks @jiangxin * add forced-update message * fix lint * splite writePktLine * add refs/for/<target-branch>/<topic-branch> support also * Add test code add fix api * fix lint * fix test * skip test if git version < 2.29 * try test with git 2.30.1 * fix permission check bug * fix some nit * logic implify and test code update * fix bug * apply suggestions from code review * prepare for merge Signed-off-by: Andrew Thornton <art27@cantab.net> * fix permission check bug - test code update - apply suggestions from code review @zeripath Signed-off-by: a1012112796 <1012112796@qq.com> * fix bug when target branch isn't exist * prevent some special push and fix some nits * fix lint * try splite * Apply suggestions from code review - fix permission check - handle user rename * fix version negotiation * remane * fix template * handle empty repo * ui: fix branch link under the title * fix nits Co-authored-by: Andrew Thornton <art27@cantab.net> Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Diffstat (limited to 'services/agit/agit.go')
-rw-r--r--services/agit/agit.go288
1 files changed, 288 insertions, 0 deletions
diff --git a/services/agit/agit.go b/services/agit/agit.go
new file mode 100644
index 0000000000..a89c255fe7
--- /dev/null
+++ b/services/agit/agit.go
@@ -0,0 +1,288 @@
+// 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 agit
+
+import (
+ "fmt"
+ "net/http"
+ "os"
+ "strings"
+
+ "code.gitea.io/gitea/models"
+ "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"
+ pull_service "code.gitea.io/gitea/services/pull"
+)
+
+// ProcRecive handle proc receive work
+func ProcRecive(ctx *context.PrivateContext, opts *private.HookOptions) []private.HockProcReceiveRefResult {
+ // TODO: Add more options?
+ var (
+ topicBranch string
+ title string
+ description string
+ forcePush bool
+ )
+
+ results := make([]private.HockProcReceiveRefResult, 0, len(opts.OldCommitIDs))
+ repo := ctx.Repo.Repository
+ gitRepo := ctx.Repo.GitRepo
+ ownerName := ctx.Repo.Repository.OwnerName
+ repoName := ctx.Repo.Repository.Name
+
+ topicBranch = opts.GitPushOptions["topic"]
+ _, forcePush = opts.GitPushOptions["force-push"]
+
+ for i := range opts.OldCommitIDs {
+ if opts.NewCommitIDs[i] == git.EmptySHA {
+ results = append(results, private.HockProcReceiveRefResult{
+ OriginalRef: opts.RefFullNames[i],
+ OldOID: opts.OldCommitIDs[i],
+ NewOID: opts.NewCommitIDs[i],
+ Err: "Can't delete not exist branch",
+ })
+ continue
+ }
+
+ if !strings.HasPrefix(opts.RefFullNames[i], git.PullRequestPrefix) {
+ results = append(results, private.HockProcReceiveRefResult{
+ IsNotMatched: true,
+ OriginalRef: opts.RefFullNames[i],
+ })
+ continue
+ }
+
+ baseBranchName := opts.RefFullNames[i][len(git.PullRequestPrefix):]
+ curentTopicBranch := ""
+ if !gitRepo.IsBranchExist(baseBranchName) {
+ // try match refs/for/<target-branch>/<topic-branch>
+ for p, v := range baseBranchName {
+ if v == '/' && gitRepo.IsBranchExist(baseBranchName[:p]) && p != len(baseBranchName)-1 {
+ curentTopicBranch = baseBranchName[p+1:]
+ baseBranchName = baseBranchName[:p]
+ break
+ }
+ }
+ }
+
+ if len(topicBranch) == 0 && len(curentTopicBranch) == 0 {
+ results = append(results, private.HockProcReceiveRefResult{
+ OriginalRef: opts.RefFullNames[i],
+ OldOID: opts.OldCommitIDs[i],
+ NewOID: opts.NewCommitIDs[i],
+ Err: "topic-branch is not set",
+ })
+ continue
+ }
+
+ headBranch := ""
+ userName := strings.ToLower(opts.UserName)
+
+ if len(curentTopicBranch) == 0 {
+ curentTopicBranch = topicBranch
+ }
+
+ // because different user maybe want to use same topic,
+ // So it's better to make sure the topic branch name
+ // has user name prefix
+ if !strings.HasPrefix(curentTopicBranch, userName+"/") {
+ headBranch = userName + "/" + curentTopicBranch
+ } else {
+ headBranch = curentTopicBranch
+ }
+
+ pr, err := models.GetUnmergedPullRequest(repo.ID, repo.ID, headBranch, baseBranchName, models.PullRequestFlowAGit)
+ if err != nil {
+ if !models.IsErrPullRequestNotExist(err) {
+ log.Error("Failed to get unmerged agit flow 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 flow pull request in repository: %s/%s Error: %v", ownerName, repoName, err),
+ })
+ return nil
+ }
+
+ // 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 nil
+ }
+ 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 nil
+ }
+
+ prIssue := &models.Issue{
+ RepoID: repo.ID,
+ Title: title,
+ PosterID: pusher.ID,
+ Poster: pusher,
+ IsPull: true,
+ Content: description,
+ }
+
+ pr := &models.PullRequest{
+ HeadRepoID: repo.ID,
+ BaseRepoID: repo.ID,
+ HeadBranch: headBranch,
+ HeadCommitID: opts.NewCommitIDs[i],
+ BaseBranch: baseBranchName,
+ HeadRepo: repo,
+ BaseRepo: repo,
+ MergeBase: "",
+ Type: models.PullRequestGitea,
+ Flow: models.PullRequestFlowAGit,
+ }
+
+ 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 nil
+ }
+ ctx.Error(http.StatusInternalServerError, "NewPullRequest", err.Error())
+ return nil
+ }
+
+ log.Trace("Pull request created: %d/%d", repo.ID, prIssue.ID)
+
+ results = append(results, private.HockProcReceiveRefResult{
+ Ref: pr.GetGitRefName(),
+ OriginalRef: opts.RefFullNames[i],
+ OldOID: git.EmptySHA,
+ NewOID: opts.NewCommitIDs[i],
+ })
+ continue
+ }
+
+ // update exist pull request
+ 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("Unable to load base repository for PR[%d] Error: %v", pr.ID, err),
+ })
+ return nil
+ }
+
+ oldCommitID, err := gitRepo.GetRefCommitID(pr.GetGitRefName())
+ if err != nil {
+ 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("Unable to get ref commit id in base repository for PR[%d] Error: %v", pr.ID, err),
+ })
+ return nil
+ }
+
+ if oldCommitID == opts.NewCommitIDs[i] {
+ results = append(results, private.HockProcReceiveRefResult{
+ 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("rev-list", "--max-count=1", oldCommitID, "^"+opts.NewCommitIDs[i]).RunInDirWithEnv(repo.RepoPath(), os.Environ())
+ if err != nil {
+ log.Error("Unable to detect force push between: %s and %s in %-v Error: %v", oldCommitID, opts.NewCommitIDs[i], repo, err)
+ ctx.JSON(http.StatusInternalServerError, private.Response{
+ Err: fmt.Sprintf("Fail to detect force push: %v", err),
+ })
+ return nil
+ } else if len(output) > 0 {
+ results = append(results, private.HockProcReceiveRefResult{
+ OriginalRef: oldCommitID,
+ 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(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 nil
+ }
+
+ 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 nil
+ }
+ 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 nil
+ }
+ comment, err := models.CreatePushPullComment(pusher, pr, oldCommitID, opts.NewCommitIDs[i])
+ if err == nil && comment != nil {
+ notification.NotifyPullRequestPushCommits(pusher, pr, comment)
+ }
+ notification.NotifyPullRequestSynchronized(pusher, pr)
+ isForcePush := comment != nil && comment.IsForcePush
+
+ results = append(results, private.HockProcReceiveRefResult{
+ OldOID: oldCommitID,
+ NewOID: opts.NewCommitIDs[i],
+ Ref: pr.GetGitRefName(),
+ OriginalRef: opts.RefFullNames[i],
+ IsForcePush: isForcePush,
+ })
+ }
+
+ return results
+}
+
+// UserNameChanged hanle user name change for agit flow pull
+func UserNameChanged(user *models.User, newName string) error {
+ pulls, err := models.GetAllUnmergedAgitPullRequestByPoster(user.ID)
+ if err != nil {
+ return err
+ }
+
+ newName = strings.ToLower(newName)
+
+ for _, pull := range pulls {
+ pull.HeadBranch = strings.TrimPrefix(pull.HeadBranch, user.LowerName+"/")
+ pull.HeadBranch = newName + "/" + pull.HeadBranch
+ if err = pull.UpdateCols("head_branch"); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}