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

pull.go 15 kB

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