work on PR conversation

This commit is contained in:
Unknwon 2015-09-01 19:07:02 -04:00
parent 63fecac537
commit 8c046073a8
20 changed files with 784 additions and 303 deletions

View file

@ -513,13 +513,14 @@ func runWeb(ctx *cli.Context) {
m.Post("/edit/:tagname", bindIgnErr(auth.EditReleaseForm{}), repo.EditReleasePost) m.Post("/edit/:tagname", bindIgnErr(auth.EditReleaseForm{}), repo.EditReleasePost)
}, reqRepoAdmin, middleware.RepoRef()) }, 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)) }, reqSignIn, middleware.RepoAssignment(true))
m.Group("/:username/:reponame", func() { m.Group("/:username/:reponame", func() {
m.Get("/releases", middleware.RepoRef(), repo.Releases) m.Get("/releases", middleware.RepoRef(), repo.Releases)
m.Get("/issues", repo.RetrieveLabels, repo.Issues) 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("/labels/", repo.RetrieveLabels, repo.Labels)
m.Get("/milestones", repo.Milestones) m.Get("/milestones", repo.Milestones)
m.Get("/pulls", repo.Pulls) m.Get("/pulls", repo.Pulls)

View file

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

View file

@ -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) 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)
}
// _________ __ // _________ __
// \_ ___ \ ____ _____ _____ ____ _____/ |_ // \_ ___ \ ____ _____ _____ ____ _____/ |_
// / \ \/ / _ \ / \ / \_/ __ \ / \ __\ // / \ \/ / _ \ / \ / \_/ __ \ / \ __\

View file

@ -9,6 +9,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"mime/multipart" "mime/multipart"
"os" "os"
"path" "path"
@ -21,6 +22,7 @@ import (
"github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/base"
"github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/log"
"github.com/gogits/gogs/modules/process"
"github.com/gogits/gogs/modules/setting" "github.com/gogits/gogs/modules/setting"
gouuid "github.com/gogits/gogs/modules/uuid" gouuid "github.com/gogits/gogs/modules/uuid"
) )
@ -47,6 +49,7 @@ type Issue struct {
Assignee *User `xorm:"-"` Assignee *User `xorm:"-"`
IsRead bool `xorm:"-"` IsRead bool `xorm:"-"`
IsPull bool // Indicates whether is a pull request or not. IsPull bool // Indicates whether is a pull request or not.
PullRepo *PullRepo `xorm:"-"`
IsClosed bool IsClosed bool
Content string `xorm:"TEXT"` Content string `xorm:"TEXT"`
RenderedContent string `xorm:"-"` RenderedContent string `xorm:"-"`
@ -92,6 +95,11 @@ func (i *Issue) AfterSet(colName string, _ xorm.Cell) {
if err != nil { if err != nil {
log.Error(3, "GetUserByID[%d]: %v", i.ID, err) 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": case "created":
i.Created = regulateTimeZone(i.Created) i.Created = regulateTimeZone(i.Created)
} }
@ -273,30 +281,11 @@ func (i *Issue) ChangeStatus(doer *User, isClosed bool) (err error) {
return sess.Commit() return sess.Commit()
} }
// CreateIssue creates new issue with labels for repository. // It's caller's responsibility to create action.
func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) { func newIssue(e *xorm.Session, repo *Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) {
// Check attachments. if _, err = e.Insert(issue); err != nil {
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 return err
} } else if _, err = e.Exec("UPDATE `repository` SET num_issues=num_issues+1 WHERE id=?", issue.RepoID); err != nil {
if _, err = sess.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 {
return err return err
} }
@ -306,34 +295,62 @@ func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string)
continue continue
} }
label, err = getLabelByID(sess, id) label, err = getLabelByID(e, id)
if err != nil { if err != nil {
return err return err
} }
if err = issue.addLabel(sess, label); err != nil { if err = issue.addLabel(e, label); err != nil {
return fmt.Errorf("addLabel: %v", err) return fmt.Errorf("addLabel: %v", err)
} }
} }
if issue.MilestoneID > 0 { if issue.MilestoneID > 0 {
if err = changeMilestoneAssign(sess, 0, issue); err != nil { if err = changeMilestoneAssign(e, 0, issue); err != nil {
return err return err
} }
} }
if err = newIssueUsers(sess, repo, issue); err != nil { if err = newIssueUsers(e, repo, issue); err != nil {
return err 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 { for i := range attachments {
attachments[i].IssueID = issue.ID attachments[i].IssueID = issue.ID
// No assign value could be 0, so ignore AllCols(). // 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 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. // Notify watchers.
act := &Action{ act := &Action{
ActUserID: issue.Poster.Id, ActUserID: issue.Poster.Id,
@ -813,6 +830,117 @@ func UpdateIssueUsersByMentions(uids []int64, iid int64) error {
return nil 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
}
// .____ ___. .__ // .____ ___. .__
// | | _____ \_ |__ ____ | | // | | _____ \_ |__ ____ | |
// | | \__ \ | __ \_/ __ \| | // | | \__ \ | __ \_/ __ \| |

View file

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

View file

@ -160,7 +160,6 @@ type Repository struct {
IsFork bool `xorm:"NOT NULL DEFAULT false"` IsFork bool `xorm:"NOT NULL DEFAULT false"`
ForkID int64 ForkID int64
BaseRepo *Repository `xorm:"-"` BaseRepo *Repository `xorm:"-"`
ForkInfo *ForkInfo `xorm:"-"`
Created time.Time `xorm:"CREATED"` Created time.Time `xorm:"CREATED"`
Updated time.Time `xorm:"UPDATED"` Updated time.Time `xorm:"UPDATED"`
@ -168,15 +167,6 @@ type Repository struct {
func (repo *Repository) AfterSet(colName string, _ xorm.Cell) { func (repo *Repository) AfterSet(colName string, _ xorm.Cell) {
switch colName { 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": case "updated":
repo.Updated = regulateTimeZone(repo.Updated) repo.Updated = regulateTimeZone(repo.Updated)
} }
@ -1047,8 +1037,6 @@ func DeleteRepository(uid, repoID int64) error {
if repo.IsFork { if repo.IsFork {
if _, err = sess.Exec("UPDATE `repository` SET num_forks=num_forks-1 WHERE id=?", repo.ForkID); err != nil { 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) 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 { 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) 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. // HasForkedRepo checks if given user has already forked a repository with given ID.
func HasForkedRepo(ownerID, repoID int64) (*Repository, bool) { func HasForkedRepo(ownerID, repoID int64) (*Repository, bool) {
repo := new(Repository) 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 { if _, err = sess.Exec("UPDATE `repository` SET num_forks=num_forks+1 WHERE id=?", oldRepo.ID); err != nil {
return nil, err 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() oldRepoPath, err := oldRepo.RepoPath()
if err != nil { if err != nil {

File diff suppressed because one or more lines are too long

86
modules/git/repo_pull.go Normal file
View file

@ -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 one or more lines are too long

View file

@ -152,6 +152,22 @@
margin-top: 10px; 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 { .comment-list {
&:before { &:before {
display: block; display: block;

View file

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

View file

@ -18,6 +18,7 @@ import (
"github.com/gogits/gogs/models" "github.com/gogits/gogs/models"
"github.com/gogits/gogs/modules/auth" "github.com/gogits/gogs/modules/auth"
"github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/base"
"github.com/gogits/gogs/modules/git"
"github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/log"
"github.com/gogits/gogs/modules/mailer" "github.com/gogits/gogs/modules/mailer"
"github.com/gogits/gogs/modules/middleware" "github.com/gogits/gogs/modules/middleware"
@ -185,11 +186,32 @@ func Issues(ctx *middleware.Context) {
} }
func renderAttachmentSettings(ctx *middleware.Context) { func renderAttachmentSettings(ctx *middleware.Context) {
ctx.Data["RequireDropzone"] = true
ctx.Data["IsAttachmentEnabled"] = setting.AttachmentEnabled ctx.Data["IsAttachmentEnabled"] = setting.AttachmentEnabled
ctx.Data["AttachmentAllowedTypes"] = setting.AttachmentAllowedTypes ctx.Data["AttachmentAllowedTypes"] = setting.AttachmentAllowedTypes
ctx.Data["AttachmentMaxFiles"] = setting.AttachmentMaxFiles 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 { func RetrieveRepoMetas(ctx *middleware.Context, repo *models.Repository) []*models.Label {
if !ctx.Repo.IsAdmin() { if !ctx.Repo.IsAdmin() {
return nil return nil
@ -202,29 +224,17 @@ func RetrieveRepoMetas(ctx *middleware.Context, repo *models.Repository) []*mode
} }
ctx.Data["Labels"] = labels ctx.Data["Labels"] = labels
ctx.Data["OpenMilestones"], err = models.GetMilestones(repo.ID, -1, false) RetrieveRepoMilestonesAndAssignees(ctx, repo)
if err != nil { if ctx.Written() {
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)
return nil return nil
} }
ctx.Data["Assignees"], err = repo.GetAssignees()
if err != nil {
ctx.Handle(500, "GetAssignees: %v", err)
return nil
}
return labels return labels
} }
func NewIssue(ctx *middleware.Context) { func NewIssue(ctx *middleware.Context) {
ctx.Data["Title"] = ctx.Tr("repo.issues.new") ctx.Data["Title"] = ctx.Tr("repo.issues.new")
ctx.Data["PageIsIssueList"] = true ctx.Data["PageIsIssueList"] = true
ctx.Data["RequireDropzone"] = true
renderAttachmentSettings(ctx) renderAttachmentSettings(ctx)
RetrieveRepoMetas(ctx, ctx.Repo.Repository) RetrieveRepoMetas(ctx, ctx.Repo.Repository)
@ -235,29 +245,23 @@ func NewIssue(ctx *middleware.Context) {
ctx.HTML(200, ISSUE_NEW) ctx.HTML(200, ISSUE_NEW)
} }
func NewIssuePost(ctx *middleware.Context, form auth.CreateIssueForm) { func ValidateRepoMetas(ctx *middleware.Context, form auth.CreateIssueForm) ([]int64, int64, int64) {
ctx.Data["Title"] = ctx.Tr("repo.issues.new")
ctx.Data["PageIsIssueList"] = true
ctx.Data["RequireDropzone"] = true
renderAttachmentSettings(ctx)
var ( var (
repo = ctx.Repo.Repository repo = ctx.Repo.Repository
labelIDs []int64
milestoneID int64
assigneeID int64
attachments []string
err error err error
) )
if ctx.Repo.IsAdmin() { labels := RetrieveRepoMetas(ctx, ctx.Repo.Repository)
labels := RetrieveRepoMetas(ctx, repo)
if ctx.Written() { if ctx.Written() {
return return nil, 0, 0
}
if !ctx.Repo.IsAdmin() {
return nil, 0, 0
} }
// Check labels. // Check labels.
labelIDs = base.StringsToInt64s(strings.Split(form.LabelIDs, ",")) labelIDs := base.StringsToInt64s(strings.Split(form.LabelIDs, ","))
labelIDMark := base.Int64sToMap(labelIDs) labelIDMark := base.Int64sToMap(labelIDs)
hasSelected := false hasSelected := false
for i := range labels { for i := range labels {
@ -271,26 +275,43 @@ func NewIssuePost(ctx *middleware.Context, form auth.CreateIssueForm) {
ctx.Data["Labels"] = labels ctx.Data["Labels"] = labels
// Check milestone. // Check milestone.
milestoneID = form.MilestoneID milestoneID := form.MilestoneID
if milestoneID > 0 { if milestoneID > 0 {
ctx.Data["Milestone"], err = repo.GetMilestoneByID(milestoneID) ctx.Data["Milestone"], err = repo.GetMilestoneByID(milestoneID)
if err != nil { if err != nil {
ctx.Handle(500, "GetMilestoneByID: %v", err) ctx.Handle(500, "GetMilestoneByID: %v", err)
return return nil, 0, 0
} }
ctx.Data["milestone_id"] = milestoneID ctx.Data["milestone_id"] = milestoneID
} }
// Check assignee. // Check assignee.
assigneeID = form.AssigneeID assigneeID := form.AssigneeID
if assigneeID > 0 { if assigneeID > 0 {
ctx.Data["Assignee"], err = repo.GetAssigneeByID(assigneeID) ctx.Data["Assignee"], err = repo.GetAssigneeByID(assigneeID)
if err != nil { if err != nil {
ctx.Handle(500, "GetAssigneeByID: %v", err) ctx.Handle(500, "GetAssigneeByID: %v", err)
return return nil, 0, 0
} }
ctx.Data["assignee_id"] = assigneeID 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
renderAttachmentSettings(ctx)
var (
repo = ctx.Repo.Repository
attachments []string
)
labelIDs, milestoneID, assigneeID := ValidateRepoMetas(ctx, form)
if ctx.Written() {
return
} }
if setting.AttachmentEnabled { if setting.AttachmentEnabled {
@ -332,7 +353,7 @@ func NewIssuePost(ctx *middleware.Context, form auth.CreateIssueForm) {
// Mail watchers and mentions. // Mail watchers and mentions.
if setting.Service.EnableNotifyMail { 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 { if err != nil {
ctx.Handle(500, "SendIssueNotifyMail", err) ctx.Handle(500, "SendIssueNotifyMail", err)
return return
@ -348,13 +369,13 @@ func NewIssuePost(ctx *middleware.Context, form auth.CreateIssueForm) {
newTos = append(newTos, m) newTos = append(newTos, m)
} }
if err = mailer.SendIssueMentionMail(ctx.Render, ctx.User, ctx.Repo.Owner, 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) ctx.Handle(500, "SendIssueMentionMail", err)
return 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)) ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + com.ToStr(issue.Index))
} }
@ -421,6 +442,15 @@ func ViewIssue(ctx *middleware.Context) {
} }
ctx.Data["Title"] = issue.Name 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 { if err = issue.GetPoster(); err != nil {
ctx.Handle(500, "GetPoster", err) ctx.Handle(500, "GetPoster", err)
return return
@ -429,6 +459,30 @@ func ViewIssue(ctx *middleware.Context) {
repo := ctx.Repo.Repository 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. // Metas.
// Check labels. // Check labels.
if err = issue.GetLabels(); err != nil { if err = issue.GetLabels(); err != nil {
@ -456,20 +510,8 @@ func ViewIssue(ctx *middleware.Context) {
// Check milestone and assignee. // Check milestone and assignee.
if ctx.Repo.IsAdmin() { if ctx.Repo.IsAdmin() {
ctx.Data["OpenMilestones"], err = models.GetMilestones(repo.ID, -1, false) RetrieveRepoMilestonesAndAssignees(ctx, repo)
if err != nil { if ctx.Written() {
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 return
} }
} }

View file

@ -5,9 +5,11 @@
package repo package repo
import ( import (
"fmt" "path"
"strings" "strings"
"github.com/Unknwon/com"
"github.com/gogits/gogs/models" "github.com/gogits/gogs/models"
"github.com/gogits/gogs/modules/auth" "github.com/gogits/gogs/modules/auth"
"github.com/gogits/gogs/modules/base" "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) ctx.Redirect(setting.AppSubUrl + "/" + ctxUser.Name + "/" + repo.Name)
} }
func CompareAndPullRequest(ctx *middleware.Context) { func Pulls(ctx *middleware.Context) {
ctx.Data["Title"] = ctx.Tr("repo.pulls.compare_changes") ctx.Data["IsRepoToolbarPulls"] = true
ctx.Data["PageIsComparePull"] = 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. // Get compare branch information.
infos := strings.Split(ctx.Params("*"), "...") infos := strings.Split(ctx.Params("*"), "...")
if len(infos) != 2 { if len(infos) != 2 {
ctx.Handle(404, "CompareAndPullRequest", nil) ctx.Handle(404, "CompareAndPullRequest", nil)
return return nil, nil, nil, nil, "", ""
} }
baseBranch := infos[0] baseBranch := infos[0]
@ -143,47 +147,221 @@ func CompareAndPullRequest(ctx *middleware.Context) {
headInfos := strings.Split(infos[1], ":") headInfos := strings.Split(infos[1], ":")
if len(headInfos) != 2 { if len(headInfos) != 2 {
ctx.Handle(404, "CompareAndPullRequest", nil) ctx.Handle(404, "CompareAndPullRequest", nil)
return return nil, nil, nil, nil, "", ""
} }
headUser := headInfos[0] headUsername := headInfos[0]
headBranch := headInfos[1] headBranch := headInfos[1]
ctx.Data["HeadBranch"] = headBranch ctx.Data["HeadBranch"] = headBranch
// TODO: check if branches are valid. headUser, err := models.GetUserByName(headUsername)
fmt.Println(baseBranch, headUser, headBranch) if err != nil {
if models.IsErrUserNotExist(err) {
// TODO: add organization support ctx.Handle(404, "GetUserByName", nil)
// Check if current user has fork of repository. } else {
headRepo, has := models.HasForkedRepo(ctx.User.Id, repo.ID) ctx.Handle(500, "GetUserByName", err)
if !has { }
ctx.Handle(404, "HasForkedRepo", nil) return nil, nil, nil, nil, "", ""
return
} }
headGitRepo, err := git.OpenRepository(models.RepoPath(ctx.User.Name, headRepo.Name)) 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, "", ""
}
// Check if current user has fork of repository.
headRepo, has := models.HasForkedRepo(headUser.Id, repo.ID)
if !has || !ctx.User.IsAdminOfRepo(headRepo) {
ctx.Handle(404, "HasForkedRepo", nil)
return nil, nil, nil, nil, "", ""
}
headGitRepo, err := git.OpenRepository(models.RepoPath(headUser.Name, headRepo.Name))
if err != nil { if err != nil {
ctx.Handle(500, "OpenRepository", err) 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() headBranches, err := headGitRepo.GetBranches()
if err != nil { if err != nil {
ctx.Handle(500, "GetBranches", err) ctx.Handle(500, "GetBranches", err)
return return nil, nil, nil, nil, "", ""
} }
ctx.Data["HeadBranches"] = headBranches 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. // Setup information for new form.
RetrieveRepoMetas(ctx, ctx.Repo.Repository) RetrieveRepoMetas(ctx, ctx.Repo.Repository)
if ctx.Written() { if ctx.Written() {
return return
} }
// Get diff information.
ctx.HTML(200, COMPARE_PULL) ctx.HTML(200, COMPARE_PULL)
} }
func Pulls(ctx *middleware.Context) { func CompareAndPullRequestPost(ctx *middleware.Context, form auth.CreateIssueForm) {
ctx.Data["IsRepoToolbarPulls"] = true ctx.Data["Title"] = ctx.Tr("repo.pulls.compare_changes")
ctx.HTML(200, PULLS) 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))
} }

View file

@ -10,9 +10,11 @@
</form> </form>
</div> </div>
{{else if .IsDiffCompare}} {{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}} {{end}}
</h4> </h4>
{{if .Commits}}
<div class="ui attached table segment"> <div class="ui attached table segment">
<table class="ui very basic striped commits table"> <table class="ui very basic striped commits table">
<thead> <thead>
@ -24,8 +26,6 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{{ $username := .Username}}
{{ $reponame := .Reponame}}
{{ $r:= List .Commits}} {{ $r:= List .Commits}}
{{range $r}} {{range $r}}
<tr> <tr>
@ -36,7 +36,7 @@
<img class="ui avatar image" src="{{AvatarLink .Author.Email}}" alt=""/>&nbsp;&nbsp;{{.Author.Name}} <img class="ui avatar image" src="{{AvatarLink .Author.Email}}" alt=""/>&nbsp;&nbsp;{{.Author.Name}}
{{end}} {{end}}
</td> </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="message"><span class="text truncate">{{RenderCommitMessage .Summary $.RepoLink}}</span></td>
<td class="date">{{TimeSince .Author.When $.Lang}}</td> <td class="date">{{TimeSince .Author.When $.Lang}}</td>
</tr> </tr>
@ -44,6 +44,7 @@
</tbody> </tbody>
</table> </table>
</div> </div>
{{end}}
{{with .Page}} {{with .Page}}
{{if gt .TotalPages 1}} {{if gt .TotalPages 1}}

View file

@ -41,98 +41,7 @@
</div> </div>
{{end}} {{end}}
{{if .DiffNotAvailable}} {{template "repo/diff_box" .}}
<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}}
</div> </div>
</div> </div>
{{template "base/footer" .}} {{template "base/footer" .}}

View file

@ -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}}

View file

@ -9,7 +9,31 @@
</div> </div>
</div> </div>
<div class="ui divider"></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" .}} {{template "repo/issue/view_content" .}}
</div> </div>
{{else}}
{{template "repo/issue/view_content" .}}
{{end}}
</div>
</div> </div>
{{template "base/footer" .}} {{template "base/footer" .}}

View file

@ -4,41 +4,11 @@
{{template "base/alert" .}} {{template "base/alert" .}}
</div> </div>
{{end}} {{end}}
<div class="sixteen wide column title"> {{if not .Issue.IsPull}}
<div class="ui grid"> {{template "repo/issue/view_title" .}}
<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}} {{end}}
{{ $createdStr:= TimeSince .Issue.Created $.Lang }} {{ $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>
<div class="twelve wide column comment-list"> <div class="twelve wide column comment-list">
<ui class="ui comments"> <ui class="ui comments">
<div class="comment"> <div class="comment">
@ -63,7 +33,7 @@
{{end}} {{end}}
</div> </div>
<div class="raw-content hide">{{.Issue.Content}}</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> </div>
{{if .Issue.Attachments}} {{if .Issue.Attachments}}
<div class="ui bottom attached segment"> <div class="ui bottom attached segment">
@ -167,7 +137,7 @@
<img src="{{.SignedUser.AvatarLink}}"> <img src="{{.SignedUser.AvatarLink}}">
</a> </a>
<div class="content"> <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" .}} {{template "repo/issue/comment_tab" .}}
{{.CsrfTokenHtml}} {{.CsrfTokenHtml}}
<input id="status" name="status" type="hidden"> <input id="status" name="status" type="hidden">

View file

@ -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>

View file

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