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 18KB

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