Merge "MergeOp: Log IntegrationExceptions that are treated as conflict only as warning"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 1c84768..37bf3e1 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -3644,7 +3644,10 @@
 [[plugins.mandatory]]plugins.mandatory::
 +
 List of mandatory plugins. If a plugin from this list does not load,
-Gerrit start will fail.
+Gerrit will fail to start.
++
+Disabling and restarting of a mandatory plugin is rejected, but reloading
+of a mandatory plugin is still possible.
 
 [[plugins.jsLoadTimeout]]plugins.jsLoadTimeout::
 +
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
index 48bafdf..9e1744c 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -98,7 +98,7 @@
 Tag the plugins:
 
 ----
-  git submodule foreach git tag -s -m "v$version" "v$version"
+  git submodule foreach '[ "$path" == "modules/jgit" ] || git tag -s -m "v$version" "v$version"'
 ----
 
 [[build-gerrit]]
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
index b326834..1973b00 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
@@ -70,6 +70,7 @@
 import java.util.TimeZone;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.InvalidObjectIdException;
+import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.ObjectReader;
@@ -381,7 +382,14 @@
       throw new BadRequestException(String.format("Base %s doesn't represent a valid SHA-1", base));
     }
 
-    RevCommit baseCommit = revWalk.parseCommit(baseObjectId);
+    RevCommit baseCommit;
+    try {
+      baseCommit = revWalk.parseCommit(baseObjectId);
+    } catch (MissingObjectException e) {
+      throw new UnprocessableEntityException(
+          String.format("Base %s doesn't exist", baseObjectId.name()), e);
+    }
+
     InternalChangeQuery changeQuery = queryProvider.get();
     changeQuery.enforceVisibility(true);
     List<ChangeData> changeDatas = changeQuery.byBranchCommit(project, destRef.getName(), base);
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 2d94566..32941ff 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -53,6 +53,7 @@
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.PatchSetApproval;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
 import com.google.gerrit.extensions.api.changes.DraftApi;
@@ -974,6 +975,42 @@
   }
 
   @Test
+  public void cherryPickToNonExistingBranch() throws Exception {
+    PushOneCommit.Result result = createChange();
+
+    CherryPickInput input = new CherryPickInput();
+    input.message = "foo bar";
+    input.destination = "non-existing";
+    // TODO(ekempin): This should rather result in an UnprocessableEntityException.
+    BadRequestException thrown =
+        assertThrows(
+            BadRequestException.class,
+            () -> gApi.changes().id(result.getChangeId()).current().cherryPick(input));
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo(
+            String.format("Branch %s does not exist.", RefNames.REFS_HEADS + input.destination));
+  }
+
+  @Test
+  public void cherryPickToNonExistingBaseCommit() throws Exception {
+    createBranch(BranchNameKey.create(project, "foo"));
+    PushOneCommit.Result result = createChange();
+
+    CherryPickInput input = new CherryPickInput();
+    input.message = "foo bar";
+    input.destination = "foo";
+    input.base = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
+    UnprocessableEntityException thrown =
+        assertThrows(
+            UnprocessableEntityException.class,
+            () -> gApi.changes().id(result.getChangeId()).current().cherryPick(input));
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo(String.format("Base %s doesn't exist", input.base));
+  }
+
+  @Test
   public void canRebase() throws Exception {
     PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result r1 = push.to("refs/for/master");
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
index 383687f..25c7738 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
@@ -259,12 +259,14 @@
         lastNotify: {left: 1, right: 1},
       };
 
+      const rangesCache = new Map();
+
       this._processPromise = util.makeCancelable(this._loadHLJS()
           .then(() => {
             return new Promise(resolve => {
               const nextStep = () => {
                 this._processHandle = null;
-                this._processNextLine(state);
+                this._processNextLine(state, rangesCache);
 
                 // Move to the next line in the section.
                 state.lineIndex++;
@@ -321,12 +323,21 @@
      * Highlight.js emits and emit a list of text ranges and classes for the
      * markers.
      * @param {string} str The string of HTML.
+     * @param {Map<string, !Array<!Object>>} rangesCache A map for caching
+     * ranges for each string. A cache is read and written by this method.
+     * Since diff is mostly comparing same file on two sides, there is good rate
+     * of duplication at least for parts that are on left and right parts.
      * @return {!Array<!Object>} The list of ranges.
      */
-    _rangesFromString(str) {
+    _rangesFromString(str, rangesCache) {
+      const cached = rangesCache.get(str);
+      if (cached) return cached;
+
       const div = document.createElement('div');
       div.innerHTML = str;
-      return this._rangesFromElement(div, 0);
+      const ranges = this._rangesFromElement(div, 0);
+      rangesCache.set(str, ranges);
+      return ranges;
     },
 
     _rangesFromElement(elem, offset) {
@@ -357,7 +368,7 @@
      * lines).
      * @param {!Object} state The processing state for the layer.
      */
-    _processNextLine(state) {
+    _processNextLine(state, rangesCache) {
       let baseLine;
       let revisionLine;
 
@@ -386,7 +397,8 @@
         baseLine = this._workaround(this._baseLanguage, baseLine);
         result = this._hljs.highlight(this._baseLanguage, baseLine, true,
             state.baseContext);
-        this.push('_baseRanges', this._rangesFromString(result.value));
+        this.push('_baseRanges',
+            this._rangesFromString(result.value, rangesCache));
         state.baseContext = result.top;
       }
 
@@ -395,7 +407,8 @@
         revisionLine = this._workaround(this._revisionLanguage, revisionLine);
         result = this._hljs.highlight(this._revisionLanguage, revisionLine,
             true, state.revisionContext);
-        this.push('_revisionRanges', this._rangesFromString(result.value));
+        this.push('_revisionRanges',
+            this._rangesFromString(result.value, rangesCache));
         state.revisionContext = result.top;
       }
     },
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
index 472db21..4e3492f 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
@@ -388,10 +388,20 @@
         '<span class="non-whtelisted-class">',
         '<span class="gr-diff gr-syntax gr-syntax-keyword">public</span>',
         '</span>'].join('');
-      const result = element._rangesFromString(str);
+      const result = element._rangesFromString(str, new Map());
       assert.notEqual(result.length, 0);
     });
 
+    test('_rangesFromString cache same syntax markers', () => {
+      sandbox.spy(element, '_rangesFromElement');
+      const str =
+        '<span class="gr-diff gr-syntax gr-syntax-keyword">public</span>';
+      const cacheMap = new Map();
+      element._rangesFromString(str, cacheMap);
+      element._rangesFromString(str, cacheMap);
+      assert.isTrue(element._rangesFromElement.calledOnce);
+    });
+
     test('_isSectionDone', () => {
       let state = {sectionIndex: 0, lineIndex: 0};
       assert.isFalse(element._isSectionDone(state));