Merge "Fix index out of bounds exception when nothing able to merge"
diff --git a/Documentation/user-review-ui.txt b/Documentation/user-review-ui.txt
index 8553634..b7311d6 100644
--- a/Documentation/user-review-ui.txt
+++ b/Documentation/user-review-ui.txt
@@ -187,6 +187,19 @@
 the link:access-control.html#category_submit[Submit] access right is
 assigned.
 
+** [[revert]]`Revert`:
++
+Reverts the change via creating a new one.
++
+The `Revert` button is available if the change has been submitted.
++
+When the `Revert` button is pressed, a panel will appear to allow
+the user to enter a commit message for the reverting change.
++
+Once a revert change is created, the original author and any reviewers
+of the original change are added as reviewers and a message is posted
+to the original change linking to the revert.
+
 ** [[abandon]]`Abandon`:
 +
 Abandons the change.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
index 5f48daa..86e068c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
@@ -24,12 +24,14 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
@@ -66,6 +68,8 @@
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.text.MessageFormat;
+import java.util.HashSet;
+import java.util.Set;
 
 @Singleton
 public class Revert implements RestModifyView<ChangeResource, RevertInput>,
@@ -82,6 +86,7 @@
   private final RevertedSender.Factory revertedSenderFactory;
   private final ChangeJson.Factory json;
   private final PersonIdent serverIdent;
+  private final ApprovalsUtil approvalsUtil;
 
   @Inject
   Revert(Provider<ReviewDb> db,
@@ -93,7 +98,8 @@
       PatchSetUtil psUtil,
       RevertedSender.Factory revertedSenderFactory,
       ChangeJson.Factory json,
-      @GerritPersonIdent PersonIdent serverIdent) {
+      @GerritPersonIdent PersonIdent serverIdent,
+      ApprovalsUtil approvalsUtil) {
     this.db = db;
     this.repoManager = repoManager;
     this.changeInserterFactory = changeInserterFactory;
@@ -104,6 +110,7 @@
     this.revertedSenderFactory = revertedSenderFactory;
     this.json = json;
     this.serverIdent = serverIdent;
+    this.approvalsUtil = approvalsUtil;
   }
 
   @Override
@@ -182,6 +189,13 @@
             .setTopic(changeToRevert.getTopic());
         ins.setMessage("Uploaded patch set 1.");
 
+        Set<Account.Id> reviewers = new HashSet<>();
+        reviewers.add(changeToRevert.getOwner());
+        reviewers.addAll(
+            approvalsUtil.getReviewers(db.get(), ctl.getNotes()).all());
+        reviewers.remove(user.getAccountId());
+        ins.setReviewers(reviewers);
+
         try (BatchUpdate bu = updateFactory.create(
             db.get(), project, user, now)) {
           bu.setRepository(git, revWalk, oi);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
index b55b416..0e62600 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -43,10 +44,12 @@
   protected final NotesMigration migration;
   protected final ChangeNoteUtil noteUtil;
   protected final String anonymousCowardName;
-  protected final ChangeNotes notes;
   protected final Account.Id accountId;
   protected final PersonIdent authorIdent;
   protected final Date when;
+
+  @Nullable private final ChangeNotes notes;
+  private final Change change;
   private final PersonIdent serverIdent;
 
   protected PatchSet.Id psId;
@@ -59,15 +62,16 @@
       String anonymousCowardName,
       ChangeNoteUtil noteUtil,
       Date when) {
-    this(
-        migration,
-        noteUtil,
-        serverIdent,
-        anonymousCowardName,
-        ctl.getNotes(),
-        accountId(ctl.getUser()),
-        ident(noteUtil, serverIdent, anonymousCowardName, ctl.getUser(), when),
-        when);
+    this.migration = migration;
+    this.noteUtil = noteUtil;
+    this.serverIdent = new PersonIdent(serverIdent, when);
+    this.anonymousCowardName = anonymousCowardName;
+    this.notes = ctl.getNotes();
+    this.change = notes.getChange();
+    this.accountId = accountId(ctl.getUser());
+    this.authorIdent =
+        ident(noteUtil, serverIdent, anonymousCowardName, ctl.getUser(), when);
+    this.when = when;
   }
 
   protected AbstractChangeUpdate(
@@ -75,15 +79,21 @@
       ChangeNoteUtil noteUtil,
       PersonIdent serverIdent,
       String anonymousCowardName,
-      ChangeNotes notes,
+      @Nullable ChangeNotes notes,
+      @Nullable Change change,
       Account.Id accountId,
       PersonIdent authorIdent,
       Date when) {
+    checkArgument(
+        (notes != null && change == null)
+            || (notes == null && change != null),
+        "exactly one of notes or change required");
     this.migration = migration;
     this.noteUtil = noteUtil;
     this.serverIdent = new PersonIdent(serverIdent, when);
     this.anonymousCowardName = anonymousCowardName;
     this.notes = notes;
+    this.change = change != null ? change : notes.getChange();
     this.accountId = accountId;
     this.authorIdent = authorIdent;
     this.when = when;
@@ -114,15 +124,16 @@
   }
 
   public Change.Id getId() {
-    return notes.getChangeId();
+    return change.getId();
   }
 
+  @Nullable
   public ChangeNotes getNotes() {
     return notes;
   }
 
   public Change getChange() {
-    return notes.getChange();
+    return change;
   }
 
   public Date getWhen() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
index 84e647e..7b59a47 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
@@ -21,6 +21,7 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -62,6 +63,8 @@
   public interface Factory {
     ChangeDraftUpdate create(ChangeNotes notes, Account.Id accountId,
         PersonIdent authorIdent, Date when);
+    ChangeDraftUpdate create(Change change, Account.Id accountId,
+        PersonIdent authorIdent, Date when);
   }
 
   @AutoValue
@@ -76,8 +79,8 @@
 
   private final AllUsersName draftsProject;
 
-  private List<PatchLineComment> put;
-  private Set<Key> delete;
+  private List<PatchLineComment> put = new ArrayList<>();
+  private Set<Key> delete = new HashSet<>();
 
   @AssistedInject
   private ChangeDraftUpdate(
@@ -90,11 +93,25 @@
       @Assisted Account.Id accountId,
       @Assisted PersonIdent authorIdent,
       @Assisted Date when) {
-    super(migration, noteUtil, serverIdent, anonymousCowardName, notes,
+    super(migration, noteUtil, serverIdent, anonymousCowardName, notes, null,
         accountId, authorIdent, when);
     this.draftsProject = allUsers;
-    this.put = new ArrayList<>();
-    this.delete = new HashSet<>();
+  }
+
+  @AssistedInject
+  private ChangeDraftUpdate(
+      @GerritPersonIdent PersonIdent serverIdent,
+      @AnonymousCowardName String anonymousCowardName,
+      NotesMigration migration,
+      AllUsersName allUsers,
+      ChangeNoteUtil noteUtil,
+      @Assisted Change change,
+      @Assisted Account.Id accountId,
+      @Assisted PersonIdent authorIdent,
+      @Assisted Date when) {
+    super(migration, noteUtil, serverIdent, anonymousCowardName, null, change,
+        accountId, authorIdent, when);
+    this.draftsProject = allUsers;
   }
 
   public void putComment(PatchLineComment c) {
@@ -179,14 +196,17 @@
       // If reading from changes is enabled, then the old DraftCommentNotes
       // already parsed the revision notes. We can reuse them as long as the ref
       // hasn't advanced.
-      DraftCommentNotes draftNotes =
-          getNotes().load().getDraftCommentNotes();
-      if (draftNotes != null) {
-        ObjectId idFromNotes =
-            firstNonNull(draftNotes.getRevision(), ObjectId.zeroId());
-        RevisionNoteMap rnm = draftNotes.getRevisionNoteMap();
-        if (idFromNotes.equals(curr) && rnm != null) {
-          return rnm;
+      ChangeNotes changeNotes = getNotes();
+      if (changeNotes != null) {
+        DraftCommentNotes draftNotes =
+            changeNotes.load().getDraftCommentNotes();
+        if (draftNotes != null) {
+          ObjectId idFromNotes =
+              firstNonNull(draftNotes.getRevision(), ObjectId.zeroId());
+          RevisionNoteMap rnm = draftNotes.getRevisionNoteMap();
+          if (idFromNotes.equals(curr) && rnm != null) {
+            return rnm;
+          }
         }
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilderImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilderImpl.java
index 5f16eb4..5105fc0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilderImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilderImpl.java
@@ -116,7 +116,6 @@
 
   private final AccountCache accountCache;
   private final ChangeDraftUpdate.Factory draftUpdateFactory;
-  private final ChangeNotes.Factory notesFactory;
   private final ChangeNoteUtil changeNoteUtil;
   private final ChangeUpdate.Factory updateFactory;
   private final NoteDbUpdateManager.Factory updateManagerFactory;
@@ -129,7 +128,6 @@
   ChangeRebuilderImpl(SchemaFactory<ReviewDb> schemaFactory,
       AccountCache accountCache,
       ChangeDraftUpdate.Factory draftUpdateFactory,
-      ChangeNotes.Factory notesFactory,
       ChangeNoteUtil changeNoteUtil,
       ChangeUpdate.Factory updateFactory,
       NoteDbUpdateManager.Factory updateManagerFactory,
@@ -140,7 +138,6 @@
     super(schemaFactory);
     this.accountCache = accountCache;
     this.draftUpdateFactory = draftUpdateFactory;
-    this.notesFactory = notesFactory;
     this.changeNoteUtil = changeNoteUtil;
     this.updateFactory = updateFactory;
     this.updateManagerFactory = updateManagerFactory;
@@ -388,8 +385,7 @@
       labelNameComparator = Ordering.natural();
     }
     ChangeUpdate update = updateFactory.create(
-        notesFactory.createWithAutoRebuildingDisabled(
-            change, manager.getChangeRepo().cmds),
+        change,
         events.getAccountId(),
         events.newAuthorIdent(),
         events.getWhen(),
@@ -406,13 +402,12 @@
 
   private void flushEventsToDraftUpdate(NoteDbUpdateManager manager,
       EventList<PatchLineCommentEvent> events, Change change)
-      throws OrmException, IOException {
+      throws OrmException {
     if (events.isEmpty()) {
       return;
     }
     ChangeDraftUpdate update = draftUpdateFactory.create(
-        notesFactory.createWithAutoRebuildingDisabled(
-            change, manager.getChangeRepo().cmds),
+        change,
         events.getAccountId(),
         events.newAuthorIdent(),
         events.getWhen());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 3c7f909..2af7bbf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -95,7 +95,7 @@
   public interface Factory {
     ChangeUpdate create(ChangeControl ctl);
     ChangeUpdate create(ChangeControl ctl, Date when);
-    ChangeUpdate create(ChangeNotes notes, @Nullable Account.Id accountId,
+    ChangeUpdate create(Change change, @Nullable Account.Id accountId,
         PersonIdent authorIdent, Date when,
         Comparator<String> labelNameComparator);
 
@@ -204,12 +204,12 @@
       NoteDbUpdateManager.Factory updateManagerFactory,
       ChangeDraftUpdate.Factory draftUpdateFactory,
       ChangeNoteUtil noteUtil,
-      @Assisted ChangeNotes notes,
+      @Assisted Change change,
       @Assisted @Nullable Account.Id accountId,
       @Assisted PersonIdent authorIdent,
       @Assisted Date when,
       @Assisted Comparator<String> labelNameComparator) {
-    super(migration, noteUtil, serverIdent, anonymousCowardName, notes,
+    super(migration, noteUtil, serverIdent, anonymousCowardName, null, change,
         accountId, authorIdent, when);
     this.accountCache = accountCache;
     this.draftUpdateFactory = draftUpdateFactory;
@@ -328,8 +328,14 @@
   @VisibleForTesting
   ChangeDraftUpdate createDraftUpdateIfNull() {
     if (draftUpdate == null) {
-      draftUpdate =
-          draftUpdateFactory.create(getNotes(), accountId, authorIdent, when);
+      ChangeNotes notes = getNotes();
+      if (notes != null) {
+        draftUpdate =
+            draftUpdateFactory.create(notes, accountId, authorIdent, when);
+      } else {
+        draftUpdate = draftUpdateFactory.create(
+            getChange(), accountId, authorIdent, when);
+      }
     }
     return draftUpdate;
   }
@@ -428,10 +434,13 @@
       // If reading from changes is enabled, then the old ChangeNotes already
       // parsed the revision notes. We can reuse them as long as the ref hasn't
       // advanced.
-      ObjectId idFromNotes =
-          firstNonNull(getNotes().load().getRevision(), ObjectId.zeroId());
-      if (idFromNotes.equals(curr)) {
-        return checkNotNull(getNotes().revisionNoteMap);
+      ChangeNotes notes = getNotes();
+      if (notes != null) {
+        ObjectId idFromNotes =
+            firstNonNull(notes.load().getRevision(), ObjectId.zeroId());
+        if (idFromNotes.equals(curr)) {
+          return checkNotNull(getNotes().revisionNoteMap);
+        }
       }
     }
     NoteMap noteMap = NoteMap.read(rw.getObjectReader(), rw.parseCommit(curr));
diff --git a/plugins/replication b/plugins/replication
index 945c842..43af15b 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 945c842f9c884469ec0fb2d883d6cda552e97747
+Subproject commit 43af15baab9b2312677000c47313026f34352abb
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index dd63764..544ad4c 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -62,10 +62,7 @@
       },
       changeNum: String,
       patchNum: String,
-      commitInfo: {
-        type: Object,
-        readOnly: true,
-      },
+      commitInfo: Object,
       _loading: {
         type: Boolean,
         value: true,
@@ -237,11 +234,6 @@
 
     _handleRevertDialogConfirm: function() {
       var el = this.$.confirmRevertDialog;
-      if (!el.message) {
-        // TODO(viktard): Fix validation.
-        alert('The revert commit message can’t be empty.');
-        return;
-      }
       this.$.overlay.close();
       el.hidden = false;
       this._fireAction(
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index 098e097..b0f5a5c 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -221,12 +221,6 @@
         fireActionStub.restore();
       });
 
-      test('validation', function() {
-        element._handleRevertDialogConfirm();
-        assert.notOk(fireActionStub.called);
-        assert.ok(alertStub.called);
-      });
-
       test('works', function() {
         var revertButton = element.$$('gr-button[data-action-key="revert"]');
         MockInteractions.tap(revertButton);
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
index d416e14..5f76a79 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
@@ -104,6 +104,18 @@
       <span class="value">[[change.branch]]</span>
     </section>
     <section>
+      <span class="title">Commit</span>
+      <span class="value">
+        <template is="dom-if" if="[[_computeShowWebLink(commitInfo)]]">
+          <a target="_blank"
+              href$="[[_computeWebLink(commitInfo)]]">[[_computeShortHash(change)]]</a>
+        </template>
+        <template is="dom-if" if="[[!_computeShowWebLink(commitInfo)]]">
+          [[_computeShortHash(change)]]
+        </template>
+      </span>
+    </section>
+    <section>
       <span class="title">Topic</span>
       <span class="value">[[change.topic]]</span>
     </section>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index 4fa98ca..8e2e000 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -27,6 +27,7 @@
 
     properties: {
       change: Object,
+      commitInfo: Object,
       mutable: Boolean,
       serverConfig: Object,
     },
@@ -35,6 +36,18 @@
       Gerrit.RESTClientBehavior,
     ],
 
+    _computeShowWebLink: function(commitInfo) {
+      return commitInfo.web_links && commitInfo.web_links.length;
+    },
+
+    _computeWebLink: function(commitInfo) {
+      return '../../' + commitInfo.web_links[0].url;
+    },
+
+    _computeShortHash: function(change) {
+      return change.current_revision.slice(0, 6);
+    },
+
     _computeHideStrategy: function(change) {
       return !this.changeIsOpen(change.status);
     },
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index 3175517..78d4123 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -248,6 +248,7 @@
         <div class="changeInfo-column changeMetadata">
           <gr-change-metadata
               change="[[_change]]"
+              commit-info="[[_commitInfo]]"
               server-config="[[serverConfig]]"
               mutable="[[_loggedIn]]"></gr-change-metadata>
           <gr-change-actions id="actions"
@@ -272,6 +273,7 @@
         </div>
       </section>
       <gr-file-list id="fileList"
+          change="[[_change]]"
           change-num="[[_changeNum]]"
           patch-range="[[_patchRange]]"
           comments="[[_comments]]"
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html
index a3ab470..979a06a 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html
@@ -23,7 +23,6 @@
     <style>
       :host {
         display: block;
-        width: 30em;
       }
       :host([disabled]) {
         opacity: .5;
@@ -31,19 +30,18 @@
       }
       label {
         cursor: pointer;
+        display: block;
+        width: 100%;
       }
       iron-autogrow-textarea {
+        font-family: var(--monospace-font-family);
         padding: 0;
-      }
-      .main label,
-      .main input[type="text"] {
-        display: block;
-        font: inherit;
-        width: 100%;
-      }
-      .main .message {
-        border: groove;
-        width: 100%;
+        width: 73ch; /* Add a char to account for the border. */
+
+        --iron-autogrow-textarea {
+          border: 1px solid #ddd;
+          font-family: var(--monospace-font-family);
+        }
       }
     </style>
     <gr-confirm-dialog
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
index 5371ce5..ff0fc32 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
@@ -34,7 +34,6 @@
       message: String,
       commitInfo: {
         type: Object,
-        readOnly: true,
         observer: '_commitInfoChanged',
       },
     },
@@ -49,7 +48,8 @@
           new RegExp('\n{1,2}' + revertCommitText + '\\w+.\n*', 'gm');
       commitMessage = commitMessage.replace(previousRevertText, '');
       this.message = 'Revert "' + commitMessage + '"\n\n' +
-          revertCommitText + commitInfo.commit + '.';
+          revertCommitText + commitInfo.commit + '.\n\n' +
+          'Reason for revert: <INSERT REASONING HERE>\n\n';
     },
 
     _handleConfirmTap: function(e) {
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
index 976cf9b..f15c654 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -170,6 +170,8 @@
         </div>
       </div>
       <gr-diff hidden
+          project="[[change.project]]"
+          commit="[[change.current_revision]]"
           change-num="[[changeNum]]"
           patch-range="[[patchRange]]"
           path="[[file.__path]]"
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index 3257469..82c47d6 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -36,6 +36,7 @@
         type: Object,
         value: function() { return document.body; },
       },
+      change: Object,
 
       _files: {
         type: Array,
@@ -332,11 +333,14 @@
       for (var node = el; node; node = node.offsetParent) {
         top += node.offsetTop;
       }
-      if (this._showInlineDiffs) {
-        window.scrollTo(0, top - this.topMargin);
-      } else {
-        window.scrollTo(0, top - document.body.clientHeight / 2);
+
+      // Don't scroll if it's already in view.
+      if (top > window.pageYOffset + this.topMargin &&
+          top < window.pageYOffset + window.innerHeight - el.clientHeight) {
+        return;
       }
+
+      window.scrollTo(0, top - document.body.clientHeight / 2);
     },
 
     _computeFileSelected: function(index, selectedIndex) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
index aa5d8bd..7304ac7 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
@@ -61,10 +61,8 @@
       }
 
       var draft = this._newDraft(opt_lineNum);
+      draft.__editing = true;
       this.push('comments', draft);
-      this.async(function() {
-        this._commentElWithDraftID(draft.__draftID).editing = true;
-      }.bind(this), 1);
     },
 
     _getLoggedIn: function() {
@@ -121,13 +119,8 @@
             function(line) { return ' > ' + line; }).join('\n') + '\n\n';
       }
       var reply = this._newReply(comment.id, comment.line, quoteStr);
+      reply.__editing = true;
       this.push('comments', reply);
-
-      // Allow the reply to render in the dom-repeat.
-      this.async(function() {
-        var commentEl = this._commentElWithDraftID(reply.__draftID);
-        commentEl.editing = true;
-      }.bind(this), 1);
     },
 
     _handleCommentDone: function(e) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
index 525bac0..7a15754 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
@@ -15,6 +15,7 @@
   'use strict';
 
   var STORAGE_DEBOUNCE_INTERVAL = 400;
+  var UPDATE_DEBOUNCE_INTERVAL = 500;
 
   Polymer({
     is: 'gr-diff-comment',
@@ -43,11 +44,18 @@
      * @event comment-save
      */
 
+    /**
+     * Fired when this comment is updated.
+     *
+     * @event comment-update
+     */
+
     properties: {
       changeNum: String,
       comment: {
         type: Object,
         notify: true,
+        observer: '_commentChanged',
       },
       disabled: {
         type: Boolean,
@@ -81,6 +89,10 @@
       '_loadLocalDraft(changeNum, patchNum, comment)',
     ],
 
+    detached: function() {
+      this.flushDebouncer('fire-update');
+    },
+
     save: function() {
       this.comment.message = this._messageText;
       this.disabled = true;
@@ -106,8 +118,7 @@
           }
           this.comment = comment;
           this.editing = false;
-          this.fire('comment-save');
-
+          this.fire('comment-save', {comment: this.comment});
           return obj;
         }.bind(this));
       }.bind(this)).catch(function(err) {
@@ -116,6 +127,16 @@
       }.bind(this));
     },
 
+    _commentChanged: function(comment) {
+      this.editing = !!comment.__editing;
+    },
+
+    _fireUpdate: function() {
+      this.debounce('fire-update', function() {
+        this.fire('comment-update', {comment: this.comment});
+      }, UPDATE_DEBOUNCE_INTERVAL);
+    },
+
     _draftChanged: function(draft) {
       this.$.container.classList.toggle('draft', draft);
     },
@@ -134,6 +155,10 @@
       if (this.comment && this.comment.id) {
         this.$$('.cancel').hidden = !editing;
       }
+      if (this.comment) {
+        this.comment.__editing = this.editing;
+      }
+      this._fireUpdate();
     },
 
     _computeLinkToComment: function(comment) {
@@ -174,6 +199,7 @@
         } else {
           this.$.storage.setDraftComment(commentLocation, message);
         }
+        this._fireUpdate();
       }, STORAGE_DEBOUNCE_INTERVAL);
     },
 
@@ -218,7 +244,7 @@
     _handleCancel: function(e) {
       this._preventDefaultAndBlur(e);
       if (this.comment.message == null || this.comment.message.length == 0) {
-        this.fire('comment-discard');
+        this.fire('comment-discard', {comment: this.comment});
         return;
       }
       this._messageText = this.comment.message;
@@ -234,20 +260,20 @@
       this.disabled = true;
       if (!this.comment.id) {
         this.disabled = false;
-        this.fire('comment-discard');
+        this.fire('comment-discard', {comment: this.comment});
         return;
       }
 
-      this._xhrPromise =
-          this._deleteDraft(this.comment).then(function(response) {
-        this.disabled = false;
-        if (!response.ok) { return response; }
+      this._xhrPromise = this._deleteDraft(this.comment).then(
+          function(response) {
+            this.disabled = false;
+            if (!response.ok) { return response; }
 
-        this.fire('comment-discard');
-      }.bind(this)).catch(function(err) {
-        this.disabled = false;
-        throw err;
-      }.bind(this));;
+            this.fire('comment-discard', {comment: this.comment});
+          }.bind(this)).catch(function(err) {
+            this.disabled = false;
+            throw err;
+          }.bind(this));
     },
 
     _preventDefaultAndBlur: function(e) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
index 8ef222e..6c27a36 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
@@ -204,16 +204,50 @@
     });
 
     test('draft saving/editing', function(done) {
+      var fireStub = sinon.stub(element, 'fire');
+
       element.draft = true;
       MockInteractions.tap(element.$$('.edit'));
       element._messageText = 'good news, everyone!';
+      element.flushDebouncer('fire-update');
+      element.flushDebouncer('store');
+      assert(fireStub.calledWith('comment-update'),
+             'comment-update should be sent');
+      assert.deepEqual(fireStub.lastCall.args, [
+        'comment-update', {
+          comment: {
+            __draft: true,
+            __draftID: 'temp_draft_id',
+            __editing: true,
+            line: 5,
+            path: '/path/to/file',
+          },
+        },
+      ]);
       MockInteractions.tap(element.$$('.save'));
+
       assert.isTrue(element.disabled,
           'Element should be disabled when creating draft.');
 
       element._xhrPromise.then(function(draft) {
+        assert(fireStub.calledWith('comment-save'),
+               'comment-save should be sent');
+        assert.deepEqual(fireStub.lastCall.args, [
+          'comment-save', {
+            comment: {
+              __draft: true,
+              __draftID: 'temp_draft_id',
+              __editing: false,
+              id: 'baf0414d_40572e03',
+              line: 5,
+              message: 'saved!',
+              path: '/path/to/file',
+              updated: '2015-12-08 21:52:36.177000000',
+            },
+          },
+        ]);
         assert.isFalse(element.disabled,
-            'Element should be enabled when done creating draft.');
+                       'Element should be enabled when done creating draft.');
         assert.equal(draft.message, 'saved!');
         assert.isFalse(element.editing);
       }).then(function() {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
index 3f71892..3de3ae4 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
@@ -172,9 +172,9 @@
     },
 
     _rowHasSide: function(row) {
-      var selector = '.content';
-      selector += this.side === DiffSides.LEFT ? '.left' : '.right';
-      return row.querySelector(selector);
+      var selector = (this.side === DiffSides.LEFT ? '.left' : '.right') +
+          ' + .content';
+      return !!row.querySelector(selector);
     },
 
     _isFirstRowOfChunk: function(row) {
@@ -278,7 +278,7 @@
         }
 
         for (i = 0;
-            i < splice.removed && splicee.removed.length;
+            i < splice.removed && splice.removed.length;
             i++) {
           this.unlisten(splice.removed[i], 'render', 'handleDiffUpdate');
         }
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
index 0e99a15..903eabf 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
@@ -174,12 +174,16 @@
             available-patches="[[_computeAvailablePatches(_change.revisions)]]">
         </gr-patch-range-select>
         <div>
-          <select id="modeSelect" on-change="_handleModeChange">
+          <select
+              id="modeSelect"
+              on-change="_handleModeChange"
+              hidden$="[[_computeModeSelectHidden(_isImageDiff)]]">
             <option value="SIDE_BY_SIDE">Side By Side</option>
             <option value="UNIFIED_DIFF">Unified</option>
           </select>
           <span hidden$="[[_computePrefsButtonHidden(_prefs, _loggedIn)]]">
-            /
+            <span
+                hidden$="[[_computeModeSelectHidden(_isImageDiff)]]">/</span>
             <gr-button link
                 class="prefsButton"
                 on-tap="_handlePrefsTap">Preferences</gr-button>
@@ -193,6 +197,9 @@
             on-cancel="_handlePrefsCancel"></gr-diff-preferences>
       </gr-overlay>
       <gr-diff id="diff"
+          project="[[_change.project]]"
+          commit="[[_change.current_revision]]"
+          is-image-diff="{{_isImageDiff}}"
           change-num="[[_changeNum]]"
           patch-range="[[_patchRange]]"
           path="[[_path]]"
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index c75cb72..17aa0c4 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -74,6 +74,7 @@
         type: String,
         computed: '_getDiffViewMode(changeViewState.diffMode, _userPrefs)'
       },
+      _isImageDiff: Boolean,
     },
 
     behaviors: [
@@ -293,11 +294,11 @@
         this._userPrefs = prefs;
       }.bind(this)));
 
-      promises.push(this.$.diff.reload());
+      promises.push(this._getChangeDetail(this._changeNum));
 
-      Promise.all(promises).then(function() {
-        this._loading = false;
-      }.bind(this));
+      Promise.all(promises)
+          .then(function() { return this.$.diff.reload(); }.bind(this))
+          .then(function() { this._loading = false; }.bind(this));
     },
 
     _pathChanged: function(path) {
@@ -462,5 +463,9 @@
         this.$.modeSelect.value = mode;
       }
     },
+
+    _computeModeSelectHidden: function() {
+      return this._isImageDiff;
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-image.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-image.js
new file mode 100644
index 0000000..b897708
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-image.js
@@ -0,0 +1,100 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the 'License');
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function(window, GrDiffBuilderSideBySide) {
+  'use strict';
+
+  function GrDiffBuilderImage(diff, comments, prefs, outputEl, baseImage,
+      revisionImage) {
+    GrDiffBuilderSideBySide.call(this, diff, comments, prefs, outputEl);
+    this._baseImage = baseImage;
+    this._revisionImage = revisionImage;
+  }
+
+  GrDiffBuilderImage.prototype = Object.create(
+      GrDiffBuilderSideBySide.prototype);
+  GrDiffBuilderImage.prototype.constructor = GrDiffBuilderImage;
+
+  GrDiffBuilderImage.prototype.emitDiff = function() {
+    this.emitGroup(this._groups[0]);
+
+    var section = this._createElement('tbody', 'image-diff');
+
+    this._emitImagePair(section);
+    this._emitImageLabels(section);
+
+    this._outputEl.appendChild(section);
+  };
+
+  GrDiffBuilderImage.prototype._emitImagePair = function(section) {
+    var tr = this._createElement('tr');
+
+    tr.appendChild(this._createElement('td'));
+    tr.appendChild(this._createImageCell(this._baseImage, 'left'));
+
+    tr.appendChild(this._createElement('td'));
+    tr.appendChild(this._createImageCell(this._revisionImage, 'right'));
+
+    section.appendChild(tr);
+  };
+
+  GrDiffBuilderImage.prototype._createImageCell = function(image, className) {
+    var td = this._createElement('td', className);
+    if (image) {
+      var imageEl = this._createElement('img');
+      imageEl.src = 'data:' + image.type + ';base64, ' + image.body;
+      image._height = imageEl.naturalHeight;
+      image._width = imageEl.naturalWidth;
+      imageEl.addEventListener('error', function(e) {
+        imageEl.remove();
+        td.textContent = '[Image failed to load]';
+      });
+      td.appendChild(imageEl);
+    }
+    return td;
+  };
+
+  GrDiffBuilderImage.prototype._emitImageLabels = function(section) {
+    var tr = this._createElement('tr');
+
+    tr.appendChild(this._createElement('td'));
+    var td = this._createElement('td', 'left');
+    var label = this._createElement('label');
+    label.textContent = this._getImageLabel(this._baseImage);
+    td.appendChild(label);
+    tr.appendChild(td);
+
+    tr.appendChild(this._createElement('td'));
+    td = this._createElement('td', 'right');
+    label = this._createElement('label');
+    label.textContent = this._getImageLabel(this._revisionImage);
+    td.appendChild(label);
+    tr.appendChild(td);
+
+    section.appendChild(tr);
+  };
+
+  GrDiffBuilderImage.prototype._getImageLabel = function(image) {
+    if (image) {
+      var type = image.type || image._expectedType;
+      if (image._width && image._height) {
+        return image._width + '⨉' + image._height + ' ' + type;
+      } else {
+        return type;
+      }
+    }
+    return 'No image';
+  };
+
+  window.GrDiffBuilderImage = GrDiffBuilderImage;
+})(window, GrDiffBuilderSideBySide);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-side-by-side.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-side-by-side.js
index bf93112..7e8779f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-side-by-side.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-side-by-side.js
@@ -20,7 +20,7 @@
   GrDiffBuilderSideBySide.prototype = Object.create(GrDiffBuilder.prototype);
   GrDiffBuilderSideBySide.prototype.constructor = GrDiffBuilderSideBySide;
 
-  GrDiffBuilderSideBySide.prototype.emitGroup = function(group,
+  GrDiffBuilderSideBySide.prototype.buildSectionElement = function(group,
       opt_beforeSection) {
     var sectionEl = this._createElement('tbody', 'section');
     sectionEl.classList.add(group.type);
@@ -29,7 +29,7 @@
       sectionEl.appendChild(this._createRow(sectionEl, pairs[i].left,
           pairs[i].right));
     }
-    this._outputEl.insertBefore(sectionEl, opt_beforeSection);
+    return sectionEl;
   };
 
   GrDiffBuilderSideBySide.prototype._createRow = function(section, leftLine,
@@ -48,13 +48,14 @@
 
   GrDiffBuilderSideBySide.prototype._appendPair = function(section, row, line,
       lineNumber, side) {
-    row.appendChild(this._createLineEl(line, lineNumber, line.type, side));
+    var lineEl = this._createLineEl(line, lineNumber, line.type, side);
+    lineEl.classList.add(side);
+    row.appendChild(lineEl);
     var action = this._createContextControl(section, line);
     if (action) {
       row.appendChild(action);
     } else {
       var textEl = this._createTextEl(line);
-      textEl.classList.add(side);
       var threadEl = this._commentThreadForLine(line, side);
       if (threadEl) {
         textEl.appendChild(threadEl);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-unified.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-unified.js
index 86340bd..2f1aac6 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-unified.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-unified.js
@@ -20,23 +20,26 @@
   GrDiffBuilderUnified.prototype = Object.create(GrDiffBuilder.prototype);
   GrDiffBuilderUnified.prototype.constructor = GrDiffBuilderUnified;
 
-  GrDiffBuilderUnified.prototype.emitGroup = function(group,
-      opt_beforeSection) {
+  GrDiffBuilderUnified.prototype.buildSectionElement = function(group) {
     var sectionEl = this._createElement('tbody', 'section');
     sectionEl.classList.add(group.type);
 
     for (var i = 0; i < group.lines.length; ++i) {
       sectionEl.appendChild(this._createRow(sectionEl, group.lines[i]));
     }
-    this._outputEl.insertBefore(sectionEl, opt_beforeSection);
+    return sectionEl;
   };
 
   GrDiffBuilderUnified.prototype._createRow = function(section, line) {
     var row = this._createElement('tr', line.type);
-    row.appendChild(this._createLineEl(line, line.beforeNumber,
-        GrDiffLine.Type.REMOVE));
-    row.appendChild(this._createLineEl(line, line.afterNumber,
-        GrDiffLine.Type.ADD));
+    var lineEl = this._createLineEl(line, line.beforeNumber,
+        GrDiffLine.Type.REMOVE);
+    lineEl.classList.add('left');
+    row.appendChild(lineEl);
+    lineEl = this._createLineEl(line, line.afterNumber,
+        GrDiffLine.Type.ADD);
+    lineEl.classList.add('right');
+    row.appendChild(lineEl);
     row.classList.add('diff-row', 'unified');
 
     var action = this._createContextControl(section, line);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder.js
index c2197eb..d2a7c25 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder.js
@@ -15,6 +15,7 @@
   'use strict';
 
   function GrDiffBuilder(diff, comments, prefs, outputEl) {
+    this._diff = diff;
     this._comments = comments;
     this._prefs = prefs;
     this._outputEl = outputEl;
@@ -56,8 +57,51 @@
     }
   };
 
+  GrDiffBuilder.prototype.buildSectionElement = function(
+      group, opt_beforeSection) {
+    throw Error('Subclasses must implement buildGroupElement');
+  };
+
   GrDiffBuilder.prototype.emitGroup = function(group, opt_beforeSection) {
-    throw Error('Subclasses must implement emitGroup');
+    var element = this.buildSectionElement(group);
+    this._outputEl.insertBefore(element, opt_beforeSection);
+    group.element = element;
+  };
+
+  GrDiffBuilder.prototype.renderSection = function(element) {
+    for (var i = 0; i < this._groups.length; i++) {
+      var group = this._groups[i];
+      if (group.element === element) {
+        var newElement = this.buildSectionElement(group);
+        group.element.parentElement.replaceChild(newElement, group.element);
+        group.element = newElement;
+        break;
+      }
+    }
+  };
+
+  GrDiffBuilder.prototype.getSectionsByLineRange = function(
+      startLine, endLine, opt_side) {
+    var sections = [];
+    for (var i = 0; i < this._groups.length; i++) {
+      var group = this._groups[i];
+      if (group.lines.length === 0) {
+        continue;
+      }
+      var groupStartLine;
+      var groupEndLine;
+      if (opt_side === GrDiffBuilder.Side.LEFT) {
+        groupStartLine = group.lines[0].beforeNumber;
+        groupEndLine = group.lines[group.lines.length - 1].beforeNumber;
+      } else if (opt_side === GrDiffBuilder.Side.RIGHT) {
+        groupStartLine = group.lines[0].afterNumber;
+        groupEndLine = group.lines[group.lines.length - 1].afterNumber;
+      }
+      if (startLine <= groupEndLine && endLine >= groupStartLine) {
+        sections.push(group.element);
+      }
+    }
+    return sections;
   };
 
   GrDiffBuilder.prototype._processContent = function(content, groups, context) {
@@ -182,6 +226,7 @@
           currentChunk.ab.push(chunk[j]);
         }
       }
+      // != instead of !== because we want to cover both undefined and null.
       if (currentChunk.ab != null && currentChunk.ab.length > 0) {
         result.push(currentChunk);
       }
@@ -260,7 +305,8 @@
     }
 
     var ctxLine = new GrDiffLine(GrDiffLine.Type.CONTEXT_CONTROL);
-    ctxLine.contextLines = hiddenLines;
+    ctxLine.contextGroup =
+        new GrDiffGroup(GrDiffGroup.Type.BOTH, hiddenLines);
     groups.push(new GrDiffGroup(GrDiffGroup.Type.CONTEXT_CONTROL,
         [ctxLine]));
 
@@ -310,13 +356,14 @@
   };
 
   GrDiffBuilder.prototype._createContextControl = function(section, line) {
-    if (!line.contextLines.length) {
+    if (!line.contextGroup || !line.contextGroup.lines.length) {
       return null;
     }
+    var contextLines = line.contextGroup.lines;
     var td = this._createElement('td');
     var button = this._createElement('gr-button', 'showContext');
     button.setAttribute('link', true);
-    var commonLines = line.contextLines.length;
+    var commonLines = contextLines.length;
     var text = 'Show ' + commonLines + ' common line';
     if (commonLines > 1) {
       text += 's';
@@ -325,7 +372,7 @@
     button.textContent = text;
     button.addEventListener('tap', function(e) {
       e.detail = {
-        group: new GrDiffGroup(GrDiffGroup.Type.BOTH, line.contextLines),
+        group: line.contextGroup,
         section: section,
       };
       // Let it bubble up the DOM tree.
@@ -382,7 +429,7 @@
     }
 
     var patchNum = this._comments.meta.patchRange.patchNum;
-    var side = 'REVISION';
+    var side = comments[0].side || 'REVISION';
     if (line.type === GrDiffLine.Type.REMOVE ||
         opt_side === GrDiffBuilder.Side.LEFT) {
       if (this._comments.meta.patchRange.basePatchNum === 'PARENT') {
@@ -412,7 +459,7 @@
     } else if (line.type === GrDiffLine.Type.CONTEXT_CONTROL) {
       td.classList.add('contextLineNum');
       td.setAttribute('data-value', '@@');
-    } else if (line.type === GrDiffLine.Type.BOTH || line.type == type) {
+    } else if (line.type === GrDiffLine.Type.BOTH || line.type === type) {
       td.classList.add('lineNum');
       td.setAttribute('data-value', number);
     }
@@ -475,18 +522,18 @@
 
     // Tags don't count as characters
     while (index < html.length &&
-           html.charCodeAt(index) == GrDiffBuilder.LESS_THAN_CODE) {
+           html.charCodeAt(index) === GrDiffBuilder.LESS_THAN_CODE) {
       while (index < html.length &&
-             html.charCodeAt(index) != GrDiffBuilder.GREATER_THAN_CODE) {
+             html.charCodeAt(index) !== GrDiffBuilder.GREATER_THAN_CODE) {
         index++;
       }
       index++;  // skip the ">" itself
     }
     // An HTML entity (e.g., &lt;) counts as one character.
     if (index < html.length &&
-        html.charCodeAt(index) == GrDiffBuilder.AMPERSAND_CODE) {
+        html.charCodeAt(index) === GrDiffBuilder.AMPERSAND_CODE) {
       while (index < html.length &&
-             html.charCodeAt(index) != GrDiffBuilder.SEMICOLON_CODE) {
+             html.charCodeAt(index) !== GrDiffBuilder.SEMICOLON_CODE) {
         index++;
       }
     }
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder_test.html
index 22b9072..4392de0 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder_test.html
@@ -144,8 +144,9 @@
       assert.equal(groups[0].lines[0].afterNumber, GrDiffLine.FILE);
 
       assert.equal(groups[1].type, GrDiffGroup.Type.CONTEXT_CONTROL);
-      assert.equal(groups[1].lines[0].contextLines.length, 90);
-      groups[1].lines[0].contextLines.forEach(function(l) {
+      assert.instanceOf(groups[1].lines[0].contextGroup, GrDiffGroup);
+      assert.equal(groups[1].lines[0].contextGroup.lines.length, 90);
+      groups[1].lines[0].contextGroup.lines.forEach(function(l) {
         assert.equal(l.text, content[0].ab[0]);
       });
 
@@ -179,8 +180,9 @@
       });
 
       assert.equal(groups[7].type, GrDiffGroup.Type.CONTEXT_CONTROL);
-      assert.equal(groups[7].lines[0].contextLines.length, 90);
-      groups[7].lines[0].contextLines.forEach(function(l) {
+      assert.instanceOf(groups[7].lines[0].contextGroup, GrDiffGroup);
+      assert.equal(groups[7].lines[0].contextGroup.lines.length, 90);
+      groups[7].lines[0].contextGroup.lines.forEach(function(l) {
         assert.equal(l.text, content[4].ab[0]);
       });
 
@@ -215,8 +217,9 @@
       });
 
       assert.equal(groups[3].type, GrDiffGroup.Type.CONTEXT_CONTROL);
-      assert.equal(groups[3].lines[0].contextLines.length, 30);
-      groups[3].lines[0].contextLines.forEach(function(l) {
+      assert.instanceOf(groups[3].lines[0].contextGroup, GrDiffGroup);
+      assert.equal(groups[3].lines[0].contextGroup.lines.length, 30);
+      groups[3].lines[0].contextGroup.lines.forEach(function(l) {
         assert.equal(l.text, content[1].ab[0]);
       });
 
@@ -517,5 +520,54 @@
         }
       ]);
     });
+
+    suite('rendering', function() {
+      var content;
+      var outputEl;
+
+      setup(function() {
+        var prefs = {
+          line_length: 10,
+          show_tabs: true,
+          tab_size: 4,
+          context: -1
+        };
+        content = [
+          {ab: []},
+          {a: ['all work and no play make andybons a dull boy']},
+          {ab: []},
+          {b: ['elgoog elgoog elgoog']},
+          {ab: []},
+        ];
+        outputEl = document.createElement('out');
+        builder =
+            new GrDiffBuilder(
+                {content: content}, {left: [], right: []}, prefs, outputEl);
+        builder.buildSectionElement = function(group) {
+          var section = document.createElement('stub');
+          section.textContent = group.lines.reduce(function(acc, line) {
+            return acc + line.text;
+          }, '');
+          return section;
+        };
+        builder.emitDiff();
+      });
+
+      test('renderSection', function() {
+        var section = outputEl.querySelector('stub:nth-of-type(2)');
+        var prevInnerHTML = section.innerHTML;
+        section.innerHTML = 'wiped';
+        builder.renderSection(section);
+        section = outputEl.querySelector('stub:nth-of-type(2)');
+        assert.equal(section.innerHTML, prevInnerHTML);
+      });
+
+      test('getSectionsByLineRange', function() {
+        var section = outputEl.querySelector('stub:nth-of-type(2)');
+        var sections = builder.getSectionsByLineRange(1, 1, 'left');
+        assert.equal(sections.length, 1);
+        assert.strictEqual(sections[0], section);
+      });
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js
index 750f7da..7c7c508 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js
@@ -25,6 +25,8 @@
     }
   }
 
+  GrDiffGroup.prototype.element = null;
+
   GrDiffGroup.Type = {
     BOTH: 'both',
     CONTEXT_CONTROL: 'contextControl',
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.js
index ea00a3d..4acde0c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.js
@@ -16,13 +16,14 @@
 
   function GrDiffLine(type) {
     this.type = type;
-    this.contextLines = [];
     this.highlights = [];
   }
 
+  GrDiffLine.prototype.afterNumber = 0;
+
   GrDiffLine.prototype.beforeNumber = 0;
 
-  GrDiffLine.prototype.afterNumber = 0;
+  GrDiffLine.prototype.contextGroup = null;
 
   GrDiffLine.prototype.text = '';
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
index e4603b8..826a21e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -43,6 +43,17 @@
       .section {
         background-color: #eee;
       }
+      .image-diff .gr-diff {
+        text-align: center;
+      }
+      .image-diff img {
+        max-width: 50em;
+        outline: 1px solid #ccc;
+      }
+      .image-diff label {
+        font-family: var(--font-family);
+        font-style: italic;
+      }
       .diff-row.target-row.target-side-left .lineNum.left,
       .diff-row.target-row.target-side-right .lineNum.right,
       .diff-row.target-row.unified .lineNum {
@@ -151,5 +162,6 @@
   <script src="gr-diff-builder.js"></script>
   <script src="gr-diff-builder-side-by-side.js"></script>
   <script src="gr-diff-builder-unified.js"></script>
+  <script src="gr-diff-builder-image.js"></script>
   <script src="gr-diff.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index 660b86f..d813933 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -42,6 +42,13 @@
         type: Object,
         observer: '_projectConfigChanged',
       },
+      project: String,
+      commit: String,
+      isImageDiff: {
+        type: Boolean,
+        computed: '_computeIsImageDiff(_diff)',
+        notify: true,
+      },
 
       _loggedIn: {
         type: Boolean,
@@ -64,15 +71,17 @@
       '_prefsChanged(prefs.*, viewMode)',
     ],
 
+    listeners: {
+      'thread-discard': '_handleThreadDiscard',
+      'comment-discard': '_handleCommentDiscard',
+      'comment-update': '_handleCommentUpdate',
+      'comment-save': '_handleCommentSave',
+    },
+
     attached: function() {
       this._getLoggedIn().then(function(loggedIn) {
         this._loggedIn = loggedIn;
       }.bind(this));
-
-      this.addEventListener('thread-discard',
-          this._handleThreadDiscard.bind(this));
-      this.addEventListener('comment-discard',
-          this._handleCommentDiscard.bind(this));
     },
 
     reload: function() {
@@ -82,6 +91,7 @@
 
       promises.push(this._getDiff().then(function(diff) {
         this._diff = diff;
+        return this._loadDiffAssets();
       }.bind(this)));
 
       promises.push(this._getDiffCommentsAndDrafts().then(function(comments) {
@@ -205,7 +215,7 @@
       } else {
         var patchNum = this.patchRange.patchNum;
         var side = 'REVISION';
-        if (contentEl.classList.contains(DiffSide.LEFT) ||
+        if (lineEl.classList.contains(DiffSide.LEFT) ||
             contentEl.classList.contains('remove')) {
           if (this.patchRange.basePatchNum === 'PARENT') {
             side = 'PARENT';
@@ -226,29 +236,71 @@
     },
 
     _handleCommentDiscard: function(e) {
-      var comment = Polymer.dom(e).rootTarget.comment;
-      this._removeComment(comment);
+      var comment = e.detail.comment;
+      this._removeComment(comment, e.target.patchNum);
     },
 
-    _removeComment: function(comment) {
-      if (!comment.id) { return; }
-      this._removeCommentFromSide(comment, DiffSide.LEFT) ||
-          this._removeCommentFromSide(comment, DiffSide.RIGHT);
+    _removeComment: function(comment, opt_patchNum) {
+      var side = this._findCommentSide(comment, opt_patchNum);
+      this._removeCommentFromSide(comment, side);
+    },
+
+    _findCommentSide: function(comment, opt_patchNum) {
+      if (comment.side === 'PARENT') {
+        return DiffSide.LEFT;
+      } else {
+        return this._comments.meta.patchRange.basePatchNum === opt_patchNum ?
+            DiffSide.LEFT : DiffSide.RIGHT;
+      }
+    },
+
+    _handleCommentSave: function(e) {
+      var comment = e.detail.comment;
+      var side = this._findCommentSide(comment, e.target.patchNum);
+      var idx = this._findDraftIndex(comment, side);
+      this.set(['_comments', side, idx], comment);
+    },
+
+    _handleCommentUpdate: function(e) {
+      var comment = e.detail.comment;
+      var side = this._findCommentSide(comment, e.target.patchNum);
+      var idx = this._findCommentIndex(comment, side);
+      if (idx === -1) {
+        idx = this._findDraftIndex(comment, side);
+      }
+      if (idx !== -1) { // Update draft or comment.
+        this.set(['_comments', side, idx], comment);
+      } else { // Create new draft.
+        this.push(['_comments', side], comment);
+      }
     },
 
     _removeCommentFromSide: function(comment, side) {
-      var idx = -1;
-      for (var i = 0; i < this._comments[side].length; i++) {
-        if (this._comments[side][i].id === comment.id) {
-          idx = i;
-          break;
-        }
+      var idx = this._findCommentIndex(comment, side);
+      if (idx === -1) {
+        idx = this._findDraftIndex(comment, side);
       }
       if (idx !== -1) {
         this.splice('_comments.' + side, idx, 1);
-        return true;
       }
-      return false;
+    },
+
+    _findCommentIndex: function(comment, side) {
+      if (!comment.id || !this._comments[side]) {
+        return -1;
+      }
+      return this._comments[side].findIndex(function(item) {
+        return item.id === comment.id;
+      });
+    },
+
+    _findDraftIndex: function(comment, side) {
+      if (!comment.__draftID || !this._comments[side]) {
+        return -1;
+      }
+      return this._comments[side].findIndex(function(item) {
+        return item.__draftID === comment.__draftID;
+      });
     },
 
     _handleMouseDown: function(e) {
@@ -312,6 +364,13 @@
     },
 
     _showContext: function(group, sectionEl) {
+      var groups = this._builder._groups;
+      // TODO(viktard): Polyfill findIndex for IE10.
+      var contextIndex = groups.findIndex(function(group) {
+        return group.element == sectionEl;
+      });
+      groups[contextIndex] = group;
+
       this._builder.emitGroup(group, sectionEl);
       sectionEl.parentNode.removeChild(sectionEl);
 
@@ -331,14 +390,17 @@
     },
 
     _render: function() {
-      this._clearDiffContent();
-      this._builder = this._getDiffBuilder(this._diff, this._comments,
-          this.prefs);
-      this._builder.emitDiff(this._diff.content);
+      this._builder =
+          this._getDiffBuilder(this._diff, this._comments, this.prefs);
+      this._renderDiff();
+    },
 
+    _renderDiff: function() {
+      this._clearDiffContent();
+      this._builder.emitDiff();
       this.async(function() {
         this.fire('render', null, {bubbles: false});
-      }.bind(this), 1);
+      }, 1);
     },
 
     _clearDiffContent: function() {
@@ -414,8 +476,40 @@
       return this.$.restAPI.getLoggedIn();
     },
 
+    _computeIsImageDiff: function() {
+      if (!this._diff) { return false; }
+
+      var isA = this._diff.meta_a &&
+          this._diff.meta_a.content_type.indexOf('image/') === 0;
+      var isB = this._diff.meta_b &&
+          this._diff.meta_b.content_type.indexOf('image/') === 0;
+
+      return this._diff.binary && (isA || isB);
+    },
+
+    _loadDiffAssets: function() {
+      if (this.isImageDiff) {
+        return this._getImages().then(function(images) {
+          this._baseImage = images.baseImage;
+          this._revisionImage = images.revisionImage;
+        }.bind(this));
+      } else {
+        this._baseImage = null;
+        this._revisionImage = null;
+        return Promise.resolve();
+      }
+    },
+
+    _getImages: function() {
+      return this.$.restAPI.getImagesForDiff(this.project, this.commit,
+          this.changeNum, this._diff, this.patchRange);
+    },
+
     _getDiffBuilder: function(diff, comments, prefs) {
-      if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) {
+      if (this.isImageDiff) {
+        return new GrDiffBuilderImage(diff, comments, prefs, this.$.diffTable,
+            this._baseImage, this._revisionImage);
+      } else if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) {
         return new GrDiffBuilderSideBySide(diff, comments, prefs,
             this.$.diffTable);
       } else if (this.viewMode === DiffViewMode.UNIFIED) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
index f02b861..aec32b6 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -20,6 +20,7 @@
 
 <script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
 <script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/util.js"></script>
 
 <link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
 <link rel="import" href="gr-diff.html">
@@ -34,85 +35,29 @@
   suite('gr-diff tests', function() {
     var element;
 
-    setup(function() {
-      stub('gr-rest-api-interface', {
-        getLoggedIn: function() { return Promise.resolve(false); },
+    suite('not logged in', function() {
+
+      setup(function() {
+        stub('gr-rest-api-interface', {
+          getLoggedIn: function() { return Promise.resolve(false); },
+        });
+        element = fixture('basic');
       });
-      element = fixture('basic');
-    });
 
-    test('get drafts logged out', function(done) {
-      element.patchRange = {basePatchNum: 0, patchNum: 0};
+      test('get drafts', function(done) {
+        element.patchRange = {basePatchNum: 0, patchNum: 0};
 
-      var getDraftsStub = sinon.stub(element.$.restAPI, 'getDiffDrafts');
-      var loggedInStub = sinon.stub(element, '_getLoggedIn',
-          function() { return Promise.resolve(false); });
-      element._getDiffDrafts().then(function(result) {
-        assert.deepEqual(result, {baseComments: [], comments: []});
-        sinon.assert.notCalled(getDraftsStub);
-        loggedInStub.restore();
-        getDraftsStub.restore();
-        done();
+        var getDraftsStub = sinon.stub(element.$.restAPI, 'getDiffDrafts');
+        element._getDiffDrafts().then(function(result) {
+          assert.deepEqual(result, {baseComments: [], comments: []});
+          sinon.assert.notCalled(getDraftsStub);
+          getDraftsStub.restore();
+          done();
+        });
       });
-    });
 
-    test('get drafts logged in', function(done) {
-      element.patchRange = {basePatchNum: 0, patchNum: 0};
-      var draftsResponse = {
-        baseComments: [{id: 'foo'}],
-        comments: [{id: 'bar'}],
-      };
-      var getDraftsStub = sinon.stub(element.$.restAPI, 'getDiffDrafts',
-          function() { return Promise.resolve(draftsResponse); });
-      var loggedInStub = sinon.stub(element, '_getLoggedIn',
-          function() { return Promise.resolve(true); });
-      element._getDiffDrafts().then(function(result) {
-        assert.deepEqual(result, draftsResponse);
-        loggedInStub.restore();
-        getDraftsStub.restore();
-        done();
-      });
-    });
-
-    test('get comments and drafts', function(done) {
-      var loggedInStub = sinon.stub(element, '_getLoggedIn',
-          function() { return Promise.resolve(true); });
-      var comments = {
-        baseComments: [
-          {id: 'bc1'},
-          {id: 'bc2'},
-        ],
-        comments: [
-          {id: 'c1'},
-          {id: 'c2'},
-        ],
-      };
-      var diffCommentsStub = sinon.stub(element, '_getDiffComments',
-          function() { return Promise.resolve(comments); });
-
-      var drafts = {
-        baseComments: [
-          {id: 'bd1'},
-          {id: 'bd2'},
-        ],
-        comments: [
-          {id: 'd1'},
-          {id: 'd2'},
-        ],
-      };
-      var diffDraftsStub = sinon.stub(element, '_getDiffDrafts',
-          function() { return Promise.resolve(drafts); });
-
-      element.changeNum = '42';
-      element.patchRange = {
-        basePatchNum: 'PARENT',
-        patchNum: 3,
-      };
-      element.path = '/path/to/foo';
-      element.projectConfig = {foo: 'bar'};
-
-      element._getDiffCommentsAndDrafts().then(function(result) {
-        assert.deepEqual(result, {
+      test('remove comment', function() {
+        element._comments = {
           meta: {
             changeNum: '42',
             patchRange: {
@@ -123,10 +68,10 @@
             projectConfig: {foo: 'bar'},
           },
           left: [
-            {id: 'bc1'},
-            {id: 'bc2'},
-            {id: 'bd1', __draft: true},
-            {id: 'bd2', __draft: true},
+            {id: 'bc1', side: 'PARENT'},
+            {id: 'bc2', side: 'PARENT'},
+            {id: 'bd1', __draft: true, side: 'PARENT'},
+            {id: 'bd2', __draft: true, side: 'PARENT'},
           ],
           right: [
             {id: 'c1'},
@@ -134,113 +79,348 @@
             {id: 'd1', __draft: true},
             {id: 'd2', __draft: true},
           ],
-        });
+        };
 
-        diffCommentsStub.restore();
-        diffDraftsStub.restore();
-        loggedInStub.restore();
-        done();
+        element._removeComment({});
+        // Using JSON.stringify because Safari 9.1 (11601.5.17.1) doesn’t seem
+        // to believe that one object deepEquals another even when they do :-/.
+        assert.equal(JSON.stringify(element._comments), JSON.stringify({
+          meta: {
+            changeNum: '42',
+            patchRange: {
+              basePatchNum: 'PARENT',
+              patchNum: 3,
+            },
+            path: '/path/to/foo',
+            projectConfig: {foo: 'bar'},
+          },
+          left: [
+            {id: 'bc1', side: 'PARENT'},
+            {id: 'bc2', side: 'PARENT'},
+            {id: 'bd1', __draft: true, side: 'PARENT'},
+            {id: 'bd2', __draft: true, side: 'PARENT'},
+          ],
+          right: [
+            {id: 'c1'},
+            {id: 'c2'},
+            {id: 'd1', __draft: true},
+            {id: 'd2', __draft: true},
+          ],
+        }));
+
+        element._removeComment({id: 'bc2', side: 'PARENT'});
+        assert.equal(JSON.stringify(element._comments), JSON.stringify({
+          meta: {
+            changeNum: '42',
+            patchRange: {
+              basePatchNum: 'PARENT',
+              patchNum: 3,
+            },
+            path: '/path/to/foo',
+            projectConfig: {foo: 'bar'},
+          },
+          left: [
+            {id: 'bc1', side: 'PARENT'},
+            {id: 'bd1', __draft: true, side: 'PARENT'},
+            {id: 'bd2', __draft: true, side: 'PARENT'},
+          ],
+          right: [
+            {id: 'c1'},
+            {id: 'c2'},
+            {id: 'd1', __draft: true},
+            {id: 'd2', __draft: true},
+          ],
+        }));
+
+        element._removeComment({id: 'd2'});
+        assert.deepEqual(JSON.stringify(element._comments), JSON.stringify({
+          meta: {
+            changeNum: '42',
+            patchRange: {
+              basePatchNum: 'PARENT',
+              patchNum: 3,
+            },
+            path: '/path/to/foo',
+            projectConfig: {foo: 'bar'},
+          },
+          left: [
+            {id: 'bc1', side: 'PARENT'},
+            {id: 'bd1', __draft: true, side: 'PARENT'},
+            {id: 'bd2', __draft: true, side: 'PARENT'},
+          ],
+          right: [
+            {id: 'c1'},
+            {id: 'c2'},
+            {id: 'd1', __draft: true},
+          ],
+        }));
+      });
+
+      test('renders image diffs', function(done) {
+        var mockDiff = {
+          meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
+          meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 560},
+          intraline_status: 'OK',
+          change_type: 'MODIFIED',
+          diff_header: [
+            'diff --git a/carrot.jpg b/carrot.jpg',
+            'index 2adc47d..f9c2f2c 100644',
+            '--- a/carrot.jpg',
+            '+++ b/carrot.jpg',
+            'Binary files differ',
+          ],
+          content: [{skip: 66}],
+          binary: true,
+        };
+        var mockFile1 = {
+          body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAEwsA' +
+              'AAAAAAAAAAAAAAAA/w==',
+          type: 'image/bmp',
+        };
+        var mockFile2 = {
+          body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAEwsA' +
+              'AAAAAAAAAAAA/////w==',
+          type: 'image/bmp'
+        };
+        var mockCommit = {
+          commit: '9a1a1d10baece5efbba10bc4ccf808a67a50ac0a',
+          parents: [{
+            commit: '7338aa9adfe57909f1fdaf88975cdea467d3382f',
+            subject: 'Added a carrot',
+          }],
+          author: {
+            name: 'Wyatt Allen',
+            email: 'wyatta@google.com',
+            date: '2016-05-23 21:44:51.000000000',
+            tz: -420,
+          },
+          committer: {
+            name: 'Wyatt Allen',
+            email: 'wyatta@google.com',
+            date: '2016-05-25 00:25:41.000000000',
+            tz: -420,
+          },
+          subject: 'Updated the carrot',
+          message: 'Updated the carrot\n\nChange-Id: Iabcd123\n',
+        };
+        var mockComments = {baseComments: [], comments: []};
+
+        var stubs = [];
+        stubs.push(sinon.stub(element, '_getDiff',
+            function() { return Promise.resolve(mockDiff); }));
+        stubs.push(sinon.stub(element.$.restAPI, 'getCommitInfo',
+            function() { return Promise.resolve(mockCommit); }));
+        stubs.push(sinon.stub(element.$.restAPI,
+            'getCommitFileContents',
+            function() { return Promise.resolve(mockFile1); }));
+        stubs.push(sinon.stub(element.$.restAPI,
+            'getChangeFileContents',
+            function() { return Promise.resolve(mockFile2); }));
+        stubs.push(sinon.stub(element.$.restAPI, '_getDiffComments',
+            function() { return Promise.resolve(mockComments); }));
+        stubs.push(sinon.stub(element.$.restAPI, 'getDiffDrafts',
+            function() { return Promise.resolve(mockComments); }));
+
+        element.patchRange = {basePatchNum: 'PARENT', patchNum: 1};
+
+        var rendered = function() {
+          // Recognizes that it should be an image diff.
+          assert.isTrue(element.isImageDiff);
+          assert.instanceOf(element._getDiffBuilder(element._diff,
+              element._comments, element.prefs), GrDiffBuilderImage);
+
+          // Left image rendered with the parent commit's version of the file.
+          var leftInmage = element.$.diffTable.querySelector('td.left img');
+          assert.isOk(leftInmage);
+          assert.equal(leftInmage.getAttribute('src'),
+              'data:image/bmp;base64, ' + mockFile1.body);
+
+          // Right image rendered with this change's revision of the image.
+          var rightInmage = element.$.diffTable.querySelector('td.right img');
+          assert.isOk(rightInmage);
+          assert.equal(rightInmage.getAttribute('src'),
+              'data:image/bmp;base64, ' + mockFile2.body);
+
+          // Cleanup.
+          element.removeEventListener('render', rendered);
+          stubs.forEach(function(stub) { stub.restore(); });
+
+          done();
+        };
+
+        element.addEventListener('render', rendered);
+
+        element.$.restAPI.getDiffPreferences().then(function(prefs) {
+          element.prefs = prefs;
+          element.reload();
+        });
       });
     });
 
-    test('remove comment', function() {
-      element._comments = {
-        meta: {
-          changeNum: '42',
-          patchRange: {
-            basePatchNum: 'PARENT',
-            patchNum: 3,
-          },
-          path: '/path/to/foo',
-          projectConfig: {foo: 'bar'},
-        },
-        left: [
-          {id: 'bc1'},
-          {id: 'bc2'},
-          {id: 'bd1', __draft: true},
-          {id: 'bd2', __draft: true},
-        ],
-        right: [
-          {id: 'c1'},
-          {id: 'c2'},
-          {id: 'd1', __draft: true},
-          {id: 'd2', __draft: true},
-        ],
-      };
+    suite('logged in', function() {
 
-      element._removeComment({});
-      // Using JSON.stringify because Safari 9.1 (11601.5.17.1) doesn’t seem to
-      // believe that one object deepEquals another even when they do :-/.
-      assert.equal(JSON.stringify(element._comments), JSON.stringify({
-        meta: {
-          changeNum: '42',
-          patchRange: {
-            basePatchNum: 'PARENT',
-            patchNum: 3,
-          },
-          path: '/path/to/foo',
-          projectConfig: {foo: 'bar'},
-        },
-        left: [
-          {id: 'bc1'},
-          {id: 'bc2'},
-          {id: 'bd1', __draft: true},
-          {id: 'bd2', __draft: true},
-        ],
-        right: [
-          {id: 'c1'},
-          {id: 'c2'},
-          {id: 'd1', __draft: true},
-          {id: 'd2', __draft: true},
-        ],
-      }));
+      setup(function() {
+        stub('gr-rest-api-interface', {
+          getLoggedIn: function() { return Promise.resolve(true); },
+        });
+        element = fixture('basic');
+      });
 
-      element._removeComment({id: 'bc2'});
-      assert.equal(JSON.stringify(element._comments), JSON.stringify({
-        meta: {
-          changeNum: '42',
-          patchRange: {
-            basePatchNum: 'PARENT',
-            patchNum: 3,
-          },
-          path: '/path/to/foo',
-          projectConfig: {foo: 'bar'},
-        },
-        left: [
-          {id: 'bc1'},
-          {id: 'bd1', __draft: true},
-          {id: 'bd2', __draft: true},
-        ],
-        right: [
-          {id: 'c1'},
-          {id: 'c2'},
-          {id: 'd1', __draft: true},
-          {id: 'd2', __draft: true},
-        ],
-      }));
+      test('get drafts', function(done) {
+        element.patchRange = {basePatchNum: 0, patchNum: 0};
+        var draftsResponse = {
+          baseComments: [{id: 'foo'}],
+          comments: [{id: 'bar'}],
+        };
+        var getDraftsStub = sinon.stub(element.$.restAPI, 'getDiffDrafts',
+            function() { return Promise.resolve(draftsResponse); });
+        element._getDiffDrafts().then(function(result) {
+          assert.deepEqual(result, draftsResponse);
+          getDraftsStub.restore();
+          done();
+        });
+      });
 
-      element._removeComment({id: 'd2'});
-      assert.deepEqual(JSON.stringify(element._comments), JSON.stringify({
-        meta: {
-          changeNum: '42',
-          patchRange: {
-            basePatchNum: 'PARENT',
-            patchNum: 3,
-          },
-          path: '/path/to/foo',
-          projectConfig: {foo: 'bar'},
-        },
-        left: [
-          {id: 'bc1'},
-          {id: 'bd1', __draft: true},
-          {id: 'bd2', __draft: true},
-        ],
-        right: [
-          {id: 'c1'},
-          {id: 'c2'},
-          {id: 'd1', __draft: true},
-        ],
-      }));
+      test('get comments and drafts', function(done) {
+        var comments = {
+          baseComments: [
+            {id: 'bc1'},
+            {id: 'bc2'},
+          ],
+          comments: [
+            {id: 'c1'},
+            {id: 'c2'},
+          ],
+        };
+        var diffCommentsStub = sinon.stub(element, '_getDiffComments',
+            function() { return Promise.resolve(comments); });
+
+        var drafts = {
+          baseComments: [
+            {id: 'bd1'},
+            {id: 'bd2'},
+          ],
+          comments: [
+            {id: 'd1'},
+            {id: 'd2'},
+          ],
+        };
+        var diffDraftsStub = sinon.stub(element, '_getDiffDrafts',
+            function() { return Promise.resolve(drafts); });
+
+        element.changeNum = '42';
+        element.patchRange = {
+          basePatchNum: 'PARENT',
+          patchNum: 3,
+        };
+        element.path = '/path/to/foo';
+        element.projectConfig = {foo: 'bar'};
+
+        element._getDiffCommentsAndDrafts().then(function(result) {
+          assert.deepEqual(result, {
+            meta: {
+              changeNum: '42',
+              patchRange: {
+                basePatchNum: 'PARENT',
+                patchNum: 3,
+              },
+              path: '/path/to/foo',
+              projectConfig: {foo: 'bar'},
+            },
+            left: [
+              {id: 'bc1'},
+              {id: 'bc2'},
+              {id: 'bd1', __draft: true},
+              {id: 'bd2', __draft: true},
+            ],
+            right: [
+              {id: 'c1'},
+              {id: 'c2'},
+              {id: 'd1', __draft: true},
+              {id: 'd2', __draft: true},
+            ],
+          });
+
+          diffCommentsStub.restore();
+          diffDraftsStub.restore();
+          done();
+        });
+      });
+
+      suite('handle comment-update', function() {
+
+        setup(function() {
+          element._comments = {
+            meta: {
+              changeNum: '42',
+              patchRange: {
+                basePatchNum: 'PARENT',
+                patchNum: 3,
+              },
+              path: '/path/to/foo',
+              projectConfig: {foo: 'bar'},
+            },
+            left: [
+              {id: 'bc1', side: 'PARENT'},
+              {id: 'bc2', side: 'PARENT'},
+              {id: 'bd1', __draft: true, side: 'PARENT'},
+              {id: 'bd2', __draft: true, side: 'PARENT'},
+            ],
+            right: [
+              {id: 'c1'},
+              {id: 'c2'},
+              {id: 'd1', __draft: true},
+              {id: 'd2', __draft: true},
+            ],
+          };
+        });
+
+        test('creating a draft', function() {
+          var comment = {__draft: true, __draftID: 'tempID', side: 'PARENT'};
+          element.fire('comment-update', {comment: comment});
+          assert.include(element._comments.left, comment);
+        });
+
+        test('saving a draft', function() {
+          var draftID = 'tempID';
+          var id = 'savedID';
+          element._comments.left.push(
+              {__draft: true, __draftID: draftID, side: 'PARENT'});
+          element.fire('comment-update', {comment:
+              {id: id, __draft: true, __draftID: draftID, side: 'PARENT'},
+          });
+          var drafts = element._comments.left.filter(function(item) {
+            return item.__draftID === draftID;
+          });
+          assert.equal(drafts.length, 1);
+          assert.equal(drafts[0].id, id);
+        });
+      });
+    });
+
+    suite('renderDiff', function() {
+      setup(function(done) {
+        sinon.stub(element, 'fire');
+        element._builder = {
+          emitDiff: sinon.stub(),
+        };
+        element._renderDiff();
+        flush(function() {
+          done();
+        });
+      });
+
+      teardown(function() {
+        element.fire.restore();
+      });
+
+      test('fires render', function() {
+        assert(element.fire.calledWithExactly(
+            'render', null, {bubbles: false}));
+      });
+      test('calls emitDiff on builder', function() {
+        assert(element._builder.emitDiff.calledOnce);
+      });
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
index 10c8a29..72ae4e4 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
@@ -18,7 +18,6 @@
 <script src="../../../bower_components/fetch/fetch.js"></script>
 
 <dom-module id="gr-rest-api-interface">
-  <template></template>
   <script src="gr-rest-api-interface.js"></script>
 </dom-module>
 
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index e681bf9..9604ded 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -682,5 +682,84 @@
       return '';
     },
 
+    getCommitInfo: function(project, commit) {
+      return this.fetchJSON(
+          '/projects/' + encodeURIComponent(project) +
+          '/commits/' + encodeURIComponent(commit));
+    },
+
+    _fetchB64File: function(url) {
+      return fetch(url).then(function(response) {
+        var type = response.headers.get('X-FYI-Content-Type');
+        return response.text()
+          .then(function(text) {
+            return {body: text, type: type};
+          });
+      });
+    },
+
+    getChangeFileContents: function(changeId, patchNum, path) {
+      return this._fetchB64File(
+          '/changes/' + encodeURIComponent(changeId) +
+          '/revisions/' + encodeURIComponent(patchNum) +
+          '/files/' + encodeURIComponent(path) +
+          '/content');
+    },
+
+    getCommitFileContents: function(projectName, commit, path) {
+      return this._fetchB64File(
+          '/projects/' + encodeURIComponent(projectName) +
+          '/commits/' + encodeURIComponent(commit) +
+          '/files/' + encodeURIComponent(path) +
+          '/content');
+    },
+
+    getImagesForDiff: function(project, commit, changeNum, diff, patchRange) {
+      var promiseA;
+      var promiseB;
+
+      if (diff.meta_a && diff.meta_a.content_type.indexOf('image/') === 0) {
+        if (patchRange.basePatchNum === 'PARENT') {
+          // Need the commit info know the parent SHA.
+          promiseA = this.getCommitInfo(project, commit).then(function(info) {
+            if (info.parents.length !== 1) {
+              return Promise.reject('Change commit has multiple parents.');
+            }
+            var parent = info.parents[0].commit;
+            return this.getCommitFileContents(project, parent,
+                diff.meta_a.name);
+          }.bind(this));
+
+        } else {
+          promiseA = this.getChangeFileContents(changeNum,
+              patchRange.basePatchNum, diff.meta_a.name);
+        }
+      } else {
+        promiseA = Promise.resolve(null);
+      }
+
+      if (diff.meta_b && diff.meta_b.content_type.indexOf('image/') === 0) {
+        promiseB = this.getChangeFileContents(changeNum, patchRange.patchNum,
+            diff.meta_b.name);
+      } else {
+        promiseB = Promise.resolve(null);
+      }
+
+      return Promise.all([promiseA, promiseB])
+        .then(function(results) {
+          var baseImage = results[0];
+          var revisionImage = results[1];
+
+          // Sometimes the server doesn't send back the content type.
+          if (baseImage) {
+            baseImage._expectedType = diff.meta_a.content_type;
+          }
+          if (revisionImage) {
+            revisionImage._expectedType = diff.meta_b.content_type;
+          }
+
+          return {baseImage: baseImage, revisionImage: revisionImage};
+        }.bind(this));
+    },
   });
 })();