Merge pull request #305 from nuss-justin/feature/commit-issue-fix
Fix #302. Show referencing commits in issue.
This commit is contained in:
		
						commit
						a76a948a02
					
				|  | @ -8,8 +8,10 @@ import ( | |||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 	"unicode" | ||||
| 
 | ||||
| 	"github.com/gogits/git" | ||||
| 
 | ||||
|  | @ -32,6 +34,20 @@ const ( | |||
| 	OP_COMMENT_ISSUE | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	ErrNotImplemented = errors.New("Not implemented yet") | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	// Same as Github. See https://help.github.com/articles/closing-issues-via-commit-messages
 | ||||
| 	IssueKeywords    = []string{"close", "closes", "closed", "fix", "fixes", "fixed", "resolve", "resolves", "resolved"} | ||||
| 	IssueKeywordsPat *regexp.Regexp | ||||
| ) | ||||
| 
 | ||||
| func init() { | ||||
| 	IssueKeywordsPat = regexp.MustCompile(fmt.Sprintf(`(?i)(?:%s) \S+`, strings.Join(IssueKeywords, "|"))) | ||||
| } | ||||
| 
 | ||||
| // Action represents user operation type and other information to repository.,
 | ||||
| // it implemented interface base.Actioner so that can be used in template render.
 | ||||
| type Action struct { | ||||
|  | @ -78,6 +94,81 @@ func (a Action) GetContent() string { | |||
| 	return a.Content | ||||
| } | ||||
| 
 | ||||
| func updateIssuesCommit(userId, repoId int64, repoUserName, repoName string, commits []*base.PushCommit) error { | ||||
| 	for _, c := range commits { | ||||
| 		refs := IssueKeywordsPat.FindAllString(c.Message, -1) | ||||
| 
 | ||||
| 		for _, ref := range refs { | ||||
| 			ref := ref[strings.IndexByte(ref, byte(' '))+1:] | ||||
| 			ref = strings.TrimRightFunc(ref, func(c rune) bool { | ||||
| 				return !unicode.IsDigit(c) | ||||
| 			}) | ||||
| 
 | ||||
| 			if len(ref) == 0 { | ||||
| 				continue | ||||
| 			} | ||||
| 
 | ||||
| 			// Add repo name if missing
 | ||||
| 			if ref[0] == '#' { | ||||
| 				ref = fmt.Sprintf("%s/%s%s", repoUserName, repoName, ref) | ||||
| 			} else if strings.Contains(ref, "/") == false { | ||||
| 				// We don't support User#ID syntax yet
 | ||||
| 				// return ErrNotImplemented
 | ||||
| 
 | ||||
| 				continue | ||||
| 			} | ||||
| 
 | ||||
| 			issue, err := GetIssueByRef(ref) | ||||
| 
 | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 
 | ||||
| 			url := fmt.Sprintf("/%s/%s/commit/%s", repoUserName, repoName, c.Sha1) | ||||
| 			message := fmt.Sprintf(`<a href="%s">%s</a>`, url, c.Message) | ||||
| 
 | ||||
| 			if err = CreateComment(userId, issue.RepoId, issue.Id, 0, 0, COMMIT, message); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 
 | ||||
| 			if issue.RepoId == repoId { | ||||
| 				if issue.IsClosed { | ||||
| 					continue | ||||
| 				} | ||||
| 
 | ||||
| 				issue.IsClosed = true | ||||
| 
 | ||||
| 				if err = UpdateIssue(issue); err != nil { | ||||
| 					return err | ||||
| 				} | ||||
| 
 | ||||
| 				issue.Repo, err = GetRepositoryById(issue.RepoId) | ||||
| 
 | ||||
| 				if err != nil { | ||||
| 					return err | ||||
| 				} | ||||
| 
 | ||||
| 				issue.Repo.NumClosedIssues++ | ||||
| 
 | ||||
| 				if err = UpdateRepository(issue.Repo); err != nil { | ||||
| 					return err | ||||
| 				} | ||||
| 
 | ||||
| 				if err = ChangeMilestoneIssueStats(issue); err != nil { | ||||
| 					return err | ||||
| 				} | ||||
| 
 | ||||
| 				// If commit happened in the referenced repository, it means the issue can be closed.
 | ||||
| 				if err = CreateComment(userId, repoId, issue.Id, 0, 0, CLOSE, ""); err != nil { | ||||
| 					return err | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // CommitRepoAction adds new action for committing repository.
 | ||||
| func CommitRepoAction(userId, repoUserId int64, userName, actEmail string, | ||||
| 	repoId int64, repoUserName, repoName string, refFullName string, commit *base.PushCommits) error { | ||||
|  | @ -107,6 +198,12 @@ func CommitRepoAction(userId, repoUserId int64, userName, actEmail string, | |||
| 		return errors.New("action.CommitRepoAction(UpdateRepository): " + err.Error()) | ||||
| 	} | ||||
| 
 | ||||
| 	err = updateIssuesCommit(userId, repoId, repoUserName, repoName, commit.Commits) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		log.Debug("action.CommitRepoAction(updateIssuesCommit): ", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if err = NotifyWatchers(&Action{ActUserId: userId, ActUserName: userName, ActEmail: actEmail, | ||||
| 		OpType: opType, Content: string(bs), RepoId: repoId, RepoUserName: repoUserName, | ||||
| 		RepoName: repoName, RefName: refName, | ||||
|  |  | |||
							
								
								
									
										106
									
								
								models/issue.go
								
								
								
								
							
							
						
						
									
										106
									
								
								models/issue.go
								
								
								
								
							|  | @ -7,6 +7,8 @@ package models | |||
| import ( | ||||
| 	"bytes" | ||||
| 	"errors" | ||||
| 	"html/template" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
|  | @ -16,10 +18,11 @@ import ( | |||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	ErrIssueNotExist     = errors.New("Issue does not exist") | ||||
| 	ErrLabelNotExist     = errors.New("Label does not exist") | ||||
| 	ErrMilestoneNotExist = errors.New("Milestone does not exist") | ||||
| 	ErrWrongIssueCounter = errors.New("Invalid number of issues for this milestone") | ||||
| 	ErrIssueNotExist      = errors.New("Issue does not exist") | ||||
| 	ErrLabelNotExist      = errors.New("Label does not exist") | ||||
| 	ErrMilestoneNotExist  = errors.New("Milestone does not exist") | ||||
| 	ErrWrongIssueCounter  = errors.New("Invalid number of issues for this milestone") | ||||
| 	ErrMissingIssueNumber = errors.New("No issue number specified") | ||||
| ) | ||||
| 
 | ||||
| // Issue represents an issue or pull request of repository.
 | ||||
|  | @ -122,6 +125,29 @@ func NewIssue(issue *Issue) (err error) { | |||
| 	return | ||||
| } | ||||
| 
 | ||||
| // GetIssueByRef returns an Issue specified by a GFM reference.
 | ||||
| // See https://help.github.com/articles/writing-on-github#references for more information on the syntax.
 | ||||
| func GetIssueByRef(ref string) (issue *Issue, err error) { | ||||
| 	var issueNumber int64 | ||||
| 	var repo *Repository | ||||
| 
 | ||||
| 	n := strings.IndexByte(ref, byte('#')) | ||||
| 
 | ||||
| 	if n == -1 { | ||||
| 		return nil, ErrMissingIssueNumber | ||||
| 	} | ||||
| 
 | ||||
| 	if issueNumber, err = strconv.ParseInt(ref[n+1:], 10, 64); err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if repo, err = GetRepositoryByRef(ref[:n]); err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	return GetIssueByIndex(repo.Id, issueNumber) | ||||
| } | ||||
| 
 | ||||
| // GetIssueByIndex returns issue by given index in repository.
 | ||||
| func GetIssueByIndex(rid, index int64) (*Issue, error) { | ||||
| 	issue := &Issue{RepoId: rid, Index: index} | ||||
|  | @ -400,6 +426,11 @@ func GetUserIssueStats(uid int64, filterMode int) *IssueStats { | |||
| // UpdateIssue updates information of issue.
 | ||||
| func UpdateIssue(issue *Issue) error { | ||||
| 	_, err := x.Id(issue.Id).AllCols().Update(issue) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
|  | @ -670,6 +701,32 @@ func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) { | |||
| 	return sess.Commit() | ||||
| } | ||||
| 
 | ||||
| // ChangeMilestoneIssueStats updates the open/closed issues counter and progress for the
 | ||||
| // milestone associated witht the given issue.
 | ||||
| func ChangeMilestoneIssueStats(issue *Issue) error { | ||||
| 	if issue.MilestoneId == 0 { | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	m, err := GetMilestoneById(issue.MilestoneId) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if issue.IsClosed { | ||||
| 		m.NumOpenIssues-- | ||||
| 		m.NumClosedIssues++ | ||||
| 	} else { | ||||
| 		m.NumOpenIssues++ | ||||
| 		m.NumClosedIssues-- | ||||
| 	} | ||||
| 
 | ||||
| 	m.Completeness = m.NumClosedIssues * 100 / m.NumIssues | ||||
| 
 | ||||
| 	return UpdateMilestone(m) | ||||
| } | ||||
| 
 | ||||
| // ChangeMilestoneAssign changes assignment of milestone for issue.
 | ||||
| func ChangeMilestoneAssign(oldMid, mid int64, issue *Issue) (err error) { | ||||
| 	sess := x.NewSession() | ||||
|  | @ -693,6 +750,7 @@ func ChangeMilestoneAssign(oldMid, mid int64, issue *Issue) (err error) { | |||
| 		} else { | ||||
| 			m.Completeness = 0 | ||||
| 		} | ||||
| 
 | ||||
| 		if _, err = sess.Id(m.Id).Update(m); err != nil { | ||||
| 			sess.Rollback() | ||||
| 			return err | ||||
|  | @ -710,6 +768,7 @@ func ChangeMilestoneAssign(oldMid, mid int64, issue *Issue) (err error) { | |||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		m.NumIssues++ | ||||
| 		if issue.IsClosed { | ||||
| 			m.NumClosedIssues++ | ||||
|  | @ -731,6 +790,7 @@ func ChangeMilestoneAssign(oldMid, mid int64, issue *Issue) (err error) { | |||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return sess.Commit() | ||||
| } | ||||
| 
 | ||||
|  | @ -774,17 +834,33 @@ func DeleteMilestone(m *Milestone) (err error) { | |||
| //  \______  /\____/|__|_|  /__|_|  /\___  >___|  /__|
 | ||||
| //         \/             \/      \/     \/     \/
 | ||||
| 
 | ||||
| // Issue types.
 | ||||
| // CommentType defines whether a comment is just a simple comment, an action (like close) or a reference.
 | ||||
| type CommentType int | ||||
| 
 | ||||
| const ( | ||||
| 	IT_PLAIN  = iota // Pure comment.
 | ||||
| 	IT_REOPEN        // Issue reopen status change prompt.
 | ||||
| 	IT_CLOSE         // Issue close status change prompt.
 | ||||
| 	// Plain comment, can be associated with a commit (CommitId > 0) and a line (Line > 0)
 | ||||
| 	COMMENT CommentType = iota | ||||
| 
 | ||||
| 	// Reopen action
 | ||||
| 	REOPEN | ||||
| 
 | ||||
| 	// Close action
 | ||||
| 	CLOSE | ||||
| 
 | ||||
| 	// Reference from another issue
 | ||||
| 	ISSUE | ||||
| 
 | ||||
| 	// Reference from some commit (not part of a pull request)
 | ||||
| 	COMMIT | ||||
| 
 | ||||
| 	// Reference from some pull request
 | ||||
| 	PULL | ||||
| ) | ||||
| 
 | ||||
| // Comment represents a comment in commit and issue page.
 | ||||
| type Comment struct { | ||||
| 	Id       int64 | ||||
| 	Type     int | ||||
| 	Type     CommentType | ||||
| 	PosterId int64 | ||||
| 	Poster   *User `xorm:"-"` | ||||
| 	IssueId  int64 | ||||
|  | @ -795,7 +871,7 @@ type Comment struct { | |||
| } | ||||
| 
 | ||||
| // CreateComment creates comment of issue or commit.
 | ||||
| func CreateComment(userId, repoId, issueId, commitId, line int64, cmtType int, content string) error { | ||||
| func CreateComment(userId, repoId, issueId, commitId, line int64, cmtType CommentType, content string) error { | ||||
| 	sess := x.NewSession() | ||||
| 	defer sess.Close() | ||||
| 	if err := sess.Begin(); err != nil { | ||||
|  | @ -810,19 +886,19 @@ func CreateComment(userId, repoId, issueId, commitId, line int64, cmtType int, c | |||
| 
 | ||||
| 	// Check comment type.
 | ||||
| 	switch cmtType { | ||||
| 	case IT_PLAIN: | ||||
| 	case COMMENT: | ||||
| 		rawSql := "UPDATE `issue` SET num_comments = num_comments + 1 WHERE id = ?" | ||||
| 		if _, err := sess.Exec(rawSql, issueId); err != nil { | ||||
| 			sess.Rollback() | ||||
| 			return err | ||||
| 		} | ||||
| 	case IT_REOPEN: | ||||
| 	case REOPEN: | ||||
| 		rawSql := "UPDATE `repository` SET num_closed_issues = num_closed_issues - 1 WHERE id = ?" | ||||
| 		if _, err := sess.Exec(rawSql, repoId); err != nil { | ||||
| 			sess.Rollback() | ||||
| 			return err | ||||
| 		} | ||||
| 	case IT_CLOSE: | ||||
| 	case CLOSE: | ||||
| 		rawSql := "UPDATE `repository` SET num_closed_issues = num_closed_issues + 1 WHERE id = ?" | ||||
| 		if _, err := sess.Exec(rawSql, repoId); err != nil { | ||||
| 			sess.Rollback() | ||||
|  | @ -832,6 +908,10 @@ func CreateComment(userId, repoId, issueId, commitId, line int64, cmtType int, c | |||
| 	return sess.Commit() | ||||
| } | ||||
| 
 | ||||
| func (c *Comment) ContentHtml() template.HTML { | ||||
| 	return template.HTML(c.Content) | ||||
| } | ||||
| 
 | ||||
| // GetIssueComments returns list of comment by given issue id.
 | ||||
| func GetIssueComments(issueId int64) ([]Comment, error) { | ||||
| 	comments := make([]Comment, 0, 10) | ||||
|  |  | |||
|  | @ -7,9 +7,9 @@ package models | |||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"html" | ||||
| 	"html/template" | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| 	"path" | ||||
| 	"path/filepath" | ||||
|  | @ -43,6 +43,7 @@ var ( | |||
| 	ErrRepoNameIllegal   = errors.New("Repository name contains illegal characters") | ||||
| 	ErrRepoFileNotLoaded = errors.New("Repository file not loaded") | ||||
| 	ErrMirrorNotExist    = errors.New("Mirror does not exist") | ||||
| 	ErrInvalidReference  = errors.New("Invalid reference specified") | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
|  | @ -837,6 +838,26 @@ func DeleteRepository(userId, repoId int64, userName string) error { | |||
| 	return sess.Commit() | ||||
| } | ||||
| 
 | ||||
| // GetRepositoryByRef returns a Repository specified by a GFM reference.
 | ||||
| // See https://help.github.com/articles/writing-on-github#references for more information on the syntax.
 | ||||
| func GetRepositoryByRef(ref string) (*Repository, error) { | ||||
| 	n := strings.IndexByte(ref, byte('/')) | ||||
| 
 | ||||
| 	if n < 2 { | ||||
| 		return nil, ErrInvalidReference | ||||
| 	} | ||||
| 
 | ||||
| 	userName, repoName := ref[:n], ref[n+1:] | ||||
| 
 | ||||
| 	user, err := GetUserByName(userName) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return GetRepositoryByName(user.Id, repoName) | ||||
| } | ||||
| 
 | ||||
| // GetRepositoryByName returns the repository by given name under user if exists.
 | ||||
| func GetRepositoryByName(userId int64, repoName string) (*Repository, error) { | ||||
| 	repo := &Repository{ | ||||
|  | @ -1017,4 +1038,4 @@ func IsWatching(uid, rid int64) bool { | |||
| 
 | ||||
| func ForkRepository(repoName string, uid int64) { | ||||
| 
 | ||||
| } | ||||
| } | ||||
|  |  | |||
|  | @ -1258,9 +1258,16 @@ body { | |||
| } | ||||
| #issue .issue-child .panel-heading .user, | ||||
| #issue .issue-closed a.user, | ||||
| #issue .issue-opened a.user { | ||||
| #issue .issue-opened a.user, | ||||
| #issue .issue-reference a.user { | ||||
|     font-weight: bold; | ||||
| } | ||||
| 
 | ||||
| #issue .issue-child .issue-content .user .avatar { | ||||
|     height: 21px; | ||||
|     width: 21px; | ||||
| }  | ||||
| 
 | ||||
| #issue .issue-line { | ||||
|     border-color: #CCC; | ||||
| } | ||||
|  | @ -1280,18 +1287,26 @@ body { | |||
|     width: 60%; | ||||
| } | ||||
| #issue .issue-closed .issue-content, | ||||
| #issue .issue-opened .issue-content { | ||||
| #issue .issue-opened .issue-content, | ||||
| #issue .issue-reference .issue-content { | ||||
|     line-height: 42px; | ||||
| } | ||||
| #issue .issue-closed, | ||||
| #issue .issue-opened { | ||||
| #issue .issue-opened, | ||||
| #issue .issue-reference { | ||||
|     border-bottom: 2px solid #CCC; | ||||
|     margin-bottom: 24px; | ||||
|     padding-bottom: 24px; | ||||
| } | ||||
| 
 | ||||
| #issue .issue-reference { | ||||
|     padding-bottom: 6px; | ||||
| } | ||||
| 
 | ||||
| #issue .issue-closed .label-danger, | ||||
| #issue .issue-opened .label-success { | ||||
|     margin: 0 .8em; | ||||
| #issue .issue-opened .label-success, | ||||
| #issue .issue-reference .label-primary { | ||||
|     margin: 0.8em; | ||||
| } | ||||
| #issue .milestone-item .actions { | ||||
|     margin-top: 10px; | ||||
|  |  | |||
|  | @ -393,7 +393,10 @@ func ViewIssue(ctx *middleware.Context, params martini.Params) { | |||
| 			return | ||||
| 		} | ||||
| 		comments[i].Poster = u | ||||
| 		comments[i].Content = string(base.RenderMarkdown([]byte(comments[i].Content), ctx.Repo.RepoLink)) | ||||
| 
 | ||||
| 		if comments[i].Type == models.COMMENT { | ||||
| 			comments[i].Content = string(base.RenderMarkdown([]byte(comments[i].Content), ctx.Repo.RepoLink)) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	ctx.Data["Title"] = issue.Name | ||||
|  | @ -644,30 +647,14 @@ func Comment(ctx *middleware.Context, params martini.Params) { | |||
| 
 | ||||
| 			// Change open/closed issue counter for the associated milestone
 | ||||
| 			if issue.MilestoneId > 0 { | ||||
| 				l, err := models.GetMilestoneById(issue.MilestoneId) | ||||
| 
 | ||||
| 				if err != nil { | ||||
| 					ctx.Handle(500, "issue.Comment(GetLabelById)", err) | ||||
| 					return | ||||
| 				} | ||||
| 
 | ||||
| 				if issue.IsClosed { | ||||
| 					l.NumOpenIssues = l.NumOpenIssues - 1 | ||||
| 					l.NumClosedIssues = l.NumClosedIssues + 1 | ||||
| 				} else { | ||||
| 					l.NumOpenIssues = l.NumOpenIssues + 1 | ||||
| 					l.NumClosedIssues = l.NumClosedIssues - 1 | ||||
| 				} | ||||
| 
 | ||||
| 				if err = models.UpdateMilestone(l); err != nil { | ||||
| 					ctx.Handle(500, "issue.Comment(UpdateLabel)", err) | ||||
| 					return | ||||
| 				if err = models.ChangeMilestoneIssueStats(issue); err != nil { | ||||
| 					ctx.Handle(500, "issue.Comment(ChangeMilestoneIssueStats)", err) | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			cmtType := models.IT_CLOSE | ||||
| 			cmtType := models.CLOSE | ||||
| 			if !issue.IsClosed { | ||||
| 				cmtType = models.IT_REOPEN | ||||
| 				cmtType = models.REOPEN | ||||
| 			} | ||||
| 
 | ||||
| 			if err = models.CreateComment(ctx.User.Id, ctx.Repo.Repository.Id, issue.Id, 0, 0, cmtType, ""); err != nil { | ||||
|  | @ -683,7 +670,7 @@ func Comment(ctx *middleware.Context, params martini.Params) { | |||
| 	if len(content) > 0 { | ||||
| 		switch params["action"] { | ||||
| 		case "new": | ||||
| 			if err = models.CreateComment(ctx.User.Id, ctx.Repo.Repository.Id, issue.Id, 0, 0, models.IT_PLAIN, content); err != nil { | ||||
| 			if err = models.CreateComment(ctx.User.Id, ctx.Repo.Repository.Id, issue.Id, 0, 0, models.COMMENT, content); err != nil { | ||||
| 				ctx.Handle(500, "issue.Comment(create comment)", err) | ||||
| 				return | ||||
| 			} | ||||
|  |  | |||
|  | @ -49,6 +49,7 @@ | |||
|                         </div> | ||||
|                     </div> | ||||
|                     {{range .Comments}} | ||||
|                     {{/* 0 = COMMENT, 1 = REOPEN, 2 = CLOSE, 3 = ISSUE, 4 = COMMIT, 5 = PULL */}} | ||||
|                     {{if eq .Type 0}} | ||||
|                     <div class="issue-child" id="issue-comment-{{.Id}}"> | ||||
|                         <a class="user pull-left" href="/user/{{.Poster.Name}}"><img class="avatar" src="{{.Poster.AvatarLink}}" alt=""/></a> | ||||
|  | @ -78,6 +79,17 @@ | |||
|                             <a class="user pull-left" href="/user/{{.Poster.Name}}">{{.Poster.Name}}</a> <span class="label label-danger">Closed</span> this issue <span class="time">{{TimeSince .Created}}</span> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     {{else if eq .Type 4}} | ||||
|                     <div class="issue-child issue-reference issue-reference-commit"> | ||||
|                         <a class="user pull-left" href="/user/{{.Poster.Name}}"><img class="avatar" src="{{.Poster.AvatarLink}}" alt=""/></a> | ||||
|                         <div class="issue-content"> | ||||
|                             <a class="user pull-left" href="/user/{{.Poster.Name}}">{{.Poster.Name}}</a> <span class="label label-primary">Referenced</span> this issue <span class="time">{{TimeSince .Created}}</span> | ||||
|                             <p> | ||||
|                                 <a class="user pull-left" href="/user/{{.Poster.Name}}"><img class="avatar" src="{{.Poster.AvatarLink}}" alt=""/></a> | ||||
|                                 {{.ContentHtml}} | ||||
|                             </p> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     {{end}} | ||||
|                     {{end}} | ||||
|                     <hr class="issue-line"/> | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue