diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt index cd9ee3e499..47a9a0b47d 100644 --- a/Documentation/config-gerrit.txt +++ b/Documentation/config-gerrit.txt @@ -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, diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt index 87cccabb21..8c89b1ec73 100644 --- a/Documentation/rest-api-changes.txt +++ b/Documentation/rest-api-changes.txt @@ -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. diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt index 3a64dc0b7a..661abb02e5 100644 --- a/Documentation/rest-api-config.txt +++ b/Documentation/rest-api-config.txt @@ -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]. diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/BlameInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/BlameInfo.java new file mode 100644 index 0000000000..df3f373ed5 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/BlameInfo.java @@ -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 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(); + } +} diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RangeInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RangeInfo.java new file mode 100644 index 0000000000..5268825b14 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RangeInfo.java @@ -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; + } +} diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/Resources.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/Resources.java index ae7114fcb7..95751fab13 100644 --- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/Resources.java +++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/Resources.java @@ -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(); diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ServerInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ServerInfo.java index 66a859aa89..c91d08d567 100644 --- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ServerInfo.java +++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ServerInfo.java @@ -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; }-*/; diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/RangeInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/RangeInfo.java new file mode 100644 index 0000000000..f86d8f4225 --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/RangeInfo.java @@ -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() { + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/blame/BlameInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/blame/BlameInfo.java new file mode 100644 index 0000000000..bbd939a69a --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/blame/BlameInfo.java @@ -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 ranges() /*-{ return this.ranges; }-*/; + + protected BlameInfo() { + } + +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java index b7c8a5e28b..b181341bf3 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java @@ -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"; diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java index a6ff06de41..4374986ed0 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java @@ -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 list, DiffInfo info, boolean editExists, boolean current, boolean open, boolean binary) { this.changeType = info.changeType(); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java index 216115a838..d2b740eac1 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java @@ -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>() { + + @Override + public void onSuccess(JsArray 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; diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java index 5dc1661fa5..fff556b0b5 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java @@ -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()); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java index 0bc42cbb49..b1991693d7 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java @@ -54,6 +54,7 @@ public interface PatchConstants extends Constants { String download(); String edit(); + String blame(); String addFileCommentToolTip(); String cannedReplyDone(); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties index 9f7c62eebb..13f0afac89 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties @@ -37,6 +37,7 @@ nextFileHelp = Next file download = Download edit = Edit +blame = Blame addFileCommentToolTip = Click to add file comment sideBySideDiff = Side-by-side diff diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/BlameConfig.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/BlameConfig.java new file mode 100644 index 0000000000..7418795d4c --- /dev/null +++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/BlameConfig.java @@ -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); +} diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/BlameConfig.properties b/gerrit-gwtui/src/main/java/net/codemirror/lib/BlameConfig.properties new file mode 100644 index 0000000000..658b50f60f --- /dev/null +++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/BlameConfig.properties @@ -0,0 +1,2 @@ +shortBlameMsg={0} {1} {2} +detailedBlameMsg=commit {0}\nAuthor: {1}\nDate: {2}\n\n{3} diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/Extras.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/Extras.java index f2fc074198..a5d170b0bc 100644 --- a/gerrit-gwtui/src/main/java/net/codemirror/lib/Extras.java +++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/Extras.java @@ -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 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 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 annotations) /*-{ + return { + getAnnotations: function(text, options, cm) { return annotations; } + }; + }-*/; + + public final native JsArray getBlameInfo() /*-{ + return this.blameInfos; + }-*/; + + public final native void setBlameInfo(JsArray blameInfos) /*-{ + this['blameInfos'] = blameInfos; + }-*/; + + public final void toggleAnnotation() { + toggleAnnotation(getBlameInfo()); + } + + public final void toggleAnnotation(JsArray blameInfos) { + if (isAnnotated()) { + clearAnnotations(); + } else { + setAnnotations(blameInfos); + } + } } diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/LintLine.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/LintLine.java new file mode 100644 index 0000000000..b1e20c1adf --- /dev/null +++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/LintLine.java @@ -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() { + } +} diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/style.css b/gerrit-gwtui/src/main/java/net/codemirror/lib/style.css index aa4c0021dc..022a800762 100644 --- a/gerrit-gwtui/src/main/java/net/codemirror/lib/style.css +++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/style.css @@ -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; +} diff --git a/gerrit-server/BUCK b/gerrit-server/BUCK index 922552019b..6aa9e2c414 100644 --- a/gerrit-server/BUCK +++ b/gerrit-server/BUCK @@ -34,6 +34,7 @@ java_library( '//gerrit-util-ssl:ssl', '//lib:args4j', '//lib:automaton', + '//lib:blame-cache', '//lib:grappa', '//lib:gson', '//lib:guava', diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetBlame.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetBlame.java new file mode 100644 index 0000000000..abed01b55d --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetBlame.java @@ -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 { + + 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> 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 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> r = Response.ok(result); + if (resource.isCacheable()) { + r.caching(CacheControl.PRIVATE(7, TimeUnit.DAYS)); + } + return r; + } + } + + private List blame(ObjectId id, String path, + Repository repository, RevWalk revWalk) throws IOException { + ListMultimap ranges = ArrayListMultimap.create(); + List result = new ArrayList<>(); + if (blameCache.findLastCommit(repository, id, path) == null) { + return result; + } + + List 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; + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java index 4c9c0bf9b3..41b1019c4c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java @@ -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); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java index 49f69b0778..39fd617af7 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java @@ -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()); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java index d8a3984030..6df7a2ea2a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java @@ -167,6 +167,7 @@ public class GetServerInfo implements RestReadView { 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 { } public static class ChangeConfigInfo { + public Boolean allowBlame; public Boolean allowDrafts; public int largeChange; public String replyLabel; diff --git a/lib/BUCK b/lib/BUCK index 1ce8759927..0c424ad096 100644 --- a/lib/BUCK +++ b/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, +) diff --git a/lib/codemirror/cm.defs b/lib/codemirror/cm.defs index bc7545c49a..969a29ceed 100644 --- a/lib/codemirror/cm.defs +++ b/lib/codemirror/cm.defs @@ -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,