Merge branch 'stable-2.12'

* stable-2.12:
  Add 2.12 release notes to release notes index
  Make email validation case insensitive
  Add tooltip with subject message in related changes tab
  Correct timezone logged for DST changes
  Add hmac-sha2-256 and hmac-sha2-512 as MACs for sshd
  Fix double slash on URL when switching account.
  commit-msg: Do not add Change-Id to temp commits
  Use image instead of Unicode Character for Copy Button

Change-Id: I6dc9c93e0962a19e2c32dbcf86f0389b5e2a7acc
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index db52a1b..359f281 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -3515,7 +3515,8 @@
 are enabled in addition to the default MACs, MAC names starting with
 `-` are removed from the default MACs.
 +
-Supported MACs: hmac-md5, hmac-md5-96, hmac-sha1, hmac-sha1-96.
+Supported MACs: hmac-md5, hmac-md5-96, hmac-sha1, hmac-sha1-96,
+hmac-sha2-256, hmac-sha2-512.
 +
 By default, all supported MACs are available.
 
diff --git a/ReleaseNotes/index.txt b/ReleaseNotes/index.txt
index e1831ad..05368c8 100644
--- a/ReleaseNotes/index.txt
+++ b/ReleaseNotes/index.txt
@@ -1,6 +1,11 @@
 Gerrit Code Review - Release Notes
 ==================================
 
+[[2_12]]
+Version 2.12.x
+--------------
+* link:ReleaseNotes-2.12.html[2.12]
+
 [[2_11]]
 Version 2.11.x
 --------------
diff --git a/gerrit-gwtexpui/BUCK b/gerrit-gwtexpui/BUCK
index 189445a..4b2cb03 100644
--- a/gerrit-gwtexpui/BUCK
+++ b/gerrit-gwtexpui/BUCK
@@ -7,6 +7,7 @@
   resources = [
     SRC + 'clippy/client/clippy.css',
     SRC + 'clippy/client/clippy.swf',
+    SRC + 'clippy/client/clipboard-16.png',
     SRC + 'clippy/client/CopyableLabelText.properties',
   ],
   provided_deps = ['//lib/gwt:user'],
@@ -14,6 +15,7 @@
     ':SafeHtml',
     ':UserAgent',
     '//lib:LICENSE-clippy',
+    '//lib:LICENSE-drifty',
   ],
   visibility = ['PUBLIC'],
 )
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyResources.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyResources.java
index dfa7679..dd3cc18 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyResources.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyResources.java
@@ -18,6 +18,7 @@
 import com.google.gwt.resources.client.ClientBundle;
 import com.google.gwt.resources.client.DataResource;
 import com.google.gwt.resources.client.DataResource.DoNotEmbed;
+import com.google.gwt.resources.client.ImageResource;
 
 public interface ClippyResources extends ClientBundle {
   public static final ClippyResources I = GWT.create(ClippyResources.class);
@@ -28,4 +29,7 @@
   @Source("clippy.swf")
   @DoNotEmbed
   DataResource swf();
+
+  @Source("clipboard-16.png")
+  ImageResource clipboard();
 }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java
index 4b6e7e4..8d54b2f 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java
@@ -119,7 +119,12 @@
     }
 
     if (UserAgent.hasJavaScriptClipboard()) {
-      copier = new Button("📋"); // CLIPBOARD
+      copier = new Button(new SafeHtmlBuilder()
+          .openElement("img")
+          .setAttribute("src", ClippyResources.I.clipboard().getSafeUri().asString())
+          .setWidth(14)
+          .setHeight(14)
+          .closeSelf());
       copier.setStyleName(ClippyResources.I.css().copier());
       Tooltip.addStyle(copier);
       Tooltip.setLabel(copier, CopyableLabelText.I.tooltip());
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/clipboard-16.png b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/clipboard-16.png
new file mode 100644
index 0000000..9c6e10a
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/clipboard-16.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java
index 00036a8..0924796 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java
@@ -54,7 +54,7 @@
         switchAccount.setHref(Gerrit.info().auth().switchAccountUrl());
       } else if (Gerrit.info().auth().isDev()
           || Gerrit.info().auth().isOpenId()) {
-        switchAccount.setHref(Gerrit.selfRedirect("/login/"));
+        switchAccount.setHref(Gerrit.selfRedirect("/login"));
       } else {
         switchAccount.removeFromParent();
         switchAccount = null;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChangesTab.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChangesTab.java
index 525f5a9..23959e7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChangesTab.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChangesTab.java
@@ -292,6 +292,7 @@
         if (url.startsWith("#")) {
           sb.setAttribute("onclick", OPEN);
         }
+        sb.setAttribute("title", info.commit().subject());
         if (showProjects) {
           sb.append(info.project()).append(": ");
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
index 9e5ff12..2768733 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
@@ -196,7 +196,8 @@
   private final GroupBackend groupBackend;
   private final String anonymousCowardName;
   private final Boolean disableReverseDnsLookup;
-  private final Set<String> validEmails = Sets.newHashSetWithExpectedSize(4);
+  private final Set<String> validEmails =
+      Sets.newTreeSet(String.CASE_INSENSITIVE_ORDER);
 
   @Nullable
   private final Provider<SocketAddress> remotePeerProvider;
@@ -284,7 +285,7 @@
       validEmails.add(email);
       return true;
     } else if (invalidEmails == null) {
-      invalidEmails = Sets.newHashSetWithExpectedSize(4);
+      invalidEmails = Sets.newTreeSet(String.CASE_INSENSITIVE_ORDER);
     }
     invalidEmails.add(email);
     return false;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AbstractRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AbstractRealm.java
index 34f83f7..30420e0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AbstractRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AbstractRealm.java
@@ -25,7 +25,6 @@
 
 import java.util.Collection;
 import java.util.HashSet;
-import java.util.Objects;
 import java.util.Set;
 
 /** Basic implementation of {@link Realm}.  */
@@ -57,7 +56,7 @@
   @Override
   public boolean hasEmailAddress(IdentifiedUser user, String email) {
     for (AccountExternalId ext : user.state().getExternalIds()) {
-      if (Objects.equals(ext.getEmailAddress(), email)) {
+      if (email != null && email.equalsIgnoreCase(ext.getEmailAddress())) {
         return true;
       }
     }
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg b/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
index be88bd4..6988459 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
@@ -38,6 +38,12 @@
 		return
 	fi
 
+	# Do not add Change-Id to temp commits
+	if echo "$clean_message" | head -1 | grep -q '^\(fixup\|squash\)!'
+	then
+		return
+	fi
+
 	if test "false" = "`git config --bool --get gerrit.createChangeId`"
 	then
 		return
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/IdentifiedUserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/IdentifiedUserTest.java
new file mode 100644
index 0000000..039871e
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/IdentifiedUserTest.java
@@ -0,0 +1,131 @@
+// 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.inject.Scopes.SINGLETON;
+
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.account.FakeRealm;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.Realm;
+import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.config.AnonymousCowardNameProvider;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.DisableReverseDnsLookup;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.testutil.ConfigSuite;
+import com.google.gerrit.testutil.FakeAccountCache;
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.util.Providers;
+
+import org.eclipse.jgit.lib.Config;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+@RunWith(ConfigSuite.class)
+public class IdentifiedUserTest {
+  @ConfigSuite.Parameter
+  public Config config;
+
+  private IdentifiedUser identifiedUser;
+
+  @Inject
+  private IdentifiedUser.GenericFactory identifiedUserFactory;
+
+  private static final String[] TEST_CASES = {
+    "",
+    "FirstName.LastName@Corporation.com",
+    "!#$%&'+-/=.?^`{|}~@[IPv6:0123:4567:89AB:CDEF:0123:4567:89AB:CDEF]"
+  };
+
+  @Before
+  public void setUp() throws Exception {
+    final FakeAccountCache accountCache = new FakeAccountCache();
+    final Realm mockRealm = new FakeRealm() {
+      HashSet<String> emails = new HashSet<>(Arrays.asList(TEST_CASES));
+
+      @Override
+      public boolean hasEmailAddress(IdentifiedUser who, String email) {
+        return emails.contains(email);
+      }
+
+      @Override
+      public Set<String> getEmailAddresses(IdentifiedUser who) {
+        return emails;
+      }
+    };
+
+    AbstractModule mod = new AbstractModule() {
+      @Override
+      protected void configure() {
+        bind(Boolean.class).annotatedWith(DisableReverseDnsLookup.class)
+          .toInstance(Boolean.FALSE);
+        bind(Config.class).annotatedWith(GerritServerConfig.class).toInstance(config);
+        bind(String.class).annotatedWith(AnonymousCowardName.class)
+          .toProvider(AnonymousCowardNameProvider.class);
+        bind(String.class).annotatedWith(CanonicalWebUrl.class)
+          .toInstance("http://localhost:8080/");
+        bind(AccountCache.class).toInstance(accountCache);
+        bind(GroupBackend.class).to(SystemGroupBackend.class).in(SINGLETON);
+        bind(CapabilityControl.Factory.class)
+          .toProvider(Providers.<CapabilityControl.Factory>of(null));
+        bind(Realm.class).toInstance(mockRealm);
+
+      }
+    };
+
+    Injector injector = Guice.createInjector(mod);
+    injector.injectMembers(this);
+
+    Account account = new Account(new Account.Id(1), TimeUtil.nowTs());
+    Account.Id ownerId = account.getId();
+
+    identifiedUser = identifiedUserFactory.create(ownerId);
+
+    /* Trigger identifiedUser to load the email addresses from mockRealm */
+    identifiedUser.getEmailAddresses();
+  }
+
+  @Test
+  public void testEmailsExistence() {
+    assertThat(identifiedUser.hasEmailAddress(TEST_CASES[0])).isTrue();
+    assertThat(identifiedUser.hasEmailAddress(TEST_CASES[1].toLowerCase())).isTrue();
+    assertThat(identifiedUser.hasEmailAddress(TEST_CASES[1])).isTrue();
+    assertThat(identifiedUser.hasEmailAddress(TEST_CASES[1].toUpperCase())).isTrue();
+    /* assert again to test cached email address by IdentifiedUser.validEmails */
+    assertThat(identifiedUser.hasEmailAddress(TEST_CASES[1])).isTrue();
+
+    assertThat(identifiedUser.hasEmailAddress(TEST_CASES[2])).isTrue();
+    assertThat(identifiedUser.hasEmailAddress(TEST_CASES[2].toLowerCase())).isTrue();
+
+
+    assertThat(identifiedUser.hasEmailAddress("non-exist@email.com")).isFalse();
+    /* assert again to test cached email address by IdentifiedUser.invalidEmails */
+    assertThat(identifiedUser.hasEmailAddress("non-exist@email.com")).isFalse();
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
index 7ca5d4d..18e950a 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
@@ -83,6 +83,8 @@
 import org.apache.sshd.common.mac.HMACMD596;
 import org.apache.sshd.common.mac.HMACSHA1;
 import org.apache.sshd.common.mac.HMACSHA196;
+import org.apache.sshd.common.mac.HMACSHA256;
+import org.apache.sshd.common.mac.HMACSHA512;
 import org.apache.sshd.common.random.BouncyCastleRandom;
 import org.apache.sshd.common.random.JceRandom;
 import org.apache.sshd.common.random.SingletonRandomFactory;
@@ -552,9 +554,13 @@
   }
 
   private void initMacs(final Config cfg) {
-    setMacFactories(filter(cfg, "mac", new HMACMD5.Factory(),
-        new HMACSHA1.Factory(), new HMACMD596.Factory(),
-        new HMACSHA196.Factory()));
+    setMacFactories(filter(cfg, "mac",
+        new HMACMD5.Factory(),
+        new HMACSHA1.Factory(),
+        new HMACMD596.Factory(),
+        new HMACSHA196.Factory(),
+        new HMACSHA256.Factory(),
+        new HMACSHA512.Factory()));
   }
 
   @SafeVarargs
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLogLayout.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLogLayout.java
index 2bed29d..541081e 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLogLayout.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLogLayout.java
@@ -20,7 +20,6 @@
 
 import java.text.SimpleDateFormat;
 import java.util.Calendar;
-import java.util.Date;
 import java.util.TimeZone;
 
 public final class SshLogLayout extends Layout {
@@ -36,15 +35,15 @@
   private final Calendar calendar;
   private long lastTimeMillis;
   private final char[] lastTimeString = new char[20];
-  private final char[] timeZone;
+  private final SimpleDateFormat tzFormat;
+  private char[] timeZone;
 
  public SshLogLayout() {
     final TimeZone tz = TimeZone.getDefault();
     calendar = Calendar.getInstance(tz);
 
-    final SimpleDateFormat sdf = new SimpleDateFormat("Z");
-    sdf.setTimeZone(tz);
-    timeZone = sdf.format(new Date()).toCharArray();
+    tzFormat = new SimpleDateFormat("Z");
+    tzFormat.setTimeZone(tz);
   }
 
   @Override
@@ -53,8 +52,6 @@
 
     buf.append('[');
     formatDate(event.getTimeStamp(), buf);
-    buf.append(' ');
-    buf.append(timeZone);
     buf.append(']');
 
     req(P_SESSION, buf, event);
@@ -94,11 +91,14 @@
         sbuf.append(',');
         sbuf.getChars(start, sbuf.length(), lastTimeString, 0);
         lastTimeMillis = rounded;
+        timeZone = tzFormat.format(calendar.getTime()).toCharArray();
       }
     } else {
       sbuf.append(lastTimeString);
     }
     sbuf.append(String.format("%03d", millis));
+    sbuf.append(' ');
+    sbuf.append(timeZone);
   }
 
   private String toTwoDigits(int input) {
diff --git a/lib/BUCK b/lib/BUCK
index 73983da..a92f910 100644
--- a/lib/BUCK
+++ b/lib/BUCK
@@ -12,6 +12,7 @@
 define_license(name = 'clippy')
 define_license(name = 'codemirror')
 define_license(name = 'diffy')
+define_license(name = 'drifty')
 define_license(name = 'freebie_application_icon_set')
 define_license(name = 'h2')
 define_license(name = 'jgit')
diff --git a/lib/LICENSE-drifty b/lib/LICENSE-drifty
new file mode 100644
index 0000000..18ab118
--- /dev/null
+++ b/lib/LICENSE-drifty
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Drifty (http://drifty.com/)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.