You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

pull.go 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624
  1. // Copyright 2016 The Gitea Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package repo
  5. import (
  6. "fmt"
  7. "strings"
  8. "code.gitea.io/git"
  9. "code.gitea.io/gitea/models"
  10. "code.gitea.io/gitea/modules/auth"
  11. "code.gitea.io/gitea/modules/context"
  12. "code.gitea.io/gitea/modules/log"
  13. api "code.gitea.io/sdk/gitea"
  14. )
  15. // ListPullRequests returns a list of all PRs
  16. func ListPullRequests(ctx *context.APIContext, form api.ListPullRequestsOptions) {
  17. // swagger:operation GET /repos/{owner}/{repo}/pulls repository repoListPullRequests
  18. // ---
  19. // summary: List a repo's pull requests
  20. // produces:
  21. // - application/json
  22. // parameters:
  23. // - name: owner
  24. // in: path
  25. // description: owner of the repo
  26. // type: string
  27. // required: true
  28. // - name: repo
  29. // in: path
  30. // description: name of the repo
  31. // type: string
  32. // required: true
  33. // responses:
  34. // "200":
  35. // "$ref": "#/responses/PullRequestList"
  36. prs, maxResults, err := models.PullRequests(ctx.Repo.Repository.ID, &models.PullRequestsOptions{
  37. Page: ctx.QueryInt("page"),
  38. State: ctx.QueryTrim("state"),
  39. SortType: ctx.QueryTrim("sort"),
  40. Labels: ctx.QueryStrings("labels"),
  41. MilestoneID: ctx.QueryInt64("milestone"),
  42. })
  43. if err != nil {
  44. ctx.Error(500, "PullRequests", err)
  45. return
  46. }
  47. apiPrs := make([]*api.PullRequest, len(prs))
  48. for i := range prs {
  49. if err = prs[i].LoadIssue(); err != nil {
  50. ctx.Error(500, "LoadIssue", err)
  51. return
  52. }
  53. if err = prs[i].LoadAttributes(); err != nil {
  54. ctx.Error(500, "LoadAttributes", err)
  55. return
  56. }
  57. if err = prs[i].GetBaseRepo(); err != nil {
  58. ctx.Error(500, "GetBaseRepo", err)
  59. return
  60. }
  61. if err = prs[i].GetHeadRepo(); err != nil {
  62. ctx.Error(500, "GetHeadRepo", err)
  63. return
  64. }
  65. apiPrs[i] = prs[i].APIFormat()
  66. }
  67. ctx.SetLinkHeader(int(maxResults), models.ItemsPerPage)
  68. ctx.JSON(200, &apiPrs)
  69. }
  70. // GetPullRequest returns a single PR based on index
  71. func GetPullRequest(ctx *context.APIContext) {
  72. // swagger:operation GET /repos/{owner}/{repo}/pulls/{index} repository repoGetPullRequest
  73. // ---
  74. // summary: Get a pull request
  75. // produces:
  76. // - application/json
  77. // parameters:
  78. // - name: owner
  79. // in: path
  80. // description: owner of the repo
  81. // type: string
  82. // required: true
  83. // - name: repo
  84. // in: path
  85. // description: name of the repo
  86. // type: string
  87. // required: true
  88. // - name: index
  89. // in: path
  90. // description: index of the pull request to get
  91. // type: integer
  92. // required: true
  93. // responses:
  94. // "200":
  95. // "$ref": "#/responses/PullRequest"
  96. pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  97. if err != nil {
  98. if models.IsErrPullRequestNotExist(err) {
  99. ctx.Status(404)
  100. } else {
  101. ctx.Error(500, "GetPullRequestByIndex", err)
  102. }
  103. return
  104. }
  105. if err = pr.GetBaseRepo(); err != nil {
  106. ctx.Error(500, "GetBaseRepo", err)
  107. return
  108. }
  109. if err = pr.GetHeadRepo(); err != nil {
  110. ctx.Error(500, "GetHeadRepo", err)
  111. return
  112. }
  113. ctx.JSON(200, pr.APIFormat())
  114. }
  115. // CreatePullRequest does what it says
  116. func CreatePullRequest(ctx *context.APIContext, form api.CreatePullRequestOption) {
  117. // swagger:operation POST /repos/{owner}/{repo}/pulls repository repoCreatePullRequest
  118. // ---
  119. // summary: Create a pull request
  120. // consumes:
  121. // - application/json
  122. // produces:
  123. // - application/json
  124. // parameters:
  125. // - name: owner
  126. // in: path
  127. // description: owner of the repo
  128. // type: string
  129. // required: true
  130. // - name: repo
  131. // in: path
  132. // description: name of the repo
  133. // type: string
  134. // required: true
  135. // - name: body
  136. // in: body
  137. // schema:
  138. // "$ref": "#/definitions/CreatePullRequestOption"
  139. // responses:
  140. // "201":
  141. // "$ref": "#/responses/PullRequest"
  142. var (
  143. repo = ctx.Repo.Repository
  144. labelIDs []int64
  145. assigneeID int64
  146. milestoneID int64
  147. )
  148. // Get repo/branch information
  149. headUser, headRepo, headGitRepo, prInfo, baseBranch, headBranch := parseCompareInfo(ctx, form)
  150. if ctx.Written() {
  151. return
  152. }
  153. // Check if another PR exists with the same targets
  154. existingPr, err := models.GetUnmergedPullRequest(headRepo.ID, ctx.Repo.Repository.ID, headBranch, baseBranch)
  155. if err != nil {
  156. if !models.IsErrPullRequestNotExist(err) {
  157. ctx.Error(500, "GetUnmergedPullRequest", err)
  158. return
  159. }
  160. } else {
  161. err = models.ErrPullRequestAlreadyExists{
  162. ID: existingPr.ID,
  163. IssueID: existingPr.Index,
  164. HeadRepoID: existingPr.HeadRepoID,
  165. BaseRepoID: existingPr.BaseRepoID,
  166. HeadBranch: existingPr.HeadBranch,
  167. BaseBranch: existingPr.BaseBranch,
  168. }
  169. ctx.Error(409, "GetUnmergedPullRequest", err)
  170. return
  171. }
  172. if len(form.Labels) > 0 {
  173. labels, err := models.GetLabelsInRepoByIDs(ctx.Repo.Repository.ID, form.Labels)
  174. if err != nil {
  175. ctx.Error(500, "GetLabelsInRepoByIDs", err)
  176. return
  177. }
  178. labelIDs = make([]int64, len(labels))
  179. for i := range labels {
  180. labelIDs[i] = labels[i].ID
  181. }
  182. }
  183. if form.Milestone > 0 {
  184. milestone, err := models.GetMilestoneByRepoID(ctx.Repo.Repository.ID, milestoneID)
  185. if err != nil {
  186. if models.IsErrMilestoneNotExist(err) {
  187. ctx.Status(404)
  188. } else {
  189. ctx.Error(500, "GetMilestoneByRepoID", err)
  190. }
  191. return
  192. }
  193. milestoneID = milestone.ID
  194. }
  195. if len(form.Assignee) > 0 {
  196. assigneeUser, err := models.GetUserByName(form.Assignee)
  197. if err != nil {
  198. if models.IsErrUserNotExist(err) {
  199. ctx.Error(422, "", fmt.Sprintf("assignee does not exist: [name: %s]", form.Assignee))
  200. } else {
  201. ctx.Error(500, "GetUserByName", err)
  202. }
  203. return
  204. }
  205. assignee, err := repo.GetAssigneeByID(assigneeUser.ID)
  206. if err != nil {
  207. ctx.Error(500, "GetAssigneeByID", err)
  208. return
  209. }
  210. assigneeID = assignee.ID
  211. }
  212. patch, err := headGitRepo.GetPatch(prInfo.MergeBase, headBranch)
  213. if err != nil {
  214. ctx.Error(500, "GetPatch", err)
  215. return
  216. }
  217. prIssue := &models.Issue{
  218. RepoID: repo.ID,
  219. Index: repo.NextIssueIndex(),
  220. Title: form.Title,
  221. PosterID: ctx.User.ID,
  222. Poster: ctx.User,
  223. MilestoneID: milestoneID,
  224. AssigneeID: assigneeID,
  225. IsPull: true,
  226. Content: form.Body,
  227. }
  228. pr := &models.PullRequest{
  229. HeadRepoID: headRepo.ID,
  230. BaseRepoID: repo.ID,
  231. HeadUserName: headUser.Name,
  232. HeadBranch: headBranch,
  233. BaseBranch: baseBranch,
  234. HeadRepo: headRepo,
  235. BaseRepo: repo,
  236. MergeBase: prInfo.MergeBase,
  237. Type: models.PullRequestGitea,
  238. }
  239. if err := models.NewPullRequest(repo, prIssue, labelIDs, []string{}, pr, patch); err != nil {
  240. ctx.Error(500, "NewPullRequest", err)
  241. return
  242. } else if err := pr.PushToBaseRepo(); err != nil {
  243. ctx.Error(500, "PushToBaseRepo", err)
  244. return
  245. }
  246. log.Trace("Pull request created: %d/%d", repo.ID, prIssue.ID)
  247. ctx.JSON(201, pr.APIFormat())
  248. }
  249. // EditPullRequest does what it says
  250. func EditPullRequest(ctx *context.APIContext, form api.EditPullRequestOption) {
  251. // swagger:operation PATCH /repos/{owner}/{repo}/pulls/{index} repository repoEditPullRequest
  252. // ---
  253. // summary: Update a pull request
  254. // consumes:
  255. // - application/json
  256. // produces:
  257. // - application/json
  258. // parameters:
  259. // - name: owner
  260. // in: path
  261. // description: owner of the repo
  262. // type: string
  263. // required: true
  264. // - name: repo
  265. // in: path
  266. // description: name of the repo
  267. // type: string
  268. // required: true
  269. // - name: index
  270. // in: path
  271. // description: index of the pull request to edit
  272. // type: integer
  273. // required: true
  274. // - name: body
  275. // in: body
  276. // schema:
  277. // "$ref": "#/definitions/EditPullRequestOption"
  278. // responses:
  279. // "201":
  280. // "$ref": "#/responses/PullRequest"
  281. pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  282. if err != nil {
  283. if models.IsErrPullRequestNotExist(err) {
  284. ctx.Status(404)
  285. } else {
  286. ctx.Error(500, "GetPullRequestByIndex", err)
  287. }
  288. return
  289. }
  290. pr.LoadIssue()
  291. issue := pr.Issue
  292. if !issue.IsPoster(ctx.User.ID) && !ctx.Repo.IsWriter() {
  293. ctx.Status(403)
  294. return
  295. }
  296. if len(form.Title) > 0 {
  297. issue.Title = form.Title
  298. }
  299. if len(form.Body) > 0 {
  300. issue.Content = form.Body
  301. }
  302. if ctx.Repo.IsWriter() && len(form.Assignee) > 0 &&
  303. (issue.Assignee == nil || issue.Assignee.LowerName != strings.ToLower(form.Assignee)) {
  304. if len(form.Assignee) == 0 {
  305. issue.AssigneeID = 0
  306. } else {
  307. assignee, err := models.GetUserByName(form.Assignee)
  308. if err != nil {
  309. if models.IsErrUserNotExist(err) {
  310. ctx.Error(422, "", fmt.Sprintf("assignee does not exist: [name: %s]", form.Assignee))
  311. } else {
  312. ctx.Error(500, "GetUserByName", err)
  313. }
  314. return
  315. }
  316. issue.AssigneeID = assignee.ID
  317. }
  318. if err = models.UpdateIssueUserByAssignee(issue); err != nil {
  319. ctx.Error(500, "UpdateIssueUserByAssignee", err)
  320. return
  321. }
  322. }
  323. if ctx.Repo.IsWriter() && form.Milestone != 0 &&
  324. issue.MilestoneID != form.Milestone {
  325. oldMilestoneID := issue.MilestoneID
  326. issue.MilestoneID = form.Milestone
  327. if err = models.ChangeMilestoneAssign(issue, ctx.User, oldMilestoneID); err != nil {
  328. ctx.Error(500, "ChangeMilestoneAssign", err)
  329. return
  330. }
  331. }
  332. if err = models.UpdateIssue(issue); err != nil {
  333. ctx.Error(500, "UpdateIssue", err)
  334. return
  335. }
  336. if form.State != nil {
  337. if err = issue.ChangeStatus(ctx.User, ctx.Repo.Repository, api.StateClosed == api.StateType(*form.State)); err != nil {
  338. ctx.Error(500, "ChangeStatus", err)
  339. return
  340. }
  341. }
  342. // Refetch from database
  343. pr, err = models.GetPullRequestByIndex(ctx.Repo.Repository.ID, pr.Index)
  344. if err != nil {
  345. if models.IsErrPullRequestNotExist(err) {
  346. ctx.Status(404)
  347. } else {
  348. ctx.Error(500, "GetPullRequestByIndex", err)
  349. }
  350. return
  351. }
  352. // TODO this should be 200, not 201
  353. ctx.JSON(201, pr.APIFormat())
  354. }
  355. // IsPullRequestMerged checks if a PR exists given an index
  356. func IsPullRequestMerged(ctx *context.APIContext) {
  357. // swagger:operation GET /repos/{owner}/{repo}/pulls/{index}/merge repository repoPullRequestIsMerged
  358. // ---
  359. // summary: Check if a pull request has been merged
  360. // produces:
  361. // - application/json
  362. // parameters:
  363. // - name: owner
  364. // in: path
  365. // description: owner of the repo
  366. // type: string
  367. // required: true
  368. // - name: repo
  369. // in: path
  370. // description: name of the repo
  371. // type: string
  372. // required: true
  373. // - name: index
  374. // in: path
  375. // description: index of the pull request
  376. // type: integer
  377. // required: true
  378. // responses:
  379. // "204":
  380. // description: pull request has been merged
  381. // schema:
  382. // "$ref": "#/responses/empty"
  383. // "404":
  384. // description: pull request has not been merged
  385. // schema:
  386. // "$ref": "#/responses/empty"
  387. pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  388. if err != nil {
  389. if models.IsErrPullRequestNotExist(err) {
  390. ctx.Status(404)
  391. } else {
  392. ctx.Error(500, "GetPullRequestByIndex", err)
  393. }
  394. return
  395. }
  396. if pr.HasMerged {
  397. ctx.Status(204)
  398. }
  399. ctx.Status(404)
  400. }
  401. // MergePullRequest merges a PR given an index
  402. func MergePullRequest(ctx *context.APIContext, form auth.MergePullRequestForm) {
  403. // swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/merge repository repoMergePullRequest
  404. // ---
  405. // summary: Merge a pull request
  406. // produces:
  407. // - application/json
  408. // parameters:
  409. // - name: owner
  410. // in: path
  411. // description: owner of the repo
  412. // type: string
  413. // required: true
  414. // - name: repo
  415. // in: path
  416. // description: name of the repo
  417. // type: string
  418. // required: true
  419. // - name: index
  420. // in: path
  421. // description: index of the pull request to merge
  422. // type: integer
  423. // required: true
  424. // responses:
  425. // "200":
  426. // "$ref": "#/responses/empty"
  427. // "405":
  428. // "$ref": "#/responses/empty"
  429. pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  430. if err != nil {
  431. if models.IsErrPullRequestNotExist(err) {
  432. ctx.Handle(404, "GetPullRequestByIndex", err)
  433. } else {
  434. ctx.Error(500, "GetPullRequestByIndex", err)
  435. }
  436. return
  437. }
  438. if err = pr.GetHeadRepo(); err != nil {
  439. ctx.Handle(500, "GetHeadRepo", err)
  440. return
  441. }
  442. pr.LoadIssue()
  443. pr.Issue.Repo = ctx.Repo.Repository
  444. if ctx.IsSigned {
  445. // Update issue-user.
  446. if err = pr.Issue.ReadBy(ctx.User.ID); err != nil {
  447. ctx.Error(500, "ReadBy", err)
  448. return
  449. }
  450. }
  451. if pr.Issue.IsClosed {
  452. ctx.Status(404)
  453. return
  454. }
  455. if !pr.CanAutoMerge() || pr.HasMerged {
  456. ctx.Status(405)
  457. return
  458. }
  459. if len(form.Do) == 0 {
  460. form.Do = string(models.MergeStyleMerge)
  461. }
  462. message := strings.TrimSpace(form.MergeTitleField)
  463. if len(message) == 0 {
  464. if models.MergeStyle(form.Do) == models.MergeStyleMerge {
  465. message = pr.GetDefaultMergeMessage()
  466. }
  467. if models.MergeStyle(form.Do) == models.MergeStyleSquash {
  468. message = pr.GetDefaultSquashMessage()
  469. }
  470. }
  471. form.MergeMessageField = strings.TrimSpace(form.MergeMessageField)
  472. if len(form.MergeMessageField) > 0 {
  473. message += "\n\n" + form.MergeMessageField
  474. }
  475. if err := pr.Merge(ctx.User, ctx.Repo.GitRepo, models.MergeStyle(form.Do), message); err != nil {
  476. if models.IsErrInvalidMergeStyle(err) {
  477. ctx.Status(405)
  478. return
  479. }
  480. ctx.Error(500, "Merge", err)
  481. return
  482. }
  483. log.Trace("Pull request merged: %d", pr.ID)
  484. ctx.Status(200)
  485. }
  486. func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) (*models.User, *models.Repository, *git.Repository, *git.PullRequestInfo, string, string) {
  487. baseRepo := ctx.Repo.Repository
  488. // Get compared branches information
  489. // format: <base branch>...[<head repo>:]<head branch>
  490. // base<-head: master...head:feature
  491. // same repo: master...feature
  492. // TODO: Validate form first?
  493. baseBranch := form.Base
  494. var (
  495. headUser *models.User
  496. headBranch string
  497. isSameRepo bool
  498. err error
  499. )
  500. // If there is no head repository, it means pull request between same repository.
  501. headInfos := strings.Split(form.Head, ":")
  502. if len(headInfos) == 1 {
  503. isSameRepo = true
  504. headUser = ctx.Repo.Owner
  505. headBranch = headInfos[0]
  506. } else if len(headInfos) == 2 {
  507. headUser, err = models.GetUserByName(headInfos[0])
  508. if err != nil {
  509. if models.IsErrUserNotExist(err) {
  510. ctx.Handle(404, "GetUserByName", nil)
  511. } else {
  512. ctx.Handle(500, "GetUserByName", err)
  513. }
  514. return nil, nil, nil, nil, "", ""
  515. }
  516. headBranch = headInfos[1]
  517. } else {
  518. ctx.Status(404)
  519. return nil, nil, nil, nil, "", ""
  520. }
  521. ctx.Repo.PullRequest.SameRepo = isSameRepo
  522. log.Info("Base branch: %s", baseBranch)
  523. log.Info("Repo path: %s", ctx.Repo.GitRepo.Path)
  524. // Check if base branch is valid.
  525. if !ctx.Repo.GitRepo.IsBranchExist(baseBranch) {
  526. ctx.Status(404)
  527. return nil, nil, nil, nil, "", ""
  528. }
  529. // Check if current user has fork of repository or in the same repository.
  530. headRepo, has := models.HasForkedRepo(headUser.ID, baseRepo.ID)
  531. if !has && !isSameRepo {
  532. log.Trace("parseCompareInfo[%d]: does not have fork or in same repository", baseRepo.ID)
  533. ctx.Status(404)
  534. return nil, nil, nil, nil, "", ""
  535. }
  536. var headGitRepo *git.Repository
  537. if isSameRepo {
  538. headRepo = ctx.Repo.Repository
  539. headGitRepo = ctx.Repo.GitRepo
  540. } else {
  541. headGitRepo, err = git.OpenRepository(models.RepoPath(headUser.Name, headRepo.Name))
  542. if err != nil {
  543. ctx.Error(500, "OpenRepository", err)
  544. return nil, nil, nil, nil, "", ""
  545. }
  546. }
  547. if !ctx.User.IsWriterOfRepo(headRepo) && !ctx.User.IsAdmin {
  548. log.Trace("ParseCompareInfo[%d]: does not have write access or site admin", baseRepo.ID)
  549. ctx.Status(404)
  550. return nil, nil, nil, nil, "", ""
  551. }
  552. // Check if head branch is valid.
  553. if !headGitRepo.IsBranchExist(headBranch) {
  554. ctx.Status(404)
  555. return nil, nil, nil, nil, "", ""
  556. }
  557. prInfo, err := headGitRepo.GetPullRequestInfo(models.RepoPath(baseRepo.Owner.Name, baseRepo.Name), baseBranch, headBranch)
  558. if err != nil {
  559. ctx.Error(500, "GetPullRequestInfo", err)
  560. return nil, nil, nil, nil, "", ""
  561. }
  562. return headUser, headRepo, headGitRepo, prInfo, baseBranch, headBranch
  563. }