]> source.dussan.org Git - gitea.git/commitdiff
Sort repository tree entries in natural way (#2506)
authorLauris BH <lauris@nix.lv>
Tue, 19 Sep 2017 08:37:03 +0000 (11:37 +0300)
committerGitHub <noreply@github.com>
Tue, 19 Sep 2017 08:37:03 +0000 (11:37 +0300)
* Sort repository tree entries in natural way

* Fix sort for different length strings with first parts equal

* Improve test case

* Refactor return statements

* Update gitea/git dependency

modules/base/natural_sort.go [new file with mode: 0644]
modules/base/natural_sort_test.go [new file with mode: 0644]
routers/repo/view.go
vendor/code.gitea.io/git/tree_entry.go
vendor/vendor.json

diff --git a/modules/base/natural_sort.go b/modules/base/natural_sort.go
new file mode 100644 (file)
index 0000000..60db363
--- /dev/null
@@ -0,0 +1,89 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package base
+
+import (
+       "math/big"
+       "unicode/utf8"
+)
+
+// NaturalSortLess compares two strings so that they could be sorted in natural order
+func NaturalSortLess(s1, s2 string) bool {
+       var i1, i2 int
+       for {
+               rune1, j1, end1 := getNextRune(s1, i1)
+               rune2, j2, end2 := getNextRune(s2, i2)
+               if end1 || end2 {
+                       return end1 != end2 && end1
+               }
+               dec1 := isDecimal(rune1)
+               dec2 := isDecimal(rune2)
+               var less, equal bool
+               if dec1 && dec2 {
+                       i1, i2, less, equal = compareByNumbers(s1, i1, s2, i2)
+               } else if !dec1 && !dec2 {
+                       equal = rune1 == rune2
+                       less = rune1 < rune2
+                       i1 = j1
+                       i2 = j2
+               } else {
+                       return rune1 < rune2
+               }
+               if !equal {
+                       return less
+               }
+       }
+}
+
+func getNextRune(str string, pos int) (rune, int, bool) {
+       if pos < len(str) {
+               r, w := utf8.DecodeRuneInString(str[pos:])
+               // Fallback to ascii
+               if r == utf8.RuneError {
+                       r = rune(str[pos])
+                       w = 1
+               }
+               return r, pos + w, false
+       }
+       return 0, pos, true
+}
+
+func isDecimal(r rune) bool {
+       return '0' <= r && r <= '9'
+}
+
+func compareByNumbers(str1 string, pos1 int, str2 string, pos2 int) (i1, i2 int, less, equal bool) {
+       var d1, d2 bool = true, true
+       var dec1, dec2 string
+       for d1 || d2 {
+               if d1 {
+                       r, j, end := getNextRune(str1, pos1)
+                       if !end && isDecimal(r) {
+                               dec1 += string(r)
+                               pos1 = j
+                       } else {
+                               d1 = false
+                       }
+               }
+               if d2 {
+                       r, j, end := getNextRune(str2, pos2)
+                       if !end && isDecimal(r) {
+                               dec2 += string(r)
+                               pos2 = j
+                       } else {
+                               d2 = false
+                       }
+               }
+       }
+       less, equal = compareBigNumbers(dec1, dec2)
+       return pos1, pos2, less, equal
+}
+
+func compareBigNumbers(dec1, dec2 string) (less, equal bool) {
+       d1, _ := big.NewInt(0).SetString(dec1, 10)
+       d2, _ := big.NewInt(0).SetString(dec2, 10)
+       cmp := d1.Cmp(d2)
+       return cmp < 0, cmp == 0
+}
diff --git a/modules/base/natural_sort_test.go b/modules/base/natural_sort_test.go
new file mode 100644 (file)
index 0000000..b8d45e4
--- /dev/null
@@ -0,0 +1,24 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package base
+
+import (
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+)
+
+func TestNaturalSortLess(t *testing.T) {
+       test := func(s1, s2 string, less bool) {
+               assert.Equal(t, less, NaturalSortLess(s1, s2))
+       }
+       test("v1.20.0", "v1.2.0", false)
+       test("v1.20.0", "v1.29.0", true)
+       test("v1.20.0", "v1.20.0", false)
+       test("abc", "bcd", "abc" < "bcd")
+       test("a-1-a", "a-1-b", true)
+       test("2", "12", true)
+       test("a", "ab", "a" < "ab")
+}
index 991d2c0400608c844f1403fcbdc2338c22f4548c..d794a574059eb429287f79d7141a369c4807b1ba 100644 (file)
@@ -47,7 +47,7 @@ func renderDirectory(ctx *context.Context, treeLink string) {
                ctx.Handle(500, "ListEntries", err)
                return
        }
-       entries.Sort()
+       entries.CustomSort(base.NaturalSortLess)
 
        ctx.Data["Files"], err = entries.GetCommitsInfo(ctx.Repo.Commit, ctx.Repo.TreePath)
        if err != nil {
index 1e4934e81fcbd40f6ae20970d40eb516ee8adcbd..d5730a0d44a0cc16283aeadc088cacdb02fbafe9 100644 (file)
@@ -116,35 +116,51 @@ func (te *TreeEntry) GetSubJumpablePathName() string {
 // Entries a list of entry
 type Entries []*TreeEntry
 
-var sorter = []func(t1, t2 *TreeEntry) bool{
-       func(t1, t2 *TreeEntry) bool {
+type customSortableEntries struct {
+       Comparer func(s1, s2 string) bool
+       Entries
+}
+
+var sorter = []func(t1, t2 *TreeEntry, cmp func(s1, s2 string) bool) bool{
+       func(t1, t2 *TreeEntry, cmp func(s1, s2 string) bool) bool {
                return (t1.IsDir() || t1.IsSubModule()) && !t2.IsDir() && !t2.IsSubModule()
        },
-       func(t1, t2 *TreeEntry) bool {
-               return t1.name < t2.name
+       func(t1, t2 *TreeEntry, cmp func(s1, s2 string) bool) bool {
+               return cmp(t1.name, t2.name)
        },
 }
 
-func (tes Entries) Len() int      { return len(tes) }
-func (tes Entries) Swap(i, j int) { tes[i], tes[j] = tes[j], tes[i] }
-func (tes Entries) Less(i, j int) bool {
-       t1, t2 := tes[i], tes[j]
+func (ctes customSortableEntries) Len() int { return len(ctes.Entries) }
+
+func (ctes customSortableEntries) Swap(i, j int) {
+       ctes.Entries[i], ctes.Entries[j] = ctes.Entries[j], ctes.Entries[i]
+}
+
+func (ctes customSortableEntries) Less(i, j int) bool {
+       t1, t2 := ctes.Entries[i], ctes.Entries[j]
        var k int
        for k = 0; k < len(sorter)-1; k++ {
                s := sorter[k]
                switch {
-               case s(t1, t2):
+               case s(t1, t2, ctes.Comparer):
                        return true
-               case s(t2, t1):
+               case s(t2, t1, ctes.Comparer):
                        return false
                }
        }
-       return sorter[k](t1, t2)
+       return sorter[k](t1, t2, ctes.Comparer)
 }
 
 // Sort sort the list of entry
 func (tes Entries) Sort() {
-       sort.Sort(tes)
+       sort.Sort(customSortableEntries{func(s1, s2 string) bool {
+               return s1 < s2
+       }, tes})
+}
+
+// CustomSort customizable string comparing sort entry list
+func (tes Entries) CustomSort(cmp func(s1, s2 string) bool) {
+       sort.Sort(customSortableEntries{cmp, tes})
 }
 
 type commitInfo struct {
index d07c9d1a74077a10bfe9fc552f391ee1b50e95f9..d9cb18cd1ac6a2499eaff882f5a200a68955f4e6 100644 (file)
@@ -3,10 +3,10 @@
        "ignore": "test appengine",
        "package": [
                {
-                       "checksumSHA1": "fR5YDSoG7xYv2aLO23rne95gWps=",
+                       "checksumSHA1": "9dxw/SGpdhNNm704gt6F02ItYtQ=",
                        "path": "code.gitea.io/git",
-                       "revision": "479f87e5d189e7b8f1fd51dbcd25faa32b632cd2",
-                       "revisionTime": "2017-08-03T00:53:29Z"
+                       "revision": "d7487da878e40ee6c4fac7280b518c0ed0be702c",
+                       "revisionTime": "2017-09-16T17:49:37Z"
                },
                {
                        "checksumSHA1": "Zgp5RqU+20L2p9TNl1rSsUIWEEE=",