Merge "Prevent "test.git.git" turning into "test.git" in the Changes table."
diff --git a/ReleaseNotes/ReleaseNotes-2.8.txt b/ReleaseNotes/ReleaseNotes-2.8.txt
index 59c807e..1c63983f 100644
--- a/ReleaseNotes/ReleaseNotes-2.8.txt
+++ b/ReleaseNotes/ReleaseNotes-2.8.txt
@@ -70,6 +70,9 @@
 information and enforce them on the user profile during login and beyond.
 
 * link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/config_gerrit.html#_a_id_httpd_a_section_httpd[
+Customizable registration page for HTTP authentication].
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/config_gerrit.html#_a_id_httpd_a_section_httpd[
 Configurable external `robots.txt` file].
 
 * Support for
@@ -249,6 +252,9 @@
 * link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-projects.html#get-child-project[
 Get child project]
 
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-projects.html#set-config[
+Set configuration]
+
 
 Capabilities
 ~~~~~~~~~~~~
@@ -278,6 +284,9 @@
 * The `RebasedPatchSet` template is removed.  Email notifications for rebased
 changes are now sent with the `ReplacePatchSet` template.
 
+* Comment notification emails now include context of comments that are replied
+to, and links to the file(s) in which comments are made.
+
 
 Plugins
 ~~~~~~~
@@ -383,6 +392,9 @@
 Since there can be multiple changes with the same commit on different branches,
 use the parent change on the same branch during rebase.
 
+* link:https://code.google.com/p/gerrit/issues/detail?id=600[Issue 600]:
+Fix change stuck in SUBMITTED state but actually merged.
+
 Configuration
 ~~~~~~~~~~~~~
 
@@ -400,6 +412,9 @@
 * link:https://code.google.com/p/gerrit/issues/detail?id=2045[Issue 2045]:
 Define user scope when parsing server config.
 
+* link:https://code.google.com/p/gerrit/issues/detail?id=1990[Issue 1990]:
+Support optional Certificate Revocation List (CRL) with `CLIENT_SSL_CERT_LDAP`.
+
 Web UI
 ~~~~~~
 
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 3c38374..e462e2b 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
@@ -42,6 +42,7 @@
     String hideNumber();
     String range();
     String rangeHighlight();
+    String showtabs();
   }
 
   @UiField
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml
index 58940f2..4714fca 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml
@@ -21,7 +21,7 @@
     @external .CodeMirror, .CodeMirror-lines, .CodeMirror-selectedtext;
     @external .CodeMirror-linenumber, .CodeMirror-vscrollbar;
     @external .cm-keymap-fat-cursor, CodeMirror-cursor;
-    @external .cm-searching, .cm-trailingspace;
+    @external .cm-searching, .cm-trailingspace, .cm-tab;
     .difftable .CodeMirror-lines {
       padding: 0;
     }
@@ -108,6 +108,10 @@
       opacity: 0.8;
       z-index: 2;
     }
+    .showtabs .cm-tab:before {
+      content: "\00bb";
+      color: #f00;
+    }
   </ui:style>
   <g:HTMLPanel styleName='{style.difftable}'>
     <table class='{style.table}'>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide2.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide2.java
index e37e8c1..8972102 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide2.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide2.java
@@ -126,7 +126,7 @@
   private CommentLinkProcessor commentLinkProcessor;
   private Map<String, PublishedBox> publishedMap;
   private Map<LineHandle, CommentBox> lineActiveBoxMap;
-  private Map<LineHandle, PublishedBox> lineLastPublishedBoxMap;
+  private Map<LineHandle, List<PublishedBox>> linePublishedBoxesMap;
   private Map<LineHandle, PaddingManager> linePaddingManagerMap;
   private Map<LineHandle, LinePaddingWidgetWrapper> linePaddingOnOtherSideMap;
   private List<DiffChunkInfo> diffChunks;
@@ -297,8 +297,6 @@
       }
     });
     cm.addKeyMap(KeyMap.create()
-        .on("'j'", moveCursorDown(cm, 1))
-        .on("'k'", moveCursorDown(cm, -1))
         .on("'a'", openReplyBox())
         .on("'u'", upToChange())
         .on("'r'", toggleReviewed())
@@ -330,8 +328,9 @@
             new ShowHelpCommand().onKeyPress(null);
           }
         })
-        .on("Alt-N", diffChunkNav(cm, false))
-        .on("Alt-P", diffChunkNav(cm, true))
+        .on("N", maybeNextVimSearch(cm))
+        .on("P", diffChunkNav(cm, true))
+        .on("Shift-O", openClosePublished(cm))
         .on("Shift-Left", flipCursorSide(cm, true))
         .on("Shift-Right", flipCursorSide(cm, false)));
   }
@@ -416,7 +415,7 @@
     render(diffInfo);
     Collections.sort(diffChunks, getDiffChunkComparator());
     lineActiveBoxMap = new HashMap<LineHandle, CommentBox>();
-    lineLastPublishedBoxMap = new HashMap<LineHandle, PublishedBox>();
+    linePublishedBoxesMap = new HashMap<LineHandle, List<PublishedBox>>();
     linePaddingManagerMap = new HashMap<LineHandle, PaddingManager>();
     if (publishedBase != null || publishedRevision != null) {
       publishedMap = new HashMap<String, PublishedBox>();
@@ -442,6 +441,9 @@
         resizeCodeMirror();
       }
     });
+    if (pref.isShowTabs()) {
+      diffTable.addStyleName(DiffTable.style.showtabs());
+    }
   }
 
   private CodeMirror displaySide(DiffInfo.FileMeta meta, String contents,
@@ -628,8 +630,9 @@
   void removeDraft(DraftBox box, int line) {
     LineHandle handle = getCmFromSide(box.getSide()).getLineHandle(line);
     lineActiveBoxMap.remove(handle);
-    if (lineLastPublishedBoxMap.containsKey(handle)) {
-      lineActiveBoxMap.put(handle, lineLastPublishedBoxMap.get(handle));
+    if (linePublishedBoxesMap.containsKey(handle)) {
+      List<PublishedBox> list = linePublishedBoxesMap.get(handle);
+      lineActiveBoxMap.put(handle, list.get(list.size() - 1));
     }
   }
 
@@ -677,7 +680,13 @@
       }
       int line = info.line() - 1;
       LineHandle handle = cm.getLineHandle(line);
-      lineLastPublishedBoxMap.put(handle, box);
+      if (linePublishedBoxesMap.containsKey(handle)) {
+        linePublishedBoxesMap.get(handle).add(box);
+      } else {
+        List<PublishedBox> list = new ArrayList<PublishedBox>();
+        list.add(box);
+        linePublishedBoxesMap.put(handle, list);
+      }
       lineActiveBoxMap.put(handle, box);
       addCommentBox(info, box);
     }
@@ -1065,14 +1074,6 @@
     };
   }
 
-  private Runnable moveCursorDown(final CodeMirror cm, final int numLines) {
-    return new Runnable() {
-      public void run() {
-        cm.moveCursorDown(numLines);
-      }
-    };
-  }
-
   private Runnable openReplyBox() {
     return new Runnable() {
       public void run() {
@@ -1085,6 +1086,31 @@
     };
   }
 
+  private Runnable openClosePublished(final CodeMirror cm) {
+    return new Runnable() {
+      @Override
+      public void run() {
+        if (cm.hasActiveLine()) {
+          List<PublishedBox> list =
+              linePublishedBoxesMap.get(cm.getActiveLine());
+          if (list == null) {
+            return;
+          }
+          boolean open = false;
+          for (PublishedBox box : list) {
+            if (!box.isOpen()) {
+              open = true;
+              break;
+            }
+          }
+          for (PublishedBox box : list) {
+            box.setOpen(open);
+          }
+        }
+      }
+    };
+  }
+
   private Runnable upToChange() {
     return new Runnable() {
       public void run() {
@@ -1117,6 +1143,19 @@
     };
   }
 
+  private Runnable maybeNextVimSearch(final CodeMirror cm) {
+    return new Runnable() {
+      @Override
+      public void run() {
+        if (cm.hasVimSearchHighlight()) {
+          CodeMirror.handleVimKey(cm, "n");
+        } else {
+          diffChunkNav(cm, false).run();
+        }
+      }
+    };
+  }
+
   private Runnable diffChunkNav(final CodeMirror cm, final boolean prev) {
     return new Runnable() {
       @Override
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
index 8286ed6..4db989c 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
@@ -265,10 +265,6 @@
     return this.lineCount();
   }-*/;
 
-  public final native void moveCursorDown(int numLines) /*-{
-    this.moveV(numLines, "line");
-  }-*/;
-
   public final native Element getGutterElement() /*-{
     return this.getGutterElement();
   }-*/;
@@ -307,6 +303,19 @@
     $wnd.CodeMirror.keyMap[name] = km;
   }-*/;
 
+  public static final native void handleVimKey(CodeMirror cm, String key) /*-{
+    $wnd.CodeMirror.Vim.handleKey(cm, key);
+  }-*/;
+
+  public static final native void mapVimKey(String alias, String actual) /*-{
+    $wnd.CodeMirror.Vim.map(alias, actual);
+  }-*/;
+
+  public final native boolean hasVimSearchHighlight() /*-{
+    return this.state.vim && this.state.vim.searchState_ &&
+        !!this.state.vim.searchState_.getOverlay();
+  }-*/;
+
   protected CodeMirror() {
   }
 
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/Loader.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/Loader.java
index 37adf1e..2a515b7 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/Loader.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/Loader.java
@@ -90,10 +90,14 @@
   private static void initVimKeys() {
     // TODO: Better custom keybindings, remove temporary navigation hacks.
     KeyMap km = CodeMirror.cloneKeyMap("vim");
-    for (String s : new String[] {"A", "C", "J", "K", "O", "R", "U", "Ctrl-C"}) {
+    for (String s : new String[] {"A", "C", "O", "R", "U", "Ctrl-C"}) {
       km.remove(s);
     }
     CodeMirror.addKeyMap("vim_ro", km);
+    CodeMirror.mapVimKey("j", "gj");
+    CodeMirror.mapVimKey("k", "gk");
+    CodeMirror.mapVimKey("Down", "gj");
+    CodeMirror.mapVimKey("Up", "gk");
   }
 
   private static void error(Exception e) {
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
index 91dd015..f809f49 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
@@ -114,6 +114,13 @@
       throw fail(e);
     }
 
+    if (!sitePaths.index_dir.exists()) {
+      throw new ProvisionException("No index versions ready; run Reindex");
+    } else if (!sitePaths.index_dir.isDirectory()) {
+      log.warn("Not a directory: %s", sitePaths.index_dir.getAbsolutePath());
+      throw new ProvisionException("No index versions ready; run Reindex");
+    }
+
     TreeMap<Integer, Version> versions = scanVersions(cfg);
     // Search from the most recent ready version.
     // Write to the most recent ready version and the most recent version.
@@ -162,7 +169,7 @@
     for (Schema<ChangeData> schema : ChangeSchemas.ALL.values()) {
       File f = getDir(sitePaths, schema);
       boolean exists = f.exists() && f.isDirectory();
-      if (exists && !f.isDirectory()) {
+      if (f.exists() && !f.isDirectory()) {
         log.warn("Not a directory: %s", f.getAbsolutePath());
       }
       int v = schema.getVersion();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
index 4277307..5b3f59a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
@@ -231,25 +231,27 @@
   }
 
   private void appendQuotedParent(StringBuilder out, PatchLineComment child) {
-    PatchLineComment parent;
-    try {
-      parent = args.db.get().patchComments().get(
-          new PatchLineComment.Key(
-              child.getKey().getParentKey(),
-              child.getParentUuid()));
-    } catch (OrmException e) {
-      parent = null;
-    }
-    if (parent != null) {
-      String msg = parent.getMessage().trim();
-      if (msg.length() > 75) {
-        msg = msg.substring(0, 75);
+    if (child.getParentUuid() != null) {
+      PatchLineComment parent;
+      try {
+        parent = args.db.get().patchComments().get(
+            new PatchLineComment.Key(
+                child.getKey().getParentKey(),
+                child.getParentUuid()));
+      } catch (OrmException e) {
+        parent = null;
       }
-      int lf = msg.indexOf('\n');
-      if (lf > 0) {
-        msg = msg.substring(0, lf);
+      if (parent != null) {
+        String msg = parent.getMessage().trim();
+        if (msg.length() > 75) {
+          msg = msg.substring(0, 75);
+        }
+        int lf = msg.indexOf('\n');
+        if (lf > 0) {
+          msg = msg.substring(0, lf);
+        }
+        out.append("> ").append(msg).append('\n');
       }
-      out.append("> ").append(msg).append('\n');
     }
   }
 
diff --git a/plugins/cookbook-plugin b/plugins/cookbook-plugin
index cae2762..ce6467a 160000
--- a/plugins/cookbook-plugin
+++ b/plugins/cookbook-plugin
@@ -1 +1 @@
-Subproject commit cae2762b4882d5ba34ac3f2773f21d2acd748dfe
+Subproject commit ce6467a8e68ddfc564a106c63e3cf0f92a071229