Browse Source

Sort repository tree entries in natural way (#2506)

* 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
Lauris BH 2 years ago
parent
commit
23645fe05f

+ 89 - 0
modules/base/natural_sort.go

@@ -0,0 +1,89 @@
1
+// Copyright 2017 The Gitea Authors. All rights reserved.
2
+// Use of this source code is governed by a MIT-style
3
+// license that can be found in the LICENSE file.
4
+
5
+package base
6
+
7
+import (
8
+	"math/big"
9
+	"unicode/utf8"
10
+)
11
+
12
+// NaturalSortLess compares two strings so that they could be sorted in natural order
13
+func NaturalSortLess(s1, s2 string) bool {
14
+	var i1, i2 int
15
+	for {
16
+		rune1, j1, end1 := getNextRune(s1, i1)
17
+		rune2, j2, end2 := getNextRune(s2, i2)
18
+		if end1 || end2 {
19
+			return end1 != end2 && end1
20
+		}
21
+		dec1 := isDecimal(rune1)
22
+		dec2 := isDecimal(rune2)
23
+		var less, equal bool
24
+		if dec1 && dec2 {
25
+			i1, i2, less, equal = compareByNumbers(s1, i1, s2, i2)
26
+		} else if !dec1 && !dec2 {
27
+			equal = rune1 == rune2
28
+			less = rune1 < rune2
29
+			i1 = j1
30
+			i2 = j2
31
+		} else {
32
+			return rune1 < rune2
33
+		}
34
+		if !equal {
35
+			return less
36
+		}
37
+	}
38
+}
39
+
40
+func getNextRune(str string, pos int) (rune, int, bool) {
41
+	if pos < len(str) {
42
+		r, w := utf8.DecodeRuneInString(str[pos:])
43
+		// Fallback to ascii
44
+		if r == utf8.RuneError {
45
+			r = rune(str[pos])
46
+			w = 1
47
+		}
48
+		return r, pos + w, false
49
+	}
50
+	return 0, pos, true
51
+}
52
+
53
+func isDecimal(r rune) bool {
54
+	return '0' <= r && r <= '9'
55
+}
56
+
57
+func compareByNumbers(str1 string, pos1 int, str2 string, pos2 int) (i1, i2 int, less, equal bool) {
58
+	var d1, d2 bool = true, true
59
+	var dec1, dec2 string
60
+	for d1 || d2 {
61
+		if d1 {
62
+			r, j, end := getNextRune(str1, pos1)
63
+			if !end && isDecimal(r) {
64
+				dec1 += string(r)
65
+				pos1 = j
66
+			} else {
67
+				d1 = false
68
+			}
69
+		}
70
+		if d2 {
71
+			r, j, end := getNextRune(str2, pos2)
72
+			if !end && isDecimal(r) {
73
+				dec2 += string(r)
74
+				pos2 = j
75
+			} else {
76
+				d2 = false
77
+			}
78
+		}
79
+	}
80
+	less, equal = compareBigNumbers(dec1, dec2)
81
+	return pos1, pos2, less, equal
82
+}
83
+
84
+func compareBigNumbers(dec1, dec2 string) (less, equal bool) {
85
+	d1, _ := big.NewInt(0).SetString(dec1, 10)
86
+	d2, _ := big.NewInt(0).SetString(dec2, 10)
87
+	cmp := d1.Cmp(d2)
88
+	return cmp < 0, cmp == 0
89
+}

+ 24 - 0
modules/base/natural_sort_test.go

@@ -0,0 +1,24 @@
1
+// Copyright 2017 The Gitea Authors. All rights reserved.
2
+// Use of this source code is governed by a MIT-style
3
+// license that can be found in the LICENSE file.
4
+
5
+package base
6
+
7
+import (
8
+	"testing"
9
+
10
+	"github.com/stretchr/testify/assert"
11
+)
12
+
13
+func TestNaturalSortLess(t *testing.T) {
14
+	test := func(s1, s2 string, less bool) {
15
+		assert.Equal(t, less, NaturalSortLess(s1, s2))
16
+	}
17
+	test("v1.20.0", "v1.2.0", false)
18
+	test("v1.20.0", "v1.29.0", true)
19
+	test("v1.20.0", "v1.20.0", false)
20
+	test("abc", "bcd", "abc" < "bcd")
21
+	test("a-1-a", "a-1-b", true)
22
+	test("2", "12", true)
23
+	test("a", "ab", "a" < "ab")
24
+}

+ 1 - 1
routers/repo/view.go

@@ -47,7 +47,7 @@ func renderDirectory(ctx *context.Context, treeLink string) {
47 47
 		ctx.Handle(500, "ListEntries", err)
48 48
 		return
49 49
 	}
50
-	entries.Sort()
50
+	entries.CustomSort(base.NaturalSortLess)
51 51
 
52 52
 	ctx.Data["Files"], err = entries.GetCommitsInfo(ctx.Repo.Commit, ctx.Repo.TreePath)
53 53
 	if err != nil {

+ 28 - 12
vendor/code.gitea.io/git/tree_entry.go

@@ -116,35 +116,51 @@ func (te *TreeEntry) GetSubJumpablePathName() string {
116 116
 // Entries a list of entry
117 117
 type Entries []*TreeEntry
118 118
 
119
-var sorter = []func(t1, t2 *TreeEntry) bool{
120
-	func(t1, t2 *TreeEntry) bool {
119
+type customSortableEntries struct {
120
+	Comparer func(s1, s2 string) bool
121
+	Entries
122
+}
123
+
124
+var sorter = []func(t1, t2 *TreeEntry, cmp func(s1, s2 string) bool) bool{
125
+	func(t1, t2 *TreeEntry, cmp func(s1, s2 string) bool) bool {
121 126
 		return (t1.IsDir() || t1.IsSubModule()) && !t2.IsDir() && !t2.IsSubModule()
122 127
 	},
123
-	func(t1, t2 *TreeEntry) bool {
124
-		return t1.name < t2.name
128
+	func(t1, t2 *TreeEntry, cmp func(s1, s2 string) bool) bool {
129
+		return cmp(t1.name, t2.name)
125 130
 	},
126 131
 }
127 132
 
128
-func (tes Entries) Len() int      { return len(tes) }
129
-func (tes Entries) Swap(i, j int) { tes[i], tes[j] = tes[j], tes[i] }
130
-func (tes Entries) Less(i, j int) bool {
131
-	t1, t2 := tes[i], tes[j]
133
+func (ctes customSortableEntries) Len() int { return len(ctes.Entries) }
134
+
135
+func (ctes customSortableEntries) Swap(i, j int) {
136
+	ctes.Entries[i], ctes.Entries[j] = ctes.Entries[j], ctes.Entries[i]
137
+}
138
+
139
+func (ctes customSortableEntries) Less(i, j int) bool {
140
+	t1, t2 := ctes.Entries[i], ctes.Entries[j]
132 141
 	var k int
133 142
 	for k = 0; k < len(sorter)-1; k++ {
134 143
 		s := sorter[k]
135 144
 		switch {
136
-		case s(t1, t2):
145
+		case s(t1, t2, ctes.Comparer):
137 146
 			return true
138
-		case s(t2, t1):
147
+		case s(t2, t1, ctes.Comparer):
139 148
 			return false
140 149
 		}
141 150
 	}
142
-	return sorter[k](t1, t2)
151
+	return sorter[k](t1, t2, ctes.Comparer)
143 152
 }
144 153
 
145 154
 // Sort sort the list of entry
146 155
 func (tes Entries) Sort() {
147
-	sort.Sort(tes)
156
+	sort.Sort(customSortableEntries{func(s1, s2 string) bool {
157
+		return s1 < s2
158
+	}, tes})
159
+}
160
+
161
+// CustomSort customizable string comparing sort entry list
162
+func (tes Entries) CustomSort(cmp func(s1, s2 string) bool) {
163
+	sort.Sort(customSortableEntries{cmp, tes})
148 164
 }
149 165
 
150 166
 type commitInfo struct {

+ 3 - 3
vendor/vendor.json

@@ -3,10 +3,10 @@
3 3
 	"ignore": "test appengine",
4 4
 	"package": [
5 5
 		{
6
-			"checksumSHA1": "fR5YDSoG7xYv2aLO23rne95gWps=",
6
+			"checksumSHA1": "9dxw/SGpdhNNm704gt6F02ItYtQ=",
7 7
 			"path": "code.gitea.io/git",
8
-			"revision": "479f87e5d189e7b8f1fd51dbcd25faa32b632cd2",
9
-			"revisionTime": "2017-08-03T00:53:29Z"
8
+			"revision": "d7487da878e40ee6c4fac7280b518c0ed0be702c",
9
+			"revisionTime": "2017-09-16T17:49:37Z"
10 10
 		},
11 11
 		{
12 12
 			"checksumSHA1": "Zgp5RqU+20L2p9TNl1rSsUIWEEE=",