Merge "Update org.pegdown:pegdown to 1.2.1"
diff --git a/Documentation/intro-change-screen.txt b/Documentation/intro-change-screen.txt
index a85a0a6..a8f46e4 100644
--- a/Documentation/intro-change-screen.txt
+++ b/Documentation/intro-change-screen.txt
@@ -225,12 +225,25 @@
 
 * Related Changes
 
-This tab page shows open changes on which the current change depends and open
-changes that depend on the current change. For merge commits it shows also the
-closed changes that are going to be merged into the destination branch by
-accepting the merge commit. It is kind of a dependency too, but not in the
-sense of "these changes have to be submitted in order to submit this
-change".
+This tab page shows changes on which the current change depends (ancestors) and
+open changes that depend on the current change (descendants). For merge commits
+it shows also the closed changes that are going to be merged into the
+destination branch by accepting the merge commit. It is kind of a dependency
+too, but not in the sense of "these changes have to be submitted in order to
+submit this change".
+
+Indicators are appended on the commit message headlines of related changes to
+signify dependencies on outdated changes, or commits that are not associated to
+changes under review:
+
+If the selected patch set of a change is older than its latest patch set,
+the change is marked with an orange dot.
+
+If a descendant change depends on a patch set that is older than the selected
+patch set of a change, the descendent change is marked with a tilde (~).
+
+If an ancestor commit is not associated to a Gerrit change, i.e. has been pushed
+directly to the repository bypassing review, it is marked with a black dot.
 
 This tab is only visible if related changes for the current active
 change exist.
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 81df883..cd79812 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -182,8 +182,8 @@
 +
 Changes that match 'TEXT' string in any comment left by a reviewer.
 
-[[file]]
-file:'PATH', f:'PATH'::
+[[path]]
+path:'PATH'::
 +
 Matches any change touching file at 'PATH'. By default exact path
 matching is used, but regular expressions can be enabled by starting
@@ -202,6 +202,18 @@
 files named like 'name1.xml', 'name2.xml', and 'name3.xml' use
 `file:"^name[1-3].xml"`.
 
+[[file]]
+file:'NAME', f:'NAME'::
++
+Matches any change touching a file containing the path component
+'NAME'.  For example a `file:src` will match changes that modify
+files named `gerrit-server/src/main/java/Foo.java`. Name matching
+is exact match, `file:Foo.java` finds any change touching a file
+named exactly `Foo.java` and does not match `AbstractFoo.java`.
++
+Regular expression matching can be enabled by starting the string
+with `^`. In this mode `file:` is an alias of `path:` (see above).
+
 [[has]]
 has:draft::
 +
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritServer.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritServer.java
index 9a138f0..6caa2a6 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritServer.java
@@ -170,15 +170,7 @@
   }
 
   private static InetAddress getLocalHost() throws UnknownHostException {
-    try {
-      return InetAddress.getLocalHost();
-    } catch (UnknownHostException e1) {
-      try {
-        return InetAddress.getByName("localhost");
-      } catch (UnknownHostException e2) {
-        return InetAddress.getByName("127.0.0.1");
-      }
-    }
+    return InetAddress.getLoopbackAddress();
   }
 
   private File sitePath;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
index 7972e52..9a6db2f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
@@ -276,7 +276,7 @@
 
   public static GitwebLink getGitwebLink() {
     GitwebConfig gw = getConfig().getGitwebLink();
-    return gw != null ? new GitwebLink(gw) : null;
+    return gw != null && gw.type != null ? new GitwebLink(gw) : null;
   }
 
   /** Site theme information (site specific colors)/ */
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java
index 4b7e14a..52b988f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java
@@ -52,7 +52,7 @@
   }
 
   @UiField Style style;
-  @UiField Element commitName;
+  @UiField CopyableLabel commitName;
   @UiField AnchorElement browserLink;
   @UiField InlineHyperlink authorNameEmail;
   @UiField Element authorDate;
@@ -99,7 +99,7 @@
       }
     }
 
-    commitName.setInnerText(revision);
+    commitName.setText(revision);
     idText.setText("Change-Id: " + change.change_id());
     idText.setPreviewText(change.change_id());
     formatLink(commit.author(), authorNameEmail,
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.ui.xml
index 8b543ba..c02e9f8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.ui.xml
@@ -100,7 +100,7 @@
               <ui:attribute name='title'/>
             </x:InlineHyperlink>
         </td>
-        <td ui:field='authorDate' class='{style.date}'/>
+        <td ui:field='authorDate' class='{style.date}' colspan="2"/>
       </tr>
       <tr>
         <th><ui:msg>Committer</ui:msg></th>
@@ -109,12 +109,12 @@
               <ui:attribute name='title'/>
             </x:InlineHyperlink>
         </td>
-        <td ui:field='committerDate' class='{style.date}'/>
+        <td ui:field='committerDate' class='{style.date}' colspan="2"/>
       </tr>
       <tr>
         <th><ui:msg>Commit</ui:msg></th>
-        <td ui:field='commitName'/>
-        <td><a ui:field='browserLink' href=""/></td>
+        <td><clippy:CopyableLabel styleName='{style.clippy}' ui:field='commitName'/></td>
+        <td><a style="margin-left:16px;" ui:field='browserLink' href=""/></td>
       </tr>
       <tr>
         <th><ui:msg>Change-Id</ui:msg></th>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/History.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/History.java
index c461ae9..62652de 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/History.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/History.java
@@ -104,7 +104,7 @@
 
     for (String path : map.keySet()) {
       for (CommentInfo c : Natives.asList(map.get(path))) {
-        c.setPath(path);
+        c.path(path);
         if (c.author() != null) {
           AuthorRevision k = new AuthorRevision(c.author(), id);
           List<CommentInfo> l = byAuthor.get(k);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
index dfece94..77e6867 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
@@ -428,7 +428,7 @@
 
   private static List<CommentInfo> copyPath(String path, JsArray<CommentInfo> l) {
     for (int i = 0; i < l.length(); i++) {
-      l.get(i).setPath(path);
+      l.get(i).path(path);
     }
     return Natives.asList(l);
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentApi.java
index d87cb5e..66cd485 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentApi.java
@@ -43,13 +43,13 @@
     revision(id, "drafts").id(draftId).get(cb);
   }
 
-  public static void createDraft(PatchSet.Id id, CommentInput content,
+  public static void createDraft(PatchSet.Id id, CommentInfo content,
       AsyncCallback<CommentInfo> cb) {
     revision(id, "drafts").put(content, cb);
   }
 
   public static void updateDraft(PatchSet.Id id, String draftId,
-      CommentInput content, AsyncCallback<CommentInfo> cb) {
+      CommentInfo content, AsyncCallback<CommentInfo> cb) {
     revision(id, "drafts").id(draftId).put(content, cb);
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java
index 0879a50..e1ec47a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java
@@ -23,42 +23,65 @@
 import java.sql.Timestamp;
 
 public class CommentInfo extends JavaScriptObject {
-  public static CommentInfo createRange(String path, Side side, int line,
-      String in_reply_to, String message, CommentRange range) {
-    CommentInfo info = createFile(path, side, in_reply_to, message);
-    info.setRange(range);
-    info.setLine(range == null ? line : range.end_line());
-    return info;
+  public static CommentInfo create(String path, Side side,
+      int line, CommentRange range) {
+    CommentInfo n = createObject().cast();
+    n.path(path);
+    n.side(side);
+    if (range != null) {
+      n.line(range.end_line());
+      n.range(range);
+    } else if (line > 0) {
+      n.line(line);
+    }
+    return n;
   }
 
-  public static CommentInfo createFile(String path, Side side,
-      String in_reply_to, String message) {
-    CommentInfo info = createObject().cast();
-    info.setPath(path);
-    info.setSide(side);
-    info.setInReplyTo(in_reply_to);
-    info.setMessage(message);
-    return info;
+  public static CommentInfo createReply(CommentInfo r) {
+    CommentInfo n = createObject().cast();
+    n.path(r.path());
+    n.side(r.side());
+    n.in_reply_to(r.id());
+    if (r.has_range()) {
+      n.line(r.range().end_line());
+      n.range(r.range());
+    } else if (r.has_line()) {
+      n.line(r.line());
+    }
+    return n;
   }
 
-  private final native void setId(String id) /*-{ this.id = id; }-*/;
-  public final native void setPath(String path) /*-{ this.path = path; }-*/;
-
-  private final void setSide(Side side) {
-    setSideRaw(side.toString());
+  public static CommentInfo copy(CommentInfo s) {
+    CommentInfo n = createObject().cast();
+    n.path(s.path());
+    n.side(s.side());
+    n.id(s.id());
+    n.in_reply_to(s.in_reply_to());
+    n.message(s.message());
+    if (s.has_range()) {
+      n.line(s.range().end_line());
+      n.range(s.range());
+    } else if (s.has_line()) {
+      n.line(s.line());
+    }
+    return n;
   }
-  private final native void setSideRaw(String side) /*-{ this.side = side; }-*/;
 
-  private final native void setLine(int line) /*-{ this.line = line; }-*/;
+  public final native void path(String p) /*-{ this.path = p }-*/;
+  public final native void id(String i) /*-{ this.id = i }-*/;
+  public final native void line(int n) /*-{ this.line = n }-*/;
+  public final native void range(CommentRange r) /*-{ this.range = r }-*/;
+  public final native void in_reply_to(String i) /*-{ this.in_reply_to = i }-*/;
+  public final native void message(String m) /*-{ this.message = m }-*/;
 
-  private final native void setInReplyTo(String in_reply_to) /*-{
-    this.in_reply_to = in_reply_to;
-  }-*/;
+  public final void side(Side side) {
+    sideRaw(side.toString());
+  }
+  private final native void sideRaw(String s) /*-{ this.side = s }-*/;
 
-  private final native void setMessage(String message) /*-{ this.message = message; }-*/;
-
-  public final native String id() /*-{ return this.id; }-*/;
-  public final native String path() /*-{ return this.path; }-*/;
+  public final native String path() /*-{ return this.path }-*/;
+  public final native String id() /*-{ return this.id }-*/;
+  public final native String in_reply_to() /*-{ return this.in_reply_to }-*/;
 
   public final Side side() {
     String s = sideRaw();
@@ -68,10 +91,6 @@
   }
   private final native String sideRaw() /*-{ return this.side }-*/;
 
-  public final native int line() /*-{ return this.line || 0; }-*/;
-  public final native String in_reply_to() /*-{ return this.in_reply_to; }-*/;
-  public final native String message() /*-{ return this.message; }-*/;
-
   public final Timestamp updated() {
     Timestamp r = updatedTimestamp();
     if (r == null) {
@@ -83,17 +102,16 @@
     }
     return r;
   }
-  private final native String updatedRaw() /*-{ return this.updated; }-*/;
+  private final native String updatedRaw() /*-{ return this.updated }-*/;
   private final native Timestamp updatedTimestamp() /*-{ return this._ts }-*/;
   private final native void updatedTimestamp(Timestamp t) /*-{ this._ts = t }-*/;
 
-  public final native AccountInfo author() /*-{ return this.author; }-*/;
-
-  public final native boolean has_line() /*-{ return this.hasOwnProperty('line'); }-*/;
-
-  public final native CommentRange range() /*-{ return this.range; }-*/;
-
-  public final native void setRange(CommentRange range) /*-{ this.range = range; }-*/;
+  public final native AccountInfo author() /*-{ return this.author }-*/;
+  public final native int line() /*-{ return this.line || 0 }-*/;
+  public final native boolean has_line() /*-{ return this.hasOwnProperty('line') }-*/;
+  public final native boolean has_range() /*-{ return this.hasOwnProperty('range') }-*/;
+  public final native CommentRange range() /*-{ return this.range }-*/;
+  public final native String message() /*-{ return this.message }-*/;
 
   protected CommentInfo() {
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInput.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInput.java
deleted file mode 100644
index c96a67f..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInput.java
+++ /dev/null
@@ -1,82 +0,0 @@
-// Copyright (C) 2013 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.changes;
-
-import com.google.gerrit.client.diff.CommentRange;
-import com.google.gerrit.common.changes.Side;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwtjsonrpc.client.impl.ser.JavaSqlTimestamp_JsonSerializer;
-
-import java.sql.Timestamp;
-
-public class CommentInput extends JavaScriptObject {
-  public static CommentInput create(CommentInfo original) {
-    CommentInput input = createObject().cast();
-    input.setId(original.id());
-    input.setPath(original.path());
-    input.setSide(original.side());
-    if (original.has_line()) {
-      input.setLine(original.line());
-    }
-    input.setRange(original.range());
-    input.setInReplyTo(original.in_reply_to());
-    input.setMessage(original.message());
-    return input;
-  }
-
-  public final native void setId(String id) /*-{ this.id = id; }-*/;
-  public final native void setPath(String path) /*-{ this.path = path; }-*/;
-
-  public final void setSide(Side side) {
-    setSideRaw(side.toString());
-  }
-  private final native void setSideRaw(String side) /*-{ this.side = side; }-*/;
-
-  public final native void setLine(int line) /*-{ this.line = line; }-*/;
-
-  public final native void setInReplyTo(String in_reply_to) /*-{
-    this.in_reply_to = in_reply_to;
-  }-*/;
-
-  public final native void setMessage(String message) /*-{ this.message = message; }-*/;
-  public final native String id() /*-{ return this.id; }-*/;
-  public final native String path() /*-{ return this.path; }-*/;
-
-  public final Side side() {
-    String s = sideRaw();
-    return s != null
-        ? Side.valueOf(s)
-        : Side.REVISION;
-  }
-  private final native String sideRaw() /*-{ return this.side }-*/;
-
-  public final native int line() /*-{ return this.line; }-*/;
-  public final native String in_reply_to() /*-{ return this.in_reply_to; }-*/;
-  public final native String message() /*-{ return this.message; }-*/;
-
-  public final Timestamp updated() {
-    return JavaSqlTimestamp_JsonSerializer.parseTimestamp(updatedRaw());
-  }
-  private final native String updatedRaw() /*-{ return this.updated; }-*/;
-
-  public final native boolean has_line() /*-{ return this.hasOwnProperty('line'); }-*/;
-
-  public final native CommentRange range() /*-{ return this.range; }-*/;
-
-  public final native void setRange(CommentRange range) /*-{ this.range = range; }-*/;
-
-  protected CommentInput() {
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java
index ee28ece..1ae8bbd 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java
@@ -16,26 +16,24 @@
 
 import com.google.gerrit.client.diff.DiffInfo.Region;
 import com.google.gerrit.client.diff.DiffInfo.Span;
-import com.google.gerrit.client.diff.PaddingManager.LinePaddingWidgetWrapper;
 import com.google.gerrit.client.rpc.Natives;
 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.dom.client.Style.Unit;
+import com.google.gwt.user.client.DOM;
 
 import net.codemirror.lib.CodeMirror;
 import net.codemirror.lib.CodeMirror.LineClassWhere;
-import net.codemirror.lib.CodeMirror.LineHandle;
 import net.codemirror.lib.Configuration;
 import net.codemirror.lib.LineCharacter;
+import net.codemirror.lib.LineWidget;
 import net.codemirror.lib.TextMarker;
 
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 
 /** Colors modified regions for {@link SideBySide2}. */
 class ChunkManager {
@@ -48,7 +46,7 @@
   private List<DiffChunkInfo> chunks;
   private List<TextMarker> markers;
   private List<Runnable> undo;
-  private Map<LineHandle, LinePaddingWidgetWrapper> paddingOnOtherSide;
+  private List<LineWidget> padding;
 
   ChunkManager(SideBySide2 host,
       CodeMirror cmA,
@@ -66,7 +64,15 @@
   }
 
   DiffChunkInfo getFirst() {
-    return chunks.isEmpty() ? null : chunks.get(0);
+    if (!chunks.isEmpty()) {
+      for (DiffChunkInfo d : chunks) {
+        if (d.getSide() == DisplaySide.B) {
+          return d;
+        }
+      }
+      return chunks.get(0);
+    }
+    return null;
   }
 
   void reset() {
@@ -77,8 +83,8 @@
     for (Runnable r : undo) {
       r.run();
     }
-    for (LinePaddingWidgetWrapper x : paddingOnOtherSide.values()) {
-      x.getWidget().clear();
+    for (LineWidget w : padding) {
+      w.clear();
     }
   }
 
@@ -86,7 +92,7 @@
     chunks = new ArrayList<DiffChunkInfo>();
     markers = new ArrayList<TextMarker>();
     undo = new ArrayList<Runnable>();
-    paddingOnOtherSide = new HashMap<LineHandle, LinePaddingWidgetWrapper>();
+    padding = new ArrayList<LineWidget>();
 
     String diffColor = diff.meta_a() == null || diff.meta_b() == null
         ? DiffTable.style.intralineBg()
@@ -118,16 +124,18 @@
     colorLines(cmB, color, startB, bLen);
     markEdit(cmA, startA, a, region.edit_a());
     markEdit(cmB, startB, b, region.edit_b());
+    addPadding(cmA, startA + aLen - 1, bLen - aLen);
+    addPadding(cmB, startB + bLen - 1, aLen - bLen);
     addGutterTag(region, startA, startB);
     mapper.appendReplace(aLen, bLen);
 
     int endA = mapper.getLineA() - 1;
     int endB = mapper.getLineB() - 1;
     if (aLen > 0) {
-      addDiffChunkAndPadding(cmB, endB, endA, aLen, bLen > 0);
+      addDiffChunk(cmB, endB, endA, aLen, bLen > 0);
     }
     if (bLen > 0) {
-      addDiffChunkAndPadding(cmA, endA, endB, bLen, aLen > 0);
+      addDiffChunk(cmA, endA, endB, bLen, aLen > 0);
     }
   }
 
@@ -194,13 +202,37 @@
     }
   }
 
-  private void addDiffChunkAndPadding(CodeMirror cmToPad, int lineToPad,
+  /**
+   * Insert a new padding div below the given line.
+   *
+   * @param cm parent CodeMirror to add extra space into.
+   * @param line line to put the padding below.
+   * @param len number of lines to pad. Padding is inserted only if
+   *        {@code len >= 1}.
+   */
+  private void addPadding(CodeMirror cm, int line, int len) {
+    if (0 < len) {
+      // DiffTable adds 1px bottom padding to each line to preserve
+      // sufficient space for underscores commonly appearing in code.
+      // Padding should be 1em + 1px high for each line. Add within
+      // the browser using height + padding-bottom.
+      Element pad = DOM.createDiv();
+      pad.setClassName(DiffTable.style.padding());
+      pad.getStyle().setHeight(len, Unit.EM);
+      pad.getStyle().setPaddingBottom(len, Unit.PX);
+      padding.add(cm.addLineWidget(
+        line == -1 ? 0 : line,
+        pad,
+        Configuration.create()
+          .set("coverGutter", true)
+          .set("noHScroll", true)
+          .set("above", line == -1)));
+    }
+  }
+
+  private void addDiffChunk(CodeMirror cmToPad, int lineToPad,
       int lineOnOther, int chunkSize, boolean edit) {
-    CodeMirror otherCm = host.otherCm(cmToPad);
-    paddingOnOtherSide.put(otherCm.getLineHandle(lineOnOther),
-        new LinePaddingWidgetWrapper(host.addPaddingWidget(cmToPad,
-            lineToPad, 0, Unit.EM, null), lineToPad, chunkSize));
-    chunks.add(new DiffChunkInfo(otherCm.side(),
+    chunks.add(new DiffChunkInfo(host.otherCm(cmToPad).side(),
         lineOnOther - chunkSize + 1, lineOnOther, edit));
   }
 
@@ -283,62 +315,4 @@
     }
     return null;
   }
-
-  void resizePadding(final CodeMirror cm,
-      final LineHandle line,
-      final DisplaySide side) {
-    if (paddingOnOtherSide.containsKey(line)) {
-      host.defer(new Runnable() {
-        @Override
-        public void run() {
-          resizePaddingOnOtherSide(side, cm.getLineNumber(line));
-        }
-      });
-    }
-  }
-
-  void resizePaddingOnOtherSide(DisplaySide mySide, int line) {
-    CodeMirror cm = host.getCmFromSide(mySide);
-    LineHandle handle = cm.getLineHandle(line);
-    final LinePaddingWidgetWrapper otherWrapper = paddingOnOtherSide.get(handle);
-    double myChunkHeight = cm.heightAtLine(line + 1) -
-        cm.heightAtLine(line - otherWrapper.getChunkLength() + 1);
-    Element otherPadding = otherWrapper.getElement();
-    int otherPaddingHeight = otherPadding.getOffsetHeight();
-    CodeMirror otherCm = host.otherCm(cm);
-    int otherLine = otherWrapper.getOtherLine();
-    LineHandle other = otherCm.getLineHandle(otherLine);
-    if (paddingOnOtherSide.containsKey(other)) {
-      LinePaddingWidgetWrapper myWrapper = paddingOnOtherSide.get(other);
-      Element myPadding = paddingOnOtherSide.get(other).getElement();
-      int myPaddingHeight = myPadding.getOffsetHeight();
-      myChunkHeight -= myPaddingHeight;
-      double otherChunkHeight = otherCm.heightAtLine(otherLine + 1) -
-          otherCm.heightAtLine(otherLine - myWrapper.getChunkLength() + 1) -
-          otherPaddingHeight;
-      double delta = myChunkHeight - otherChunkHeight;
-      if (delta > 0) {
-        if (myPaddingHeight != 0) {
-          myPadding.getStyle().setHeight((double) 0, Unit.PX);
-          myWrapper.getWidget().changed();
-        }
-        if (otherPaddingHeight != delta) {
-          otherPadding.getStyle().setHeight(delta, Unit.PX);
-          otherWrapper.getWidget().changed();
-        }
-      } else {
-        if (myPaddingHeight != -delta) {
-          myPadding.getStyle().setHeight(-delta, Unit.PX);
-          myWrapper.getWidget().changed();
-        }
-        if (otherPaddingHeight != 0) {
-          otherPadding.getStyle().setHeight((double) 0, Unit.PX);
-          otherWrapper.getWidget().changed();
-        }
-      }
-    } else if (otherPaddingHeight != myChunkHeight) {
-      otherPadding.getStyle().setHeight(myChunkHeight, Unit.PX);
-      otherWrapper.getWidget().changed();
-    }
-  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java
index 83d2f99..0e82c4cf 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.client.diff;
 
 import com.google.gerrit.client.changes.CommentInfo;
-import com.google.gerrit.client.diff.PaddingManager.PaddingWidgetWrapper;
 import com.google.gerrit.client.diff.SidePanel.GutterWrapper;
 import com.google.gwt.event.dom.client.MouseOutEvent;
 import com.google.gwt.event.dom.client.MouseOutHandler;
@@ -34,23 +33,17 @@
     Resources.I.style().ensureInjected();
   }
 
-  private PaddingManager widgetManager;
-  private PaddingWidgetWrapper selfWidgetWrapper;
-  private CommentManager commentManager;
-  private CodeMirror cm;
-  private DiffChunkInfo diffChunkInfo;
+  private final CommentGroup group;
   private GutterWrapper gutterWrapper;
   private FromTo fromTo;
   private TextMarker rangeMarker;
   private TextMarker rangeHighlightMarker;
 
-  CommentBox(CommentManager commentManager, CodeMirror cm, CommentInfo info) {
-    this.commentManager = commentManager;
-    this.cm = cm;
-    CommentRange range = info.range();
+  CommentBox(CommentGroup group, CommentRange range) {
+    this.group = group;
     if (range != null) {
       fromTo = FromTo.create(range);
-      rangeMarker = cm.markText(
+      rangeMarker = group.getCm().markText(
           fromTo.getFrom(),
           fromTo.getTo(),
           Configuration.create()
@@ -70,62 +63,21 @@
     }, MouseOutEvent.getType());
   }
 
-  @Override
-  protected void onLoad() {
-    resizePaddingWidget();
-  }
-
-  void resizePaddingWidget() {
-    if (!getCommentInfo().has_line()) {
-      return;
-    }
-    commentManager.getSideBySide2().defer(new Runnable() {
-      @Override
-      public void run() {
-        assert selfWidgetWrapper != null;
-        selfWidgetWrapper.getWidget().changed();
-        if (diffChunkInfo != null) {
-          commentManager.getSideBySide2().getChunkManager()
-            .resizePaddingOnOtherSide(cm.side(), diffChunkInfo.getEnd());
-        } else {
-          assert widgetManager != null;
-          widgetManager.resizePaddingWidget();
-        }
-      }
-    });
-  }
-
   abstract CommentInfo getCommentInfo();
   abstract boolean isOpen();
 
   void setOpen(boolean open) {
-    resizePaddingWidget();
+    group.resize();
     setRangeHighlight(open);
     getCm().focus();
   }
 
+  CommentGroup getCommentGroup() {
+    return group;
+  }
+
   CommentManager getCommentManager() {
-    return commentManager;
-  }
-
-  PaddingManager getPaddingManager() {
-    return widgetManager;
-  }
-
-  void setPaddingManager(PaddingManager manager) {
-    widgetManager = manager;
-  }
-
-  void setSelfWidgetWrapper(PaddingWidgetWrapper wrapper) {
-    selfWidgetWrapper = wrapper;
-  }
-
-  PaddingWidgetWrapper getSelfWidgetWrapper() {
-    return selfWidgetWrapper;
-  }
-
-  void setDiffChunkInfo(DiffChunkInfo info) {
-    this.diffChunkInfo = info;
+    return group.getCommentManager();
   }
 
   void setGutterWrapper(GutterWrapper wrapper) {
@@ -135,7 +87,7 @@
   void setRangeHighlight(boolean highlight) {
     if (fromTo != null) {
       if (highlight && rangeHighlightMarker == null) {
-        rangeHighlightMarker = cm.markText(
+        rangeHighlightMarker = group.getCm().markText(
             fromTo.getFrom(),
             fromTo.getTo(),
             Configuration.create()
@@ -150,6 +102,7 @@
   void clearRange() {
     if (rangeMarker != null) {
       rangeMarker.clear();
+      rangeMarker = null;
     }
   }
 
@@ -158,6 +111,6 @@
   }
 
   CodeMirror getCm() {
-    return cm;
+    return group.getCm();
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBoxUi.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBoxUi.css
index ffce7e6..8efc9f7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBoxUi.css
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBoxUi.css
@@ -13,18 +13,18 @@
  * limitations under the License.
  */
 
-.commentBox {
-  position: relative;
-  width: 679px;
-  min-height: 16px;
+.commentWidgets {
   font-family: sans-serif;
   background-color: #fcfa96;
   border: 1px solid black;
   -webkit-box-shadow: 3px 3px 3px #888888;
   -moz-box-shadow: 3px 3px 3px #888888;
   box-shadow: 3px 3px 3px #888888;
+
+  /* margin-bottom is fixed in CommentGroup.computeHeight() */
   margin-bottom: 5px;
   margin-right: 5px;
+
   -webkit-touch-callout: initial;
   -webkit-user-select: initial;
   -khtml-user-select: initial;
@@ -32,7 +32,15 @@
   -ms-user-select: initial;
 }
 
-.header { cursor: pointer; }
+.commentBox {
+  position: relative;
+  min-height: 16px;
+}
+
+.header {
+  max-width: 650px;
+  cursor: pointer;
+}
 
 .summary {
   color: #777;
@@ -58,13 +66,15 @@
   padding-top: 2px;
   position: relative;
 }
-.contents p,
-.contents ul,
-.contents blockquote {
+.message {
+  overflow-x: auto;
+}
+.message p,
+.message ul,
+.message blockquote {
   -webkit-margin-before: 0;
   -webkit-margin-after: 0.3em;
 }
-
 .commentBox button {
   margin-right: 3px;
   margin-bottom: 1px;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentGroup.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentGroup.java
new file mode 100644
index 0000000..5294cd0
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentGroup.java
@@ -0,0 +1,225 @@
+// Copyright (C) 2013 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.diff;
+
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.SimplePanel;
+
+import net.codemirror.lib.CodeMirror;
+import net.codemirror.lib.Configuration;
+import net.codemirror.lib.LineWidget;
+
+/**
+ * LineWidget attached to a CodeMirror container.
+ *
+ * When a comment is placed on a line a CommentWidget is created on both sides.
+ * The group tracks all comment boxes on that same line, and also includes an
+ * empty padding element to keep subsequent lines vertically aligned.
+ */
+class CommentGroup extends Composite {
+  static void pair(CommentGroup a, CommentGroup b) {
+    a.peer = b;
+    b.peer = a;
+  }
+
+  private final CommentManager manager;
+  private final CodeMirror cm;
+  private final int line;
+  private final FlowPanel comments;
+  private final Element padding;
+  private LineWidget lineWidget;
+  private Timer resizeTimer;
+  private CommentGroup peer;
+
+  CommentGroup(CommentManager manager, CodeMirror cm, int line) {
+    this.manager = manager;
+    this.cm = cm;
+    this.line = line;
+
+    comments = new FlowPanel();
+    comments.setStyleName(Resources.I.style().commentWidgets());
+    comments.setVisible(false);
+    initWidget(new SimplePanel(comments));
+
+    padding = DOM.createDiv();
+    padding.setClassName(DiffTable.style.padding());
+    getElement().appendChild(padding);
+  }
+
+  CommentManager getCommentManager() {
+    return manager;
+  }
+
+  CodeMirror getCm() {
+    return cm;
+  }
+
+  CommentGroup getPeer() {
+    return peer;
+  }
+
+  int getLine() {
+    return line;
+  }
+
+  void add(PublishedBox box) {
+    comments.add(box);
+    comments.setVisible(true);
+  }
+
+  void add(DraftBox box) {
+    PublishedBox p = box.getReplyToBox();
+    if (p != null) {
+      for (int i = 0; i < getBoxCount(); i++) {
+        if (p == getCommentBox(i)) {
+          comments.insert(box, i + 1);
+          comments.setVisible(true);
+          resize();
+          return;
+        }
+      }
+    }
+    comments.add(box);
+    comments.setVisible(true);
+    resize();
+  }
+
+  CommentBox getCommentBox(int i) {
+    return (CommentBox) comments.getWidget(i);
+  }
+
+  int getBoxCount() {
+    return comments.getWidgetCount();
+  }
+
+  void openCloseLast() {
+    if (0 < getBoxCount()) {
+      CommentBox box = getCommentBox(getBoxCount() - 1);
+      box.setOpen(!box.isOpen());
+    }
+  }
+
+  void openCloseAll() {
+    boolean open = false;
+    for (int i = 0; i < getBoxCount(); i++) {
+      if (!getCommentBox(i).isOpen()) {
+        open = true;
+        break;
+      }
+    }
+    for (int i = 0; i < getBoxCount(); i++) {
+      getCommentBox(i).setOpen(open);
+    }
+  }
+
+  void remove(DraftBox box) {
+    comments.remove(box);
+    comments.setVisible(0 < getBoxCount());
+
+    if (0 < getBoxCount() || 0 < peer.getBoxCount()) {
+      resize();
+    } else {
+      detach();
+      peer.detach();
+    }
+  }
+
+  private void detach() {
+    if (lineWidget != null) {
+      lineWidget.clear();
+      lineWidget = null;
+    }
+    manager.clearLine(cm.side(), line);
+    removeFromParent();
+  }
+
+  void attach(DiffTable parent) {
+    parent.add(this);
+    lineWidget = cm.addLineWidget(Math.max(0, line - 1), getElement(),
+        Configuration.create()
+          .set("coverGutter", true)
+          .set("noHScroll", true)
+          .set("above", line <= 0)
+          .set("insertAt", 0));
+  }
+
+  void handleRedraw() {
+    lineWidget.onRedraw(new Runnable() {
+      @Override
+      public void run() {
+        if (canComputeHeight() && peer.canComputeHeight()) {
+          if (resizeTimer != null) {
+            resizeTimer.cancel();
+            resizeTimer = null;
+          }
+          adjustPadding(CommentGroup.this, peer);
+        } else if (resizeTimer == null) {
+          resizeTimer = new Timer() {
+            @Override
+            public void run() {
+              if (canComputeHeight() && peer.canComputeHeight()) {
+                cancel();
+                resizeTimer = null;
+                adjustPadding(CommentGroup.this, peer);
+              }
+            }
+          };
+          resizeTimer.scheduleRepeating(5);
+        }
+      }
+    });
+  }
+
+  @Override
+  protected void onUnload() {
+    super.onUnload();
+    if (resizeTimer != null) {
+      resizeTimer.cancel();
+    }
+  }
+
+  void resize() {
+    if (lineWidget != null) {
+      adjustPadding(this, peer);
+    }
+  }
+
+  private boolean canComputeHeight() {
+    return !comments.isVisible() || comments.getOffsetHeight() > 0;
+  }
+
+  private int computeHeight() {
+    if (comments.isVisible()) {
+      // Include margin-bottom: 5px from CSS class.
+      return comments.getOffsetHeight() + 5;
+    }
+    return 0;
+  }
+
+  private static void adjustPadding(CommentGroup a, CommentGroup b) {
+    int apx = a.computeHeight();
+    int bpx = b.computeHeight();
+    int h = Math.max(apx, bpx);
+    a.padding.getStyle().setHeight(Math.max(0, h - apx), Unit.PX);
+    b.padding.getStyle().setHeight(Math.max(0, h - bpx), Unit.PX);
+    a.lineWidget.changed();
+    b.lineWidget.changed();
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java
index ce9595c..57c74d2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.CommentInfo;
-import com.google.gerrit.client.diff.PaddingManager.PaddingWidgetWrapper;
 import com.google.gerrit.client.patches.SkippedLine;
 import com.google.gerrit.client.rpc.CallbackGroup;
 import com.google.gerrit.client.rpc.Natives;
@@ -24,14 +23,10 @@
 import com.google.gerrit.common.changes.Side;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.JsArray;
-import com.google.gwt.core.client.Scheduler;
-import com.google.gwt.core.client.Scheduler.ScheduledCommand;
-import com.google.gwt.dom.client.Style.Unit;
 
 import net.codemirror.lib.CodeMirror;
-import net.codemirror.lib.Configuration;
-import net.codemirror.lib.LineWidget;
 import net.codemirror.lib.CodeMirror.LineHandle;
+import net.codemirror.lib.LineCharacter;
 import net.codemirror.lib.TextMarker.FromTo;
 
 import java.util.ArrayList;
@@ -40,6 +35,8 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
 
 /** Tracks comment widgets for {@link SideBySide2}. */
 class CommentManager {
@@ -50,10 +47,10 @@
   private final CommentLinkProcessor commentLinkProcessor;
 
   private final Map<String, PublishedBox> published;
-  private final Map<LineHandle, CommentBox> lineActiveBox;
-  private final Map<LineHandle, List<PublishedBox>> linePublishedBoxes;
-  private final Map<LineHandle, PaddingManager> linePaddingManager;
+  private final SortedMap<Integer, CommentGroup> sideA;
+  private final SortedMap<Integer, CommentGroup> sideB;
   private final Set<DraftBox> unsavedDrafts;
+  private boolean attached;
 
   CommentManager(SideBySide2 host,
       PatchSet.Id base, PatchSet.Id revision,
@@ -66,9 +63,8 @@
     this.commentLinkProcessor = clp;
 
     published = new HashMap<String, PublishedBox>();
-    lineActiveBox = new HashMap<LineHandle, CommentBox>();
-    linePublishedBoxes = new HashMap<LineHandle, List<PublishedBox>>();
-    linePaddingManager = new HashMap<LineHandle, PaddingManager>();
+    sideA = new TreeMap<Integer, CommentGroup>();
+    sideB = new TreeMap<Integer, CommentGroup>();
     unsavedDrafts = new HashSet<DraftBox>();
   }
 
@@ -82,7 +78,46 @@
     }
   }
 
-  void render(CommentsCollections in) {
+  Runnable commentNav(final CodeMirror src, final Direction dir) {
+    return new Runnable() {
+      @Override
+      public void run() {
+        // Every comment appears in both side maps as a linked pair.
+        // It is only necessary to search one side to find a comment
+        // on either side of the editor pair.
+        SortedMap<Integer, CommentGroup> map = map(src.side());
+        int line = src.hasActiveLine()
+            ? src.getLineNumber(src.getActiveLine()) + 1
+            : 0;
+        if (dir == Direction.NEXT) {
+          map = map.tailMap(line + 1);
+          if (map.isEmpty()) {
+            return;
+          }
+          line = map.firstKey();
+        } else {
+          map = map.headMap(line);
+          if (map.isEmpty()) {
+            return;
+          }
+          line = map.lastKey();
+        }
+
+        CommentGroup g = map.get(line);
+        if (g.getBoxCount() == 0) {
+          g = g.getPeer();
+        }
+
+        CodeMirror cm = g.getCm();
+        double y = cm.heightAtLine(g.getLine() - 1, "local");
+        cm.setCursor(LineCharacter.create(g.getLine() - 1));
+        cm.scrollToY(y - 0.5 * cm.getScrollbarV().getClientHeight());
+        cm.focus();
+      }
+    };
+  }
+
+  void render(CommentsCollections in, boolean expandAll) {
     if (in.publishedBase != null) {
       renderPublished(DisplaySide.A, in.publishedBase);
     }
@@ -95,64 +130,100 @@
     if (in.draftsRevision != null) {
       renderDrafts(DisplaySide.B, in.draftsRevision);
     }
+    if (expandAll) {
+      setExpandAllComments(true);
+    }
+    for (CommentGroup g : sideA.values()) {
+      g.attach(host.diffTable);
+    }
+    for (CommentGroup g : sideB.values()) {
+      g.attach(host.diffTable);
+      g.handleRedraw();
+    }
+    attached = true;
   }
 
   private void renderPublished(DisplaySide forSide, JsArray<CommentInfo> in) {
     for (CommentInfo info : Natives.asList(in)) {
       DisplaySide side = displaySide(info, forSide);
-      if (side == null) {
-        continue;
+      if (side != null) {
+        CommentGroup group = group(side, info.line());
+        PublishedBox box = new PublishedBox(
+            group,
+            commentLinkProcessor,
+            getPatchSetIdFromSide(side),
+            info);
+        group.add(box);
+        box.setGutterWrapper(host.diffTable.sidePanel.addGutter(
+            host.getCmFromSide(side),
+            Math.max(0, info.line() - 1),
+            SidePanel.GutterType.COMMENT));
+        published.put(info.id(), box);
       }
-
-      CodeMirror cm = host.getCmFromSide(side);
-      PublishedBox box = new PublishedBox(this, cm, commentLinkProcessor,
-          getPatchSetIdFromSide(side), info);
-      published.put(info.id(), box);
-      if (!info.has_line()) {
-        host.diffTable.addFileCommentBox(box);
-        continue;
-      }
-
-      int line = info.line() - 1;
-      LineHandle handle = cm.getLineHandle(line);
-      if (linePublishedBoxes.containsKey(handle)) {
-        linePublishedBoxes.get(handle).add(box);
-      } else {
-        List<PublishedBox> list = new ArrayList<PublishedBox>(4);
-        list.add(box);
-        linePublishedBoxes.put(handle, list);
-      }
-      lineActiveBox.put(handle, box);
-      addCommentBox(info, box);
     }
   }
 
   private void renderDrafts(DisplaySide forSide, JsArray<CommentInfo> in) {
     for (CommentInfo info : Natives.asList(in)) {
       DisplaySide side = displaySide(info, forSide);
-      if (side == null) {
-        continue;
+      if (side != null) {
+        addDraftBox(side, info);
       }
-
-      CodeMirror cm = host.getCmFromSide(side);
-      DraftBox box = new DraftBox(this, cm, commentLinkProcessor,
-          getPatchSetIdFromSide(side), info);
-      if (info.in_reply_to() != null) {
-        PublishedBox r = published.get(info.in_reply_to());
-        if (r != null) {
-          r.registerReplyBox(box);
-        }
-      }
-      if (!info.has_line()) {
-        host.diffTable.addFileCommentBox(box);
-        continue;
-      }
-
-      lineActiveBox.put(cm.getLineHandle(info.line() - 1), box);
-      addCommentBox(info, box);
     }
   }
 
+  /**
+   * Create a new {@link DraftBox} at the specified line and focus it.
+   *
+   * @param side which side the draft will appear on.
+   * @param line the line the draft will be at. Lines are 1-based. Line 0 is a
+   *        special case creating a file level comment.
+   */
+  void insertNewDraft(DisplaySide side, int line) {
+    if (line == 0) {
+      host.getSkipManager().ensureFirstLineIsVisible();
+    }
+
+    CommentGroup group = group(side, line);
+    if (0 < group.getBoxCount()) {
+      CommentBox last = group.getCommentBox(group.getBoxCount() - 1);
+      if (last instanceof DraftBox) {
+        ((DraftBox)last).setEdit(true);
+      } else {
+        ((PublishedBox)last).doReply();
+      }
+    } else {
+      addDraftBox(side, CommentInfo.create(
+          path,
+          getStoredSideFromDisplaySide(side),
+          line,
+          null)).setEdit(true);
+    }
+  }
+
+  DraftBox addDraftBox(DisplaySide side, CommentInfo info) {
+    CommentGroup group = group(side, info.line());
+    DraftBox box = new DraftBox(
+        group,
+        commentLinkProcessor,
+        getPatchSetIdFromSide(side),
+        info);
+
+    if (info.in_reply_to() != null) {
+      PublishedBox r = published.get(info.in_reply_to());
+      if (r != null) {
+        r.setReplyBox(box);
+      }
+    }
+
+    group.add(box);
+    box.setGutterWrapper(host.diffTable.sidePanel.addGutter(
+        host.getCmFromSide(side),
+        Math.max(0, info.line() - 1),
+        SidePanel.GutterType.DRAFT));
+    return box;
+  }
+
   private DisplaySide displaySide(CommentInfo info, DisplaySide forSide) {
     if (info.side() == Side.PARENT) {
       return base == null ? DisplaySide.A : null;
@@ -161,15 +232,21 @@
   }
 
   List<SkippedLine> splitSkips(int context, List<SkippedLine> skips) {
+    if (sideB.containsKey(0)) {
+      // Special case of file comment; cannot skip first line.
+      for (SkippedLine skip : skips) {
+        if (skip.getStartB() == 0) {
+          skip.incrementStart(1);
+        }
+      }
+    }
+
     // TODO: This is not optimal, but shouldn't be too costly in most cases.
     // Maybe rewrite after done keeping track of diff chunk positions.
-    for (CommentBox box : lineActiveBox.values()) {
-      int boxLine = box.getCommentInfo().line();
-      boolean sideA = box.getCm().side() == DisplaySide.A;
-
+    for (int boxLine : sideB.tailMap(1).keySet()) {
       List<SkippedLine> temp = new ArrayList<SkippedLine>(skips.size() + 2);
       for (SkippedLine skip : skips) {
-        int startLine = sideA ? skip.getStartA() : skip.getStartB();
+        int startLine = skip.getStartB();
         int deltaBefore = boxLine - startLine;
         int deltaAfter = startLine + skip.getSize() - boxLine;
         if (deltaBefore < -context || deltaAfter < -context) {
@@ -203,36 +280,33 @@
     }
   }
 
+  void clearLine(DisplaySide side, int line) {
+    map(side).remove(line);
+  }
+
   Runnable toggleOpenBox(final CodeMirror cm) {
     return new Runnable() {
       public void run() {
-        CommentBox box = lineActiveBox.get(cm.getActiveLine());
-        if (box != null) {
-          box.setOpen(!box.isOpen());
+        if (cm.hasActiveLine()) {
+          CommentGroup w = map(cm.side()).get(
+              cm.getLineNumber(cm.getActiveLine()) + 1);
+          if (w != null) {
+            w.openCloseLast();
+          }
         }
       }
     };
   }
 
-  Runnable openClosePublished(final CodeMirror cm) {
+  Runnable openCloseAll(final CodeMirror cm) {
     return new Runnable() {
       @Override
       public void run() {
         if (cm.hasActiveLine()) {
-          List<PublishedBox> list =
-              linePublishedBoxes.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);
+          CommentGroup w = map(cm.side()).get(
+              cm.getLineNumber(cm.getActiveLine()) + 1);
+          if (w != null) {
+            w.openCloseAll();
           }
         }
       }
@@ -244,156 +318,38 @@
       return new Runnable() {
         @Override
         public void run() {
-          Gerrit.doSignIn(host.getToken());
+          String token = host.getToken();
+          if (cm.hasActiveLine()) {
+            LineHandle handle = cm.getActiveLine();
+            int line = cm.getLineNumber(handle) + 1;
+            token += "@" + (cm.side() == DisplaySide.A ? "a" : "") + line;
+          }
+          Gerrit.doSignIn(token);
         }
      };
     }
 
     return new Runnable() {
       public void run() {
-        LineHandle handle = cm.getActiveLine();
-        int line = cm.getLineNumber(handle);
-        CommentBox box = lineActiveBox.get(handle);
-        FromTo fromTo = cm.getSelectedRange();
-        if (cm.somethingSelected()) {
-          lineActiveBox.put(handle,
-              newRangeDraft(cm, line, fromTo.getTo().getLine() == line ? fromTo : null));
-          cm.setSelection(cm.getCursor());
-        } else if (box == null) {
-          lineActiveBox.put(handle, newRangeDraft(cm, line, null));
-        } else if (box instanceof DraftBox) {
-          ((DraftBox) box).setEdit(true);
-        } else {
-          ((PublishedBox) box).doReply();
+        if (cm.hasActiveLine()) {
+          newDraft(cm);
         }
       }
     };
   }
 
-  private DraftBox newRangeDraft(CodeMirror cm, int line, FromTo fromTo) {
-    DisplaySide side = cm.side();
-    return addDraftBox(CommentInfo.createRange(
-        path,
-        getStoredSideFromDisplaySide(side),
-        line + 1,
-        null,
-        null,
-        CommentRange.create(fromTo)), side);
-  }
-
-  DraftBox newFileDraft(DisplaySide side) {
-    return addDraftBox(CommentInfo.createFile(
-      path,
-      getStoredSideFromDisplaySide(side),
-      null, null), side);
-  }
-
-  CommentInfo createReply(CommentInfo replyTo) {
-    if (!replyTo.has_line() && replyTo.range() == null) {
-      return CommentInfo.createFile(path, replyTo.side(), replyTo.id(), null);
+  private void newDraft(CodeMirror cm) {
+    int line = cm.getLineNumber(cm.getActiveLine()) + 1;
+    if (cm.somethingSelected()) {
+      FromTo fromTo = cm.getSelectedRange();
+      addDraftBox(cm.side(), CommentInfo.create(
+              path,
+              getStoredSideFromDisplaySide(cm.side()),
+              line,
+              CommentRange.create(fromTo))).setEdit(true);
+      cm.setSelection(cm.getCursor());
     } else {
-      return CommentInfo.createRange(path, replyTo.side(), replyTo.line(),
-          replyTo.id(), null, replyTo.range());
-    }
-  }
-
-  DraftBox addDraftBox(CommentInfo info, DisplaySide side) {
-    CodeMirror cm = host.getCmFromSide(side);
-    final DraftBox box = new DraftBox(this, cm, commentLinkProcessor,
-        getPatchSetIdFromSide(side), info);
-    if (info.id() == null) {
-      Scheduler.get().scheduleDeferred(new ScheduledCommand() {
-        @Override
-        public void execute() {
-          box.setOpen(true);
-          box.setEdit(true);
-        }
-      });
-    }
-    if (!info.has_line()) {
-      return box;
-    }
-    addCommentBox(info, box);
-    box.setVisible(true);
-    LineHandle handle = cm.getLineHandle(info.line() - 1);
-    lineActiveBox.put(handle, box);
-    return box;
-  }
-
-  private CommentBox addCommentBox(CommentInfo info, final CommentBox box) {
-    host.diffTable.add(box);
-    CodeMirror cm = box.getCm();
-    CodeMirror other = host.otherCm(cm);
-    int line = info.line() - 1; // CommentInfo is 1-based, but CM is 0-based
-    LineHandle handle = cm.getLineHandle(line);
-    PaddingManager manager;
-    if (linePaddingManager.containsKey(handle)) {
-      manager = linePaddingManager.get(handle);
-    } else {
-      // Estimated height at 28px, fixed by deferring after display
-      manager = new PaddingManager(host.addPaddingWidget(cm, line, 0, Unit.PX, 0));
-      linePaddingManager.put(handle, manager);
-    }
-
-    int lineToPad = host.lineOnOther(cm.side(), line).getLine();
-    LineHandle otherHandle = other.getLineHandle(lineToPad);
-    ChunkManager chunkMgr = host.getChunkManager();
-    DiffChunkInfo myChunk = chunkMgr.getDiffChunk(cm.side(), line);
-    DiffChunkInfo otherChunk = chunkMgr.getDiffChunk(other.side(), lineToPad);
-    PaddingManager otherManager;
-    if (linePaddingManager.containsKey(otherHandle)) {
-      otherManager = linePaddingManager.get(otherHandle);
-    } else {
-      otherManager =
-          new PaddingManager(host.addPaddingWidget(other, lineToPad, 0, Unit.PX, 0));
-      linePaddingManager.put(otherHandle, otherManager);
-    }
-    if ((myChunk == null && otherChunk == null) || (myChunk != null && otherChunk != null)) {
-      PaddingManager.link(manager, otherManager);
-    }
-
-    int index = manager.getCurrentCount();
-    manager.insert(box, index);
-    Configuration config = Configuration.create()
-      .set("coverGutter", true)
-      .set("insertAt", index)
-      .set("noHScroll", true);
-    LineWidget boxWidget = host.addLineWidget(cm, line, box, config);
-    box.setPaddingManager(manager);
-    box.setSelfWidgetWrapper(new PaddingWidgetWrapper(boxWidget, box.getElement()));
-    if (otherChunk == null) {
-      box.setDiffChunkInfo(myChunk);
-    }
-    box.setGutterWrapper(host.diffTable.sidePanel.addGutter(cm, info.line() - 1,
-        box instanceof DraftBox
-          ? SidePanel.GutterType.DRAFT
-          : SidePanel.GutterType.COMMENT));
-    return box;
-  }
-
-  void removeDraft(DraftBox box) {
-    int line = box.getCommentInfo().line() - 1;
-    LineHandle handle = box.getCm().getLineHandle(line);
-    lineActiveBox.remove(handle);
-    if (linePublishedBoxes.containsKey(handle)) {
-      List<PublishedBox> list = linePublishedBoxes.get(handle);
-      lineActiveBox.put(handle, list.get(list.size() - 1));
-    }
-    unsavedDrafts.remove(box);
-  }
-
-  void addFileCommentBox(CommentBox box) {
-    host.diffTable.addFileCommentBox(box);
-  }
-
-  void removeFileCommentBox(DraftBox box) {
-    host.diffTable.onRemoveDraftBox(box);
-  }
-
-  void resizePadding(LineHandle handle) {
-    CommentBox box = lineActiveBox.get(handle);
-    if (box != null) {
-      box.resizePaddingWidget();
+      insertNewDraft(cm.side(), line);
     }
   }
 
@@ -411,6 +367,47 @@
     }
   }
 
+  private CommentGroup group(DisplaySide side, int line) {
+    CommentGroup w = map(side).get(line);
+    if (w != null) {
+      return w;
+    }
+
+    int lineA, lineB;
+    if (line == 0) {
+      lineA = lineB = 0;
+    } else if (side == DisplaySide.A) {
+      lineA = line;
+      lineB = host.lineOnOther(side, line - 1).getLine() + 1;
+    } else {
+      lineA = host.lineOnOther(side, line - 1).getLine() + 1;
+      lineB = line;
+    }
+
+    CommentGroup a = newGroup(DisplaySide.A, lineA);
+    CommentGroup b = newGroup(DisplaySide.B, lineB);
+    CommentGroup.pair(a, b);
+
+    sideA.put(lineA, a);
+    sideB.put(lineB, b);
+
+    if (attached) {
+      a.attach(host.diffTable);
+      b.attach(host.diffTable);
+      b.handleRedraw();
+    }
+
+    return side == DisplaySide.A ? a : b;
+  }
+
+  private CommentGroup newGroup(DisplaySide side, int line) {
+    return new CommentGroup(this, host.getCmFromSide(side), line);
+  }
+
+  private SortedMap<Integer, CommentGroup> map(DisplaySide side) {
+    return side == DisplaySide.A ? sideA : sideB;
+  }
+
   private Side getStoredSideFromDisplaySide(DisplaySide side) {
     return side == DisplaySide.A && base == null ? Side.PARENT : Side.REVISION;
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentsCollections.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentsCollections.java
index 587c162..1fe85b3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentsCollections.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentsCollections.java
@@ -89,8 +89,11 @@
     };
   }
 
-  private static JsArray<CommentInfo> sort(JsArray<CommentInfo> in) {
+  private JsArray<CommentInfo> sort(JsArray<CommentInfo> in) {
     if (in != null) {
+      for (CommentInfo c : Natives.asList(in)) {
+        c.path(path);
+      }
       Collections.sort(Natives.asList(in), new Comparator<CommentInfo>() {
         @Override
         public int compare(CommentInfo a, CommentInfo b) {
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 3fb8c32..695f2b5 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
@@ -24,6 +24,7 @@
 import com.google.gwt.uibinder.client.UiBinder;
 import com.google.gwt.uibinder.client.UiField;
 import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.HTMLPanel;
 import com.google.gwt.user.client.ui.UIObject;
 import com.google.gwt.user.client.ui.Widget;
@@ -50,23 +51,14 @@
     String padding();
   }
 
-  @UiField
-  Element cmA;
-
-  @UiField
-  Element cmB;
-
-  @UiField
-  SidePanel sidePanel;
-
-  @UiField
-  Element patchSetNavRow;
-
-  @UiField
-  Element patchSetNavCellA;
-
-  @UiField
-  Element patchSetNavCellB;
+  @UiField Element cmA;
+  @UiField Element cmB;
+  @UiField SidePanel sidePanel;
+  @UiField Element patchSetNavRow;
+  @UiField Element patchSetNavCellA;
+  @UiField Element patchSetNavCellB;
+  @UiField FlowPanel widgets;
+  @UiField static DiffTableStyle style;
 
   @UiField(provided = true)
   PatchSetSelectBox2 patchSetSelectBoxA;
@@ -74,36 +66,16 @@
   @UiField(provided = true)
   PatchSetSelectBox2 patchSetSelectBoxB;
 
-  @UiField
-  Element fileCommentRow;
-
-  @UiField
-  Element fileCommentCellA;
-
-  @UiField
-  Element fileCommentCellB;
-
-  @UiField(provided = true)
-  FileCommentPanel fileCommentPanelA;
-
-  @UiField(provided = true)
-  FileCommentPanel fileCommentPanelB;
-
-  @UiField
-  static DiffTableStyle style;
-
   private SideBySide2 parent;
   private boolean headerVisible;
 
   DiffTable(SideBySide2 parent, PatchSet.Id base, PatchSet.Id revision, String path) {
     patchSetSelectBoxA = new PatchSetSelectBox2(
-        this, DisplaySide.A, revision.getParentKey(), base, path);
+        parent, DisplaySide.A, revision.getParentKey(), base, path);
     patchSetSelectBoxB = new PatchSetSelectBox2(
-        this, DisplaySide.B, revision.getParentKey(), revision, path);
+        parent, DisplaySide.B, revision.getParentKey(), revision, path);
     PatchSetSelectBox2.link(patchSetSelectBoxA, patchSetSelectBoxB);
 
-    fileCommentPanelA = new FileCommentPanel(parent, this, DisplaySide.A);
-    fileCommentPanelB = new FileCommentPanel(parent, this, DisplaySide.B);
     initWidget(uiBinder.createAndBindUi(this));
     this.parent = parent;
     this.headerVisible = true;
@@ -117,9 +89,6 @@
     headerVisible = show;
     Gerrit.setHeaderVisible(show && !parent.getPrefs().hideTopMenu());
     UIObject.setVisible(patchSetNavRow, show);
-    UIObject.setVisible(fileCommentRow, show
-        && (fileCommentPanelA.getBoxCount() > 0
-            || fileCommentPanelB.getBoxCount() > 0));
     if (show) {
       parent.header.removeStyleName(style.fullscreen());
     } else {
@@ -128,25 +97,8 @@
     parent.resizeCodeMirror();
   }
 
-  private FileCommentPanel getPanelFromSide(DisplaySide side) {
-    return side == DisplaySide.A ? fileCommentPanelA : fileCommentPanelB;
-  }
-
-  void createOrEditFileComment(DisplaySide side) {
-    getPanelFromSide(side).createOrEditFileComment();
-    setHeaderVisible(true);
-  }
-
-  void addFileCommentBox(CommentBox box) {
-    getPanelFromSide(box.getCm().side()).addFileComment(box);
-  }
-
-  void onRemoveDraftBox(DraftBox box) {
-    getPanelFromSide(box.getCm().side()).onRemoveDraftBox(box);
-  }
-
   int getHeaderHeight() {
-    return fileCommentRow.getOffsetHeight() + patchSetSelectBoxA.getOffsetHeight();
+    return patchSetSelectBoxA.getOffsetHeight();
   }
 
   void setUpPatchSetNav(JsArray<RevisionInfo> list, DiffInfo info) {
@@ -155,6 +107,6 @@
   }
 
   void add(Widget widget) {
-    ((HTMLPanel) getWidget()).add(widget);
+    widgets.add(widget);
   }
 }
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 06dfd3f..3cee248 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
@@ -44,14 +44,19 @@
     .difftable .CodeMirror-lines { padding: 0; }
     .difftable .CodeMirror pre {
       padding: 0;
-      padding-bottom: 0.11em;
       overflow: hidden;
       border-right: 0;
       width: auto;
     }
+
+    /* Preserve space for underscores. If this changes
+     * see ChunkManager.addPadding() and adjust there.
+     */
+    .difftable .CodeMirror pre,
     .difftable .CodeMirror pre span {
-      padding-bottom: 0.11em;
+      padding-bottom: 1px;
     }
+
     .contentCell {
       padding: 0;
     }
@@ -144,14 +149,6 @@
                 <d:PatchSetSelectBox2 ui:field='patchSetSelectBoxB' />
               </td>
             </tr>
-            <tr ui:field='fileCommentRow' class='{style.fileCommentRow}'>
-              <td ui:field='fileCommentCellA' class='{style.fileCommentCell}'>
-                <d:FileCommentPanel ui:field='fileCommentPanelA' />
-              </td>
-              <td ui:field='fileCommentCellB' class='{style.fileCommentCell}'>
-                <d:FileCommentPanel ui:field='fileCommentPanelB' />
-              </td>
-            </tr>
             <tr>
               <td ui:field='cmA' class='{style.a}'></td>
               <td ui:field='cmB' class='{style.b}'></td>
@@ -161,5 +158,6 @@
         <td class='{style.sidePanelCell}'><d:SidePanel ui:field='sidePanel'/></td>
       </tr>
     </table>
+    <g:FlowPanel ui:field='widgets' visible='false'/>
   </g:HTMLPanel>
 </ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.java
index 711cab0..c618bbe 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.changes.CommentApi;
 import com.google.gerrit.client.changes.CommentInfo;
-import com.google.gerrit.client.changes.CommentInput;
 import com.google.gerrit.client.rpc.CallbackGroup;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.CommentLinkProcessor;
@@ -27,6 +26,7 @@
 import com.google.gwt.core.client.Scheduler;
 import com.google.gwt.core.client.Scheduler.RepeatingCommand;
 import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.dom.client.BlurEvent;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.dom.client.DoubleClickEvent;
@@ -35,6 +35,8 @@
 import com.google.gwt.event.dom.client.KeyDownEvent;
 import com.google.gwt.event.dom.client.MouseMoveEvent;
 import com.google.gwt.event.dom.client.MouseMoveHandler;
+import com.google.gwt.event.dom.client.MouseUpEvent;
+import com.google.gwt.event.dom.client.MouseUpHandler;
 import com.google.gwt.uibinder.client.UiBinder;
 import com.google.gwt.uibinder.client.UiField;
 import com.google.gwt.uibinder.client.UiHandler;
@@ -47,8 +49,6 @@
 import com.google.gwtexpui.globalkey.client.NpTextArea;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
-import net.codemirror.lib.CodeMirror;
-
 /** An HtmlPanel for displaying and editing a draft */
 class DraftBox extends CommentBox {
   interface Binder extends UiBinder<HTMLPanel, DraftBox> {}
@@ -62,6 +62,8 @@
   private CommentInfo comment;
   private PublishedBox replyToBox;
   private Timer expandTimer;
+  private Timer resizeTimer;
+  private int editAreaHeight;
   private boolean autoClosed;
 
   @UiField Widget header;
@@ -80,12 +82,11 @@
   @UiField Button discard2;
 
   DraftBox(
-      CommentManager manager,
-      CodeMirror cm,
+      CommentGroup group,
       CommentLinkProcessor clp,
       PatchSet.Id id,
       CommentInfo info) {
-    super(manager, cm, info);
+    super(group, info.range());
 
     linkProcessor = clp;
     psId = id;
@@ -112,6 +113,7 @@
         }
       }
     }, ClickEvent.getType());
+
     addDomHandler(new DoubleClickHandler() {
       @Override
       public void onDoubleClick(DoubleClickEvent event) {
@@ -123,12 +125,8 @@
         }
       }
     }, DoubleClickEvent.getType());
-    addDomHandler(new MouseMoveHandler() {
-      @Override
-      public void onMouseMove(MouseMoveEvent event) {
-        resizePaddingWidget();
-      }
-    }, MouseMoveEvent.getType());
+
+    initResizeHandler();
   }
 
   private void set(CommentInfo info) {
@@ -170,7 +168,8 @@
     if (editArea.getVisibleLines() != rows) {
       editArea.setVisibleLines(rows);
     }
-    resizePaddingWidget();
+    editAreaHeight = editArea.getOffsetHeight();
+    getCommentGroup().resize();
   }
 
   boolean isEdit() {
@@ -191,6 +190,7 @@
       editArea.setFocus(true);
       cancel.setVisible(!isNew());
       expandText();
+      editAreaHeight = editArea.getOffsetHeight();
       if (msg.length() > 0) {
         Scheduler.get().scheduleFixedDelay(new RepeatingCommand() {
           @Override
@@ -202,18 +202,24 @@
       }
     } else {
       expandTimer.cancel();
+      resizeTimer.cancel();
     }
     getCommentManager().setUnsaved(this, edit);
-    resizePaddingWidget();
+    getCommentGroup().resize();
   }
 
-  void registerReplyToBox(PublishedBox box) {
+  PublishedBox getReplyToBox() {
+    return replyToBox;
+  }
+
+  void setReplyToBox(PublishedBox box) {
     replyToBox = box;
   }
 
   @Override
   protected void onUnload() {
     expandTimer.cancel();
+    resizeTimer.cancel();
     super.onUnload();
   }
 
@@ -221,20 +227,13 @@
     if (replyToBox != null) {
       replyToBox.unregisterReplyBox();
     }
-    clearRange();
+
+    getCommentManager().setUnsaved(this, false);
     setRangeHighlight(false);
-    removeFromParent();
-    if (!getCommentInfo().has_line()) {
-      getCommentManager().removeFileCommentBox(this);
-      return;
-    }
-    PaddingManager manager = getPaddingManager();
-    manager.remove(this);
-    getCommentManager().removeDraft(this);
-    getCm().focus();
-    getSelfWidgetWrapper().getWidget().clear();
+    clearRange();
     getGutterWrapper().remove();
-    resizePaddingWidget();
+    getCommentGroup().remove(this);
+    getCm().focus();
   }
 
   @UiHandler("message")
@@ -265,9 +264,8 @@
       return;
     }
 
-    CommentInfo original = comment;
-    CommentInput input = CommentInput.create(original);
-    input.setMessage(message);
+    CommentInfo input = CommentInfo.copy(comment);
+    input.message(message);
     enableEdit(false);
 
     GerritCallback<CommentInfo> cb = new GerritCallback<CommentInfo>() {
@@ -288,11 +286,11 @@
         super.onFailure(e);
       }
     };
-    if (original.id() == null) {
+    if (input.id() == null) {
       CommentApi.createDraft(psId, input, group == null ? cb : group.add(cb));
     } else {
       CommentApi.updateDraft(
-          psId, original.id(), input, group == null ? cb : group.add(cb));
+          psId, input.id(), input, group == null ? cb : group.add(cb));
     }
     getCm().focus();
   }
@@ -337,6 +335,7 @@
 
   @UiHandler("editArea")
   void onKeyDown(KeyDownEvent e) {
+    resizeTimer.cancel();
     if ((e.isControlKeyDown() || e.isMetaKeyDown())
         && !e.isAltKeyDown() && !e.isShiftKeyDown()) {
       switch (e.getNativeKeyCode()) {
@@ -362,6 +361,40 @@
     expandTimer.schedule(250);
   }
 
+  @UiHandler("editArea")
+  void onBlur(BlurEvent e) {
+    resizeTimer.cancel();
+  }
+
+  private void initResizeHandler() {
+    resizeTimer = new Timer() {
+      @Override
+      public void run() {
+        getCommentGroup().resize();
+      }
+    };
+
+    addDomHandler(new MouseMoveHandler() {
+      @Override
+      public void onMouseMove(MouseMoveEvent event) {
+        int h = editArea.getOffsetHeight();
+        if (isEdit() && h != editAreaHeight) {
+          getCommentGroup().resize();
+          resizeTimer.scheduleRepeating(50);
+          editAreaHeight = h;
+        }
+      }
+    }, MouseMoveEvent.getType());
+
+    addDomHandler(new MouseUpHandler() {
+      @Override
+      public void onMouseUp(MouseUpEvent event) {
+        resizeTimer.cancel();
+        getCommentGroup().resize();
+      }
+    }, MouseUpEvent.getType());
+  }
+
   private boolean isNew() {
     return comment.id() == null;
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.ui.xml
index 52ef0ff..0c97e8b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.ui.xml
@@ -49,7 +49,7 @@
         <div ui:field='date' class='{res.style.date}'/>
       </g:HTMLPanel>
       <div ui:field='p_view' aria-hidden='true' style='display: NONE'>
-        <g:HTML ui:field='message' styleName=''/>
+        <g:HTML ui:field='message' styleName='{res.style.message}'/>
         <div style='position: relative'>
           <g:Button ui:field='edit'
               title='Edit this draft comment'
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/FileCommentPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/FileCommentPanel.java
deleted file mode 100644
index 6282855..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/FileCommentPanel.java
+++ /dev/null
@@ -1,76 +0,0 @@
-//Copyright (C) 2013 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.diff;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlowPanel;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * HTMLPanel to hold file comments.
- * TODO: Need to resize CodeMirror if this is resized since we don't have the
- * system scrollbar.
- */
-class FileCommentPanel extends Composite {
-  private final SideBySide2 parent;
-  private DiffTable table;
-  private DisplaySide side;
-  private List<CommentBox> boxes;
-  private FlowPanel body;
-
-  FileCommentPanel(SideBySide2 host, DiffTable table, DisplaySide side) {
-    this.parent = host;
-    this.table = table;
-    this.side = side;
-    boxes = new ArrayList<CommentBox>();
-    initWidget(body = new FlowPanel());
-  }
-
-  void createOrEditFileComment() {
-    if (!Gerrit.isSignedIn()) {
-      Gerrit.doSignIn(parent.getToken());
-      return;
-    }
-
-    if (boxes.isEmpty()) {
-      addFileComment(parent.getCommentManager().newFileDraft(side));
-    } else {
-      CommentBox box = boxes.get(boxes.size() - 1);
-      if (box instanceof DraftBox) {
-        ((DraftBox) box).setEdit(true);
-      } else {
-        addFileComment(((PublishedBox) box).addReplyBox());
-      }
-    }
-  }
-
-  int getBoxCount() {
-    return boxes.size();
-  }
-
-  void addFileComment(CommentBox box) {
-    boxes.add(box);
-    body.add(box);
-    table.setHeaderVisible(true);
-  }
-
-  void onRemoveDraftBox(DraftBox box) {
-    boxes.remove(box);
-    table.setHeaderVisible(true);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java
index 9cfa04d..46a7af7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java
@@ -310,6 +310,18 @@
     }
   }
 
+  Runnable reviewedAndNext() {
+    return new Runnable() {
+      @Override
+      public void run() {
+        if (Gerrit.isSignedIn()) {
+          reviewed.setValue(true, true);
+        }
+        navigate(Direction.NEXT).run();
+      }
+    };
+  }
+
   String getNextPath() {
     return nextPath;
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/LineMapper.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/LineMapper.java
index 968b1f6..31b6baa 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/LineMapper.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/LineMapper.java
@@ -52,11 +52,9 @@
   void appendReplace(int aLen, int bLen) {
     appendCommon(Math.min(aLen, bLen));
     if (aLen < bLen) { // Edit with insertion
-      int insertCnt = bLen - aLen;
-      appendInsert(insertCnt);
+      appendInsert(bLen - aLen);
     } else if (aLen > bLen) { // Edit with deletion
-      int deleteCnt = aLen - bLen;
-      appendDelete(deleteCnt);
+      appendDelete(aLen - bLen);
     }
   }
 
@@ -114,7 +112,7 @@
    *      ...
    */
   LineOnOtherInfo lineOnOther(DisplaySide mySide, int line) {
-    List<LineGap> lineGaps = mySide == DisplaySide.A ? lineMapAtoB : lineMapBtoA;
+    List<LineGap> lineGaps = gapList(mySide);
     // Create a dummy LineGap for the search.
     int ret = Collections.binarySearch(lineGaps, new LineGap(line));
     if (ret == -1) {
@@ -132,6 +130,38 @@
     }
   }
 
+  AlignedPair align(DisplaySide mySide, int line) {
+    List<LineGap> gaps = gapList(mySide);
+    int idx = Collections.binarySearch(gaps, new LineGap(line));
+    if (idx == -1) {
+      return new AlignedPair(line, line);
+    }
+
+    LineGap g = gaps.get(0 <= idx ? idx : -idx - 2);
+    if (g.start <= line && line <= g.end && g.end != -1) {
+      if (0 < g.start) {
+        // Line falls within this gap, use alignment before.
+        return new AlignedPair(g.start - 1, g.end + g.delta);
+      }
+      return new AlignedPair(g.end, g.end + g.delta + 1);
+    }
+    return new AlignedPair(line, line + g.delta);
+  }
+
+  private List<LineGap> gapList(DisplaySide mySide) {
+    return mySide == DisplaySide.A ? lineMapAtoB : lineMapBtoA;
+  }
+
+  static class AlignedPair {
+    final int src;
+    final int dst;
+
+    AlignedPair(int s, int d) {
+      src = s;
+      dst = d;
+    }
+  }
+
   /**
    * @field line The line number on the other side.
    * @field aligned Whether the two lines are at the same height when displayed.
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PaddingManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PaddingManager.java
deleted file mode 100644
index 3ff7c99..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PaddingManager.java
+++ /dev/null
@@ -1,170 +0,0 @@
-// Copyright (C) 2013 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.diff;
-
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.Style.Unit;
-
-import net.codemirror.lib.LineWidget;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Manages paddings for CommentBoxes. Each line that may need to be padded owns
- * a PaddingManager instance, which maintains a padding widget whose height
- * changes as necessary. PaddingManager calculates padding by taking the
- * difference of the sum of CommentBox heights on the two sides.
- *
- * Note that in the case of an insertion or deletion gap, A PaddingManager
- * can map to a list of managers on the other side. The padding needed is then
- * calculated from the sum of all their heights.
- *
- * TODO: Let PaddingManager also take care of the paddings introduced by
- * insertions and deletions.
- */
-class PaddingManager {
-  private List<CommentBox> comments;
-  private PaddingWidgetWrapper wrapper;
-  private List<PaddingManager> others;
-
-  PaddingManager(PaddingWidgetWrapper padding) {
-    comments = new ArrayList<CommentBox>();
-    others = new ArrayList<PaddingManager>();
-    this.wrapper = padding;
-  }
-
-  static void link(PaddingManager a, PaddingManager b) {
-    if (!a.others.contains(b)) {
-      a.others.add(b);
-    }
-    if (!b.others.contains(a)) {
-      b.others.add(a);
-    }
-  }
-
-  private int getMyTotalHeight() {
-    int total = 0;
-    for (CommentBox box : comments) {
-      /**
-       * This gets the height of CM's line widget div, taking the margin and
-       * the horizontal scrollbar into account.
-       */
-      total += box.getSelfWidgetWrapper().getElement().getParentElement().getOffsetHeight();
-    }
-    return total;
-  }
-
-  /**
-   * If this instance is on the insertion side, its counterpart on the other
-   * side will map to a group of PaddingManagers on this side, so we calculate
-   * the group's total height instead of an individual one's.
-   */
-  private int getGroupTotalHeight() {
-    if (others.size() > 1) {
-      return getMyTotalHeight();
-    } else {
-      return others.get(0).getOthersTotalHeight();
-    }
-  }
-
-  private int getOthersTotalHeight() {
-    int total = 0;
-    for (PaddingManager manager : others) {
-      total += manager.getMyTotalHeight();
-    }
-    return total;
-  }
-
-  private void setPaddingHeight(int height) {
-    wrapper.element.getStyle().setHeight((double) height, Unit.PX);
-    wrapper.widget.changed();
-  }
-
-  void resizePaddingWidget() {
-    if (others.isEmpty()) {
-      return;
-    }
-    int myHeight = getGroupTotalHeight();
-    int othersHeight = getOthersTotalHeight();
-    int paddingNeeded = othersHeight - myHeight;
-    if (paddingNeeded < 0) {
-      for (PaddingManager manager : others.get(0).others) {
-        manager.setPaddingHeight(0);
-      }
-      others.get(others.size() - 1).setPaddingHeight(-paddingNeeded);
-    } else {
-      setPaddingHeight(paddingNeeded);
-      for (PaddingManager other : others) {
-        other.setPaddingHeight(0);
-      }
-    }
-  }
-
-  /** This is unused now because threading info is ignored. */
-  int getReplyIndex(CommentBox box) {
-    return comments.indexOf(box) + 1;
-  }
-
-  int getCurrentCount() {
-    return comments.size();
-  }
-
-  void insert(CommentBox box, int index) {
-    comments.add(index, box);
-  }
-
-  void remove(CommentBox box) {
-    comments.remove(box);
-  }
-
-  static class PaddingWidgetWrapper {
-    private LineWidget widget;
-    private Element element;
-
-    PaddingWidgetWrapper(LineWidget w, Element e) {
-      widget = w;
-      element = e;
-    }
-
-    LineWidget getWidget() {
-      return widget;
-    }
-
-    Element getElement() {
-      return element;
-    }
-  }
-
-  static class LinePaddingWidgetWrapper extends PaddingWidgetWrapper {
-    private int chunkLength;
-    private int otherLine;
-
-    LinePaddingWidgetWrapper(PaddingWidgetWrapper pair, int otherLine, int chunkLength) {
-      super(pair.widget, pair.element);
-
-      this.otherLine = otherLine;
-      this.chunkLength = chunkLength;
-    }
-
-    int getChunkLength() {
-      return chunkLength;
-    }
-
-    int getOtherLine() {
-      return otherLine;
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox2.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox2.java
index aca4122..7154c9f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox2.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox2.java
@@ -49,7 +49,7 @@
   @UiField HTMLPanel linkPanel;
   @UiField BoxStyle style;
 
-  private DiffTable table;
+  private SideBySide2 parent;
   private DisplaySide side;
   private boolean sideA;
   private String path;
@@ -58,12 +58,16 @@
   private PatchSet.Id idActive;
   private PatchSetSelectBox2 other;
 
-  PatchSetSelectBox2(DiffTable table, final DisplaySide side,
-      final Change.Id changeId, final PatchSet.Id revision, String path) {
+  PatchSetSelectBox2(SideBySide2 parent,
+      DisplaySide side,
+      Change.Id changeId,
+      PatchSet.Id revision,
+      String path) {
     initWidget(uiBinder.createAndBindUi(this));
     icon.setTitle(PatchUtil.C.addFileCommentToolTip());
     icon.addStyleName(Gerrit.RESOURCES.css().link());
-    this.table = table;
+
+    this.parent = parent;
     this.side = side;
     this.sideA = side == DisplaySide.A;
     this.changeId = changeId;
@@ -127,6 +131,7 @@
 
   @UiHandler("icon")
   void onIconClick(ClickEvent e) {
-    table.createOrEditFileComment(side);
+    parent.getCmFromSide(side).scrollToY(0);
+    parent.getCommentManager().insertNewDraft(side, 0);
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java
index 823e57a..4b95c6a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.CommentApi;
 import com.google.gerrit.client.changes.CommentInfo;
-import com.google.gerrit.client.changes.CommentInput;
 import com.google.gerrit.client.changes.Util;
 import com.google.gerrit.client.patches.PatchUtil;
 import com.google.gerrit.client.rpc.GerritCallback;
@@ -39,8 +38,6 @@
 import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
-import net.codemirror.lib.CodeMirror;
-
 /** An HtmlPanel for displaying a published comment */
 class PublishedBox extends CommentBox {
   interface Binder extends UiBinder<HTMLPanel, PublishedBox> {}
@@ -68,12 +65,11 @@
   AvatarImage avatar;
 
   PublishedBox(
-      CommentManager manager,
-      CodeMirror cm,
+      CommentGroup group,
       CommentLinkProcessor clp,
       PatchSet.Id psId,
       CommentInfo info) {
-    super(manager, cm, info);
+    super(group, info.range());
 
     this.psId = psId;
     this.comment = info;
@@ -125,9 +121,9 @@
     super.setOpen(open);
   }
 
-  void registerReplyBox(DraftBox box) {
+  void setReplyBox(DraftBox box) {
     replyBox = box;
-    box.registerReplyToBox(this);
+    box.setReplyToBox(this);
   }
 
   void unregisterReplyBox() {
@@ -139,21 +135,17 @@
     replyBox.setEdit(true);
   }
 
-  DraftBox addReplyBox() {
-    DraftBox box = getCommentManager().addDraftBox(
-        getCommentManager().createReply(comment), getCm().side());
-    registerReplyBox(box);
-    return box;
+  void addReplyBox() {
+    getCommentManager().addDraftBox(
+      getCm().side(),
+      CommentInfo.createReply(comment)).setEdit(true);
   }
 
   void doReply() {
     if (!Gerrit.isSignedIn()) {
       Gerrit.doSignIn(getCommentManager().getSideBySide2().getToken());
     } else if (replyBox == null) {
-      DraftBox box = addReplyBox();
-      if (!getCommentInfo().has_line()) {
-        getCommentManager().addFileCommentBox(box);
-      }
+      addReplyBox();
     } else {
       openReplyBox();
     }
@@ -172,19 +164,15 @@
       Gerrit.doSignIn(getCommentManager().getSideBySide2().getToken());
     } else if (replyBox == null) {
       done.setEnabled(false);
-      CommentInput input = CommentInput.create(getCommentManager().createReply(comment));
-      input.setMessage(PatchUtil.C.cannedReplyDone());
+      CommentInfo input = CommentInfo.createReply(comment);
+      input.message(PatchUtil.C.cannedReplyDone());
       CommentApi.createDraft(psId, input,
           new GerritCallback<CommentInfo>() {
             @Override
             public void onSuccess(CommentInfo result) {
               done.setEnabled(true);
               setOpen(false);
-              DraftBox box = getCommentManager().addDraftBox(result, getCm().side());
-              registerReplyBox(box);
-              if (!getCommentInfo().has_line()) {
-                getCommentManager().addFileCommentBox(box);
-              }
+              getCommentManager().addDraftBox(getCm().side(), result);
             }
           });
     } else {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.ui.xml
index f3beda1..6e88c5b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.ui.xml
@@ -53,7 +53,8 @@
         <div ui:field='summary' class='{res.style.summary}'/>
         <div ui:field='date' class='{res.style.date}'/>
       </g:HTMLPanel>
-      <div ui:field='message' aria-hidden='true' style='display: NONE'/>
+      <div ui:field='message' class='{res.style.message}'
+           aria-hidden='true' style='display: NONE'/>
       <div ui:field='buttons' aria-hidden='true' style='display: NONE'>
         <g:Button ui:field='reply' styleName=''
             title='Reply to this comment'>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Resources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Resources.java
index 5356f2c..8c4fc51 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Resources.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Resources.java
@@ -30,8 +30,10 @@
   @Source("gear.png") ImageResource gear();
 
   interface Style extends CssResource {
+    String commentWidgets();
     String commentBox();
     String contents();
+    String message();
     String header();
     String summary();
     String date();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollSynchronizer.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollSynchronizer.java
index 898f066..033b08b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollSynchronizer.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollSynchronizer.java
@@ -14,11 +14,9 @@
 
 package com.google.gerrit.client.diff;
 
-import com.google.gerrit.client.diff.LineMapper.LineOnOtherInfo;
 import com.google.gwt.user.client.Timer;
 
 import net.codemirror.lib.CodeMirror;
-import net.codemirror.lib.CodeMirror.Viewport;
 import net.codemirror.lib.ScrollInfo;
 
 class ScrollSynchronizer {
@@ -75,7 +73,7 @@
       if (active == this) {
         ScrollInfo si = src.getScrollInfo();
         updateScreenHeader(si);
-        dst.scrollTo(si.getLeft(), si.getTop());
+        dst.scrollTo(si.getLeft(), align(si.getTop()));
         state = 0;
       }
     }
@@ -84,37 +82,32 @@
       switch (state) {
         case 0:
           state = 1;
+          dst.scrollToY(align(src.getScrollInfo().getTop()));
           break;
         case 1:
           state = 2;
-          return;
+          break;
         case 2:
           active = null;
           fixup.cancel();
-          return;
+          break;
       }
+    }
 
+    private double align(double srcTop) {
       // Since CM doesn't always take the height of line widgets into
       // account when calculating scrollInfo when scrolling too fast (e.g.
       // throw scrolling), simply setting scrollTop to be the same doesn't
       // guarantee alignment.
       //
-      // Iterate over the viewport to find the first line that isn't part of
-      // an insertion or deletion gap, for which isAligned() will be true.
-      // We then manually examine if the lines that should be aligned are at
-      // the same height. If not, perform additional scrolling.
-      Viewport fromTo = src.getViewport();
-      for (int line = fromTo.getFrom(); line <= fromTo.getTo(); line++) {
-        LineOnOtherInfo info = mapper.lineOnOther(srcSide, line);
-        if (info.isAligned()) {
-          double sy = src.heightAtLine(line);
-          double dy = dst.heightAtLine(info.getLine());
-          if (Math.abs(dy - sy) >= 1) {
-            dst.scrollToY(dst.getScrollInfo().getTop() + (dy - sy));
-          }
-          break;
-        }
-      }
+      // Find a pair of lines that are aligned and near the top of
+      // the viewport. Use that distance to correct the Y coordinate.
+      int line = src.lineAtHeight(srcTop, "local");
+      LineMapper.AlignedPair p = mapper.align(srcSide, line);
+
+      double sy = src.heightAtLine(p.src, "local");
+      double dy = dst.heightAtLine(p.dst, "local");
+      return Math.max(0, dy + (srcTop - sy));
     }
   }
 }
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 fb3c570..e20686c 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
@@ -22,7 +22,6 @@
 import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.changes.ChangeList;
 import com.google.gerrit.client.diff.LineMapper.LineOnOtherInfo;
-import com.google.gerrit.client.diff.PaddingManager.PaddingWidgetWrapper;
 import com.google.gerrit.client.patches.PatchUtil;
 import com.google.gerrit.client.projects.ConfigInfoCache;
 import com.google.gerrit.client.rpc.CallbackGroup;
@@ -56,8 +55,6 @@
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Image;
-import com.google.gwt.user.client.ui.SimplePanel;
-import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtexpui.globalkey.client.GlobalKey;
 import com.google.gwtexpui.globalkey.client.KeyCommand;
 import com.google.gwtexpui.globalkey.client.KeyCommandSet;
@@ -68,12 +65,10 @@
 import net.codemirror.lib.CodeMirror.GutterClickHandler;
 import net.codemirror.lib.CodeMirror.LineClassWhere;
 import net.codemirror.lib.CodeMirror.LineHandle;
-import net.codemirror.lib.CodeMirror.RenderLineHandler;
 import net.codemirror.lib.CodeMirror.Viewport;
 import net.codemirror.lib.Configuration;
 import net.codemirror.lib.KeyMap;
 import net.codemirror.lib.LineCharacter;
-import net.codemirror.lib.LineWidget;
 import net.codemirror.lib.ModeInjector;
 import net.codemirror.lib.Rect;
 
@@ -112,7 +107,6 @@
   private KeyCommandSet keysAction;
   private KeyCommandSet keysComment;
   private List<HandlerRegistration> handlers;
-  private List<Runnable> deferred;
   private PreferencesAction prefsAction;
   private int reloadVersionId;
 
@@ -232,7 +226,7 @@
     });
     diffTable.sidePanel.adjustGutters(cmB);
 
-    if (startSide == null && diff.meta_b() != null) {
+    if (startLine == 0 && diff.meta_b() != null) {
       DiffChunkInfo d = chunkManager.getFirst();
       if (d != null) {
         startSide = d.getSide();
@@ -294,7 +288,6 @@
     cm.on("beforeSelectionChange", onSelectionChange(cm));
     cm.on("cursorActivity", updateActiveLine(cm));
     cm.on("gutterClick", onGutterClick(cm));
-    cm.on("renderLine", resizeLinePadding(cm.side()));
     cm.on("viewportChange", adjustGutters(cm));
     cm.on("focus", new Runnable() {
       @Override
@@ -313,7 +306,10 @@
         .on("'c'", commentManager.insertNewDraft(cm))
         .on("N", maybeNextVimSearch(cm))
         .on("P", chunkManager.diffChunkNav(cm, Direction.PREV))
-        .on("Shift-O", commentManager.openClosePublished(cm))
+        .on("Shift-M", header.reviewedAndNext())
+        .on("Shift-N", commentManager.commentNav(cm, Direction.NEXT))
+        .on("Shift-P", commentManager.commentNav(cm, Direction.PREV))
+        .on("Shift-O", commentManager.openCloseAll(cm))
         .on("Shift-Left", moveCursorToSide(cm, DisplaySide.A))
         .on("Shift-Right", moveCursorToSide(cm, DisplaySide.B))
         .on("'i'", new Runnable() {
@@ -412,6 +408,9 @@
     keysNavigation.add(
         new NoOpKeyCommand(0, 'n', PatchUtil.C.chunkNext2()),
         new NoOpKeyCommand(0, 'p', PatchUtil.C.chunkPrev2()));
+    keysNavigation.add(
+        new NoOpKeyCommand(KeyCommand.M_SHIFT, 'n', PatchUtil.C.commentNext()),
+        new NoOpKeyCommand(KeyCommand.M_SHIFT, 'p', PatchUtil.C.commentPrev()));
 
     keysAction = new KeyCommandSet(Gerrit.C.sectionActions());
     keysAction.add(new NoOpKeyCommand(0, KeyCodes.KEY_ENTER,
@@ -425,6 +424,13 @@
         header.toggleReviewed().run();
       }
     });
+    keysAction.add(new KeyCommand(
+        KeyCommand.M_SHIFT, 'm', PatchUtil.C.markAsReviewedAndGoToNext()) {
+      @Override
+      public void onKeyPress(KeyPressEvent event) {
+        header.reviewedAndNext().run();
+      }
+    });
     keysAction.add(new KeyCommand(0, 'a', PatchUtil.C.openReply()) {
       @Override
       public void onKeyPress(KeyPressEvent event) {
@@ -486,10 +492,7 @@
         cmB.setHeight(height);
 
         render(diff);
-        commentManager.render(comments);
-        if (prefs.expandAllComments()) {
-          commentManager.setExpandAllComments(true);
-        }
+        commentManager.render(comments, prefs.expandAllComments());
         skipManager.render(prefs.context(), diff);
       }
     });
@@ -620,42 +623,6 @@
     return chunkManager.getLineMapper().lineOnOther(side, line);
   }
 
-  PaddingWidgetWrapper addPaddingWidget(CodeMirror cm,
-      int line, double height, Unit unit, Integer index) {
-    SimplePanel padding = new SimplePanel();
-    padding.setStyleName(DiffTable.style.padding());
-    padding.getElement().getStyle().setHeight(height, unit);
-    Configuration config = Configuration.create()
-        .set("coverGutter", true)
-        .set("above", line == -1)
-        .set("noHScroll", true);
-    if (index != null) {
-      config = config.set("insertAt", index);
-    }
-    LineWidget widget = addLineWidget(cm, line == -1 ? 0 : line, padding, config);
-    return new PaddingWidgetWrapper(widget, padding.getElement());
-  }
-
-  /**
-   * A LineWidget needs to be added to diffTable in order to respond to browser
-   * events, but CodeMirror doesn't render the widget until the containing line
-   * is scrolled into viewportMargin, causing it to appear at the bottom of the
-   * DOM upon loading. Fix by hiding the widget until it is first scrolled into
-   * view (when CodeMirror fires a "redraw" event on the widget).
-   */
-  LineWidget addLineWidget(CodeMirror cm, int line,
-      final Widget widget, Configuration options) {
-    widget.setVisible(false);
-    LineWidget lineWidget = cm.addLineWidget(line, widget.getElement(), options);
-    lineWidget.onFirstRedraw(new Runnable() {
-      @Override
-      public void run() {
-        widget.setVisible(true);
-      }
-    });
-    return lineWidget;
-  }
-
   private void clearActiveLine(CodeMirror cm) {
     if (cm.hasActiveLine()) {
       LineHandle activeLine = cm.getActiveLine();
@@ -684,34 +651,37 @@
     final CodeMirror other = otherCm(cm);
     return new Runnable() {
       public void run() {
-        /**
-         * The rendering of active lines has to be deferred. Reflow
-         * caused by adding and removing styles chokes Firefox when arrow
-         * key (or j/k) is held down. Performance on Chrome is fine
-         * without the deferral.
-         */
-        defer(new Runnable() {
+        // The rendering of active lines has to be deferred. Reflow
+        // caused by adding and removing styles chokes Firefox when arrow
+        // key (or j/k) is held down. Performance on Chrome is fine
+        // without the deferral.
+        //
+        Scheduler.get().scheduleDeferred(new ScheduledCommand() {
           @Override
-          public void run() {
-            LineHandle handle = cm.getLineHandleVisualStart(
-                cm.getCursor("end").getLine());
-            if (cm.hasActiveLine() && cm.getActiveLine().equals(handle)) {
-              return;
-            }
+          public void execute() {
+            operation(new Runnable() {
+              public void run() {
+                LineHandle handle = cm.getLineHandleVisualStart(
+                    cm.getCursor("end").getLine());
+                if (cm.hasActiveLine() && cm.getActiveLine().equals(handle)) {
+                  return;
+                }
 
-            clearActiveLine(cm);
-            clearActiveLine(other);
-            cm.setActiveLine(handle);
-            cm.addLineClass(
-                handle, LineClassWhere.WRAP, DiffTable.style.activeLine());
-            LineOnOtherInfo info =
-                lineOnOther(cm.side(), cm.getLineNumber(handle));
-            if (info.isAligned()) {
-              LineHandle oLineHandle = other.getLineHandle(info.getLine());
-              other.setActiveLine(oLineHandle);
-              other.addLineClass(oLineHandle, LineClassWhere.WRAP,
-                  DiffTable.style.activeLine());
-            }
+                clearActiveLine(cm);
+                clearActiveLine(other);
+                cm.setActiveLine(handle);
+                cm.addLineClass(
+                    handle, LineClassWhere.WRAP, DiffTable.style.activeLine());
+                LineOnOtherInfo info =
+                    lineOnOther(cm.side(), cm.getLineNumber(handle));
+                if (info.isAligned()) {
+                  LineHandle oLineHandle = other.getLineHandle(info.getLine());
+                  other.setActiveLine(oLineHandle);
+                  other.addLineClass(oLineHandle, LineClassWhere.WRAP,
+                      DiffTable.style.activeLine());
+                }
+              }
+            });
           }
         });
       }
@@ -798,39 +768,6 @@
     };
   }
 
-
-  void defer(Runnable thunk) {
-    if (deferred == null) {
-      final ArrayList<Runnable> list = new ArrayList<Runnable>();
-      deferred = list;
-      Scheduler.get().scheduleDeferred(new ScheduledCommand() {
-        @Override
-        public void execute() {
-          deferred = null;
-          operation(new Runnable() {
-            public void run() {
-              for (Runnable thunk : list) {
-                thunk.run();
-              }
-            }
-          });
-        }
-      });
-    }
-    deferred.add(thunk);
-  }
-
-  // TODO: Maybe integrate this with PaddingManager.
-  private RenderLineHandler resizeLinePadding(final DisplaySide side) {
-    return new RenderLineHandler() {
-      @Override
-      public void handle(CodeMirror cm, LineHandle lh, Element e) {
-        commentManager.resizePadding(lh);
-        chunkManager.resizePadding(cm, lh, side);
-      }
-    };
-  }
-
   void resizeCodeMirror() {
     int height = getCodeMirrorHeight();
     cmA.setHeight(height);
@@ -873,6 +810,10 @@
     return commentManager;
   }
 
+  SkipManager getSkipManager() {
+    return skipManager;
+  }
+
   void operation(final Runnable apply) {
     cmA.operation(new Runnable() {
       @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java
index ac644ce..6ab456a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java
@@ -52,6 +52,7 @@
   private final SkipManager manager;
   private final CodeMirror cm;
 
+  private int start;
   private LineWidget lineWidget;
   private TextMarker textMarker;
   private SkipBar otherBar;
@@ -72,6 +73,14 @@
     }, ClickEvent.getType());
   }
 
+  int getStart() {
+    return start;
+  }
+
+  DisplaySide getSide() {
+    return cm.side();
+  }
+
   void collapse(int start, int end, boolean attach) {
     if (attach) {
       boolean isNew = lineWidget == null;
@@ -84,16 +93,17 @@
         lineWidget = cm.addLineWidget(start - 1, getElement(), cfg);
       }
       if (isNew) {
-        setVisible(false);
         lineWidget.onFirstRedraw(new Runnable() {
           @Override
           public void run() {
             int w = cm.getGutterElement().getOffsetWidth();
             getElement().getStyle().setPaddingLeft(w, Unit.PX);
-            setVisible(true);
           }
         });
+      } else {
+        manager.move(this, this.start, start);
       }
+      this.start = start;
     }
 
     textMarker = cm.markText(
@@ -124,22 +134,32 @@
     lineWidget.clear();
   }
 
-  void expandAll() {
-    clearMarkerAndWidget();
-    removeFromParent();
-    updateSelection();
+  void expandBefore(int cnt) {
+    expandSideBefore(cnt);
+    otherBar.expandSideBefore(cnt);
   }
 
-  private void expandBefore() {
+  private void expandSideBefore(int cnt) {
     FromTo range = textMarker.find();
     int oldStart = range.getFrom().getLine();
-    int newStart = oldStart + NUM_ROWS_TO_EXPAND;
+    int newStart = oldStart + cnt;
     int end = range.getTo().getLine();
     clearMarkerAndWidget();
     collapse(newStart, end, true);
     updateSelection();
   }
 
+  void expandAll() {
+    expandSideAll();
+    otherBar.expandSideAll();
+  }
+
+  private void expandSideAll() {
+    clearMarkerAndWidget();
+    removeFromParent();
+    updateSelection();
+  }
+
   private void expandAfter() {
     FromTo range = textMarker.find();
     int start = range.getFrom().getLine();
@@ -159,16 +179,14 @@
 
   @UiHandler("skipNum")
   void onExpandAll(ClickEvent e) {
-    manager.remove(this, otherBar);
-    otherBar.expandAll();
     expandAll();
+    manager.remove(this, otherBar);
     cm.focus();
   }
 
   @UiHandler("upArrow")
   void onExpandBefore(ClickEvent e) {
-    otherBar.expandBefore();
-    expandBefore();
+    expandBefore(NUM_ROWS_TO_EXPAND);
     cm.focus();
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipManager.java
index 575d8ee..e4be137 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipManager.java
@@ -22,15 +22,16 @@
 import net.codemirror.lib.CodeMirror;
 
 import java.util.ArrayList;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
 
 /** Collapses common regions with {@link SkipBar} for {@link SideBySide2}. */
 class SkipManager {
   private final SideBySide2 host;
   private final CommentManager commentManager;
-  private Set<SkipBar> skipBars;
+  private SortedMap<Integer, SkipBar> sideA;
+  private SortedMap<Integer, SkipBar> sideB;
 
   SkipManager(SideBySide2 host, CommentManager commentManager) {
     this.host = host;
@@ -71,13 +72,15 @@
       CodeMirror cmA = host.getCmFromSide(DisplaySide.A);
       CodeMirror cmB = host.getCmFromSide(DisplaySide.B);
 
-      skipBars = new HashSet<SkipBar>();
+      sideA = new TreeMap<Integer, SkipBar>();
+      sideB = new TreeMap<Integer, SkipBar>();
+
       for (SkippedLine skip : skips) {
         SkipBar barA = newSkipBar(cmA, DisplaySide.A, skip);
         SkipBar barB = newSkipBar(cmB, DisplaySide.B, skip);
         SkipBar.link(barA, barB);
-        skipBars.add(barA);
-        skipBars.add(barB);
+        sideA.put(barA.getStart(), barA);
+        sideB.put(barB.getStart(), barB);
 
         if (skip.getStartA() == 0 || skip.getStartB() == 0) {
           barA.upArrow.setVisible(false);
@@ -91,23 +94,41 @@
     }
   }
 
+  void ensureFirstLineIsVisible() {
+    if (sideB != null) {
+      SkipBar line0 = sideB.get(0);
+      if (line0 != null) {
+        line0.expandBefore(1);
+      }
+    }
+  }
+
   void removeAll() {
-    if (skipBars != null) {
-      for (SkipBar bar : skipBars) {
+    if (sideB != null) {
+      for (SkipBar bar : sideB.values()) {
         bar.expandAll();
       }
-      skipBars = null;
+      sideA = null;
+      sideB = null;
     }
   }
 
   void remove(SkipBar a, SkipBar b) {
-    skipBars.remove(a);
-    skipBars.remove(b);
-    if (skipBars.isEmpty()) {
-      skipBars = null;
+    map(a.getSide()).remove(a.getStart());
+    map(b.getSide()).remove(b.getStart());
+
+    if (sideB.isEmpty()) {
+      sideA = null;
+      sideB = null;
     }
   }
 
+  void move(SkipBar bar, int oldLine, int newLine) {
+    SortedMap<Integer, SkipBar> map = map(bar.getSide());
+    map.remove(oldLine);
+    map.put(newLine, bar);
+  }
+
   private SkipBar newSkipBar(CodeMirror cm, DisplaySide side, SkippedLine skip) {
     int start = side == DisplaySide.A ? skip.getStartA() : skip.getStartB();
     int end = start + skip.getSize() - 1;
@@ -117,4 +138,8 @@
     bar.collapse(start, end, true);
     return bar;
   }
+
+  private SortedMap<Integer, SkipBar> map(DisplaySide side) {
+    return side == DisplaySide.A ? sideA : sideB;
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
index deaeed9..8f50b10 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.client.account.AccountInfo;
 import com.google.gerrit.client.changes.CommentApi;
 import com.google.gerrit.client.changes.CommentInfo;
-import com.google.gerrit.client.changes.CommentInput;
 import com.google.gerrit.client.changes.PatchTable;
 import com.google.gerrit.client.changes.Util;
 import com.google.gerrit.client.rpc.GerritCallback;
@@ -941,7 +940,7 @@
       if (p == null) {
         enableButtons(false);
         final PatchSet.Id psId = newComment.getKey().getParentKey().getParentKey();
-        CommentInput in = CommentEditorPanel.toInput(newComment);
+        CommentInfo in = CommentEditorPanel.toInput(newComment);
         CommentApi.createDraft(psId, in,
             new GerritCallback<CommentInfo>() {
               @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java
index 1bc2c42..32302f4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.CommentApi;
 import com.google.gerrit.client.changes.CommentInfo;
-import com.google.gerrit.client.changes.CommentInput;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.CommentLinkProcessor;
 import com.google.gerrit.client.ui.CommentPanel;
@@ -268,7 +267,7 @@
         onSave.onFailure(caught);
       }
     };
-    CommentInput input = toInput(comment);
+    CommentInfo input = toInput(comment);
     if (wasNew) {
       CommentApi.createDraft(psId, input, cb);
     } else {
@@ -336,16 +335,16 @@
     return null;
   }
 
-  public static CommentInput toInput(PatchLineComment c) {
-    CommentInput i = CommentInput.createObject().cast();
-    i.setId(c.getKey().get());
-    i.setPath(c.getKey().getParentKey().get());
-    i.setSide(c.getSide() == 0 ? Side.PARENT : Side.REVISION);
+  public static CommentInfo toInput(PatchLineComment c) {
+    CommentInfo i = CommentInfo.createObject().cast();
+    i.id(c.getKey().get());
+    i.path(c.getKey().getParentKey().get());
+    i.side(c.getSide() == 0 ? Side.PARENT : Side.REVISION);
     if (c.getLine() > 0) {
-      i.setLine(c.getLine());
+      i.line(c.getLine());
     }
-    i.setInReplyTo(c.getParentUuid());
-    i.setMessage(c.getMessage());
+    i.in_reply_to(c.getParentUuid());
+    i.message(c.getMessage());
     return i;
   }
 
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 150978d..a1883cf 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
@@ -327,6 +327,9 @@
   public static class Viewport extends JavaScriptObject {
     public final native int getFrom() /*-{ return this.from; }-*/;
     public final native int getTo() /*-{ return this.to; }-*/;
+    public final boolean contains(int line) {
+      return getFrom() <= line && line < getTo();
+    }
 
     protected Viewport() {
     }
diff --git a/gerrit-gwtui/src/test/java/com/google/gerrit/client/diff/LineMapperTest.java b/gerrit-gwtui/src/test/java/com/google/gerrit/client/diff/LineMapperTest.java
index 756e879..25c8270 100644
--- a/gerrit-gwtui/src/test/java/com/google/gerrit/client/diff/LineMapperTest.java
+++ b/gerrit-gwtui/src/test/java/com/google/gerrit/client/diff/LineMapperTest.java
@@ -102,4 +102,31 @@
     assertEquals(new LineOnOtherInfo(10, true),
         mapper.lineOnOther(DisplaySide.B, 0));
   }
+
+  @Test
+  public void testReplaceWithInsertInB() {
+    // 0 c c
+    // 1 a b
+    // 2 a b
+    // 3 - b
+    // 4 - b
+    // 5 c c
+    LineMapper mapper = new LineMapper();
+    mapper.appendCommon(1);
+    mapper.appendReplace(2, 4);
+    mapper.appendCommon(1);
+
+    assertEquals(4, mapper.getLineA());
+    assertEquals(6, mapper.getLineB());
+
+    assertEquals(new LineOnOtherInfo(1, true),
+        mapper.lineOnOther(DisplaySide.B, 1));
+    assertEquals(new LineOnOtherInfo(3, true),
+        mapper.lineOnOther(DisplaySide.B, 5));
+
+    assertEquals(new LineOnOtherInfo(2, true),
+        mapper.lineOnOther(DisplaySide.B, 2));
+    assertEquals(new LineOnOtherInfo(2, false),
+        mapper.lineOnOther(DisplaySide.B, 3));
+  }
 }
diff --git a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
index bd8f081..82981c1 100644
--- a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -156,10 +156,7 @@
     final Method main;
     try {
       main = clazz.getMethod("main", argv.getClass());
-    } catch (SecurityException e) {
-      System.err.println("fatal: unknown command " + name);
-      return 1;
-    } catch (NoSuchMethodException e) {
+    } catch (SecurityException | NoSuchMethodException e) {
       System.err.println("fatal: unknown command " + name);
       return 1;
     }
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index 9d9ce22..b92d1cb 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -54,6 +54,7 @@
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 
+import org.apache.lucene.analysis.Analyzer;
 import org.apache.lucene.analysis.standard.StandardAnalyzer;
 import org.apache.lucene.analysis.util.CharArraySet;
 import org.apache.lucene.document.Document;
@@ -157,6 +158,7 @@
   private final ChangeData.Factory changeDataFactory;
   private final File dir;
   private final Schema<ChangeData> schema;
+  private final QueryBuilder queryBuilder;
   private final SubIndex openIndex;
   private final SubIndex closedIndex;
 
@@ -186,6 +188,10 @@
         LUCENE_VERSIONS.get(schema),
         "unknown Lucene version for index schema: %s", schema);
 
+    Analyzer analyzer =
+        new StandardAnalyzer(luceneVersion, CharArraySet.EMPTY_SET);
+    queryBuilder = new QueryBuilder(schema, analyzer);
+
     IndexWriterConfig openConfig =
         getIndexWriterConfig(luceneVersion, cfg, "changes_open");
     IndexWriterConfig closedConfig =
@@ -298,7 +304,7 @@
     if (!Sets.intersection(statuses, CLOSED_STATUSES).isEmpty()) {
       indexes.add(closedIndex);
     }
-    return new QuerySource(indexes, QueryBuilder.toQuery(schema, p), limit,
+    return new QuerySource(indexes, queryBuilder.toQuery(p), limit,
         ChangeQueryBuilder.hasNonTrivialSortKeyAfter(schema, p));
   }
 
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/QueryBuilder.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/QueryBuilder.java
index f99cce8..faa9fe0 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/QueryBuilder.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/QueryBuilder.java
@@ -33,9 +33,9 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.SortKeyPredicate;
 
+import org.apache.lucene.analysis.Analyzer;
 import org.apache.lucene.index.Term;
 import org.apache.lucene.search.BooleanQuery;
-import org.apache.lucene.search.FuzzyQuery;
 import org.apache.lucene.search.MatchAllDocsQuery;
 import org.apache.lucene.search.NumericRangeQuery;
 import org.apache.lucene.search.PrefixQuery;
@@ -55,27 +55,34 @@
     return intTerm(ID_FIELD, cd.getId().get());
   }
 
-  public static Query toQuery(Schema<ChangeData> schema, Predicate<ChangeData> p)
-      throws QueryParseException {
+  private final Schema<ChangeData> schema;
+  private final org.apache.lucene.util.QueryBuilder queryBuilder;
+
+  public QueryBuilder(Schema<ChangeData> schema, Analyzer analyzer) {
+    this.schema = schema;
+    queryBuilder = new org.apache.lucene.util.QueryBuilder(analyzer);
+  }
+
+  public Query toQuery(Predicate<ChangeData> p) throws QueryParseException {
     if (p instanceof AndPredicate) {
-      return and(schema, p);
+      return and(p);
     } else if (p instanceof OrPredicate) {
-      return or(schema, p);
+      return or(p);
     } else if (p instanceof NotPredicate) {
-      return not(schema, p);
+      return not(p);
     } else if (p instanceof IndexPredicate) {
-      return fieldQuery(schema, (IndexPredicate<ChangeData>) p);
+      return fieldQuery((IndexPredicate<ChangeData>) p);
     } else {
       throw new QueryParseException("cannot create query for index: " + p);
     }
   }
 
-  private static Query or(Schema<ChangeData> schema, Predicate<ChangeData> p)
+  private Query or(Predicate<ChangeData> p)
       throws QueryParseException {
     try {
       BooleanQuery q = new BooleanQuery();
       for (int i = 0; i < p.getChildCount(); i++) {
-        q.add(toQuery(schema, p.getChild(i)), SHOULD);
+        q.add(toQuery(p.getChild(i)), SHOULD);
       }
       return q;
     } catch (BooleanQuery.TooManyClauses e) {
@@ -83,7 +90,7 @@
     }
   }
 
-  private static Query and(Schema<ChangeData> schema, Predicate<ChangeData> p)
+  private Query and(Predicate<ChangeData> p)
       throws QueryParseException {
     try {
       BooleanQuery b = new BooleanQuery();
@@ -95,10 +102,10 @@
           if (n instanceof TimestampRangePredicate) {
             b.add(notTimestamp((TimestampRangePredicate<ChangeData>) n), MUST);
           } else {
-            not.add(toQuery(schema, n));
+            not.add(toQuery(n));
           }
         } else {
-          b.add(toQuery(schema, c), MUST);
+          b.add(toQuery(c), MUST);
         }
       }
       for (Query q : not) {
@@ -110,7 +117,7 @@
     }
   }
 
-  private static Query not(Schema<ChangeData> schema, Predicate<ChangeData> p)
+  private Query not(Predicate<ChangeData> p)
       throws QueryParseException {
     Predicate<ChangeData> n = p.getChild(0);
     if (n instanceof TimestampRangePredicate) {
@@ -120,12 +127,12 @@
     // Lucene does not support negation, start with all and subtract.
     BooleanQuery q = new BooleanQuery();
     q.add(new MatchAllDocsQuery(), MUST);
-    q.add(toQuery(schema, n), MUST_NOT);
+    q.add(toQuery(n), MUST_NOT);
     return q;
   }
 
-  private static Query fieldQuery(Schema<ChangeData> schema,
-      IndexPredicate<ChangeData> p) throws QueryParseException {
+  private Query fieldQuery(IndexPredicate<ChangeData> p)
+      throws QueryParseException {
     if (p.getType() == FieldType.INTEGER) {
       return intQuery(p);
     } else if (p.getType() == FieldType.TIMESTAMP) {
@@ -137,7 +144,7 @@
     } else if (p.getType() == FieldType.FULL_TEXT) {
       return fullTextQuery(p);
     } else if (p instanceof SortKeyPredicate) {
-      return sortKeyQuery(schema, (SortKeyPredicate) p);
+      return sortKeyQuery((SortKeyPredicate) p);
     } else {
       throw badFieldType(p.getType());
     }
@@ -149,7 +156,7 @@
     return new Term(name, bytes);
   }
 
-  private static Query intQuery(IndexPredicate<ChangeData> p)
+  private Query intQuery(IndexPredicate<ChangeData> p)
       throws QueryParseException {
     int value;
     try {
@@ -162,7 +169,7 @@
     return new TermQuery(intTerm(p.getField().getName(), value));
   }
 
-  private static Query sortKeyQuery(Schema<ChangeData> schema, SortKeyPredicate p) {
+  private Query sortKeyQuery(SortKeyPredicate p) {
     long min = p.getMinValue(schema);
     long max = p.getMaxValue(schema);
     return NumericRangeQuery.newLongRange(
@@ -172,7 +179,7 @@
         false, false);
   }
 
-  private static Query timestampQuery(IndexPredicate<ChangeData> p)
+  private Query timestampQuery(IndexPredicate<ChangeData> p)
       throws QueryParseException {
     if (p instanceof TimestampRangePredicate) {
       TimestampRangePredicate<ChangeData> r =
@@ -186,7 +193,7 @@
     throw new QueryParseException("not a timestamp: " + p);
   }
 
-  private static Query notTimestamp(TimestampRangePredicate<ChangeData> r)
+  private Query notTimestamp(TimestampRangePredicate<ChangeData> r)
       throws QueryParseException {
     if (r.getMinTimestamp().getTime() == 0) {
       return NumericRangeQuery.newIntRange(
@@ -198,7 +205,7 @@
     throw new QueryParseException("cannot negate: " + r);
   }
 
-  private static Query exactQuery(IndexPredicate<ChangeData> p) {
+  private Query exactQuery(IndexPredicate<ChangeData> p) {
     if (p instanceof RegexPredicate<?>) {
       return regexQuery(p);
     } else {
@@ -206,7 +213,7 @@
     }
   }
 
-  private static Query regexQuery(IndexPredicate<ChangeData> p) {
+  private Query regexQuery(IndexPredicate<ChangeData> p) {
     String re = p.getValue();
     if (re.startsWith("^")) {
       re = re.substring(1);
@@ -217,12 +224,12 @@
     return new RegexpQuery(new Term(p.getField().getName(), re));
   }
 
-  private static Query prefixQuery(IndexPredicate<ChangeData> p) {
+  private Query prefixQuery(IndexPredicate<ChangeData> p) {
     return new PrefixQuery(new Term(p.getField().getName(), p.getValue()));
   }
 
-  private static Query fullTextQuery(IndexPredicate<ChangeData> p) {
-    return new FuzzyQuery(new Term(p.getField().getName(), p.getValue()));
+  private Query fullTextQuery(IndexPredicate<ChangeData> p) {
+    return queryBuilder.createPhraseQuery(p.getField().getName(), p.getValue());
   }
 
   public static int toIndexTime(Timestamp ts) {
@@ -232,7 +239,4 @@
   public static IllegalArgumentException badFieldType(FieldType<?> t) {
     return new IllegalArgumentException("unknown index field type " + t);
   }
-
-  private QueryBuilder() {
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
index 6481d5f..a42827a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
@@ -107,7 +107,8 @@
       db.rollback();
     }
 
-    CheckedFuture<?, IOException> indexFuture = indexer.indexAsync(change);
+    CheckedFuture<?, IOException> indexFuture =
+        indexer.indexAsync(change.getId());
     try {
       ReplyToChangeSender cm = abandonedSenderFactory.create(change);
       cm.setFrom(caller.getAccountId());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityChecker.java
index 6c1202f..bf9f65d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityChecker.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityChecker.java
@@ -185,7 +185,8 @@
    *         reindexes the change (whether the mergeability flag was updated or
    *         not)
    */
-  public CheckedFuture<?, IOException> updateAndIndexAsync(final Change change) {
+  public CheckedFuture<?, IOException> updateAndIndexAsync(Change change) {
+    final Change.Id id = change.getId();
     return Futures.makeChecked(
         Futures.transform(updateAsync(change),
           new AsyncFunction<Boolean, Object>() {
@@ -194,7 +195,7 @@
             public ListenableFuture<Object> apply(Boolean indexUpdated)
                 throws Exception {
               if (!indexUpdated) {
-                return (ListenableFuture<Object>) indexer.indexAsync(change);
+                return (ListenableFuture<Object>) indexer.indexAsync(id);
               }
               return Futures.immediateFuture(null);
             }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
index 70936af..0203e3b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
@@ -139,7 +139,7 @@
 
     CheckedFuture<?, IOException> indexWrite;
     if (dirty) {
-      indexWrite = indexer.indexAsync(change);
+      indexWrite = indexer.indexAsync(change.getId());
     } else {
       indexWrite = Futures.<Void, IOException> immediateCheckedFuture(null);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
index 1d6dfe7..46893df 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
@@ -228,7 +228,8 @@
       db.rollback();
     }
 
-    CheckedFuture<?, IOException> indexFuture = indexer.indexAsync(rsrc.getChange());
+    CheckedFuture<?, IOException> indexFuture =
+        indexer.indexAsync(rsrc.getChange().getId());
     result.reviewers = Lists.newArrayListWithCapacity(added.size());
     for (PatchSetApproval psa : added) {
       result.reviewers.add(json.format(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Publish.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Publish.java
index d49527d..f522de5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Publish.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Publish.java
@@ -77,7 +77,7 @@
       if (!updatedPatchSet.isDraft()
           || updatedChange.getStatus() == Change.Status.NEW) {
         CheckedFuture<?, IOException> indexFuture =
-            indexer.indexAsync(updatedChange);
+            indexer.indexAsync(updatedChange.getId());
         hooks.doDraftPublishedHook(updatedChange, updatedPatchSet, dbProvider.get());
         sender.send(rsrc.getChange().getStatus() == Change.Status.DRAFT,
             rsrc.getUser(), updatedChange, updatedPatchSet,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
index 84ace98..a9534a5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
@@ -115,7 +115,8 @@
       } finally {
         db.rollback();
       }
-      CheckedFuture<?, IOException> indexFuture = indexer.indexAsync(change);
+      CheckedFuture<?, IOException> indexFuture =
+          indexer.indexAsync(change.getId());
       hooks.doTopicChangedHook(change, currentUser.getAccount(),
           oldTopicName, db);
       indexFuture.checkedGet();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
index b763079..b3641e5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
@@ -108,7 +108,8 @@
       db.rollback();
     }
 
-    CheckedFuture<?, IOException> indexFuture = indexer.indexAsync(change);
+    CheckedFuture<?, IOException> indexFuture =
+        indexer.indexAsync(change.getId());
     try {
       ReplyToChangeSender cm = restoredSenderFactory.create(change);
       cm.setFrom(caller.getAccountId());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStoreProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStoreProvider.java
index d5cd11a..835dc40 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStoreProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStoreProvider.java
@@ -85,21 +85,10 @@
       Class.forName(PGPPublicKey.class.getName());
       addBouncyCastleProvider();
       return true;
-    } catch (NoClassDefFoundError noBouncyCastle) {
-      return false;
-    } catch (ClassNotFoundException noBouncyCastle) {
-      return false;
-    } catch (SecurityException noBouncyCastle) {
-      return false;
-    } catch (NoSuchMethodException noBouncyCastle) {
-      return false;
-    } catch (InstantiationException noBouncyCastle) {
-      return false;
-    } catch (IllegalAccessException noBouncyCastle) {
-      return false;
-    } catch (InvocationTargetException noBouncyCastle) {
-      return false;
-    } catch (ClassCastException noBouncyCastle) {
+    } catch (NoClassDefFoundError | ClassNotFoundException | SecurityException
+        | NoSuchMethodException | InstantiationException
+        | IllegalAccessException | InvocationTargetException
+        | ClassCastException noBouncyCastle) {
       return false;
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index 3dae94f..e4d0de8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -1043,7 +1043,7 @@
 
     CheckedFuture<?, IOException> indexFuture;
     if (change != null) {
-      indexFuture = indexer.indexAsync(change);
+      indexFuture = indexer.indexAsync(change.getId());
     } else {
       indexFuture = null;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
index 36208c3..9fa46fb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.index;
 
+import com.google.common.base.Splitter;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.Account;
@@ -160,10 +161,11 @@
         }
       };
 
-  /** List of filenames modified in the current patch set. */
-  public static final FieldDef<ChangeData, Iterable<String>> FILE =
+  /** List of full file paths modified in the current patch set. */
+  public static final FieldDef<ChangeData, Iterable<String>> PATH =
       new FieldDef.Repeatable<ChangeData, String>(
-          ChangeQueryBuilder.FIELD_FILE, FieldType.EXACT, false) {
+          // Named for backwards compatibility.
+          "file", FieldType.EXACT, false) {
         @Override
         public Iterable<String> get(ChangeData input, FillArgs args)
             throws OrmException {
@@ -171,6 +173,28 @@
         }
       };
 
+  public static Set<String> getFileParts(ChangeData cd) throws OrmException {
+    Splitter s = Splitter.on('/').omitEmptyStrings();
+    Set<String> r = Sets.newHashSet();
+    for (String path : cd.currentFilePaths()) {
+      for (String part : s.split(path)) {
+        r.add(part);
+      }
+    }
+    return r;
+  }
+
+  /** Components of each file path modified in the current patch set. */
+  public static final FieldDef<ChangeData, Iterable<String>> FILE_PART =
+      new FieldDef.Repeatable<ChangeData, String>(
+          "filepart", FieldType.EXACT, false) {
+        @Override
+        public Iterable<String> get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return getFileParts(input);
+        }
+      };
+
   /** Owner/creator of the change. */
   public static final FieldDef<ChangeData, Integer> OWNER =
       new FieldDef.Single<ChangeData, Integer>(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java
index 612df9a..b500e8a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java
@@ -112,12 +112,12 @@
   /**
    * Start indexing a change.
    *
-   * @param change change to index.
+   * @param id change to index.
    * @return future for the indexing task.
    */
-  public CheckedFuture<?, IOException> indexAsync(Change change) {
+  public CheckedFuture<?, IOException> indexAsync(Change.Id id) {
     return executor != null
-        ? submit(new Task(change, false))
+        ? submit(new Task(id, false))
         : Futures.<Object, IOException> immediateCheckedFuture(null);
   }
 
@@ -145,12 +145,12 @@
   /**
    * Start deleting a change.
    *
-   * @param change change to delete.
+   * @param id change to delete.
    * @return future for the deleting task.
    */
-  public CheckedFuture<?, IOException> deleteAsync(Change change) {
+  public CheckedFuture<?, IOException> deleteAsync(Change.Id id) {
     return executor != null
-        ? submit(new Task(change, true))
+        ? submit(new Task(id, true))
         : Futures.<Object, IOException> immediateCheckedFuture(null);
   }
 
@@ -186,11 +186,11 @@
   }
 
   private class Task implements Callable<Void> {
-    private final Change change;
+    private final Change.Id id;
     private final boolean delete;
 
-    private Task(Change change, boolean delete) {
-      this.change = change;
+    private Task(Change.Id id, boolean delete) {
+      this.id = id;
       this.delete = delete;
     }
 
@@ -225,7 +225,7 @@
         RequestContext oldCtx = context.setContext(newCtx);
         try {
           ChangeData cd = changeDataFactory.create(
-              newCtx.getReviewDbProvider().get(), change);
+              newCtx.getReviewDbProvider().get(), id);
           if (delete) {
             for (ChangeIndex i : getWriteIndexes()) {
               i.delete(cd);
@@ -244,16 +244,14 @@
           }
         }
       } catch (Exception e) {
-        log.error(String.format(
-            "Failed to index change %d in %s",
-            change.getId().get(), change.getProject().get()), e);
+        log.error(String.format("Failed to index change %d", id), e);
         throw e;
       }
     }
 
     @Override
     public String toString() {
-      return "index-change-" + change.getId().get();
+      return "index-change-" + id.get();
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
index eb2c527..475e7d1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
@@ -40,7 +40,7 @@
         ChangeField.TOPIC,
         ChangeField.UPDATED,
         ChangeField.LEGACY_SORTKEY,
-        ChangeField.FILE,
+        ChangeField.PATH,
         ChangeField.OWNER,
         ChangeField.REVIEWER,
         ChangeField.COMMIT,
@@ -60,7 +60,7 @@
         ChangeField.TOPIC,
         ChangeField.UPDATED,
         ChangeField.LEGACY_SORTKEY,
-        ChangeField.FILE,
+        ChangeField.PATH,
         ChangeField.OWNER,
         ChangeField.REVIEWER,
         ChangeField.COMMIT,
@@ -81,7 +81,7 @@
         ChangeField.TOPIC,
         ChangeField.UPDATED,
         ChangeField.SORTKEY,
-        ChangeField.FILE,
+        ChangeField.PATH,
         ChangeField.OWNER,
         ChangeField.REVIEWER,
         ChangeField.COMMIT,
@@ -105,7 +105,7 @@
         ChangeField.TOPIC,
         ChangeField.UPDATED,
         ChangeField.SORTKEY,
-        ChangeField.FILE,
+        ChangeField.PATH,
         ChangeField.OWNER,
         ChangeField.REVIEWER,
         ChangeField.COMMIT,
@@ -121,6 +121,30 @@
   // For upgrade to Lucene 4.6.0 index format only.
   static final Schema<ChangeData> V6 = release(V5.getFields().values());
 
+  static final Schema<ChangeData> V7 = release(
+        ChangeField.LEGACY_ID,
+        ChangeField.ID,
+        ChangeField.STATUS,
+        ChangeField.PROJECT,
+        ChangeField.REF,
+        ChangeField.TOPIC,
+        ChangeField.UPDATED,
+        ChangeField.SORTKEY,
+        ChangeField.FILE_PART,
+        ChangeField.PATH,
+        ChangeField.OWNER,
+        ChangeField.REVIEWER,
+        ChangeField.COMMIT,
+        ChangeField.TR,
+        ChangeField.LABEL,
+        ChangeField.REVIEWED,
+        ChangeField.COMMIT_MESSAGE,
+        ChangeField.COMMENT,
+        ChangeField.CHANGE,
+        ChangeField.APPROVAL,
+        ChangeField.MERGEABLE);
+
+
   private static Schema<ChangeData> release(Collection<FieldDef<ChangeData, ?>> fields) {
     return new Schema<ChangeData>(true, fields);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/RegexPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/RegexPredicate.java
index 198c7b0..b73674d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/RegexPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/RegexPredicate.java
@@ -18,4 +18,8 @@
   protected RegexPredicate(FieldDef<I, ?> def, String value) {
     super(def, value);
   }
+
+  protected RegexPredicate(FieldDef<I, ?> def, String name, String value) {
+    super(def, name, value);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
index aa120e1..32c4ef8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
@@ -45,19 +45,19 @@
 @RequiresCapability(GlobalCapability.CREATE_PROJECT)
 public class CreateProject implements RestModifyView<TopLevelResource, Input> {
   public static class Input {
-    String name;
-    String parent;
-    String description;
-    boolean permissionsOnly;
-    boolean createEmptyCommit;
-    SubmitType submitType;
-    List<String> branches;
-    List<String> owners;
-    InheritableBoolean useContributorAgreements;
-    InheritableBoolean useSignedOffBy;
-    InheritableBoolean useContentMerge;
-    InheritableBoolean requireChangeId;
-    String maxObjectSizeLimit;
+    public String name;
+    public String parent;
+    public String description;
+    public boolean permissionsOnly;
+    public boolean createEmptyCommit;
+    public SubmitType submitType;
+    public List<String> branches;
+    public List<String> owners;
+    public InheritableBoolean useContributorAgreements;
+    public InheritableBoolean useSignedOffBy;
+    public InheritableBoolean useContentMerge;
+    public InheritableBoolean requireChangeId;
+    public String maxObjectSizeLimit;
   }
 
   public static interface Factory {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index 2e29c80..8f8cb70 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -16,7 +16,6 @@
 
 import static com.google.gerrit.server.ApprovalsUtil.sortApprovals;
 
-import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Objects;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ImmutableList;
@@ -157,7 +156,6 @@
   private ChangedLines changedLines;
   private boolean patchesLoaded;
 
-  @VisibleForTesting
   @AssistedInject
   public ChangeData(
       GitRepositoryManager repoManager,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 99dd2e7..c0b8a63 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -99,6 +99,7 @@
   public static final String FIELD_OWNER = "owner";
   public static final String FIELD_OWNERIN = "ownerin";
   public static final String FIELD_PARENTPROJECT = "parentproject";
+  public static final String FIELD_PATH = "path";
   public static final String FIELD_PROJECT = "project";
   public static final String FIELD_REF = "ref";
   public static final String FIELD_REVIEWER = "reviewer";
@@ -384,9 +385,18 @@
   @Operator
   public Predicate<ChangeData> file(String file) throws QueryParseException {
     if (file.startsWith("^")) {
-      return new RegexFilePredicate(file);
+      return new RegexPathPredicate(FIELD_FILE, file);
     } else {
-      return new EqualsFilePredicate(file);
+      return EqualsFilePredicate.create(args, file);
+    }
+  }
+
+  @Operator
+  public Predicate<ChangeData> path(String path) throws QueryParseException {
+    if (path.startsWith("^")) {
+      return new RegexPathPredicate(FIELD_PATH, path);
+    } else {
+      return new EqualsPathPredicate(FIELD_PATH, path);
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
index 2102541..3c8b2c0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
@@ -70,7 +70,8 @@
       List<Predicate<ChangeData>> filePredicates =
           Lists.newArrayListWithCapacity(files.size());
       for (String file : files) {
-        filePredicates.add(new EqualsFilePredicate(file));
+        filePredicates.add(
+            new EqualsPathPredicate(ChangeQueryBuilder.FIELD_PATH, file));
       }
 
       List<Predicate<ChangeData>> predicatesForOneChange =
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java
index 13bd08b..e5fc51d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java
@@ -16,31 +16,31 @@
 
 import com.google.gerrit.server.index.ChangeField;
 import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments;
 import com.google.gwtorm.server.OrmException;
 
-import java.util.Collections;
-import java.util.List;
-
 class EqualsFilePredicate extends IndexPredicate<ChangeData> {
+  static Predicate<ChangeData> create(Arguments args, String value) {
+    Predicate<ChangeData> eqPath =
+        new EqualsPathPredicate(ChangeQueryBuilder.FIELD_FILE, value);
+    if (!args.indexes.getSearchIndex().getSchema().getFields().containsKey(
+        ChangeField.FILE_PART.getName())) {
+      return eqPath;
+    }
+    return Predicate.or(eqPath, new EqualsFilePredicate(value));
+  }
+
   private final String value;
 
-  EqualsFilePredicate(String value) {
-    super(ChangeField.FILE, value);
+  private EqualsFilePredicate(String value) {
+    super(ChangeField.FILE_PART, ChangeQueryBuilder.FIELD_FILE, value);
     this.value = value;
   }
 
   @Override
   public boolean match(ChangeData object) throws OrmException {
-    List<String> files = object.currentFilePaths();
-    if (files != null) {
-      return Collections.binarySearch(files, value) >= 0;
-    } else {
-      // The ChangeData can't do expensive lookups right now. Bypass
-      // them and include the result anyway. We might be able to do
-      // a narrow later on to a smaller set.
-      //
-      return true;
-    }
+    return ChangeField.getFileParts(object).contains(value);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsPathPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsPathPredicate.java
new file mode 100644
index 0000000..055b6d5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsPathPredicate.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2013 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.query.change;
+
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gwtorm.server.OrmException;
+
+import java.util.Collections;
+import java.util.List;
+
+class EqualsPathPredicate extends IndexPredicate<ChangeData> {
+  private final String value;
+
+  EqualsPathPredicate(String fieldName, String value) {
+    super(ChangeField.PATH, fieldName, value);
+    this.value = value;
+  }
+
+  @Override
+  public boolean match(ChangeData object) throws OrmException {
+    List<String> files = object.currentFilePaths();
+    return files != null && Collections.binarySearch(files, value) >= 0;
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexFilePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexPathPredicate.java
similarity index 94%
rename from gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexFilePredicate.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexPathPredicate.java
index f39dc14..d073002 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexFilePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexPathPredicate.java
@@ -25,7 +25,7 @@
 import java.util.Collections;
 import java.util.List;
 
-class RegexFilePredicate extends RegexPredicate<ChangeData> {
+class RegexPathPredicate extends RegexPredicate<ChangeData> {
   private final RunAutomaton pattern;
 
   private final String prefixBegin;
@@ -33,8 +33,8 @@
   private final int prefixLen;
   private final boolean prefixOnly;
 
-  RegexFilePredicate(String re) {
-    super(ChangeField.FILE, re);
+  RegexPathPredicate(String fieldName, String re) {
+    super(ChangeField.PATH, re);
 
     if (re.startsWith("^")) {
       re = re.substring(1);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeIndex.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeIndex.java
index d8a0f65..339a1bb 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeIndex.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeIndex.java
@@ -30,7 +30,7 @@
   static Schema<ChangeData> V2 = new Schema<ChangeData>(2, false,
     ImmutableList.of(
       ChangeField.STATUS,
-      ChangeField.FILE,
+      ChangeField.PATH,
       ChangeField.SORTKEY));
 
   private static class Source implements ChangeDataSource {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index ec485bb..0e772f2 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -349,12 +349,27 @@
     RevCommit commit2 = repo.parseBody(repo.commit().message("two").create());
     Change change2 = newChange(repo, commit2, null, null, null).insert();
 
-    assertTrue(query("topic:foo").isEmpty());
+    assertTrue(query("message:foo").isEmpty());
     assertResultEquals(change1, queryOne("message:one"));
     assertResultEquals(change2, queryOne("message:two"));
   }
 
   @Test
+  public void fullTextWithNumbers() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    RevCommit commit1 =
+        repo.parseBody(repo.commit().message("12345 67890").create());
+    Change change1 = newChange(repo, commit1, null, null, null).insert();
+    RevCommit commit2 =
+        repo.parseBody(repo.commit().message("12346 67891").create());
+    Change change2 = newChange(repo, commit2, null, null, null).insert();
+
+    assertTrue(query("message:1234").isEmpty());
+    assertResultEquals(change1, queryOne("message:12345"));
+    assertResultEquals(change2, queryOne("message:12346"));
+  }
+
+  @Test
   public void byLabel() throws Exception {
     accountManager.authenticate(AuthRequest.forUser("anotheruser"));
     TestRepository<InMemoryRepository> repo = createProject("repo");
@@ -574,13 +589,16 @@
     TestRepository<InMemoryRepository> repo = createProject("repo");
     RevCommit commit = repo.parseBody(
         repo.commit().message("one")
-        .add("file1", "contents1").add("file2", "contents2")
+        .add("dir/file1", "contents1").add("dir/file2", "contents2")
         .create());
     Change change = newChange(repo, commit, null, null, null).insert();
 
     assertTrue(query("file:file").isEmpty());
+    assertResultEquals(change, queryOne("file:dir"));
     assertResultEquals(change, queryOne("file:file1"));
     assertResultEquals(change, queryOne("file:file2"));
+    assertResultEquals(change, queryOne("file:dir/file1"));
+    assertResultEquals(change, queryOne("file:dir/file2"));
   }
 
   @Test
@@ -588,12 +606,43 @@
     TestRepository<InMemoryRepository> repo = createProject("repo");
     RevCommit commit = repo.parseBody(
         repo.commit().message("one")
-        .add("file1", "contents1").add("file2", "contents2")
+        .add("dir/file1", "contents1").add("dir/file2", "contents2")
         .create());
     Change change = newChange(repo, commit, null, null, null).insert();
 
-    assertTrue(query("file:file.*").isEmpty());
-    assertResultEquals(change, queryOne("file:^file.*"));
+    assertTrue(query("file:.*file.*").isEmpty());
+    assertTrue(query("file:^file.*").isEmpty()); // Whole path only.
+    assertResultEquals(change, queryOne("file:^dir.file.*"));
+  }
+
+  @Test
+  public void byPathExact() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    RevCommit commit = repo.parseBody(
+        repo.commit().message("one")
+        .add("dir/file1", "contents1").add("dir/file2", "contents2")
+        .create());
+    Change change = newChange(repo, commit, null, null, null).insert();
+
+    assertTrue(query("path:file").isEmpty());
+    assertTrue(query("path:dir").isEmpty());
+    assertTrue(query("path:file1").isEmpty());
+    assertTrue(query("path:file2").isEmpty());
+    assertResultEquals(change, queryOne("path:dir/file1"));
+    assertResultEquals(change, queryOne("path:dir/file2"));
+  }
+
+  @Test
+  public void byPathRegex() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    RevCommit commit = repo.parseBody(
+        repo.commit().message("one")
+        .add("dir/file1", "contents1").add("dir/file2", "contents2")
+        .create());
+    Change change = newChange(repo, commit, null, null, null).insert();
+
+    assertTrue(query("path:.*file.*").isEmpty());
+    assertResultEquals(change, queryOne("path:^dir.file.*"));
   }
 
   @Test
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/RegexFilePredicateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/RegexPathPredicateTest.java
similarity index 85%
rename from gerrit-server/src/test/java/com/google/gerrit/server/query/change/RegexFilePredicateTest.java
rename to gerrit-server/src/test/java/com/google/gerrit/server/query/change/RegexPathPredicateTest.java
index 363fa26..4e5dcdd 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/RegexFilePredicateTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/RegexPathPredicateTest.java
@@ -23,10 +23,10 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
-public class RegexFilePredicateTest {
+public class RegexPathPredicateTest {
   @Test
   public void testPrefixOnlyOptimization() throws OrmException {
-    RegexFilePredicate p = predicate("^a/b/.*");
+    RegexPathPredicate p = predicate("^a/b/.*");
     assertTrue(p.match(change("a/b/source.c")));
     assertFalse(p.match(change("source.c")));
 
@@ -36,7 +36,7 @@
 
   @Test
   public void testPrefixReducesSearchSpace() throws OrmException {
-    RegexFilePredicate p = predicate("^a/b/.*\\.[ch]");
+    RegexPathPredicate p = predicate("^a/b/.*\\.[ch]");
     assertTrue(p.match(change("a/b/source.c")));
     assertFalse(p.match(change("a/b/source.res")));
     assertFalse(p.match(change("source.res")));
@@ -46,7 +46,7 @@
 
   @Test
   public void testFileExtension_Constant() throws OrmException {
-    RegexFilePredicate p = predicate("^.*\\.res");
+    RegexPathPredicate p = predicate("^.*\\.res");
     assertTrue(p.match(change("test.res")));
     assertTrue(p.match(change("foo/bar/test.res")));
     assertFalse(p.match(change("test.res.bar")));
@@ -54,7 +54,7 @@
 
   @Test
   public void testFileExtension_CharacterGroup() throws OrmException {
-    RegexFilePredicate p = predicate("^.*\\.[ch]");
+    RegexPathPredicate p = predicate("^.*\\.[ch]");
     assertTrue(p.match(change("test.c")));
     assertTrue(p.match(change("test.h")));
     assertFalse(p.match(change("test.C")));
@@ -71,14 +71,14 @@
 
   @Test
   public void testExactMatch() throws OrmException {
-    RegexFilePredicate p = predicate("^foo.c");
+    RegexPathPredicate p = predicate("^foo.c");
     assertTrue(p.match(change("foo.c")));
     assertFalse(p.match(change("foo.cc")));
     assertFalse(p.match(change("bar.c")));
   }
 
-  private static RegexFilePredicate predicate(String pattern) {
-    return new RegexFilePredicate(pattern);
+  private static RegexPathPredicate predicate(String pattern) {
+    return new RegexPathPredicate(ChangeQueryBuilder.FIELD_PATH, pattern);
   }
 
   private static ChangeData change(String... files) {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
index 40c79b0..df2de80 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
@@ -191,26 +191,12 @@
       Constructor<?> c =
           clazz.getConstructor(Integer.class, int.class, String.class);
       return (Module) c.newInstance(version, 0, null);
-    } catch (ClassNotFoundException e) {
-      throw newProvisionException(e);
-    } catch (SecurityException e) {
-      throw newProvisionException(e);
-    } catch (NoSuchMethodException e) {
-      throw newProvisionException(e);
-    } catch (IllegalArgumentException e) {
-      throw newProvisionException(e);
-    } catch (InstantiationException e) {
-      throw newProvisionException(e);
-    } catch (IllegalAccessException e) {
-      throw newProvisionException(e);
-    } catch (InvocationTargetException e) {
-      throw newProvisionException(e);
+    } catch (ClassNotFoundException | SecurityException | NoSuchMethodException
+        | IllegalArgumentException | InstantiationException
+        | IllegalAccessException | InvocationTargetException e) {
+      ProvisionException pe = new ProvisionException(e.getMessage());
+      pe.initCause(e);
+      throw pe;
     }
   }
-
-  private static ProvisionException newProvisionException(Throwable cause) {
-    ProvisionException pe = new ProvisionException(cause.getMessage());
-    pe.initCause(cause);
-    return pe;
-  }
 }
diff --git a/gerrit-solr/BUCK b/gerrit-solr/BUCK
index c072830..ec3c728 100644
--- a/gerrit-solr/BUCK
+++ b/gerrit-solr/BUCK
@@ -12,6 +12,7 @@
     '//lib/guice:guice',
     '//lib/jgit:jgit',
     '//lib/log:api',
+    '//lib/lucene:analyzers-common',
     '//lib/lucene:core',
     '//lib/solr:solrj',
   ],
diff --git a/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrChangeIndex.java b/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrChangeIndex.java
index 73dc07e..7101a4d 100644
--- a/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrChangeIndex.java
+++ b/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrChangeIndex.java
@@ -45,7 +45,10 @@
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Provider;
 
+import org.apache.lucene.analysis.standard.StandardAnalyzer;
+import org.apache.lucene.analysis.util.CharArraySet;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.util.Version;
 import org.apache.solr.client.solrj.SolrQuery;
 import org.apache.solr.client.solrj.SolrServer;
 import org.apache.solr.client.solrj.SolrServerException;
@@ -79,6 +82,7 @@
   private final CloudSolrServer openIndex;
   private final CloudSolrServer closedIndex;
   private final Schema<ChangeData> schema;
+  private final QueryBuilder queryBuilder;
 
   SolrChangeIndex(
       @GerritServerConfig Config cfg,
@@ -101,6 +105,14 @@
       throw new IllegalStateException("index.solr.url must be supplied");
     }
 
+    // Version is only used to determine the list of stop words used by the
+    // analyzer, so use the latest version rather than trying to match the Solr
+    // server version.
+    @SuppressWarnings("deprecation")
+    Version v = Version.LUCENE_CURRENT;
+    queryBuilder = new QueryBuilder(
+        schema, new StandardAnalyzer(v, CharArraySet.EMPTY_SET));
+
     base = Strings.nullToEmpty(base);
     openIndex = new CloudSolrServer(url);
     openIndex.setDefaultCollection(base + CHANGES_OPEN);
@@ -208,7 +220,7 @@
     if (!Sets.intersection(statuses, CLOSED_STATUSES).isEmpty()) {
       indexes.add(closedIndex);
     }
-    return new QuerySource(indexes, QueryBuilder.toQuery(schema, p), limit,
+    return new QuerySource(indexes, queryBuilder.toQuery(p), limit,
         ChangeQueryBuilder.hasNonTrivialSortKeyAfter(schema, p));
   }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
index 90c5e94..90c1f2c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
@@ -14,24 +14,31 @@
 
 package com.google.gerrit.sshd.commands;
 
+import com.google.common.base.Function;
+import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.errors.ProjectCreationFailedException;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
 import com.google.gerrit.reviewdb.client.Project.SubmitType;
-import com.google.gerrit.server.project.PerformCreateProject;
-import com.google.gerrit.server.project.CreateProjectArgs;
+import com.google.gerrit.server.project.CreateProject;
+import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.SuggestParentCandidates;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 
+import java.io.IOException;
 import java.util.List;
 
 /** Create a new project. **/
@@ -120,35 +127,46 @@
   }
 
   @Inject
-  private PerformCreateProject.Factory factory;
+  private Provider<CreateProject.Factory> createProjectFactory;
 
   @Inject
   private SuggestParentCandidates.Factory suggestParentCandidatesFactory;
 
   @Override
-  protected void run() throws Exception {
+  protected void run() throws UnloggedFailure {
     try {
       if (!suggestParent) {
         if (projectName == null) {
           throw new UnloggedFailure(1, "fatal: Project name is required.");
         }
-        final CreateProjectArgs args = new CreateProjectArgs();
-        args.setProjectName(projectName);
-        args.ownerIds = ownerIds;
-        args.newParent = newParent;
-        args.permissionsOnly = permissionsOnly;
-        args.projectDescription = projectDescription;
-        args.submitType = submitType;
-        args.contributorAgreements = contributorAgreements;
-        args.signedOffBy = signedOffBy;
-        args.contentMerge = contentMerge;
-        args.changeIdRequired = requireChangeID;
-        args.branch = branch;
-        args.createEmptyCommit = createEmptyCommit;
-        args.maxObjectSizeLimit = maxObjectSizeLimit;
 
-        final PerformCreateProject createProject = factory.create(args);
-        createProject.createProject();
+        CreateProject.Input input = new CreateProject.Input();
+        input.name = projectName;
+        if (ownerIds != null) {
+          input.owners = Lists.transform(ownerIds,
+            new Function<AccountGroup.UUID, String>() {
+              @Override
+              public String apply(AccountGroup.UUID uuid) {
+                return uuid.get();
+              }
+            });
+        }
+        if (newParent != null) {
+          input.parent = newParent.getProject().getName();
+        }
+        input.permissionsOnly = permissionsOnly;
+        input.description = projectDescription;
+        input.submitType = submitType;
+        input.useContributorAgreements = contributorAgreements;
+        input.useSignedOffBy = signedOffBy;
+        input.useContentMerge = contentMerge;
+        input.requireChangeId = requireChangeID;
+        input.branches = branch;
+        input.createEmptyCommit = createEmptyCommit;
+        input.maxObjectSizeLimit = maxObjectSizeLimit;
+
+        createProjectFactory.get().create(projectName)
+            .apply(TopLevelResource.INSTANCE, input);
       } else {
         List<Project.NameKey> parentCandidates =
             suggestParentCandidatesFactory.create().getNameKeys();
@@ -157,7 +175,8 @@
           stdout.print(parent + "\n");
         }
       }
-    } catch (ProjectCreationFailedException err) {
+    } catch (RestApiException | ProjectCreationFailedException | IOException
+        | NoSuchProjectException | OrmException err) {
       throw new UnloggedFailure(1, "fatal: " + err.getMessage(), err);
     }
   }
diff --git a/lib/asciidoctor/java/DocIndexer.java b/lib/asciidoctor/java/DocIndexer.java
index c8a3a55..6cb9d62 100644
--- a/lib/asciidoctor/java/DocIndexer.java
+++ b/lib/asciidoctor/java/DocIndexer.java
@@ -39,10 +39,13 @@
 import java.io.InputStreamReader;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import java.util.zip.ZipOutputStream;
 
 public class DocIndexer {
   private static final Version LUCENE_VERSION = Version.LUCENE_46;
+  private static final Pattern SECTION_HEADER = Pattern.compile("^=+ (.*)");
 
   @Option(name = "-z", usage = "output zip file")
   private String zipFile;
@@ -94,6 +97,10 @@
         title = titleReader.readLine();
       }
       titleReader.close();
+      Matcher matcher = SECTION_HEADER.matcher(title);
+      if (matcher.matches()) {
+        title = matcher.group(1);
+      }
 
       String outputFile = AsciiDoctor.mapInFileToOutFile(
           inputFile, inExt, outExt);