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

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