From 0f05470cb84a4dcfd00e69e5af51b4420b74e9d4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kim=20=22BKC=22=20Carlb=C3=A4cker?=
 <kim.carlbacker@gmail.com>
Date: Fri, 2 Dec 2016 12:10:39 +0100
Subject: [PATCH] [API] Pull Requests (#248)

---
 models/error.go             |  22 ++
 models/issue.go             |  16 ++
 models/pull.go              |  84 +++++++
 modules/context/api.go      |  22 ++
 routers/api/v1/api.go       |  15 ++
 routers/api/v1/repo/pull.go | 433 ++++++++++++++++++++++++++++++++++++
 6 files changed, 592 insertions(+)
 create mode 100644 routers/api/v1/repo/pull.go

diff --git a/models/error.go b/models/error.go
index 33815cdc96..d11a9eeb1f 100644
--- a/models/error.go
+++ b/models/error.go
@@ -584,6 +584,28 @@ func (err ErrPullRequestNotExist) Error() string {
 		err.ID, err.IssueID, err.HeadRepoID, err.BaseRepoID, err.HeadBarcnh, err.BaseBranch)
 }
 
+// ErrPullRequestAlreadyExists represents a "PullRequestAlreadyExists"-error
+type ErrPullRequestAlreadyExists struct {
+	ID         int64
+	IssueID    int64
+	HeadRepoID int64
+	BaseRepoID int64
+	HeadBranch string
+	BaseBranch string
+}
+
+// IsErrPullRequestAlreadyExists checks if an error is a ErrPullRequestAlreadyExists.
+func IsErrPullRequestAlreadyExists(err error) bool {
+	_, ok := err.(ErrPullRequestAlreadyExists)
+	return ok
+}
+
+// Error does pretty-printing :D
+func (err ErrPullRequestAlreadyExists) Error() string {
+	return fmt.Sprintf("pull request already exists for these targets [id: %d, issue_id: %d, head_repo_id: %d, base_repo_id: %d, head_branch: %s, base_branch: %s]",
+		err.ID, err.IssueID, err.HeadRepoID, err.BaseRepoID, err.HeadBranch, err.BaseBranch)
+}
+
 // _________                                       __
 // \_   ___ \  ____   _____   _____   ____   _____/  |_
 // /    \  \/ /  _ \ /     \ /     \_/ __ \ /    \   __\
diff --git a/models/issue.go b/models/issue.go
index 990d4a4bc5..7ef3d00e41 100644
--- a/models/issue.go
+++ b/models/issue.go
@@ -170,6 +170,22 @@ func (issue *Issue) HTMLURL() string {
 	return fmt.Sprintf("%s/%s/%d", issue.Repo.HTMLURL(), path, issue.Index)
 }
 
+// DiffURL returns the absolute URL to this diff
+func (issue *Issue) DiffURL() string {
+	if issue.IsPull {
+		return fmt.Sprintf("%s/pulls/%d.diff", issue.Repo.HTMLURL(), issue.Index)
+	}
+	return ""
+}
+
+// PatchURL returns the absolute URL to this patch
+func (issue *Issue) PatchURL() string {
+	if issue.IsPull {
+		return fmt.Sprintf("%s/pulls/%d.patch", issue.Repo.HTMLURL(), issue.Index)
+	}
+	return ""
+}
+
 // State returns string representation of issue status.
 func (issue *Issue) State() api.StateType {
 	if issue.IsClosed {
diff --git a/models/pull.go b/models/pull.go
index eef9a7557b..daed84bf22 100644
--- a/models/pull.go
+++ b/models/pull.go
@@ -121,7 +121,26 @@ func (pr *PullRequest) LoadIssue() (err error) {
 // Required - Issue
 // Optional - Merger
 func (pr *PullRequest) APIFormat() *api.PullRequest {
+
 	apiIssue := pr.Issue.APIFormat()
+	baseBranch, _ := pr.BaseRepo.GetBranch(pr.BaseBranch)
+	baseCommit, _ := baseBranch.GetCommit()
+	headBranch, _ := pr.HeadRepo.GetBranch(pr.HeadBranch)
+	headCommit, _ := headBranch.GetCommit()
+	apiBaseBranchInfo := &api.PRBranchInfo{
+		Name:       pr.BaseBranch,
+		Ref:        pr.BaseBranch,
+		Sha:        baseCommit.ID.String(),
+		RepoID:     pr.BaseRepoID,
+		Repository: pr.BaseRepo.APIFormat(nil),
+	}
+	apiHeadBranchInfo := &api.PRBranchInfo{
+		Name:       pr.HeadBranch,
+		Ref:        pr.HeadBranch,
+		Sha:        headCommit.ID.String(),
+		RepoID:     pr.HeadRepoID,
+		Repository: pr.HeadRepo.APIFormat(nil),
+	}
 	apiPullRequest := &api.PullRequest{
 		ID:        pr.ID,
 		Index:     pr.Index,
@@ -134,7 +153,12 @@ func (pr *PullRequest) APIFormat() *api.PullRequest {
 		State:     apiIssue.State,
 		Comments:  apiIssue.Comments,
 		HTMLURL:   pr.Issue.HTMLURL(),
+		DiffURL:   pr.Issue.DiffURL(),
+		PatchURL:  pr.Issue.PatchURL(),
 		HasMerged: pr.HasMerged,
+		Base:      apiBaseBranchInfo,
+		Head:      apiHeadBranchInfo,
+		MergeBase: pr.MergeBase,
 	}
 
 	if pr.Status != PullRequestStatusChecking {
@@ -472,6 +496,46 @@ func NewPullRequest(repo *Repository, pull *Issue, labelIDs []int64, uuids []str
 	return nil
 }
 
+// PullRequestsOptions holds the options for PRs
+type PullRequestsOptions struct {
+	Page        int
+	State       string
+	SortType    string
+	Labels      []string
+	MilestoneID int64
+}
+
+func listPullRequestStatement(baseRepoID int64, opts *PullRequestsOptions) *xorm.Session {
+	sess := x.Where("pull_request.base_repo_id=?", baseRepoID)
+
+	sess.Join("INNER", "issue", "pull_request.issue_id = issue.id")
+	switch opts.State {
+	case "closed", "open":
+		sess.And("issue.is_closed=?", opts.State == "closed")
+	}
+
+	return sess
+}
+
+// PullRequests returns all pull requests for a base Repo by the given conditions
+func PullRequests(baseRepoID int64, opts *PullRequestsOptions) ([]*PullRequest, int64, error) {
+	if opts.Page <= 0 {
+		opts.Page = 1
+	}
+
+	countSession := listPullRequestStatement(baseRepoID, opts)
+	maxResults, err := countSession.Count(new(PullRequest))
+	if err != nil {
+		log.Error(4, "Count PRs", err)
+		return nil, maxResults, err
+	}
+
+	prs := make([]*PullRequest, 0, ItemsPerPage)
+	findSession := listPullRequestStatement(baseRepoID, opts)
+	findSession.Limit(ItemsPerPage, (opts.Page-1)*ItemsPerPage)
+	return prs, maxResults, findSession.Find(&prs)
+}
+
 // GetUnmergedPullRequest returnss a pull request that is open and has not been merged
 // by given head/base and repo/branch.
 func GetUnmergedPullRequest(headRepoID, baseRepoID int64, headBranch, baseBranch string) (*PullRequest, error) {
@@ -512,6 +576,26 @@ func GetUnmergedPullRequestsByBaseInfo(repoID int64, branch string) ([]*PullRequ
 		Find(&prs)
 }
 
+// GetPullRequestByIndex returns a pull request by the given index
+func GetPullRequestByIndex(repoID int64, index int64) (*PullRequest, error) {
+	pr := &PullRequest{
+		BaseRepoID: repoID,
+		Index:      index,
+	}
+
+	has, err := x.Get(pr)
+	if err != nil {
+		return nil, err
+	} else if !has {
+		return nil, ErrPullRequestNotExist{0, repoID, index, 0, "", ""}
+	}
+
+	pr.LoadAttributes()
+	pr.LoadIssue()
+
+	return pr, nil
+}
+
 func getPullRequestByID(e Engine, id int64) (*PullRequest, error) {
 	pr := new(PullRequest)
 	has, err := e.Id(id).Get(pr)
diff --git a/modules/context/api.go b/modules/context/api.go
index 7706e9c499..7a3ff990b6 100644
--- a/modules/context/api.go
+++ b/modules/context/api.go
@@ -8,6 +8,7 @@ import (
 	"fmt"
 	"strings"
 
+	"code.gitea.io/git"
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/log"
@@ -103,3 +104,24 @@ func ExtractOwnerAndRepo() macaron.Handler {
 		ctx.Data["Repository"] = repo
 	}
 }
+
+// ReferencesGitRepo injects the GitRepo into the Context
+func ReferencesGitRepo() macaron.Handler {
+	return func(ctx *APIContext) {
+		// Empty repository does not have reference information.
+		if ctx.Repo.Repository.IsBare {
+			return
+		}
+
+		// For API calls.
+		if ctx.Repo.GitRepo == nil {
+			repoPath := models.RepoPath(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
+			gitRepo, err := git.OpenRepository(repoPath)
+			if err != nil {
+				ctx.Error(500, "RepoRef Invalid repo "+repoPath, err)
+				return
+			}
+			ctx.Repo.GitRepo = gitRepo
+		}
+	}
+}
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index 506a615624..b920a23593 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -167,6 +167,13 @@ func mustEnableIssues(ctx *context.APIContext) {
 	}
 }
 
+func mustAllowPulls(ctx *context.Context) {
+	if !ctx.Repo.Repository.AllowsPulls() {
+		ctx.Status(404)
+		return
+	}
+}
+
 // RegisterRoutes registers all v1 APIs routes to web application.
 // FIXME: custom form error response
 func RegisterRoutes(m *macaron.Macaron) {
@@ -305,6 +312,14 @@ func RegisterRoutes(m *macaron.Macaron) {
 						Delete(reqRepoWriter(), repo.DeleteMilestone)
 				})
 				m.Get("/editorconfig/:filename", context.RepoRef(), repo.GetEditorconfig)
+				m.Group("/pulls", func() {
+					m.Combo("").Get(bind(api.ListPullRequestsOptions{}), repo.ListPullRequests).Post(reqRepoWriter(), bind(api.CreatePullRequestOption{}), repo.CreatePullRequest)
+					m.Group("/:index", func() {
+						m.Combo("").Get(repo.GetPullRequest).Patch(reqRepoWriter(), bind(api.EditPullRequestOption{}), repo.EditPullRequest)
+						m.Combo("/merge").Get(repo.IsPullRequestMerged).Post(reqRepoWriter(), repo.MergePullRequest)
+					})
+
+				}, mustAllowPulls, context.ReferencesGitRepo())
 			}, repoAssignment())
 		}, reqToken())
 
diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go
new file mode 100644
index 0000000000..4c10024bfb
--- /dev/null
+++ b/routers/api/v1/repo/pull.go
@@ -0,0 +1,433 @@
+// Copyright 2016 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package repo
+
+import (
+	"fmt"
+	"strings"
+
+	"code.gitea.io/git"
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/log"
+
+	api "code.gitea.io/sdk/gitea"
+)
+
+// ListPullRequests returns a list of all PRs
+func ListPullRequests(ctx *context.APIContext, form api.ListPullRequestsOptions) {
+	prs, maxResults, err := models.PullRequests(ctx.Repo.Repository.ID, &models.PullRequestsOptions{
+		Page:        ctx.QueryInt("page"),
+		State:       ctx.QueryTrim("state"),
+		SortType:    ctx.QueryTrim("sort"),
+		Labels:      ctx.QueryStrings("labels"),
+		MilestoneID: ctx.QueryInt64("milestone"),
+	})
+
+	/*prs, maxResults, err := models.PullRequests(ctx.Repo.Repository.ID, &models.PullRequestsOptions{
+		Page:  form.Page,
+		State: form.State,
+	})*/
+	if err != nil {
+		ctx.Error(500, "PullRequests", err)
+		return
+	}
+
+	apiPrs := make([]*api.PullRequest, len(prs))
+	for i := range prs {
+		prs[i].LoadIssue()
+		prs[i].LoadAttributes()
+		prs[i].GetBaseRepo()
+		prs[i].GetHeadRepo()
+		apiPrs[i] = prs[i].APIFormat()
+	}
+
+	ctx.SetLinkHeader(int(maxResults), models.ItemsPerPage)
+	ctx.JSON(200, &apiPrs)
+}
+
+// GetPullRequest returns a single PR based on index
+func GetPullRequest(ctx *context.APIContext) {
+	pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	if err != nil {
+		if models.IsErrPullRequestNotExist(err) {
+			ctx.Status(404)
+		} else {
+			ctx.Error(500, "GetPullRequestByIndex", err)
+		}
+		return
+	}
+
+	pr.GetBaseRepo()
+	pr.GetHeadRepo()
+	ctx.JSON(200, pr.APIFormat())
+}
+
+// CreatePullRequest does what it says
+func CreatePullRequest(ctx *context.APIContext, form api.CreatePullRequestOption) {
+	var (
+		repo        = ctx.Repo.Repository
+		labelIDs    []int64
+		assigneeID  int64
+		milestoneID int64
+	)
+
+	// Get repo/branch information
+	headUser, headRepo, headGitRepo, prInfo, baseBranch, headBranch := parseCompareInfo(ctx, form)
+	if ctx.Written() {
+		return
+	}
+
+	// Check if another PR exists with the same targets
+	existingPr, err := models.GetUnmergedPullRequest(headRepo.ID, ctx.Repo.Repository.ID, headBranch, baseBranch)
+	if err != nil {
+		if !models.IsErrPullRequestNotExist(err) {
+			ctx.Error(500, "GetUnmergedPullRequest", err)
+			return
+		}
+	} else {
+		err = models.ErrPullRequestAlreadyExists{
+			ID:         existingPr.ID,
+			IssueID:    existingPr.Index,
+			HeadRepoID: existingPr.HeadRepoID,
+			BaseRepoID: existingPr.BaseRepoID,
+			HeadBranch: existingPr.HeadBranch,
+			BaseBranch: existingPr.BaseBranch,
+		}
+		ctx.Error(409, "GetUnmergedPullRequest", err)
+		return
+	}
+
+	if len(form.Labels) > 0 {
+		labels, err := models.GetLabelsInRepoByIDs(ctx.Repo.Repository.ID, form.Labels)
+		if err != nil {
+			ctx.Error(500, "GetLabelsInRepoByIDs", err)
+			return
+		}
+
+		labelIDs = make([]int64, len(labels))
+		for i := range labels {
+			labelIDs[i] = labels[i].ID
+		}
+	}
+
+	if form.Milestone > 0 {
+		milestone, err := models.GetMilestoneByRepoID(ctx.Repo.Repository.ID, milestoneID)
+		if err != nil {
+			if models.IsErrMilestoneNotExist(err) {
+				ctx.Status(404)
+			} else {
+				ctx.Error(500, "GetMilestoneByRepoID", err)
+			}
+			return
+		}
+
+		milestoneID = milestone.ID
+	}
+
+	if len(form.Assignee) > 0 {
+		assigneeUser, err := models.GetUserByName(form.Assignee)
+		if err != nil {
+			if models.IsErrUserNotExist(err) {
+				ctx.Error(422, "", fmt.Sprintf("assignee does not exist: [name: %s]", form.Assignee))
+			} else {
+				ctx.Error(500, "GetUserByName", err)
+			}
+			return
+		}
+
+		assignee, err := repo.GetAssigneeByID(assigneeUser.ID)
+		if err != nil {
+			ctx.Error(500, "GetAssigneeByID", err)
+			return
+		}
+
+		assigneeID = assignee.ID
+	}
+
+	patch, err := headGitRepo.GetPatch(prInfo.MergeBase, headBranch)
+	if err != nil {
+		ctx.Error(500, "GetPatch", err)
+		return
+	}
+
+	prIssue := &models.Issue{
+		RepoID:      repo.ID,
+		Index:       repo.NextIssueIndex(),
+		Title:       form.Title,
+		PosterID:    ctx.User.ID,
+		Poster:      ctx.User,
+		MilestoneID: milestoneID,
+		AssigneeID:  assigneeID,
+		IsPull:      true,
+		Content:     form.Body,
+	}
+	pr := &models.PullRequest{
+		HeadRepoID:   headRepo.ID,
+		BaseRepoID:   repo.ID,
+		HeadUserName: headUser.Name,
+		HeadBranch:   headBranch,
+		BaseBranch:   baseBranch,
+		HeadRepo:     headRepo,
+		BaseRepo:     repo,
+		MergeBase:    prInfo.MergeBase,
+		Type:         models.PullRequestGitea,
+	}
+
+	if err := models.NewPullRequest(repo, prIssue, labelIDs, []string{}, pr, patch); err != nil {
+		ctx.Error(500, "NewPullRequest", err)
+		return
+	} else if err := pr.PushToBaseRepo(); err != nil {
+		ctx.Error(500, "PushToBaseRepo", err)
+		return
+	}
+
+	log.Trace("Pull request created: %d/%d", repo.ID, prIssue.ID)
+	ctx.JSON(201, pr.APIFormat())
+}
+
+// EditPullRequest does what it says
+func EditPullRequest(ctx *context.APIContext, form api.EditPullRequestOption) {
+	pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	if err != nil {
+		if models.IsErrPullRequestNotExist(err) {
+			ctx.Status(404)
+		} else {
+			ctx.Error(500, "GetPullRequestByIndex", err)
+		}
+		return
+	}
+
+	pr.LoadIssue()
+	issue := pr.Issue
+
+	if !issue.IsPoster(ctx.User.ID) && !ctx.Repo.IsWriter() {
+		ctx.Status(403)
+		return
+	}
+
+	if len(form.Title) > 0 {
+		issue.Title = form.Title
+	}
+	if len(form.Body) > 0 {
+		issue.Content = form.Body
+	}
+
+	if ctx.Repo.IsWriter() && len(form.Assignee) > 0 &&
+		(issue.Assignee == nil || issue.Assignee.LowerName != strings.ToLower(form.Assignee)) {
+		if len(form.Assignee) == 0 {
+			issue.AssigneeID = 0
+		} else {
+			assignee, err := models.GetUserByName(form.Assignee)
+			if err != nil {
+				if models.IsErrUserNotExist(err) {
+					ctx.Error(422, "", fmt.Sprintf("assignee does not exist: [name: %s]", form.Assignee))
+				} else {
+					ctx.Error(500, "GetUserByName", err)
+				}
+				return
+			}
+			issue.AssigneeID = assignee.ID
+		}
+
+		if err = models.UpdateIssueUserByAssignee(issue); err != nil {
+			ctx.Error(500, "UpdateIssueUserByAssignee", err)
+			return
+		}
+	}
+	if ctx.Repo.IsWriter() && form.Milestone != 0 &&
+		issue.MilestoneID != form.Milestone {
+		oldMilestoneID := issue.MilestoneID
+		issue.MilestoneID = form.Milestone
+		if err = models.ChangeMilestoneAssign(issue, oldMilestoneID); err != nil {
+			ctx.Error(500, "ChangeMilestoneAssign", err)
+			return
+		}
+	}
+
+	if err = models.UpdateIssue(issue); err != nil {
+		ctx.Error(500, "UpdateIssue", err)
+		return
+	}
+	if form.State != nil {
+		if err = issue.ChangeStatus(ctx.User, ctx.Repo.Repository, api.StateClosed == api.StateType(*form.State)); err != nil {
+			ctx.Error(500, "ChangeStatus", err)
+			return
+		}
+	}
+
+	// Refetch from database
+	pr, err = models.GetPullRequestByIndex(ctx.Repo.Repository.ID, pr.Index)
+	if err != nil {
+		if models.IsErrPullRequestNotExist(err) {
+			ctx.Status(404)
+		} else {
+			ctx.Error(500, "GetPullRequestByIndex", err)
+		}
+		return
+	}
+
+	ctx.JSON(201, pr.APIFormat())
+}
+
+// IsPullRequestMerged checks if a PR exists given an index
+//  - Returns 204 if it exists
+//    Otherwise 404
+func IsPullRequestMerged(ctx *context.APIContext) {
+	pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	if err != nil {
+		if models.IsErrPullRequestNotExist(err) {
+			ctx.Status(404)
+		} else {
+			ctx.Error(500, "GetPullRequestByIndex", err)
+		}
+		return
+	}
+
+	if pr.HasMerged {
+		ctx.Status(204)
+	}
+	ctx.Status(404)
+}
+
+// MergePullRequest merges a PR given an index
+func MergePullRequest(ctx *context.APIContext) {
+	pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	if err != nil {
+		if models.IsErrPullRequestNotExist(err) {
+			ctx.Handle(404, "GetPullRequestByIndex", err)
+		} else {
+			ctx.Error(500, "GetPullRequestByIndex", err)
+		}
+		return
+	}
+
+	if err = pr.GetHeadRepo(); err != nil {
+		ctx.Handle(500, "GetHeadRepo", err)
+		return
+	}
+
+	pr.LoadIssue()
+	pr.Issue.Repo = ctx.Repo.Repository
+
+	if ctx.IsSigned {
+		// Update issue-user.
+		if err = pr.Issue.ReadBy(ctx.User.ID); err != nil {
+			ctx.Error(500, "ReadBy", err)
+			return
+		}
+	}
+
+	if pr.Issue.IsClosed {
+		ctx.Status(404)
+		return
+	}
+
+	if !pr.CanAutoMerge() || pr.HasMerged {
+		ctx.Status(405)
+		return
+	}
+
+	if err := pr.Merge(ctx.User, ctx.Repo.GitRepo); err != nil {
+		ctx.Error(500, "Merge", err)
+		return
+	}
+
+	log.Trace("Pull request merged: %d", pr.ID)
+	ctx.Status(200)
+}
+
+func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) (*models.User, *models.Repository, *git.Repository, *git.PullRequestInfo, string, string) {
+	baseRepo := ctx.Repo.Repository
+
+	// Get compared branches information
+	// format: <base branch>...[<head repo>:]<head branch>
+	// base<-head: master...head:feature
+	// same repo: master...feature
+
+	// TODO: Validate form first?
+
+	baseBranch := form.Base
+
+	var (
+		headUser   *models.User
+		headBranch string
+		isSameRepo bool
+		err        error
+	)
+
+	// If there is no head repository, it means pull request between same repository.
+	headInfos := strings.Split(form.Head, ":")
+	if len(headInfos) == 1 {
+		isSameRepo = true
+		headUser = ctx.Repo.Owner
+		headBranch = headInfos[0]
+
+	} else if len(headInfos) == 2 {
+		headUser, err = models.GetUserByName(headInfos[0])
+		if err != nil {
+			if models.IsErrUserNotExist(err) {
+				ctx.Handle(404, "GetUserByName", nil)
+			} else {
+				ctx.Handle(500, "GetUserByName", err)
+			}
+			return nil, nil, nil, nil, "", ""
+		}
+		headBranch = headInfos[1]
+
+	} else {
+		ctx.Status(404)
+		return nil, nil, nil, nil, "", ""
+	}
+
+	ctx.Repo.PullRequest.SameRepo = isSameRepo
+	log.Info("Base branch: %s", baseBranch)
+	log.Info("Repo path: %s", ctx.Repo.GitRepo.Path)
+	// Check if base branch is valid.
+	if !ctx.Repo.GitRepo.IsBranchExist(baseBranch) {
+		ctx.Status(404)
+		return nil, nil, nil, nil, "", ""
+	}
+
+	// Check if current user has fork of repository or in the same repository.
+	headRepo, has := models.HasForkedRepo(headUser.ID, baseRepo.ID)
+	if !has && !isSameRepo {
+		log.Trace("parseCompareInfo[%d]: does not have fork or in same repository", baseRepo.ID)
+		ctx.Status(404)
+		return nil, nil, nil, nil, "", ""
+	}
+
+	var headGitRepo *git.Repository
+	if isSameRepo {
+		headRepo = ctx.Repo.Repository
+		headGitRepo = ctx.Repo.GitRepo
+	} else {
+		headGitRepo, err = git.OpenRepository(models.RepoPath(headUser.Name, headRepo.Name))
+		if err != nil {
+			ctx.Error(500, "OpenRepository", err)
+			return nil, nil, nil, nil, "", ""
+		}
+	}
+
+	if !ctx.User.IsWriterOfRepo(headRepo) && !ctx.User.IsAdmin {
+		log.Trace("ParseCompareInfo[%d]: does not have write access or site admin", baseRepo.ID)
+		ctx.Status(404)
+		return nil, nil, nil, nil, "", ""
+	}
+
+	// Check if head branch is valid.
+	if !headGitRepo.IsBranchExist(headBranch) {
+		ctx.Status(404)
+		return nil, nil, nil, nil, "", ""
+	}
+
+	prInfo, err := headGitRepo.GetPullRequestInfo(models.RepoPath(baseRepo.Owner.Name, baseRepo.Name), baseBranch, headBranch)
+	if err != nil {
+		ctx.Error(500, "GetPullRequestInfo", err)
+		return nil, nil, nil, nil, "", ""
+	}
+
+	return headUser, headRepo, headGitRepo, prInfo, baseBranch, headBranch
+}