Browse Source

work on PR conversation

Unknwon 9 years ago
parent
commit
8c046073a8

+ 3 - 2
cmd/web.go

@@ -513,13 +513,14 @@ func runWeb(ctx *cli.Context) {
 			m.Post("/edit/:tagname", bindIgnErr(auth.EditReleaseForm{}), repo.EditReleasePost)
 		}, reqRepoAdmin, middleware.RepoRef())
 
-		m.Combo("/compare/*").Get(repo.CompareAndPullRequest)
+		m.Combo("/compare/*").Get(repo.CompareAndPullRequest).
+			Post(bindIgnErr(auth.CreateIssueForm{}), repo.CompareAndPullRequestPost)
 	}, reqSignIn, middleware.RepoAssignment(true))
 
 	m.Group("/:username/:reponame", func() {
 		m.Get("/releases", middleware.RepoRef(), repo.Releases)
 		m.Get("/issues", repo.RetrieveLabels, repo.Issues)
-		m.Get("/issues/:index", repo.ViewIssue)
+		m.Get("/:type(issues|pulls)/:index", repo.ViewIssue)
 		m.Get("/labels/", repo.RetrieveLabels, repo.Labels)
 		m.Get("/milestones", repo.Milestones)
 		m.Get("/pulls", repo.Pulls)

+ 3 - 0
conf/locale/locale_en-US.ini

@@ -464,6 +464,9 @@ pulls.compare_changes = Compare Changes
 pulls.compare_changes_desc = Compare two branches and make a pull request for changes.
 pulls.no_results = No results found.
 pulls.create = Create Pull Request
+pulls.tab_conversation = Conversation
+pulls.tab_commits = Commits
+pulls.tab_files = Files changed
 
 milestones.new = New Milestone
 milestones.open_tab = %d Open

+ 21 - 0
models/error.go

@@ -281,6 +281,27 @@ func (err ErrIssueNotExist) Error() string {
 	return fmt.Sprintf("issue does not exist [id: %d, repo_id: %d, index: %d]", err.ID, err.RepoID, err.Index)
 }
 
+// __________      .__  .__ __________                                     __
+// \______   \__ __|  | |  |\______   \ ____  ________ __   ____   _______/  |_
+//  |     ___/  |  \  | |  | |       _// __ \/ ____/  |  \_/ __ \ /  ___/\   __\
+//  |    |   |  |  /  |_|  |_|    |   \  ___< <_|  |  |  /\  ___/ \___ \  |  |
+//  |____|   |____/|____/____/____|_  /\___  >__   |____/  \___  >____  > |__|
+//                                  \/     \/   |__|           \/     \/
+
+type ErrPullRepoNotExist struct {
+	ID     int64
+	PullID int64
+}
+
+func IsErrPullRepoNotExist(err error) bool {
+	_, ok := err.(ErrPullRepoNotExist)
+	return ok
+}
+
+func (err ErrPullRepoNotExist) Error() string {
+	return fmt.Sprintf("pull repo does not exist [id: %d, pull_id: %d]", err.ID, err.PullID)
+}
+
 // _________                                       __
 // \_   ___ \  ____   _____   _____   ____   _____/  |_
 // /    \  \/ /  _ \ /     \ /     \_/ __ \ /    \   __\

+ 159 - 31
models/issue.go

@@ -9,6 +9,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"io/ioutil"
 	"mime/multipart"
 	"os"
 	"path"
@@ -21,6 +22,7 @@ import (
 
 	"github.com/gogits/gogs/modules/base"
 	"github.com/gogits/gogs/modules/log"
+	"github.com/gogits/gogs/modules/process"
 	"github.com/gogits/gogs/modules/setting"
 	gouuid "github.com/gogits/gogs/modules/uuid"
 )
@@ -44,9 +46,10 @@ type Issue struct {
 	MilestoneID     int64
 	Milestone       *Milestone `xorm:"-"`
 	AssigneeID      int64
-	Assignee        *User `xorm:"-"`
-	IsRead          bool  `xorm:"-"`
-	IsPull          bool  // Indicates whether is a pull request or not.
+	Assignee        *User     `xorm:"-"`
+	IsRead          bool      `xorm:"-"`
+	IsPull          bool      // Indicates whether is a pull request or not.
+	PullRepo        *PullRepo `xorm:"-"`
 	IsClosed        bool
 	Content         string `xorm:"TEXT"`
 	RenderedContent string `xorm:"-"`
@@ -92,6 +95,11 @@ func (i *Issue) AfterSet(colName string, _ xorm.Cell) {
 		if err != nil {
 			log.Error(3, "GetUserByID[%d]: %v", i.ID, err)
 		}
+	case "is_pull":
+		i.PullRepo, err = GetPullRepoByPullID(i.ID)
+		if err != nil {
+			log.Error(3, "GetPullRepoByPullID[%d]: %v", i.ID, err)
+		}
 	case "created":
 		i.Created = regulateTimeZone(i.Created)
 	}
@@ -273,30 +281,11 @@ func (i *Issue) ChangeStatus(doer *User, isClosed bool) (err error) {
 	return sess.Commit()
 }
 
-// CreateIssue creates new issue with labels for repository.
-func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) {
-	// Check attachments.
-	attachments := make([]*Attachment, 0, len(uuids))
-	for _, uuid := range uuids {
-		attach, err := GetAttachmentByUUID(uuid)
-		if err != nil {
-			if IsErrAttachmentNotExist(err) {
-				continue
-			}
-			return fmt.Errorf("GetAttachmentByUUID[%s]: %v", uuid, err)
-		}
-		attachments = append(attachments, attach)
-	}
-
-	sess := x.NewSession()
-	defer sessionRelease(sess)
-	if err = sess.Begin(); err != nil {
-		return err
-	}
-
-	if _, err = sess.Insert(issue); err != nil {
+// It's caller's responsibility to create action.
+func newIssue(e *xorm.Session, repo *Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) {
+	if _, err = e.Insert(issue); err != nil {
 		return err
-	} else if _, err = sess.Exec("UPDATE `repository` SET num_issues=num_issues+1 WHERE id=?", issue.RepoID); err != nil {
+	} else if _, err = e.Exec("UPDATE `repository` SET num_issues=num_issues+1 WHERE id=?", issue.RepoID); err != nil {
 		return err
 	}
 
@@ -306,34 +295,62 @@ func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string)
 			continue
 		}
 
-		label, err = getLabelByID(sess, id)
+		label, err = getLabelByID(e, id)
 		if err != nil {
 			return err
 		}
-		if err = issue.addLabel(sess, label); err != nil {
+		if err = issue.addLabel(e, label); err != nil {
 			return fmt.Errorf("addLabel: %v", err)
 		}
 
 	}
 
 	if issue.MilestoneID > 0 {
-		if err = changeMilestoneAssign(sess, 0, issue); err != nil {
+		if err = changeMilestoneAssign(e, 0, issue); err != nil {
 			return err
 		}
 	}
 
-	if err = newIssueUsers(sess, repo, issue); err != nil {
+	if err = newIssueUsers(e, repo, issue); err != nil {
 		return err
 	}
 
+	// Check attachments.
+	attachments := make([]*Attachment, 0, len(uuids))
+	for _, uuid := range uuids {
+		attach, err := getAttachmentByUUID(e, uuid)
+		if err != nil {
+			if IsErrAttachmentNotExist(err) {
+				continue
+			}
+			return fmt.Errorf("getAttachmentByUUID[%s]: %v", uuid, err)
+		}
+		attachments = append(attachments, attach)
+	}
+
 	for i := range attachments {
 		attachments[i].IssueID = issue.ID
 		// No assign value could be 0, so ignore AllCols().
-		if _, err = sess.Id(attachments[i].ID).Update(attachments[i]); err != nil {
+		if _, err = e.Id(attachments[i].ID).Update(attachments[i]); err != nil {
 			return fmt.Errorf("update attachment[%d]: %v", attachments[i].ID, err)
 		}
 	}
 
+	return nil
+}
+
+// NewIssue creates new issue with labels for repository.
+func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) {
+	sess := x.NewSession()
+	defer sessionRelease(sess)
+	if err = sess.Begin(); err != nil {
+		return err
+	}
+
+	if err = newIssue(sess, repo, issue, labelIDs, uuids); err != nil {
+		return fmt.Errorf("newIssue: %v", err)
+	}
+
 	// Notify watchers.
 	act := &Action{
 		ActUserID:    issue.Poster.Id,
@@ -813,6 +830,117 @@ func UpdateIssueUsersByMentions(uids []int64, iid int64) error {
 	return nil
 }
 
+// __________      .__  .__ __________                                     __
+// \______   \__ __|  | |  |\______   \ ____  ________ __   ____   _______/  |_
+//  |     ___/  |  \  | |  | |       _// __ \/ ____/  |  \_/ __ \ /  ___/\   __\
+//  |    |   |  |  /  |_|  |_|    |   \  ___< <_|  |  |  /\  ___/ \___ \  |  |
+//  |____|   |____/|____/____/____|_  /\___  >__   |____/  \___  >____  > |__|
+//                                  \/     \/   |__|           \/     \/
+
+type PullRequestType int
+
+const (
+	PULL_REQUEST_GOGS = iota
+	PLLL_ERQUEST_GIT
+)
+
+// PullRepo represents relation between pull request and repositories.
+type PullRepo struct {
+	ID           int64       `xorm:"pk autoincr"`
+	PullID       int64       `xorm:"INDEX"`
+	HeadRepoID   int64       `xorm:"UNIQUE(s)"`
+	HeadRepo     *Repository `xorm:"-"`
+	BaseRepoID   int64       `xorm:"UNIQUE(s)"`
+	HeadBarcnh   string      `xorm:"UNIQUE(s)"`
+	BaseBranch   string      `xorm:"UNIQUE(s)"`
+	MergeBase    string      `xorm:"VARCHAR(40)"`
+	Type         PullRequestType
+	CanAutoMerge bool
+}
+
+func (pr *PullRepo) AfterSet(colName string, _ xorm.Cell) {
+	var err error
+	switch colName {
+	case "head_repo_id":
+		pr.HeadRepo, err = GetRepositoryByID(pr.HeadRepoID)
+		if err != nil {
+			log.Error(3, "GetRepositoryByID[%d]: %v", pr.ID, err)
+		}
+	}
+}
+
+// NewPullRequest creates new pull request with labels for repository.
+func NewPullRequest(repo *Repository, pr *Issue, labelIDs []int64, uuids []string, pullRepo *PullRepo, patch []byte) (err error) {
+
+	sess := x.NewSession()
+	defer sessionRelease(sess)
+	if err = sess.Begin(); err != nil {
+		return err
+	}
+
+	if err = newIssue(sess, repo, pr, labelIDs, uuids); err != nil {
+		return fmt.Errorf("newIssue: %v", err)
+	}
+
+	// Notify watchers.
+	act := &Action{
+		ActUserID:    pr.Poster.Id,
+		ActUserName:  pr.Poster.Name,
+		ActEmail:     pr.Poster.Email,
+		OpType:       PULL_REQUEST,
+		Content:      fmt.Sprintf("%d|%s", pr.Index, pr.Name),
+		RepoID:       repo.ID,
+		RepoUserName: repo.Owner.Name,
+		RepoName:     repo.Name,
+		IsPrivate:    repo.IsPrivate,
+	}
+	if err = notifyWatchers(sess, act); err != nil {
+		return err
+	}
+
+	// Test apply patch.
+	repoPath, err := repo.RepoPath()
+	if err != nil {
+		return fmt.Errorf("RepoPath: %v", err)
+	}
+	patchPath := path.Join(repoPath, "pulls", com.ToStr(pr.ID)+".patch")
+
+	os.MkdirAll(path.Dir(patchPath), os.ModePerm)
+	if err = ioutil.WriteFile(patchPath, patch, 0644); err != nil {
+		return fmt.Errorf("save patch: %v", err)
+	}
+	defer os.Remove(patchPath)
+
+	stdout, stderr, err := process.ExecDir(-1, repoPath,
+		fmt.Sprintf("NewPullRequest(git apply --check): %d", repo.ID),
+		"git", "apply", "--check", "-v", patchPath)
+	if err != nil {
+		if strings.Contains(stderr, "fatal:") {
+			return fmt.Errorf("git apply --check: %v - %s", err, stderr)
+		}
+	}
+	pullRepo.CanAutoMerge = !strings.Contains(stdout, "error: patch failed:")
+
+	pullRepo.PullID = pr.ID
+	if _, err = sess.Insert(pullRepo); err != nil {
+		return fmt.Errorf("insert pull repo: %v", err)
+	}
+
+	return sess.Commit()
+}
+
+// GetPullRepoByPullID returns pull repo by given pull ID.
+func GetPullRepoByPullID(pullID int64) (*PullRepo, error) {
+	pullRepo := new(PullRepo)
+	has, err := x.Where("pull_id=?", pullID).Get(pullRepo)
+	if err != nil {
+		return nil, err
+	} else if !has {
+		return nil, ErrPullRepoNotExist{0, pullID}
+	}
+	return pullRepo, nil
+}
+
 // .____          ___.          .__
 // |    |   _____ \_ |__   ____ |  |
 // |    |   \__  \ | __ \_/ __ \|  |

+ 2 - 2
models/models.go

@@ -78,8 +78,8 @@ func init() {
 	tables = append(tables,
 		new(User), new(PublicKey), new(Oauth2), new(AccessToken),
 		new(Repository), new(DeployKey), new(Collaboration), new(Access),
-		new(Watch), new(Star), new(ForkInfo), new(Follow), new(Action),
-		new(Issue), new(Comment), new(Attachment), new(IssueUser),
+		new(Watch), new(Star), new(Follow), new(Action),
+		new(Issue), new(PullRepo), new(Comment), new(Attachment), new(IssueUser),
 		new(Label), new(IssueLabel), new(Milestone),
 		new(Mirror), new(Release), new(LoginSource), new(Webhook),
 		new(UpdateTask), new(HookTask),

+ 0 - 29
models/repo.go

@@ -160,7 +160,6 @@ type Repository struct {
 	IsFork   bool `xorm:"NOT NULL DEFAULT false"`
 	ForkID   int64
 	BaseRepo *Repository `xorm:"-"`
-	ForkInfo *ForkInfo   `xorm:"-"`
 
 	Created time.Time `xorm:"CREATED"`
 	Updated time.Time `xorm:"UPDATED"`
@@ -168,15 +167,6 @@ type Repository struct {
 
 func (repo *Repository) AfterSet(colName string, _ xorm.Cell) {
 	switch colName {
-	case "is_fork":
-		forkInfo := new(ForkInfo)
-		has, err := x.Where("repo_id=?", repo.ID).Get(forkInfo)
-		if err != nil {
-			log.Error(3, "get fork in[%d]: %v", repo.ID, err)
-			return
-		} else if has {
-			repo.ForkInfo = forkInfo
-		}
 	case "updated":
 		repo.Updated = regulateTimeZone(repo.Updated)
 	}
@@ -1047,8 +1037,6 @@ func DeleteRepository(uid, repoID int64) error {
 	if repo.IsFork {
 		if _, err = sess.Exec("UPDATE `repository` SET num_forks=num_forks-1 WHERE id=?", repo.ForkID); err != nil {
 			return fmt.Errorf("decrease fork count: %v", err)
-		} else if _, err = sess.Delete(&ForkInfo{RepoID: repo.ID}); err != nil {
-			return fmt.Errorf("delete fork info: %v", err)
 		}
 	}
 
@@ -1095,9 +1083,6 @@ func DeleteRepository(uid, repoID int64) error {
 			if _, err = x.Exec("UPDATE `repository` SET fork_id=0,is_fork=? WHERE fork_id=?", false, repo.ID); err != nil {
 				log.Error(4, "reset 'fork_id' and 'is_fork': %v", err)
 			}
-			if _, err = x.Delete(&ForkInfo{ForkID: repo.ID}); err != nil {
-				log.Error(4, "clear fork infos: %v", err)
-			}
 		}
 	}
 
@@ -1669,13 +1654,6 @@ func IsStaring(uid, repoId int64) bool {
 //  \___  / \____/|__|  |__|_ \
 //      \/                   \/
 
-type ForkInfo struct {
-	ID            int64 `xorm:"pk autoincr"`
-	ForkID        int64
-	RepoID        int64  `xorm:"UNIQUE"`
-	StartCommitID string `xorm:"VARCHAR(40)"`
-}
-
 // HasForkedRepo checks if given user has already forked a repository with given ID.
 func HasForkedRepo(ownerID, repoID int64) (*Repository, bool) {
 	repo := new(Repository)
@@ -1709,13 +1687,6 @@ func ForkRepository(u *User, oldRepo *Repository, name, desc string) (_ *Reposit
 	if _, err = sess.Exec("UPDATE `repository` SET num_forks=num_forks+1 WHERE id=?", oldRepo.ID); err != nil {
 		return nil, err
 	}
-	// else if _, err = sess.Insert(&ForkInfo{
-	// 	ForkID:        oldRepo.ID,
-	// 	RepoID:        repo.ID,
-	// 	StartCommitID: "",
-	// }); err != nil {
-	// 	return nil, fmt.Errorf("insert fork info: %v", err)
-	// }
 
 	oldRepoPath, err := oldRepo.RepoPath()
 	if err != nil {

File diff suppressed because it is too large
+ 0 - 0
modules/bindata/bindata.go


+ 86 - 0
modules/git/repo_pull.go

@@ -0,0 +1,86 @@
+// Copyright 2015 The Gogs 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 git
+
+import (
+	"container/list"
+	"fmt"
+	"strings"
+	"time"
+
+	"github.com/Unknwon/com"
+)
+
+type PullRequestInfo struct {
+	MergeBase string
+	Commits   *list.List
+	// Diff      *Diff
+	NumFiles int
+}
+
+// GetPullRequestInfo generates and returns pull request information
+// between base and head branches of repositories.
+func (repo *Repository) GetPullRequestInfo(basePath, baseBranch, headBranch string) (*PullRequestInfo, error) {
+	// Add a temporary remote.
+	tmpRemote := com.ToStr(time.Now().UnixNano())
+	_, stderr, err := com.ExecCmdDir(repo.Path, "git", "remote", "add", "-f", tmpRemote, basePath)
+	if err != nil {
+		return nil, fmt.Errorf("add base as remote: %v", concatenateError(err, stderr))
+	}
+	defer func() {
+		com.ExecCmdDir(repo.Path, "git", "remote", "remove", tmpRemote)
+	}()
+
+	prInfo := new(PullRequestInfo)
+
+	var stdout string
+	remoteBranch := "remotes/" + tmpRemote + "/" + baseBranch
+	// Get merge base commit.
+	stdout, stderr, err = com.ExecCmdDir(repo.Path, "git", "merge-base", remoteBranch, headBranch)
+	if err != nil {
+		return nil, fmt.Errorf("get merge base: %v", concatenateError(err, stderr))
+	}
+	prInfo.MergeBase = strings.TrimSpace(stdout)
+
+	stdout, stderr, err = com.ExecCmdDir(repo.Path, "git", "log", remoteBranch+"..."+headBranch, prettyLogFormat)
+	if err != nil {
+		return nil, fmt.Errorf("list diff logs: %v", concatenateError(err, stderr))
+	}
+	prInfo.Commits, err = parsePrettyFormatLog(repo, []byte(stdout))
+	if err != nil {
+		return nil, fmt.Errorf("parsePrettyFormatLog: %v", err)
+	}
+
+	// Count number of changed files.
+	stdout, stderr, err = com.ExecCmdDir(repo.Path, "git", "diff", "--name-only", remoteBranch+"..."+headBranch)
+	if err != nil {
+		return nil, fmt.Errorf("list changed files: %v", concatenateError(err, stderr))
+	}
+	prInfo.NumFiles = len(strings.Split(stdout, "\n")) - 1
+
+	return prInfo, nil
+}
+
+// GetPatch generates and returns patch data between given branches.
+func (repo *Repository) GetPatch(basePath, baseBranch, headBranch string) ([]byte, error) {
+	// Add a temporary remote.
+	tmpRemote := com.ToStr(time.Now().UnixNano())
+	_, stderr, err := com.ExecCmdDirBytes(repo.Path, "git", "remote", "add", "-f", tmpRemote, basePath)
+	if err != nil {
+		return nil, fmt.Errorf("add base as remote: %v", concatenateError(err, string(stderr)))
+	}
+	defer func() {
+		com.ExecCmdDir(repo.Path, "git", "remote", "remove", tmpRemote)
+	}()
+
+	var stdout []byte
+	remoteBranch := "remotes/" + tmpRemote + "/" + baseBranch
+	stdout, stderr, err = com.ExecCmdDirBytes(repo.Path, "git", "diff", "-p", remoteBranch, headBranch)
+	if err != nil {
+		return nil, concatenateError(err, string(stderr))
+	}
+
+	return stdout, nil
+}

File diff suppressed because it is too large
+ 0 - 0
public/css/gogs.min.css


+ 16 - 0
public/less/_repository.less

@@ -152,6 +152,22 @@
 		    margin-top: 10px;
 			}
 		}
+		.pull {
+				&.tabular.menu {
+					margin-bottom: 10px;
+					.octicon {
+						margin-right: 5px;
+					}
+				}
+
+				&.tab.segment {
+					border: none;
+			    padding: 0;
+			    padding-top: 10px;
+			    box-shadow: none;
+			    background-color: inherit;
+				}
+		}
 		.comment-list {
 			&:before {
 				display: block;

+ 1 - 0
routers/repo/commit.go

@@ -297,6 +297,7 @@ func CompareDiff(ctx *middleware.Context) {
 	}
 	commits = models.ValidateCommitsWithEmails(commits)
 
+	ctx.Data["CommitRepoLink"] = ctx.Repo.RepoLink
 	ctx.Data["Commits"] = commits
 	ctx.Data["CommitCount"] = commits.Len()
 	ctx.Data["BeforeCommitID"] = beforeCommitID

+ 119 - 77
routers/repo/issue.go

@@ -18,6 +18,7 @@ import (
 	"github.com/gogits/gogs/models"
 	"github.com/gogits/gogs/modules/auth"
 	"github.com/gogits/gogs/modules/base"
+	"github.com/gogits/gogs/modules/git"
 	"github.com/gogits/gogs/modules/log"
 	"github.com/gogits/gogs/modules/mailer"
 	"github.com/gogits/gogs/modules/middleware"
@@ -185,11 +186,32 @@ func Issues(ctx *middleware.Context) {
 }
 
 func renderAttachmentSettings(ctx *middleware.Context) {
+	ctx.Data["RequireDropzone"] = true
 	ctx.Data["IsAttachmentEnabled"] = setting.AttachmentEnabled
 	ctx.Data["AttachmentAllowedTypes"] = setting.AttachmentAllowedTypes
 	ctx.Data["AttachmentMaxFiles"] = setting.AttachmentMaxFiles
 }
 
+func RetrieveRepoMilestonesAndAssignees(ctx *middleware.Context, repo *models.Repository) {
+	var err error
+	ctx.Data["OpenMilestones"], err = models.GetMilestones(repo.ID, -1, false)
+	if err != nil {
+		ctx.Handle(500, "GetMilestones: %v", err)
+		return
+	}
+	ctx.Data["ClosedMilestones"], err = models.GetMilestones(repo.ID, -1, true)
+	if err != nil {
+		ctx.Handle(500, "GetMilestones: %v", err)
+		return
+	}
+
+	ctx.Data["Assignees"], err = repo.GetAssignees()
+	if err != nil {
+		ctx.Handle(500, "GetAssignees: %v", err)
+		return
+	}
+}
+
 func RetrieveRepoMetas(ctx *middleware.Context, repo *models.Repository) []*models.Label {
 	if !ctx.Repo.IsAdmin() {
 		return nil
@@ -202,29 +224,17 @@ func RetrieveRepoMetas(ctx *middleware.Context, repo *models.Repository) []*mode
 	}
 	ctx.Data["Labels"] = labels
 
-	ctx.Data["OpenMilestones"], err = models.GetMilestones(repo.ID, -1, false)
-	if err != nil {
-		ctx.Handle(500, "GetMilestones: %v", err)
-		return nil
-	}
-	ctx.Data["ClosedMilestones"], err = models.GetMilestones(repo.ID, -1, true)
-	if err != nil {
-		ctx.Handle(500, "GetMilestones: %v", err)
+	RetrieveRepoMilestonesAndAssignees(ctx, repo)
+	if ctx.Written() {
 		return nil
 	}
 
-	ctx.Data["Assignees"], err = repo.GetAssignees()
-	if err != nil {
-		ctx.Handle(500, "GetAssignees: %v", err)
-		return nil
-	}
 	return labels
 }
 
 func NewIssue(ctx *middleware.Context) {
 	ctx.Data["Title"] = ctx.Tr("repo.issues.new")
 	ctx.Data["PageIsIssueList"] = true
-	ctx.Data["RequireDropzone"] = true
 	renderAttachmentSettings(ctx)
 
 	RetrieveRepoMetas(ctx, ctx.Repo.Repository)
@@ -235,62 +245,73 @@ func NewIssue(ctx *middleware.Context) {
 	ctx.HTML(200, ISSUE_NEW)
 }
 
+func ValidateRepoMetas(ctx *middleware.Context, form auth.CreateIssueForm) ([]int64, int64, int64) {
+	var (
+		repo = ctx.Repo.Repository
+		err  error
+	)
+
+	labels := RetrieveRepoMetas(ctx, ctx.Repo.Repository)
+	if ctx.Written() {
+		return nil, 0, 0
+	}
+
+	if !ctx.Repo.IsAdmin() {
+		return nil, 0, 0
+	}
+
+	// Check labels.
+	labelIDs := base.StringsToInt64s(strings.Split(form.LabelIDs, ","))
+	labelIDMark := base.Int64sToMap(labelIDs)
+	hasSelected := false
+	for i := range labels {
+		if labelIDMark[labels[i].ID] {
+			labels[i].IsChecked = true
+			hasSelected = true
+		}
+	}
+	ctx.Data["HasSelectedLabel"] = hasSelected
+	ctx.Data["label_ids"] = form.LabelIDs
+	ctx.Data["Labels"] = labels
+
+	// Check milestone.
+	milestoneID := form.MilestoneID
+	if milestoneID > 0 {
+		ctx.Data["Milestone"], err = repo.GetMilestoneByID(milestoneID)
+		if err != nil {
+			ctx.Handle(500, "GetMilestoneByID: %v", err)
+			return nil, 0, 0
+		}
+		ctx.Data["milestone_id"] = milestoneID
+	}
+
+	// Check assignee.
+	assigneeID := form.AssigneeID
+	if assigneeID > 0 {
+		ctx.Data["Assignee"], err = repo.GetAssigneeByID(assigneeID)
+		if err != nil {
+			ctx.Handle(500, "GetAssigneeByID: %v", err)
+			return nil, 0, 0
+		}
+		ctx.Data["assignee_id"] = assigneeID
+	}
+
+	return labelIDs, milestoneID, assigneeID
+}
+
 func NewIssuePost(ctx *middleware.Context, form auth.CreateIssueForm) {
 	ctx.Data["Title"] = ctx.Tr("repo.issues.new")
 	ctx.Data["PageIsIssueList"] = true
-	ctx.Data["RequireDropzone"] = true
 	renderAttachmentSettings(ctx)
 
 	var (
 		repo        = ctx.Repo.Repository
-		labelIDs    []int64
-		milestoneID int64
-		assigneeID  int64
 		attachments []string
-		err         error
 	)
 
-	if ctx.Repo.IsAdmin() {
-		labels := RetrieveRepoMetas(ctx, repo)
-		if ctx.Written() {
-			return
-		}
-
-		// Check labels.
-		labelIDs = base.StringsToInt64s(strings.Split(form.LabelIDs, ","))
-		labelIDMark := base.Int64sToMap(labelIDs)
-		hasSelected := false
-		for i := range labels {
-			if labelIDMark[labels[i].ID] {
-				labels[i].IsChecked = true
-				hasSelected = true
-			}
-		}
-		ctx.Data["HasSelectedLabel"] = hasSelected
-		ctx.Data["label_ids"] = form.LabelIDs
-		ctx.Data["Labels"] = labels
-
-		// Check milestone.
-		milestoneID = form.MilestoneID
-		if milestoneID > 0 {
-			ctx.Data["Milestone"], err = repo.GetMilestoneByID(milestoneID)
-			if err != nil {
-				ctx.Handle(500, "GetMilestoneByID: %v", err)
-				return
-			}
-			ctx.Data["milestone_id"] = milestoneID
-		}
-
-		// Check assignee.
-		assigneeID = form.AssigneeID
-		if assigneeID > 0 {
-			ctx.Data["Assignee"], err = repo.GetAssigneeByID(assigneeID)
-			if err != nil {
-				ctx.Handle(500, "GetAssigneeByID: %v", err)
-				return
-			}
-			ctx.Data["assignee_id"] = assigneeID
-		}
+	labelIDs, milestoneID, assigneeID := ValidateRepoMetas(ctx, form)
+	if ctx.Written() {
+		return
 	}
 
 	if setting.AttachmentEnabled {
@@ -332,7 +353,7 @@ func NewIssuePost(ctx *middleware.Context, form auth.CreateIssueForm) {
 
 	// Mail watchers and mentions.
 	if setting.Service.EnableNotifyMail {
-		tos, err := mailer.SendIssueNotifyMail(ctx.User, ctx.Repo.Owner, ctx.Repo.Repository, issue)
+		tos, err := mailer.SendIssueNotifyMail(ctx.User, ctx.Repo.Owner, repo, issue)
 		if err != nil {
 			ctx.Handle(500, "SendIssueNotifyMail", err)
 			return
@@ -348,13 +369,13 @@ func NewIssuePost(ctx *middleware.Context, form auth.CreateIssueForm) {
 			newTos = append(newTos, m)
 		}
 		if err = mailer.SendIssueMentionMail(ctx.Render, ctx.User, ctx.Repo.Owner,
-			ctx.Repo.Repository, issue, models.GetUserEmailsByNames(newTos)); err != nil {
+			repo, issue, models.GetUserEmailsByNames(newTos)); err != nil {
 			ctx.Handle(500, "SendIssueMentionMail", err)
 			return
 		}
 	}
 
-	log.Trace("Issue created: %d/%d", ctx.Repo.Repository.ID, issue.ID)
+	log.Trace("Issue created: %d/%d", repo.ID, issue.ID)
 	ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + com.ToStr(issue.Index))
 }
 
@@ -421,6 +442,15 @@ func ViewIssue(ctx *middleware.Context) {
 	}
 	ctx.Data["Title"] = issue.Name
 
+	// Make sure type and URL matches.
+	if ctx.Params(":type") == "issues" && issue.IsPull {
+		ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index))
+		return
+	} else if ctx.Params(":type") == "pulls" && !issue.IsPull {
+		ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + com.ToStr(issue.Index))
+		return
+	}
+
 	if err = issue.GetPoster(); err != nil {
 		ctx.Handle(500, "GetPoster", err)
 		return
@@ -429,6 +459,30 @@ func ViewIssue(ctx *middleware.Context) {
 
 	repo := ctx.Repo.Repository
 
+	// Get more information if it's a pull request.
+	if issue.IsPull {
+		headRepoPath, err := issue.PullRepo.HeadRepo.RepoPath()
+		if err != nil {
+			ctx.Handle(500, "PullRepo.HeadRepo.RepoPath", err)
+			return
+		}
+
+		headGitRepo, err := git.OpenRepository(headRepoPath)
+		if err != nil {
+			ctx.Handle(500, "OpenRepository", err)
+			return
+		}
+
+		prInfo, err := headGitRepo.GetPullRequestInfo(models.RepoPath(repo.Owner.Name, repo.Name),
+			issue.PullRepo.BaseBranch, issue.PullRepo.HeadBarcnh)
+		if err != nil {
+			ctx.Handle(500, "GetPullRequestInfo", err)
+			return
+		}
+		ctx.Data["NumCommits"] = prInfo.Commits.Len()
+		ctx.Data["NumFiles"] = prInfo.NumFiles
+	}
+
 	// Metas.
 	// Check labels.
 	if err = issue.GetLabels(); err != nil {
@@ -456,20 +510,8 @@ func ViewIssue(ctx *middleware.Context) {
 
 	// Check milestone and assignee.
 	if ctx.Repo.IsAdmin() {
-		ctx.Data["OpenMilestones"], err = models.GetMilestones(repo.ID, -1, false)
-		if err != nil {
-			ctx.Handle(500, "GetMilestones: %v", err)
-			return
-		}
-		ctx.Data["ClosedMilestones"], err = models.GetMilestones(repo.ID, -1, true)
-		if err != nil {
-			ctx.Handle(500, "GetMilestones: %v", err)
-			return
-		}
-
-		ctx.Data["Assignees"], err = repo.GetAssignees()
-		if err != nil {
-			ctx.Handle(500, "GetAssignees: %v", err)
+		RetrieveRepoMilestonesAndAssignees(ctx, repo)
+		if ctx.Written() {
 			return
 		}
 	}

+ 200 - 22
routers/repo/pull.go

@@ -5,9 +5,11 @@
 package repo
 
 import (
-	"fmt"
+	"path"
 	"strings"
 
+	"github.com/Unknwon/com"
+
 	"github.com/gogits/gogs/models"
 	"github.com/gogits/gogs/modules/auth"
 	"github.com/gogits/gogs/modules/base"
@@ -124,17 +126,19 @@ func ForkPost(ctx *middleware.Context, form auth.CreateRepoForm) {
 	ctx.Redirect(setting.AppSubUrl + "/" + ctxUser.Name + "/" + repo.Name)
 }
 
-func CompareAndPullRequest(ctx *middleware.Context) {
-	ctx.Data["Title"] = ctx.Tr("repo.pulls.compare_changes")
-	ctx.Data["PageIsComparePull"] = true
+func Pulls(ctx *middleware.Context) {
+	ctx.Data["IsRepoToolbarPulls"] = true
+	ctx.HTML(200, PULLS)
+}
 
-	repo := ctx.Repo.Repository
+// func ViewPull
 
+func ParseCompareInfo(ctx *middleware.Context) (*models.User, *models.Repository, *git.Repository, *git.PullRequestInfo, string, string) {
 	// Get compare branch information.
 	infos := strings.Split(ctx.Params("*"), "...")
 	if len(infos) != 2 {
 		ctx.Handle(404, "CompareAndPullRequest", nil)
-		return
+		return nil, nil, nil, nil, "", ""
 	}
 
 	baseBranch := infos[0]
@@ -143,47 +147,221 @@ func CompareAndPullRequest(ctx *middleware.Context) {
 	headInfos := strings.Split(infos[1], ":")
 	if len(headInfos) != 2 {
 		ctx.Handle(404, "CompareAndPullRequest", nil)
-		return
+		return nil, nil, nil, nil, "", ""
 	}
-	headUser := headInfos[0]
+	headUsername := headInfos[0]
 	headBranch := headInfos[1]
 	ctx.Data["HeadBranch"] = headBranch
 
-	// TODO: check if branches are valid.
-	fmt.Println(baseBranch, headUser, headBranch)
+	headUser, err := models.GetUserByName(headUsername)
+	if err != nil {
+		if models.IsErrUserNotExist(err) {
+			ctx.Handle(404, "GetUserByName", nil)
+		} else {
+			ctx.Handle(500, "GetUserByName", err)
+		}
+		return nil, nil, nil, nil, "", ""
+	}
+
+	repo := ctx.Repo.Repository
+
+	// Check if base branch is valid.
+	if !ctx.Repo.GitRepo.IsBranchExist(baseBranch) {
+		ctx.Handle(404, "IsBranchExist", nil)
+		return nil, nil, nil, nil, "", ""
+	}
 
-	// TODO: add organization support
 	// Check if current user has fork of repository.
-	headRepo, has := models.HasForkedRepo(ctx.User.Id, repo.ID)
-	if !has {
+	headRepo, has := models.HasForkedRepo(headUser.Id, repo.ID)
+	if !has || !ctx.User.IsAdminOfRepo(headRepo) {
 		ctx.Handle(404, "HasForkedRepo", nil)
-		return
+		return nil, nil, nil, nil, "", ""
 	}
 
-	headGitRepo, err := git.OpenRepository(models.RepoPath(ctx.User.Name, headRepo.Name))
+	headGitRepo, err := git.OpenRepository(models.RepoPath(headUser.Name, headRepo.Name))
 	if err != nil {
 		ctx.Handle(500, "OpenRepository", err)
-		return
+		return nil, nil, nil, nil, "", ""
+	}
+
+	// Check if head branch is valid.
+	if !headGitRepo.IsBranchExist(headBranch) {
+		ctx.Handle(404, "IsBranchExist", nil)
+		return nil, nil, nil, nil, "", ""
 	}
+
 	headBranches, err := headGitRepo.GetBranches()
 	if err != nil {
 		ctx.Handle(500, "GetBranches", err)
-		return
+		return nil, nil, nil, nil, "", ""
 	}
 	ctx.Data["HeadBranches"] = headBranches
 
+	prInfo, err := headGitRepo.GetPullRequestInfo(models.RepoPath(repo.Owner.Name, repo.Name), baseBranch, headBranch)
+	if err != nil {
+		ctx.Handle(500, "GetPullRequestInfo", err)
+		return nil, nil, nil, nil, "", ""
+	}
+	ctx.Data["BeforeCommitID"] = prInfo.MergeBase
+
+	return headUser, headRepo, headGitRepo, prInfo, baseBranch, headBranch
+}
+
+func PrepareCompareDiff(
+	ctx *middleware.Context,
+	headUser *models.User,
+	headRepo *models.Repository,
+	headGitRepo *git.Repository,
+	prInfo *git.PullRequestInfo,
+	baseBranch, headBranch string) {
+
+	var (
+		repo = ctx.Repo.Repository
+		err  error
+	)
+
+	// Get diff information.
+	ctx.Data["CommitRepoLink"], err = headRepo.RepoLink()
+	if err != nil {
+		ctx.Handle(500, "RepoLink", err)
+		return
+	}
+
+	headCommitID, err := headGitRepo.GetCommitIdOfBranch(headBranch)
+	if err != nil {
+		ctx.Handle(500, "GetCommitIdOfBranch", err)
+		return
+	}
+	ctx.Data["AfterCommitID"] = headCommitID
+
+	diff, err := models.GetDiffRange(models.RepoPath(headUser.Name, headRepo.Name),
+		prInfo.MergeBase, headCommitID, setting.Git.MaxGitDiffLines)
+	if err != nil {
+		ctx.Handle(500, "GetDiffRange", err)
+		return
+	}
+	ctx.Data["Diff"] = diff
+	ctx.Data["DiffNotAvailable"] = diff.NumFiles() == 0
+
+	headCommit, err := headGitRepo.GetCommit(headCommitID)
+	if err != nil {
+		ctx.Handle(500, "GetCommit", err)
+		return
+	}
+	isImageFile := func(name string) bool {
+		blob, err := headCommit.GetBlobByPath(name)
+		if err != nil {
+			return false
+		}
+
+		dataRc, err := blob.Data()
+		if err != nil {
+			return false
+		}
+		buf := make([]byte, 1024)
+		n, _ := dataRc.Read(buf)
+		if n > 0 {
+			buf = buf[:n]
+		}
+		_, isImage := base.IsImageFile(buf)
+		return isImage
+	}
+
+	prInfo.Commits = models.ValidateCommitsWithEmails(prInfo.Commits)
+	ctx.Data["Commits"] = prInfo.Commits
+	ctx.Data["CommitCount"] = prInfo.Commits.Len()
+	ctx.Data["Username"] = headUser.Name
+	ctx.Data["Reponame"] = headRepo.Name
+	ctx.Data["IsImageFile"] = isImageFile
+	ctx.Data["SourcePath"] = setting.AppSubUrl + "/" + path.Join(headUser.Name, repo.Name, "src", headCommitID)
+	ctx.Data["BeforeSourcePath"] = setting.AppSubUrl + "/" + path.Join(headUser.Name, repo.Name, "src", prInfo.MergeBase)
+	ctx.Data["RawPath"] = setting.AppSubUrl + "/" + path.Join(headUser.Name, repo.Name, "raw", headCommitID)
+}
+
+func CompareAndPullRequest(ctx *middleware.Context) {
+	ctx.Data["Title"] = ctx.Tr("repo.pulls.compare_changes")
+	ctx.Data["PageIsComparePull"] = true
+	ctx.Data["IsDiffCompare"] = true
+	renderAttachmentSettings(ctx)
+
+	headUser, headRepo, headGitRepo, prInfo, baseBranch, headBranch := ParseCompareInfo(ctx)
+	if ctx.Written() {
+		return
+	}
+
+	PrepareCompareDiff(ctx, headUser, headRepo, headGitRepo, prInfo, baseBranch, headBranch)
+	if ctx.Written() {
+		return
+	}
+
 	// Setup information for new form.
 	RetrieveRepoMetas(ctx, ctx.Repo.Repository)
 	if ctx.Written() {
 		return
 	}
 
-	// Get diff information.
-
 	ctx.HTML(200, COMPARE_PULL)
 }
 
-func Pulls(ctx *middleware.Context) {
-	ctx.Data["IsRepoToolbarPulls"] = true
-	ctx.HTML(200, PULLS)
+func CompareAndPullRequestPost(ctx *middleware.Context, form auth.CreateIssueForm) {
+	ctx.Data["Title"] = ctx.Tr("repo.pulls.compare_changes")
+	ctx.Data["PageIsComparePull"] = true
+	ctx.Data["IsDiffCompare"] = true
+	renderAttachmentSettings(ctx)
+
+	var (
+		repo        = ctx.Repo.Repository
+		attachments []string
+	)
+
+	_, headRepo, headGitRepo, prInfo, baseBranch, headBranch := ParseCompareInfo(ctx)
+	if ctx.Written() {
+		return
+	}
+
+	patch, err := headGitRepo.GetPatch(models.RepoPath(repo.Owner.Name, repo.Name), baseBranch, headBranch)
+	if err != nil {
+		ctx.Handle(500, "GetPatch", err)
+		return
+	}
+
+	labelIDs, milestoneID, assigneeID := ValidateRepoMetas(ctx, form)
+	if ctx.Written() {
+		return
+	}
+
+	if setting.AttachmentEnabled {
+		attachments = form.Attachments
+	}
+
+	if ctx.HasError() {
+		ctx.HTML(200, COMPARE_PULL)
+		return
+	}
+
+	pr := &models.Issue{
+		RepoID:      repo.ID,
+		Index:       int64(repo.NumIssues) + 1,
+		Name:        form.Title,
+		PosterID:    ctx.User.Id,
+		Poster:      ctx.User,
+		MilestoneID: milestoneID,
+		AssigneeID:  assigneeID,
+		IsPull:      true,
+		Content:     form.Content,
+	}
+	if err := models.NewPullRequest(repo, pr, labelIDs, attachments, &models.PullRepo{
+		HeadRepoID: headRepo.ID,
+		BaseRepoID: repo.ID,
+		HeadBarcnh: headBranch,
+		BaseBranch: baseBranch,
+		MergeBase:  prInfo.MergeBase,
+		Type:       models.PULL_REQUEST_GOGS,
+	}, patch); err != nil {
+		ctx.Handle(500, "NewPullRequest", err)
+		return
+	}
+
+	log.Trace("Pull request created: %d/%d", repo.ID, pr.ID)
+	ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index))
 }

+ 6 - 5
templates/repo/commits_table.tmpl

@@ -10,9 +10,11 @@
     </form>
   </div>
   {{else if .IsDiffCompare}}
-  <a href="{{$.RepoLink}}/commit/{{.BeforeCommitID}}" class="ui green sha label">{{ShortSha .BeforeCommitID}}</a> ... <a href="{{$.RepoLink}}/commit/{{.AfterCommitID}}" class="ui green sha label">{{ShortSha .AfterCommitID}}</a>
+  <a href="{{$.CommitRepoLink}}/commit/{{.BeforeCommitID}}" class="ui green sha label">{{ShortSha .BeforeCommitID}}</a> ... <a href="{{$.CommitRepoLink}}/commit/{{.AfterCommitID}}" class="ui green sha label">{{ShortSha .AfterCommitID}}</a>
   {{end}}
 </h4>
+
+{{if .Commits}}
 <div class="ui attached table segment">
   <table class="ui very basic striped commits table">
     <thead>
@@ -24,9 +26,7 @@
       </tr>
     </thead>
     <tbody>
-    {{ $username := .Username}}
-    {{ $reponame := .Reponame}}
-    {{  $r:= List .Commits}}
+    {{ $r:= List .Commits}}
     {{range $r}}
       <tr>
         <td class="author">
@@ -36,7 +36,7 @@
           <img class="ui avatar image" src="{{AvatarLink .Author.Email}}" alt=""/>&nbsp;&nbsp;{{.Author.Name}}
           {{end}}
         </td>
-        <td class="sha"><a rel="nofollow" class="ui green sha label" href="{{AppSubUrl}}/{{$username}}/{{$reponame}}/commit/{{.Id}} ">{{SubStr .Id.String 0 10}} </a></td>
+        <td class="sha"><a rel="nofollow" class="ui green sha label" href="{{AppSubUrl}}/{{$.Username}}/{{$.Reponame}}/commit/{{.Id}} ">{{SubStr .Id.String 0 10}} </a></td>
         <td class="message"><span class="text truncate">{{RenderCommitMessage .Summary $.RepoLink}}</span></td>
         <td class="date">{{TimeSince .Author.When $.Lang}}</td>
       </tr>
@@ -44,6 +44,7 @@
     </tbody>
   </table>
 </div>
+{{end}}
 
 {{with .Page}}
 {{if gt .TotalPages 1}}

+ 1 - 92
templates/repo/diff.tmpl

@@ -41,98 +41,7 @@
     </div>
     {{end}}
     
-    {{if .DiffNotAvailable}}
-    <h4>{{.i18n.Tr "repo.diff.data_not_available"}}</h4>
-    {{else}}
-    <div class="diff-detail-box diff-box">
-      <div>
-        <i class="fa fa-retweet"></i>
-        {{.i18n.Tr "repo.diff.stats_desc" .Diff.NumFiles .Diff.TotalAddition .Diff.TotalDeletion | Str2html}}
-        <div class="ui right">
-          <a class="ui tiny basic black toggle button" data-target="#diff-files">{{.i18n.Tr "repo.diff.show_diff_stats"}}</a>
-        </div>
-      </div>
-      <ol class="detail-files hide" id="diff-files">
-        {{range .Diff.Files}}
-        <li>
-          <div class="diff-counter count pull-right">
-            {{if not .IsBin}}
-            <span class="add" data-line="{{.Addition}}">{{.Addition}}</span>
-            <span class="bar">
-              <span class="pull-left add"></span>
-              <span class="pull-left del"></span>
-            </span>
-            <span class="del" data-line="{{.Deletion}}">{{.Deletion}}</span>
-            {{else}}
-            <span>{{$.i18n.Tr "repo.diff.bin"}}</span>
-            {{end}}
-          </div>
-          <!-- todo finish all file status, now modify, add, delete and rename -->
-          <span class="status {{DiffTypeToStr .Type}} poping up" data-content="{{DiffTypeToStr .Type}}" data-variation="inverted tiny" data-position="right center">&nbsp;</span>
-          <a class="file" href="#diff-{{.Index}}">{{.Name}}</a>
-        </li>
-        {{end}}
-      </ol>
-    </div>
-
-    {{range $i, $file := .Diff.Files}}
-    <div class="diff-file-box diff-box file-content" id="diff-{{.Index}}">
-      <h4 class="ui top attached normal header">
-        <div class="diff-counter count ui left">
-            {{if not $file.IsBin}}
-            <span class="add" data-line="{{.Addition}}">+ {{.Addition}}</span>
-            <span class="bar">
-              <span class="pull-left add"></span>
-              <span class="pull-left del"></span>
-            </span>
-            <span class="del" data-line="{{.Deletion}}">- {{.Deletion}}</span>
-            {{else}}
-            {{$.i18n.Tr "repo.diff.bin"}}
-            {{end}}
-        </div>
-        <span class="file">{{$file.Name}}</span>
-        <div class="ui right">
-          {{if $file.IsDeleted}}
-          <a class="ui basic tiny button" rel="nofollow" href="{{EscapePound $.BeforeSourcePath}}/{{EscapePound .Name}}">{{$.i18n.Tr "repo.diff.view_file"}}</a>
-          {{else}}
-          <a class="ui basic tiny button" rel="nofollow" href="{{EscapePound $.SourcePath}}/{{EscapePound .Name}}">{{$.i18n.Tr "repo.diff.view_file"}}</a>
-          {{end}}
-        </div>
-      </h4>
-      <div class="ui attached table segment">
-        {{$isImage := (call $.IsImageFile $file.Name)}}
-        {{if $isImage}}
-        <div class="center">
-          <img src="{{$.RawPath}}/{{EscapePound .Name}}">
-        </div>
-        {{else}}
-        <div class="file-body file-code code-view code-diff">
-          <table>
-            <tbody>
-              {{range .Sections}}
-              {{range $k, $line := .Lines}}
-              <tr class="{{DiffLineTypeToStr .Type}}-code nl-{{$k}} ol-{{$k}}">
-                <td class="lines-num lines-num-old">
-                  <span rel="{{if $line.LeftIdx}}diff-{{Sha1 $file.Name}}L{{$line.LeftIdx}}{{end}}">{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}</span>
-                </td>
-                <td class="lines-num lines-num-new">
-                  <span rel="{{if $line.RightIdx}}diff-{{Sha1 $file.Name}}R{{$line.RightIdx}}{{end}}">{{if $line.RightIdx}}{{$line.RightIdx}}{{end}}</span>
-                </td>
-                <td class="lines-code">
-                  <pre>{{$line.Content}}</pre>
-                </td>
-              </tr>
-              {{end}}
-              {{end}}
-            </tbody>
-          </table>
-        </div>
-        {{end}}
-      </div>
-    </div>
-    <br>
-    {{end}}
-    {{end}}
+    {{template "repo/diff_box" .}}
   </div>
 </div>
 {{template "base/footer" .}}

+ 92 - 0
templates/repo/diff_box.tmpl

@@ -0,0 +1,92 @@
+{{if .DiffNotAvailable}}
+<h4>{{.i18n.Tr "repo.diff.data_not_available"}}</h4>
+{{else}}
+<div class="diff-detail-box diff-box">
+  <div>
+    <i class="fa fa-retweet"></i>
+    {{.i18n.Tr "repo.diff.stats_desc" .Diff.NumFiles .Diff.TotalAddition .Diff.TotalDeletion | Str2html}}
+    <div class="ui right">
+      <a class="ui tiny basic black toggle button" data-target="#diff-files">{{.i18n.Tr "repo.diff.show_diff_stats"}}</a>
+    </div>
+  </div>
+  <ol class="detail-files hide" id="diff-files">
+    {{range .Diff.Files}}
+    <li>
+      <div class="diff-counter count pull-right">
+        {{if not .IsBin}}
+        <span class="add" data-line="{{.Addition}}">{{.Addition}}</span>
+        <span class="bar">
+          <span class="pull-left add"></span>
+          <span class="pull-left del"></span>
+        </span>
+        <span class="del" data-line="{{.Deletion}}">{{.Deletion}}</span>
+        {{else}}
+        <span>{{$.i18n.Tr "repo.diff.bin"}}</span>
+        {{end}}
+      </div>
+      <!-- todo finish all file status, now modify, add, delete and rename -->
+      <span class="status {{DiffTypeToStr .Type}} poping up" data-content="{{DiffTypeToStr .Type}}" data-variation="inverted tiny" data-position="right center">&nbsp;</span>
+      <a class="file" href="#diff-{{.Index}}">{{.Name}}</a>
+    </li>
+    {{end}}
+  </ol>
+</div>
+
+{{range $i, $file := .Diff.Files}}
+<div class="diff-file-box diff-box file-content" id="diff-{{.Index}}">
+  <h4 class="ui top attached normal header">
+    <div class="diff-counter count ui left">
+        {{if not $file.IsBin}}
+        <span class="add" data-line="{{.Addition}}">+ {{.Addition}}</span>
+        <span class="bar">
+          <span class="pull-left add"></span>
+          <span class="pull-left del"></span>
+        </span>
+        <span class="del" data-line="{{.Deletion}}">- {{.Deletion}}</span>
+        {{else}}
+        {{$.i18n.Tr "repo.diff.bin"}}
+        {{end}}
+    </div>
+    <span class="file">{{$file.Name}}</span>
+    <div class="ui right">
+      {{if $file.IsDeleted}}
+      <a class="ui basic tiny button" rel="nofollow" href="{{EscapePound $.BeforeSourcePath}}/{{EscapePound .Name}}">{{$.i18n.Tr "repo.diff.view_file"}}</a>
+      {{else}}
+      <a class="ui basic tiny button" rel="nofollow" href="{{EscapePound $.SourcePath}}/{{EscapePound .Name}}">{{$.i18n.Tr "repo.diff.view_file"}}</a>
+      {{end}}
+    </div>
+  </h4>
+  <div class="ui attached table segment">
+    {{$isImage := (call $.IsImageFile $file.Name)}}
+    {{if $isImage}}
+    <div class="center">
+      <img src="{{$.RawPath}}/{{EscapePound .Name}}">
+    </div>
+    {{else}}
+    <div class="file-body file-code code-view code-diff">
+      <table>
+        <tbody>
+          {{range .Sections}}
+          {{range $k, $line := .Lines}}
+          <tr class="{{DiffLineTypeToStr .Type}}-code nl-{{$k}} ol-{{$k}}">
+            <td class="lines-num lines-num-old">
+              <span rel="{{if $line.LeftIdx}}diff-{{Sha1 $file.Name}}L{{$line.LeftIdx}}{{end}}">{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}</span>
+            </td>
+            <td class="lines-num lines-num-new">
+              <span rel="{{if $line.RightIdx}}diff-{{Sha1 $file.Name}}R{{$line.RightIdx}}{{end}}">{{if $line.RightIdx}}{{$line.RightIdx}}{{end}}</span>
+            </td>
+            <td class="lines-code">
+              <pre>{{$line.Content}}</pre>
+            </td>
+          </tr>
+          {{end}}
+          {{end}}
+        </tbody>
+      </table>
+    </div>
+    {{end}}
+  </div>
+</div>
+<br>
+{{end}}
+{{end}}

+ 24 - 0
templates/repo/issue/view.tmpl

@@ -9,7 +9,31 @@
 			</div>
 		</div>
 		<div class="ui divider"></div>
+		{{if .Issue.IsPull}}
+		{{template "repo/issue/view_title" .}}
+		<div class="ui top attached pull tabular menu">
+		  <a class="item active" href="{{.RepoLink}}/pulls/{{.Issue.Index}}">
+		  	<span class="octicon octicon-comment-discussion"></span>
+		  	{{$.i18n.Tr "repo.pulls.tab_conversation"}}
+		  	<span class="ui label">{{.Issue.NumComments}}</span>
+		  </a>
+		  <a class="item" href="{{.RepoLink}}/pulls/{{.Issue.Index}}/commits">
+		  	<span class="octicon octicon-git-commit"></span>
+		  	{{$.i18n.Tr "repo.pulls.tab_commits"}}
+		  	<span class="ui label">{{.NumCommits}}</span>
+		  </a>
+		  <a class="item" href="{{.RepoLink}}/pulls/{{.Issue.Index}}/files">
+		  	<span class="octicon octicon-diff"></span>
+		  	{{$.i18n.Tr "repo.pulls.tab_files"}}
+		  	<span class="ui label">{{.NumFiles}}</span>
+		  </a>
+		</div>
+	  <div class="ui bottom attached tab pull segment active" data-tab="request-{{.ID}}">
+	  	{{template "repo/issue/view_content" .}}
+	  </div>
+		{{else}}
 		{{template "repo/issue/view_content" .}}
+		{{end}}
 	</div>
 </div>
 {{template "base/footer" .}}

+ 7 - 37
templates/repo/issue/view_content.tmpl

@@ -4,41 +4,11 @@
   	{{template "base/alert" .}}
   </div>
   {{end}}
-  <div class="sixteen wide column title">
-  	<div class="ui grid">
-			<h1 class="twelve wide column">
-				<span class="index">#{{.Issue.Index}}</span> <span id="issue-title">{{.Issue.Name}}</span>
-				<div id="edit-title-input" class="ui input" style="display: none">
-				  <input value="{{.Issue.Name}}">
-				</div>
-			</h1>
-			{{if .IsIssueOwner}}
-			<div class="four wide column">
-				<div class="edit-zone text right">
-					<div id="edit-title" class="ui basic green not-in-edit button">{{.i18n.Tr "repo.issues.edit"}}</div>
-					<div id="cancel-edit-title" class="ui basic blue in-edit button" style="display: none">{{.i18n.Tr "repo.issues.cancel"}}</div>
-					<div id="save-edit-title" class="ui green in-edit button" style="display: none" data-update-url="{{.Link}}/title">{{.i18n.Tr "repo.issues.save"}}</div>
-				</div>
-			</div>
-			{{end}}
-  	</div>
-		{{if .Issue.IsClosed}}
-		<div class="ui red large label"><i class="octicon octicon-issue-closed"></i> {{.i18n.Tr "repo.issues.closed_title"}}</div>
-		{{else}}
-		<div class="ui green large label"><i class="octicon octicon-issue-opened"></i> {{.i18n.Tr "repo.issues.open_title"}}</div>
-		{{end}}
-		{{ $createdStr:= TimeSince .Issue.Created $.Lang }}
-		<span class="time-desc">
-			{{if gt .Issue.Poster.Id 0}}
-			{{$.i18n.Tr "repo.issues.opened_by" $createdStr .Issue.Poster.HomeLink .Issue.Poster.Name | Safe}}
-			{{else}}
-			{{$.i18n.Tr "repo.issues.opened_by_fake" $createdStr .Issue.Poster.Name | Safe}}
-			{{end}}
-			·
-			{{$.i18n.Tr "repo.issues.num_comments" .Issue.NumComments}}
-		</span>
-  	<div class="ui divider"></div>
-  </div>
+  {{if not .Issue.IsPull}}
+  {{template "repo/issue/view_title" .}}
+  {{end}}
+  
+  {{ $createdStr:= TimeSince .Issue.Created $.Lang }}
 	<div class="twelve wide column comment-list">
   	<ui class="ui comments">
   		<div class="comment">
@@ -63,7 +33,7 @@
 				    	{{end}}
 			    	</div>
 			    	<div class="raw-content hide">{{.Issue.Content}}</div>
-			    	<div class="edit-content-zone hide" data-write="issue-{{.Issue.ID}}-write" data-preview="issue-{{.Issue.ID}}-preview" data-update-url="{{.Link}}/content" data-context="{{.RepoLink}}"></div>
+			    	<div class="edit-content-zone hide" data-write="issue-{{.Issue.ID}}-write" data-preview="issue-{{.Issue.ID}}-preview" data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/content" data-context="{{.RepoLink}}"></div>
 	  			</div>
 	  			{{if .Issue.Attachments}}
 					<div class="ui bottom attached segment">
@@ -167,7 +137,7 @@
 		      <img src="{{.SignedUser.AvatarLink}}">
 		    </a>
 		    <div class="content">
-			    <form class="ui segment form" id="comment-form" action="{{.Link}}/comments" method="post">
+			    <form class="ui segment form" id="comment-form" action="{{$.RepoLink}}/issues/{{.Issue.Index}}/comments" method="post">
 						{{template "repo/issue/comment_tab" .}}
 						{{.CsrfTokenHtml}}
 						<input id="status" name="status" type="hidden">

+ 35 - 0
templates/repo/issue/view_title.tmpl

@@ -0,0 +1,35 @@
+<div class="sixteen wide column title">
+	<div class="ui grid">
+		<h1 class="twelve wide column">
+			<span class="index">#{{.Issue.Index}}</span> <span id="issue-title">{{.Issue.Name}}</span>
+			<div id="edit-title-input" class="ui input" style="display: none">
+			  <input value="{{.Issue.Name}}">
+			</div>
+		</h1>
+		{{if .IsIssueOwner}}
+		<div class="four wide column">
+			<div class="edit-zone text right">
+				<div id="edit-title" class="ui basic green not-in-edit button">{{.i18n.Tr "repo.issues.edit"}}</div>
+				<div id="cancel-edit-title" class="ui basic blue in-edit button" style="display: none">{{.i18n.Tr "repo.issues.cancel"}}</div>
+				<div id="save-edit-title" class="ui green in-edit button" style="display: none" data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/title">{{.i18n.Tr "repo.issues.save"}}</div>
+			</div>
+		</div>
+		{{end}}
+	</div>
+	{{if .Issue.IsClosed}}
+	<div class="ui red large label"><i class="octicon octicon-issue-closed"></i> {{.i18n.Tr "repo.issues.closed_title"}}</div>
+	{{else}}
+	<div class="ui green large label"><i class="octicon octicon-issue-opened"></i> {{.i18n.Tr "repo.issues.open_title"}}</div>
+	{{end}}
+	{{ $createdStr:= TimeSince .Issue.Created $.Lang }}
+	<span class="time-desc">
+		{{if gt .Issue.Poster.Id 0}}
+		{{$.i18n.Tr "repo.issues.opened_by" $createdStr .Issue.Poster.HomeLink .Issue.Poster.Name | Safe}}
+		{{else}}
+		{{$.i18n.Tr "repo.issues.opened_by_fake" $createdStr .Issue.Poster.Name | Safe}}
+		{{end}}
+		·
+		{{$.i18n.Tr "repo.issues.num_comments" .Issue.NumComments}}
+	</span>
+	<div class="ui divider"></div>
+</div>

+ 3 - 0
templates/repo/pulls/compare.tmpl

@@ -46,6 +46,9 @@
 			 	</div>
 			</div>
 			{{template "repo/issue/new_form" .}}
+
+	    {{template "repo/commits_table" .}}
+	    {{template "repo/diff_box" .}}
 		</div>
 
 	</div>

Some files were not shown because too many files changed in this diff