feat(ui): commit view redesign for pull request page (#7948)

Co-authored-by: 0ko <0ko@noreply.codeberg.org>
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/7948
Reviewed-by: 0ko <0ko@noreply.codeberg.org>
This commit is contained in:
Yashwanth Rathakrishnan
2026-06-02 20:12:32 +02:00
committed by 0ko
co-authored by 0ko
parent 1d12151086
commit cce8152879
16 changed files with 402 additions and 62 deletions
+2 -2
View File
@@ -1125,7 +1125,7 @@ func fullHashPatternProcessor(ctx *RenderContext, node *html.Node) {
if hash != "" {
text += " (" + hash + ")"
}
replaceContent(node, start, end, createCodeLink(urlFull, text, "commit"))
replaceContent(node, start, end, createCodeLink(urlFull, text, ""))
node = node.NextSibling.NextSibling
}
}
@@ -1409,7 +1409,7 @@ func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) {
}
link := util.URLJoin(ctx.Links.Prefix(), ctx.Metas["user"], ctx.Metas["repo"], "commit", hash)
replaceContent(node, m[2], m[3], createCodeLink(link, base.ShortSha(hash), "commit"))
replaceContent(node, m[2], m[3], createCodeLink(link, base.ShortSha(hash), ""))
start = 0
node = node.NextSibling.NextSibling
}
+17 -17
View File
@@ -340,52 +340,52 @@ func TestRender_AutoLink(t *testing.T) {
t.Run("Commit", func(t *testing.T) {
// render valid commit URLs
tmp := util.URLJoin(TestRepoURL, "commit", "d8a994ef243349f321568f9e36d5c3f444b99cae")
assert(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">d8a994ef24</code></a>", TestRepoURLWithoutSlash)
assert(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">"+TestOrgRepo+"@d8a994ef24</code></a>", "/forgejo/forgejo")
assert(tmp, "<a href=\""+tmp+"\"><code class=\"nohighlight\">d8a994ef24</code></a>", TestRepoURLWithoutSlash)
assert(tmp, "<a href=\""+tmp+"\"><code class=\"nohighlight\">"+TestOrgRepo+"@d8a994ef24</code></a>", "/forgejo/forgejo")
assert(
tmp+"#diff-2",
"<a href=\""+tmp+"#diff-2\" class=\"commit\"><code class=\"nohighlight\">d8a994ef24 (diff-2)</code></a>",
"<a href=\""+tmp+"#diff-2\"><code class=\"nohighlight\">d8a994ef24 (diff-2)</code></a>",
TestRepoURL,
)
assert(
tmp+"#diff-953bb4f01b7c77fa18f0cd54211255051e647dbc",
"<a href=\""+tmp+"#diff-953bb4f01b7c77fa18f0cd54211255051e647dbc\" class=\"commit\"><code class=\"nohighlight\">d8a994ef24 (diff-953bb4f01b)</code></a>",
"<a href=\""+tmp+"#diff-953bb4f01b7c77fa18f0cd54211255051e647dbc\"><code class=\"nohighlight\">d8a994ef24 (diff-953bb4f01b)</code></a>",
TestRepoURLWithoutSlash,
)
// render other commit URLs
tmp = "https://external-link.gitea.io/go-gitea/gitea/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2"
assert(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">external-link.gitea.io/go-gitea/gitea@d8a994ef24 (diff-2)</code></a>", TestOrgRepo)
assert(tmp, "<a href=\""+tmp+"\"><code class=\"nohighlight\">external-link.gitea.io/go-gitea/gitea@d8a994ef24 (diff-2)</code></a>", TestOrgRepo)
defer test.MockVariableValue(&setting.AppURL, "https://external-link.gitea.io/")()
assert(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">d8a994ef24 (diff-2)</code></a>", "https://external-link.gitea.io/go-gitea/gitea")
assert(tmp, "<a href=\""+tmp+"\"><code class=\"nohighlight\">d8a994ef24 (diff-2)</code></a>", "https://external-link.gitea.io/go-gitea/gitea")
tmp = TestAppURL + "gogits/gogs/src/commit/190d9492934af498c3f669d6a2431dc5459e5b20"
assert(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">localhost:3000/gogits/gogs@190d949293</code></a>", "https://external-link.gitea.io/go-gitea/gitea")
assert(tmp, "<a href=\""+tmp+"\"><code class=\"nohighlight\">localhost:3000/gogits/gogs@190d949293</code></a>", "https://external-link.gitea.io/go-gitea/gitea")
defer test.MockVariableValue(&setting.AppURL, TestAppURL)()
assert(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">190d949293</code></a>", "http://localhost:3000/gogits/gogs")
assert(tmp, "<a href=\""+tmp+"\"><code class=\"nohighlight\">190d949293</code></a>", "http://localhost:3000/gogits/gogs")
tmp = "http://localhost:3000/sub/gogits/gogs/src/commit/190d9492934af498c3f669d6a2431dc5459e5b20"
assert(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">localhost:3000/sub/gogits/gogs@190d949293</code></a>", TestRepoURLWithoutSlash)
assert(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">localhost:3000/sub/gogits/gogs@190d949293</code></a>", "https://external-link.gitea.io/go-gitea/gitea")
assert(tmp, "<a href=\""+tmp+"\"><code class=\"nohighlight\">localhost:3000/sub/gogits/gogs@190d949293</code></a>", TestRepoURLWithoutSlash)
assert(tmp, "<a href=\""+tmp+"\"><code class=\"nohighlight\">localhost:3000/sub/gogits/gogs@190d949293</code></a>", "https://external-link.gitea.io/go-gitea/gitea")
defer test.MockVariableValue(&setting.AppURL, TestAppURL+"sub/")()
assert(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">190d949293</code></a>", "http://localhost:3000/sub/gogits/gogs")
assert(tmp, "<a href=\""+tmp+"\"><code class=\"nohighlight\">190d949293</code></a>", "http://localhost:3000/sub/gogits/gogs")
tmp = "http://localhost:3000/sub1/sub2/sub3/gogits/gogs/src/commit/190d9492934af498c3f669d6a2431dc5459e5b20"
defer test.MockVariableValue(&setting.AppURL, TestAppURL+"sub1/sub2/sub3/")()
assert(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">190d949293</code></a>", "http://localhost:3000/sub1/sub2/sub3/gogits/gogs")
assert(tmp, "<a href=\""+tmp+"\"><code class=\"nohighlight\">190d949293</code></a>", "http://localhost:3000/sub1/sub2/sub3/gogits/gogs")
defer test.MockVariableValue(&setting.AppURL, TestAppURL)()
assert(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">localhost:3000/sub1/sub2/sub3/gogits/gogs@190d949293</code></a>", "http://localhost:3000/sub1/gogits/gogs")
assert(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">localhost:3000/sub1/sub2/sub3/gogits/gogs@190d949293</code></a>", "https://external-link.gitea.io/go-gitea/gitea")
assert(tmp, "<a href=\""+tmp+"\"><code class=\"nohighlight\">localhost:3000/sub1/sub2/sub3/gogits/gogs@190d949293</code></a>", "http://localhost:3000/sub1/gogits/gogs")
assert(tmp, "<a href=\""+tmp+"\"><code class=\"nohighlight\">localhost:3000/sub1/sub2/sub3/gogits/gogs@190d949293</code></a>", "https://external-link.gitea.io/go-gitea/gitea")
// if the repository happens to be named like one of the known app routes (e.g. `src`),
// we can parse the URL correctly, if there is no sub path
tmp = "http://localhost:3000/gogits/src/commit/190d9492934af498c3f669d6a2431dc5459e5b20"
assert(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">gogits/src@190d949293</code></a>", TestRepoURL)
assert(tmp, "<a href=\""+tmp+"\"><code class=\"nohighlight\">gogits/src@190d949293</code></a>", TestRepoURL)
tmp = "http://localhost:3000/gogits/src/src/commit/190d9492934af498c3f669d6a2431dc5459e5b20"
assert(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">gogits/src@190d949293</code></a>", TestRepoURL)
assert(tmp, "<a href=\""+tmp+"\"><code class=\"nohighlight\">gogits/src@190d949293</code></a>", TestRepoURL)
// but if there is a sub path, we cannot reliably distinguish the repo name from the app route
tmp = "http://localhost:3000/sub/gogits/src/commit/190d9492934af498c3f669d6a2431dc5459e5b20"
assert(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">sub/gogits@190d949293</code></a>", TestRepoURL)
assert(tmp, "<a href=\""+tmp+"\"><code class=\"nohighlight\">sub/gogits@190d949293</code></a>", TestRepoURL)
})
t.Run("Compare", func(t *testing.T) {
+9
View File
@@ -26,6 +26,11 @@ func (du *DateUtils) AbsoluteShort(time any) template.HTML {
return dateTimeFormat("short", time)
}
// AbsoluteShort renders in "Sun, 01 Jan, 2006" format
func (du *DateUtils) AbsoluteShortWithDay(time any) template.HTML {
return dateTimeFormat("short-with-day", time)
}
// AbsoluteLong renders in "January 01, 2006" format
func (du *DateUtils) AbsoluteLong(time any) template.HTML {
return dateTimeFormat("long", time)
@@ -114,6 +119,10 @@ func dateTimeFormat(format string, datetime any) template.HTML {
case "short", "long": // date only
attrs = append(attrs, `month="`+format+`"`, `day="numeric"`)
return template.HTML(fmt.Sprintf(`<absolute-date %s date="%s">%s</absolute-date>`, strings.Join(attrs, " "), datetimeEscaped, textEscaped))
case "short-with-day":
attrs = append(attrs, `month="short"`, `day="numeric"`)
attrs[0] = `weekday="short"`
return template.HTML(fmt.Sprintf(`<absolute-date %s date="%s">%s</absolute-date>`, strings.Join(attrs, " "), datetimeEscaped, textEscaped))
case "full": // full date including time
attrs = append(attrs, `format="datetime"`, `month="short"`, `day="numeric"`, `hour="numeric"`, `minute="numeric"`, `second="numeric"`, `data-tooltip-content`, `data-tooltip-interactive="true"`)
return template.HTML(fmt.Sprintf(`<relative-time %s datetime="%s">%s</relative-time>`, strings.Join(attrs, " "), datetimeEscaped, textEscaped))
+6 -6
View File
@@ -20,7 +20,7 @@ import (
"github.com/stretchr/testify/assert"
)
const testInput = ` space @mention-user
const testInput = ` space @mention-user
/just/a/path.bin
https://example.com/file.bin
[local link](file.bin)
@@ -113,7 +113,7 @@ func TestRenderCommitBody(t *testing.T) {
[[remote link|<a href="https://example.com/image.jpg" class="link">https://example.com/image.jpg</a>]]
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" class="compare"><code class="nohighlight">88fc37a3c0...12fc37a3c0 (hash)</code></a>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" class="commit"><code class="nohighlight">88fc37a3c0</code></a>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb"><code class="nohighlight">88fc37a3c0</code></a>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
<span class="emoji" aria-label="thumbs up" data-alias="+1">👍</span>
<a href="mailto:mail@domain.com" class="mailto">mail@domain.com</a>
@@ -125,7 +125,7 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
}
func TestRenderCommitMessage(t *testing.T) {
expected := `space <a href="/mention-user" class="mention">@mention-user</a> `
expected := `space <a href="/mention-user" class="mention">@mention-user</a>`
assert.EqualValues(t, expected, RenderCommitMessage(t.Context(), testInput, testMetas))
}
@@ -137,7 +137,7 @@ func TestRenderCommitMessageLinkSubject(t *testing.T) {
}
func TestRenderIssueTitle(t *testing.T) {
expected := ` space @mention-user
expected := ` space @mention-user
/just/a/path.bin
https://example.com/file.bin
[local link](file.bin)
@@ -163,7 +163,7 @@ mail@domain.com
}
func TestRenderRefIssueTitle(t *testing.T) {
expected := ` space @mention-user
expected := ` space @mention-user
/just/a/path.bin
https://example.com/file.bin
[local link](file.bin)
@@ -189,7 +189,7 @@ mail@domain.com
}
func TestRenderMarkdownToHtml(t *testing.T) {
expected := `<p>space <a href="/mention-user" class="mention" rel="nofollow">@mention-user</a><br/>
expected := `<p>space <a href="/mention-user" class="mention" rel="nofollow">@mention-user</a>
/just/a/path.bin
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a>
<a href="/file.bin" rel="nofollow">local link</a>
+15
View File
@@ -28,6 +28,21 @@
</div>
</td>
<td class="sha">
{{$class := "ui sha label"}}
{{if .Signature}}
{{$class = (print $class " isSigned")}}
{{if .Verification.Verified}}
{{if eq .Verification.TrustStatus "trusted"}}
{{$class = (print $class " isVerified")}}
{{else if eq .Verification.TrustStatus "untrusted"}}
{{$class = (print $class " isVerifiedUntrusted")}}
{{else}}
{{$class = (print $class " isVerifiedUnmatched")}}
{{end}}
{{else if .Verification.Warning}}
{{$class = (print $class " isWarning")}}
{{end}}
{{end}}
{{$commitShaLink := ""}}
{{if $.PageIsWiki}}
{{$commitShaLink = (printf "%s/wiki/commit/%s" $commitRepoLink (PathEscape .ID.String))}}
+23 -24
View File
@@ -1,27 +1,22 @@
<h4 class="ui top attached header commits-table tw-flex tw-items-center tw-justify-between">
<div class="commits-table-left tw-flex tw-items-center">
{{if or .PageIsCommits (gt .CommitCount 0)}}
{{ctx.Locale.TrPluralString .CommitCount "counters.n_commits" (ctx.Locale.PrettyNumber .CommitCount)}}
{{else if .IsNothingToCompare}}
{{ctx.Locale.Tr "repo.commits.nothing_to_compare"}}
{{else}}
{{ctx.Locale.Tr "repo.commits.no_commits" $.BaseBranch $.HeadBranch}}
{{end}}
</div>
{{if .IsDiffCompare}}
<div class="commits-table-right tw-whitespace-nowrap">
<a href="{{$.CommitRepoLink}}/commit/{{.BeforeCommitID | PathEscape}}" class="primary sha label tw-mx-0">
<span class="shortsha">{{if not .BaseIsCommit}}{{if .BaseIsBranch}}{{svg "octicon-git-branch"}}{{else if .BaseIsTag}}{{svg "octicon-tag"}}{{end}}{{.BaseBranch}}{{else}}{{ShortSha .BaseBranch}}{{end}}</span>
</a>
...
<a href="{{$.CommitRepoLink}}/commit/{{.AfterCommitID | PathEscape}}" class="primary sha label tw-mx-0">
<span class="shortsha">{{if not .HeadIsCommit}}{{if .HeadIsBranch}}{{svg "octicon-git-branch"}}{{else if .HeadIsTag}}{{svg "octicon-tag"}}{{end}}{{.HeadBranch}}{{else}}{{ShortSha .HeadBranch}}{{end}}</span>
</a>
</div>
{{end}}
</h4>
{{if .PageIsCommits}}
<h4 class="ui top attached header commits-table tw-flex tw-items-center tw-justify-between">
<div class="commits-table-left tw-flex tw-items-center">
{{if or .PageIsCommits (gt .CommitCount 0)}}
{{ctx.Locale.TrPluralString .CommitCount "counters.n_commits" (ctx.Locale.PrettyNumber .CommitCount)}}
{{else if .IsNothingToCompare}}
{{ctx.Locale.Tr "repo.commits.nothing_to_compare"}}
{{else}}
{{ctx.Locale.Tr "repo.commits.no_commits" $.BaseBranch $.HeadBranch}}
{{end}}
</div>
{{if .IsDiffCompare}}
<div class="commits-table-right tw-whitespace-nowrap">
<a href="{{$.CommitRepoLink}}/commit/{{.BeforeCommitID | PathEscape}}" class="ui green sha label tw-mx-0">{{if not .BaseIsCommit}}{{if .BaseIsBranch}}{{svg "octicon-git-branch"}}{{else if .BaseIsTag}}{{svg "octicon-tag"}}{{end}}{{.BaseBranch}}{{else}}{{ShortSha .BaseBranch}}{{end}}</a>
...
<a href="{{$.CommitRepoLink}}/commit/{{.AfterCommitID | PathEscape}}" class="ui green sha label tw-mx-0">{{if not .HeadIsCommit}}{{if .HeadIsBranch}}{{svg "octicon-git-branch"}}{{else if .HeadIsTag}}{{svg "octicon-tag"}}{{end}}{{.HeadBranch}}{{else}}{{ShortSha .HeadBranch}}{{end}}</a>
</div>
{{end}}
</h4>
<div class="ui attached segment">
<form class="ignore-dirty" action="{{.RepoLink}}/commits/{{.BranchNameSubURL}}/search">
<div class="ui small fluid action input">
@@ -34,7 +29,11 @@
{{end}}
{{if and .Commits (gt .CommitCount 0)}}
{{template "repo/commits_list" .}}
{{if .PageIsPullCommits}}
{{template "repo/pulls/commits_list" .}}
{{else}}
{{template "repo/commits_list" .}}
{{end}}
{{end}}
{{if .OldFilename}}
+114
View File
@@ -0,0 +1,114 @@
{{$commitRepoLink := $.RepoLink}}
{{if $.CommitRepoLink}}{{$commitRepoLink = $.CommitRepoLink}}{{end}}
{{$prevDate := ""}}
{{range .Commits}}
{{$commitTime := .Committer.When}}
{{if not $commitTime}}{{$commitTime = .Author.When}}{{end}}
{{$commitDate := $commitTime.Format "2006-01-02"}}
{{if ne $commitDate $prevDate}}
{{if ne $prevDate ""}}</div></article>{{end}} <!-- close previous date group -->
{{$prevDate = $commitDate}}
<!-- Start a new commit group -->
<article class="commit-group">
<h4>{{DateUtils.AbsoluteShortWithDay $commitTime}}</h4>
<div class="commits tw-divide-y tw-divide-[--color-secondary]">
{{end}}
<div class="commit">
<div class="author">
{{$userName := .Author.Name}}
{{if .User}}
{{if and .User.FullName DefaultShowFullName}}
{{$userName = .User.FullName}}
{{end}}
{{ctx.AvatarUtils.Avatar .User 28}}<a class="muted author-name" href="{{.User.HomeLink}}">{{$userName}}</a>
{{else}}
{{ctx.AvatarUtils.AvatarByEmail .Author.Email .Author.Name 28}}
<span class="author-name">{{$userName}}</span>
{{end}}
</div>
<div class="message">
{{$commitLink := (printf "%s/pulls/%d/commits/%s" $commitRepoLink $.Issue.Index (PathEscape .ID.String))}}
<div class="message-wrapper">
{{RenderCommitMessageLinkSubject $.Context .Message $commitLink ($.Repository.ComposeMetas ctx)}}
{{if IsMultilineCommitMessage .Message}}
<button class="ui button js-toggle-commit-body ellipsis-button" aria-expanded="false">...</button>
{{end}}
<span class="ci-status">
{{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}}
</span>
{{if IsMultilineCommitMessage .Message}}
<pre class="commit-body tw-hidden">{{RenderCommitBody $.Context .Message ($.Repository.ComposeMetas ctx)}}</pre>
{{end}}
</div>
</div>
<div class="actions">
<div class="button-sequence">
{{$commitShaLink := (printf "%s/pulls/%d/commits/%s" $commitRepoLink $.Issue.Index (PathEscape .ID.String))}}
{{template "repo/shabox" (dict
"sha1" .ID.String
"commitLink" $commitShaLink
"signature" .Signature
"verification" .Verification
)}}
<button class="btn interact-bg tw-p-2" data-clipboard-text="{{.ID}}">
{{svg "octicon-copy"}}
</button>
</div>
<details class="dropdown dir-rtl">
<summary class="border" aria-label="{{ctx.Locale.Tr "toggle_menu"}}">
{{svg "octicon-kebab-horizontal"}}
</summary>
<div class="content">
<ul>
{{if $.FileName}}
<li>
<a
class="interact item"
data-tooltip-content="{{ctx.Locale.Tr "repo.commits.view_single_diff"}}"
href="{{printf "%s/commit/%s?files=%s" $commitRepoLink (PathEscape .ID.String) (PathEscapeSegments $.FileName)}}">
{{svg "octicon-file-diff"}}
</a>
</li>
{{end}}
<li>
<a
class="interact item"
data-tooltip-content="{{ctx.Locale.Tr "repo.commits.view_path"}}"
href="{{if $.FileName}}{{printf "%s/src/commit/%s/%s" $commitRepoLink (PathEscape .ID.String) (PathEscapeSegments $.FileName)}}{{else}}{{printf "%s/src/commit/%s" $commitRepoLink (PathEscape .ID.String)}}{{end}}">
{{svg "octicon-file-code"}} {{ctx.Locale.Tr "repo.commits.view_path"}}
</a>
</li>
</ul>
</div>
</details>
</div>
<div class="date">
{{if .Committer}}{{DateUtils.TimeSince .Committer.When}}{{else}}{{DateUtils.TimeSince .Author.When}}{{end}}
</div>
<div class="commit-buttons">
<button class="btn interact-bg tw-p-2" data-tooltip-content="{{ctx.Locale.Tr "copy_hash"}}" data-clipboard-text="{{.ID}}">{{svg "octicon-copy"}}</button>
{{if $.FileName}}
<a
class="btn interact-bg tw-p-2"
data-tooltip-content="{{ctx.Locale.Tr "repo.commits.view_single_diff"}}"
href="{{printf "%s/commit/%s?files=%s" $commitRepoLink (PathEscape .ID.String) (PathEscapeSegments $.FileName)}}">
{{svg "octicon-file-diff"}}
</a>
{{end}}
<a
class="btn interact-bg tw-p-2"
data-tooltip-content="{{ctx.Locale.Tr "repo.commits.view_path"}}"
href="{{if $.FileName}}{{printf "%s/src/commit/%s/%s" $commitRepoLink (PathEscape .ID.String) (PathEscapeSegments $.FileName)}}{{else}}{{printf "%s/src/commit/%s" $commitRepoLink (PathEscape .ID.String)}}{{end}}">
{{svg "octicon-file-code"}}
</a>
</div>
</div>
{{end}}
</div></div> <!-- Final group close -->
+3 -1
View File
@@ -1,7 +1,9 @@
{{if .IsSigned}}
<style>
.comment,
.commit-summary,
.singular-commit,
.commit-header,
.message-wrapper,
.commit-body {
.mention[href="{{AppSubUrl}}/{{.SignedUser.Name}}" i] {
background-color: var(--color-primary-alpha-30);
+97
View File
@@ -0,0 +1,97 @@
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-or-later
// @watch start
// templates/repo/pulls/commits_list.tmpl
// web_src/css/repo.css
// web_src/css/repo/commit-list.css
// @watch end
import {expect} from '@playwright/test';
import {test} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
test.describe(`PR commits`, () => {
test.use({user: 'user2'});
test('Any layout', async ({page}) => {
const response = await page.goto('/user2/repo1/pulls/3/commits');
expect(response?.status()).toBe(200);
const commitGroup = page.locator('.commit-group:first-of-type');
// Date group visibility test
await expect(commitGroup).toBeVisible();
await expect(commitGroup.locator('h4')).toBeVisible();
const commit = commitGroup.locator('.commit:first-child');
await expect(commit).toHaveCSS('display', 'grid');
});
test('Mobile responsive layout checks', async ({page, isMobile}) => {
test.skip(!isMobile);
const response = await page.goto('/user2/repo1/pulls/3/commits');
expect(response?.status()).toBe(200);
// Mobile-specific visibility test
const commit = page.locator('.commit-group:first-of-type .commit:first-child');
await expect(commit.locator('.commit-buttons')).toBeHidden();
await expect(commit.locator('.button-sequence button[data-clipboard-text]')).toBeVisible();
// Mobile-specific grid positioning
// toHaveCSS returns absolute values in px with decimals. This matcher only
// checks if the string has two \S+px separated by one \s+
await expect(commit).toHaveCSS('grid-template-columns', /^\S+px\s+\S+px$/);
await expect(commit.locator('.author')).toHaveCSS('grid-column-start', '1');
await expect(commit.locator('.date')).toHaveCSS('grid-column-start', '2');
await expect(commit.locator('.message')).toHaveCSS('grid-column-end', 'span 2');
// Horizontal scrolling to check for overflow
await expect(page.locator('.commits').first()).not.toHaveCSS('overflow-x', 'scroll');
await screenshot(page);
});
test('Dropdown check in mobile viewport', async ({page, isMobile}) => {
test.skip(!isMobile);
const response = await page.goto('/user2/repo1/pulls/3/commits');
expect(response?.status()).toBe(200);
const commit = page.locator('.commit-group:first-of-type .commit:first-child');
// Click dropdown btn
const dropdown = commit.locator('details.dropdown');
await expect(dropdown).toBeVisible();
await dropdown.locator('summary').click();
// List menu items of dropdown
const menuItem = commit.locator('details.dropdown ul li a'); // repo_path; always visible
await expect(menuItem).toHaveAttribute('href', '/user2/repo1/src/commit/5f22f7d0d95d614d25a5b68592adb345a4b5c7fd');
await menuItem.click();
await page.waitForURL(/.*\/user2\/repo1\/src\/commit\/5f22f7d0d95d614d25a5b68592adb345a4b5c7f/);
await screenshot(page);
});
test('Desktop responsive layout checks', async ({page, isMobile}) => {
test.skip(isMobile);
const response = await page.goto('/user2/repo1/pulls/3/commits');
expect(response?.status()).toBe(200);
// Desktop-specific visibility test
const commit = page.locator('.commit-group:first-of-type .commit:first-child');
await expect(commit.locator('.commit-buttons')).toBeVisible();
await expect(commit.locator('.button-sequence button[data-clipboard-text]')).toBeHidden();
await expect(commit.locator('details.dropdown')).toBeHidden();
// Desktop layout is has specific grid-template-columns
// toHaveCSS returns absolute values in px with decimals. This matcher only
// checks if the string has five \S+px separated by four \s+
await expect(commit).toHaveCSS('grid-template-columns', /^\S+px\s+\S+px\s+\S+px\s+\S+px\s+\S+px$/);
await screenshot(page);
});
});
+1 -1
View File
@@ -115,7 +115,7 @@ test('PR: Navigate by single commit', async ({page}) => {
const response = await page.goto('/user2/repo1/pulls/3/commits');
expect(response?.status()).toBe(200);
await page.locator('tbody.commit-list td.message a').nth(1).click();
await page.locator('.commit .message-wrapper a').nth(1).click();
await page.waitForURL(/.*\/user2\/repo1\/pulls\/3\/commits\/4a357436d925b5c974181ff12a994538ddc5a269/);
await screenshot(page);
+1 -1
View File
@@ -724,7 +724,7 @@ func doAutoPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) {
doc := NewHTMLParser(t, resp.Body)
// Get first commit URL
commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Last().Attr("href")
commitURL, exists := doc.doc.Find(".commits .commit .sha.label").Last().Attr("href")
assert.True(t, exists)
assert.NotEmpty(t, commitURL)
+4 -4
View File
@@ -53,11 +53,11 @@ func TestPullCommitLinks(t *testing.T) {
htmlDoc := NewHTMLParser(t, resp.Body)
commitSha := htmlDoc.Find(".commit-list td.sha a.sha.label").First()
commitSha := htmlDoc.Find(".commits .commit a").First()
commitShaHref, _ := commitSha.Attr("href")
assert.Equal(t, "/user2/repo1/pulls/3/commits/5f22f7d0d95d614d25a5b68592adb345a4b5c7fd", commitShaHref)
commitLink := htmlDoc.Find(".commit-list td.message a").First()
commitLink := htmlDoc.Find(".commit .message-wrapper a").First()
commitLinkHref, _ := commitLink.Attr("href")
assert.Equal(t, "/user2/repo1/pulls/3/commits/5f22f7d0d95d614d25a5b68592adb345a4b5c7fd", commitLinkHref)
}
@@ -75,12 +75,12 @@ func TestPullCommitLinksSHA256(t *testing.T) {
htmlDoc := NewHTMLParser(t, resp.Body)
commitSha := htmlDoc.Find(".commit-list td.sha a.sha.label").First()
commitSha := htmlDoc.Find(".commits .commit a").First()
commitShaHref, commitShaOk := commitSha.Attr("href")
assert.True(t, commitShaOk)
assert.Equal(t, "/user2/repo256/pulls/1/commits/004581b3bb63754502364664021404490ee747ce58e98d27c046f2e46f5f2f55", commitShaHref)
commitLink := htmlDoc.Find(".commit-list td.message a").First()
commitLink := htmlDoc.Find(".commit .message-wrapper a").First()
commitLinkHref, commitLinkOk := commitLink.Attr("href")
assert.True(t, commitLinkOk)
assert.Equal(t, "/user2/repo256/pulls/1/commits/004581b3bb63754502364664021404490ee747ce58e98d27c046f2e46f5f2f55", commitLinkHref)
+3 -3
View File
@@ -44,7 +44,7 @@ func TestPullCreate_CommitStatus(t *testing.T) {
doc := NewHTMLParser(t, resp.Body)
// Get first commit URL
commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Last().Attr("href")
commitURL, exists := doc.doc.Find(".commits .commit .sha.label").Last().Attr("href")
assert.True(t, exists)
assert.NotEmpty(t, commitURL)
@@ -84,12 +84,12 @@ func TestPullCreate_CommitStatus(t *testing.T) {
resp = session.MakeRequest(t, req, http.StatusOK)
doc = NewHTMLParser(t, resp.Body)
commitURL, exists = doc.doc.Find("#commits-table tbody tr td.sha a").Last().Attr("href")
commitURL, exists = doc.doc.Find(".commits .commit .sha.label").Last().Attr("href")
assert.True(t, exists)
assert.NotEmpty(t, commitURL)
assert.Equal(t, commitID, path.Base(commitURL))
cls, ok := doc.doc.Find("#commits-table tbody tr td.message .commit-status").Last().Attr("class")
cls, ok := doc.doc.Find(".commits .commit .message-wrapper .commit-status").Last().Attr("class")
assert.True(t, ok)
assert.Contains(t, cls, statesIcons[status])
}
+1
View File
@@ -63,6 +63,7 @@
@import "./form.css";
@import "./repo.css";
@import "./repo/commit-list.css";
@import "./repo/file-view.css";
@import "./repo/release-tag.css";
@import "./repo/issue-card.css";
+2 -3
View File
@@ -14,7 +14,6 @@
font-family: var(--fonts-monospace);
font-size: 13px;
font-weight: var(--font-weight-normal);
line-height: 1;
}
.primary.sha.label {
@@ -24,13 +23,13 @@
}
.sha.label .shortsha {
padding: 0.33rem 0.5rem;
padding: 0.1rem 0.5rem;
}
.sha.label .signature {
color: var(--color-text);
background: var(--color-light);
padding: 0.25rem 0.33rem;
padding: 0.15rem 0.33rem;
border-inline-start: 1px solid var(--color-light-border);
border-start-end-radius: inherit;
border-end-end-radius: inherit;
+104
View File
@@ -0,0 +1,104 @@
/* Copyright 2025 The Forgejo Authors. All rights reserved.
* SPDX-License-Identifier: GPL-3.0-or-later */
/* Modern layout for templates/repo/pulls/commits_list.tmpl using responsive grid */
.commit-group {
margin-bottom: 1rem;
h4 {
font-weight: var(--font-weight-semibold);
}
.commits {
background: var(--color-box-body);
border: 1px solid var(--color-secondary);
border-radius: var(--border-radius-medium);
}
.commits .commit {
display: grid;
padding: 0.5rem;
gap: 0.5rem 1rem;
grid-template-columns: minmax(8ch, 15%) 1fr auto minmax(12ch, 10%) auto;
align-items: center;
.date {
max-width: 16ch;
white-space: nowrap;
justify-self: end;
overflow: hidden;
text-overflow: ellipsis;
}
}
.commits .commit .author {
display: flex;
column-gap: 0.5rem;
}
.commits .commit .author .author-name {
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
align-self: center;
white-space: nowrap;
}
.commits .commit .sha.label {
/* Unwanted property of .sha.label, needs fix upstream in hashbox.css */
margin-inline: 0 !important;
}
}
@media (max-width: 440px) {
.commit-group .commits .commit .sha.label .signature {
/* Hide signature badge (lock icon) on narrow layout */
display: none;
}
}
@media (min-width: 640px) {
.commit-group .commits .commit {
details.dropdown,
.button-sequence button[data-clipboard-text] {
/* Hide additional copy button and dropdown opener that are exclusive to mobile layout */
display: none;
}
.button-sequence {
display: contents;
}
}
}
@media (max-width: 639.9px) {
.commit-group .commits .commit {
grid-template-columns: 1fr 1fr;
.author {
grid-row: 1;
grid-column: 1;
}
.date {
grid-row: 1;
grid-column: 2;
}
.message {
grid-row: 2;
grid-column: 1 / span 2;
}
.actions {
grid-column: 1 / -1;
display: flex;
justify-content: space-between;
}
.commit-buttons {
display: none;
}
}
}