Merge changes from topic 'arbitrary-download-schemes'

* changes:
  Sort download schemes and command names
  SetPreferences: Enforce download scheme is registered
  Describe allowed values for download_scheme field
  DownloadUrlLink: Kill KnownScheme enum
  Store preferred download scheme as arbitrary strings
  DownloadConfig: Make set methods return ImmutableSet
  Remove DEFAULT_SCHEMES and DEFAULT_COMMANDS
diff --git a/Documentation/config-mail.txt b/Documentation/config-mail.txt
index 62d0219..da213a8 100644
--- a/Documentation/config-mail.txt
+++ b/Documentation/config-mail.txt
@@ -28,6 +28,12 @@
 to a change being abandoned.  It is a `ChangeEmail`: see `ChangeSubject.vm` and
 `ChangeFooter.vm`.
 
+=== AddKey.vm
+
+The `AddKey.vm` template will determine the contents of the email related to
+SSH and GPG keys being added to a user account. This notification is not sent
+when the key is administratively added to another user account.
+
 === ChangeFooter.vm
 
 The `ChangeFooter.vm` template will determine the contents of the footer
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index adf77cf..eda4b5d 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -367,6 +367,19 @@
 Changes where 'USER' has commented on the change more recently than the
 last update (comment or patch set) from the change owner.
 
+[[author]]
+author:'AUTHOR'::
++
+Changes where 'AUTHOR' is the author of the current patch set. 'AUTHOR' may be
+the author's exact email address, or part of the name or email address.
+
+[[committer]]
+committer:'COMMITTER'::
++
+Changes where 'COMMITTER' is the committer of the current patch set.
+'COMMITTER' may be the committer's exact email address, or part of the name or
+email address.
+
 
 == Argument Quoting
 
diff --git a/gerrit-acceptance-tests/BUCK b/gerrit-acceptance-tests/BUCK
index 833070b..3adab73 100644
--- a/gerrit-acceptance-tests/BUCK
+++ b/gerrit-acceptance-tests/BUCK
@@ -44,6 +44,7 @@
     '//lib/mina:sshd',
   ],
   visibility = [
+    '//gerrit-plugin-api/...',
     '//tools/eclipse:classpath',
     '//gerrit-acceptance-tests/...',
   ],
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 18ee2ef..8d97136 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -76,6 +76,7 @@
 import org.junit.AfterClass;
 import org.junit.Rule;
 import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
 import org.junit.rules.TestRule;
 import org.junit.runner.Description;
 import org.junit.runner.RunWith;
@@ -193,6 +194,9 @@
     }
   };
 
+  @Rule
+  public TemporaryFolder tempSiteDir = new TemporaryFolder();
+
   @AfterClass
   public static void stopCommonServer() throws Exception {
     if (commonServer != null) {
@@ -222,12 +226,14 @@
     return cfg.getBoolean("change", null, "submitWholeTopic", false);
   }
 
-  private void beforeTest(Description description) throws Exception {
+  protected void beforeTest(Description description) throws Exception {
     GerritServer.Description classDesc =
       GerritServer.Description.forTestClass(description, configName);
     GerritServer.Description methodDesc =
       GerritServer.Description.forTestMethod(description, configName);
 
+    baseConfig.setString("gerrit", null, "tempSiteDir",
+        tempSiteDir.getRoot().getPath());
     if (classDesc.equals(methodDesc)) {
       if (commonServer == null) {
         commonServer = GerritServer.start(classDesc, baseConfig);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
index 8f4c2d4..634db7c 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
@@ -62,7 +62,7 @@
     // TODO(dborowitz): Use jimfs.
     bind(Path.class)
       .annotatedWith(SitePath.class)
-      .toInstance(Paths.get("UNIT_TEST_GERRIT_SITE"));
+      .toInstance(Paths.get(cfg.getString("gerrit", null, "tempSiteDir")));
 
     bind(GitRepositoryManager.class)
       .toInstance(new InMemoryRepositoryManager());
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/PluginDaemonTest.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/PluginDaemonTest.java
new file mode 100644
index 0000000..4913488
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/PluginDaemonTest.java
@@ -0,0 +1,218 @@
+// Copyright (C) 2015 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.acceptance;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Strings;
+import com.google.gerrit.server.config.SitePaths;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.junit.runner.Description;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.ProcessBuilder.Redirect;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.util.Properties;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public abstract class PluginDaemonTest extends AbstractDaemonTest {
+
+  private static final String BUCKLC = "buck";
+  private static final String BUCKOUT = "buck-out";
+
+  private Path gen;
+  private Path testSite;
+  private Path pluginRoot;
+  private Path pluginsSitePath;
+  private Path pluginSubPath;
+  private Path pluginSource;
+  private String pluginName;
+  private boolean standalone;
+
+  @Override
+  protected void beforeTest(Description description) throws Exception {
+    locatePaths();
+    retrievePluginName();
+    buildPluginJar();
+    createTestSiteDirs();
+    copyJarToTestSite();
+    super.beforeTest(description);
+  }
+
+  protected void setPluginConfigString(String name, String value)
+      throws IOException, ConfigInvalidException {
+    SitePaths sitePath = new SitePaths(testSite);
+    FileBasedConfig cfg = getGerritConfigFile(sitePath);
+    cfg.load();
+    cfg.setString("plugin", pluginName, name, value);
+    cfg.save();
+  }
+
+  private FileBasedConfig getGerritConfigFile(SitePaths sitePath)
+      throws IOException {
+    FileBasedConfig cfg =
+        new FileBasedConfig(sitePath.gerrit_config.toFile(), FS.DETECTED);
+    if (!cfg.getFile().exists()) {
+      Path etc_path = Files.createDirectories(sitePath.etc_dir);
+      Files.createFile(etc_path.resolve("gerrit.config"));
+    }
+    return cfg;
+  }
+
+  private void locatePaths() {
+    URL pluginClassesUrl =
+        getClass().getProtectionDomain().getCodeSource().getLocation();
+    Path basePath = Paths.get(pluginClassesUrl.getPath()).getParent();
+
+    int idx = 0;
+    int buckOutIdx = 0;
+    int pluginsIdx = 0;
+    for (Path subPath : basePath) {
+      if (subPath.endsWith("plugins")) {
+        pluginsIdx = idx;
+      }
+      if (subPath.endsWith(BUCKOUT)) {
+        buckOutIdx = idx;
+      }
+      idx++;
+    }
+    standalone = checkStandalone(basePath);
+    pluginRoot = basePath.getRoot().resolve(basePath.subpath(0, buckOutIdx));
+    gen = pluginRoot.resolve(BUCKOUT).resolve("gen");
+
+    if (standalone) {
+      pluginSource = pluginRoot;
+    } else {
+      pluginSubPath = basePath.subpath(pluginsIdx, pluginsIdx + 2);
+      pluginSource = pluginRoot.resolve(pluginSubPath);
+    }
+  }
+
+  private boolean checkStandalone(Path basePath) {
+    String pathCharStringOrNone = "[a-zA-Z0-9._-]*?";
+    Pattern pattern = Pattern.compile(pathCharStringOrNone + "gerrit" +
+        pathCharStringOrNone);
+    Path partialPath = basePath;
+    for (int i = basePath.getNameCount(); i > 0; i--) {
+      int count = partialPath.getNameCount();
+      if (count > 1) {
+        String gerritDirCandidate =
+            partialPath.subpath(count - 2, count - 1).toString();
+        if (pattern.matcher(gerritDirCandidate).matches()) {
+          if (partialPath.endsWith(gerritDirCandidate + "/" + BUCKOUT)) {
+            return false;
+          }
+        }
+      }
+      partialPath = partialPath.getParent();
+    }
+    return true;
+  }
+
+  private void retrievePluginName() throws IOException {
+    Path buckFile = pluginSource.resolve("BUCK");
+    byte[] bytes = Files.readAllBytes(buckFile);
+    String buckContent =
+        new String(bytes, StandardCharsets.UTF_8).replaceAll("\\s+", "");
+    Matcher matcher =
+        Pattern.compile("gerrit_plugin\\(name='(.*?)'").matcher(buckContent);
+    if (matcher.find()) {
+      pluginName = matcher.group(1);
+    }
+    if (Strings.isNullOrEmpty(pluginName)) {
+      if (standalone) {
+        pluginName = pluginRoot.getFileName().toString();
+      } else {
+        pluginName = pluginSubPath.getFileName().toString();
+      }
+    }
+  }
+
+  private void buildPluginJar() throws IOException, InterruptedException {
+    Properties properties = loadBuckProperties();
+    String buck =
+        MoreObjects.firstNonNull(properties.getProperty(BUCKLC), BUCKLC);
+    String target;
+    if (standalone) {
+      target = "//:" + pluginName;
+    } else {
+      target = pluginSubPath.toString();
+    }
+
+    ProcessBuilder processBuilder =
+        new ProcessBuilder(buck, "build", target).directory(pluginRoot.toFile())
+            .redirectErrorStream(true);
+    // otherwise plugin jar creation fails:
+    processBuilder.environment().put("NO_BUCKD", "1");
+
+    Path forceJar = pluginSource.resolve("src/main/java/ForceJarIfMissing.java");
+    // if exists after cancelled test:
+    Files.deleteIfExists(forceJar);
+
+    Files.createFile(forceJar);
+    testSite = tempSiteDir.getRoot().toPath();
+
+    // otherwise process often hangs:
+    Path log = testSite.resolve("log");
+    processBuilder.redirectErrorStream(true);
+    processBuilder.redirectOutput(Redirect.appendTo(log.toFile()));
+
+    try {
+      processBuilder.start().waitFor();
+    } finally {
+      Files.delete(forceJar);
+      // otherwise jar not made next time if missing again:
+      processBuilder.start().waitFor();
+    }
+  }
+
+  private Properties loadBuckProperties() throws IOException {
+    Properties properties = new Properties();
+    Path propertiesPath = gen.resolve("tools").resolve("buck.properties");
+    if (Files.exists(propertiesPath)) {
+      try (InputStream in = Files.newInputStream(propertiesPath)) {
+        properties.load(in);
+      }
+    }
+    return properties;
+  }
+
+  private void createTestSiteDirs() throws IOException {
+    SitePaths sitePath = new SitePaths(testSite);
+    pluginsSitePath = Files.createDirectories(sitePath.plugins_dir);
+    Files.createDirectories(sitePath.tmp_dir);
+  }
+
+  private void copyJarToTestSite() throws IOException {
+    Path pluginOut;
+    if (standalone) {
+      pluginOut = gen;
+    } else {
+      pluginOut = gen.resolve(pluginSubPath);
+    }
+    Path jar = pluginOut.resolve(pluginName + ".jar");
+    Path dest = pluginsSitePath.resolve(jar.getFileName());
+    Files.copy(jar, dest, StandardCopyOption.REPLACE_EXISTING);
+  }
+}
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java
index 670adba..80e3500 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java
@@ -27,6 +27,7 @@
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 import com.google.common.io.BaseEncoding;
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.common.GpgKeyInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -41,6 +42,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.mail.AddKeySender;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -54,6 +56,8 @@
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.RefUpdate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
@@ -71,20 +75,24 @@
     public List<String> delete;
   }
 
+  private final Logger log = LoggerFactory.getLogger(getClass());
   private final Provider<PersonIdent> serverIdent;
   private final Provider<ReviewDb> db;
   private final Provider<PublicKeyStore> storeProvider;
   private final PublicKeyChecker checker;
+  private final AddKeySender.Factory addKeyFactory;
 
   @Inject
   PostGpgKeys(@GerritPersonIdent Provider<PersonIdent> serverIdent,
       Provider<ReviewDb> db,
       Provider<PublicKeyStore> storeProvider,
-      PublicKeyChecker checker) {
+      PublicKeyChecker checker,
+      AddKeySender.Factory addKeyFactory) {
     this.serverIdent = serverIdent;
     this.db = db;
     this.storeProvider = storeProvider;
     this.checker = checker;
+    this.addKeyFactory = addKeyFactory;
   }
 
   @Override
@@ -180,6 +188,7 @@
       Set<Fingerprint> toRemove) throws BadRequestException,
       ResourceConflictException, PGPException, IOException {
     try (PublicKeyStore store = storeProvider.get()) {
+      List<String> addedKeys = new ArrayList<>();
       for (PGPPublicKeyRing keyRing : keyRings) {
         PGPPublicKey key = keyRing.getPublicKey();
         CheckResult result = checker.check(key);
@@ -188,6 +197,7 @@
               "Problems with public key %s:\n%s",
               keyToString(key), Joiner.on('\n').join(result.getProblems())));
         }
+        addedKeys.add(PublicKeyStore.keyToString(key));
         store.add(keyRing);
       }
       for (Fingerprint fp : toRemove) {
@@ -204,6 +214,13 @@
         case NEW:
         case FAST_FORWARD:
         case FORCED:
+          try {
+            addKeyFactory.create(rsrc.getUser(), addedKeys).send();
+          } catch (EmailException e) {
+            log.error("Cannot send GPG key added message to "
+                + rsrc.getUser().getAccount().getPreferredEmail(), e);
+          }
+          break;
         case NO_CHANGE:
           break;
         default:
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
index 20afa19..6c2fd04 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
@@ -72,6 +72,8 @@
     suggestions.add("owner:");
     suggestions.add("owner:self");
     suggestions.add("ownerin:");
+    suggestions.add("author:");
+    suggestions.add("committer:");
 
     suggestions.add("reviewer:");
     suggestions.add("reviewer:self");
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
index 8a227ac..6270a15 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
@@ -100,6 +100,7 @@
     chmod(0700, site.tmp_dir);
 
     extractMailExample("Abandoned.vm");
+    extractMailExample("AddKey.vm");
     extractMailExample("ChangeFooter.vm");
     extractMailExample("ChangeSubject.vm");
     extractMailExample("Comment.vm");
diff --git a/gerrit-plugin-api/BUCK b/gerrit-plugin-api/BUCK
index 6a4e4c0..ed11e0f 100644
--- a/gerrit-plugin-api/BUCK
+++ b/gerrit-plugin-api/BUCK
@@ -20,6 +20,7 @@
 java_library(
   name = 'lib',
   exported_deps = PLUGIN_API + [
+    '//gerrit-acceptance-tests:lib',
     '//gerrit-antlr:query_exception',
     '//gerrit-antlr:query_parser',
     '//gerrit-common:annotations',
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java
index 3c21d17..7ec659e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java
@@ -18,6 +18,7 @@
 
 import com.google.common.collect.Iterables;
 import com.google.common.io.ByteSource;
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.common.errors.InvalidSshKeyException;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -30,6 +31,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AddSshKey.Input;
 import com.google.gerrit.server.account.GetSshKeys.SshKeyInfo;
+import com.google.gerrit.server.mail.AddKeySender;
 import com.google.gerrit.server.ssh.SshKeyCache;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
@@ -37,12 +39,17 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.Collections;
 
 @Singleton
 public class AddSshKey implements RestModifyView<AccountResource, Input> {
+  private static final Logger log = LoggerFactory.getLogger(AddSshKey.class);
+
   public static class Input {
     public RawInput raw;
   }
@@ -50,13 +57,15 @@
   private final Provider<CurrentUser> self;
   private final Provider<ReviewDb> dbProvider;
   private final SshKeyCache sshKeyCache;
+  private final AddKeySender.Factory addKeyFactory;
 
   @Inject
   AddSshKey(Provider<CurrentUser> self, Provider<ReviewDb> dbProvider,
-      SshKeyCache sshKeyCache) {
+      SshKeyCache sshKeyCache, AddKeySender.Factory addKeyFactory) {
     this.self = self;
     this.dbProvider = dbProvider;
     this.sshKeyCache = sshKeyCache;
+    this.addKeyFactory = addKeyFactory;
   }
 
   @Override
@@ -96,6 +105,12 @@
           sshKeyCache.create(new AccountSshKey.Id(
               user.getAccountId(), max + 1), sshPublicKey);
       dbProvider.get().accountSshKeys().insert(Collections.singleton(sshKey));
+      try {
+        addKeyFactory.create(user, sshKey).send();
+      } catch (EmailException e) {
+        log.error("Cannot send SSH key added message to "
+            + user.getAccount().getPreferredEmail(), e);
+      }
       sshKeyCache.evict(user.getUserName());
       return Response.<SshKeyInfo>created(new SshKeyInfo(sshKey));
     } catch (InvalidSshKeyException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index cf1053d..3964115 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -95,6 +95,7 @@
 import com.google.gerrit.server.group.GroupModule;
 import com.google.gerrit.server.index.ReindexAfterUpdate;
 import com.google.gerrit.server.mail.AddReviewerSender;
+import com.google.gerrit.server.mail.AddKeySender;
 import com.google.gerrit.server.mail.CreateChangeSender;
 import com.google.gerrit.server.mail.EmailModule;
 import com.google.gerrit.server.mail.FromAddressGenerator;
@@ -186,6 +187,7 @@
 
     factory(AccountInfoCacheFactory.Factory.class);
     factory(AddReviewerSender.Factory.class);
+    factory(AddKeySender.Factory.class);
     factory(CapabilityControl.Factory.class);
     factory(ChangeData.Factory.class);
     factory(ChangeJson.Factory.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
index 5d7229a..e711306 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.MoreObjects.firstNonNull;
 
+import com.google.common.base.CharMatcher;
 import com.google.common.base.Function;
 import com.google.common.base.Splitter;
 import com.google.common.collect.ImmutableList;
@@ -38,12 +39,14 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.protobuf.CodedOutputStream;
 
+import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.revwalk.FooterLine;
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
@@ -404,6 +407,64 @@
         }
       };
 
+  private static Set<String> getPersonParts(PersonIdent person) {
+    if (person == null) {
+      return ImmutableSet.of();
+    }
+    HashSet<String> parts = Sets.newHashSet();
+    String email = person.getEmailAddress().toLowerCase();
+    parts.add(email);
+    parts.addAll(Arrays.asList(email.split("@")));
+    Splitter s = Splitter.on(CharMatcher.anyOf("@.- ")).omitEmptyStrings();
+    Iterables.addAll(parts, s.split(email));
+    Iterables.addAll(parts, s.split(person.getName().toLowerCase()));
+    return parts;
+  }
+
+  public static Set<String> getAuthorParts(ChangeData cd) throws OrmException {
+    try {
+      return getPersonParts(cd.getAuthor());
+    } catch (IOException e) {
+      throw new OrmException(e);
+    }
+  }
+
+  public static Set<String> getCommitterParts(ChangeData cd) throws OrmException {
+    try {
+      return getPersonParts(cd.getCommitter());
+    } catch (IOException e) {
+      throw new OrmException(e);
+    }
+  }
+
+  /**
+   * The exact email address, or any part of the author name or email address,
+   * in the current patch set.
+   */
+  public static final FieldDef<ChangeData, Iterable<String>> AUTHOR =
+      new FieldDef.Repeatable<ChangeData, String>(
+          ChangeQueryBuilder.FIELD_AUTHOR, FieldType.FULL_TEXT, false) {
+        @Override
+        public Iterable<String> get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return getAuthorParts(input);
+        }
+      };
+
+  /**
+   * The exact email address, or any part of the committer name or email address,
+   * in the current patch set.
+   */
+  public static final FieldDef<ChangeData, Iterable<String>> COMMITTER =
+      new FieldDef.Repeatable<ChangeData, String>(
+          ChangeQueryBuilder.FIELD_COMMITTER, FieldType.FULL_TEXT, false) {
+        @Override
+        public Iterable<String> get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return getCommitterParts(input);
+        }
+      };
+
   public static class ChangeProtoField extends FieldDef.Single<ChangeData, byte[]> {
     public static final ProtobufCodec<Change> CODEC =
         CodecFactory.encoder(Change.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
index 4915a663..a8a97a8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
@@ -308,6 +308,7 @@
       ChangeField.REVIEWEDBY,
       ChangeField.EXACT_COMMIT);
 
+  @Deprecated
   static final Schema<ChangeData> V23 = schema(
       ChangeField.LEGACY_ID2,
       ChangeField.ID,
@@ -341,6 +342,40 @@
       ChangeField.REVIEWEDBY,
       ChangeField.EXACT_COMMIT);
 
+  static final Schema<ChangeData> V24 = schema(
+      ChangeField.LEGACY_ID2,
+      ChangeField.ID,
+      ChangeField.STATUS,
+      ChangeField.PROJECT,
+      ChangeField.PROJECTS,
+      ChangeField.REF,
+      ChangeField.EXACT_TOPIC,
+      ChangeField.FUZZY_TOPIC,
+      ChangeField.UPDATED,
+      ChangeField.FILE_PART,
+      ChangeField.PATH,
+      ChangeField.OWNER,
+      ChangeField.REVIEWER,
+      ChangeField.COMMIT,
+      ChangeField.TR,
+      ChangeField.LABEL,
+      ChangeField.COMMIT_MESSAGE,
+      ChangeField.COMMENT,
+      ChangeField.CHANGE,
+      ChangeField.APPROVAL,
+      ChangeField.MERGEABLE,
+      ChangeField.ADDED,
+      ChangeField.DELETED,
+      ChangeField.DELTA,
+      ChangeField.HASHTAG,
+      ChangeField.COMMENTBY,
+      ChangeField.PATCH_SET,
+      ChangeField.GROUP,
+      ChangeField.EDITBY,
+      ChangeField.REVIEWEDBY,
+      ChangeField.EXACT_COMMIT,
+      ChangeField.AUTHOR,
+      ChangeField.COMMITTER);
 
   private static Schema<ChangeData> schema(Collection<FieldDef<ChangeData, ?>> fields) {
     return new Schema<>(ImmutableList.copyOf(fields));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddKeySender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddKeySender.java
new file mode 100644
index 0000000..0f1e86e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddKeySender.java
@@ -0,0 +1,113 @@
+// Copyright (C) 2015 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.mail;
+
+import com.google.common.base.Joiner;
+import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+
+import java.util.List;
+
+public class AddKeySender extends OutgoingEmail {
+  public interface Factory {
+    public AddKeySender create(IdentifiedUser user, AccountSshKey sshKey);
+
+    public AddKeySender create(IdentifiedUser user, List<String> gpgKey);
+  }
+
+  private final IdentifiedUser callingUser;
+  private final IdentifiedUser user;
+  private final AccountSshKey sshKey;
+  private final List<String> gpgKeys;
+
+  @AssistedInject
+  public AddKeySender(EmailArguments ea,
+      IdentifiedUser callingUser,
+      @Assisted IdentifiedUser user,
+      @Assisted AccountSshKey sshKey) {
+    super(ea, "addkey");
+    this.callingUser = callingUser;
+    this.user = user;
+    this.sshKey = sshKey;
+    this.gpgKeys = null;
+  }
+
+  @AssistedInject
+  public AddKeySender(EmailArguments ea,
+      IdentifiedUser callingUser,
+      @Assisted IdentifiedUser user,
+      @Assisted List<String> gpgKeys) {
+    super(ea, "addkey");
+    this.callingUser = callingUser;
+    this.user = user;
+    this.sshKey = null;
+    this.gpgKeys = gpgKeys;
+  }
+
+  @Override
+  protected void init() throws EmailException {
+    super.init();
+    setHeader("Subject",
+        String.format("[Gerrit Code Review] New %s Keys Added", getKeyType()));
+    add(RecipientType.TO, new Address(getEmail()));
+  }
+
+  @Override
+  protected boolean shouldSendMessage() {
+    /*
+     * Don't send an email if no keys are added, or an admin is adding a key to
+     * a user.
+     */
+    return (sshKey != null || gpgKeys.size() > 0) &&
+        (user.equals(callingUser) ||
+        !callingUser.getCapabilities().canAdministrateServer());
+  }
+
+  @Override
+  protected void format() throws EmailException {
+    appendText(velocifyFile("AddKey.vm"));
+  }
+
+  public String getEmail() {
+    return user.getAccount().getPreferredEmail();
+  }
+
+  public String getUserNameEmail() {
+    return getUserNameEmailFor(user.getAccountId());
+  }
+
+  public String getKeyType() {
+    if (sshKey != null) {
+      return "SSH";
+    } else if (gpgKeys != null) {
+      return "GPG";
+    }
+    return "Unknown";
+  }
+
+  public String getSshKey() {
+    return (sshKey != null) ? sshKey.getSshPublicKey() + "\n" : null;
+  }
+
+  public String getGpgKeys() {
+    if (gpgKeys != null) {
+      return Joiner.on("\n").join(gpgKeys);
+    }
+    return null;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
index 1e4fec7..a2f369b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
@@ -268,6 +268,13 @@
     return name;
   }
 
+  /**
+   * Gets the human readable name and email for an account;
+   * if neither are available, returns the Anonymous Coward name.
+   *
+   * @param accountId user to fetch.
+   * @return name/email of account, or Anonymous Coward if unset.
+   */
   public String getNameEmailFor(Account.Id accountId) {
     AccountState who = args.accountCache.get(accountId);
     String name = who.getAccount().getFullName();
@@ -286,6 +293,33 @@
     }
   }
 
+  /**
+   * Gets the human readable name and email for an account;
+   * if both are unavailable, returns the username.  If no
+   * username is set, this function returns null.
+   *
+   * @param accountId user to fetch.
+   * @return name/email of account, username, or null if unset.
+   */
+  public String getUserNameEmailFor(Account.Id accountId) {
+    AccountState who = args.accountCache.get(accountId);
+    String name = who.getAccount().getFullName();
+    String email = who.getAccount().getPreferredEmail();
+
+    if (name != null && email != null) {
+      return name + " <" + email + ">";
+    } else if (email != null) {
+      return email;
+    } else if (name != null) {
+      return name;
+    }
+    String username = who.getUserName();
+    if (username != null) {
+      return username;
+    }
+    return null;
+  }
+
   protected boolean shouldSendMessage() {
     if (body.length() == 0) {
       // If we have no message body, don't send.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java
index eb32700..beada69 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java
@@ -58,22 +58,7 @@
   }
 
   public String getUserNameEmail() {
-    String name = user.getAccount().getFullName();
-    String email = user.getAccount().getPreferredEmail();
-
-    if (name != null && email != null) {
-      return name + " <" + email + ">";
-    } else if (email != null) {
-      return email;
-    } else if (name != null) {
-      return name;
-    } else {
-      String username = user.getUserName();
-      if (username != null) {
-        return username;
-      }
-    }
-    return null;
+    return getUserNameEmailFor(user.getAccountId());
   }
 
   public String getEmailRegistrationToken() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AuthorPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AuthorPredicate.java
new file mode 100644
index 0000000..193a061
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AuthorPredicate.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import static com.google.gerrit.server.index.ChangeField.AUTHOR;
+import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_AUTHOR;
+
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gwtorm.server.OrmException;
+
+public class AuthorPredicate extends IndexPredicate<ChangeData>  {
+  AuthorPredicate(String value) {
+    super(AUTHOR, FIELD_AUTHOR, value);
+  }
+
+  @Override
+  public boolean match(ChangeData object) throws OrmException {
+    return ChangeField.getAuthorParts(object).contains(
+        getValue().toLowerCase());
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index 0523d73..8061a26 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -62,6 +62,7 @@
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.FooterLine;
@@ -317,6 +318,8 @@
   private Boolean mergeable;
   private Set<Account.Id> editsByUser;
   private Set<Account.Id> reviewedBy;
+  private PersonIdent author;
+  private PersonIdent committer;
 
   @AssistedInject
   private ChangeData(
@@ -620,6 +623,24 @@
     return commitFooters;
   }
 
+  public PersonIdent getAuthor() throws IOException, OrmException {
+    if (author == null) {
+      if (!loadCommitData()) {
+        return null;
+      }
+    }
+    return author;
+  }
+
+  public PersonIdent getCommitter() throws IOException, OrmException {
+    if (committer == null) {
+      if (!loadCommitData()) {
+        return null;
+      }
+    }
+    return committer;
+  }
+
   private boolean loadCommitData() throws OrmException,
       RepositoryNotFoundException, IOException, MissingObjectException,
       IncorrectObjectTypeException {
@@ -633,6 +654,8 @@
       RevCommit c = walk.parseCommit(ObjectId.fromString(sha1));
       commitMessage = c.getFullMessage();
       commitFooters = c.getFooterLines();
+      author = c.getAuthorIdent();
+      committer = c.getCommitterIdent();
     }
     return true;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index c6ffc27..776a7f6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -91,12 +91,14 @@
   public static final String FIELD_ADDED = "added";
   public static final String FIELD_AFTER = "after";
   public static final String FIELD_AGE = "age";
+  public static final String FIELD_AUTHOR = "author";
   public static final String FIELD_BEFORE = "before";
   public static final String FIELD_BRANCH = "branch";
   public static final String FIELD_CHANGE = "change";
   public static final String FIELD_COMMENT = "comment";
   public static final String FIELD_COMMENTBY = "commentby";
   public static final String FIELD_COMMIT = "commit";
+  public static final String FIELD_COMMITTER = "committer";
   public static final String FIELD_CONFLICTS = "conflicts";
   public static final String FIELD_DELETED = "deleted";
   public static final String FIELD_DELTA = "delta";
@@ -843,6 +845,16 @@
     throw new QueryParseException("Unknown named destination: " + name);
   }
 
+  @Operator
+  public Predicate<ChangeData> author(String who) {
+    return new AuthorPredicate(who);
+  }
+
+  @Operator
+  public Predicate<ChangeData> committer(String who) {
+    return new CommitterPredicate(who);
+  }
+
   @Override
   protected Predicate<ChangeData> defaultField(String query) throws QueryParseException {
     if (query.startsWith("refs/")) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitterPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitterPredicate.java
new file mode 100644
index 0000000..e5d9529
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitterPredicate.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import static com.google.gerrit.server.index.ChangeField.COMMITTER;
+import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_COMMITTER;
+
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gwtorm.server.OrmException;
+
+public class CommitterPredicate extends IndexPredicate<ChangeData>  {
+  CommitterPredicate(String value) {
+    super(COMMITTER, FIELD_COMMITTER, value);
+  }
+
+  @Override
+  public boolean match(ChangeData object) throws OrmException {
+    return ChangeField.getCommitterParts(object).contains(
+        getValue().toLowerCase());
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AddKey.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AddKey.vm
new file mode 100644
index 0000000..c60ce8b
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AddKey.vm
@@ -0,0 +1,61 @@
+## Copyright (C) 2015 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.
+##
+##
+## Template Type:
+## -------------
+## This is a velocity mail template, see: http://velocity.apache.org and the
+## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates.
+##
+## Template File Names and extensions:
+## ----------------------------------
+## Gerrit will use templates ending in ".vm" but will ignore templates ending
+## in ".vm.example".  If a .vm template does not exist, the default internal
+## gerrit template which is the same as the .vm.example will be used.  If you
+## want to override the default template, copy the .vm.example file to a .vm
+## file and edit it appropriately.
+##
+## This Template:
+## --------------
+## The AddKey.vm template will determine the contents of the email
+## related to adding a new SSH or GPG key to an account.
+##
+One or more new ${email.keyType} keys have been added to Gerrit Code Review at ${email.gerritHost}:
+
+#if($email.sshKey)
+$email.sshKey
+#elseif($email.gpgKeys)
+$email.gpgKeys
+#end
+
+If this is not expected, please contact your Gerrit Administrators
+immediately.
+
+You can also manage your ${email.keyType} keys by visiting
+#if($email.sshKey)
+$email.gerritUrl#/settings/ssh-keys
+#elseif($email.gpgKeys)
+$email.gerritUrl#/settings/gpg-keys
+#end
+#if($email.userNameEmail)
+(while signed in as $email.userNameEmail)
+#else
+(while signed in as $email.email)
+#end
+
+If clicking the link above does not work, copy and paste the URL in a
+new browser window instead.
+
+This is a send-only email address.  Replies to this message will not
+be read or answered.
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 27c3443..499caa2 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -380,6 +380,52 @@
   }
 
   @Test
+  public void byAuthor() throws Exception {
+    TestRepository<Repo> repo = createProject("repo");
+    Change change1 = newChange(repo, null, null, userId.get(), null).insert();
+
+    // By exact email address
+    assertQuery("author:jauthor@example.com", change1);
+
+    // By email address part
+    assertQuery("author:jauthor", change1);
+    assertQuery("author:example", change1);
+    assertQuery("author:example.com", change1);
+
+    // By name part
+    assertQuery("author:Author", change1);
+
+    // By non-existing email address / name / part
+    assertQuery("author:jcommitter@example.com");
+    assertQuery("author:somewhere.com");
+    assertQuery("author:jcommitter");
+    assertQuery("author:Committer");
+  }
+
+  @Test
+  public void byCommitter() throws Exception {
+    TestRepository<Repo> repo = createProject("repo");
+    Change change1 = newChange(repo, null, null, userId.get(), null).insert();
+
+    // By exact email address
+    assertQuery("committer:jcommitter@example.com", change1);
+
+    // By email address part
+    assertQuery("committer:jcommitter", change1);
+    assertQuery("committer:example", change1);
+    assertQuery("committer:example.com", change1);
+
+    // By name part
+    assertQuery("committer:Committer", change1);
+
+    // By non-existing email address / name / part
+    assertQuery("committer:jauthor@example.com");
+    assertQuery("committer:somewhere.com");
+    assertQuery("committer:jauthor");
+    assertQuery("committer:Author");
+  }
+
+  @Test
   public void byOwnerIn() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
     Change change1 = newChange(repo, null, null, userId.get(), null).insert();
diff --git a/lib/bouncycastle/BUCK b/lib/bouncycastle/BUCK
index 0ce5817..ff6e6c5 100644
--- a/lib/bouncycastle/BUCK
+++ b/lib/bouncycastle/BUCK
@@ -9,6 +9,7 @@
   id = 'org.bouncycastle:bcprov-jdk15on:' + VERSION,
   sha1 = '88a941faf9819d371e3174b5ed56a3f3f7d73269',
   license = 'DO_NOT_DISTRIBUTE', #'bouncycastle'
+  exclude = ['META-INF/BCKEY.*'],
 )
 
 maven_jar(
@@ -16,6 +17,7 @@
   id = 'org.bouncycastle:bcpg-jdk15on:' + VERSION,
   sha1 = 'ff4665a4b5633ff6894209d5dd10b7e612291858',
   license = 'DO_NOT_DISTRIBUTE', #'bouncycastle'
+  exclude = ['META-INF/BCKEY.*'],
   deps = [':bcprov'],
 )
 
diff --git a/plugins/cookbook-plugin b/plugins/cookbook-plugin
index b9d3ca8..ec6ed89 160000
--- a/plugins/cookbook-plugin
+++ b/plugins/cookbook-plugin
@@ -1 +1 @@
-Subproject commit b9d3ca8a65030071e28be19296ba867ab424fbbf
+Subproject commit ec6ed89c47ba7223f82d9cb512926a6c5081343e