Refine sorting within the file list

+ Always place the Commit message at the top.
+ Put {.h|.hxx|.hpp} files before others with the same base name.

Bug: Issue 3852
Bug: Issue 4065
Change-Id: I5295a19734711516c69831fd8250f13f962cbba2
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index 39e50a9..de28184 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -126,21 +126,8 @@
     },
 
     _getFiles: function() {
-      return this.$.restAPI.getChangeFiles(this.changeNum, this.patchNum).then(
-          this._normalizeFilesResponse.bind(this));
-    },
-
-    _normalizeFilesResponse: function(response) {
-      var paths = Object.keys(response).sort();
-      var files = [];
-      for (var i = 0; i < paths.length; i++) {
-        var info = response[paths[i]];
-        info.__path = paths[i];
-        info.lines_inserted = info.lines_inserted || 0;
-        info.lines_deleted = info.lines_deleted || 0;
-        files.push(info);
-      }
-      return files;
+      return this.$.restAPI.getChangeFilesAsSpeciallySortedArray(
+          this.changeNum, this.patchNum);
     },
 
     _handleKey: function(e) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index c4f2a83..12d7f69 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -109,9 +109,9 @@
     },
 
     _getFiles: function(changeNum, patchNum) {
-      return this.$.restAPI.getChangeFiles(changeNum, patchNum).then(
-          function(files) {
-            this._fileList = Object.keys(files).sort();
+      return this.$.restAPI.getChangeFilePathsAsSpeciallySortedArray(
+          changeNum, patchNum).then(function(files) {
+            this._fileList = files;
           }.bind(this));
     },
 
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index c84c3e8..affbd06 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -238,6 +238,67 @@
           this.getChangeActionURL(changeNum, patchNum, '/files'));
     },
 
+    getChangeFilesAsSpeciallySortedArray: function(changeNum, patchNum) {
+      return this.getChangeFiles(changeNum, patchNum).then(
+          this._normalizeChangeFilesResponse.bind(this));
+    },
+
+    getChangeFilePathsAsSpeciallySortedArray: function(changeNum, patchNum) {
+      return this.getChangeFiles(changeNum, patchNum).then(function(files) {
+        return Object.keys(files).sort(this._specialFilePathCompare.bind(this));
+      }.bind(this));
+    },
+
+    _normalizeChangeFilesResponse: function(response) {
+      var paths = Object.keys(response).sort(
+          this._specialFilePathCompare.bind(this));
+      var files = [];
+      for (var i = 0; i < paths.length; i++) {
+        var info = response[paths[i]];
+        info.__path = paths[i];
+        info.lines_inserted = info.lines_inserted || 0;
+        info.lines_deleted = info.lines_deleted || 0;
+        files.push(info);
+      }
+      return files;
+    },
+
+    _specialFilePathCompare: function(a, b) {
+      var COMMIT_MESSAGE_PATH = '/COMMIT_MSG';
+      // The commit message always goes first.
+      if (a === COMMIT_MESSAGE_PATH) {
+        return -1;
+      }
+      if (b === COMMIT_MESSAGE_PATH) {
+        return 1;
+      }
+
+      var aLastDotIndex = a.lastIndexOf('.');
+      var aExt = a.substr(aLastDotIndex + 1);
+      var aFile = a.substr(0, aLastDotIndex);
+
+      var bLastDotIndex = b.lastIndexOf('.');
+      var bExt = b.substr(bLastDotIndex + 1);
+      var bFile = a.substr(0, bLastDotIndex);
+
+      // Sort header files above others with the same base name.
+      var headerExts = ['h', 'hxx', 'hpp'];
+      if (aFile.length > 0 && aFile === bFile) {
+        if (headerExts.indexOf(aExt) !== -1 &&
+            headerExts.indexOf(bExt) !== -1) {
+          return a.localeCompare(b);
+        }
+        if (headerExts.indexOf(aExt) !== -1) {
+          return -1;
+        }
+        if (headerExts.indexOf(bExt) !== -1) {
+          return 1;
+        }
+      }
+
+      return a.localeCompare(b);
+    },
+
     getChangeRevisionActions: function(changeNum, patchNum) {
       return this.fetchJSON(
           this.getChangeActionURL(changeNum, patchNum, '/actions'));
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index 4e2c5ad..752d718 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -196,5 +196,31 @@
         });
     });
 
+    test('special file path sorting', function() {
+      assert.deepEqual(
+          ['.b', '/COMMIT_MSG', '.a', 'file'].sort(
+              element._specialFilePathCompare),
+          ['/COMMIT_MSG', '.a', '.b', 'file']);
+
+      assert.deepEqual(
+          ['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.h'].sort(
+              element._specialFilePathCompare),
+          ['/COMMIT_MSG', '.b', 'foo/bar/baz.h', 'foo/bar/baz.cc']);
+
+      assert.deepEqual(
+          ['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.hpp'].sort(
+              element._specialFilePathCompare),
+          ['/COMMIT_MSG', '.b', 'foo/bar/baz.hpp', 'foo/bar/baz.cc']);
+
+      assert.deepEqual(
+          ['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.hxx'].sort(
+              element._specialFilePathCompare),
+          ['/COMMIT_MSG', '.b', 'foo/bar/baz.hxx', 'foo/bar/baz.cc']);
+
+      assert.deepEqual(
+          ['foo/bar.h', 'foo/bar.hxx', 'foo/bar.hpp'].sort(
+              element._specialFilePathCompare),
+          ['foo/bar.h', 'foo/bar.hpp', 'foo/bar.hxx']);
+    });
   });
 </script>