gitea源码

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. // Copyright 2021 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package agit
  4. import (
  5. "context"
  6. "encoding/base64"
  7. "fmt"
  8. "os"
  9. "strings"
  10. git_model "code.gitea.io/gitea/models/git"
  11. issues_model "code.gitea.io/gitea/models/issues"
  12. repo_model "code.gitea.io/gitea/models/repo"
  13. user_model "code.gitea.io/gitea/models/user"
  14. "code.gitea.io/gitea/modules/git"
  15. "code.gitea.io/gitea/modules/git/gitcmd"
  16. "code.gitea.io/gitea/modules/gitrepo"
  17. "code.gitea.io/gitea/modules/log"
  18. "code.gitea.io/gitea/modules/private"
  19. "code.gitea.io/gitea/modules/setting"
  20. "code.gitea.io/gitea/modules/util"
  21. notify_service "code.gitea.io/gitea/services/notify"
  22. pull_service "code.gitea.io/gitea/services/pull"
  23. )
  24. func parseAgitPushOptionValue(s string) string {
  25. if base64Value, ok := strings.CutPrefix(s, "{base64}"); ok {
  26. decoded, err := base64.StdEncoding.DecodeString(base64Value)
  27. return util.Iif(err == nil, string(decoded), s)
  28. }
  29. return s
  30. }
  31. // ProcReceive handle proc receive work
  32. func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, opts *private.HookOptions) ([]private.HookProcReceiveRefResult, error) {
  33. results := make([]private.HookProcReceiveRefResult, 0, len(opts.OldCommitIDs))
  34. forcePush := opts.GitPushOptions.Bool(private.GitPushOptionForcePush)
  35. topicBranch := opts.GitPushOptions["topic"]
  36. // some options are base64-encoded with "{base64}" prefix if they contain new lines
  37. // other agit push options like "issue", "reviewer" and "cc" are not supported
  38. title := parseAgitPushOptionValue(opts.GitPushOptions["title"])
  39. description := parseAgitPushOptionValue(opts.GitPushOptions["description"])
  40. objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
  41. userName := strings.ToLower(opts.UserName)
  42. pusher, err := user_model.GetUserByID(ctx, opts.UserID)
  43. if err != nil {
  44. return nil, fmt.Errorf("failed to get user. Error: %w", err)
  45. }
  46. for i := range opts.OldCommitIDs {
  47. if opts.NewCommitIDs[i] == objectFormat.EmptyObjectID().String() {
  48. results = append(results, private.HookProcReceiveRefResult{
  49. OriginalRef: opts.RefFullNames[i],
  50. OldOID: opts.OldCommitIDs[i],
  51. NewOID: opts.NewCommitIDs[i],
  52. Err: "Can't delete not exist branch",
  53. })
  54. continue
  55. }
  56. if !opts.RefFullNames[i].IsFor() {
  57. results = append(results, private.HookProcReceiveRefResult{
  58. IsNotMatched: true,
  59. OriginalRef: opts.RefFullNames[i],
  60. })
  61. continue
  62. }
  63. baseBranchName := opts.RefFullNames[i].ForBranchName()
  64. currentTopicBranch := ""
  65. if !gitrepo.IsBranchExist(ctx, repo, baseBranchName) {
  66. // try match refs/for/<target-branch>/<topic-branch>
  67. for p, v := range baseBranchName {
  68. if v == '/' && gitrepo.IsBranchExist(ctx, repo, baseBranchName[:p]) && p != len(baseBranchName)-1 {
  69. currentTopicBranch = baseBranchName[p+1:]
  70. baseBranchName = baseBranchName[:p]
  71. break
  72. }
  73. }
  74. }
  75. if len(topicBranch) == 0 && len(currentTopicBranch) == 0 {
  76. results = append(results, private.HookProcReceiveRefResult{
  77. OriginalRef: opts.RefFullNames[i],
  78. OldOID: opts.OldCommitIDs[i],
  79. NewOID: opts.NewCommitIDs[i],
  80. Err: "topic-branch is not set",
  81. })
  82. continue
  83. }
  84. if len(currentTopicBranch) == 0 {
  85. currentTopicBranch = topicBranch
  86. }
  87. // because different user maybe want to use same topic,
  88. // So it's better to make sure the topic branch name
  89. // has username prefix
  90. var headBranch string
  91. if !strings.HasPrefix(currentTopicBranch, userName+"/") {
  92. headBranch = userName + "/" + currentTopicBranch
  93. } else {
  94. headBranch = currentTopicBranch
  95. }
  96. pr, err := issues_model.GetUnmergedPullRequest(ctx, repo.ID, repo.ID, headBranch, baseBranchName, issues_model.PullRequestFlowAGit)
  97. if err != nil {
  98. if !issues_model.IsErrPullRequestNotExist(err) {
  99. return nil, fmt.Errorf("failed to get unmerged agit flow pull request in repository: %s Error: %w", repo.FullName(), err)
  100. }
  101. var commit *git.Commit
  102. if title == "" || description == "" {
  103. commit, err = gitRepo.GetCommit(opts.NewCommitIDs[i])
  104. if err != nil {
  105. return nil, fmt.Errorf("failed to get commit %s in repository: %s Error: %w", opts.NewCommitIDs[i], repo.FullName(), err)
  106. }
  107. }
  108. // create a new pull request
  109. if title == "" {
  110. title = strings.Split(commit.CommitMessage, "\n")[0]
  111. }
  112. if description == "" {
  113. _, description, _ = strings.Cut(commit.CommitMessage, "\n\n")
  114. }
  115. if description == "" {
  116. description = title
  117. }
  118. prIssue := &issues_model.Issue{
  119. RepoID: repo.ID,
  120. Title: title,
  121. PosterID: pusher.ID,
  122. Poster: pusher,
  123. IsPull: true,
  124. Content: description,
  125. }
  126. pr := &issues_model.PullRequest{
  127. HeadRepoID: repo.ID,
  128. BaseRepoID: repo.ID,
  129. HeadBranch: headBranch,
  130. HeadCommitID: opts.NewCommitIDs[i],
  131. BaseBranch: baseBranchName,
  132. HeadRepo: repo,
  133. BaseRepo: repo,
  134. MergeBase: "",
  135. Type: issues_model.PullRequestGitea,
  136. Flow: issues_model.PullRequestFlowAGit,
  137. }
  138. prOpts := &pull_service.NewPullRequestOptions{
  139. Repo: repo,
  140. Issue: prIssue,
  141. PullRequest: pr,
  142. }
  143. if err := pull_service.NewPullRequest(ctx, prOpts); err != nil {
  144. return nil, err
  145. }
  146. log.Trace("Pull request created: %d/%d", repo.ID, prIssue.ID)
  147. results = append(results, private.HookProcReceiveRefResult{
  148. Ref: pr.GetGitHeadRefName(),
  149. OriginalRef: opts.RefFullNames[i],
  150. OldOID: objectFormat.EmptyObjectID().String(),
  151. NewOID: opts.NewCommitIDs[i],
  152. IsCreatePR: false, // AGit always creates a pull request so there is no point in prompting user to create one
  153. URL: fmt.Sprintf("%s/pulls/%d", repo.HTMLURL(), pr.Index),
  154. ShouldShowMessage: setting.Git.PullRequestPushMessage && repo.AllowsPulls(ctx),
  155. HeadBranch: headBranch,
  156. })
  157. continue
  158. }
  159. // update exist pull request
  160. if err := pr.LoadBaseRepo(ctx); err != nil {
  161. return nil, fmt.Errorf("unable to load base repository for PR[%d] Error: %w", pr.ID, err)
  162. }
  163. oldCommitID, err := gitRepo.GetRefCommitID(pr.GetGitHeadRefName())
  164. if err != nil {
  165. return nil, fmt.Errorf("unable to get ref commit id in base repository for PR[%d] Error: %w", pr.ID, err)
  166. }
  167. if oldCommitID == opts.NewCommitIDs[i] {
  168. results = append(results, private.HookProcReceiveRefResult{
  169. OriginalRef: opts.RefFullNames[i],
  170. OldOID: opts.OldCommitIDs[i],
  171. NewOID: opts.NewCommitIDs[i],
  172. Err: "new commit is same with old commit",
  173. })
  174. continue
  175. }
  176. if !forcePush.Value() {
  177. output, _, err := gitcmd.NewCommand("rev-list", "--max-count=1").
  178. AddDynamicArguments(oldCommitID, "^"+opts.NewCommitIDs[i]).
  179. RunStdString(ctx, &gitcmd.RunOpts{Dir: repo.RepoPath(), Env: os.Environ()})
  180. if err != nil {
  181. return nil, fmt.Errorf("failed to detect force push: %w", err)
  182. } else if len(output) > 0 {
  183. results = append(results, private.HookProcReceiveRefResult{
  184. OriginalRef: opts.RefFullNames[i],
  185. OldOID: opts.OldCommitIDs[i],
  186. NewOID: opts.NewCommitIDs[i],
  187. Err: "request `force-push` push option",
  188. })
  189. continue
  190. }
  191. }
  192. // Store old commit ID for review staleness checking
  193. oldHeadCommitID := pr.HeadCommitID
  194. pr.HeadCommitID = opts.NewCommitIDs[i]
  195. if err = pull_service.UpdateRef(ctx, pr); err != nil {
  196. return nil, fmt.Errorf("failed to update pull ref. Error: %w", err)
  197. }
  198. // Mark existing reviews as stale when PR content changes (same as regular GitHub flow)
  199. if oldHeadCommitID != opts.NewCommitIDs[i] {
  200. if err := issues_model.MarkReviewsAsStale(ctx, pr.IssueID); err != nil {
  201. log.Error("MarkReviewsAsStale: %v", err)
  202. }
  203. // Dismiss all approval reviews if protected branch rule item enabled
  204. pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
  205. if err != nil {
  206. log.Error("GetFirstMatchProtectedBranchRule: %v", err)
  207. }
  208. if pb != nil && pb.DismissStaleApprovals {
  209. if err := pull_service.DismissApprovalReviews(ctx, pusher, pr); err != nil {
  210. log.Error("DismissApprovalReviews: %v", err)
  211. }
  212. }
  213. // Mark reviews for the new commit as not stale
  214. if err := issues_model.MarkReviewsAsNotStale(ctx, pr.IssueID, opts.NewCommitIDs[i]); err != nil {
  215. log.Error("MarkReviewsAsNotStale: %v", err)
  216. }
  217. }
  218. pull_service.StartPullRequestCheckImmediately(ctx, pr)
  219. err = pr.LoadIssue(ctx)
  220. if err != nil {
  221. return nil, fmt.Errorf("failed to load pull issue. Error: %w", err)
  222. }
  223. comment, err := pull_service.CreatePushPullComment(ctx, pusher, pr, oldCommitID, opts.NewCommitIDs[i], forcePush.Value())
  224. if err == nil && comment != nil {
  225. notify_service.PullRequestPushCommits(ctx, pusher, pr, comment)
  226. }
  227. notify_service.PullRequestSynchronized(ctx, pusher, pr)
  228. isForcePush := comment != nil && comment.IsForcePush
  229. results = append(results, private.HookProcReceiveRefResult{
  230. OldOID: oldCommitID,
  231. NewOID: opts.NewCommitIDs[i],
  232. Ref: pr.GetGitHeadRefName(),
  233. OriginalRef: opts.RefFullNames[i],
  234. IsForcePush: isForcePush,
  235. IsCreatePR: false,
  236. URL: fmt.Sprintf("%s/pulls/%d", repo.HTMLURL(), pr.Index),
  237. ShouldShowMessage: setting.Git.PullRequestPushMessage && repo.AllowsPulls(ctx),
  238. })
  239. }
  240. return results, nil
  241. }
  242. // UserNameChanged handle user name change for agit flow pull
  243. func UserNameChanged(ctx context.Context, user *user_model.User, newName string) error {
  244. pulls, err := issues_model.GetAllUnmergedAgitPullRequestByPoster(ctx, user.ID)
  245. if err != nil {
  246. return err
  247. }
  248. newName = strings.ToLower(newName)
  249. for _, pull := range pulls {
  250. pull.HeadBranch = strings.TrimPrefix(pull.HeadBranch, user.LowerName+"/")
  251. pull.HeadBranch = newName + "/" + pull.HeadBranch
  252. if err = pull.UpdateCols(ctx, "head_branch"); err != nil {
  253. return err
  254. }
  255. }
  256. return nil
  257. }