Merge "Display the originator of each access rule"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 864092e..2fbe58c 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -397,14 +397,43 @@
 [[cache_options]]Cache Options
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
-cache.diff.intraline::
+cache.diff_intraline.maxIdleWorkers::
++
+Number of idle worker threads to maintain for the intraline difference
+computations.  There is no upper bound on how many concurrent requests
+can occur at once, if additional threads are started to handle a peak
+load, only this many will remaining idle afterwards.
++
+Default is 1.5x number of available CPUs.
+
+cache.diff_intraline.timeout::
++
+Maximum number of milliseconds to wait for intraline difference data
+before giving up and disabling it for a particular file pair.  This is
+a work around for an infinite loop bug in the intraline difference
+implementation.  If computation takes longer than the timeout the
+worker thread is terminated and no intraline difference is displayed.
++
+Values should use common unit suffixes to express their setting:
++
+* ms, milliseconds
+* s, sec, second, seconds
+* m, min, minute, minutes
+* h, hr, hour, hours
+
++
+If a unit suffix is not specified, `milliseconds` is assumed.
++
+Default is 5 seconds.
+
+cache.diff_intraline.enabled::
 +
 Boolean to enable or disable the computation of intraline differences
-when populating a diff cache entry.  Changing this setting in the
-server configuration requires flushing the "diff" cache after a
-restart, otherwise older cache entries stored on disk may not reflect
-the current server setting.  This flag is provided primarily as a
-backdoor to disable the intraline difference feature if necessary.
+when populating a diff cache entry.  This flag is provided primarily
+as a backdoor to disable the intraline difference feature if
+necessary.  To maintain backwards compatability with prior versions,
+this setting will fallback to `cache.diff.intraline` if not set in the
+configuration.
 +
 Default is true, enabled.
 
diff --git a/ReleaseNotes/ReleaseNotes-2.1.6.txt b/ReleaseNotes/ReleaseNotes-2.1.6.txt
new file mode 100644
index 0000000..198a064
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.1.6.txt
@@ -0,0 +1,322 @@
+Release notes for Gerrit 2.1.6
+==============================
+
+Gerrit 2.1.6 is now available:
+
+link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.1.6.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.1.6.war]
+
+Schema Change
+-------------
+
+*WARNING* This release contains multiple schema changes.  To upgrade:
+----
+  java -jar gerrit.war init -d site_path
+----
+
+
+New Features
+------------
+
+Web UI
+~~~~~~
+* issue 312 Abandoned changes can now be restored.
+* issue 698 Make date and time fields customizable
+* issue 556 Preference to display patch sets in reverse order
+* issue 584 Allow deleted and/or uncommented files to be skipped
+
+* Use HistogramDiff for content differences
++
+HistogramDiff is an adaptation of Bram Cohen's Patience Difference
+algorithm, and was recently included in the upstream JGit project.
+Patience Difference tends to produce more readable differences for
+source code files, and JGit's HistogramDiff implementation tends to
+run several times faster than the prior Myers O(ND) algorithm.
+
+* Automatic merge file content during submit
++
+Project owners can now enable file-level content merge during submit,
+allowing Gerrit to automatically resolve many path conflict cases.
+This is built upon experimental merge code inherited from JGit,
+and is therefore still experimental in Gerrit.
+
+Change Query
+~~~~~~~~~~~~
+* issue 688 Match branch, topic, project, ref by regular expressions
++
+Similar to other features in Gerrit Code Review, starting any of these
+expressions with \^ will now treat the argument as a regular
+expression instead of an exact string match.
+
+* Search changes by commit messages with `message:` operator.
+
+* issue 729 query: Add a \--all-approvals option to queries
++
+The new flag includes approval information for all patch sets in the
+resulting query output.
+
+Notifications
+~~~~~~~~~~~~ 
+* Customize email notification templates
++
+Email notifications are now driven by the Velocity template engine,
+and may be modified by the site administrator by editing a template
+file under `'$site_path'/etc/mail`.
+
+* issue 311 Clarify email texts/subject
++
+The default email notification formatting was changed to make the
+subject lines and message bodies more consistent, and easier to
+understand.
+
+* issue 204 Add project list popup under Settings > Watched Projects
++
+The project list panel makes it easier for users to browse all
+projects they have at least READ +1 access to, and add them to their
+watched project set so notifications can be configured.
+
+* stream-event support for all ref-update events
++
+Whenever a ref is updated via either a direct push to a branch or a
+Gerrit change submission, Gerrit will now send a new "ref-updated"
+event to the event stream.
+
+User Management
+~~~~~~~~~~~~~~~
+* SSO via client SSL certificates
++
+A new auth.type of CLIENT_SSL_CERT_LDAP supports authenticating users
+using client SSL certificates.  This feature requires using the
+embedded Jetty web server with SSL enabled, and an LDAP directory to
+lookup individual account information.
+
+* issue 503 Inactive acounts may be disabled.
++
+Administrators can manually update the accounts table, setting
+inactive = `Y` to mark user accounts inactive.  Inactive accounts
+cannot sign-in, cannot be added as a reviewer, and cannot be added
+to a group.
+
+* Improve the no-interactive-shell error message over SSH
++
+Instead of giving a short 'no shell available' error, Gerrit Code
+Review now prints a banner letting the user know they have
+authenticated successfully, interactive shells are disabled, and how
+to clone a hosted project:
++
+----
+$ ssh -p 29418 review.example.com
+
+  ****    Welcome to Gerrit Code Review    ****
+
+  Hi A. U. Thor, you have successfully connected over SSH.
+
+  Unfortunately, interactive shells are disabled.
+  To clone a hosted Git repository, use:
+
+  git clone ssh://author@review.example.com:29418/REPOSITORY_NAME.git
+
+Connection to review.example.com closed.
+----
+
+* Configure SSHD maxAuthTries, loginGraceTime, maxConnectionsPerUser
++
+The internal SSH daemon now supports additional configuration
+settings to reduce the risk of abuse.
+
+Administration
+~~~~~~~~~~~~~~
+* issue 558 Allow Access rights to be edited by clicking on them.
+
+* New 'Project Owner' system group to define default rights
++
+The new system group 'Project Owners' can be used in access
+rights to mean any user that is a member of any group that
+has the 'Owner' access category granted within that project.
+This system group is primarily useful in higher level projects
+such as '\-- All Projects \--' to define standard access rights
+for all project owners.
+
+* issue 557 Allow rejection of changes without Change-Id line.
++
+Project owners can set a flag to require all commits to include
+the Gerrit specific 'Change-Id: I...' line during initial upload,
+reducing the risk of confusion when amends need to occur to
+incorporate reviewer feedback.
+
+* issue 613 create-project: Add --permissions-only option
++
+The new flag skips creating the associated Git repository, making the
+new project suitable for use as a parent to inherit permissions from.
+
+* create-project: Optionally create empty initial commit
++
+The `repo` tool used by Android doesn't like to clone an empty Git
+repository, making it difficult to setup a review for the initial file
+contents.  create-project can now optionally create an empty initial
+commit, permitting repo to sync the empty project.
+
+* Block off commands on a server for certain user groups.
++
+The upload.allowGroup and receive.allowGroup settings in gerrit.config
+can be used to restrict which users can perform git clone/fetch or git
+push on this server.  This can be useful if clone/fetch should be
+limited to only site administrators, while normal users are supposed
+to use to less expensive mirror servers.
+
+* issue 685 Define gerrit.replicateOnStartup to control replication
++
+The automatic replicate every project action that occurs during server
+startup can now be disabled by setting replicateOnStartup = false.
+This is primarily useful for sites with extremely large numbers of
+projects and replication targets, but runs the risk of having a target
+be out of date relative to the master server.
+
+* New non-blocking function category "NoBlock"
++
+Site defined approval categories may now use the function "NoBlock"
+to permit scoring without blocking submission.  This is mostly
+useful for automated tools to provide optional feedback on a change.
+
+* Ability to reject commits from entering repository
++
+The Git-note style branch `refs/meta/reject-commits` can be created
+by the project owner or site administrator to define a list of
+commits that must not be pushed into the repository.  This can be
+useful after performing a project-wide filter-branch operation to
+prevent the older (pre-filter-branch) history from being reintroduced
+into the repository.
+
+Bug Fixes
+---------
+
+Web UI
+~~~~~~
+* issue 498 Enable Keyboard navigation after change submit
+* issue 691 Make ']' on last file go up to change
+* issue 741 Make ENTER work for 'Create Group'
+* issue 622 Denote a symbolic link in side-by-side viewer
+* issue 612 Display empty branch list when project has no repository
+* issue 672 Fix deleting exclusive branch level rights
+* issue 645 Display 'No difference' between unchanged patchsets
+* Display groups as links to group information
+* Remove ctrl-d keybinding to discard comment, honor browser default
+* Do not auto enable save buttons, wait for changes to be made
+* Disable 'Create Group' button if group name not entered
+* Show commit message in PatchScreen if old patch sets are compared
+* Fixed a number of focus and shortcut bugs in Firefox, Chrome
+
+* issue 487 Work around buggy MyersDiff by killing threads
++
+MyersDiff sometimes locked up in an infinite loop when computing
+the intraline difference information for a file.  These threads
+are now killed after an administrator specified timeout
+(cache.diff_intraline.timeout, default is 5 seconds).  If the
+timeout is reached the file content is displayed without intraline
+differences.  This offers reduced functionality to the end-user, but
+prevents the "path of death" which usually took down a Gerrit server.
+
+* Hide access rights not visible to user
++
+Users were able to view access rights for branches they didn't
+actually have READ +1 permission on.  This may have leaked
+information about branches and/or groups to users that shouldn't
+know about code names contained within either string.  Users that
+are not project owners may now only view access rights for branches
+they have at least READ +1 permission on.
+
+Change Query
+~~~~~~~~~~~~
+* issue 689 Fix age:4days to parse correctly
+* Make branch: operator slightly less ambiguous
+
+Push Support
+~~~~~~~~~~~~
+* issue 695 Permit changing only the author of a commit
++
+Correcting only the author of a change failed to upload the new patch
+set onto the existing change, as neither the message nor the files
+were modified.  Fixed.
+
+* issue 576 Allow Push Branch +3 to force replace a tag
++
+Previously it was not possible to replace a tag object, even if
+`git push \--force` was used.  Fixed.
+
+* issue 690 Refuse to run receive-pack if refs/for/branch exists
++
+If a server repository was corrupted by an administrator manually
+creating a reference within the magical refs/for/ namespace, Gerrit
+became confused when changes were uploaded for review.  If this case
+occurs push now aborts very early, with a clear error message
+indicating the problem.  To recover an administrator must clear the
+refs/for/ namespace manually.
+
+* Allow receive-pack without Read +2 but with Push Head +1
++
+Users who had direct branch push permission but lacked the ability to
+create changes for review were unable to push to a project.  Fixed.
+This (finally) makes Gerrit a replacement for Gitosis or Gitolite.
+
+Replication
+~~~~~~~~~~~
+* issue 683 Don't assume authGroup = "Registered Users" in replication
++
+Previously a misconfigured authGroup in replication.config may have
+caused the server to assume "Registered Users" instead of the group(s)
+admin actually wanted.  This may have caused the replication to see
+(or not see) the correct set of projects.
+
+* issue 482 Upon replication fail, automatically retry later
++
+If replication fails (for example due to temporary network
+connectivity problems), other pending replication events to the
+same server are deferred and retried later until successful.
+
+* Replicate all refs received from push
++
+Replication now replicates all references, not just those that
+appear under `refs/heads`, `refs/tags`, or `refs/changes`.  This
+fix may be relevant if the server supports user-private sandboxes
+such as `refs/dev/'$\{username\}'/*`.
+
+* issue 658 Allow refspec shortcuts (push = master) for replication
+
+User Management
+~~~~~~~~~~~~~~~
+* Ensure proper escaping of LDAP group names
++
+Some special characters may appear in LDAP group names, these must be
+escape when looking up the group information from JNDI, otherwise the
+lookup fails.  Fixed by applying the necessary escape sequences.
+
+* Let login fail if user name cannot be set
++
+If the user name for a new account is supposed to import from LDAP
+but cannot because it is already in use by another user on this
+server, the new account won't be created.
+
+Administration
+~~~~~~~~~~~~~~
+* gerrit.sh: actually verify running processes
++
+Previously `gerrit.sh check` claimed a server was running if the
+pid file was present, even if the process itself was dead.  It now
+checks `ps` for the process before claiming it is running.
+
+* Don't allow exclusive branch rights to block Owner inheritance
++
+Exclusive branch level rights prevented the a higher level branch
+owner from managing the branch rights, unless they had an additional
+access right for the exclusive rights.  Now Owner inheritance cannot
+be blocked, ensuring that the higher level owner can manage their
+entire namespace.
+
+* Allow overriding permissions from parent project
++
+Permissions in the parent project could not be overridden in the
+child project.  Permissions can now be overidden if the category,
+group name and reference name all match.
+
+Version
+-------
+ef16a1816f293d00c33de9f90470021e2468a709
diff --git a/ReleaseNotes/index.txt b/ReleaseNotes/index.txt
index 985c0dd..bfb4625 100644
--- a/ReleaseNotes/index.txt
+++ b/ReleaseNotes/index.txt
@@ -4,6 +4,7 @@
 [[2_1]]
 Version 2.1.x
 -------------
+* link:ReleaseNotes-2.1.6.html[2.1.6]
 * link:ReleaseNotes-2.1.5.html[2.1.5]
 * link:ReleaseNotes-2.1.4.html[2.1.4]
 * link:ReleaseNotes-2.1.3.html[2.1.3]
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupAdminService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupAdminService.java
index b83bc7d..f4fa721 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupAdminService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupAdminService.java
@@ -47,7 +47,7 @@
 
   @SignInRequired
   void renameGroup(AccountGroup.Id groupId, String newName,
-      AsyncCallback<VoidResult> callback);
+      AsyncCallback<GroupDetail> callback);
 
   @SignInRequired
   void changeGroupType(AccountGroup.Id groupId, AccountGroup.Type newType,
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
index e24405f..24cdee4 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
@@ -55,6 +55,7 @@
   protected List<Patch> history;
   protected boolean hugeFile;
   protected boolean intralineDifference;
+  protected boolean intralineFailure;
 
   public PatchScript(final Change.Key ck, final ChangeType ct, final String on,
       final String nn, final FileMode om, final FileMode nm,
@@ -62,7 +63,7 @@
       final SparseFileContent ca, final SparseFileContent cb,
       final List<Edit> e, final DisplayMethod ma, final DisplayMethod mb,
       final CommentDetail cd, final List<Patch> hist, final boolean hf,
-      final boolean id) {
+      final boolean id, final boolean idf) {
     changeId = ck;
     changeType = ct;
     oldName = on;
@@ -80,6 +81,7 @@
     history = hist;
     hugeFile = hf;
     intralineDifference = id;
+    intralineFailure = idf;
   }
 
   protected PatchScript() {
@@ -149,6 +151,10 @@
     return intralineDifference;
   }
 
+  public boolean hasIntralineFailure() {
+    return intralineFailure;
+  }
+
   public SparseFileContent getA() {
     return a;
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java
index be808fa..b3bd0cb 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java
@@ -134,9 +134,9 @@
     table.setWidget(row, 1, new CheckBox());
     table.setWidget(row, 2, fp);
 
-    addNotifyButton(AccountProjectWatch.Type.NEW_CHANGES, info, row, 3);
-    addNotifyButton(AccountProjectWatch.Type.COMMENTS, info, row, 4);
-    addNotifyButton(AccountProjectWatch.Type.SUBMITS, info, row, 5);
+    addNotifyButton(AccountProjectWatch.NotifyType.NEW_CHANGES, info, row, 3);
+    addNotifyButton(AccountProjectWatch.NotifyType.ALL_COMMENTS, info, row, 4);
+    addNotifyButton(AccountProjectWatch.NotifyType.SUBMITTED_CHANGES, info, row, 5);
 
     final FlexCellFormatter fmt = table.getFlexCellFormatter();
     fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().iconCell());
@@ -148,7 +148,7 @@
     setRowItem(row, info);
   }
 
-  protected void addNotifyButton(final AccountProjectWatch.Type type,
+  protected void addNotifyButton(final AccountProjectWatch.NotifyType type,
       final AccountProjectWatchInfo info, final int row, final int col) {
     final CheckBox cbox = new CheckBox();
 
@@ -157,13 +157,16 @@
       public void onClick(final ClickEvent event) {
         final boolean oldVal = info.getWatch().isNotify(type);
         info.getWatch().setNotify(type, cbox.getValue());
+        cbox.setEnabled(false);
         Util.ACCOUNT_SVC.updateProjectWatch(info.getWatch(),
             new GerritCallback<VoidResult>() {
               public void onSuccess(final VoidResult result) {
+                cbox.setEnabled(true);
               }
 
               @Override
               public void onFailure(final Throwable caught) {
+                cbox.setEnabled(true);
                 info.getWatch().setNotify(type, oldVal);
                 cbox.setValue(oldVal);
                 super.onFailure(caught);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java
index 389f22b..fd06cdb 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java
@@ -124,10 +124,10 @@
       public void onClick(final ClickEvent event) {
         final String newName = groupNameTxt.getText().trim();
         Util.GROUP_SVC.renameGroup(groupId, newName,
-            new GerritCallback<VoidResult>() {
-              public void onSuccess(final VoidResult result) {
+            new GerritCallback<GroupDetail>() {
+              public void onSuccess(final GroupDetail groupDetail) {
                 saveName.setEnabled(false);
-                setPageTitle(Util.M.group(newName));
+                display(groupDetail);
               }
             });
       }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java
index 93e8deb..dbbc9c3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java
@@ -31,6 +31,7 @@
 
   String patchHistoryTitle();
   String disabledOnLargeFiles();
+  String intralineFailure();
 
   String upToChange();
   String linePrev();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties
index 90def1d..590007d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties
@@ -13,6 +13,7 @@
 patchHeaderNew = New Version
 patchHistoryTitle = Patch History
 disabledOnLargeFiles = Disabled on very large source files.
+intralineFailure = Intraline difference not available due to server error.
 
 upToChange = Up to change
 linePrev = Previous line
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java
index 812a74e..cbe037b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.client.patches;
 
 import com.google.gerrit.client.Dispatcher;
+import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.RpcStatus;
 import com.google.gerrit.client.changes.CommitMessageBlock;
@@ -140,6 +141,7 @@
   /** Keys that cause an action on this screen */
   private KeyCommandSet keysNavigation;
   private HandlerRegistration regNavigation;
+  private boolean intralineFailure;
 
   /**
    * How this patch should be displayed in the patch screen.
@@ -461,6 +463,17 @@
       settingsPanel.getReviewedCheckBox().setValue(true);
       setReviewedByCurrentUser(true /* reviewed */);
     }
+
+    intralineFailure = isFirst && script.hasIntralineFailure();
+  }
+
+  @Override
+  public void onShowView() {
+    super.onShowView();
+    if (intralineFailure) {
+      intralineFailure = false;
+      new ErrorDialog(PatchUtil.C.intralineFailure()).show();
+    }
   }
 
   private void showPatch(final boolean showPatch) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectServlet.java
index 09e2cce..8c1e5df 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectServlet.java
@@ -198,7 +198,7 @@
       UploadPack up = new UploadPack(repo);
       up.setPackConfig(packConfig);
       if (!pc.allRefsAreVisible()) {
-        up.setRefFilter(new VisibleRefFilter(repo, pc, db.get()));
+        up.setRefFilter(new VisibleRefFilter(repo, pc, db.get(), true));
       }
       return up;
     }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java
index 870d77c..80fe678 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java
@@ -163,7 +163,7 @@
   }
 
   public void renameGroup(final AccountGroup.Id groupId, final String newName,
-      final AsyncCallback<VoidResult> callback) {
+      final AsyncCallback<GroupDetail> callback) {
     renameGroupFactory.create(groupId, newName).to(callback);
   }
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/RenameGroup.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/RenameGroup.java
index 63b5bce..19cd29f 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/RenameGroup.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/RenameGroup.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.httpd.rpc.account;
 
+import com.google.gerrit.common.data.GroupDetail;
 import com.google.gerrit.common.errors.NameAlreadyUsedException;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.AccountGroup;
@@ -22,7 +23,6 @@
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.account.NoSuchGroupException;
-import com.google.gwtjsonrpc.client.VoidResult;
 import com.google.gwtorm.client.OrmDuplicateKeyException;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
@@ -30,7 +30,7 @@
 
 import java.util.Collections;
 
-class RenameGroup extends Handler<VoidResult> {
+class RenameGroup extends Handler<GroupDetail> {
   interface Factory {
     RenameGroup create(AccountGroup.Id id, String newName);
   }
@@ -38,6 +38,7 @@
   private final ReviewDb db;
   private final GroupCache groupCache;
   private final GroupControl.Factory groupControlFactory;
+  private final GroupDetailFactory.Factory groupDetailFactory;
 
   private final AccountGroup.Id groupId;
   private final String newName;
@@ -45,18 +46,18 @@
   @Inject
   RenameGroup(final ReviewDb db, final GroupCache groupCache,
       final GroupControl.Factory groupControlFactory,
-
+      final GroupDetailFactory.Factory groupDetailFactory,
       @Assisted final AccountGroup.Id groupId, @Assisted final String newName) {
     this.db = db;
     this.groupCache = groupCache;
     this.groupControlFactory = groupControlFactory;
-
+    this.groupDetailFactory = groupDetailFactory;
     this.groupId = groupId;
     this.newName = newName;
   }
 
   @Override
-  public VoidResult call() throws OrmException, NameAlreadyUsedException,
+  public GroupDetail call() throws OrmException, NameAlreadyUsedException,
       NoSuchGroupException {
     final GroupControl ctl = groupControlFactory.validateFor(groupId);
     final AccountGroup group = db.accountGroups().get(groupId);
@@ -75,7 +76,7 @@
       //
       AccountGroupName other = db.accountGroupNames().get(key);
       if (other != null && other.getId().equals(groupId)) {
-        return VoidResult.INSTANCE;
+        return groupDetailFactory.create(groupId).call();
       }
 
       // Otherwise, someone else has this identity.
@@ -94,6 +95,6 @@
     groupCache.evict(group);
     groupCache.evictAfterRename(old);
 
-    return VoidResult.INSTANCE;
+    return groupDetailFactory.create(groupId).call();
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptBuilder.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptBuilder.java
index 30d7716..302135e 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptBuilder.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptBuilder.java
@@ -20,12 +20,14 @@
 import com.google.gerrit.prettify.common.EditList;
 import com.google.gerrit.prettify.common.SparseFileContent;
 import com.google.gerrit.reviewdb.AccountDiffPreference;
+import com.google.gerrit.reviewdb.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.Patch;
 import com.google.gerrit.reviewdb.PatchLineComment;
-import com.google.gerrit.reviewdb.AccountDiffPreference.Whitespace;
+import com.google.gerrit.reviewdb.Project;
 import com.google.gerrit.server.FileTypeRegistry;
 import com.google.gerrit.server.patch.IntraLineDiff;
+import com.google.gerrit.server.patch.IntraLineDiffKey;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListEntry;
 import com.google.gerrit.server.patch.Text;
@@ -65,6 +67,7 @@
   };
 
   private Repository db;
+  private Project.NameKey projectKey;
   private ObjectReader reader;
   private Change change;
   private AccountDiffPreference diffPrefs;
@@ -88,8 +91,9 @@
     patchListCache = plc;
   }
 
-  void setRepository(final Repository r) {
-    db = r;
+  void setRepository(Repository r, Project.NameKey projectKey) {
+    this.db = r;
+    this.projectKey = projectKey;
   }
 
   void setChange(final Change c) {
@@ -127,7 +131,8 @@
   private PatchScript build(final PatchListEntry content,
       final CommentDetail comments, final List<Patch> history)
       throws IOException {
-    boolean intralineDifference = diffPrefs.isIntralineDifference();
+    boolean intralineDifferenceIsPossible = true;
+    boolean intralineFailure = false;
 
     a.path = oldName(content);
     b.path = newName(content);
@@ -137,16 +142,31 @@
 
     edits = new ArrayList<Edit>(content.getEdits());
 
-    if (intralineDifference) {
-      if (isModify(content)) {
-        IntraLineDiff d = patchListCache.get(a.id, a.src, b.id, b.src, edits);
-        if (d != null) {
-          edits = new ArrayList<Edit>(d.getEdits());
-        } else {
-          intralineDifference = false;
+    if (!isModify(content)) {
+      intralineDifferenceIsPossible = false;
+    } else if (diffPrefs.isIntralineDifference()) {
+      IntraLineDiff d =
+          patchListCache.getIntraLineDiff(new IntraLineDiffKey(a.id, a.src,
+              b.id, b.src, edits, projectKey, bId, b.path));
+      if (d != null) {
+        switch (d.getStatus()) {
+          case EDIT_LIST:
+            edits = new ArrayList<Edit>(d.getEdits());
+            break;
+
+          case DISABLED:
+            intralineDifferenceIsPossible = false;
+            break;
+
+          case ERROR:
+          case TIMEOUT:
+            intralineDifferenceIsPossible = false;
+            intralineFailure = true;
+            break;
         }
       } else {
-        intralineDifference = false;
+        intralineDifferenceIsPossible = false;
+        intralineFailure = true;
       }
     }
 
@@ -187,10 +207,11 @@
       packContent(diffPrefs.getIgnoreWhitespace() != Whitespace.IGNORE_NONE);
     }
 
-    return new PatchScript(change.getKey(), content.getChangeType(), content
-        .getOldName(), content.getNewName(), a.fileMode, b.fileMode, content
-        .getHeaderLines(), diffPrefs, a.dst, b.dst, edits, a.displayMethod,
-        b.displayMethod, comments, history, hugeFile, intralineDifference);
+    return new PatchScript(change.getKey(), content.getChangeType(),
+        content.getOldName(), content.getNewName(), a.fileMode, b.fileMode,
+        content.getHeaderLines(), diffPrefs, a.dst, b.dst, edits,
+        a.displayMethod, b.displayMethod, comments, history, hugeFile,
+        intralineDifferenceIsPossible, intralineFailure);
   }
 
   private static boolean isModify(PatchListEntry content) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptFactory.java
index 377cf49..fcaa0c5 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptFactory.java
@@ -173,7 +173,7 @@
   private PatchScriptBuilder newBuilder(final PatchList list, Repository git) {
     final AccountDiffPreference dp = new AccountDiffPreference(diffPrefs);
     final PatchScriptBuilder b = builderFactory.get();
-    b.setRepository(git);
+    b.setRepository(git, projectKey);
     b.setChange(change);
     b.setDiffPrefs(dp);
     b.setTrees(list.isAgainstParent(), list.getOldId(), list.getNewId());
diff --git a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/gerrit.sh b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/gerrit.sh
index 6adab03..96e7dbb 100755
--- a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/gerrit.sh
+++ b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/gerrit.sh
@@ -425,6 +425,17 @@
       fi
     fi
 
+    if test $UID = 0; then
+        PID=`cat "$GERRIT_PID"`
+        if test -f "/proc/${PID}/oom_score_adj" ; then
+            echo -1000 > "/proc/${PID}/oom_score_adj"
+        else
+            if test -f "/proc/${PID}/oom_adj" ; then
+                echo -16 > "/proc/${PID}/oom_adj"
+            fi
+        fi
+    fi
+
     TIMEOUT=90  # seconds
     sleep 1
     while running "$GERRIT_PID" && test $TIMEOUT -gt 0 ; do
@@ -522,8 +533,10 @@
     echo
 
     if test -f "$GERRIT_PID" ; then
-        echo "Gerrit running pid="`cat "$GERRIT_PID"`
-        exit 0
+        if running "$GERRIT_PID" ; then
+            echo "Gerrit running pid="`cat "$GERRIT_PID"`
+            exit 0
+        fi
     fi
     exit 1
   ;;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountProjectWatch.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountProjectWatch.java
index 6713d8f..c18ae82 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountProjectWatch.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountProjectWatch.java
@@ -21,8 +21,8 @@
 /** An {@link Account} interested in a {@link Project}. */
 public final class AccountProjectWatch {
 
-  public enum Type {
-    NEW_CHANGES, SUBMITS, COMMENTS
+  public enum NotifyType {
+    NEW_CHANGES, ALL_COMMENTS, SUBMITTED_CHANGES
   }
 
   public static final String FILTER_ALL = "*";
@@ -124,46 +124,32 @@
     return FILTER_ALL.equals(key.filter.get()) ? null : key.filter.get();
   }
 
-  public boolean isNotifyNewChanges() {
-    return notifyNewChanges;
-  }
+  public boolean isNotify(final NotifyType type) {
+    switch (type) {
+      case NEW_CHANGES:
+        return notifyNewChanges;
 
-  public void setNotifyNewChanges(final boolean a) {
-    notifyNewChanges = a;
-  }
+      case ALL_COMMENTS:
+        return notifyAllComments;
 
-  public boolean isNotifyAllComments() {
-    return notifyAllComments;
-  }
-
-  public void setNotifyAllComments(final boolean a) {
-    notifyAllComments = a;
-  }
-
-  public boolean isNotifySubmittedChanges() {
-    return notifySubmittedChanges;
-  }
-
-  public void setNotifySubmittedChanges(final boolean a) {
-    notifySubmittedChanges = a;
-  }
-
-  public boolean isNotify(final Type type) {
-    switch(type) {
-      case NEW_CHANGES: return notifySubmittedChanges;
-      case SUBMITS:     return notifyNewChanges;
-      case COMMENTS:    return notifyAllComments;
+      case SUBMITTED_CHANGES:
+        return notifySubmittedChanges;
     }
     return false;
   }
 
-  public void setNotify(final Type type, final boolean v) {
-    switch(type) {
-      case NEW_CHANGES: notifySubmittedChanges = v;
+  public void setNotify(final NotifyType type, final boolean v) {
+    switch (type) {
+      case NEW_CHANGES:
+        notifyNewChanges = v;
         break;
-      case SUBMITS:     notifyNewChanges = v;
+
+      case ALL_COMMENTS:
+        notifyAllComments = v;
         break;
-      case COMMENTS:    notifyAllComments = v;
+
+      case SUBMITTED_CHANGES:
+        notifySubmittedChanges = v;
         break;
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitProjectImporter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitProjectImporter.java
index 33661ab..4a33999 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitProjectImporter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitProjectImporter.java
@@ -79,6 +79,13 @@
 
       if (FileKey.isGitRepository(f, FS.DETECTED)) {
         if (name.equals(".git")) {
+          if ("".equals(prefix)) {
+            // If the git base path is itself a git repository working
+            // directory, this is a bit nonsensical for Gerrit Code Review.
+            // Skip the path and do the next one.
+            messages.warning("Skipping " + f.getAbsolutePath());
+            continue;
+          }
           name = prefix.substring(0, prefix.length() - 1);
 
         } else if (name.endsWith(".git")) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java
index 369602a..4ac55ba 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java
@@ -30,6 +30,9 @@
  * environment.
  */
 public interface GitRepositoryManager {
+  /** Note tree listing commits we refuse {@code refs/meta/reject-commits} */
+  public static final String REF_REJECT_COMMITS = "refs/meta/reject-commits";
+
   /**
    * Get (or open) a repository by name.
    *
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushOp.java
index 1c100df..043e7c4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushOp.java
@@ -296,7 +296,7 @@
         return Collections.emptyList();
       }
       try {
-        local = new VisibleRefFilter(db, pc, meta).filter(local);
+        local = new VisibleRefFilter(db, pc, meta, true).filter(local);
       } finally {
         meta.close();
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index 10efeb1..d9a00890 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -161,6 +161,7 @@
   private final Map<RevCommit, ReplaceRequest> replaceByCommit =
       new HashMap<RevCommit, ReplaceRequest>();
 
+  private Collection<ObjectId> existingObjects;
   private Map<ObjectId, Ref> refsById;
 
   private String destTopicName;
@@ -207,8 +208,9 @@
 
     if (!projectControl.allRefsAreVisible()) {
       rp.setCheckReferencedObjectsAreReachable(true);
-      rp.setRefFilter(new VisibleRefFilter(repo, projectControl, db));
+      rp.setRefFilter(new VisibleRefFilter(repo, projectControl, db, false));
     }
+    rp.setRefFilter(new ReceiveCommitsRefFilter(rp.getRefFilter()));
 
     rp.setPreReceiveHook(this);
     rp.setPostReceiveHook(this);
@@ -300,8 +302,8 @@
           }
         }
 
-        if (isHead(c) || isTag(c)) {
-          // We only schedule heads and tags for replication.
+        if (!c.getRefName().startsWith(NEW_CHANGE)) {
+          // We only schedule direct refs updates for replication.
           // Change refs are scheduled when they are created.
           //
           replication.scheduleUpdate(project.getNameKey(), c.getRefName());
@@ -728,18 +730,18 @@
    * @throws IOException the map cannot be loaded.
    */
   private NoteMap loadRejectCommitsMap() throws IOException {
-    String rejectNotes = "refs/meta/reject-commits";
     try {
-      Ref ref = repo.getRef(rejectNotes);
+      Ref ref = repo.getRef(GitRepositoryManager.REF_REJECT_COMMITS);
       if (ref == null) {
-        return null;
+        return NoteMap.newEmptyMap();
       }
 
       RevWalk rw = rp.getRevWalk();
       RevCommit map = rw.parseCommit(ref.getObjectId());
       return NoteMap.read(rw.getObjectReader(), map);
     } catch (IOException badMap) {
-      throw new IOException("Cannot load " + rejectNotes, badMap);
+      throw new IOException("Cannot load "
+          + GitRepositoryManager.REF_REJECT_COMMITS, badMap);
     }
   }
 
@@ -808,9 +810,9 @@
     walk.sort(RevSort.REVERSE, true);
     try {
       walk.markStart(walk.parseCommit(newChange.getNewId()));
-      for (final Ref r : rp.getAdvertisedRefs().values()) {
+      for (ObjectId id : existingObjects()) {
         try {
-          walk.markUninteresting(walk.parseCommit(r.getObjectId()));
+          walk.markUninteresting(walk.parseCommit(id));
         } catch (IOException e) {
           continue;
         }
@@ -1412,9 +1414,9 @@
     walk.sort(RevSort.NONE);
     try {
       walk.markStart(walk.parseCommit(cmd.getNewId()));
-      for (final Ref r : rp.getAdvertisedRefs().values()) {
+      for (ObjectId id : existingObjects()) {
         try {
-          walk.markUninteresting(walk.parseCommit(r.getObjectId()));
+          walk.markUninteresting(walk.parseCommit(id));
         } catch (IOException e) {
           continue;
         }
@@ -1432,6 +1434,17 @@
     }
   }
 
+  private Collection<ObjectId> existingObjects() {
+    if (existingObjects == null) {
+      Map<String, Ref> refs = repo.getAllRefs();
+      existingObjects = new ArrayList<ObjectId>(refs.size());
+      for (Ref r : refs.values()) {
+        existingObjects.add(r.getObjectId());
+      }
+    }
+    return existingObjects;
+  }
+
   private boolean validCommit(final RefControl ctl, final ReceiveCommand cmd,
       final RevCommit c) throws MissingObjectException, IOException {
     rp.getRevWalk().parseBody(c);
@@ -1507,7 +1520,7 @@
     }
 
     // Check for banned commits to prevent them from entering the tree again.
-    if (rejectCommits != null && rejectCommits.contains(c)) {
+    if (rejectCommits.contains(c)) {
       reject(newChange, "contains banned commit " + c.getName());
       return false;
     }
@@ -1624,10 +1637,10 @@
     sendMergedEmail(result);
   }
 
-  private Map<ObjectId, Ref> changeRefsById() {
+  private Map<ObjectId, Ref> changeRefsById() throws IOException {
     if (refsById == null) {
       refsById = new HashMap<ObjectId, Ref>();
-      for (final Ref r : repo.getAllRefs().values()) {
+      for (Ref r : repo.getRefDatabase().getRefs("refs/changes/").values()) {
         if (PatchSet.isRef(r.getName())) {
           refsById.put(r.getObjectId(), r);
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsRefFilter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsRefFilter.java
new file mode 100644
index 0000000..730305c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsRefFilter.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2010 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.git;
+
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.transport.RefFilter;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/** Exposes only the non refs/changes/ reference names. */
+class ReceiveCommitsRefFilter implements RefFilter {
+  private final RefFilter base;
+
+  public ReceiveCommitsRefFilter(RefFilter base) {
+    this.base = base != null ? base : RefFilter.DEFAULT;
+  }
+
+  @Override
+  public Map<String, Ref> filter(Map<String, Ref> refs) {
+    HashMap<String, Ref> r = new HashMap<String, Ref>();
+    for (Map.Entry<String, Ref> e : refs.entrySet()) {
+      if (!e.getKey().startsWith("refs/changes/")) {
+        r.put(e.getKey(), e.getValue());
+      }
+    }
+    return base.filter(r);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
index ffc3fd8..8e4d8ea 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
@@ -52,12 +52,15 @@
   private final Repository db;
   private final ProjectControl projectCtl;
   private final ReviewDb reviewDb;
+  private final boolean showChanges;
 
   public VisibleRefFilter(final Repository db,
-      final ProjectControl projectControl, final ReviewDb reviewDb) {
+      final ProjectControl projectControl, final ReviewDb reviewDb,
+      final boolean showChanges) {
     this.db = db;
     this.projectCtl = projectControl;
     this.reviewDb = reviewDb;
+    this.showChanges = showChanges;
   }
 
   @Override
@@ -99,6 +102,10 @@
   }
 
   private Set<Change.Id> visibleChanges() {
+    if (!showChanges) {
+      return Collections.emptySet();
+    }
+
     final Project project = projectCtl.getProject();
     try {
       final Set<Change.Id> visibleChanges = new HashSet<Change.Id>();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
index 71712af..0377291 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.reviewdb.PatchSetApproval;
 import com.google.gerrit.reviewdb.PatchSetInfo;
 import com.google.gerrit.reviewdb.StarredChange;
+import com.google.gerrit.reviewdb.AccountProjectWatch.NotifyType;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListEntry;
@@ -299,7 +300,7 @@
       // BCC anyone else who has interest in this project's changes
       //
       for (final AccountProjectWatch w : getWatches()) {
-        if (w.isNotifyAllComments()) {
+        if (w.isNotify(NotifyType.ALL_COMMENTS)) {
           add(RecipientType.BCC, w.getAccountId());
         }
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
index 18bfe976..c14ff1b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.reviewdb.AccountGroupMember;
 import com.google.gerrit.reviewdb.AccountProjectWatch;
 import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.AccountProjectWatch.NotifyType;
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
@@ -61,7 +62,7 @@
       // BCC anyone who has interest in this project's changes
       //
       for (final AccountProjectWatch w : getWatches()) {
-        if (w.isNotifyNewChanges()) {
+        if (w.isNotify(NotifyType.NEW_CHANGES)) {
           if (owners.contains(w.getAccountId())) {
             add(RecipientType.TO, w.getAccountId());
           } else {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
index 40f4790..b2a1c44 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.reviewdb.Branch;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.PatchSetApproval;
+import com.google.gerrit.reviewdb.AccountProjectWatch.NotifyType;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -149,7 +150,7 @@
       // BCC anyone else who has interest in this project's changes
       //
       for (final AccountProjectWatch w : getWatches()) {
-        if (w.isNotifySubmittedChanges()) {
+        if (w.isNotify(NotifyType.SUBMITTED_CHANGES)) {
           add(RecipientType.BCC, w.getAccountId());
         }
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiff.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiff.java
index f8f339e..3805f8f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiff.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiff.java
@@ -14,9 +14,13 @@
 
 package com.google.gerrit.server.patch;
 
+import static com.google.gerrit.server.ioutil.BasicSerialization.readEnum;
 import static com.google.gerrit.server.ioutil.BasicSerialization.readVarInt32;
+import static com.google.gerrit.server.ioutil.BasicSerialization.writeEnum;
 import static com.google.gerrit.server.ioutil.BasicSerialization.writeVarInt32;
 
+import com.google.gerrit.reviewdb.CodedEnum;
+
 import org.eclipse.jgit.diff.Edit;
 import org.eclipse.jgit.diff.ReplaceEdit;
 
@@ -33,10 +37,36 @@
 public class IntraLineDiff implements Serializable {
   static final long serialVersionUID = IntraLineDiffKey.serialVersionUID;
 
-  private List<Edit> edits;
+  public static enum Status implements CodedEnum {
+    EDIT_LIST('e'), DISABLED('D'), TIMEOUT('T'), ERROR('E');
+
+    private final char code;
+
+    Status(char code) {
+      this.code = code;
+    }
+
+    @Override
+    public char getCode() {
+      return code;
+    }
+  }
+
+  private transient Status status;
+  private transient List<Edit> edits;
+
+  IntraLineDiff(Status status) {
+    this.status = status;
+    this.edits = Collections.emptyList();
+  }
 
   IntraLineDiff(List<Edit> edits) {
-    this.edits = edits;
+    this.status = Status.EDIT_LIST;
+    this.edits = Collections.unmodifiableList(edits);
+  }
+
+  public Status getStatus() {
+    return status;
   }
 
   public List<Edit> getEdits() {
@@ -44,6 +74,7 @@
   }
 
   private void writeObject(final ObjectOutputStream out) throws IOException {
+    writeEnum(out, status);
     writeVarInt32(out, edits.size());
     for (Edit e : edits) {
       writeEdit(out, e);
@@ -61,6 +92,7 @@
   }
 
   private void readObject(final ObjectInputStream in) throws IOException {
+    status = readEnum(in, Status.values());
     int editCount = readVarInt32(in);
     Edit[] editArray = new Edit[editCount];
     for (int i = 0; i < editCount; i++) {
@@ -69,11 +101,13 @@
       int innerCount = readVarInt32(in);
       if (0 < innerCount) {
         Edit[] inner = new Edit[innerCount];
-        for (int j = 0; j < innerCount; j++)
+        for (int j = 0; j < innerCount; j++) {
           inner[j] = readEdit(in);
+        }
         editArray[i] = new ReplaceEdit(editArray[i], toList(inner));
       }
     }
+    edits = toList(editArray);
   }
 
   private static void writeEdit(OutputStream out, Edit e) throws IOException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java
index 899dda5..a8d62fc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java
@@ -17,6 +17,8 @@
 import static org.eclipse.jgit.lib.ObjectIdSerialization.readNotNull;
 import static org.eclipse.jgit.lib.ObjectIdSerialization.writeNotNull;
 
+import com.google.gerrit.reviewdb.Project;
+
 import org.eclipse.jgit.diff.Edit;
 import org.eclipse.jgit.lib.ObjectId;
 
@@ -27,7 +29,7 @@
 import java.util.List;
 
 public class IntraLineDiffKey implements Serializable {
-  static final long serialVersionUID = 1L;
+  static final long serialVersionUID = 3L;
 
   private transient ObjectId aId;
   private transient ObjectId bId;
@@ -38,14 +40,22 @@
   private transient Text bText;
   private transient List<Edit> edits;
 
-  IntraLineDiffKey(ObjectId aId, Text aText, ObjectId bId, Text bText,
-      List<Edit> edits) {
+  private transient Project.NameKey projectKey;
+  private transient ObjectId commit;
+  private transient String path;
+
+  public IntraLineDiffKey(ObjectId aId, Text aText, ObjectId bId, Text bText,
+      List<Edit> edits, Project.NameKey projectKey, ObjectId commit, String path) {
     this.aId = aId;
     this.bId = bId;
 
     this.aText = aText;
     this.bText = bText;
     this.edits = edits;
+
+    this.projectKey = projectKey;
+    this.commit = commit;
+    this.path = path;
   }
 
   Text getTextA() {
@@ -60,6 +70,26 @@
     return edits;
   }
 
+  ObjectId getBlobA() {
+    return aId;
+  }
+
+  ObjectId getBlobB() {
+    return bId;
+  }
+
+  Project.NameKey getProject() {
+    return projectKey;
+  }
+
+  ObjectId getCommit() {
+    return commit;
+  }
+
+  String getPath() {
+    return path;
+  }
+
   @Override
   public int hashCode() {
     int h = 0;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
new file mode 100644
index 0000000..0ac1af2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
@@ -0,0 +1,472 @@
+// Copyright (C) 2009 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.patch;
+
+import com.google.gerrit.server.cache.EntryCreator;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.diff.Edit;
+import org.eclipse.jgit.diff.MyersDiff;
+import org.eclipse.jgit.diff.ReplaceEdit;
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.regex.Pattern;
+
+class IntraLineLoader extends EntryCreator<IntraLineDiffKey, IntraLineDiff> {
+  private static final Logger log = LoggerFactory
+      .getLogger(IntraLineLoader.class);
+
+  private static final Pattern BLANK_LINE_RE = Pattern
+      .compile("^[ \\t]*(|[{}]|/\\*\\*?|\\*)[ \\t]*$");
+
+  private static final Pattern CONTROL_BLOCK_START_RE = Pattern
+      .compile("[{:][ \\t]*$");
+
+  private final BlockingQueue<Worker> workerPool;
+  private final long timeoutMillis;
+
+  @Inject
+  IntraLineLoader(final @GerritServerConfig Config cfg) {
+    final int workers =
+        cfg.getInt("cache", PatchListCacheImpl.INTRA_NAME, "maxIdleWorkers",
+            Runtime.getRuntime().availableProcessors() * 3 / 2);
+    workerPool = new ArrayBlockingQueue<Worker>(workers, true /* fair */);
+
+    timeoutMillis =
+        ConfigUtil.getTimeUnit(cfg, "cache", PatchListCacheImpl.INTRA_NAME,
+            "timeout", TimeUnit.MILLISECONDS.convert(5, TimeUnit.SECONDS),
+            TimeUnit.MILLISECONDS);
+  }
+
+  @Override
+  public IntraLineDiff createEntry(IntraLineDiffKey key) throws Exception {
+    Worker w = workerPool.poll();
+    if (w == null) {
+      w = new Worker();
+    }
+
+    Worker.Result r = w.computeWithTimeout(key, timeoutMillis);
+
+    if (r == Worker.Result.TIMEOUT) {
+      // Don't keep this thread. We have to murder it unsafely, which
+      // means its unable to be reused in the future. Return back a
+      // null result, indicating the cache cannot load this key.
+      //
+      return new IntraLineDiff(IntraLineDiff.Status.TIMEOUT);
+    }
+
+    if (!workerPool.offer(w)) {
+      // If the idle worker pool is full, terminate this thread.
+      //
+      w.end();
+    }
+
+    if (r.error != null) {
+      // If there was an error computing the result, carry it
+      // up to the caller so the cache knows this key is invalid.
+      //
+      throw r.error;
+    }
+
+    return r.diff;
+  }
+
+  private static class Worker {
+    private static final AtomicInteger count = new AtomicInteger(1);
+
+    private final ArrayBlockingQueue<Input> input;
+    private final ArrayBlockingQueue<Result> result;
+    private final Thread thread;
+
+    Worker() {
+      input = new ArrayBlockingQueue<Input>(1);
+      result = new ArrayBlockingQueue<Result>(1);
+
+      thread = new Thread(new Runnable() {
+        public void run() {
+          workerLoop();
+        }
+      });
+      thread.setName("IntraLineDiff-" + count.getAndIncrement());
+      thread.setDaemon(true);
+      thread.start();
+    }
+
+    Result computeWithTimeout(IntraLineDiffKey key, long timeoutMillis)
+        throws Exception {
+      if (!input.offer(new Input(key))) {
+        log.error("Cannot enqueue task to thread " + thread.getName());
+        return null;
+      }
+
+      Result r = result.poll(timeoutMillis, TimeUnit.MILLISECONDS);
+      if (r != null) {
+        return r;
+      } else {
+        log.warn(timeoutMillis + " ms timeout reached for IntraLineDiff"
+            + " in project " + key.getProject().get() //
+            + " on commit " + key.getCommit().name() //
+            + " for path " + key.getPath() //
+            + " comparing " + key.getBlobA().name() //
+            + ".." + key.getBlobB().name() //
+            + ".  Killing " + thread.getName());
+        try {
+          thread.stop();
+        } catch (Throwable error) {
+          // Ignore any reason the thread won't stop.
+          log.error("Cannot stop runaway thread " + thread.getName(), error);
+        }
+        return Result.TIMEOUT;
+      }
+    }
+
+    void end() {
+      if (!input.offer(Input.END_THREAD)) {
+        log.error("Cannot gracefully stop thread " + thread.getName());
+      }
+    }
+
+    private void workerLoop() {
+      try {
+        for (;;) {
+          Input in;
+          try {
+            in = input.take();
+          } catch (InterruptedException e) {
+            log.error("Unexpected interrupt on " + thread.getName());
+            continue;
+          }
+
+          if (in == Input.END_THREAD) {
+            return;
+          }
+
+          Result r;
+          try {
+            r = new Result(IntraLineLoader.compute(in.key));
+          } catch (Exception error) {
+            r = new Result(error);
+          }
+
+          if (!result.offer(r)) {
+            log.error("Cannot return result from " + thread.getName());
+          }
+        }
+      } catch (ThreadDeath iHaveBeenShot) {
+        // Handle thread death by gracefully returning to the caller,
+        // allowing the thread to be destroyed.
+      }
+    }
+
+    private static class Input {
+      static final Input END_THREAD = new Input(null);
+
+      final IntraLineDiffKey key;
+
+      Input(IntraLineDiffKey key) {
+        this.key = key;
+      }
+    }
+
+    static class Result {
+      static final Result TIMEOUT = new Result((IntraLineDiff) null);
+
+      final IntraLineDiff diff;
+      final Exception error;
+
+      Result(IntraLineDiff diff) {
+        this.diff = diff;
+        this.error = null;
+      }
+
+      Result(Exception error) {
+        this.diff = null;
+        this.error = error;
+      }
+    }
+  }
+
+  private static IntraLineDiff compute(IntraLineDiffKey key) throws Exception {
+    List<Edit> edits = new ArrayList<Edit>(key.getEdits());
+    Text aContent = key.getTextA();
+    Text bContent = key.getTextB();
+    combineLineEdits(edits, aContent, bContent);
+
+    for (int i = 0; i < edits.size(); i++) {
+      Edit e = edits.get(i);
+
+      if (e.getType() == Edit.Type.REPLACE) {
+        CharText a = new CharText(aContent, e.getBeginA(), e.getEndA());
+        CharText b = new CharText(bContent, e.getBeginB(), e.getEndB());
+        CharTextComparator cmp = new CharTextComparator();
+
+        List<Edit> wordEdits = MyersDiff.INSTANCE.diff(cmp, a, b);
+
+        // Combine edits that are really close together. If they are
+        // just a few characters apart we tend to get better results
+        // by joining them together and taking the whole span.
+        //
+        for (int j = 0; j < wordEdits.size() - 1;) {
+          Edit c = wordEdits.get(j);
+          Edit n = wordEdits.get(j + 1);
+
+          if (n.getBeginA() - c.getEndA() <= 5
+              || n.getBeginB() - c.getEndB() <= 5) {
+            int ab = c.getBeginA();
+            int ae = n.getEndA();
+
+            int bb = c.getBeginB();
+            int be = n.getEndB();
+
+            if (canCoalesce(a, c.getEndA(), n.getBeginA())
+                && canCoalesce(b, c.getEndB(), n.getBeginB())) {
+              wordEdits.set(j, new Edit(ab, ae, bb, be));
+              wordEdits.remove(j + 1);
+              continue;
+            }
+          }
+
+          j++;
+        }
+
+        // Apply some simple rules to fix up some of the edits. Our
+        // logic above, along with our per-character difference tends
+        // to produce some crazy stuff.
+        //
+        for (int j = 0; j < wordEdits.size(); j++) {
+          Edit c = wordEdits.get(j);
+          int ab = c.getBeginA();
+          int ae = c.getEndA();
+
+          int bb = c.getBeginB();
+          int be = c.getEndB();
+
+          // Sometimes the diff generator produces an INSERT or DELETE
+          // right up against a REPLACE, but we only find this after
+          // we've also played some shifting games on the prior edit.
+          // If that happened to us, coalesce them together so we can
+          // correct this mess for the user. If we don't we wind up
+          // with silly stuff like "es" -> "es = Addresses".
+          //
+          if (1 < j) {
+            Edit p = wordEdits.get(j - 1);
+            if (p.getEndA() == ab || p.getEndB() == bb) {
+              if (p.getEndA() == ab && p.getBeginA() < p.getEndA()) {
+                ab = p.getBeginA();
+              }
+              if (p.getEndB() == bb && p.getBeginB() < p.getEndB()) {
+                bb = p.getBeginB();
+              }
+              wordEdits.remove(--j);
+            }
+          }
+
+          // We sometimes collapsed an edit together in a strange way,
+          // such that the edges of each text is identical. Fix by
+          // by dropping out that incorrectly replaced region.
+          //
+          while (ab < ae && bb < be && cmp.equals(a, ab, b, bb)) {
+            ab++;
+            bb++;
+          }
+          while (ab < ae && bb < be && cmp.equals(a, ae - 1, b, be - 1)) {
+            ae--;
+            be--;
+          }
+
+          // The leading part of an edit and its trailing part in the same
+          // text might be identical. Slide down that edit and use the tail
+          // rather than the leading bit. If however the edit is only on a
+          // whitespace block try to shift it to the left margin, assuming
+          // that it is an indentation change.
+          //
+          boolean aShift = true;
+          if (ab < ae && isOnlyWhitespace(a, ab, ae)) {
+            int lf = findLF(wordEdits, j, a, ab);
+            if (lf < ab && a.charAt(lf) == '\n') {
+              int nb = lf + 1;
+              int p = 0;
+              while (p < ae - ab) {
+                if (cmp.equals(a, ab + p, a, ab + p))
+                  p++;
+                else
+                  break;
+              }
+              if (p == ae - ab) {
+                ab = nb;
+                ae = nb + p;
+                aShift = false;
+              }
+            }
+          }
+          if (aShift) {
+            while (0 < ab && ab < ae && a.charAt(ab - 1) != '\n'
+                && cmp.equals(a, ab - 1, a, ae - 1)) {
+              ab--;
+              ae--;
+            }
+            if (!a.isLineStart(ab) || !a.contains(ab, ae, '\n')) {
+              while (ab < ae && ae < a.size() && cmp.equals(a, ab, a, ae)) {
+                ab++;
+                ae++;
+                if (a.charAt(ae - 1) == '\n') {
+                  break;
+                }
+              }
+            }
+          }
+
+          boolean bShift = true;
+          if (bb < be && isOnlyWhitespace(b, bb, be)) {
+            int lf = findLF(wordEdits, j, b, bb);
+            if (lf < bb && b.charAt(lf) == '\n') {
+              int nb = lf + 1;
+              int p = 0;
+              while (p < be - bb) {
+                if (cmp.equals(b, bb + p, b, bb + p))
+                  p++;
+                else
+                  break;
+              }
+              if (p == be - bb) {
+                bb = nb;
+                be = nb + p;
+                bShift = false;
+              }
+            }
+          }
+          if (bShift) {
+            while (0 < bb && bb < be && b.charAt(bb - 1) != '\n'
+                && cmp.equals(b, bb - 1, b, be - 1)) {
+              bb--;
+              be--;
+            }
+            if (!b.isLineStart(bb) || !b.contains(bb, be, '\n')) {
+              while (bb < be && be < b.size() && cmp.equals(b, bb, b, be)) {
+                bb++;
+                be++;
+                if (b.charAt(be - 1) == '\n') {
+                  break;
+                }
+              }
+            }
+          }
+
+          // If most of a line was modified except the LF was common, make
+          // the LF part of the modification region. This is easier to read.
+          //
+          if (ab < ae //
+              && (ab == 0 || a.charAt(ab - 1) == '\n') //
+              && ae < a.size() && a.charAt(ae) == '\n') {
+            ae++;
+          }
+          if (bb < be //
+              && (bb == 0 || b.charAt(bb - 1) == '\n') //
+              && be < b.size() && b.charAt(be) == '\n') {
+            be++;
+          }
+
+          wordEdits.set(j, new Edit(ab, ae, bb, be));
+        }
+
+        edits.set(i, new ReplaceEdit(e, wordEdits));
+      }
+    }
+
+    return new IntraLineDiff(edits);
+  }
+
+  private static void combineLineEdits(List<Edit> edits, Text a, Text b) {
+    for (int j = 0; j < edits.size() - 1;) {
+      Edit c = edits.get(j);
+      Edit n = edits.get(j + 1);
+
+      // Combine edits that are really close together. Right now our rule
+      // is, coalesce two line edits which are only one line apart if that
+      // common context line is either a "pointless line", or is identical
+      // on both sides and starts a new block of code. These are mostly
+      // block reindents to add or remove control flow operators.
+      //
+      final int ad = n.getBeginA() - c.getEndA();
+      final int bd = n.getBeginB() - c.getEndB();
+      if ((1 <= ad && isBlankLineGap(a, c.getEndA(), n.getBeginA()))
+          || (1 <= bd && isBlankLineGap(b, c.getEndB(), n.getBeginB()))
+          || (ad == 1 && bd == 1 && isControlBlockStart(a, c.getEndA()))) {
+        int ab = c.getBeginA();
+        int ae = n.getEndA();
+
+        int bb = c.getBeginB();
+        int be = n.getEndB();
+
+        edits.set(j, new Edit(ab, ae, bb, be));
+        edits.remove(j + 1);
+        continue;
+      }
+
+      j++;
+    }
+  }
+
+  private static boolean isBlankLineGap(Text a, int b, int e) {
+    for (; b < e; b++) {
+      if (!BLANK_LINE_RE.matcher(a.getString(b)).matches()) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private static boolean isControlBlockStart(Text a, int idx) {
+    return CONTROL_BLOCK_START_RE.matcher(a.getString(idx)).find();
+  }
+
+  private static boolean canCoalesce(CharText a, int b, int e) {
+    while (b < e) {
+      if (a.charAt(b++) == '\n') {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private static int findLF(List<Edit> edits, int j, CharText t, int b) {
+    int lf = b;
+    int limit = 0 < j ? edits.get(j - 1).getEndB() : 0;
+    while (limit < lf && t.charAt(lf) != '\n') {
+      lf--;
+    }
+    return lf;
+  }
+
+  private static boolean isOnlyWhitespace(CharText t, final int b, final int e) {
+    for (int c = b; c < e; c++) {
+      if (!Character.isWhitespace(t.charAt(c))) {
+        return false;
+      }
+    }
+    return b < e;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java
index c679f4a..a7cf10a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java
@@ -16,12 +16,6 @@
 
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.AccountDiffPreference.Whitespace;
-
-import org.eclipse.jgit.diff.Edit;
-import org.eclipse.jgit.lib.ObjectId;
-
-import java.util.List;
 
 /** Provides a cached list of {@link PatchListEntry}. */
 public interface PatchListCache {
@@ -29,8 +23,5 @@
 
   public PatchList get(Change change, PatchSet patchSet);
 
-  public PatchList get(Change change, PatchSet patchSet, Whitespace whitespace);
-
-  public IntraLineDiff get(ObjectId aId, Text aText, ObjectId bId, Text bText,
-      List<Edit> edits);
+  public IntraLineDiff getIntraLineDiff(IntraLineDiffKey key);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
index 087ed51..e1a0a40 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
@@ -12,114 +12,32 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 //
-// Some portions (e.g. outputDiff) below are:
-//
-// Copyright (C) 2009, Christian Halstrick <christian.halstrick@sap.com>
-// Copyright (C) 2009, Johannes E. Schindelin
-// Copyright (C) 2009, Johannes Schindelin <johannes.schindelin@gmx.de>
-// and other copyright owners as documented in the project's IP log.
-//
-// This program and the accompanying materials are made available
-// under the terms of the Eclipse Distribution License v1.0 which
-// accompanies this distribution, is reproduced below, and is
-// available at http://www.eclipse.org/org/documents/edl-v10.php
-//
-// All rights reserved.
-//
-// Redistribution and use in source and binary forms, with or
-// without modification, are permitted provided that the following
-// conditions are met:
-//
-// - Redistributions of source code must retain the above copyright
-// notice, this list of conditions and the following disclaimer.
-//
-// - Redistributions in binary form must reproduce the above
-// copyright notice, this list of conditions and the following
-// disclaimer in the documentation and/or other materials provided
-// with the distribution.
-//
-// - Neither the name of the Eclipse Foundation, Inc. nor the
-// names of its contributors may be used to endorse or promote
-// products derived from this software without specific prior
-// written permission.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
-// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
-// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
-// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
-// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
-// NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
-// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
-// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
-// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-//
 
 package com.google.gerrit.server.patch;
 
 
+import com.google.gerrit.reviewdb.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.Patch;
 import com.google.gerrit.reviewdb.PatchSet;
 import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.AccountDiffPreference.Whitespace;
 import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.EntryCreator;
 import com.google.gerrit.server.cache.EvictionPolicy;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.Singleton;
 import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
 
-import org.eclipse.jgit.diff.DiffEntry;
-import org.eclipse.jgit.diff.DiffFormatter;
-import org.eclipse.jgit.diff.Edit;
-import org.eclipse.jgit.diff.EditList;
-import org.eclipse.jgit.diff.HistogramDiff;
-import org.eclipse.jgit.diff.MyersDiff;
-import org.eclipse.jgit.diff.RawText;
-import org.eclipse.jgit.diff.RawTextComparator;
-import org.eclipse.jgit.diff.ReplaceEdit;
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.patch.FileHeader;
-import org.eclipse.jgit.patch.FileHeader.PatchType;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevObject;
-import org.eclipse.jgit.revwalk.RevTree;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.treewalk.TreeWalk;
-import org.eclipse.jgit.treewalk.filter.TreeFilter;
-import org.eclipse.jgit.util.io.DisabledOutputStream;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.regex.Pattern;
 
 /** Provides a cached list of {@link PatchListEntry}. */
 @Singleton
 public class PatchListCacheImpl implements PatchListCache {
   private static final String FILE_NAME = "diff";
-  private static final String INTRA_NAME = "diff_intraline";
-
-  private static final Pattern BLANK_LINE_RE =
-      Pattern.compile("^[ \\t]*(|[{}]|/\\*\\*?|\\*)[ \\t]*$");
-  private static final Pattern CONTROL_BLOCK_START_RE =
-      Pattern.compile("[{:][ \\t]*$");
+  static final String INTRA_NAME = "diff_intraline";
 
   public static Module module() {
     return new CacheModule() {
@@ -159,7 +77,9 @@
     this.fileCache = fileCache;
     this.intraCache = intraCache;
 
-    this.computeIntraline = cfg.getBoolean("cache", "diff", "intraline", true);
+    this.computeIntraline =
+        cfg.getBoolean("cache", INTRA_NAME, "enabled",
+            cfg.getBoolean("cache", "diff", "intraline", true));
   }
 
   public PatchList get(final PatchListKey key) {
@@ -167,478 +87,23 @@
   }
 
   public PatchList get(final Change change, final PatchSet patchSet) {
-    return get(change, patchSet, Whitespace.IGNORE_NONE);
-  }
-
-  public PatchList get(final Change change, final PatchSet patchSet,
-      final Whitespace whitespace) {
     final Project.NameKey projectKey = change.getProject();
     final ObjectId a = null;
     final ObjectId b = ObjectId.fromString(patchSet.getRevision().get());
-    return get(new PatchListKey(projectKey, a, b, whitespace));
+    final Whitespace ws = Whitespace.IGNORE_NONE;
+    return get(new PatchListKey(projectKey, a, b, ws));
   }
 
   @Override
-  public IntraLineDiff get(ObjectId aId, Text aText, ObjectId bId, Text bText,
-      List<Edit> edits) {
+  public IntraLineDiff getIntraLineDiff(IntraLineDiffKey key) {
     if (computeIntraline) {
-      IntraLineDiffKey key =
-          new IntraLineDiffKey(aId, aText, bId, bText, edits);
-      return intraCache.get(key);
+      IntraLineDiff d = intraCache.get(key);
+      if (d == null) {
+        d = new IntraLineDiff(IntraLineDiff.Status.ERROR);
+      }
+      return d;
     } else {
-      return null;
-    }
-  }
-
-  private static RawTextComparator comparatorFor(Whitespace ws) {
-    switch (ws) {
-      case IGNORE_ALL_SPACE:
-        return RawTextComparator.WS_IGNORE_ALL;
-
-      case IGNORE_SPACE_AT_EOL:
-        return RawTextComparator.WS_IGNORE_TRAILING;
-
-      case IGNORE_SPACE_CHANGE:
-        return RawTextComparator.WS_IGNORE_CHANGE;
-
-      case IGNORE_NONE:
-      default:
-        return RawTextComparator.DEFAULT;
-    }
-  }
-
-  static class PatchListLoader extends EntryCreator<PatchListKey, PatchList> {
-    private final GitRepositoryManager repoManager;
-
-    @Inject
-    PatchListLoader(GitRepositoryManager mgr) {
-      repoManager = mgr;
-    }
-
-    @Override
-    public PatchList createEntry(final PatchListKey key) throws Exception {
-      final Repository repo = repoManager.openRepository(key.projectKey.get());
-      try {
-        return readPatchList(key, repo);
-      } finally {
-        repo.close();
-      }
-    }
-
-    private PatchList readPatchList(final PatchListKey key,
-        final Repository repo) throws IOException {
-      // TODO(jeffschu) correctly handle merge commits
-
-      final RawTextComparator cmp = comparatorFor(key.getWhitespace());
-      final ObjectReader reader = repo.newObjectReader();
-      try {
-        final RevWalk rw = new RevWalk(reader);
-        final RevCommit b = rw.parseCommit(key.getNewId());
-        final RevObject a = aFor(key, repo, rw, b);
-
-        if (a == null) {
-          // This is a merge commit, compared to its ancestor.
-          //
-          final PatchListEntry[] entries = new PatchListEntry[1];
-          entries[0] = newCommitMessage(cmp, repo, reader, null, b);
-          return new PatchList(a, b, true, entries);
-        }
-
-        final boolean againstParent =
-            b.getParentCount() > 0 && b.getParent(0) == a;
-
-        RevCommit aCommit;
-        RevTree aTree;
-        if (a instanceof RevCommit) {
-          aCommit = (RevCommit) a;
-          aTree = aCommit.getTree();
-        } else if (a instanceof RevTree) {
-          aCommit = null;
-          aTree = (RevTree) a;
-        } else {
-          throw new IOException("Unexpected type: " + a.getClass());
-        }
-
-        RevTree bTree = b.getTree();
-
-        final TreeWalk walk = new TreeWalk(reader);
-        walk.reset();
-        walk.setRecursive(true);
-        walk.addTree(aTree);
-        walk.addTree(bTree);
-        walk.setFilter(TreeFilter.ANY_DIFF);
-
-        DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE);
-        df.setRepository(repo);
-        df.setDiffComparator(cmp);
-        df.setDetectRenames(true);
-        List<DiffEntry> diffEntries = df.scan(aTree, bTree);
-
-        final int cnt = diffEntries.size();
-        final PatchListEntry[] entries = new PatchListEntry[1 + cnt];
-        entries[0] = newCommitMessage(cmp, repo, reader, //
-            againstParent ? null : aCommit, b);
-        for (int i = 0; i < cnt; i++) {
-          FileHeader fh = df.toFileHeader(diffEntries.get(i));
-          entries[1 + i] = newEntry(aTree, fh);
-        }
-        return new PatchList(a, b, againstParent, entries);
-      } finally {
-        reader.release();
-      }
-    }
-
-    private PatchListEntry newCommitMessage(final RawTextComparator cmp,
-        final Repository db, final ObjectReader reader,
-        final RevCommit aCommit, final RevCommit bCommit) throws IOException {
-      StringBuilder hdr = new StringBuilder();
-
-      hdr.append("diff --git");
-      if (aCommit != null) {
-        hdr.append(" a/" + Patch.COMMIT_MSG);
-      } else {
-        hdr.append(" " + FileHeader.DEV_NULL);
-      }
-      hdr.append(" b/" + Patch.COMMIT_MSG);
-      hdr.append("\n");
-
-      if (aCommit != null) {
-        hdr.append("--- a/" + Patch.COMMIT_MSG + "\n");
-      } else {
-        hdr.append("--- " + FileHeader.DEV_NULL + "\n");
-      }
-      hdr.append("+++ b/" + Patch.COMMIT_MSG + "\n");
-
-      Text aText =
-          aCommit != null ? Text.forCommit(db, reader, aCommit) : Text.EMPTY;
-      Text bText = Text.forCommit(db, reader, bCommit);
-
-      byte[] rawHdr = hdr.toString().getBytes("UTF-8");
-      RawText aRawText = new RawText(aText.getContent());
-      RawText bRawText = new RawText(bText.getContent());
-      EditList edits = new HistogramDiff().diff(cmp, aRawText, bRawText);
-      FileHeader fh = new FileHeader(rawHdr, edits, PatchType.UNIFIED);
-      return new PatchListEntry(fh, edits);
-    }
-
-    private PatchListEntry newEntry(RevTree aTree, FileHeader fileHeader) {
-      final FileMode oldMode = fileHeader.getOldMode();
-      final FileMode newMode = fileHeader.getNewMode();
-
-      if (oldMode == FileMode.GITLINK || newMode == FileMode.GITLINK) {
-        return new PatchListEntry(fileHeader, Collections.<Edit> emptyList());
-      }
-
-      if (aTree == null // want combined diff
-          || fileHeader.getPatchType() != PatchType.UNIFIED
-          || fileHeader.getHunks().isEmpty()) {
-        return new PatchListEntry(fileHeader, Collections.<Edit> emptyList());
-      }
-
-      List<Edit> edits = fileHeader.toEditList();
-      if (edits.isEmpty()) {
-        return new PatchListEntry(fileHeader, Collections.<Edit> emptyList());
-      } else {
-        return new PatchListEntry(fileHeader, edits);
-      }
-    }
-
-    private static RevObject aFor(final PatchListKey key,
-        final Repository repo, final RevWalk rw, final RevCommit b)
-        throws IOException {
-      if (key.getOldId() != null) {
-        return rw.parseAny(key.getOldId());
-      }
-
-      switch (b.getParentCount()) {
-        case 0:
-          return rw.parseAny(emptyTree(repo));
-        case 1: {
-          RevCommit r = b.getParent(0);
-          rw.parseBody(r);
-          return r;
-        }
-        default:
-          // merge commit, return null to force combined diff behavior
-          return null;
-      }
-    }
-
-    private static ObjectId emptyTree(final Repository repo) throws IOException {
-      ObjectInserter oi = repo.newObjectInserter();
-      try {
-        ObjectId id = oi.insert(Constants.OBJ_TREE, new byte[] {});
-        oi.flush();
-        return id;
-      } finally {
-        oi.release();
-      }
-    }
-  }
-
-  static class IntraLineLoader extends
-      EntryCreator<IntraLineDiffKey, IntraLineDiff> {
-    @Override
-    public IntraLineDiff createEntry(IntraLineDiffKey key) throws Exception {
-      List<Edit> edits = new ArrayList<Edit>(key.getEdits());
-      Text aContent = key.getTextA();
-      Text bContent = key.getTextB();
-      combineLineEdits(edits, aContent, bContent);
-
-      for (int i = 0; i < edits.size(); i++) {
-        Edit e = edits.get(i);
-
-        if (e.getType() == Edit.Type.REPLACE) {
-          CharText a = new CharText(aContent, e.getBeginA(), e.getEndA());
-          CharText b = new CharText(bContent, e.getBeginB(), e.getEndB());
-          CharTextComparator cmp = new CharTextComparator();
-
-          List<Edit> wordEdits = MyersDiff.INSTANCE.diff(cmp, a, b);
-
-          // Combine edits that are really close together. If they are
-          // just a few characters apart we tend to get better results
-          // by joining them together and taking the whole span.
-          //
-          for (int j = 0; j < wordEdits.size() - 1;) {
-            Edit c = wordEdits.get(j);
-            Edit n = wordEdits.get(j + 1);
-
-            if (n.getBeginA() - c.getEndA() <= 5
-                || n.getBeginB() - c.getEndB() <= 5) {
-              int ab = c.getBeginA();
-              int ae = n.getEndA();
-
-              int bb = c.getBeginB();
-              int be = n.getEndB();
-
-              if (canCoalesce(a, c.getEndA(), n.getBeginA())
-                  && canCoalesce(b, c.getEndB(), n.getBeginB())) {
-                wordEdits.set(j, new Edit(ab, ae, bb, be));
-                wordEdits.remove(j + 1);
-                continue;
-              }
-            }
-
-            j++;
-          }
-
-          // Apply some simple rules to fix up some of the edits. Our
-          // logic above, along with our per-character difference tends
-          // to produce some crazy stuff.
-          //
-          for (int j = 0; j < wordEdits.size(); j++) {
-            Edit c = wordEdits.get(j);
-            int ab = c.getBeginA();
-            int ae = c.getEndA();
-
-            int bb = c.getBeginB();
-            int be = c.getEndB();
-
-            // Sometimes the diff generator produces an INSERT or DELETE
-            // right up against a REPLACE, but we only find this after
-            // we've also played some shifting games on the prior edit.
-            // If that happened to us, coalesce them together so we can
-            // correct this mess for the user. If we don't we wind up
-            // with silly stuff like "es" -> "es = Addresses".
-            //
-            if (1 < j) {
-              Edit p = wordEdits.get(j - 1);
-              if (p.getEndA() == ab || p.getEndB() == bb) {
-                if (p.getEndA() == ab && p.getBeginA() < p.getEndA()) {
-                  ab = p.getBeginA();
-                }
-                if (p.getEndB() == bb && p.getBeginB() < p.getEndB()) {
-                  bb = p.getBeginB();
-                }
-                wordEdits.remove(--j);
-              }
-            }
-
-            // We sometimes collapsed an edit together in a strange way,
-            // such that the edges of each text is identical. Fix by
-            // by dropping out that incorrectly replaced region.
-            //
-            while (ab < ae && bb < be && cmp.equals(a, ab, b, bb)) {
-              ab++;
-              bb++;
-            }
-            while (ab < ae && bb < be && cmp.equals(a, ae - 1, b, be - 1)) {
-              ae--;
-              be--;
-            }
-
-            // The leading part of an edit and its trailing part in the same
-            // text might be identical. Slide down that edit and use the tail
-            // rather than the leading bit. If however the edit is only on a
-            // whitespace block try to shift it to the left margin, assuming
-            // that it is an indentation change.
-            //
-            boolean aShift = true;
-            if (ab < ae && isOnlyWhitespace(a, ab, ae)) {
-              int lf = findLF(wordEdits, j, a, ab);
-              if (lf < ab && a.charAt(lf) == '\n') {
-                int nb = lf + 1;
-                int p = 0;
-                while (p < ae - ab) {
-                  if (cmp.equals(a, ab + p, a, ab + p))
-                    p++;
-                  else
-                    break;
-                }
-                if (p == ae - ab) {
-                  ab = nb;
-                  ae = nb + p;
-                  aShift = false;
-                }
-              }
-            }
-            if (aShift) {
-              while (0 < ab && ab < ae && a.charAt(ab - 1) != '\n'
-                  && cmp.equals(a, ab - 1, a, ae - 1)) {
-                ab--;
-                ae--;
-              }
-              if (!a.isLineStart(ab) || !a.contains(ab, ae, '\n')) {
-                while (ab < ae && ae < a.size() && cmp.equals(a, ab, a, ae)) {
-                  ab++;
-                  ae++;
-                  if (a.charAt(ae - 1) == '\n') {
-                    break;
-                  }
-                }
-              }
-            }
-
-            boolean bShift = true;
-            if (bb < be && isOnlyWhitespace(b, bb, be)) {
-              int lf = findLF(wordEdits, j, b, bb);
-              if (lf < bb && b.charAt(lf) == '\n') {
-                int nb = lf + 1;
-                int p = 0;
-                while (p < be - bb) {
-                  if (cmp.equals(b, bb + p, b, bb + p))
-                    p++;
-                  else
-                    break;
-                }
-                if (p == be - bb) {
-                  bb = nb;
-                  be = nb + p;
-                  bShift = false;
-                }
-              }
-            }
-            if (bShift) {
-              while (0 < bb && bb < be && b.charAt(bb - 1) != '\n'
-                  && cmp.equals(b, bb - 1, b, be - 1)) {
-                bb--;
-                be--;
-              }
-              if (!b.isLineStart(bb) || !b.contains(bb, be, '\n')) {
-                while (bb < be && be < b.size() && cmp.equals(b, bb, b, be)) {
-                  bb++;
-                  be++;
-                  if (b.charAt(be - 1) == '\n') {
-                    break;
-                  }
-                }
-              }
-            }
-
-            // If most of a line was modified except the LF was common, make
-            // the LF part of the modification region. This is easier to read.
-            //
-            if (ab < ae //
-                && (ab == 0 || a.charAt(ab - 1) == '\n') //
-                && ae < a.size() && a.charAt(ae) == '\n') {
-              ae++;
-            }
-            if (bb < be //
-                && (bb == 0 || b.charAt(bb - 1) == '\n') //
-                && be < b.size() && b.charAt(be) == '\n') {
-              be++;
-            }
-
-            wordEdits.set(j, new Edit(ab, ae, bb, be));
-          }
-
-          edits.set(i, new ReplaceEdit(e, wordEdits));
-        }
-      }
-
-      return new IntraLineDiff(edits);
-    }
-
-    private static void combineLineEdits(List<Edit> edits, Text a, Text b) {
-      for (int j = 0; j < edits.size() - 1;) {
-        Edit c = edits.get(j);
-        Edit n = edits.get(j + 1);
-
-        // Combine edits that are really close together. Right now our rule
-        // is, coalesce two line edits which are only one line apart if that
-        // common context line is either a "pointless line", or is identical
-        // on both sides and starts a new block of code. These are mostly
-        // block reindents to add or remove control flow operators.
-        //
-        final int ad = n.getBeginA() - c.getEndA();
-        final int bd = n.getBeginB() - c.getEndB();
-        if ((1 <= ad && isBlankLineGap(a, c.getEndA(), n.getBeginA()))
-            || (1 <= bd && isBlankLineGap(b, c.getEndB(), n.getBeginB()))
-            || (ad == 1 && bd == 1 && isControlBlockStart(a, c.getEndA()))) {
-          int ab = c.getBeginA();
-          int ae = n.getEndA();
-
-          int bb = c.getBeginB();
-          int be = n.getEndB();
-
-          edits.set(j, new Edit(ab, ae, bb, be));
-          edits.remove(j + 1);
-          continue;
-        }
-
-        j++;
-      }
-    }
-
-    private static boolean isBlankLineGap(Text a, int b, int e) {
-      for (; b < e; b++) {
-        if (!BLANK_LINE_RE.matcher(a.getString(b)).matches()) {
-          return false;
-        }
-      }
-      return true;
-    }
-
-    private static boolean isControlBlockStart(Text a, int idx) {
-      final String l = a.getString(idx);
-      return CONTROL_BLOCK_START_RE.matcher(l).find();
-    }
-
-    private static boolean canCoalesce(CharText a, int b, int e) {
-      while (b < e) {
-        if (a.charAt(b++) == '\n') {
-          return false;
-        }
-      }
-      return true;
-    }
-
-    private static int findLF(List<Edit> edits, int j, CharText t, int b) {
-      int lf = b;
-      int limit = 0 < j ? edits.get(j - 1).getEndB() : 0;
-      while (limit < lf && t.charAt(lf) != '\n') {
-        lf--;
-      }
-      return lf;
-    }
-
-    private static boolean isOnlyWhitespace(CharText t, final int b, final int e) {
-      for (int c = b; c < e; c++) {
-        if (!Character.isWhitespace(t.charAt(c))) {
-          return false;
-        }
-      }
-      return b < e;
+      return new IntraLineDiff(IntraLineDiff.Status.DISABLED);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
new file mode 100644
index 0000000..d75aec6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
@@ -0,0 +1,235 @@
+// Copyright (C) 2009 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.patch;
+
+import com.google.gerrit.reviewdb.Patch;
+import com.google.gerrit.reviewdb.AccountDiffPreference.Whitespace;
+import com.google.gerrit.server.cache.EntryCreator;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.diff.Edit;
+import org.eclipse.jgit.diff.EditList;
+import org.eclipse.jgit.diff.HistogramDiff;
+import org.eclipse.jgit.diff.RawText;
+import org.eclipse.jgit.diff.RawTextComparator;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.patch.FileHeader;
+import org.eclipse.jgit.patch.FileHeader.PatchType;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.filter.TreeFilter;
+import org.eclipse.jgit.util.io.DisabledOutputStream;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+
+class PatchListLoader extends EntryCreator<PatchListKey, PatchList> {
+  private final GitRepositoryManager repoManager;
+
+  @Inject
+  PatchListLoader(GitRepositoryManager mgr) {
+    repoManager = mgr;
+  }
+
+  @Override
+  public PatchList createEntry(final PatchListKey key) throws Exception {
+    final Repository repo = repoManager.openRepository(key.projectKey.get());
+    try {
+      return readPatchList(key, repo);
+    } finally {
+      repo.close();
+    }
+  }
+
+  private static RawTextComparator comparatorFor(Whitespace ws) {
+    switch (ws) {
+      case IGNORE_ALL_SPACE:
+        return RawTextComparator.WS_IGNORE_ALL;
+
+      case IGNORE_SPACE_AT_EOL:
+        return RawTextComparator.WS_IGNORE_TRAILING;
+
+      case IGNORE_SPACE_CHANGE:
+        return RawTextComparator.WS_IGNORE_CHANGE;
+
+      case IGNORE_NONE:
+      default:
+        return RawTextComparator.DEFAULT;
+    }
+  }
+
+  private PatchList readPatchList(final PatchListKey key,
+      final Repository repo) throws IOException {
+    // TODO(jeffschu) correctly handle merge commits
+
+    final RawTextComparator cmp = comparatorFor(key.getWhitespace());
+    final ObjectReader reader = repo.newObjectReader();
+    try {
+      final RevWalk rw = new RevWalk(reader);
+      final RevCommit b = rw.parseCommit(key.getNewId());
+      final RevObject a = aFor(key, repo, rw, b);
+
+      if (a == null) {
+        // This is a merge commit, compared to its ancestor.
+        //
+        final PatchListEntry[] entries = new PatchListEntry[1];
+        entries[0] = newCommitMessage(cmp, repo, reader, null, b);
+        return new PatchList(a, b, true, entries);
+      }
+
+      final boolean againstParent =
+          b.getParentCount() > 0 && b.getParent(0) == a;
+
+      RevCommit aCommit;
+      RevTree aTree;
+      if (a instanceof RevCommit) {
+        aCommit = (RevCommit) a;
+        aTree = aCommit.getTree();
+      } else if (a instanceof RevTree) {
+        aCommit = null;
+        aTree = (RevTree) a;
+      } else {
+        throw new IOException("Unexpected type: " + a.getClass());
+      }
+
+      RevTree bTree = b.getTree();
+
+      final TreeWalk walk = new TreeWalk(reader);
+      walk.reset();
+      walk.setRecursive(true);
+      walk.addTree(aTree);
+      walk.addTree(bTree);
+      walk.setFilter(TreeFilter.ANY_DIFF);
+
+      DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE);
+      df.setRepository(repo);
+      df.setDiffComparator(cmp);
+      df.setDetectRenames(true);
+      List<DiffEntry> diffEntries = df.scan(aTree, bTree);
+
+      final int cnt = diffEntries.size();
+      final PatchListEntry[] entries = new PatchListEntry[1 + cnt];
+      entries[0] = newCommitMessage(cmp, repo, reader, //
+          againstParent ? null : aCommit, b);
+      for (int i = 0; i < cnt; i++) {
+        FileHeader fh = df.toFileHeader(diffEntries.get(i));
+        entries[1 + i] = newEntry(aTree, fh);
+      }
+      return new PatchList(a, b, againstParent, entries);
+    } finally {
+      reader.release();
+    }
+  }
+
+  private PatchListEntry newCommitMessage(final RawTextComparator cmp,
+      final Repository db, final ObjectReader reader,
+      final RevCommit aCommit, final RevCommit bCommit) throws IOException {
+    StringBuilder hdr = new StringBuilder();
+
+    hdr.append("diff --git");
+    if (aCommit != null) {
+      hdr.append(" a/" + Patch.COMMIT_MSG);
+    } else {
+      hdr.append(" " + FileHeader.DEV_NULL);
+    }
+    hdr.append(" b/" + Patch.COMMIT_MSG);
+    hdr.append("\n");
+
+    if (aCommit != null) {
+      hdr.append("--- a/" + Patch.COMMIT_MSG + "\n");
+    } else {
+      hdr.append("--- " + FileHeader.DEV_NULL + "\n");
+    }
+    hdr.append("+++ b/" + Patch.COMMIT_MSG + "\n");
+
+    Text aText =
+        aCommit != null ? Text.forCommit(db, reader, aCommit) : Text.EMPTY;
+    Text bText = Text.forCommit(db, reader, bCommit);
+
+    byte[] rawHdr = hdr.toString().getBytes("UTF-8");
+    RawText aRawText = new RawText(aText.getContent());
+    RawText bRawText = new RawText(bText.getContent());
+    EditList edits = new HistogramDiff().diff(cmp, aRawText, bRawText);
+    FileHeader fh = new FileHeader(rawHdr, edits, PatchType.UNIFIED);
+    return new PatchListEntry(fh, edits);
+  }
+
+  private PatchListEntry newEntry(RevTree aTree, FileHeader fileHeader) {
+    final FileMode oldMode = fileHeader.getOldMode();
+    final FileMode newMode = fileHeader.getNewMode();
+
+    if (oldMode == FileMode.GITLINK || newMode == FileMode.GITLINK) {
+      return new PatchListEntry(fileHeader, Collections.<Edit> emptyList());
+    }
+
+    if (aTree == null // want combined diff
+        || fileHeader.getPatchType() != PatchType.UNIFIED
+        || fileHeader.getHunks().isEmpty()) {
+      return new PatchListEntry(fileHeader, Collections.<Edit> emptyList());
+    }
+
+    List<Edit> edits = fileHeader.toEditList();
+    if (edits.isEmpty()) {
+      return new PatchListEntry(fileHeader, Collections.<Edit> emptyList());
+    } else {
+      return new PatchListEntry(fileHeader, edits);
+    }
+  }
+
+  private static RevObject aFor(final PatchListKey key,
+      final Repository repo, final RevWalk rw, final RevCommit b)
+      throws IOException {
+    if (key.getOldId() != null) {
+      return rw.parseAny(key.getOldId());
+    }
+
+    switch (b.getParentCount()) {
+      case 0:
+        return rw.parseAny(emptyTree(repo));
+      case 1: {
+        RevCommit r = b.getParent(0);
+        rw.parseBody(r);
+        return r;
+      }
+      default:
+        // merge commit, return null to force combined diff behavior
+        return null;
+    }
+  }
+
+  private static ObjectId emptyTree(final Repository repo) throws IOException {
+    ObjectInserter oi = repo.newObjectInserter();
+    try {
+      ObjectId id = oi.insert(Constants.OBJ_TREE, new byte[] {});
+      oi.flush();
+      return id;
+    } finally {
+      oi.release();
+    }
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
index a0b9d25..cd87498 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -18,6 +18,8 @@
 import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.reviewdb.ApprovalCategory;
 import com.google.gerrit.reviewdb.ApprovalCategoryValue;
+import com.google.gerrit.reviewdb.Branch;
+import com.google.gerrit.reviewdb.Branch.NameKey;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.PatchSet;
 import com.google.gerrit.reviewdb.PatchSetApproval;
@@ -26,6 +28,7 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.MergeOp;
+import com.google.gerrit.server.git.MergeOp.Factory;
 import com.google.gerrit.server.git.MergeQueue;
 import com.google.gerrit.server.patch.PublishComments;
 import com.google.gerrit.server.project.CanSubmitResult;
@@ -51,6 +54,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
 
 public class ReviewCommand extends BaseCommand {
   private static final Logger log =
@@ -113,6 +117,8 @@
   @Inject
   private PublishComments.Factory publishCommentsFactory;
 
+  private Set<PatchSet.Id> toSubmit = new HashSet<PatchSet.Id>();
+
   @Override
   public final void start(final Environment env) {
     startThread(new CommandRunnable() {
@@ -135,10 +141,42 @@
             log.error("internal error while approving " + patchSetId, e);
           }
         }
+
         if (!ok) {
           throw new UnloggedFailure(1, "one or more approvals failed;"
               + " review output above");
         }
+
+        if (!toSubmit.isEmpty()) {
+          final Set<Branch.NameKey> toMerge = new HashSet<Branch.NameKey>();
+          try {
+            for (PatchSet.Id patchSetId : toSubmit) {
+              ChangeUtil.submit(opFactory, patchSetId, currentUser, db,
+                  new MergeQueue() {
+                    @Override
+                    public void merge(MergeOp.Factory mof, Branch.NameKey branch) {
+                      toMerge.add(branch);
+                    }
+
+                    @Override
+                    public void schedule(Branch.NameKey branch) {
+                      toMerge.add(branch);
+                    }
+
+                    @Override
+                    public void recheckAfter(Branch.NameKey branch, long delay,
+                        TimeUnit delayUnit) {
+                      toMerge.add(branch);
+                    }
+                  });
+            }
+            for (Branch.NameKey branch : toMerge) {
+              merger.merge(opFactory, branch);
+            }
+          } catch (OrmException updateError) {
+            throw new Failure(1, "one or more submits failed", updateError);
+          }
+        }
       }
     });
   }
@@ -170,7 +208,7 @@
           changeControl.canSubmit(patchSetId, db, approvalTypes,
               functionStateFactory);
       if (result == CanSubmitResult.OK) {
-        ChangeUtil.submit(opFactory, patchSetId, currentUser, db, merger);
+        toSubmit.add(patchSetId);
       } else {
         throw error(result.getMessage());
       }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java
index bcc9d19..95b33f7 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java
@@ -42,7 +42,7 @@
 
     final UploadPack up = new UploadPack(repo);
     if (!projectControl.allRefsAreVisible()) {
-      up.setRefFilter(new VisibleRefFilter(repo, projectControl, db.get()));
+      up.setRefFilter(new VisibleRefFilter(repo, projectControl, db.get(), true));
     }
     up.setPackConfig(config.getPackConfig());
     up.setTimeout(config.getTimeout());
diff --git a/pom.xml b/pom.xml
index 6bbf107..c76eab7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -46,14 +46,14 @@
   </issueManagement>
 
   <properties>
-    <jgitVersion>0.9.3.197-g51bf8ea</jgitVersion>
+    <jgitVersion>0.9.3.298-g18abb81</jgitVersion>
     <gwtormVersion>1.1.4</gwtormVersion>
     <gwtjsonrpcVersion>1.2.2</gwtjsonrpcVersion>
     <gwtexpuiVersion>1.2.2</gwtexpuiVersion>
     <gwtVersion>2.0.4</gwtVersion>
     <slf4jVersion>1.6.1</slf4jVersion>
     <guiceVersion>2.0</guiceVersion>
-    <jettyVersion>7.0.2.v20100331</jettyVersion>
+    <jettyVersion>7.2.1.v20101111</jettyVersion>
     <keyappletVersion>1.0</keyappletVersion>
 
     <gwt.soyc>false</gwt.soyc>