Merge "Move string utils join logic into Gerrit"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index d2a7fbc..28ee86a 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -298,6 +298,25 @@
 +
 By default this is set to false.
 
+[[auth.userNameToLowerCase]]auth.userNameToLowerCase::
++
+If set the username that is received to authenticate a git operation
+is converted to lower case for looking up the user account in Gerrit.
++
+By setting this parameter a case insensitive authentication for the
+git operations can be achieved, if it is ensured that the usernames in
+Gerrit (scheme `username`) are stored in lower case (e.g. if the
+parameter link:#ldap.accountSshUserName[ldap.accountSshUserName] is
+set to `${sAMAccountName.toLowerCase}`). It is important that for all
+existing accounts this username is already in lower case. It is not
+possible to convert the usernames of the existing accounts to lower
+case because this would break the access to existing per-user
+branches.
++
+This parameter only affects git over http and git over SSH traffic.
++
+By default this is set to false.
+
 [[cache]]Section cache
 ~~~~~~~~~~~~~~~~~~~~~~
 
@@ -1081,6 +1100,12 @@
 Valid replacements are `${project}` for the project name in Gerrit
 and `${branch}` for the name of the branch.
 
+[[gitweb.linkname]]gitweb.linkname::
++
+Optional setting for modifying the link name presented to the user
+in the Gerrit web-UI.
++
+Default linkname for custom type is "gitweb".
 
 [[hooks]]Section hooks
 ~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1515,6 +1540,24 @@
 Default is `(memberUid=${username})` for RFC 2307,
 and unset (disabled) for Active Directory.
 
+[[ldap.localUsernameToLowerCase]]ldap.localUsernameToLowerCase::
++
+Converts the local username, that is used to login into the Gerrit
+WebUI, to lower case before doing the LDAP authentication. By setting
+this parameter to true, a case insensitive login to the Gerrit WebUI
+can be achieved.
++
+If set, it must be ensured that the local usernames for all existing
+accounts are converted to lower case, otherwise a user that has a
+local username that contains upper case characters cannot login
+anymore. The local usernames for the existing accounts can be
+converted to lower case by running the server program
+link:pgm-LocalUsernamesToLowerCase.html[LocalUsernamesToLowerCase].
+Please be aware that the conversion of the local usernames to lower
+case can't be undone. For newly created accounts the local username
+will be directly stored in lower case.
++
+By default, unset/false.
 
 [[mimetype]]Section mimetype
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/Documentation/pgm-LocalUsernamesToLowerCase.txt b/Documentation/pgm-LocalUsernamesToLowerCase.txt
new file mode 100644
index 0000000..9189fee
--- /dev/null
+++ b/Documentation/pgm-LocalUsernamesToLowerCase.txt
@@ -0,0 +1,68 @@
+LocalUsernamesToLowerCase
+=========================
+
+NAME
+----
+LocalUsernamesToLowerCase - Convert the local username of every
+account to lower case
+
+SYNOPSIS
+--------
+[verse]
+'java' -jar gerrit.war 'LocalUsernamesToLowerCase' -d <SITE_PATH>
+
+DESCRIPTION
+-----------
+Converts the local username for every account to lower case. The
+local username is the username that is used to login into the Gerrit
+WebUI.
+
+This task is only intended to be run if the configuration parameter
+link:config-gerrit.html#ldap.localUsernameToLowerCase[ldap.localUsernameToLowerCase]
+was set to true to achieve case insensitive LDAP login to the Gerrit
+WebUI.
+
+Please be aware that the conversion of the local usernames to lower
+case can't be undone.
+
+The program will produce errors if there are accounts that have the
+same local username, but with different case. In this case the local
+username for these accounts is not converted to lower case.
+
+This task can run in the background concurrently to the server if the
+database is MySQL or PostgreSQL. If the database is H2, this task
+must be run by itself.
+
+OPTIONS
+-------
+
+-d::
+\--site-path::
+	Location of the gerrit.config file, and all other per-site
+	configuration data, supporting libraries and log files.
+
+\--threads::
+	Number of threads to perform the scan work with.  Defaults to
+	twice the number of CPUs available.
+
+CONTEXT
+-------
+This command can only be run on a server which has direct
+connectivity to the metadata database.
+
+EXAMPLES
+--------
+To convert the local username of every account to lower case:
+
+====
+	$ java -jar gerrit.war LocalUsernamesToLowerCase -d site_path
+====
+
+See Also
+--------
+
+* Configuration parameter link:config-gerrit.html#ldap.localUsernameToLowerCase[ldap.localUsernameToLowerCase]
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/pgm-index.txt b/Documentation/pgm-index.txt
index 4db4ab0..5cbe6ba0 100644
--- a/Documentation/pgm-index.txt
+++ b/Documentation/pgm-index.txt
@@ -36,6 +36,9 @@
 link:pgm-ScanTrackingIds.html[ScanTrackingIds]::
 	Rescan all changes after configuring trackingids.
 
+link:pgm-LocalUsernamesToLowerCase.html[LocalUsernamesToLowerCase]::
+	Convert the local username of every account to lower case.
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GitWebType.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GitWebType.java
index 7eb6955..2ea812b 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GitWebType.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GitWebType.java
@@ -27,18 +27,22 @@
 
     if (name == null || name.isEmpty() || name.equalsIgnoreCase("gitweb")) {
       type = new GitWebType();
+      type.setLinkName("gitweb");
       type.setProject("?p=${project}.git;a=summary");
       type.setRevision("?p=${project}.git;a=commit;h=${commit}");
       type.setBranch("?p=${project}.git;a=shortlog;h=${branch}");
 
     } else if (name.equalsIgnoreCase("cgit")) {
       type = new GitWebType();
+      type.setLinkName("cgit");
       type.setProject("${project}/summary");
       type.setRevision("${project}/commit/?id=${commit}");
       type.setBranch("${project}/log/?h=${branch}");
 
     } else if (name.equalsIgnoreCase("custom")) {
       type = new GitWebType();
+      // The custom name is not defined, let's keep the old style of using GitWeb
+      type.setLinkName("gitweb");
 
     } else {
       type = null;
@@ -47,6 +51,9 @@
     return type;
   }
 
+  /** name of the type. */
+  private String name;
+
   /** String for revision view url. */
   private String revision;
 
@@ -70,6 +77,15 @@
   }
 
   /**
+   * Get the String for link-name of the type.
+   *
+   * @return The String for link-name of the type
+   */
+  public String getLinkName() {
+    return name;
+  }
+
+  /**
    * Get the String for project view.
    *
    * @return The String for project view
@@ -99,6 +115,17 @@
   }
 
   /**
+   * Set the pattern for link-name type.
+   *
+   * @param pattern The pattern for link-name type
+   */
+  public void setLinkName(final String name) {
+    if (name != null && !name.isEmpty()) {
+      this.name = name;
+    }
+  }
+
+  /**
    * Set the pattern for project view.
    *
    * @param pattern The pattern for project view
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GitwebLink.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GitwebLink.java
index 0460bf2..cf74e7c 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GitwebLink.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GitwebLink.java
@@ -36,6 +36,10 @@
     type = gitWebType;
   }
 
+  public String getLinkName() {
+    return "(" + type.getLinkName() + ")";
+  }
+
   public String toRevision(final Project.NameKey project, final PatchSet ps) {
     ParameterizedString pattern = new ParameterizedString(type.getRevision());
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
index dbbaf33..892aafc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
@@ -308,7 +308,7 @@
       }
 
       if (c != null) {
-        table.setWidget(row, 4, new Anchor("(gitweb)", false, c.toBranch(k
+        table.setWidget(row, 4, new Anchor(c.getLinkName(), false, c.toBranch(k
             .getNameKey())));
       }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
index 003a731..c65e0f8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.client.ui.AccountDashboardLink;
 import com.google.gerrit.client.ui.ComplexDisclosurePanel;
 import com.google.gerrit.client.ui.ListenableAccountDiffPreference;
+import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.ChangeDetail;
 import com.google.gerrit.common.data.GitwebLink;
 import com.google.gerrit.common.data.PatchSetDetail;
@@ -111,7 +112,7 @@
     getHeader().add(revtxt);
     if (gw != null) {
       final Anchor revlink =
-          new Anchor("(gitweb)", false, gw.toRevision(detail.getChange()
+          new Anchor(gw.getLinkName(), false, gw.toRevision(detail.getChange()
               .getProject(), ps));
       revlink.addStyleName(Gerrit.RESOURCES.css().patchSetLink());
       getHeader().add(revlink);
@@ -436,7 +437,7 @@
         @Override
         public void onClick(final ClickEvent event) {
           b.setEnabled(false);
-          new CommentedChangeActionDialog(patchSet.getId(), createCommentedCallback(b),
+          new CommentedChangeActionDialog(patchSet.getId(), createCommentedCallback(b, true),
               Util.C.revertChangeTitle(), Util.C.headingRevertMessage(),
               Util.C.buttonRevertChangeSend(), Util.C.buttonRevertChangeCancel(),
               Gerrit.RESOURCES.css().revertChangeDialog(), Gerrit.RESOURCES.css().revertMessage(),
@@ -456,7 +457,7 @@
         @Override
         public void onClick(final ClickEvent event) {
           b.setEnabled(false);
-          new CommentedChangeActionDialog(patchSet.getId(), createCommentedCallback(b),
+          new CommentedChangeActionDialog(patchSet.getId(), createCommentedCallback(b, false),
               Util.C.abandonChangeTitle(), Util.C.headingAbandonMessage(),
               Util.C.buttonAbandonChangeSend(), Util.C.buttonAbandonChangeCancel(),
               Gerrit.RESOURCES.css().abandonChangeDialog(), Gerrit.RESOURCES.css().abandonMessage()) {
@@ -475,7 +476,7 @@
         @Override
         public void onClick(final ClickEvent event) {
           b.setEnabled(false);
-          new CommentedChangeActionDialog(patchSet.getId(), createCommentedCallback(b),
+          new CommentedChangeActionDialog(patchSet.getId(), createCommentedCallback(b, false),
               Util.C.restoreChangeTitle(), Util.C.headingRestoreMessage(),
               Util.C.buttonRestoreChangeSend(), Util.C.buttonRestoreChangeCancel(),
               Gerrit.RESOURCES.css().abandonChangeDialog(), Gerrit.RESOURCES.css().abandonMessage()) {
@@ -633,10 +634,14 @@
     }
   }
 
-  private AsyncCallback<ChangeDetail> createCommentedCallback(final Button b) {
+  private AsyncCallback<ChangeDetail> createCommentedCallback(final Button b, final boolean redirect) {
     return new AsyncCallback<ChangeDetail>() {
       public void onSuccess(ChangeDetail result) {
-        changeScreen.update(result);
+        if (redirect) {
+          Gerrit.display(PageLinks.toChange(result.getChange().getId()));
+        } else {
+          changeScreen.update(result);
+        }
       }
 
       public void onFailure(Throwable caught) {
@@ -644,4 +649,4 @@
       }
     };
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/HistoryTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/HistoryTable.java
index a29acf4..e35097e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/HistoryTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/HistoryTable.java
@@ -98,7 +98,7 @@
 
       fmt.setStyleName(3, col, Gerrit.RESOURCES.css().dataCell());
       if (k.getCommentCount() > 0) {
-        table.setText(3, col, Util.M.patchTableComments(k.getCommentCount()));
+        table.setText(3, col, Integer.toString(k.getCommentCount()));
       }
       col++;
     }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java
index dabd706..11c94a7 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java
@@ -14,17 +14,22 @@
 
 package com.google.gerrit.httpd;
 
+import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
 import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
 
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gwtjsonrpc.server.XsrfException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.lib.Config;
+
 import java.io.IOException;
+import java.util.Locale;
 
 import javax.servlet.Filter;
 import javax.servlet.FilterChain;
@@ -53,12 +58,14 @@
 
   private final Provider<WebSession> session;
   private final AccountCache accountCache;
+  private final Config config;
 
   @Inject
-  ContainerAuthFilter(Provider<WebSession> session, AccountCache accountCache)
-      throws XsrfException {
+  ContainerAuthFilter(Provider<WebSession> session, AccountCache accountCache,
+      @GerritServerConfig Config config) throws XsrfException {
     this.session = session;
     this.accountCache = accountCache;
+    this.config = config;
   }
 
   @Override
@@ -83,9 +90,15 @@
 
   private boolean verify(HttpServletRequest req, HttpServletResponseWrapper rsp)
       throws IOException {
-    final String username = req.getRemoteUser();
-    final AccountState who =
-        (username == null) ? null : accountCache.getByUsername(username);
+    String username = req.getRemoteUser();
+    if (username == null) {
+      rsp.sendError(SC_FORBIDDEN);
+      return false;
+    }
+    if (config.getBoolean("auth", "userNameToLowerCase", false)) {
+      username = username.toLowerCase(Locale.US);
+    }
+    final AccountState who = accountCache.getByUsername(username);
     if (who == null || !who.getAccount().isActive()) {
       rsp.sendError(SC_UNAUTHORIZED);
       return false;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitWebConfig.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitWebConfig.java
index b37a152..4016460 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitWebConfig.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitWebConfig.java
@@ -41,6 +41,7 @@
     final String cfgCgi = cfg.getString("gitweb", null, "cgi");
 
     type = GitWebType.fromName(cfg.getString("gitweb", null, "type"));
+    type.setLinkName(cfg.getString("gitweb", null, "linkname"));
     type.setBranch(cfg.getString("gitweb", null, "branch"));
     type.setProject(cfg.getString("gitweb", null, "project"));
     type.setRevision(cfg.getString("gitweb", null, "revision"));
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java
index 929d034..4289521 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java
@@ -23,18 +23,22 @@
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gwtjsonrpc.server.SignedToken;
 import com.google.gwtjsonrpc.server.XsrfException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.lib.Config;
+
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.Locale;
 import java.util.Map;
 
 import javax.annotation.Nullable;
@@ -67,16 +71,18 @@
   private final Provider<String> urlProvider;
   private final Provider<WebSession> session;
   private final AccountCache accountCache;
+  private final Config config;
   private final SignedToken tokens;
   private ServletContext context;
 
   @Inject
   ProjectDigestFilter(@CanonicalWebUrl @Nullable Provider<String> urlProvider,
-      Provider<WebSession> session, AccountCache accountCache)
-      throws XsrfException {
+      Provider<WebSession> session, AccountCache accountCache,
+      @GerritServerConfig Config config) throws XsrfException {
     this.urlProvider = urlProvider;
     this.session = session;
     this.accountCache = accountCache;
+    this.config = config;
     this.tokens = new SignedToken((int) SECONDS.convert(1, HOURS));
   }
 
@@ -111,7 +117,7 @@
     }
 
     final Map<String, String> p = parseAuthorization(hdr);
-    final String username = p.get("username");
+    final String user = p.get("username");
     final String realm = p.get("realm");
     final String nonce = p.get("nonce");
     final String uri = p.get("uri");
@@ -121,7 +127,7 @@
     final String cnonce = p.get("cnonce");
     final String method = req.getMethod();
 
-    if (username == null //
+    if (user == null //
         || realm == null //
         || nonce == null //
         || uri == null //
@@ -133,6 +139,11 @@
       return false;
     }
 
+    String username = user;
+    if (config.getBoolean("auth", "userNameToLowerCase", false)) {
+      username = username.toLowerCase(Locale.US);
+    }
+
     final AccountState who = accountCache.getByUsername(username);
     if (who == null || ! who.getAccount().isActive()) {
       rsp.sendError(SC_UNAUTHORIZED);
@@ -145,7 +156,7 @@
       return false;
     }
 
-    final String A1 = username + ":" + realm + ":" + passwd;
+    final String A1 = user + ":" + realm + ":" + passwd;
     final String A2 = method + ":" + uri;
     final String expect =
         KD(H(A1), nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + H(A2));
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RevertChange.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RevertChange.java
index 3dfa0e3..5e007fa 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RevertChange.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RevertChange.java
@@ -105,10 +105,10 @@
       throw new NoSuchChangeException(changeId);
     }
 
-    ChangeUtil.revert(patchSetId, currentUser, message, db,
+    Change.Id revertedChangeId = ChangeUtil.revert(patchSetId, currentUser, message, db,
         revertedSenderFactory, hooks, gitManager, patchSetInfoFactory,
         replication, myIdent);
 
-    return changeDetailFactory.create(changeId).call();
+    return changeDetailFactory.create(revertedChangeId).call();
   }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
new file mode 100644
index 0000000..082279c
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
@@ -0,0 +1,145 @@
+// Copyright (C) 2011 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.pgm;
+
+import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
+
+import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.gerrit.pgm.util.SiteProgram;
+import com.google.gerrit.reviewdb.AccountExternalId;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.schema.SchemaVersionCheck;
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.client.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+
+import org.eclipse.jgit.lib.TextProgressMonitor;
+import org.kohsuke.args4j.Option;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+/** Converts the local username for all accounts to lower case */
+public class LocalUsernamesToLowerCase extends SiteProgram {
+  @Option(name = "--threads", usage = "Number of concurrent threads to run")
+  private int threads = 2;
+
+  private final LifecycleManager manager = new LifecycleManager();
+  private final TextProgressMonitor monitor = new TextProgressMonitor();
+  private List<AccountExternalId> todo;
+
+  private Injector dbInjector;
+
+  @Inject
+  private SchemaFactory<ReviewDb> database;
+
+  @Override
+  public int run() throws Exception {
+    if (threads <= 0) {
+      threads = 1;
+    }
+
+    dbInjector = createDbInjector(MULTI_USER);
+    manager.add(dbInjector,
+        dbInjector.createChildInjector(SchemaVersionCheck.module()));
+    manager.start();
+    dbInjector.injectMembers(this);
+
+    final ReviewDb db = database.open();
+    try {
+      todo = db.accountExternalIds().all().toList();
+      synchronized (monitor) {
+        monitor.beginTask("Converting local username", todo.size());
+      }
+    } finally {
+      db.close();
+    }
+
+    final List<Worker> workers = new ArrayList<Worker>(threads);
+    for (int tid = 0; tid < threads; tid++) {
+      Worker t = new Worker();
+      t.start();
+      workers.add(t);
+    }
+    for (Worker t : workers) {
+      t.join();
+    }
+    synchronized (monitor) {
+      monitor.endTask();
+    }
+    manager.stop();
+    return 0;
+  }
+
+  private void convertLocalUserToLowerCase(final ReviewDb db,
+      final AccountExternalId extId) {
+    if (extId.isScheme(AccountExternalId.SCHEME_GERRIT)) {
+      final String localUser = extId.getSchemeRest();
+      final String localUserLowerCase = localUser.toLowerCase(Locale.US);
+      if (!localUser.equals(localUserLowerCase)) {
+        final AccountExternalId.Key extIdKeyLowerCase =
+            new AccountExternalId.Key(AccountExternalId.SCHEME_GERRIT,
+                localUserLowerCase);
+        final AccountExternalId extIdLowerCase =
+            new AccountExternalId(extId.getAccountId(), extIdKeyLowerCase);
+        try {
+          db.accountExternalIds().insert(Collections.singleton(extIdLowerCase));
+          db.accountExternalIds().delete(Collections.singleton(extId));
+        } catch (OrmException error) {
+          System.err.println("ERR " + error.getMessage());
+        }
+      }
+    }
+  }
+
+  private AccountExternalId next() {
+    synchronized (todo) {
+      if (todo.isEmpty()) {
+        return null;
+      }
+      return todo.remove(todo.size() - 1);
+    }
+  }
+
+  private class Worker extends Thread {
+    @Override
+    public void run() {
+      final ReviewDb db;
+      try {
+        db = database.open();
+      } catch (OrmException e) {
+        e.printStackTrace();
+        return;
+      }
+      try {
+        for (;;) {
+          final AccountExternalId extId = next();
+          if (extId == null) {
+            break;
+          }
+          convertLocalUserToLowerCase(db, extId);
+          synchronized (monitor) {
+            monitor.update(1);
+          }
+        }
+      } finally {
+        db.close();
+      }
+    }
+  }
+}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountExternalIdAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountExternalIdAccess.java
index 0719035..4c78139 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountExternalIdAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountExternalIdAccess.java
@@ -42,4 +42,7 @@
   @Query("WHERE emailAddress >= ? AND emailAddress <= ? ORDER BY emailAddress LIMIT ?")
   ResultSet<AccountExternalId> suggestByEmailAddress(String emailA,
       String emailB, int limit) throws OrmException;
+
+  @Query
+  ResultSet<AccountExternalId> all() throws OrmException;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
index 36ac938..89f5ab2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
@@ -256,7 +256,7 @@
     hooks.doChangeAbandonedHook(updatedChange, user.getAccount(), message);
   }
 
-  public static void revert(final PatchSet.Id patchSetId,
+  public static Change.Id revert(final PatchSet.Id patchSetId,
       final IdentifiedUser user, final String message, final ReviewDb db,
       final RevertedSender.Factory revertedSenderFactory,
       final ChangeHookRunner hooks, GitRepositoryManager gitManager,
@@ -352,6 +352,8 @@
       cm.send();
 
       hooks.doPatchsetCreatedHook(change, ps);
+
+      return change.getId();
     } finally {
       revWalk.release();
       git.close();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthRequest.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthRequest.java
index 2a54029..034b176 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthRequest.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthRequest.java
@@ -52,7 +52,7 @@
     return r;
   }
 
-  private final String externalId;
+  private String externalId;
   private String password;
   private String displayName;
   private String emailAddress;
@@ -78,6 +78,14 @@
     return null;
   }
 
+  public void setLocalUser(final String localUser) {
+    if (isScheme(SCHEME_GERRIT)) {
+      final AccountExternalId.Key key =
+          new AccountExternalId.Key(SCHEME_GERRIT, localUser);
+      externalId = key.get();
+    }
+  }
+
   public String getPassword() {
     return password;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembersFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembersFactory.java
index aae3a909..58743df 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembersFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembersFactory.java
@@ -70,16 +70,22 @@
   @Override
   public Set<Account> call() throws NoSuchGroupException,
       NoSuchProjectException, OrmException {
-    if (AccountGroup.PROJECT_OWNERS.equals(groupUUID)) {
-      return getProjectOwners();
-    }
-
-    return getAllGroupMembers(groupCache.get(groupUUID),
-        new HashSet<AccountGroup.Id>());
+    return listAccounts(groupUUID, new HashSet<AccountGroup.UUID>());
   }
 
-  private Set<Account> getProjectOwners() throws NoSuchProjectException,
-      NoSuchGroupException, OrmException {
+  private Set<Account> listAccounts(final AccountGroup.UUID groupUUID,
+      final Set<AccountGroup.UUID> seen) throws NoSuchGroupException,
+      OrmException, NoSuchProjectException {
+    if (AccountGroup.PROJECT_OWNERS.equals(groupUUID)) {
+      return getProjectOwners(seen);
+    } else {
+      return getGroupMembers(groupCache.get(groupUUID), seen);
+    }
+  }
+
+  private Set<Account> getProjectOwners(final Set<AccountGroup.UUID> seen)
+      throws NoSuchProjectException, NoSuchGroupException, OrmException {
+    seen.add(AccountGroup.PROJECT_OWNERS);
     if (project == null) {
       return Collections.emptySet();
     }
@@ -90,16 +96,17 @@
 
     final HashSet<Account> projectOwners = new HashSet<Account>();
     for (final AccountGroup.UUID ownerGroup : ownerGroups) {
-      projectOwners.addAll(getAllGroupMembers(groupCache.get(ownerGroup),
-          new HashSet<AccountGroup.Id>()));
+      if (!seen.contains(ownerGroup)) {
+        projectOwners.addAll(listAccounts(ownerGroup, seen));
+      }
     }
     return projectOwners;
   }
 
-  private Set<Account> getAllGroupMembers(final AccountGroup group,
-      final Set<AccountGroup.Id> seen) throws NoSuchGroupException,
-      OrmException {
-    seen.add(group.getId());
+  private Set<Account> getGroupMembers(final AccountGroup group,
+      final Set<AccountGroup.UUID> seen) throws NoSuchGroupException,
+      OrmException, NoSuchProjectException {
+    seen.add(group.getGroupUUID());
     final GroupDetail groupDetail =
         groupDetailFactory.create(group.getId()).call();
 
@@ -110,10 +117,11 @@
       }
     }
     if (groupDetail.includes != null) {
-      for (AccountGroupInclude groupInclude : groupDetail.includes) {
-        if (!seen.contains(groupInclude.getIncludeId())) {
-          members.addAll(getAllGroupMembers(
-              groupCache.get(groupInclude.getIncludeId()), seen));
+      for (final AccountGroupInclude groupInclude : groupDetail.includes) {
+        final AccountGroup includedGroup =
+            groupCache.get(groupInclude.getIncludeId());
+        if (!seen.contains(includedGroup.getGroupUUID())) {
+          members.addAll(listAccounts(includedGroup.getGroupUUID(), seen));
         }
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
index e804ce6..72d7cb0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
@@ -49,6 +49,7 @@
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 
@@ -67,6 +68,7 @@
   private final EmailExpander emailExpander;
   private final Cache<String, Account.Id> usernameCache;
   private final Set<Account.FieldName> readOnlyAccountFields;
+  private final Config config;
 
   private final Cache<String, Set<AccountGroup.UUID>> membershipCache;
 
@@ -83,6 +85,7 @@
     this.emailExpander = emailExpander;
     this.usernameCache = usernameCache;
     this.membershipCache = membershipCache;
+    this.config = config;
 
     this.readOnlyAccountFields = new HashSet<Account.FieldName>();
 
@@ -181,6 +184,10 @@
 
   public AuthRequest authenticate(final AuthRequest who)
       throws AccountException {
+    if (config.getBoolean("ldap", "localUsernameToLowerCase", false)) {
+      who.setLocalUser(who.getLocalUser().toLowerCase(Locale.US));
+    }
+
     final String username = who.getLocalUser();
     try {
       final DirContext ctx;
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 7886ec8..cf79c6b 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
@@ -527,7 +527,7 @@
 
       // Let the core receive process handle it
     } else {
-      reject(cmd);
+      reject(cmd, "can not create new references");
     }
   }
 
@@ -541,7 +541,7 @@
       validateNewCommits(ctl, cmd);
       // Let the core receive process handle it
     } else {
-      reject(cmd);
+      reject(cmd, "can not update the reference as a fast forward");
     }
   }
 
@@ -569,7 +569,7 @@
     if (ctl.canDelete()) {
       // Let the core receive process handle it
     } else {
-      reject(cmd);
+      reject(cmd, "can not delete references");
     }
   }
 
@@ -665,7 +665,7 @@
         destBranchName.substring(0, split));
     destBranchCtl = projectControl.controlForRef(destBranch);
     if (!destBranchCtl.canUpload()) {
-      reject(cmd);
+      reject(cmd, "can not upload a change to this reference");
       return;
     }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
index 977d209..805549e 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PeerDaemonUser;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.sshd.SshScope.Context;
 import com.google.inject.Inject;
@@ -33,6 +34,7 @@
 import org.apache.sshd.common.util.Buffer;
 import org.apache.sshd.server.PublickeyAuthenticator;
 import org.apache.sshd.server.session.ServerSession;
+import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -47,6 +49,7 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
+import java.util.Locale;
 import java.util.Set;
 
 /**
@@ -61,17 +64,20 @@
   private final SshLog sshLog;
   private final IdentifiedUser.GenericFactory userFactory;
   private final PeerDaemonUser.Factory peerFactory;
+  private final Config config;
   private final Set<PublicKey> myHostKeys;
   private volatile PeerKeyCache peerKeyCache;
 
   @Inject
   DatabasePubKeyAuth(final SshKeyCacheImpl skc, final SshLog l,
       final IdentifiedUser.GenericFactory uf, final PeerDaemonUser.Factory pf,
-      final SitePaths site, final KeyPairProvider hostKeyProvider) {
+      final SitePaths site, final KeyPairProvider hostKeyProvider,
+      final @GerritServerConfig Config cfg) {
     sshKeyCache = skc;
     sshLog = l;
     userFactory = uf;
     peerFactory = pf;
+    config = cfg;
     myHostKeys = myHostKeys(hostKeyProvider);
     peerKeyCache = new PeerKeyCache(site.peer_keys);
   }
@@ -91,7 +97,7 @@
     }
   }
 
-  public boolean authenticate(final String username,
+  public boolean authenticate(String username,
       final PublicKey suppliedKey, final ServerSession session) {
     final SshSession sd = session.getAttribute(SshSession.KEY);
 
@@ -107,6 +113,10 @@
       }
     }
 
+    if (config.getBoolean("auth", "userNameToLowerCase", false)) {
+      username = username.toLowerCase(Locale.US);
+    }
+
     final Iterable<SshKeyCacheEntry> keyList = sshKeyCache.get(username);
     final SshKeyCacheEntry key = find(keyList, suppliedKey);
     if (key == null) {