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:

committed by
Michael Zhou

parent
b3ba766ffa
commit
b72d4c6d8f
@@ -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,
|
||||
|
@@ -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.
|
||||
|
@@ -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].
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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();
|
||||
|
||||
|
@@ -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; }-*/;
|
||||
|
@@ -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() {
|
||||
}
|
||||
}
|
@@ -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() {
|
||||
}
|
||||
|
||||
}
|
@@ -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";
|
||||
|
@@ -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();
|
||||
|
@@ -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;
|
||||
|
@@ -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());
|
||||
|
@@ -54,6 +54,7 @@ public interface PatchConstants extends Constants {
|
||||
|
||||
String download();
|
||||
String edit();
|
||||
String blame();
|
||||
String addFileCommentToolTip();
|
||||
|
||||
String cannedReplyDone();
|
||||
|
@@ -37,6 +37,7 @@ nextFileHelp = Next file
|
||||
|
||||
download = Download
|
||||
edit = Edit
|
||||
blame = Blame
|
||||
addFileCommentToolTip = Click to add file comment
|
||||
|
||||
sideBySideDiff = Side-by-side diff
|
||||
|
23
gerrit-gwtui/src/main/java/net/codemirror/lib/BlameConfig.java
vendored
Normal file
23
gerrit-gwtui/src/main/java/net/codemirror/lib/BlameConfig.java
vendored
Normal 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);
|
||||
}
|
2
gerrit-gwtui/src/main/java/net/codemirror/lib/BlameConfig.properties
vendored
Normal file
2
gerrit-gwtui/src/main/java/net/codemirror/lib/BlameConfig.properties
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
shortBlameMsg={0} {1} {2}
|
||||
detailedBlameMsg=commit {0}\nAuthor: {1}\nDate: {2}\n\n{3}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
54
gerrit-gwtui/src/main/java/net/codemirror/lib/LintLine.java
vendored
Normal file
54
gerrit-gwtui/src/main/java/net/codemirror/lib/LintLine.java
vendored
Normal 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() {
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
@@ -34,6 +34,7 @@ java_library(
|
||||
'//gerrit-util-ssl:ssl',
|
||||
'//lib:args4j',
|
||||
'//lib:automaton',
|
||||
'//lib:blame-cache',
|
||||
'//lib:grappa',
|
||||
'//lib:gson',
|
||||
'//lib:guava',
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
|
@@ -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());
|
||||
|
@@ -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;
|
||||
|
8
lib/BUCK
8
lib/BUCK
@@ -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,
|
||||
)
|
||||
|
@@ -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,
|
||||
|
Reference in New Issue
Block a user