Skip to content

Commit

Permalink
go-gitea#747 File blame feature
Browse files Browse the repository at this point in the history
  • Loading branch information
makhov committed Feb 7, 2017
1 parent 8bc4319 commit 995a280
Show file tree
Hide file tree
Showing 10 changed files with 288 additions and 3 deletions.
1 change: 1 addition & 0 deletions cmd/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,7 @@ func runWeb(ctx *cli.Context) error {
m.Get("/src/*", repo.SetEditorconfigIfExists, repo.Home)
m.Get("/raw/*", repo.SingleDownload)
m.Get("/commits/*", repo.RefCommits)
m.Get("/blame/*", repo.Blame)
m.Get("/graph", repo.Graph)
m.Get("/commit/:sha([a-f0-9]{7,40})$", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.Diff)
m.Get("/forks", repo.Forks)
Expand Down
97 changes: 97 additions & 0 deletions models/blame.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package models

import (
"code.gitea.io/git"
"fmt"
"github.com/Unknwon/com"
"time"
"bytes"
"regexp"
)

// Blame represents a Git object.
type BlameLine struct {
Commit *git.Commit
Author string // Signature?
When time.Time
Num string
Content string
}

// Blame represents a Git object.
type Blame struct {
File string
Lines []*BlameLine
}

// GitBlame
func (repo *Repository) GitBlame(branch, fileName string) (*Blame, error) {
repoWorkingPool.CheckIn(com.ToStr(repo.ID))
defer repoWorkingPool.CheckOut(com.ToStr(repo.ID))

if err := repo.DiscardLocalRepoBranchChanges(branch); err != nil {
return nil, fmt.Errorf("DiscardLocalRepoBranchChanges [branch: %s]: %v", branch, err)
} else if err = repo.UpdateLocalCopyBranch(branch); err != nil {
return nil, fmt.Errorf("UpdateLocalCopyBranch [branch: %s]: %v", branch, err)
}

localPath := repo.LocalCopyPath()

gitRepo, err := git.OpenRepository(repo.RepoPath())
if err != nil {
return nil, fmt.Errorf("git.OpenRepository [branch: %s]: %v", branch, err)
}

blameBytes, err := gitRepo.FileBlame(branch, localPath, fileName)
if err != nil {
return nil, fmt.Errorf("git.FileBlame [branch: %s, file %s]: %v", branch, fileName, err)
}

return repo.parseBlameOutput(blameBytes)
}

func (repo *Repository) parseBlameOutput(blameBytes []byte) (*Blame, error) {
b := new(Blame)
if len(blameBytes) == 0 {
return b, nil
}

gitRepo, err := git.OpenRepository(repo.RepoPath())
if err != nil {
return nil, fmt.Errorf("git.OpenRepository: %v", err)
}

parts := bytes.Split(blameBytes, []byte{'\n'})

r := regexp.MustCompile(`^(.{8})\s\((.+)\s(\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}\s\+\d{4})\s(\d+)\)\s(.*)`)
for _, part := range parts[0:len(parts) - 1] { // last one is empty string
matches := r.FindAllStringSubmatch(string(part), -1)
if len(matches) > 0 {
commitID := matches[0][1]
author := matches[0][2]
dateString := matches[0][3]
lineNum := matches[0][4]
content := matches[0][5]

date, err := time.Parse("2006-01-02 15:04:05 -0700", dateString)
if err != nil {
return nil, err
}
commit, err := gitRepo.GetCommit(commitID)
if err != nil {
return nil, err
}
bl := &BlameLine{
Commit: commit,
Author: author,
When: date,
Num: lineNum,
Content: content,
}

b.Lines = append(b.Lines, bl)
}
}

return b, nil
}
2 changes: 2 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,8 @@ file_raw = Raw
file_history = History
file_view_raw = View Raw
file_permalink = Permalink
file_blame = Blame
file_normal = Normal view
file_too_large = This file is too large to be shown
video_not_supported_in_browser = Your browser doesn't support HTML5 video tag.
stored_lfs = Stored with Git LFS
Expand Down
21 changes: 21 additions & 0 deletions public/css/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -1336,42 +1336,63 @@ footer .ui.language .menu {
cursor: pointer;
display: block;
}
.repository.file.list #file-content .code-view .lines-blame {
width: 30%;
}
.repository.file.list #file-content .code-view .lines-blame .time-since {
display: block;
float: right;
padding-right: 5px;
}
.repository.file.list #file-content .code-view .lines-num,
.repository.file.list #file-content .code-view .lines-blame,
.repository.file.list #file-content .code-view .lines-code {
padding: 0;
}
.repository.file.list #file-content .code-view .lines-num pre,
.repository.file.list #file-content .code-view .lines-blame pre,
.repository.file.list #file-content .code-view .lines-code pre,
.repository.file.list #file-content .code-view .lines-num ol,
.repository.file.list #file-content .code-view .lines-blame ol,
.repository.file.list #file-content .code-view .lines-code ol,
.repository.file.list #file-content .code-view .lines-num .hljs,
.repository.file.list #file-content .code-view .lines-blame .hljs,
.repository.file.list #file-content .code-view .lines-code .hljs {
background-color: white;
margin: 0;
padding: 0 !important;
}
.repository.file.list #file-content .code-view .lines-num pre li,
.repository.file.list #file-content .code-view .lines-blame pre li,
.repository.file.list #file-content .code-view .lines-code pre li,
.repository.file.list #file-content .code-view .lines-num ol li,
.repository.file.list #file-content .code-view .lines-blame ol li,
.repository.file.list #file-content .code-view .lines-code ol li,
.repository.file.list #file-content .code-view .lines-num .hljs li,
.repository.file.list #file-content .code-view .lines-blame .hljs li,
.repository.file.list #file-content .code-view .lines-code .hljs li {
display: inline-block;
width: 100%;
}
.repository.file.list #file-content .code-view .lines-num pre li.active,
.repository.file.list #file-content .code-view .lines-blame pre li.active,
.repository.file.list #file-content .code-view .lines-code pre li.active,
.repository.file.list #file-content .code-view .lines-num ol li.active,
.repository.file.list #file-content .code-view .lines-blame ol li.active,
.repository.file.list #file-content .code-view .lines-code ol li.active,
.repository.file.list #file-content .code-view .lines-num .hljs li.active,
.repository.file.list #file-content .code-view .lines-blame .hljs li.active,
.repository.file.list #file-content .code-view .lines-code .hljs li.active {
background: #ffffdd;
}
.repository.file.list #file-content .code-view .lines-num pre li:before,
.repository.file.list #file-content .code-view .lines-blame pre li:before,
.repository.file.list #file-content .code-view .lines-code pre li:before,
.repository.file.list #file-content .code-view .lines-num ol li:before,
.repository.file.list #file-content .code-view .lines-blame ol li:before,
.repository.file.list #file-content .code-view .lines-code ol li:before,
.repository.file.list #file-content .code-view .lines-num .hljs li:before,
.repository.file.list #file-content .code-view .lines-blame .hljs li:before,
.repository.file.list #file-content .code-view .lines-code .hljs li:before {
content: ' ';
}
Expand Down
9 changes: 9 additions & 0 deletions public/less/_repository.less
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,16 @@
display: block;
}
}
.lines-blame {
width: 30%;
.time-since {
display: block;
float: right;
padding-right: 5px;
}
}
.lines-num,
.lines-blame,
.lines-code {
padding: 0;
pre,
Expand Down
66 changes: 66 additions & 0 deletions routers/repo/blame.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package repo

import (
//"code.gitea.io/git"
gotemplate "html/template"

"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/git"
"fmt"
"bytes"
"code.gitea.io/gitea/modules/highlight"
"strings"
)

const (
tplBlame base.TplName = "repo/blame"
)

// Blame shows git-blame
func Blame(ctx *context.Context) {
fileName := ctx.Repo.TreePath

_, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
if err != nil {
ctx.NotFoundOrServerError("Repo.Commit.GetTreeEntryByPath", git.IsErrNotExist, err)
return
}

if len(fileName) == 0 {
ctx.Handle(404, "TreePath not foung", nil)
return
}
branchName := ctx.Repo.BranchName
blame, err := ctx.Repo.Repository.GitBlame(branchName, fileName)

ctx.Data["RequireHighlightJS"] = true
ctx.Data["HighlightClass"] = highlight.FileNameToHighlightClass(fileName)
if err != nil {
ctx.Handle(500, "FileBlame", err)
return
}
var output bytes.Buffer
for index, line := range blame.Lines {
output.WriteString(fmt.Sprintf(`<li class="L%d" rel="L%d">%s</li>`, index+1, index+1, gotemplate.HTMLEscapeString(line.Content)) + "\n")
}

var treeNames []string
paths := make([]string, 0, 5)
if len(ctx.Repo.TreePath) > 0 {
treeNames = strings.Split(ctx.Repo.TreePath, "/")
for i := range treeNames {
paths = append(paths, strings.Join(treeNames[:i+1], "/"))
}
}

ctx.Data["TreeNames"] = treeNames
ctx.Data["FileContent"] = gotemplate.HTML(output.String())
ctx.Data["Blame"] = blame
ctx.Data["FileName"] = fileName
ctx.HTML(200, tplBlame)
}
76 changes: 76 additions & 0 deletions templates/repo/blame.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{{template "base/head" .}}
<div class="repository file list blame">
{{template "repo/header" .}}
<div class="ui container">
<div class="ui secondary menu">
{{template "repo/branch_dropdown" .}}
<div class="fitted item">
<div class="ui breadcrumb">
<a class="section" href="{{.RepoLink}}/src/{{EscapePound .BranchName}}">{{EllipsisString .Repository.Name 25}}</a>
{{ $n := len .TreeNames}}
{{ $l := Subtract $n 1}}
{{range $i, $v := .TreeNames}}
<div class="divider"> / </div>
{{if eq $i $l}}
<span class="active section">{{EllipsisString $v 15}}</span>
{{else}}
{{ $p := index $.Paths $i}}
<span class="section"><a href="{{EscapePound $.BranchLink}}/{{EscapePound $p}}">{{EllipsisString $v 15}}</a></span>
{{end}}
{{end}}
</div>
</div>
</div>
<div id="file-content" class="{{TabSizeClass .Editorconfig .FileName}}">
<h4 class="ui top attached header">
<i class="file text outline icon ui left"></i>
<strong>{{.FileName}}</strong> <span class="text grey normal"></span>
<div class="ui right file-actions">
<div class="ui buttons">
<a class="ui button"
href="{{.RepoLink}}/commits/{{EscapePound .BranchName}}/{{EscapePound .TreePath}}">{{.i18n.Tr "repo.file_history"}}</a>
<a class="ui button" href="{{.RepoLink}}/src/{{EscapePound .BranchName}}/{{EscapePound .TreePath}}">{{.i18n.Tr "repo.file_normal"}}</a>
</div>
</div>
</h4>

<div class="ui attached table segment">
<div class="file-view code-view has-emoji">
<table>
<tbody>
<tr>
{{if .IsFileTooLarge}}
<td><strong>{{.i18n.Tr "repo.file_too_large"}}</strong></td>
{{else}}
<td class="lines-blame">
<ol class="linenums">
{{range .Blame.Lines}}
<li>
<a href="{{$.RepoLink}}/commit/{{.Commit.ID.String}}">
{{ShortSha .Commit.ID.String}}
</a>
({{.Commit.Author.Name}})
{{TimeSince .Commit.Committer.When $.Lang}}
</li>
{{end}}
{{.Commits}}
</ol>
</td>
<td class="lines-num">
{{range .Blame.Lines}}
<span id="L{{.Num}}">{{.Num}}</span>
{{end}}
</td>
<td class="lines-code"><pre><code class="{{.HighlightClass}} hljs"><ol class="linenums">{{.FileContent}}</ol></code></pre></td>
{{end}}
</tr>
</tbody>
</table>
</div>
</div>
</div>


</div>
</div>
{{template "base/footer" .}}
3 changes: 3 additions & 0 deletions templates/repo/view_file.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
<a class="ui button" href="{{.RepoLink}}/src/{{.CommitID}}/{{EscapePound .TreePath}}">{{.i18n.Tr "repo.file_permalink"}}</a>
{{end}}
<a class="ui button" href="{{.RepoLink}}/commits/{{EscapePound .BranchName}}/{{EscapePound .TreePath}}">{{.i18n.Tr "repo.file_history"}}</a>
{{if .IsTextFile}}
<a class="ui button" href="{{.RepoLink}}/blame/{{EscapePound .BranchName}}/{{EscapePound .TreePath}}">{{.i18n.Tr "repo.file_blame"}}</a>
{{end}}
<a class="ui button" href="{{EscapePound $.RawFileLink}}">{{.i18n.Tr "repo.file_raw"}}</a>
</div>
{{if .Repository.CanEnableEditor}}
Expand Down
10 changes: 10 additions & 0 deletions vendor/code.gitea.io/git/repo_blame.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions vendor/vendor.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
"ignore": "test",
"package": [
{
"checksumSHA1": "P2wIRW07gnEgqLZAcg7bz+Jw9Oc=",
"checksumSHA1": "sxrE5CCDQvVHLiCrOl+I2pL3pIU=",
"path": "code.gitea.io/git",
"revision": "7477742b3c79d36d099baaf614864b6bbdfdecca",
"revisionTime": "2017-01-09T15:46:57Z"
"revision": "21873601554bf2bfe3dead86064faaaf8f4dc278",
"revisionTime": "2017-02-06T08:32:33Z"
},
{
"checksumSHA1": "BKj0haFTDebzdC2nACpoGzp3s8A=",
Expand Down

1 comment on commit 995a280

@jondo
Copy link

@jondo jondo commented on 995a280 Aug 31, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the status of this branch?

Please sign in to comment.