Skip to content

Commit b218c65

Browse files
committed
add submodule diff links
This adds links to submodules in diffs, similar to the existing link when viewing a repo at a specific commit. It does this by expanding diff parsing to recognize changes to submodules, and find the specific refs that are added, deleted or changed. The templates are updated to add either a link to the submodule at a commit, or the diff between two commits in the event that the submodule is updated. A slight refactor was done to simplify calling RefURL on the submodule.
1 parent 68972a9 commit b218c65

File tree

8 files changed

+335
-4
lines changed

8 files changed

+335
-4
lines changed

modules/git/commit_submodule_file.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
"path"
1212
"regexp"
1313
"strings"
14+
15+
"code.gitea.io/gitea/modules/setting"
1416
)
1517

1618
var scpSyntax = regexp.MustCompile(`^([a-zA-Z0-9_]+@)?([a-zA-Z0-9._-]+):(.*)$`)
@@ -101,8 +103,8 @@ func getRefURL(refURL, urlPrefix, repoFullName, sshDomain string) string {
101103

102104
// RefURL guesses and returns reference URL.
103105
// FIXME: template passes AppURL as urlPrefix, it needs to figure out the correct approach (no hard-coded AppURL anymore)
104-
func (sf *CommitSubModuleFile) RefURL(urlPrefix, repoFullName, sshDomain string) string {
105-
return getRefURL(sf.refURL, urlPrefix, repoFullName, sshDomain)
106+
func (sf *CommitSubModuleFile) RefURL(repoFullName string) string {
107+
return getRefURL(sf.refURL, setting.AppURL, repoFullName, setting.SSH.Domain)
106108
}
107109

108110
// RefID returns reference ID.

options/locale/locale_en-US.ini

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2627,6 +2627,11 @@ diff.image.overlay = Overlay
26272627
diff.has_escaped = This line has hidden Unicode characters
26282628
diff.show_file_tree = Show file tree
26292629
diff.hide_file_tree = Hide file tree
2630+
diff.submodule_added = Submodule %s added at %s
2631+
diff.submodule_added_link = Submodule <a href="%[1]s">%[2]s</a> added at <a href="%[1]s/commit/%[4]s">%[3]s</a>
2632+
diff.submodule_deleted = Submodule %s deleted from %s
2633+
diff.submodule_updated = Submodule %s updated from %s to %s
2634+
diff.submodule_updated_link = Submodule <a href="%[1]s">%[2]s</a> updated <a href="%[1]s/compare/%[4]s...%[6]s">from %[3]s to %[5]s</a>
26302635
26312636
releases.desc = Track project versions and downloads.
26322637
release.releases = Releases

routers/web/repo/view.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,6 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
309309
}
310310

311311
ctx.Data["TreeLink"] = treeLink
312-
ctx.Data["SSHDomain"] = setting.SSH.Domain
313312

314313
return allEntries
315314
}

services/gitdiff/gitdiff.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,7 @@ type DiffFile struct {
372372
Language string
373373
Mode string
374374
OldMode string
375+
SubmoduleInfo *SubmoduleInfo
375376
}
376377

377378
// GetType returns type of diff file.
@@ -915,6 +916,17 @@ func parseHunks(ctx context.Context, curFile *DiffFile, maxLines, maxLineCharact
915916
}
916917
}
917918
curSection.Lines = append(curSection.Lines, diffLine)
919+
920+
// Parse submodule additions
921+
if curFile.IsSubmodule {
922+
if curFile.SubmoduleInfo == nil {
923+
curFile.SubmoduleInfo = &SubmoduleInfo{}
924+
}
925+
926+
if ref, found := bytes.CutPrefix(lineBytes, []byte("+Subproject commit ")); found {
927+
curFile.SubmoduleInfo.NewRefID = string(bytes.TrimSpace(ref))
928+
}
929+
}
918930
case '-':
919931
curFileLinesCount++
920932
curFile.Deletion++
@@ -936,6 +948,17 @@ func parseHunks(ctx context.Context, curFile *DiffFile, maxLines, maxLineCharact
936948
lastLeftIdx = len(curSection.Lines)
937949
}
938950
curSection.Lines = append(curSection.Lines, diffLine)
951+
952+
// Parse submodule deletion
953+
if curFile.IsSubmodule {
954+
if curFile.SubmoduleInfo == nil {
955+
curFile.SubmoduleInfo = &SubmoduleInfo{}
956+
}
957+
958+
if ref, found := bytes.CutPrefix(lineBytes, []byte("-Subproject commit ")); found {
959+
curFile.SubmoduleInfo.PreviousRefID = string(bytes.TrimSpace(ref))
960+
}
961+
}
939962
case ' ':
940963
curFileLinesCount++
941964
if maxLines > -1 && curFileLinesCount >= maxLines {
@@ -1195,6 +1218,14 @@ func GetDiff(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, fi
11951218
}
11961219
}
11971220

1221+
// Populate Submodule URLs
1222+
if diffFile.IsSubmodule && diffFile.SubmoduleInfo != nil {
1223+
err := diffFile.SubmoduleInfo.PopulateURL(diffFile, beforeCommit, commit)
1224+
if err != nil {
1225+
return nil, err
1226+
}
1227+
}
1228+
11981229
if !isVendored.Has() {
11991230
isVendored = optional.Some(analyze.IsVendor(diffFile.Name))
12001231
}

services/gitdiff/submodule.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package gitdiff
5+
6+
import "code.gitea.io/gitea/modules/git"
7+
8+
type SubmoduleInfo struct {
9+
SubmoduleURL string
10+
NewRefID string
11+
PreviousRefID string
12+
}
13+
14+
func (si *SubmoduleInfo) PopulateURL(diffFile *DiffFile, leftCommit, rightCommit *git.Commit) error {
15+
// If the submodule is removed, we need to check it at the left commit
16+
if diffFile.IsDeleted {
17+
if leftCommit == nil {
18+
return nil
19+
}
20+
21+
submodule, err := leftCommit.GetSubModule(diffFile.GetDiffFileName())
22+
if err != nil {
23+
return err
24+
}
25+
26+
if submodule != nil {
27+
si.SubmoduleURL = submodule.URL
28+
}
29+
30+
return nil
31+
}
32+
33+
// Even if the submodule path is updated, we check this at the right commit
34+
submodule, err := rightCommit.GetSubModule(diffFile.Name)
35+
if err != nil {
36+
return err
37+
}
38+
39+
if submodule != nil {
40+
si.SubmoduleURL = submodule.URL
41+
}
42+
return nil
43+
}
44+
45+
func (si *SubmoduleInfo) RefID() string {
46+
if si.NewRefID != "" {
47+
return si.NewRefID
48+
}
49+
return si.PreviousRefID
50+
}
51+
52+
// RefURL guesses and returns reference URL.
53+
func (si *SubmoduleInfo) RefURL(repoFullName string) string {
54+
return git.NewCommitSubModuleFile(si.SubmoduleURL, si.RefID()).RefURL(repoFullName)
55+
}

services/gitdiff/submodule_test.go

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package gitdiff
5+
6+
import (
7+
"strings"
8+
"testing"
9+
10+
"code.gitea.io/gitea/models/db"
11+
"code.gitea.io/gitea/modules/setting"
12+
13+
"github.com/stretchr/testify/assert"
14+
)
15+
16+
func TestParseSubmoduleInfo(t *testing.T) {
17+
type testcase struct {
18+
name string
19+
gitdiff string
20+
infos map[int]SubmoduleInfo
21+
}
22+
23+
tests := []testcase{
24+
{
25+
name: "added",
26+
gitdiff: `diff --git a/.gitmodules b/.gitmodules
27+
new file mode 100644
28+
index 0000000..4ac13c1
29+
--- /dev/null
30+
+++ b/.gitmodules
31+
@@ -0,0 +1,3 @@
32+
+[submodule "gitea-mirror"]
33+
+ path = gitea-mirror
34+
+ url = https://gitea.com/gitea/gitea-mirror
35+
diff --git a/gitea-mirror b/gitea-mirror
36+
new file mode 160000
37+
index 0000000..68972a9
38+
--- /dev/null
39+
+++ b/gitea-mirror
40+
@@ -0,0 +1 @@
41+
+Subproject commit 68972a994719ae5c74e28d8fa82fa27c23399bc8
42+
`,
43+
infos: map[int]SubmoduleInfo{
44+
1: {NewRefID: "68972a994719ae5c74e28d8fa82fa27c23399bc8"},
45+
},
46+
},
47+
{
48+
name: "updated",
49+
gitdiff: `diff --git a/gitea-mirror b/gitea-mirror
50+
index 68972a9..c8ffe77 160000
51+
--- a/gitea-mirror
52+
+++ b/gitea-mirror
53+
@@ -1 +1 @@
54+
-Subproject commit 68972a994719ae5c74e28d8fa82fa27c23399bc8
55+
+Subproject commit c8ffe777cf9c5bb47a38e3e0b3a3b5de6cd8813d
56+
`,
57+
infos: map[int]SubmoduleInfo{
58+
0: {
59+
PreviousRefID: "68972a994719ae5c74e28d8fa82fa27c23399bc8",
60+
NewRefID: "c8ffe777cf9c5bb47a38e3e0b3a3b5de6cd8813d",
61+
},
62+
},
63+
},
64+
{
65+
name: "rename",
66+
gitdiff: `diff --git a/.gitmodules b/.gitmodules
67+
index 4ac13c1..0510edd 100644
68+
--- a/.gitmodules
69+
+++ b/.gitmodules
70+
@@ -1,3 +1,3 @@
71+
[submodule "gitea-mirror"]
72+
- path = gitea-mirror
73+
+ path = gitea
74+
url = https://gitea.com/gitea/gitea-mirror
75+
diff --git a/gitea-mirror b/gitea
76+
similarity index 100%
77+
rename from gitea-mirror
78+
rename to gitea
79+
`,
80+
},
81+
{
82+
name: "deleted",
83+
gitdiff: `diff --git a/.gitmodules b/.gitmodules
84+
index 0510edd..e69de29 100644
85+
--- a/.gitmodules
86+
+++ b/.gitmodules
87+
@@ -1,3 +0,0 @@
88+
-[submodule "gitea-mirror"]
89+
- path = gitea
90+
- url = https://gitea.com/gitea/gitea-mirror
91+
diff --git a/gitea b/gitea
92+
deleted file mode 160000
93+
index c8ffe77..0000000
94+
--- a/gitea
95+
+++ /dev/null
96+
@@ -1 +0,0 @@
97+
-Subproject commit c8ffe777cf9c5bb47a38e3e0b3a3b5de6cd8813d
98+
`,
99+
infos: map[int]SubmoduleInfo{
100+
1: {
101+
PreviousRefID: "c8ffe777cf9c5bb47a38e3e0b3a3b5de6cd8813d",
102+
},
103+
},
104+
},
105+
{
106+
name: "moved and updated",
107+
gitdiff: `diff --git a/.gitmodules b/.gitmodules
108+
index 0510edd..bced3d8 100644
109+
--- a/.gitmodules
110+
+++ b/.gitmodules
111+
@@ -1,3 +1,3 @@
112+
[submodule "gitea-mirror"]
113+
- path = gitea
114+
+ path = gitea-1.22
115+
url = https://gitea.com/gitea/gitea-mirror
116+
diff --git a/gitea b/gitea
117+
deleted file mode 160000
118+
index c8ffe77..0000000
119+
--- a/gitea
120+
+++ /dev/null
121+
@@ -1 +0,0 @@
122+
-Subproject commit c8ffe777cf9c5bb47a38e3e0b3a3b5de6cd8813d
123+
diff --git a/gitea-1.22 b/gitea-1.22
124+
new file mode 160000
125+
index 0000000..8eefa1f
126+
--- /dev/null
127+
+++ b/gitea-1.22
128+
@@ -0,0 +1 @@
129+
+Subproject commit 8eefa1f6dedf2488db2c9e12c916e8e51f673160
130+
`,
131+
infos: map[int]SubmoduleInfo{
132+
1: {
133+
PreviousRefID: "c8ffe777cf9c5bb47a38e3e0b3a3b5de6cd8813d",
134+
},
135+
2: {
136+
NewRefID: "8eefa1f6dedf2488db2c9e12c916e8e51f673160",
137+
},
138+
},
139+
},
140+
{
141+
name: "converted to file",
142+
gitdiff: `diff --git a/.gitmodules b/.gitmodules
143+
index 0510edd..e69de29 100644
144+
--- a/.gitmodules
145+
+++ b/.gitmodules
146+
@@ -1,3 +0,0 @@
147+
-[submodule "gitea-mirror"]
148+
- path = gitea
149+
- url = https://gitea.com/gitea/gitea-mirror
150+
diff --git a/gitea b/gitea
151+
deleted file mode 160000
152+
index c8ffe77..0000000
153+
--- a/gitea
154+
+++ /dev/null
155+
@@ -1 +0,0 @@
156+
-Subproject commit c8ffe777cf9c5bb47a38e3e0b3a3b5de6cd8813d
157+
diff --git a/gitea b/gitea
158+
new file mode 100644
159+
index 0000000..33a9488
160+
--- /dev/null
161+
+++ b/gitea
162+
@@ -0,0 +1 @@
163+
+example
164+
`,
165+
infos: map[int]SubmoduleInfo{
166+
1: {
167+
PreviousRefID: "c8ffe777cf9c5bb47a38e3e0b3a3b5de6cd8813d",
168+
},
169+
},
170+
},
171+
{
172+
name: "converted to submodule",
173+
gitdiff: `diff --git a/.gitmodules b/.gitmodules
174+
index e69de29..14ee267 100644
175+
--- a/.gitmodules
176+
+++ b/.gitmodules
177+
@@ -0,0 +1,3 @@
178+
+[submodule "gitea"]
179+
+ path = gitea
180+
+ url = https://gitea.com/gitea/gitea-mirror
181+
diff --git a/gitea b/gitea
182+
deleted file mode 100644
183+
index 33a9488..0000000
184+
--- a/gitea
185+
+++ /dev/null
186+
@@ -1 +0,0 @@
187+
-example
188+
diff --git a/gitea b/gitea
189+
new file mode 160000
190+
index 0000000..68972a9
191+
--- /dev/null
192+
+++ b/gitea
193+
@@ -0,0 +1 @@
194+
+Subproject commit 68972a994719ae5c74e28d8fa82fa27c23399bc8
195+
`,
196+
infos: map[int]SubmoduleInfo{
197+
2: {
198+
NewRefID: "68972a994719ae5c74e28d8fa82fa27c23399bc8",
199+
},
200+
},
201+
},
202+
}
203+
204+
for _, testcase := range tests {
205+
testcase := testcase
206+
t.Run(testcase.name, func(t *testing.T) {
207+
diff, err := ParsePatch(db.DefaultContext, setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, strings.NewReader(testcase.gitdiff), "")
208+
assert.NoError(t, err)
209+
210+
for i, expected := range testcase.infos {
211+
actual := diff.Files[i]
212+
assert.NotNil(t, actual)
213+
assert.Equal(t, expected, *actual.SubmoduleInfo)
214+
}
215+
})
216+
}
217+
}

0 commit comments

Comments
 (0)