Merge "DiffScreen: Make final fields package private and remove getters"
diff --git a/gerrit-server/BUCK b/gerrit-server/BUCK
index 58f2346..be38240 100644
--- a/gerrit-server/BUCK
+++ b/gerrit-server/BUCK
@@ -196,6 +196,7 @@
     '//gerrit-server/src/main/prolog:common',
     '//lib:args4j',
     '//lib:grappa',
+    '//lib:gson',
     '//lib:guava',
     '//lib:guava-retrying',
     '//lib:protobuf',
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/events/EventDeserializerTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/events/EventDeserializerTest.java
new file mode 100644
index 0000000..6a006cd
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/events/EventDeserializerTest.java
@@ -0,0 +1,70 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.events;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.Supplier;
+import com.google.common.base.Suppliers;
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.RefUpdateAttribute;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+import org.junit.Test;
+
+public class EventDeserializerTest {
+
+  @Test
+  public void testRefUpdatedEvent() {
+    RefUpdatedEvent refUpdatedEvent = new RefUpdatedEvent();
+
+    RefUpdateAttribute refUpdatedAttribute = new RefUpdateAttribute();
+    refUpdatedAttribute.refName = "refs/heads/master";
+    refUpdatedEvent.refUpdate = createSupplier(refUpdatedAttribute);
+
+    AccountAttribute accountAttribute = new AccountAttribute();
+    accountAttribute.email = "some.user@domain.com";
+    refUpdatedEvent.submitter = createSupplier(accountAttribute);
+
+    Gson gsonSerializer = new GsonBuilder()
+        .registerTypeAdapter(Supplier.class, new SupplierSerializer()).create();
+    String serializedEvent = gsonSerializer.toJson(refUpdatedEvent);
+
+    Gson gsonDeserializer = new GsonBuilder()
+        .registerTypeAdapter(Event.class, new EventDeserializer())
+        .registerTypeAdapter(Supplier.class, new SupplierDeserializer())
+        .create();
+
+    RefUpdatedEvent e = (RefUpdatedEvent) gsonDeserializer
+        .fromJson(serializedEvent, Event.class);
+
+    assertThat(e).isNotNull();
+    assertThat(e.refUpdate).isInstanceOf(Supplier.class);
+    assertThat(e.refUpdate.get().refName)
+        .isEqualTo(refUpdatedAttribute.refName);
+    assertThat(e.submitter).isInstanceOf(Supplier.class);
+    assertThat(e.submitter.get().email).isEqualTo(accountAttribute.email);
+  }
+
+  private <T> Supplier<T> createSupplier(final T value) {
+    return Suppliers.memoize(new Supplier<T>() {
+      @Override
+      public T get() {
+        return value;
+      }
+    });
+  }
+}
diff --git a/lib/BUCK b/lib/BUCK
index 46ee264..1458c63 100644
--- a/lib/BUCK
+++ b/lib/BUCK
@@ -15,6 +15,7 @@
 define_license(name = 'jgit')
 define_license(name = 'jsch')
 define_license(name = 'MPL1.1')
+define_license(name = 'moment')
 define_license(name = 'ow2')
 define_license(name = 'page.js')
 define_license(name = 'polymer')
@@ -258,4 +259,3 @@
   sha1 = 'd9a09f7732226af26bf99f19e2cffe0ae219db5b',
   license = 'DO_NOT_DISTRIBUTE',
 )
-
diff --git a/lib/LICENSE-moment b/lib/LICENSE-moment
new file mode 100644
index 0000000..9ee5374
--- /dev/null
+++ b/lib/LICENSE-moment
@@ -0,0 +1,22 @@
+Copyright (c) 2011-2016 Tim Wood, Iskren Chernev, Moment.js contributors
+
+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.
diff --git a/lib/js/BUCK b/lib/js/BUCK
index 948f9b4..b0bdb67 100644
--- a/lib/js/BUCK
+++ b/lib/js/BUCK
@@ -257,6 +257,14 @@
 )
 
 bower_component(
+  name = 'moment',
+  package = 'moment/moment',
+  version = '2.12.0',
+  license = 'moment',
+  sha1 = '508d53de8f49ab87f03e209e5073e339107ed3e6',
+)
+
+bower_component(
   name = 'neon-animation',
   package = 'polymerelements/neon-animation',
   version = '1.1.1',
diff --git a/polygerrit-ui/BUCK b/polygerrit-ui/BUCK
index 0a562f9..4d8144f 100644
--- a/polygerrit-ui/BUCK
+++ b/polygerrit-ui/BUCK
@@ -10,6 +10,7 @@
     '//lib/js:iron-input',
     '//lib/js:iron-overlay-behavior',
     '//lib/js:iron-selector',
+    '//lib/js:moment',
     '//lib/js:page',
     '//lib/js:polymer',
     '//lib/js:promise-polyfill',
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
index 814e206..3576b80 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
@@ -83,7 +83,7 @@
             </div>
           </li>
           <li><a href="/switch-account">Switch account</a></li>
-          <li><a href="/logout">Logout</a></li>
+          <li><a href="/logout">Sign out</a></li>
         </ul>
       </div>
     </iron-dropdown>
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
index af4a0e4..20405b9 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
@@ -148,7 +148,7 @@
       <div class="rightItems">
         <gr-search-bar value="{{searchQuery}}" role="search"></gr-search-bar>
         <div class="accountContainer" id="accountContainer">
-          <a class="loginButton" href="/login" on-tap="_loginTapHandler">Login</a>
+          <a class="loginButton" href="/login" on-tap="_loginTapHandler">Sign in</a>
           <gr-account-dropdown account="[[_account]]"></gr-account-dropdown>
         </div>
       </div>
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
index 6d4b2ea..01deeda 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
@@ -15,6 +15,9 @@
 -->
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
+
+<script src="../../../bower_components/moment/moment.js"></script>
 
 <dom-module id="gr-date-formatter">
   <template>
@@ -23,7 +26,8 @@
         display: inline;
       }
     </style>
-    <span>[[_computeDateStr(dateStr)]]</span>
+    <span>[[_computeDateStr(dateStr, timeFormat)]]</span>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-date-formatter.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
index cb77cc1..35347f5 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
@@ -19,10 +19,12 @@
     DAY: 1000 * 60 * 60 * 24,
   };
 
-  var ShortMonthNames = [
-    'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct',
-    'Nov', 'Dec'
-  ];
+  var TimeFormats = {
+    TIME_12: 'h:mm A', // 2:14 PM
+    TIME_24: 'H:mm', // 14:14
+    MONTH_DAY: 'MMM DD', // Aug 29
+    MONTH_DAY_YEAR: 'MMM DD, YYYY', // Aug 29, 1997
+  };
 
   Polymer({
     is: 'gr-date-formatter',
@@ -31,46 +33,58 @@
       dateStr: {
         type: String,
         value: null,
-        notify: true
-      }
+        notify: true,
+      },
+      timeFormat: {
+        type: String,
+        value: 'HHMM_24',
+        notify: true,
+      },
     },
 
-    _computeDateStr: function(dateStr) {
-      return this._dateStr(this._parseDateStr(dateStr), new Date());
+    attached: function() {
+      this._fetchPreferences();
     },
 
-    _parseDateStr: function(dateStr) {
-      if (!dateStr) { return null; }
-      return util.parseDate(dateStr);
+    _fetchPreferences: function() {
+      this.$.restAPI.getPreferences().then(function(preferences) {
+        this.timeFormat = preferences && preferences.time_format;
+      }.bind(this));
     },
 
-    _dateStr: function(t, now) {
-      if (!t) { return ''; }
-      var diff = now.getTime() - t.getTime();
-      if (diff < Duration.DAY && t.getDay() == now.getDay()) {
-        // Within 24 hours and on the same day:
-        // '2:14 AM'
-        var pm = t.getHours() >= 12;
-        var hours = t.getHours();
-        if (hours == 0) {
-          hours = 12;
-        } else if (hours > 12) {
-          hours = t.getHours() - 12;
+    /**
+     * Return true if date is within 24 hours and on the same day.
+     */
+    _isWithinDay: function(now, date) {
+      var diff = -date.diff(now);
+      return diff < Duration.DAY && date.day() == now.getDay();
+    },
+
+    /**
+     * Returns true if date is from one to six months.
+     */
+    _isWithinHalfYear: function(now, date) {
+      var diff = -date.diff(now);
+      return (date.day() != now.getDay() || diff >= Duration.DAY) &&
+          diff < 180 * Duration.DAY;
+    },
+
+    _computeDateStr: function(dateStr, timeFormat) {
+      if (!dateStr) { return ''; }
+      var date = moment(dateStr + 'Z');
+      if (!date.isValid()) { return ''; }
+      var now = new Date();
+      var format = TimeFormats.MONTH_DAY_YEAR;
+      if (this._isWithinDay(now, date)) {
+        if (this.timeFormat == 'HHMM_12') {
+          format = TimeFormats.TIME_12;
+        } else {
+          format = TimeFormats.TIME_24;
         }
-        var minutes = t.getMinutes() < 10 ? '0' + t.getMinutes() :
-            t.getMinutes();
-        return hours + ':' + minutes + (pm ? ' PM' : ' AM');
-      } else if ((t.getDay() != now.getDay() || diff >= Duration.DAY) &&
-                 diff < 180 * Duration.DAY) {
-        // From one to six months:
-        // 'Aug 29'
-        return ShortMonthNames[t.getMonth()] + ' ' + t.getDate();
-      } else if (diff >= 180 * Duration.DAY) {
-        // More than six months:
-        // 'Aug 29, 1997'
-        return ShortMonthNames[t.getMonth()] + ' ' + t.getDate() + ', ' +
-            t.getFullYear();
+      } else if (this._isWithinHalfYear(now, date)) {
+        format = TimeFormats.MONTH_DAY;
       }
+      return date.format(format);
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
index 9bba517..932cba4 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
@@ -34,51 +34,107 @@
   suite('gr-date-formatter tests', function() {
     var element;
 
-    setup(function() {
-      element = fixture('basic');
-    });
-
-    test('date is parsed correctly', function() {
-      assert.notOk((new Date('foo')).valueOf());
-      var d = element._parseDateStr(element.getAttribute('date-str'));
-      assert.isAbove(d.valueOf(), 0);
-    });
-
+    /**
+     * Parse server-formatter date and normalize into current timezone.
+     */
     function normalizedDate(dateStr) {
-      var d = new Date(dateStr);
+      var d = new Date(util.parseDate(dateStr));
       d.setMinutes(d.getMinutes() + d.getTimezoneOffset());
       return d;
     }
 
     function testDates(nowStr, dateStr, expected) {
-      var now = normalizedDate(nowStr);
-      var t = normalizedDate(dateStr);
-      assert.equal(element._dateStr(t, now), expected);
+      // Normalize and convert the date to mimic server response.
+      dateStr = normalizedDate(dateStr)
+          .toJSON().replace('T', ' ').slice(0, -1);
+      var clock = sinon.useFakeTimers(
+          new Date(normalizedDate(nowStr)).getTime());
+      element.dateStr = dateStr;
+      assert.equal(element.$$('span').textContent, expected);
+      clock.restore();
     }
 
-    test('dates strings are correct', function() {
-      // Within 24 hours on same day.
-      testDates('2015-07-29T20:34:00.000Z',
-                '2015-07-29T15:34:00.000Z',
-                '3:34 PM');
-      testDates('2016-01-27T17:41:00.000Z',
-                '2016-01-27T12:41:00.000Z',
-                '12:41 PM');
+    function stubPreferences(preferences) {
+      var preferencesPromise = Promise.resolve(preferences);
+      stub('gr-rest-api-interface', {
+        getPreferences: sinon.stub().returns(preferencesPromise),
+      });
+      return preferencesPromise;
+    }
 
-      // Within 24 hours on different days.
-      testDates('2015-07-29T03:34:00.000Z',
-                '2015-07-28T20:25:00.000Z',
-                'Jul 28');
+    suite('not signed in', function() {
+      setup(function() {
+        return stubPreferences(null).then(function() {
+          element = fixture('basic');
+        });
+      });
 
-      // More than 24 hours. Less than six months.
-      testDates('2015-07-29T20:34:00.000Z',
-                '2015-06-15T03:25:00.000Z',
-                'Jun 15');
+      test('invalid dates are quietly rejected', function() {
+        assert.notOk((new Date('foo')).valueOf());
+        element.dateStr = 'foo';
+        assert.equal(element.$$('span').textContent, '');
+      });
 
-      // More than six months.
-      testDates('2015-09-15T20:34:00.000Z',
-                '2015-01-15T03:25:00.000Z',
-                'Jan 15, 2015');
+      test('Within 24 hours on same day', function() {
+        testDates('2015-07-29 20:34:14.985000000',
+                  '2015-07-29 15:34:14.985000000',
+                  '15:34');
+        testDates('2016-01-27 17:41:14.985000000',
+                  '2016-01-27 12:41:14.985000000',
+                  '12:41');
+      });
+
+      test('Within 24 hours on different days', function() {
+        testDates('2015-07-29 03:34:14.985000000',
+                  '2015-07-28 20:25:14.985000000',
+                  'Jul 28');
+      });
+
+      test('More than 24 hours but less than six months', function() {
+        testDates('2015-07-29 20:34:14.985000000',
+                  '2015-06-15 03:25:14.985000000',
+                  'Jun 15');
+      });
+
+      test('More than six months', function() {
+        testDates('2015-09-15 20:34:00.000000000',
+                  '2015-01-15 03:25:00.000000000',
+                  'Jan 15, 2015');
+      });
+    });
+
+    suite('signed in with 12 hours time format preference', function() {
+      setup(function() {
+        return stubPreferences({time_format: 'HHMM_12'}).then(function() {
+          element = fixture('basic');
+        });
+      });
+
+      test('Within 24 hours on same day', function() {
+        testDates('2015-07-29 20:34:14.985000000',
+                  '2015-07-29 15:34:14.985000000',
+                  '3:34 PM');
+        testDates('2016-01-27 17:41:14.985000000',
+                  '2016-01-27 12:41:14.985000000',
+                  '12:41 PM');
+      });
+    });
+
+    suite('signed in with 24 hours time format preference', function() {
+      setup(function() {
+        return stubPreferences({time_format: 'HHMM_24'}).then(function() {
+          element = fixture('basic');
+        });
+      });
+
+      test('Within 24 hours on same day', function() {
+        testDates('2015-07-29 20:34:14.985000000',
+                  '2015-07-29 15:34:14.985000000',
+                  '15:34');
+        testDates('2016-01-27 17:41:14.985000000',
+                  '2016-01-27 12:41:14.985000000',
+                  '12:41');
+      });
     });
   });
 </script>