Allow file annotations (blame) in side-by-side diff

Using CodeMirror's lint addon on the UI to display the blame
annotations. It works on both sides of the side-by-side
diff and also supports the auto-merge commit. It requires manual
step to enable the annotations to avoid any unnecessary git
processing and network traffic between the server and client.

Introduces a new dependency on blame-cache in gerrit-server
to reuse BlameCache.

In a following change, the gutter showing the blame info will be
made clickable. Clicking on the gutter will open a new tab that
takes the user to the corresponding change in Gerrit.

The commit SHA-1 hashes are currently not selectable. Making it
so might require an upstream change in CodeMirror's lint addon.

Bug: Issue 1642
Change-Id: I6267d30cbee448f8137e11c7120959dc424eaeeb
This commit is contained in:
Gabor Somossy
2015-10-20 23:40:07 +01:00
committed by Michael Zhou
parent b3ba766ffa
commit b72d4c6d8f
27 changed files with 655 additions and 0 deletions

View File

@@ -915,6 +915,12 @@ If 0 the update polling is disabled.
+
Default is 30 seconds.
[[change.allowBlame]]change.allowBlame::
+
Allow blame on side by side diff. If set to false, blame cannot be used.
+
Default is true.
[[change.allowDrafts]]change.allowDrafts::
+
Allow drafts workflow. If set to false, drafts cannot be created,

View File

@@ -3753,6 +3753,66 @@ differences are reported in the result. Valid values are `IGNORE_NONE`,
The `context` parameter can be specified to control the number of lines of surrounding context
in the diff. Valid values are `ALL` or number of lines.
[[get-blame]]
=== Get Blame
--
'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/files/link:#file-id[\{file-id\}]/blame'
--
Gets the blame of a file from a certain revision.
.Request
----
GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/gerrit-server%2Fsrc%2Fmain%2Fjava%2Fcom%2Fgoogle%2Fgerrit%2Fserver%2Fproject%2FRefControl.java/blame HTTP/1.0
----
As response a link:#blame-info[BlameInfo] entity is returned that describes the
blame.
.Response
----
HTTP/1.1 200 OK
Content-Disposition: attachment
Content-Type: application/json; charset=UTF-8
)]
{
[
{
"author": "Joe Daw",
"id": "64e140b4de5883a4dd74d06c2b62ccd7ffd224a7",
"time": 1421441349,
"commit_msg": "RST test\n\nChange-Id: I11e9e24bd122253f4bb10c36dce825ac2410d646\n",
"ranges": [
{
"start": 1,
"end": 10
},
{
"start": 16,
"end": 296
}
]
},
{
"author": "Jane Daw",
"id": "8d52621a0e2ac6adec73bd3a49f2371cd53137a7",
"time": 1421825421,
"commit_msg": "add banner\n\nChange-Id: I2eced9b2691015ae3c5138f4d0c4ca2b8fb15be9\n",
"ranges": [
{
"start": 13,
"end": 13
}
]
}
]
}
----
The `base` parameter can be specified to control the base patch set from which
the blame should be generated.
[[set-reviewed]]
=== Set Reviewed
--
@@ -3978,6 +4038,22 @@ NOTE: To apply different tags on on different votes/comments multiple
invocations of the REST call are required.
|===========================
[[blame-info]]
=== BlameInfo
The `BlameInfo` entity stores the commit metadata with the row coordinates where
it applies.
[options="header",cols="1,6"]
|===========================
|Field Name | Description
|`author` | The author of the commit.
|`id` | The id of the commit.
|`time` | Commit time.
|`commit_msg` | The commit message.
|`ranges` |
The blame row coordinates as link:#range-info[RangeInfo] entities.
|===========================
[[change-edit-input]]
=== ChangeEditInput
The `ChangeEditInput` entity contains information for restoring a
@@ -4637,6 +4713,17 @@ found while checking the signature or the key itself, as a
link:rest-api-accounts.html#gpg-key-info[GpgKeyInfo] entity.
|===========================
[[range-info]]
=== RangeInfo
The `RangeInfo` entity stores the coordinates of a range.
[options="header",cols="1,6"]
|===========================
|Field Name | Description
|`start` | First index.
|`end` | Last index.
|===========================
[[rebase-input]]
=== RebaseInput
The `RebaseInput` entity contains information for changing parent when rebasing.

View File

@@ -1076,6 +1076,9 @@ section.
[options="header",cols="1,^1,5"]
|=============================
|Field Name ||Description
|`allow_blame` |not set if `false`|
link:config-gerrit.html#change.allowBlame[Whether blame on side by side diff is
allowed].
|`allow_drafts` |not set if `false`|
link:config-gerrit.html#change.allowDrafts[Whether draft workflow is
allowed].

View File

@@ -0,0 +1,43 @@
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.extensions.common;
import java.util.List;
public class BlameInfo {
public String author;
public String id;
public int time;
public String commitMsg;
public List<RangeInfo> ranges;
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
BlameInfo blameInfo = (BlameInfo) o;
return id.equals(blameInfo.id);
}
@Override
public int hashCode() {
return id.hashCode();
}
}

View File

@@ -0,0 +1,25 @@
// Copyright (C) 2016 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.extensions.common;
public class RangeInfo {
public int start;
public int end;
public RangeInfo(int start, int end) {
this.start = start;
this.end = end;
}
}

View File

@@ -42,6 +42,9 @@ public interface Resources extends ClientBundle {
@Source("resultset_up_gray.png")
ImageResource arrowUp();
@Source("lightbulb.png")
ImageResource blame();
@Source("page_white_put.png")
ImageResource downloadIcon();

View File

@@ -56,6 +56,7 @@ public class ServerInfo extends JavaScriptObject {
public static class ChangeConfigInfo extends JavaScriptObject {
public final native boolean allowDrafts() /*-{ return this.allow_drafts || false; }-*/;
public final native boolean allowBlame() /*-{ return this.allow_blame || false; }-*/;
public final native int largeChange() /*-{ return this.large_change || 0; }-*/;
public final native String replyLabel() /*-{ return this.reply_label; }-*/;
public final native String replyTooltip() /*-{ return this.reply_tooltip; }-*/;

View File

@@ -0,0 +1,26 @@
// Copyright (C) 2016 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.client;
import com.google.gwt.core.client.JavaScriptObject;
public class RangeInfo extends JavaScriptObject {
public final native int start() /*-{ return this.start; }-*/;
public final native int end() /*-{ return this.end; }-*/;
protected RangeInfo() {
}
}

View File

@@ -0,0 +1,31 @@
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.client.blame;
import com.google.gerrit.client.RangeInfo;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArray;
public class BlameInfo extends JavaScriptObject {
public final native String author() /*-{ return this.author; }-*/;
public final native String id() /*-{ return this.id; }-*/;
public final native String commitMsg() /*-{ return this.commit_msg; }-*/;
public final native int time() /*-{ return this.time; }-*/;
public final native JsArray<RangeInfo> ranges() /*-{ return this.ranges; }-*/;
protected BlameInfo() {
}
}

View File

@@ -97,6 +97,14 @@ public class ChangeApi {
return call(id, "detail");
}
public static RestApi blame(PatchSet.Id id, String path, boolean base) {
return revision(id)
.view("files")
.id(path)
.view("blame")
.addParameter("base", base);
}
public static RestApi actions(int id, String revision) {
if (revision == null || revision.equals("")) {
revision = "current";

View File

@@ -98,6 +98,16 @@ abstract class DiffTable extends Composite {
return changeType;
}
void setUpBlameIconA(CodeMirror cm, boolean isBase, PatchSet.Id rev,
String path) {
patchSetSelectBoxA.setUpBlame(cm, isBase, rev, path);
}
void setUpBlameIconB(CodeMirror cm, PatchSet.Id rev,
String path) {
patchSetSelectBoxB.setUpBlame(cm, false, rev, path);
}
void set(DiffPreferences prefs, JsArray<RevisionInfo> list, DiffInfo info,
boolean editExists, boolean current, boolean open, boolean binary) {
this.changeType = info.changeType();

View File

@@ -16,9 +16,12 @@ package com.google.gerrit.client.diff;
import com.google.gerrit.client.Dispatcher;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.blame.BlameInfo;
import com.google.gerrit.client.changes.ChangeApi;
import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
import com.google.gerrit.client.info.WebLinkInfo;
import com.google.gerrit.client.patches.PatchUtil;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.Natives;
import com.google.gerrit.client.ui.InlineHyperlink;
import com.google.gerrit.reviewdb.client.Change;
@@ -27,6 +30,7 @@ import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
@@ -38,6 +42,7 @@ import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.ImageResourceRenderer;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwtorm.client.KeyUtil;
import net.codemirror.lib.CodeMirror;
import java.util.List;
@@ -124,6 +129,32 @@ class PatchSetSelectBox extends Composite {
}
}
void setUpBlame(final CodeMirror cm, final boolean isBase,
final PatchSet.Id rev, final String path) {
if (!Patch.COMMIT_MSG.equals(path) && Gerrit.isSignedIn()
&& Gerrit.info().change().allowBlame()) {
Anchor blameIcon = createBlameIcon();
blameIcon.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent clickEvent) {
if (cm.extras().getBlameInfo() != null) {
cm.extras().toggleAnnotation();
} else {
ChangeApi.blame(rev, path, isBase)
.get(new GerritCallback<JsArray<BlameInfo>>() {
@Override
public void onSuccess(JsArray<BlameInfo> lines) {
cm.extras().toggleAnnotation(lines);
}
});
}
}
});
linkPanel.add(blameIcon);
}
}
private Widget createEditIcon() {
PatchSet.Id id = (idActive == null) ? other.idActive : idActive;
Anchor anchor = new Anchor(
@@ -133,6 +164,13 @@ class PatchSetSelectBox extends Composite {
return anchor;
}
private Anchor createBlameIcon() {
Anchor anchor = new Anchor(
new ImageResourceRenderer().render(Gerrit.RESOURCES.blame()));
anchor.setTitle(PatchUtil.C.blame());
return anchor;
}
static void link(PatchSetSelectBox a, PatchSetSelectBox b) {
a.other = b;
b.other = a;

View File

@@ -192,6 +192,11 @@ public class SideBySide extends DiffScreen {
cmA = newCm(diff.metaA(), diff.textA(), diffTable.cmA);
cmB = newCm(diff.metaB(), diff.textB(), diffTable.cmB);
boolean reviewingBase = base == null;
getDiffTable().setUpBlameIconA(cmA, reviewingBase,
reviewingBase ? revision : base, path);
getDiffTable().setUpBlameIconB(cmB, revision, path);
cmA.extras().side(DisplaySide.A);
cmB.extras().side(DisplaySide.B);
setShowTabs(prefs.showTabs());

View File

@@ -54,6 +54,7 @@ public interface PatchConstants extends Constants {
String download();
String edit();
String blame();
String addFileCommentToolTip();
String cannedReplyDone();

View File

@@ -37,6 +37,7 @@ nextFileHelp = Next file
download = Download
edit = Edit
blame = Blame
addFileCommentToolTip = Click to add file comment
sideBySideDiff = Side-by-side diff

View File

@@ -0,0 +1,23 @@
// Copyright (C) 2016 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package net.codemirror.lib;
import com.google.gwt.i18n.client.Messages;
public interface BlameConfig extends Messages {
String shortBlameMsg(String commitId, String date, String author);
String detailedBlameMsg(String commitId, String author, String time,
String msg);
}

View File

@@ -0,0 +1,2 @@
shortBlameMsg={0} {1} {2}
detailedBlameMsg=commit {0}\nAuthor: {1}\nDate: {2}\n\n{3}

View File

@@ -19,16 +19,30 @@ import static com.google.gwt.dom.client.Style.Unit.PX;
import static net.codemirror.lib.CodeMirror.style;
import static net.codemirror.lib.CodeMirror.LineClassWhere.WRAP;
import com.google.gerrit.client.FormatUtil;
import com.google.gerrit.client.RangeInfo;
import com.google.gerrit.client.blame.BlameInfo;
import com.google.gerrit.client.diff.DisplaySide;
import com.google.gerrit.client.rpc.Natives;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.core.client.JsArrayString;
import com.google.gwt.dom.client.Element;
import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.user.client.DOM;
import net.codemirror.lib.CodeMirror.LineHandle;
import java.util.Date;
import java.util.Objects;
/** Additional features added to CodeMirror by Gerrit Code Review. */
public class Extras {
public static final String ANNOTATION_GUTTER_ID = "CodeMirror-lint-markers";
static final BlameConfig C = GWT.create(BlameConfig.class);
static final native Extras get(CodeMirror c) /*-{ return c.gerritExtras }-*/;
private static native void set(CodeMirror c, Extras e)
/*-{ c.gerritExtras = e }-*/;
@@ -43,6 +57,7 @@ public class Extras {
private double charWidthPx;
private double lineHeightPx;
private LineHandle activeLine;
private boolean annotated;
private Extras(CodeMirror cm) {
this.cm = cm;
@@ -140,4 +155,68 @@ public class Extras {
activeLine = null;
}
}
public boolean isAnnotated() {
return annotated;
}
public final void clearAnnotations() {
JsArrayString gutters = ((JsArrayString) JsArrayString.createArray());
cm.setOption("gutters", gutters);
annotated = false;
}
public final void setAnnotations(JsArray<BlameInfo> blameInfos) {
if (blameInfos.length() > 0) {
setBlameInfo(blameInfos);
JsArrayString gutters = ((JsArrayString) JsArrayString.createArray());
gutters.push(ANNOTATION_GUTTER_ID);
cm.setOption("gutters", gutters);
annotated = true;
DateTimeFormat format = DateTimeFormat.getFormat(
DateTimeFormat.PredefinedFormat.DATE_SHORT);
JsArray<LintLine> annotations = JsArray.createArray().cast();
for (BlameInfo blameInfo : Natives.asList(blameInfos)) {
for (RangeInfo range : Natives.asList(blameInfo.ranges())) {
Date commitTime = new Date(blameInfo.time() * 1000L);
String shortId = blameInfo.id().substring(0, 8);
String shortBlame = C.shortBlameMsg(
shortId, format.format(commitTime), blameInfo.author());
String detailedBlame = C.detailedBlameMsg(blameInfo.id(),
blameInfo.author(), FormatUtil.mediumFormat(commitTime),
blameInfo.commitMsg());
annotations.push(LintLine.create(shortBlame, detailedBlame, shortId,
Pos.create(range.start() - 1)));
}
}
cm.setOption("lint", getAnnotation(annotations));
}
}
private native JavaScriptObject getAnnotation(JsArray<LintLine> annotations) /*-{
return {
getAnnotations: function(text, options, cm) { return annotations; }
};
}-*/;
public final native JsArray<BlameInfo> getBlameInfo() /*-{
return this.blameInfos;
}-*/;
public final native void setBlameInfo(JsArray<BlameInfo> blameInfos) /*-{
this['blameInfos'] = blameInfos;
}-*/;
public final void toggleAnnotation() {
toggleAnnotation(getBlameInfo());
}
public final void toggleAnnotation(JsArray<BlameInfo> blameInfos) {
if (isAnnotated()) {
clearAnnotations();
} else {
setAnnotations(blameInfos);
}
}
}

View File

@@ -0,0 +1,54 @@
// Copyright (C) 2016 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package net.codemirror.lib;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.dom.client.StyleInjector;
public class LintLine extends JavaScriptObject {
public static LintLine create(String shortMsg, String msg, String sev,
Pos line) {
StyleInjector.inject(".CodeMirror-lint-marker-" + sev + " {\n"
+ " visibility: hidden;\n"
+ " text-overflow: ellipsis;\n"
+ " white-space: nowrap;\n"
+ " overflow: hidden;\n"
+ " position: relative;\n"
+ "}\n"
+ ".CodeMirror-lint-marker-" + sev + ":after {\n"
+ " content:'" + shortMsg + "';\n"
+ " visibility: visible;\n"
+ "}");
return create(msg, sev, line, null);
}
public static native LintLine create(String msg, String sev, Pos f, Pos t) /*-{
return {
message : msg,
severity : sev,
from : f,
to : t
};
}-*/;
public final native String message() /*-{ return this.message; }-*/;
public final native String detailedMessage() /*-{ return this.message; }-*/;
public final native String severity() /*-{ return this.severity; }-*/;
public final native Pos from() /*-{ return this.from; }-*/;
public final native Pos to() /*-{ return this.to; }-*/;
protected LintLine() {
}
}

View File

@@ -16,6 +16,8 @@
@external .CodeMirror;
@external .CodeMirror-lines;
@external .CodeMirror-linenumber;
@external .CodeMirror-lint-markers;
@external .CodeMirror-lint-tooltip;
@external .CodeMirror-overlayscroll-horizontal;
@external .CodeMirror-overlayscroll-vertical;
@external .CodeMirror-scrollbar-filler;
@@ -94,3 +96,29 @@
z-index: 2;
cursor: text;
}
.CodeMirror-lint-markers {
width: 250px;
}
.CodeMirror-lint-tooltip {
background-color: infobackground;
border: 1px solid black;
border-radius: 4px 4px 4px 4px;
color: infotext;
font-family: monospace;
font-size: 10pt;
overflow: hidden;
padding: 2px 5px;
position: fixed;
white-space: pre;
white-space: pre-wrap;
z-index: 100;
max-width: 600px;
opacity: 0;
transition: opacity .4s;
-moz-transition: opacity .4s;
-webkit-transition: opacity .4s;
-o-transition: opacity .4s;
-ms-transition: opacity .4s;
}

View File

@@ -34,6 +34,7 @@ java_library(
'//gerrit-util-ssl:ssl',
'//lib:args4j',
'//lib:automaton',
'//lib:blame-cache',
'//lib:grappa',
'//lib:gson',
'//lib:guava',

View File

@@ -0,0 +1,164 @@
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.change;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.gerrit.extensions.common.BlameInfo;
import com.google.gerrit.extensions.common.RangeInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.CacheControl;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MergeUtil;
import com.google.gerrit.server.patch.AutoMerger;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gitiles.blame.BlameCache;
import com.google.gitiles.blame.Region;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.merge.ThreeWayMergeStrategy;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.kohsuke.args4j.Option;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class GetBlame implements RestReadView<FileResource> {
private final GitRepositoryManager repoManager;
private final BlameCache blameCache;
private final boolean allowBlame;
private final ThreeWayMergeStrategy mergeStrategy;
private final AutoMerger autoMerger;
@Option(name = "--base", aliases = {"-b"},
usage = "whether to load the blame of the base revision (the direct"
+ " parent of the change) instead of the change")
private boolean base;
@Inject
GetBlame(GitRepositoryManager repoManager,
BlameCache blameCache,
@GerritServerConfig Config cfg,
AutoMerger autoMerger) {
this.repoManager = repoManager;
this.blameCache = blameCache;
this.mergeStrategy = MergeUtil.getMergeStrategy(cfg);
this.autoMerger = autoMerger;
allowBlame = cfg.getBoolean("change", "allowBlame", true);
}
@Override
public Response<List<BlameInfo>> apply(FileResource resource)
throws RestApiException, OrmException, IOException,
InvalidChangeOperationException {
if (!allowBlame) {
throw new BadRequestException("blame is disabled");
}
Project.NameKey project = resource.getRevision().getChange().getProject();
try (Repository repository = repoManager.openRepository(project);
RevWalk revWalk = new RevWalk(repository)) {
String refName = resource.getRevision().getEdit().isPresent()
? resource.getRevision().getEdit().get().getRefName()
: resource.getRevision().getPatchSet().getRefName();
Ref ref = repository.findRef(refName);
if (ref == null) {
throw new ResourceNotFoundException("unknown ref " + refName);
}
ObjectId objectId = ref.getObjectId();
RevCommit revCommit = revWalk.parseCommit(objectId);
RevCommit[] parents = revCommit.getParents();
String path = resource.getPatchKey().getFileName();
List<BlameInfo> result;
if (!base) {
result = blame(revCommit, path, repository, revWalk);
} else if (parents.length == 0) {
throw new ResourceNotFoundException("Initial commit doesn't have base");
} else if (parents.length == 1) {
result = blame(parents[0], path, repository, revWalk);
} else if (parents.length == 2) {
ObjectId automerge = autoMerger.merge(repository, revWalk, revCommit,
mergeStrategy);
result = blame(automerge, path, repository, revWalk);
} else {
throw new ResourceNotFoundException(
"Cannot generate blame for merge commit with more than 2 parents");
}
Response<List<BlameInfo>> r = Response.ok(result);
if (resource.isCacheable()) {
r.caching(CacheControl.PRIVATE(7, TimeUnit.DAYS));
}
return r;
}
}
private List<BlameInfo> blame(ObjectId id, String path,
Repository repository, RevWalk revWalk) throws IOException {
ListMultimap<BlameInfo, RangeInfo> ranges = ArrayListMultimap.create();
List<BlameInfo> result = new ArrayList<>();
if (blameCache.findLastCommit(repository, id, path) == null) {
return result;
}
List<Region> blameRegions = blameCache.get(repository, id, path);
int from = 1;
for (Region region : blameRegions) {
RevCommit commit = revWalk.parseCommit(region.getSourceCommit());
BlameInfo blameInfo = toBlameInfo(commit, region.getSourceAuthor());
ranges.put(blameInfo, new RangeInfo(from, from + region.getCount() - 1));
from += region.getCount();
}
for (BlameInfo key : ranges.keySet()) {
key.ranges = ranges.get(key);
result.add(key);
}
return result;
}
private static BlameInfo toBlameInfo(RevCommit commit,
PersonIdent sourceAuthor) {
BlameInfo blameInfo = new BlameInfo();
blameInfo.author = sourceAuthor.getName();
blameInfo.id = commit.getName();
blameInfo.commitMsg = commit.getFullMessage();
blameInfo.time = commit.getCommitTime();
return blameInfo;
}
}

View File

@@ -114,6 +114,7 @@ public class Module extends RestApiModule {
get(FILE_KIND, "content").to(GetContent.class);
get(FILE_KIND, "download").to(DownloadContent.class);
get(FILE_KIND, "diff").to(GetDiff.class);
get(FILE_KIND, "blame").to(GetBlame.class);
child(CHANGE_KIND, "edit").to(ChangeEdits.class);
delete(CHANGE_KIND, "edit").to(DeleteChangeEdit.class);

View File

@@ -145,6 +145,8 @@ import com.google.gerrit.server.validators.GroupCreationValidationListener;
import com.google.gerrit.server.validators.HashtagValidationListener;
import com.google.gerrit.server.validators.OutgoingEmailValidationListener;
import com.google.gerrit.server.validators.ProjectCreationValidationListener;
import com.google.gitiles.blame.BlameCache;
import com.google.gitiles.blame.BlameCacheImpl;
import com.google.inject.Inject;
import com.google.inject.TypeLiteral;
import com.google.inject.internal.UniqueAnnotations;
@@ -172,6 +174,7 @@ public class GerritGlobalModule extends FactoryModule {
bind(IdGenerator.class);
bind(RulesCache.class);
bind(BlameCache.class).to(BlameCacheImpl.class);
bind(Sequences.class);
install(authModule);
install(AccountByEmailCacheImpl.module());

View File

@@ -167,6 +167,7 @@ public class GetServerInfo implements RestReadView<ConfigResource> {
private ChangeConfigInfo getChangeInfo(Config cfg) {
ChangeConfigInfo info = new ChangeConfigInfo();
info.allowBlame = toBoolean(cfg.getBoolean("change", "allowBlame", true));
info.allowDrafts = toBoolean(cfg.getBoolean("change", "allowDrafts", true));
info.largeChange = cfg.getInt("change", "largeChange", 500);
info.replyTooltip =
@@ -358,6 +359,7 @@ public class GetServerInfo implements RestReadView<ConfigResource> {
}
public static class ChangeConfigInfo {
public Boolean allowBlame;
public Boolean allowDrafts;
public int largeChange;
public String replyLabel;

View File

@@ -261,3 +261,11 @@ maven_jar(
sha1 = 'd9a09f7732226af26bf99f19e2cffe0ae219db5b',
license = 'DO_NOT_DISTRIBUTE',
)
maven_jar(
name = 'blame-cache',
id = 'com/google/gitiles:blame-cache:0.1-9',
sha1 = '51d35e6f8bbc2412265066cea9653dd758c95826',
license = 'Apache2.0',
repository = GERRIT,
)

View File

@@ -3,6 +3,7 @@ CM_CSS = [
'addon/dialog/dialog.css',
'addon/scroll/simplescrollbars.css',
'addon/search/matchesonscrollbar.css',
'addon/lint/lint.css',
]
CM_JS = [
@@ -28,6 +29,7 @@ CM_ADDONS = [
'mode/multiplex.js',
'mode/overlay.js',
'mode/simple.js',
'lint/lint.js',
]
# Available themes must be enumerated here,