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.

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243
  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. "errors"
  7. "fmt"
  8. "math"
  9. "net/http"
  10. "strconv"
  11. "strings"
  12. "time"
  13. "code.gitea.io/gitea/models"
  14. repo_model "code.gitea.io/gitea/models/repo"
  15. "code.gitea.io/gitea/models/unit"
  16. user_model "code.gitea.io/gitea/models/user"
  17. "code.gitea.io/gitea/modules/context"
  18. "code.gitea.io/gitea/modules/convert"
  19. "code.gitea.io/gitea/modules/git"
  20. "code.gitea.io/gitea/modules/log"
  21. "code.gitea.io/gitea/modules/notification"
  22. api "code.gitea.io/gitea/modules/structs"
  23. "code.gitea.io/gitea/modules/timeutil"
  24. "code.gitea.io/gitea/modules/web"
  25. "code.gitea.io/gitea/routers/api/v1/utils"
  26. asymkey_service "code.gitea.io/gitea/services/asymkey"
  27. "code.gitea.io/gitea/services/forms"
  28. issue_service "code.gitea.io/gitea/services/issue"
  29. pull_service "code.gitea.io/gitea/services/pull"
  30. repo_service "code.gitea.io/gitea/services/repository"
  31. )
  32. // ListPullRequests returns a list of all PRs
  33. func ListPullRequests(ctx *context.APIContext) {
  34. // swagger:operation GET /repos/{owner}/{repo}/pulls repository repoListPullRequests
  35. // ---
  36. // summary: List a repo's pull requests
  37. // produces:
  38. // - application/json
  39. // parameters:
  40. // - name: owner
  41. // in: path
  42. // description: owner of the repo
  43. // type: string
  44. // required: true
  45. // - name: repo
  46. // in: path
  47. // description: name of the repo
  48. // type: string
  49. // required: true
  50. // - name: state
  51. // in: query
  52. // description: "State of pull request: open or closed (optional)"
  53. // type: string
  54. // enum: [closed, open, all]
  55. // - name: sort
  56. // in: query
  57. // description: "Type of sort"
  58. // type: string
  59. // enum: [oldest, recentupdate, leastupdate, mostcomment, leastcomment, priority]
  60. // - name: milestone
  61. // in: query
  62. // description: "ID of the milestone"
  63. // type: integer
  64. // format: int64
  65. // - name: labels
  66. // in: query
  67. // description: "Label IDs"
  68. // type: array
  69. // collectionFormat: multi
  70. // items:
  71. // type: integer
  72. // format: int64
  73. // - name: page
  74. // in: query
  75. // description: page number of results to return (1-based)
  76. // type: integer
  77. // - name: limit
  78. // in: query
  79. // description: page size of results
  80. // type: integer
  81. // responses:
  82. // "200":
  83. // "$ref": "#/responses/PullRequestList"
  84. listOptions := utils.GetListOptions(ctx)
  85. prs, maxResults, err := models.PullRequests(ctx.Repo.Repository.ID, &models.PullRequestsOptions{
  86. ListOptions: listOptions,
  87. State: ctx.FormTrim("state"),
  88. SortType: ctx.FormTrim("sort"),
  89. Labels: ctx.FormStrings("labels"),
  90. MilestoneID: ctx.FormInt64("milestone"),
  91. })
  92. if err != nil {
  93. ctx.Error(http.StatusInternalServerError, "PullRequests", err)
  94. return
  95. }
  96. apiPrs := make([]*api.PullRequest, len(prs))
  97. for i := range prs {
  98. if err = prs[i].LoadIssue(); err != nil {
  99. ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
  100. return
  101. }
  102. if err = prs[i].LoadAttributes(); err != nil {
  103. ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
  104. return
  105. }
  106. if err = prs[i].LoadBaseRepo(); err != nil {
  107. ctx.Error(http.StatusInternalServerError, "LoadBaseRepo", err)
  108. return
  109. }
  110. if err = prs[i].LoadHeadRepo(); err != nil {
  111. ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err)
  112. return
  113. }
  114. apiPrs[i] = convert.ToAPIPullRequest(prs[i], ctx.User)
  115. }
  116. ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
  117. ctx.SetTotalCountHeader(maxResults)
  118. ctx.JSON(http.StatusOK, &apiPrs)
  119. }
  120. // GetPullRequest returns a single PR based on index
  121. func GetPullRequest(ctx *context.APIContext) {
  122. // swagger:operation GET /repos/{owner}/{repo}/pulls/{index} repository repoGetPullRequest
  123. // ---
  124. // summary: Get a pull request
  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: index
  139. // in: path
  140. // description: index of the pull request to get
  141. // type: integer
  142. // format: int64
  143. // required: true
  144. // responses:
  145. // "200":
  146. // "$ref": "#/responses/PullRequest"
  147. // "404":
  148. // "$ref": "#/responses/notFound"
  149. pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  150. if err != nil {
  151. if models.IsErrPullRequestNotExist(err) {
  152. ctx.NotFound()
  153. } else {
  154. ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
  155. }
  156. return
  157. }
  158. if err = pr.LoadBaseRepo(); err != nil {
  159. ctx.Error(http.StatusInternalServerError, "LoadBaseRepo", err)
  160. return
  161. }
  162. if err = pr.LoadHeadRepo(); err != nil {
  163. ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err)
  164. return
  165. }
  166. ctx.JSON(http.StatusOK, convert.ToAPIPullRequest(pr, ctx.User))
  167. }
  168. // DownloadPullDiffOrPatch render a pull's raw diff or patch
  169. func DownloadPullDiffOrPatch(ctx *context.APIContext) {
  170. // swagger:operation GET /repos/{owner}/{repo}/pulls/{index}.{diffType} repository repoDownloadPullDiffOrPatch
  171. // ---
  172. // summary: Get a pull request diff or patch
  173. // produces:
  174. // - text/plain
  175. // parameters:
  176. // - name: owner
  177. // in: path
  178. // description: owner of the repo
  179. // type: string
  180. // required: true
  181. // - name: repo
  182. // in: path
  183. // description: name of the repo
  184. // type: string
  185. // required: true
  186. // - name: index
  187. // in: path
  188. // description: index of the pull request to get
  189. // type: integer
  190. // format: int64
  191. // required: true
  192. // - name: diffType
  193. // in: path
  194. // description: whether the output is diff or patch
  195. // type: string
  196. // enum: [diff, patch]
  197. // required: true
  198. // - name: binary
  199. // in: query
  200. // description: whether to include binary file changes. if true, the diff is applicable with `git apply`
  201. // type: boolean
  202. // responses:
  203. // "200":
  204. // "$ref": "#/responses/string"
  205. // "404":
  206. // "$ref": "#/responses/notFound"
  207. pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  208. if err != nil {
  209. if models.IsErrPullRequestNotExist(err) {
  210. ctx.NotFound()
  211. } else {
  212. ctx.InternalServerError(err)
  213. }
  214. return
  215. }
  216. var patch bool
  217. if ctx.Params(":diffType") == "diff" {
  218. patch = false
  219. } else {
  220. patch = true
  221. }
  222. binary := ctx.FormBool("binary")
  223. if err := pull_service.DownloadDiffOrPatch(pr, ctx, patch, binary); err != nil {
  224. ctx.InternalServerError(err)
  225. return
  226. }
  227. }
  228. // CreatePullRequest does what it says
  229. func CreatePullRequest(ctx *context.APIContext) {
  230. // swagger:operation POST /repos/{owner}/{repo}/pulls repository repoCreatePullRequest
  231. // ---
  232. // summary: Create a pull request
  233. // consumes:
  234. // - application/json
  235. // produces:
  236. // - application/json
  237. // parameters:
  238. // - name: owner
  239. // in: path
  240. // description: owner of the repo
  241. // type: string
  242. // required: true
  243. // - name: repo
  244. // in: path
  245. // description: name of the repo
  246. // type: string
  247. // required: true
  248. // - name: body
  249. // in: body
  250. // schema:
  251. // "$ref": "#/definitions/CreatePullRequestOption"
  252. // responses:
  253. // "201":
  254. // "$ref": "#/responses/PullRequest"
  255. // "409":
  256. // "$ref": "#/responses/error"
  257. // "422":
  258. // "$ref": "#/responses/validationError"
  259. form := *web.GetForm(ctx).(*api.CreatePullRequestOption)
  260. if form.Head == form.Base {
  261. ctx.Error(http.StatusUnprocessableEntity, "BaseHeadSame",
  262. "Invalid PullRequest: There are no changes between the head and the base")
  263. return
  264. }
  265. var (
  266. repo = ctx.Repo.Repository
  267. labelIDs []int64
  268. milestoneID int64
  269. )
  270. // Get repo/branch information
  271. _, headRepo, headGitRepo, compareInfo, baseBranch, headBranch := parseCompareInfo(ctx, form)
  272. if ctx.Written() {
  273. return
  274. }
  275. defer headGitRepo.Close()
  276. // Check if another PR exists with the same targets
  277. existingPr, err := models.GetUnmergedPullRequest(headRepo.ID, ctx.Repo.Repository.ID, headBranch, baseBranch, models.PullRequestFlowGithub)
  278. if err != nil {
  279. if !models.IsErrPullRequestNotExist(err) {
  280. ctx.Error(http.StatusInternalServerError, "GetUnmergedPullRequest", err)
  281. return
  282. }
  283. } else {
  284. err = models.ErrPullRequestAlreadyExists{
  285. ID: existingPr.ID,
  286. IssueID: existingPr.Index,
  287. HeadRepoID: existingPr.HeadRepoID,
  288. BaseRepoID: existingPr.BaseRepoID,
  289. HeadBranch: existingPr.HeadBranch,
  290. BaseBranch: existingPr.BaseBranch,
  291. }
  292. ctx.Error(http.StatusConflict, "GetUnmergedPullRequest", err)
  293. return
  294. }
  295. if len(form.Labels) > 0 {
  296. labels, err := models.GetLabelsInRepoByIDs(ctx.Repo.Repository.ID, form.Labels)
  297. if err != nil {
  298. ctx.Error(http.StatusInternalServerError, "GetLabelsInRepoByIDs", err)
  299. return
  300. }
  301. labelIDs = make([]int64, len(form.Labels))
  302. orgLabelIDs := make([]int64, len(form.Labels))
  303. for i := range labels {
  304. labelIDs[i] = labels[i].ID
  305. }
  306. if ctx.Repo.Owner.IsOrganization() {
  307. orgLabels, err := models.GetLabelsInOrgByIDs(ctx.Repo.Owner.ID, form.Labels)
  308. if err != nil {
  309. ctx.Error(http.StatusInternalServerError, "GetLabelsInOrgByIDs", err)
  310. return
  311. }
  312. for i := range orgLabels {
  313. orgLabelIDs[i] = orgLabels[i].ID
  314. }
  315. }
  316. labelIDs = append(labelIDs, orgLabelIDs...)
  317. }
  318. if form.Milestone > 0 {
  319. milestone, err := models.GetMilestoneByRepoID(ctx.Repo.Repository.ID, form.Milestone)
  320. if err != nil {
  321. if models.IsErrMilestoneNotExist(err) {
  322. ctx.NotFound()
  323. } else {
  324. ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoID", err)
  325. }
  326. return
  327. }
  328. milestoneID = milestone.ID
  329. }
  330. var deadlineUnix timeutil.TimeStamp
  331. if form.Deadline != nil {
  332. deadlineUnix = timeutil.TimeStamp(form.Deadline.Unix())
  333. }
  334. prIssue := &models.Issue{
  335. RepoID: repo.ID,
  336. Title: form.Title,
  337. PosterID: ctx.User.ID,
  338. Poster: ctx.User,
  339. MilestoneID: milestoneID,
  340. IsPull: true,
  341. Content: form.Body,
  342. DeadlineUnix: deadlineUnix,
  343. }
  344. pr := &models.PullRequest{
  345. HeadRepoID: headRepo.ID,
  346. BaseRepoID: repo.ID,
  347. HeadBranch: headBranch,
  348. BaseBranch: baseBranch,
  349. HeadRepo: headRepo,
  350. BaseRepo: repo,
  351. MergeBase: compareInfo.MergeBase,
  352. Type: models.PullRequestGitea,
  353. }
  354. // Get all assignee IDs
  355. assigneeIDs, err := models.MakeIDsFromAPIAssigneesToAdd(form.Assignee, form.Assignees)
  356. if err != nil {
  357. if user_model.IsErrUserNotExist(err) {
  358. ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err))
  359. } else {
  360. ctx.Error(http.StatusInternalServerError, "AddAssigneeByName", err)
  361. }
  362. return
  363. }
  364. // Check if the passed assignees is assignable
  365. for _, aID := range assigneeIDs {
  366. assignee, err := user_model.GetUserByID(aID)
  367. if err != nil {
  368. ctx.Error(http.StatusInternalServerError, "GetUserByID", err)
  369. return
  370. }
  371. valid, err := models.CanBeAssigned(assignee, repo, true)
  372. if err != nil {
  373. ctx.Error(http.StatusInternalServerError, "canBeAssigned", err)
  374. return
  375. }
  376. if !valid {
  377. ctx.Error(http.StatusUnprocessableEntity, "canBeAssigned", models.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name})
  378. return
  379. }
  380. }
  381. if err := pull_service.NewPullRequest(repo, prIssue, labelIDs, []string{}, pr, assigneeIDs); err != nil {
  382. if models.IsErrUserDoesNotHaveAccessToRepo(err) {
  383. ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
  384. return
  385. }
  386. ctx.Error(http.StatusInternalServerError, "NewPullRequest", err)
  387. return
  388. }
  389. log.Trace("Pull request created: %d/%d", repo.ID, prIssue.ID)
  390. ctx.JSON(http.StatusCreated, convert.ToAPIPullRequest(pr, ctx.User))
  391. }
  392. // EditPullRequest does what it says
  393. func EditPullRequest(ctx *context.APIContext) {
  394. // swagger:operation PATCH /repos/{owner}/{repo}/pulls/{index} repository repoEditPullRequest
  395. // ---
  396. // summary: Update a pull request. If using deadline only the date will be taken into account, and time of day ignored.
  397. // consumes:
  398. // - application/json
  399. // produces:
  400. // - application/json
  401. // parameters:
  402. // - name: owner
  403. // in: path
  404. // description: owner of the repo
  405. // type: string
  406. // required: true
  407. // - name: repo
  408. // in: path
  409. // description: name of the repo
  410. // type: string
  411. // required: true
  412. // - name: index
  413. // in: path
  414. // description: index of the pull request to edit
  415. // type: integer
  416. // format: int64
  417. // required: true
  418. // - name: body
  419. // in: body
  420. // schema:
  421. // "$ref": "#/definitions/EditPullRequestOption"
  422. // responses:
  423. // "201":
  424. // "$ref": "#/responses/PullRequest"
  425. // "403":
  426. // "$ref": "#/responses/forbidden"
  427. // "409":
  428. // "$ref": "#/responses/error"
  429. // "412":
  430. // "$ref": "#/responses/error"
  431. // "422":
  432. // "$ref": "#/responses/validationError"
  433. form := web.GetForm(ctx).(*api.EditPullRequestOption)
  434. pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  435. if err != nil {
  436. if models.IsErrPullRequestNotExist(err) {
  437. ctx.NotFound()
  438. } else {
  439. ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
  440. }
  441. return
  442. }
  443. err = pr.LoadIssue()
  444. if err != nil {
  445. ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
  446. return
  447. }
  448. issue := pr.Issue
  449. issue.Repo = ctx.Repo.Repository
  450. if !issue.IsPoster(ctx.User.ID) && !ctx.Repo.CanWrite(unit.TypePullRequests) {
  451. ctx.Status(http.StatusForbidden)
  452. return
  453. }
  454. oldTitle := issue.Title
  455. if len(form.Title) > 0 {
  456. issue.Title = form.Title
  457. }
  458. if len(form.Body) > 0 {
  459. issue.Content = form.Body
  460. }
  461. // Update or remove deadline if set
  462. if form.Deadline != nil || form.RemoveDeadline != nil {
  463. var deadlineUnix timeutil.TimeStamp
  464. if (form.RemoveDeadline == nil || !*form.RemoveDeadline) && !form.Deadline.IsZero() {
  465. deadline := time.Date(form.Deadline.Year(), form.Deadline.Month(), form.Deadline.Day(),
  466. 23, 59, 59, 0, form.Deadline.Location())
  467. deadlineUnix = timeutil.TimeStamp(deadline.Unix())
  468. }
  469. if err := models.UpdateIssueDeadline(issue, deadlineUnix, ctx.User); err != nil {
  470. ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err)
  471. return
  472. }
  473. issue.DeadlineUnix = deadlineUnix
  474. }
  475. // Add/delete assignees
  476. // Deleting is done the GitHub way (quote from their api documentation):
  477. // https://developer.github.com/v3/issues/#edit-an-issue
  478. // "assignees" (array): Logins for Users to assign to this issue.
  479. // Pass one or more user logins to replace the set of assignees on this Issue.
  480. // Send an empty array ([]) to clear all assignees from the Issue.
  481. if ctx.Repo.CanWrite(unit.TypePullRequests) && (form.Assignees != nil || len(form.Assignee) > 0) {
  482. err = issue_service.UpdateAssignees(issue, form.Assignee, form.Assignees, ctx.User)
  483. if err != nil {
  484. if user_model.IsErrUserNotExist(err) {
  485. ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err))
  486. } else {
  487. ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err)
  488. }
  489. return
  490. }
  491. }
  492. if ctx.Repo.CanWrite(unit.TypePullRequests) && form.Milestone != 0 &&
  493. issue.MilestoneID != form.Milestone {
  494. oldMilestoneID := issue.MilestoneID
  495. issue.MilestoneID = form.Milestone
  496. if err = issue_service.ChangeMilestoneAssign(issue, ctx.User, oldMilestoneID); err != nil {
  497. ctx.Error(http.StatusInternalServerError, "ChangeMilestoneAssign", err)
  498. return
  499. }
  500. }
  501. if ctx.Repo.CanWrite(unit.TypePullRequests) && form.Labels != nil {
  502. labels, err := models.GetLabelsInRepoByIDs(ctx.Repo.Repository.ID, form.Labels)
  503. if err != nil {
  504. ctx.Error(http.StatusInternalServerError, "GetLabelsInRepoByIDsError", err)
  505. return
  506. }
  507. if ctx.Repo.Owner.IsOrganization() {
  508. orgLabels, err := models.GetLabelsInOrgByIDs(ctx.Repo.Owner.ID, form.Labels)
  509. if err != nil {
  510. ctx.Error(http.StatusInternalServerError, "GetLabelsInOrgByIDs", err)
  511. return
  512. }
  513. labels = append(labels, orgLabels...)
  514. }
  515. if err = issue.ReplaceLabels(labels, ctx.User); err != nil {
  516. ctx.Error(http.StatusInternalServerError, "ReplaceLabelsError", err)
  517. return
  518. }
  519. }
  520. if form.State != nil {
  521. if pr.HasMerged {
  522. ctx.Error(http.StatusPreconditionFailed, "MergedPRState", "cannot change state of this pull request, it was already merged")
  523. return
  524. }
  525. issue.IsClosed = api.StateClosed == api.StateType(*form.State)
  526. }
  527. statusChangeComment, titleChanged, err := models.UpdateIssueByAPI(issue, ctx.User)
  528. if err != nil {
  529. if models.IsErrDependenciesLeft(err) {
  530. ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this pull request because it still has open dependencies")
  531. return
  532. }
  533. ctx.Error(http.StatusInternalServerError, "UpdateIssueByAPI", err)
  534. return
  535. }
  536. if titleChanged {
  537. notification.NotifyIssueChangeTitle(ctx.User, issue, oldTitle)
  538. }
  539. if statusChangeComment != nil {
  540. notification.NotifyIssueChangeStatus(ctx.User, issue, statusChangeComment, issue.IsClosed)
  541. }
  542. // change pull target branch
  543. if !pr.HasMerged && len(form.Base) != 0 && form.Base != pr.BaseBranch {
  544. if !ctx.Repo.GitRepo.IsBranchExist(form.Base) {
  545. ctx.Error(http.StatusNotFound, "NewBaseBranchNotExist", fmt.Errorf("new base '%s' not exist", form.Base))
  546. return
  547. }
  548. if err := pull_service.ChangeTargetBranch(pr, ctx.User, form.Base); err != nil {
  549. if models.IsErrPullRequestAlreadyExists(err) {
  550. ctx.Error(http.StatusConflict, "IsErrPullRequestAlreadyExists", err)
  551. return
  552. } else if models.IsErrIssueIsClosed(err) {
  553. ctx.Error(http.StatusUnprocessableEntity, "IsErrIssueIsClosed", err)
  554. return
  555. } else if models.IsErrPullRequestHasMerged(err) {
  556. ctx.Error(http.StatusConflict, "IsErrPullRequestHasMerged", err)
  557. return
  558. } else {
  559. ctx.InternalServerError(err)
  560. }
  561. return
  562. }
  563. notification.NotifyPullRequestChangeTargetBranch(ctx.User, pr, form.Base)
  564. }
  565. // Refetch from database
  566. pr, err = models.GetPullRequestByIndex(ctx.Repo.Repository.ID, pr.Index)
  567. if err != nil {
  568. if models.IsErrPullRequestNotExist(err) {
  569. ctx.NotFound()
  570. } else {
  571. ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
  572. }
  573. return
  574. }
  575. // TODO this should be 200, not 201
  576. ctx.JSON(http.StatusCreated, convert.ToAPIPullRequest(pr, ctx.User))
  577. }
  578. // IsPullRequestMerged checks if a PR exists given an index
  579. func IsPullRequestMerged(ctx *context.APIContext) {
  580. // swagger:operation GET /repos/{owner}/{repo}/pulls/{index}/merge repository repoPullRequestIsMerged
  581. // ---
  582. // summary: Check if a pull request has been merged
  583. // produces:
  584. // - application/json
  585. // parameters:
  586. // - name: owner
  587. // in: path
  588. // description: owner of the repo
  589. // type: string
  590. // required: true
  591. // - name: repo
  592. // in: path
  593. // description: name of the repo
  594. // type: string
  595. // required: true
  596. // - name: index
  597. // in: path
  598. // description: index of the pull request
  599. // type: integer
  600. // format: int64
  601. // required: true
  602. // responses:
  603. // "204":
  604. // description: pull request has been merged
  605. // "404":
  606. // description: pull request has not been merged
  607. pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  608. if err != nil {
  609. if models.IsErrPullRequestNotExist(err) {
  610. ctx.NotFound()
  611. } else {
  612. ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
  613. }
  614. return
  615. }
  616. if pr.HasMerged {
  617. ctx.Status(http.StatusNoContent)
  618. }
  619. ctx.NotFound()
  620. }
  621. // MergePullRequest merges a PR given an index
  622. func MergePullRequest(ctx *context.APIContext) {
  623. // swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/merge repository repoMergePullRequest
  624. // ---
  625. // summary: Merge a pull request
  626. // produces:
  627. // - application/json
  628. // parameters:
  629. // - name: owner
  630. // in: path
  631. // description: owner of the repo
  632. // type: string
  633. // required: true
  634. // - name: repo
  635. // in: path
  636. // description: name of the repo
  637. // type: string
  638. // required: true
  639. // - name: index
  640. // in: path
  641. // description: index of the pull request to merge
  642. // type: integer
  643. // format: int64
  644. // required: true
  645. // - name: body
  646. // in: body
  647. // schema:
  648. // $ref: "#/definitions/MergePullRequestOption"
  649. // responses:
  650. // "200":
  651. // "$ref": "#/responses/empty"
  652. // "405":
  653. // "$ref": "#/responses/empty"
  654. // "409":
  655. // "$ref": "#/responses/error"
  656. form := web.GetForm(ctx).(*forms.MergePullRequestForm)
  657. pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  658. if err != nil {
  659. if models.IsErrPullRequestNotExist(err) {
  660. ctx.NotFound("GetPullRequestByIndex", err)
  661. } else {
  662. ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
  663. }
  664. return
  665. }
  666. if err = pr.LoadHeadRepo(); err != nil {
  667. ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err)
  668. return
  669. }
  670. err = pr.LoadIssue()
  671. if err != nil {
  672. ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
  673. return
  674. }
  675. pr.Issue.Repo = ctx.Repo.Repository
  676. if ctx.IsSigned {
  677. // Update issue-user.
  678. if err = pr.Issue.ReadBy(ctx.User.ID); err != nil {
  679. ctx.Error(http.StatusInternalServerError, "ReadBy", err)
  680. return
  681. }
  682. }
  683. if pr.Issue.IsClosed {
  684. ctx.NotFound()
  685. return
  686. }
  687. allowedMerge, err := pull_service.IsUserAllowedToMerge(pr, ctx.Repo.Permission, ctx.User)
  688. if err != nil {
  689. ctx.Error(http.StatusInternalServerError, "IsUSerAllowedToMerge", err)
  690. return
  691. }
  692. if !allowedMerge {
  693. ctx.Error(http.StatusMethodNotAllowed, "Merge", "User not allowed to merge PR")
  694. return
  695. }
  696. if pr.HasMerged {
  697. ctx.Error(http.StatusMethodNotAllowed, "PR already merged", "")
  698. return
  699. }
  700. // handle manually-merged mark
  701. if repo_model.MergeStyle(form.Do) == repo_model.MergeStyleManuallyMerged {
  702. if err = pull_service.MergedManually(pr, ctx.User, ctx.Repo.GitRepo, form.MergeCommitID); err != nil {
  703. if models.IsErrInvalidMergeStyle(err) {
  704. ctx.Error(http.StatusMethodNotAllowed, "Invalid merge style", fmt.Errorf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do)))
  705. return
  706. }
  707. if strings.Contains(err.Error(), "Wrong commit ID") {
  708. ctx.JSON(http.StatusConflict, err)
  709. return
  710. }
  711. ctx.Error(http.StatusInternalServerError, "Manually-Merged", err)
  712. return
  713. }
  714. ctx.Status(http.StatusOK)
  715. return
  716. }
  717. if !pr.CanAutoMerge() {
  718. ctx.Error(http.StatusMethodNotAllowed, "PR not in mergeable state", "Please try again later")
  719. return
  720. }
  721. if pr.IsWorkInProgress() {
  722. ctx.Error(http.StatusMethodNotAllowed, "PR is a work in progress", "Work in progress PRs cannot be merged")
  723. return
  724. }
  725. if err := pull_service.CheckPRReadyToMerge(pr, false); err != nil {
  726. if !models.IsErrNotAllowedToMerge(err) {
  727. ctx.Error(http.StatusInternalServerError, "CheckPRReadyToMerge", err)
  728. return
  729. }
  730. if form.ForceMerge != nil && *form.ForceMerge {
  731. if isRepoAdmin, err := models.IsUserRepoAdmin(pr.BaseRepo, ctx.User); err != nil {
  732. ctx.Error(http.StatusInternalServerError, "IsUserRepoAdmin", err)
  733. return
  734. } else if !isRepoAdmin {
  735. ctx.Error(http.StatusMethodNotAllowed, "Merge", "Only repository admin can merge if not all checks are ok (force merge)")
  736. }
  737. } else {
  738. ctx.Error(http.StatusMethodNotAllowed, "PR is not ready to be merged", err)
  739. return
  740. }
  741. }
  742. if _, err := pull_service.IsSignedIfRequired(pr, ctx.User); err != nil {
  743. if !asymkey_service.IsErrWontSign(err) {
  744. ctx.Error(http.StatusInternalServerError, "IsSignedIfRequired", err)
  745. return
  746. }
  747. ctx.Error(http.StatusMethodNotAllowed, fmt.Sprintf("Protected branch %s requires signed commits but this merge would not be signed", pr.BaseBranch), err)
  748. return
  749. }
  750. if len(form.Do) == 0 {
  751. form.Do = string(repo_model.MergeStyleMerge)
  752. }
  753. message := strings.TrimSpace(form.MergeTitleField)
  754. if len(message) == 0 {
  755. if repo_model.MergeStyle(form.Do) == repo_model.MergeStyleMerge {
  756. message = pr.GetDefaultMergeMessage()
  757. }
  758. if repo_model.MergeStyle(form.Do) == repo_model.MergeStyleSquash {
  759. message = pr.GetDefaultSquashMessage()
  760. }
  761. }
  762. form.MergeMessageField = strings.TrimSpace(form.MergeMessageField)
  763. if len(form.MergeMessageField) > 0 {
  764. message += "\n\n" + form.MergeMessageField
  765. }
  766. if err := pull_service.Merge(pr, ctx.User, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), message); err != nil {
  767. if models.IsErrInvalidMergeStyle(err) {
  768. ctx.Error(http.StatusMethodNotAllowed, "Invalid merge style", fmt.Errorf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do)))
  769. return
  770. } else if models.IsErrMergeConflicts(err) {
  771. conflictError := err.(models.ErrMergeConflicts)
  772. ctx.JSON(http.StatusConflict, conflictError)
  773. } else if models.IsErrRebaseConflicts(err) {
  774. conflictError := err.(models.ErrRebaseConflicts)
  775. ctx.JSON(http.StatusConflict, conflictError)
  776. } else if models.IsErrMergeUnrelatedHistories(err) {
  777. conflictError := err.(models.ErrMergeUnrelatedHistories)
  778. ctx.JSON(http.StatusConflict, conflictError)
  779. } else if git.IsErrPushOutOfDate(err) {
  780. ctx.Error(http.StatusConflict, "Merge", "merge push out of date")
  781. return
  782. } else if git.IsErrPushRejected(err) {
  783. errPushRej := err.(*git.ErrPushRejected)
  784. if len(errPushRej.Message) == 0 {
  785. ctx.Error(http.StatusConflict, "Merge", "PushRejected without remote error message")
  786. return
  787. }
  788. ctx.Error(http.StatusConflict, "Merge", "PushRejected with remote message: "+errPushRej.Message)
  789. return
  790. }
  791. ctx.Error(http.StatusInternalServerError, "Merge", err)
  792. return
  793. }
  794. log.Trace("Pull request merged: %d", pr.ID)
  795. if form.DeleteBranchAfterMerge {
  796. var headRepo *git.Repository
  797. if ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == pr.HeadRepoID && ctx.Repo.GitRepo != nil {
  798. headRepo = ctx.Repo.GitRepo
  799. } else {
  800. headRepo, err = git.OpenRepository(pr.HeadRepo.RepoPath())
  801. if err != nil {
  802. ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.HeadRepo.RepoPath()), err)
  803. return
  804. }
  805. defer headRepo.Close()
  806. }
  807. if err := repo_service.DeleteBranch(ctx.User, pr.HeadRepo, headRepo, pr.HeadBranch); err != nil {
  808. switch {
  809. case git.IsErrBranchNotExist(err):
  810. ctx.NotFound(err)
  811. case errors.Is(err, repo_service.ErrBranchIsDefault):
  812. ctx.Error(http.StatusForbidden, "DefaultBranch", fmt.Errorf("can not delete default branch"))
  813. case errors.Is(err, repo_service.ErrBranchIsProtected):
  814. ctx.Error(http.StatusForbidden, "IsProtectedBranch", fmt.Errorf("branch protected"))
  815. default:
  816. ctx.Error(http.StatusInternalServerError, "DeleteBranch", err)
  817. }
  818. return
  819. }
  820. if err := models.AddDeletePRBranchComment(ctx.User, pr.BaseRepo, pr.Issue.ID, pr.HeadBranch); err != nil {
  821. // Do not fail here as branch has already been deleted
  822. log.Error("DeleteBranch: %v", err)
  823. }
  824. }
  825. ctx.Status(http.StatusOK)
  826. }
  827. func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) (*user_model.User, *repo_model.Repository, *git.Repository, *git.CompareInfo, string, string) {
  828. baseRepo := ctx.Repo.Repository
  829. // Get compared branches information
  830. // format: <base branch>...[<head repo>:]<head branch>
  831. // base<-head: master...head:feature
  832. // same repo: master...feature
  833. // TODO: Validate form first?
  834. baseBranch := form.Base
  835. var (
  836. headUser *user_model.User
  837. headBranch string
  838. isSameRepo bool
  839. err error
  840. )
  841. // If there is no head repository, it means pull request between same repository.
  842. headInfos := strings.Split(form.Head, ":")
  843. if len(headInfos) == 1 {
  844. isSameRepo = true
  845. headUser = ctx.Repo.Owner
  846. headBranch = headInfos[0]
  847. } else if len(headInfos) == 2 {
  848. headUser, err = user_model.GetUserByName(headInfos[0])
  849. if err != nil {
  850. if user_model.IsErrUserNotExist(err) {
  851. ctx.NotFound("GetUserByName")
  852. } else {
  853. ctx.Error(http.StatusInternalServerError, "GetUserByName", err)
  854. }
  855. return nil, nil, nil, nil, "", ""
  856. }
  857. headBranch = headInfos[1]
  858. } else {
  859. ctx.NotFound()
  860. return nil, nil, nil, nil, "", ""
  861. }
  862. ctx.Repo.PullRequest.SameRepo = isSameRepo
  863. log.Info("Base branch: %s", baseBranch)
  864. log.Info("Repo path: %s", ctx.Repo.GitRepo.Path)
  865. // Check if base branch is valid.
  866. if !ctx.Repo.GitRepo.IsBranchExist(baseBranch) {
  867. ctx.NotFound("IsBranchExist")
  868. return nil, nil, nil, nil, "", ""
  869. }
  870. // Check if current user has fork of repository or in the same repository.
  871. headRepo := models.GetForkedRepo(headUser.ID, baseRepo.ID)
  872. if headRepo == nil && !isSameRepo {
  873. log.Trace("parseCompareInfo[%d]: does not have fork or in same repository", baseRepo.ID)
  874. ctx.NotFound("GetForkedRepo")
  875. return nil, nil, nil, nil, "", ""
  876. }
  877. var headGitRepo *git.Repository
  878. if isSameRepo {
  879. headRepo = ctx.Repo.Repository
  880. headGitRepo = ctx.Repo.GitRepo
  881. } else {
  882. headGitRepo, err = git.OpenRepository(repo_model.RepoPath(headUser.Name, headRepo.Name))
  883. if err != nil {
  884. ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
  885. return nil, nil, nil, nil, "", ""
  886. }
  887. }
  888. // user should have permission to read baseRepo's codes and pulls, NOT headRepo's
  889. permBase, err := models.GetUserRepoPermission(baseRepo, ctx.User)
  890. if err != nil {
  891. headGitRepo.Close()
  892. ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
  893. return nil, nil, nil, nil, "", ""
  894. }
  895. if !permBase.CanReadIssuesOrPulls(true) || !permBase.CanRead(unit.TypeCode) {
  896. if log.IsTrace() {
  897. log.Trace("Permission Denied: User %-v cannot create/read pull requests or cannot read code in Repo %-v\nUser in baseRepo has Permissions: %-+v",
  898. ctx.User,
  899. baseRepo,
  900. permBase)
  901. }
  902. headGitRepo.Close()
  903. ctx.NotFound("Can't read pulls or can't read UnitTypeCode")
  904. return nil, nil, nil, nil, "", ""
  905. }
  906. // user should have permission to read headrepo's codes
  907. permHead, err := models.GetUserRepoPermission(headRepo, ctx.User)
  908. if err != nil {
  909. headGitRepo.Close()
  910. ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
  911. return nil, nil, nil, nil, "", ""
  912. }
  913. if !permHead.CanRead(unit.TypeCode) {
  914. if log.IsTrace() {
  915. log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v",
  916. ctx.User,
  917. headRepo,
  918. permHead)
  919. }
  920. headGitRepo.Close()
  921. ctx.NotFound("Can't read headRepo UnitTypeCode")
  922. return nil, nil, nil, nil, "", ""
  923. }
  924. // Check if head branch is valid.
  925. if !headGitRepo.IsBranchExist(headBranch) {
  926. headGitRepo.Close()
  927. ctx.NotFound()
  928. return nil, nil, nil, nil, "", ""
  929. }
  930. compareInfo, err := headGitRepo.GetCompareInfo(repo_model.RepoPath(baseRepo.Owner.Name, baseRepo.Name), baseBranch, headBranch, true, false)
  931. if err != nil {
  932. headGitRepo.Close()
  933. ctx.Error(http.StatusInternalServerError, "GetCompareInfo", err)
  934. return nil, nil, nil, nil, "", ""
  935. }
  936. return headUser, headRepo, headGitRepo, compareInfo, baseBranch, headBranch
  937. }
  938. // UpdatePullRequest merge PR's baseBranch into headBranch
  939. func UpdatePullRequest(ctx *context.APIContext) {
  940. // swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/update repository repoUpdatePullRequest
  941. // ---
  942. // summary: Merge PR's baseBranch into headBranch
  943. // produces:
  944. // - application/json
  945. // parameters:
  946. // - name: owner
  947. // in: path
  948. // description: owner of the repo
  949. // type: string
  950. // required: true
  951. // - name: repo
  952. // in: path
  953. // description: name of the repo
  954. // type: string
  955. // required: true
  956. // - name: index
  957. // in: path
  958. // description: index of the pull request to get
  959. // type: integer
  960. // format: int64
  961. // required: true
  962. // - name: style
  963. // in: query
  964. // description: how to update pull request
  965. // type: string
  966. // enum: [merge, rebase]
  967. // responses:
  968. // "200":
  969. // "$ref": "#/responses/empty"
  970. // "403":
  971. // "$ref": "#/responses/forbidden"
  972. // "404":
  973. // "$ref": "#/responses/notFound"
  974. // "409":
  975. // "$ref": "#/responses/error"
  976. // "422":
  977. // "$ref": "#/responses/validationError"
  978. pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  979. if err != nil {
  980. if models.IsErrPullRequestNotExist(err) {
  981. ctx.NotFound()
  982. } else {
  983. ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
  984. }
  985. return
  986. }
  987. if pr.HasMerged {
  988. ctx.Error(http.StatusUnprocessableEntity, "UpdatePullRequest", err)
  989. return
  990. }
  991. if err = pr.LoadIssue(); err != nil {
  992. ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
  993. return
  994. }
  995. if pr.Issue.IsClosed {
  996. ctx.Error(http.StatusUnprocessableEntity, "UpdatePullRequest", err)
  997. return
  998. }
  999. if err = pr.LoadBaseRepo(); err != nil {
  1000. ctx.Error(http.StatusInternalServerError, "LoadBaseRepo", err)
  1001. return
  1002. }
  1003. if err = pr.LoadHeadRepo(); err != nil {
  1004. ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err)
  1005. return
  1006. }
  1007. rebase := ctx.FormString("style") == "rebase"
  1008. allowedUpdateByMerge, allowedUpdateByRebase, err := pull_service.IsUserAllowedToUpdate(pr, ctx.User)
  1009. if err != nil {
  1010. ctx.Error(http.StatusInternalServerError, "IsUserAllowedToMerge", err)
  1011. return
  1012. }
  1013. if (!allowedUpdateByMerge && !rebase) || (rebase && !allowedUpdateByRebase) {
  1014. ctx.Status(http.StatusForbidden)
  1015. return
  1016. }
  1017. // default merge commit message
  1018. message := fmt.Sprintf("Merge branch '%s' into %s", pr.BaseBranch, pr.HeadBranch)
  1019. if err = pull_service.Update(pr, ctx.User, message, rebase); err != nil {
  1020. if models.IsErrMergeConflicts(err) {
  1021. ctx.Error(http.StatusConflict, "Update", "merge failed because of conflict")
  1022. return
  1023. } else if models.IsErrRebaseConflicts(err) {
  1024. ctx.Error(http.StatusConflict, "Update", "rebase failed because of conflict")
  1025. return
  1026. }
  1027. ctx.Error(http.StatusInternalServerError, "pull_service.Update", err)
  1028. return
  1029. }
  1030. ctx.Status(http.StatusOK)
  1031. }
  1032. // GetPullRequestCommits gets all commits associated with a given PR
  1033. func GetPullRequestCommits(ctx *context.APIContext) {
  1034. // swagger:operation GET /repos/{owner}/{repo}/pulls/{index}/commits repository repoGetPullRequestCommits
  1035. // ---
  1036. // summary: Get commits for a pull request
  1037. // produces:
  1038. // - application/json
  1039. // parameters:
  1040. // - name: owner
  1041. // in: path
  1042. // description: owner of the repo
  1043. // type: string
  1044. // required: true
  1045. // - name: repo
  1046. // in: path
  1047. // description: name of the repo
  1048. // type: string
  1049. // required: true
  1050. // - name: index
  1051. // in: path
  1052. // description: index of the pull request to get
  1053. // type: integer
  1054. // format: int64
  1055. // required: true
  1056. // - name: page
  1057. // in: query
  1058. // description: page number of results to return (1-based)
  1059. // type: integer
  1060. // - name: limit
  1061. // in: query
  1062. // description: page size of results
  1063. // type: integer
  1064. // responses:
  1065. // "200":
  1066. // "$ref": "#/responses/CommitList"
  1067. // "404":
  1068. // "$ref": "#/responses/notFound"
  1069. pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  1070. if err != nil {
  1071. if models.IsErrPullRequestNotExist(err) {
  1072. ctx.NotFound()
  1073. } else {
  1074. ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
  1075. }
  1076. return
  1077. }
  1078. if err := pr.LoadBaseRepo(); err != nil {
  1079. ctx.InternalServerError(err)
  1080. return
  1081. }
  1082. var prInfo *git.CompareInfo
  1083. baseGitRepo, err := git.OpenRepository(pr.BaseRepo.RepoPath())
  1084. if err != nil {
  1085. ctx.ServerError("OpenRepository", err)
  1086. return
  1087. }
  1088. defer baseGitRepo.Close()
  1089. if pr.HasMerged {
  1090. prInfo, err = baseGitRepo.GetCompareInfo(pr.BaseRepo.RepoPath(), pr.MergeBase, pr.GetGitRefName(), true, false)
  1091. } else {
  1092. prInfo, err = baseGitRepo.GetCompareInfo(pr.BaseRepo.RepoPath(), pr.BaseBranch, pr.GetGitRefName(), true, false)
  1093. }
  1094. if err != nil {
  1095. ctx.ServerError("GetCompareInfo", err)
  1096. return
  1097. }
  1098. commits := prInfo.Commits
  1099. listOptions := utils.GetListOptions(ctx)
  1100. totalNumberOfCommits := len(commits)
  1101. totalNumberOfPages := int(math.Ceil(float64(totalNumberOfCommits) / float64(listOptions.PageSize)))
  1102. userCache := make(map[string]*user_model.User)
  1103. start, end := listOptions.GetStartEnd()
  1104. if end > totalNumberOfCommits {
  1105. end = totalNumberOfCommits
  1106. }
  1107. apiCommits := make([]*api.Commit, 0, end-start)
  1108. for i := start; i < end; i++ {
  1109. apiCommit, err := convert.ToCommit(ctx.Repo.Repository, commits[i], userCache)
  1110. if err != nil {
  1111. ctx.ServerError("toCommit", err)
  1112. return
  1113. }
  1114. apiCommits = append(apiCommits, apiCommit)
  1115. }
  1116. ctx.SetLinkHeader(totalNumberOfCommits, listOptions.PageSize)
  1117. ctx.SetTotalCountHeader(int64(totalNumberOfCommits))
  1118. ctx.Header().Set("X-Page", strconv.Itoa(listOptions.Page))
  1119. ctx.Header().Set("X-PerPage", strconv.Itoa(listOptions.PageSize))
  1120. ctx.Header().Set("X-PageCount", strconv.Itoa(totalNumberOfPages))
  1121. ctx.Header().Set("X-HasMore", strconv.FormatBool(listOptions.Page < totalNumberOfPages))
  1122. ctx.AppendAccessControlExposeHeaders("X-Page", "X-PerPage", "X-PageCount", "X-HasMore")
  1123. ctx.JSON(http.StatusOK, &apiCommits)
  1124. }