diff --git a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala
index 629410a..45a6ef6 100644
--- a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala
+++ b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala
@@ -27,7 +27,6 @@
with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService
-
/**
* The repository viewer.
*/
@@ -207,10 +206,9 @@
/**
* Displays the file content of the specified branch or commit.
*/
- get("/:owner/:repository/blob/*")(referrersOnly { repository =>
+ val blobRoute = get("/:owner/:repository/blob/*")(referrersOnly { repository =>
val (id, path) = splitPath(repository, multiParams("splat").head)
val raw = params.get("raw").getOrElse("false").toBoolean
-
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
val lastModifiedCommit = JGitUtil.getLastModifiedCommit(git, revCommit, path)
@@ -222,12 +220,44 @@
}
} else {
html.blob(id, repository, path.split("/").toList, JGitUtil.getContentInfo(git, path, objectId),
- new JGitUtil.CommitInfo(lastModifiedCommit), hasWritePermission(repository.owner, repository.name, context.loginAccount))
+ new JGitUtil.CommitInfo(lastModifiedCommit),hasWritePermission(repository.owner, repository.name, context.loginAccount),
+ request.paths(2)=="blame")
}
} getOrElse NotFound
}
})
+ get("/:owner/:repository/blame/*"){
+ blobRoute.action()
+ }
+
+ /**
+ * Blame data.
+ */
+ ajaxGet("/:owner/:repository/get-blame/*")(referrersOnly { repository =>
+ val (id, path) = splitPath(repository, multiParams("splat").head)
+ contentType = formats("json")
+ using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
+ val last = git.log.add(git.getRepository.resolve(id)).addPath(path).setMaxCount(1).call.iterator.next.name
+ Map(
+ "root" -> s"${context.baseUrl}/${repository.owner}/${repository.name}",
+ "id" -> id,
+ "path" -> path,
+ "last" -> last,
+ "blame" -> JGitUtil.getBlame(git, id, path).map{ blame =>
+ Map(
+ "id" -> blame.id,
+ "author" -> view.helpers.user(blame.authorName, blame.authorEmailAddress).toString,
+ "avatar" -> view.helpers.avatarLink(blame.authorName, 32, blame.authorEmailAddress).toString,
+ "authed" -> helper.html.datetimeago(blame.authorTime).toString,
+ "parent" -> blame.parent,
+ "commited" -> blame.commitTime.getTime,
+ "message" -> blame.message,
+ "lines" -> blame.lines)
+ })
+ }
+ })
+
/**
* Displays details of the specified commit.
*/
diff --git a/src/main/scala/gitbucket/core/util/JGitUtil.scala b/src/main/scala/gitbucket/core/util/JGitUtil.scala
index d2d531b..2c0259e 100644
--- a/src/main/scala/gitbucket/core/util/JGitUtil.scala
+++ b/src/main/scala/gitbucket/core/util/JGitUtil.scala
@@ -138,6 +138,9 @@
case class BranchInfo(name: String, committerName: String, commitTime: Date, committerEmailAddress:String, mergeInfo: Option[BranchMergeInfo], commitId: String)
+ case class BlameInfo(id: String, authorName: String, authorEmailAddress: String, authorTime:java.util.Date,
+ parent: Option[String], commitTime:java.util.Date, message:String, lines:Set[Int])
+
/**
* Returns RevCommit from the commit or tag id.
*
@@ -748,4 +751,32 @@
}
}
}
+
+ def getBlame(git: Git, id: String, path: String): Iterable[BlameInfo] = {
+ Option(git.getRepository.resolve(id)).map{ commitId =>
+ val blamer = new org.eclipse.jgit.api.BlameCommand(git.getRepository);
+ blamer.setStartCommit(commitId)
+ blamer.setFilePath(path)
+ val blame = blamer.call()
+ var blameMap = Map[String, JGitUtil.BlameInfo]()
+ var idLine = List[(String, Int)]()
+ val commits = 0.to(blame.getResultContents().size()-1).map{ i =>
+ val c = blame.getSourceCommit(i)
+ if(!blameMap.contains(c.name)){
+ blameMap += c.name -> JGitUtil.BlameInfo(
+ c.name,
+ c.getAuthorIdent.getName,
+ c.getAuthorIdent.getEmailAddress,
+ c.getAuthorIdent.getWhen,
+ c.getParents().sortBy(_.getCommitTime()).lastOption.map(_.name),
+ c.getCommitterIdent.getWhen,
+ c.getShortMessage,
+ Set.empty)
+ }
+ idLine :+= (c.name, i)
+ }
+ val limeMap = idLine.groupBy(_._1).mapValues(_.map(_._2).toSet)
+ blameMap.values.map{b => b.copy(lines=limeMap(b.id))}
+ }.getOrElse(Seq.empty)
+ }
}
diff --git a/src/main/twirl/gitbucket/core/repo/blob.scala.html b/src/main/twirl/gitbucket/core/repo/blob.scala.html
index 99f1062..c5e11df 100644
--- a/src/main/twirl/gitbucket/core/repo/blob.scala.html
+++ b/src/main/twirl/gitbucket/core/repo/blob.scala.html
@@ -3,12 +3,29 @@
pathList: List[String],
content: gitbucket.core.util.JGitUtil.ContentInfo,
latestCommit: gitbucket.core.util.JGitUtil.CommitInfo,
- hasWritePermission: Boolean)(implicit context: gitbucket.core.controller.Context)
+ hasWritePermission: Boolean,
+ isBlame: Boolean)(implicit context: gitbucket.core.controller.Context)
@import context._
@import gitbucket.core.view.helpers._
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@html.menu("code", repository){
+
+
Newer
+
+
+
+
+
+
+
+
+
+
+
+
+
Older
+
@helper.html.branchcontrol(
branch,
repository,
@@ -42,6 +59,9 @@
Edit
}
Raw
+ @if(content.viewType == "text"){
+
Blame
+ }
History
@if(hasWritePermission){
Delete
@@ -52,13 +72,13 @@
@if(content.viewType == "text"){
- @defining(pathList.reverse.head) { file =>
- @if(renderableSuffixes.find(suffix => file.toLowerCase.endsWith(suffix))) {
+ @defining(renderableSuffixes.find(suffix => pathList.reverse.head.toLowerCase.endsWith(suffix))) { isRrenderable =>
+ @if(!isBlame && isRrenderable) {
@renderMarkup(pathList, content.content.get, branch, repository, false, false)
} else {
- @content.content.get
+ @content.content.get
}
}
}
@@ -84,30 +104,104 @@
updateHighlighting();
}).hashchange();
- $('pre.prettyprint ol.linenums li').each(function(i, e){
+ function updateSourceLineNum(){
+ $('.source-line-num').remove();
var pre = $('pre.prettyprint');
- pre.append($('')
- .data('line', (i + 1))
- .css({
- cursor : 'pointer',
- position: 'absolute',
- top : $(e).position().top + 'px',
- left : pre.position().left + 'px',
- width : ($(e).position().left - pre.position().left) + 'px',
- height : '16px'
- }));
- });
-
- $('div.source-line-num').click(function(e){
- var line = $(e.target).data('line');
- var hash = location.hash;
- if(e.shiftKey == true && hash.match(/#L\d+(-L\d+)?/)){
- var lines = hash.split('-');
- location.hash = lines[0] + '-L' + line;
- } else {
- location.hash = '#L' + line;
+ $('pre.prettyprint ol.linenums li').each(function(i, e){
+ var p = $(e).position();
+ var left = pre.position().left
+ pre.append($(' ')
+ .data('line', (i + 1))
+ .css({
+ cursor : 'pointer',
+ position: 'absolute',
+ top : p.top + 'px',
+ width : (p.left - left) + 'px',
+ left : left,
+ height : '16px'
+ }));
+ });
+ $('div.source-line-num').click(function(e){
+ var line = $(e.target).data('line');
+ var hash = location.hash;
+ if(e.shiftKey == true && hash.match(/#L\d+(-L\d+)?/)){
+ var lines = hash.split('-');
+ location.hash = lines[0] + '-L' + line;
+ } else {
+ location.hash = '#L' + line;
+ }
+ });
+ }
+ updateSourceLineNum();
+ var repository = $('.blame-action').data('repository');
+ $('.blame-action').click(function(e){
+ if(history.pushState && $('pre.prettyprint.no-renderable').length){
+ e.preventDefault();
+ history.pushState(null, null, this.href);
+ updateBlame();
}
});
+
+ function updateBlame(){
+ var m = /^\/(blame|blob)(\/.*)$/.exec(location.pathname.substring(repository.length));
+ var mode = m[1];
+ $('.blame-action').toggleClass("active", mode=='blame').attr('href', repository + (m[1]=='blame'?'/blob':'/blame')+m[2]);
+ var pre = $('pre.prettyprint');
+ if(pre.parents("td").find(".blame").length){
+ pre.parents("div.container").toggleClass("blame-container", mode=='blame');
+ updateSourceLineNum();
+ return;
+ }
+ if(mode=='blob'){
+ return;
+ }
+ $('.blame-action').addClass("active");
+ var base = $(' ').css({height:pre.height()}).prependTo(pre.parents("td")[0]);
+ base.parents("div.container").addClass("blame-container");
+ updateSourceLineNum();
+ $.get($('.blame-action').data('url')).done(function(data){
+ var blame = data.blame;
+ var index = [];
+ for(var i=0;i ')
+ .append($('').attr("href",data.root+'/commits/'+blame.id).text(blame.id.substr(0,7)));
+ if(blame.parent){
+ sha.append($(' '))
+ .append($('').text('prev').attr("href",data.root+'/blame/'+blame.parent+'/'+data.path));
+ }
+ lastDiv = $('')
+ .addClass('heat'+Math.min(10,Math.max(1,Math.ceil((now-blame.commited)/(24*3600*1000*70)))))
+ .toggleClass('blame-last',blame.id==data.last)
+ .data('line', (i + 1))
+ .css({
+ "top" : p.top + 'px',
+ "min-height" : h+'px',
+ })
+ .append(sha)
+ .append($(blame.avatar).addClass('avatar').css({"float":"left"}))
+ .append($(' ').text(blame.message))
+ .append($(' ').html(blame.author+ " authed "+blame.authed))
+ .appendTo(base);
+ }
+ });
+ });
+ return false;
+ };
+ updateBlame();
});
/**
diff --git a/src/main/webapp/assets/common/css/gitbucket.css b/src/main/webapp/assets/common/css/gitbucket.css
index 6888dfd..c9b6f95 100644
--- a/src/main/webapp/assets/common/css/gitbucket.css
+++ b/src/main/webapp/assets/common/css/gitbucket.css
@@ -1328,3 +1328,104 @@
h6 a.markdown-anchor-link {
top: 6px;
}
+
+/****************************************************************************/
+/* blame */
+/****************************************************************************/
+div.container.blame-container{
+ width:1270px;
+}
+.line-age-legend {
+ display: none;
+}
+.blame-container .line-age-legend {
+ display: block;
+ float: right;
+ font-size: 12px;
+ color: #777;
+}
+.blame-container .line-age-legend ol {
+ display: inline-block;
+ list-style: none;
+ margin: 0 5px;
+}
+.blame-container .line-age-legend ol li {
+ display: inline-block;
+ width: 8px;
+ height: 10px;
+}
+.blame-container pre.blob{
+ margin-left: 350px;
+}
+.blame-container pre.prettyprint ol.linenums li.blame-sep{
+ border-top: 1px solid rgb(219, 219, 219);
+ margin-top: -1px;
+}
+.blame{
+ font-size: 12px;
+ white-space: normal;
+ width: 340px;
+ float: left;
+ min-height: 100px;
+ display: none;
+}
+.blame-container .blame{
+ display: block;
+}
+.blame .blame-commit-title{
+ font-weight: bold;
+ color: #333;
+ line-height: 1.1;
+}
+.blame .avatar{
+ margin-right: 4px;
+ margin-bottom: 4px;
+}
+.blame .blame-info{
+ background: white;
+ box-shadow:rgba(113, 135, 164, 0.65098) 0px 0px 4px 0px;
+ position: absolute;
+ width: 340px;
+ padding: 2px;
+ border-right: 2px solid;
+}
+.blame-sha{
+ font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace;
+ float: right;
+ text-align: right;
+}
+.blame-sha .muted-link{
+ color: #777;
+}
+.blame-sha .muted-link:hover{
+ color: #4183c4;
+}
+
+.blame .blame-info:hover{
+ z-index: 100;
+ box-shadow:rgba(113, 135, 164, 0.65098) 0px 0px 4px 3px;
+}
+.blame .blame-info.blame-last{
+ background: #FDFCED;
+}
+.blame-info.heat1{ border-right-color:#ffeca7}
+.blame-info.heat2{ border-right-color:#ffdd8c}
+.blame-info.heat3{ border-right-color:#ffdd7c}
+.blame-info.heat4{ border-right-color:#fba447}
+.blame-info.heat5{ border-right-color:#f68736}
+.blame-info.heat6{ border-right-color:#f37636}
+.blame-info.heat7{ border-right-color:#ca6632}
+.blame-info.heat8{ border-right-color:#c0513f}
+.blame-info.heat9{ border-right-color:#a2503a}
+.blame-info.heat10{border-right-color:#793738}
+
+.heat1{background-color:#ffeca7}
+.heat2{background-color:#ffdd8c}
+.heat3{background-color:#ffdd7c}
+.heat4{background-color:#fba447}
+.heat5{background-color:#f68736}
+.heat6{background-color:#f37636}
+.heat7{background-color:#ca6632}
+.heat8{background-color:#c0513f}
+.heat9{background-color:#a2503a}
+.heat10{background-color:#793738}
|