Skip to content

Commit b094f9b

Browse files
wxiaoguangsilverwinddelvh
authored
Improve repo commit view (go-gitea#33877)
Fix go-gitea#24623 Major changes: 1. Redirect `/owner/repo/blob/*` requests to `/owner/repo/src/commit/*` (like GitHub) 2. Add a "view file diff" link (see screenshot below) 3. Refactor "AssertHTMLElement" to generic, now we can accurately assert existence or number. 4. Add more tests --------- Co-authored-by: silverwind <[email protected]> Co-authored-by: delvh <[email protected]>
1 parent 9c673d0 commit b094f9b

12 files changed

+116
-46
lines changed

options/locale/locale_en-US.ini

+1
Original file line numberDiff line numberDiff line change
@@ -1407,6 +1407,7 @@ commits.signed_by_untrusted_user_unmatched = Signed by untrusted user who does n
14071407
commits.gpg_key_id = GPG Key ID
14081408
commits.ssh_key_fingerprint = SSH Key Fingerprint
14091409
commits.view_path=View at this point in history
1410+
commits.view_file_diff = View changes to this file in this commit
14101411
14111412
commit.operations = Operations
14121413
commit.revert = Revert

routers/web/repo/view_home.go

+11
Original file line numberDiff line numberDiff line change
@@ -429,3 +429,14 @@ func RedirectRepoTreeToSrc(ctx *context.Context) {
429429
}
430430
ctx.Redirect(redirect)
431431
}
432+
433+
func RedirectRepoBlobToCommit(ctx *context.Context) {
434+
// redirect "/owner/repo/blob/*" requests to "/owner/repo/src/commit/*"
435+
// just like GitHub: browse files of a commit by "https://github/owner/repo/blob/{CommitID}"
436+
// TODO: maybe we could guess more types to redirect to the related pages in the future
437+
redirect := ctx.Repo.RepoLink + "/src/commit/" + ctx.PathParamRaw("*")
438+
if ctx.Req.URL.RawQuery != "" {
439+
redirect += "?" + ctx.Req.URL.RawQuery
440+
}
441+
ctx.Redirect(redirect)
442+
}

routers/web/web.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -1595,7 +1595,8 @@ func registerRoutes(m *web.Router) {
15951595
m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.Home)
15961596
m.Get("/*", context.RepoRefByType(""), repo.Home) // "/*" route is deprecated, and kept for backward compatibility
15971597
}, repo.SetEditorconfigIfExists)
1598-
m.Get("/tree/*", repo.RedirectRepoTreeToSrc) // redirect "/owner/repo/tree/*" requests to "/owner/repo/src/*"
1598+
m.Get("/tree/*", repo.RedirectRepoTreeToSrc) // redirect "/owner/repo/tree/*" requests to "/owner/repo/src/*"
1599+
m.Get("/blob/*", repo.RedirectRepoBlobToCommit) // redirect "/owner/repo/blob/*" requests to "/owner/repo/src/commit/*"
15991600

16001601
m.Get("/forks", context.RepoRef(), repo.Forks)
16011602
m.Get("/commit/{sha:([a-f0-9]{7,64})}.{ext:patch|diff}", repo.MustBeNotEmpty, repo.RawDiff)

templates/repo/commits_list.tmpl

+12-3
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,20 @@
6666
<td class="text right aligned">{{DateUtils.TimeSince .Author.When}}</td>
6767
{{end}}
6868
<td class="text right aligned tw-py-0">
69-
<button class="btn interact-bg tw-p-2" data-tooltip-content="{{ctx.Locale.Tr "copy_hash"}}" data-clipboard-text="{{.ID}}">{{svg "octicon-copy"}}</button>
70-
{{if not $.PageIsWiki}}{{/* at the moment, wiki doesn't support "view at history point*/}}
69+
<button class="btn interact-bg tw-p-2 copy-commit-id" data-tooltip-content="{{ctx.Locale.Tr "copy_hash"}}" data-clipboard-text="{{.ID}}">{{svg "octicon-copy"}}</button>
70+
{{/* at the moment, wiki doesn't support these "view" links like "view at history point" */}}
71+
{{if not $.PageIsWiki}}
72+
{{/* view single file diff */}}
73+
{{if $.FileName}}
74+
<a class="btn interact-bg tw-p-2 view-single-diff" data-tooltip-content="{{ctx.Locale.Tr "repo.commits.view_file_diff"}}"
75+
href="{{$commitRepoLink}}/commit/{{.ID.String}}?files={{$.FileName}}"
76+
>{{svg "octicon-file-diff"}}</a>
77+
{{end}}
78+
79+
{{/* view at history point */}}
7180
{{$viewCommitLink := printf "%s/src/commit/%s" $commitRepoLink (PathEscape .ID.String)}}
7281
{{if $.FileName}}{{$viewCommitLink = printf "%s/%s" $viewCommitLink (PathEscapeSegments $.FileName)}}{{end}}
73-
<a class="btn interact-bg tw-p-2" data-tooltip-content="{{ctx.Locale.Tr "repo.commits.view_path"}}" href="{{$viewCommitLink}}">{{svg "octicon-file-code"}}</a>
82+
<a class="btn interact-bg tw-p-2 view-commit-path" data-tooltip-content="{{ctx.Locale.Tr "repo.commits.view_path"}}" href="{{$viewCommitLink}}">{{svg "octicon-file-code"}}</a>
7483
{{end}}
7584
</td>
7685
</tr>

tests/integration/html_helper.go

+7-6
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,13 @@ func (doc *HTMLDoc) GetCSRF() string {
4242
return doc.GetInputValueByName("_csrf")
4343
}
4444

45-
// AssertElement check if element by selector exists or does not exist depending on checkExists
46-
func (doc *HTMLDoc) AssertElement(t testing.TB, selector string, checkExists bool) {
45+
// AssertHTMLElement check if element by selector exists or does not exist depending on checkExists
46+
func AssertHTMLElement[T int | bool](t testing.TB, doc *HTMLDoc, selector string, checkExists T) {
4747
sel := doc.doc.Find(selector)
48-
if checkExists {
49-
assert.Equal(t, 1, sel.Length())
50-
} else {
51-
assert.Equal(t, 0, sel.Length())
48+
switch v := any(checkExists).(type) {
49+
case bool:
50+
assert.Equal(t, v, sel.Length() > 0)
51+
case int:
52+
assert.Equal(t, v, sel.Length())
5253
}
5354
}

tests/integration/links_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ func TestRedirectsNoLogin(t *testing.T) {
5555
{"/user2/repo1/src/master/a%2fb.txt", "/user2/repo1/src/branch/master/a%2fb.txt"},
5656
{"/user2/repo1/src/master/directory/file.txt?a=1", "/user2/repo1/src/branch/master/directory/file.txt?a=1"},
5757
{"/user2/repo1/tree/a%2fb?a=1", "/user2/repo1/src/a%2fb?a=1"},
58+
{"/user2/repo1/blob/123456/%20?a=1", "/user2/repo1/src/commit/123456/%20?a=1"},
5859
{"/user/avatar/GhosT/-1", "/assets/img/avatar_default.png"},
5960
{"/user/avatar/Gitea-ActionS/0", "/assets/img/avatar_default.png"},
6061
{"/api/v1/swagger", "/api/swagger"},

tests/integration/oauth_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ func TestAuthorizeShow(t *testing.T) {
7777
resp := ctx.MakeRequest(t, req, http.StatusOK)
7878

7979
htmlDoc := NewHTMLParser(t, resp.Body)
80-
htmlDoc.AssertElement(t, "#authorize-app", true)
80+
AssertHTMLElement(t, htmlDoc, "#authorize-app", true)
8181
htmlDoc.GetCSRF()
8282
}
8383

tests/integration/signin_test.go

+12-6
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ func TestEnablePasswordSignInFormAndEnablePasskeyAuth(t *testing.T) {
112112

113113
req := NewRequest(t, "GET", "/user/login")
114114
resp := MakeRequest(t, req, http.StatusOK)
115-
NewHTMLParser(t, resp.Body).AssertElement(t, "form[action='/user/login']", false)
115+
doc := NewHTMLParser(t, resp.Body)
116+
AssertHTMLElement(t, doc, "form[action='/user/login']", false)
116117

117118
req = NewRequest(t, "POST", "/user/login")
118119
MakeRequest(t, req, http.StatusForbidden)
@@ -121,7 +122,8 @@ func TestEnablePasswordSignInFormAndEnablePasskeyAuth(t *testing.T) {
121122
defer web.RouteMockReset()
122123
web.RouteMock(web.MockAfterMiddlewares, mockLinkAccount)
123124
resp = MakeRequest(t, req, http.StatusOK)
124-
NewHTMLParser(t, resp.Body).AssertElement(t, "form[action='/user/link_account_signin']", false)
125+
doc = NewHTMLParser(t, resp.Body)
126+
AssertHTMLElement(t, doc, "form[action='/user/link_account_signin']", false)
125127
})
126128

127129
t.Run("EnablePasswordSignInForm=true", func(t *testing.T) {
@@ -130,7 +132,8 @@ func TestEnablePasswordSignInFormAndEnablePasskeyAuth(t *testing.T) {
130132

131133
req := NewRequest(t, "GET", "/user/login")
132134
resp := MakeRequest(t, req, http.StatusOK)
133-
NewHTMLParser(t, resp.Body).AssertElement(t, "form[action='/user/login']", true)
135+
doc := NewHTMLParser(t, resp.Body)
136+
AssertHTMLElement(t, doc, "form[action='/user/login']", true)
134137

135138
req = NewRequest(t, "POST", "/user/login")
136139
MakeRequest(t, req, http.StatusOK)
@@ -139,7 +142,8 @@ func TestEnablePasswordSignInFormAndEnablePasskeyAuth(t *testing.T) {
139142
defer web.RouteMockReset()
140143
web.RouteMock(web.MockAfterMiddlewares, mockLinkAccount)
141144
resp = MakeRequest(t, req, http.StatusOK)
142-
NewHTMLParser(t, resp.Body).AssertElement(t, "form[action='/user/link_account_signin']", true)
145+
doc = NewHTMLParser(t, resp.Body)
146+
AssertHTMLElement(t, doc, "form[action='/user/link_account_signin']", true)
143147
})
144148

145149
t.Run("EnablePasskeyAuth=false", func(t *testing.T) {
@@ -148,7 +152,8 @@ func TestEnablePasswordSignInFormAndEnablePasskeyAuth(t *testing.T) {
148152

149153
req := NewRequest(t, "GET", "/user/login")
150154
resp := MakeRequest(t, req, http.StatusOK)
151-
NewHTMLParser(t, resp.Body).AssertElement(t, ".signin-passkey", false)
155+
doc := NewHTMLParser(t, resp.Body)
156+
AssertHTMLElement(t, doc, ".signin-passkey", false)
152157
})
153158

154159
t.Run("EnablePasskeyAuth=true", func(t *testing.T) {
@@ -157,6 +162,7 @@ func TestEnablePasswordSignInFormAndEnablePasskeyAuth(t *testing.T) {
157162

158163
req := NewRequest(t, "GET", "/user/login")
159164
resp := MakeRequest(t, req, http.StatusOK)
160-
NewHTMLParser(t, resp.Body).AssertElement(t, ".signin-passkey", true)
165+
doc := NewHTMLParser(t, resp.Body)
166+
AssertHTMLElement(t, doc, ".signin-passkey", true)
161167
})
162168
}

tests/integration/timetracking_test.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ func testViewTimetrackingControls(t *testing.T, session *TestSession, user, repo
4242

4343
htmlDoc := NewHTMLParser(t, resp.Body)
4444

45-
htmlDoc.AssertElement(t, ".issue-start-time", canTrackTime)
46-
htmlDoc.AssertElement(t, ".issue-add-time", canTrackTime)
45+
AssertHTMLElement(t, htmlDoc, ".issue-start-time", canTrackTime)
46+
AssertHTMLElement(t, htmlDoc, ".issue-add-time", canTrackTime)
4747

4848
issueLink := path.Join(user, repo, "issues", issue)
4949
req = NewRequestWithValues(t, "POST", path.Join(issueLink, "times", "stopwatch", "toggle"), map[string]string{
@@ -59,8 +59,8 @@ func testViewTimetrackingControls(t *testing.T, session *TestSession, user, repo
5959
events := htmlDoc.doc.Find(".event > span.text")
6060
assert.Contains(t, events.Last().Text(), "started working")
6161

62-
htmlDoc.AssertElement(t, ".issue-stop-time", true)
63-
htmlDoc.AssertElement(t, ".issue-cancel-time", true)
62+
AssertHTMLElement(t, htmlDoc, ".issue-stop-time", true)
63+
AssertHTMLElement(t, htmlDoc, ".issue-cancel-time", true)
6464

6565
// Sleep for 1 second to not get wrong order for stopping timer
6666
time.Sleep(time.Second)

tests/integration/user_settings_test.go

+24-24
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,21 @@ import (
1919
func assertNavbar(t *testing.T, doc *HTMLDoc) {
2020
// Only show the account page if users can change their email notifications, delete themselves, or manage credentials
2121
if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureDeletion, setting.UserFeatureManageCredentials) && !setting.Service.EnableNotifyMail {
22-
doc.AssertElement(t, ".menu a[href='/user/settings/account']", false)
22+
AssertHTMLElement(t, doc, ".menu a[href='/user/settings/account']", false)
2323
} else {
24-
doc.AssertElement(t, ".menu a[href='/user/settings/account']", true)
24+
AssertHTMLElement(t, doc, ".menu a[href='/user/settings/account']", true)
2525
}
2626

2727
if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageMFA, setting.UserFeatureManageCredentials) {
28-
doc.AssertElement(t, ".menu a[href='/user/settings/security']", false)
28+
AssertHTMLElement(t, doc, ".menu a[href='/user/settings/security']", false)
2929
} else {
30-
doc.AssertElement(t, ".menu a[href='/user/settings/security']", true)
30+
AssertHTMLElement(t, doc, ".menu a[href='/user/settings/security']", true)
3131
}
3232

3333
if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageSSHKeys, setting.UserFeatureManageGPGKeys) {
34-
doc.AssertElement(t, ".menu a[href='/user/settings/keys']", false)
34+
AssertHTMLElement(t, doc, ".menu a[href='/user/settings/keys']", false)
3535
} else {
36-
doc.AssertElement(t, ".menu a[href='/user/settings/keys']", true)
36+
AssertHTMLElement(t, doc, ".menu a[href='/user/settings/keys']", true)
3737
}
3838
}
3939

@@ -64,11 +64,11 @@ func TestUserSettingsAccount(t *testing.T) {
6464
doc := NewHTMLParser(t, resp.Body)
6565

6666
// account navbar should display
67-
doc.AssertElement(t, ".menu a[href='/user/settings/account']", true)
67+
AssertHTMLElement(t, doc, ".menu a[href='/user/settings/account']", true)
6868

69-
doc.AssertElement(t, "#password", true)
70-
doc.AssertElement(t, "#email", true)
71-
doc.AssertElement(t, "#delete-form", true)
69+
AssertHTMLElement(t, doc, "#password", true)
70+
AssertHTMLElement(t, doc, "#email", true)
71+
AssertHTMLElement(t, doc, "#delete-form", true)
7272
})
7373

7474
t.Run("credentials disabled", func(t *testing.T) {
@@ -83,9 +83,9 @@ func TestUserSettingsAccount(t *testing.T) {
8383

8484
assertNavbar(t, doc)
8585

86-
doc.AssertElement(t, "#password", false)
87-
doc.AssertElement(t, "#email", false)
88-
doc.AssertElement(t, "#delete-form", true)
86+
AssertHTMLElement(t, doc, "#password", false)
87+
AssertHTMLElement(t, doc, "#email", false)
88+
AssertHTMLElement(t, doc, "#delete-form", true)
8989
})
9090

9191
t.Run("deletion disabled", func(t *testing.T) {
@@ -100,9 +100,9 @@ func TestUserSettingsAccount(t *testing.T) {
100100

101101
assertNavbar(t, doc)
102102

103-
doc.AssertElement(t, "#password", true)
104-
doc.AssertElement(t, "#email", true)
105-
doc.AssertElement(t, "#delete-form", false)
103+
AssertHTMLElement(t, doc, "#password", true)
104+
AssertHTMLElement(t, doc, "#email", true)
105+
AssertHTMLElement(t, doc, "#delete-form", false)
106106
})
107107

108108
t.Run("deletion, credentials and email notifications are disabled", func(t *testing.T) {
@@ -249,7 +249,7 @@ func TestUserSettingsSecurity(t *testing.T) {
249249

250250
assertNavbar(t, doc)
251251

252-
doc.AssertElement(t, "#register-webauthn", true)
252+
AssertHTMLElement(t, doc, "#register-webauthn", true)
253253
})
254254

255255
t.Run("mfa disabled", func(t *testing.T) {
@@ -263,7 +263,7 @@ func TestUserSettingsSecurity(t *testing.T) {
263263

264264
assertNavbar(t, doc)
265265

266-
doc.AssertElement(t, "#register-webauthn", false)
266+
AssertHTMLElement(t, doc, "#register-webauthn", false)
267267
})
268268

269269
t.Run("credentials and mfa disabled", func(t *testing.T) {
@@ -356,8 +356,8 @@ func TestUserSettingsKeys(t *testing.T) {
356356

357357
assertNavbar(t, doc)
358358

359-
doc.AssertElement(t, "#add-ssh-button", true)
360-
doc.AssertElement(t, "#add-gpg-key-panel", true)
359+
AssertHTMLElement(t, doc, "#add-ssh-button", true)
360+
AssertHTMLElement(t, doc, "#add-gpg-key-panel", true)
361361
})
362362

363363
t.Run("ssh keys disabled", func(t *testing.T) {
@@ -372,8 +372,8 @@ func TestUserSettingsKeys(t *testing.T) {
372372

373373
assertNavbar(t, doc)
374374

375-
doc.AssertElement(t, "#add-ssh-button", false)
376-
doc.AssertElement(t, "#add-gpg-key-panel", true)
375+
AssertHTMLElement(t, doc, "#add-ssh-button", false)
376+
AssertHTMLElement(t, doc, "#add-gpg-key-panel", true)
377377
})
378378

379379
t.Run("gpg keys disabled", func(t *testing.T) {
@@ -388,8 +388,8 @@ func TestUserSettingsKeys(t *testing.T) {
388388

389389
assertNavbar(t, doc)
390390

391-
doc.AssertElement(t, "#add-ssh-button", true)
392-
doc.AssertElement(t, "#add-gpg-key-panel", false)
391+
AssertHTMLElement(t, doc, "#add-ssh-button", true)
392+
AssertHTMLElement(t, doc, "#add-gpg-key-panel", false)
393393
})
394394

395395
t.Run("ssh & gpg keys disabled", func(t *testing.T) {

tests/integration/user_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -273,5 +273,5 @@ func TestUserLocationMapLink(t *testing.T) {
273273
req = NewRequest(t, "GET", "/user2/")
274274
resp := session.MakeRequest(t, req, http.StatusOK)
275275
htmlDoc := NewHTMLParser(t, resp.Body)
276-
htmlDoc.AssertElement(t, `a[href="https://example/foo/A%2Fb"]`, true)
276+
AssertHTMLElement(t, htmlDoc, `a[href="https://example/foo/A%2Fb"]`, true)
277277
}

tests/integration/view_test.go

+40
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,43 @@ func TestRenderFileSVGIsInImgTag(t *testing.T) {
2525
assert.True(t, exists, "The SVG image should be in an <img> tag so that scripts in the SVG are not run")
2626
assert.Equal(t, "/user2/repo2/raw/branch/master/line.svg", src)
2727
}
28+
29+
func TestCommitListActions(t *testing.T) {
30+
defer tests.PrepareTestEnv(t)()
31+
session := loginUser(t, "user2")
32+
33+
t.Run("WikiRevisionList", func(t *testing.T) {
34+
defer tests.PrintCurrentTest(t)()
35+
36+
req := NewRequest(t, "GET", "/user2/repo1/wiki/Home?action=_revision")
37+
resp := session.MakeRequest(t, req, http.StatusOK)
38+
htmlDoc := NewHTMLParser(t, resp.Body)
39+
AssertHTMLElement(t, htmlDoc, ".commit-list .copy-commit-id", true)
40+
AssertHTMLElement(t, htmlDoc, `.commit-list .view-single-diff`, false)
41+
AssertHTMLElement(t, htmlDoc, `.commit-list .view-commit-path`, false)
42+
})
43+
44+
t.Run("RepoCommitList", func(t *testing.T) {
45+
defer tests.PrintCurrentTest(t)()
46+
47+
req := NewRequest(t, "GET", "/user2/repo1/commits/branch/master")
48+
resp := session.MakeRequest(t, req, http.StatusOK)
49+
htmlDoc := NewHTMLParser(t, resp.Body)
50+
51+
AssertHTMLElement(t, htmlDoc, `.commit-list .copy-commit-id`, true)
52+
AssertHTMLElement(t, htmlDoc, `.commit-list .view-single-diff`, false)
53+
AssertHTMLElement(t, htmlDoc, `.commit-list .view-commit-path`, true)
54+
})
55+
56+
t.Run("RepoFileHistory", func(t *testing.T) {
57+
defer tests.PrintCurrentTest(t)()
58+
59+
req := NewRequest(t, "GET", "/user2/repo1/commits/branch/master/README.md")
60+
resp := session.MakeRequest(t, req, http.StatusOK)
61+
htmlDoc := NewHTMLParser(t, resp.Body)
62+
63+
AssertHTMLElement(t, htmlDoc, `.commit-list .copy-commit-id`, true)
64+
AssertHTMLElement(t, htmlDoc, `.commit-list .view-single-diff`, true)
65+
AssertHTMLElement(t, htmlDoc, `.commit-list .view-commit-path`, true)
66+
})
67+
}

0 commit comments

Comments
 (0)