feat: add tag label to commit list view (#8759)

Mainly a port of https://github.com/go-gitea/gitea/pull/31082.

closes #3573

## Screenshots

![image](/attachments/a1ba8729-e7f3-4d3d-ab20-04593b119c3f)

![image](/attachments/13790703-8a71-41a0-a875-9c087da989f8)

---

## Checklist

### Tests

- I added test coverage for Go changes...
  - [x] in their respective `*_test.go` for unit tests.
  - [ ] in the `tests/integration` directory if it involves interactions with a live Forgejo server.
- I added test coverage for JavaScript changes...
  - [ ] in `web_src/js/*.test.js` if it can be unit tested.
  - [ ] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)).

### Documentation

- [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change.
- [x] I did not document these changes and I do not expect someone else to do it.

### Release notes

- [ ] I do not want this change to show in the release notes.
- [x] I want the title to show in the release notes with a link to this pull request.
- [ ] I want the content of the `release-notes/<pull request number>.md` to be be used for the release notes instead of the title.

<!--start release-notes-assistant-->

## Release notes
<!--URL:https://codeberg.org/forgejo/forgejo-->
- User Interface features
  - [PR](https://codeberg.org/forgejo/forgejo/pulls/8759): <!--number 8759 --><!--line 0 --><!--description YWRkIHRhZyBsYWJlbCB0byBjb21taXQgbGlzdCB2aWV3-->add tag label to commit list view<!--description-->
<!--end release-notes-assistant-->

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/8759
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: pat-s <patrick.schratz@gmail.com>
Co-committed-by: pat-s <patrick.schratz@gmail.com>
This commit is contained in:
pat-s 2025-08-06 14:47:51 +02:00 committed by Earl Warren
commit b6046c17a1
11 changed files with 201 additions and 4 deletions

View file

@ -117,6 +117,8 @@ var migrations = []*Migration{
NewMigration("Add `resolved_unix` column to `abuse_report` table", AddResolvedUnixToAbuseReport), NewMigration("Add `resolved_unix` column to `abuse_report` table", AddResolvedUnixToAbuseReport),
// v38 -> v39 // v38 -> v39
NewMigration("Migrate `data` column of `secret` table to store keying material", MigrateActionSecretsToKeying), NewMigration("Migrate `data` column of `secret` table to store keying material", MigrateActionSecretsToKeying),
// v39 -> v40
NewMigration("Add index for release sha1", AddIndexForReleaseSha1),
} }
// GetCurrentDBVersion returns the current Forgejo database version. // GetCurrentDBVersion returns the current Forgejo database version.

View file

@ -0,0 +1,13 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package forgejo_migrations
import "xorm.io/xorm"
func AddIndexForReleaseSha1(x *xorm.Engine) error {
type Release struct {
Sha1 string `xorm:"INDEX VARCHAR(64)"`
}
return x.Sync(new(Release))
}

View file

@ -77,7 +77,7 @@ type Release struct {
Target string Target string
TargetBehind string `xorm:"-"` // to handle non-existing or empty target TargetBehind string `xorm:"-"` // to handle non-existing or empty target
Title string Title string
Sha1 string `xorm:"VARCHAR(64)"` Sha1 string `xorm:"INDEX VARCHAR(64)"`
HideArchiveLinks bool `xorm:"NOT NULL DEFAULT false"` HideArchiveLinks bool `xorm:"NOT NULL DEFAULT false"`
NumCommits int64 NumCommits int64
NumCommitsBehind int64 `xorm:"-"` NumCommitsBehind int64 `xorm:"-"`
@ -618,3 +618,17 @@ func InsertReleases(ctx context.Context, rels ...*Release) error {
return committer.Commit() return committer.Commit()
} }
func FindTagsByCommitIDs(ctx context.Context, repoID int64, commitIDs ...string) (map[string][]*Release, error) {
releases := make([]*Release, 0, len(commitIDs))
if err := db.GetEngine(ctx).Where("repo_id=?", repoID).
In("sha1", commitIDs).
Find(&releases); err != nil {
return nil, err
}
res := make(map[string][]*Release, len(releases))
for _, r := range releases {
res[r.Sha1] = append(res[r.Sha1], r)
}
return res, nil
}

View file

@ -49,3 +49,16 @@ func TestReleaseDisplayName(t *testing.T) {
release.Title = "Title" release.Title = "Title"
assert.Equal(t, "Title", release.DisplayName()) assert.Equal(t, "Title", release.DisplayName())
} }
func Test_FindTagsByCommitIDs(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
sha1Rels, err := FindTagsByCommitIDs(db.DefaultContext, 1, "65f1bf27bc3bf70f64657658635e66094edbcb4d")
require.NoError(t, err)
assert.Len(t, sha1Rels, 1)
rels := sha1Rels["65f1bf27bc3bf70f64657658635e66094edbcb4d"]
assert.Len(t, rels, 3)
assert.Equal(t, "v1.1", rels[0].TagName)
assert.Equal(t, "delete-tag", rels[1].TagName)
assert.Equal(t, "v1.0", rels[2].TagName)
}

View file

@ -111,6 +111,7 @@
"settings.visibility.description": "Profile visibility affects others' ability to access your non-private repositories. <a href=\"%s\" target=\"_blank\">Learn more</a>.", "settings.visibility.description": "Profile visibility affects others' ability to access your non-private repositories. <a href=\"%s\" target=\"_blank\">Learn more</a>.",
"avatar.constraints_hint": "Custom avatar may not exceed %[1]s in size or be larger than %[2]dx%[3]d pixels", "avatar.constraints_hint": "Custom avatar may not exceed %[1]s in size or be larger than %[2]dx%[3]d pixels",
"og.repo.summary_card.alt_description": "Summary card of repository %[1]s, described as: %[2]s", "og.repo.summary_card.alt_description": "Summary card of repository %[1]s, described as: %[2]s",
"repo.commit.load_tags_failed": "Load tags failed because of internal error",
"compare.branches.title": "Compare branches", "compare.branches.title": "Compare branches",
"meta.last_line": "Thank you for translating Forgejo! This line isn't seen by the users but it serves other purposes in the translation management. You can place a fun fact in the translation instead of translating it." "meta.last_line": "Thank you for translating Forgejo! This line isn't seen by the users but it serves other purposes in the translation management. You can place a fun fact in the translation instead of translating it."
} }

View file

@ -85,6 +85,17 @@ func Commits(ctx *context.Context) {
} }
ctx.Data["Commits"] = git_model.ParseCommitsWithStatus(ctx, commits, ctx.Repo.Repository) ctx.Data["Commits"] = git_model.ParseCommitsWithStatus(ctx, commits, ctx.Repo.Repository)
commitIDs := make([]string, 0, len(commits))
for _, c := range commits {
commitIDs = append(commitIDs, c.ID.String())
}
commitTagsMap, err := repo_model.FindTagsByCommitIDs(ctx, ctx.Repo.Repository.ID, commitIDs...)
if err != nil {
log.Error("FindTagsByCommitIDs: %v", err)
ctx.Flash.Error(ctx.Tr("repo.commit.load_tags_failed"))
} else {
ctx.Data["CommitTagsMap"] = commitTagsMap
}
ctx.Data["Username"] = ctx.Repo.Owner.Name ctx.Data["Username"] = ctx.Repo.Owner.Name
ctx.Data["Reponame"] = ctx.Repo.Repository.Name ctx.Data["Reponame"] = ctx.Repo.Repository.Name
ctx.Data["CommitCount"] = commitsCount ctx.Data["CommitCount"] = commitsCount

View file

@ -59,6 +59,14 @@
<button class="ui button js-toggle-commit-body ellipsis-button" aria-expanded="false">...</button> <button class="ui button js-toggle-commit-body ellipsis-button" aria-expanded="false">...</button>
{{end}} {{end}}
{{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}} {{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}}
{{if $.CommitTagsMap}}
{{$tags := index $.CommitTagsMap .ID.String}}
{{if $tags}}
{{range $tags}}
{{- template "repo/tag/name" dict "RepoLink" $.Repository.Link "TagName" .TagName "IsRelease" (not .IsTag) -}}
{{end}}
{{end}}
{{end}}
{{if IsMultilineCommitMessage .Message}} {{if IsMultilineCommitMessage .Message}}
<pre class="commit-body tw-hidden">{{RenderCommitBody $.Context .Message ($.Repository.ComposeMetas ctx)}}</pre> <pre class="commit-body tw-hidden">{{RenderCommitBody $.Context .Message ($.Repository.ComposeMetas ctx)}}</pre>
{{end}} {{end}}

View file

@ -27,9 +27,7 @@
</a> </a>
{{end}} {{end}}
{{else if eq $refGroup "tags"}} {{else if eq $refGroup "tags"}}
<a class="ui basic button" href="{{$.RepoLink}}/src/tag/{{.ShortName|PathEscape}}"> {{- template "repo/tag/name" dict "RepoLink" $.Repository.Link "TagName" .ShortName -}}
{{svg "octicon-tag"}} {{.ShortName}}
</a>
{{else if eq $refGroup "remotes"}} {{else if eq $refGroup "remotes"}}
<a class="ui basic button" href="{{$.RepoLink}}/src/commit/{{$commit.Rev|PathEscape}}"> <a class="ui basic button" href="{{$.RepoLink}}/src/commit/{{$commit.Rev|PathEscape}}">
{{svg "octicon-cross-reference"}} {{.ShortName}} {{svg "octicon-cross-reference"}} {{.ShortName}}

View file

@ -0,0 +1,3 @@
<a class="ui label basic tw-p-1 primary{{if .IsRelease}} primary{{end}}" href="{{.RepoLink}}/src/tag/{{.TagName|PathEscape}}">
{{svg "octicon-tag"}} {{.TagName}}
</a>

View file

@ -0,0 +1,73 @@
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"net/http"
"strings"
"testing"
"forgejo.org/tests"
"github.com/PuerkitoBio/goquery"
"github.com/stretchr/testify/assert"
)
// TestRepoCommitsWithTags tests that tags are displayed inline with commit messages
// in the commits list, and not in a separate column
func TestRepoCommitsWithTags(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2")
req := NewRequest(t, "GET", "/user2/repo1/commits/branch/master")
resp := session.MakeRequest(t, req, http.StatusOK)
doc := NewHTMLParser(t, resp.Body)
// Find the commit with SHA 65f1bf27bc3bf70f64657658635e66094edbcb4d
// This commit should have tags v1.1
commitRow := doc.doc.Find(`#commits-table tbody tr`).FilterFunction(func(i int, s *goquery.Selection) bool {
shaLink := s.Find("td.sha a")
href, _ := shaLink.Attr("href")
return strings.Contains(href, "65f1bf27bc3bf70f64657658635e66094edbcb4d")
})
// 1. Check for tag labels within the message cell
messageCell := commitRow.Find("td.message")
tagLabels := messageCell.Find("a.ui.label.basic")
assert.GreaterOrEqual(t, tagLabels.Length(), 1, "Should find tag label")
// 2. tag has proper HTML attr and links to the correct tag
tagFound := false
tagLabels.Each(func(i int, s *goquery.Selection) {
if strings.Contains(s.Text(), "v1.1") {
tagFound = true
href, exists := s.Attr("href")
assert.True(t, exists, "Tag should have href")
assert.Contains(t, href, "/src/tag/v1.1", "Tag link should point to tag page")
assert.Equal(t, 1, s.Find("svg.octicon-tag").Length(), "Tag should have octicon-tag icon")
}
})
assert.True(t, tagFound, "Should find v1.1 tag")
// 3. tags appear after the commit messsage and status indicators
messageHTML, _ := messageCell.Html()
messageWrapperPos := strings.Index(messageHTML, "message-wrapper")
ellipsisButtonPos := strings.Index(messageHTML, "ellipsis-button")
commitStatusPos := strings.Index(messageHTML, "commit-status")
tagLabelPos := strings.Index(messageHTML, "ui label basic")
// 4. Tags should appear after the message wrapper
assert.Greater(t, tagLabelPos, messageWrapperPos, "Tags should appear after message wrapper")
// 5. If ellipsis button exists, tags should appear after that one
if ellipsisButtonPos > 0 {
assert.Greater(t, tagLabelPos, ellipsisButtonPos, "Tags should appear after ellipsis button")
}
// 6. If commit status exists, tags should appear after that one
if commitStatusPos > 0 {
assert.Greater(t, tagLabelPos, commitStatusPos, "Tags should appear after commit status")
}
}

View file

@ -0,0 +1,61 @@
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"net/http"
"strings"
"testing"
"forgejo.org/tests"
"github.com/stretchr/testify/assert"
)
// TestRepoCommitsTemplateVariables ensures that template variables in commits_list.tmpl are correctly referenced
func TestRepoCommitsTemplateVariables(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2")
// Test the main commits page
req := NewRequest(t, "GET", "/user2/repo1/commits/branch/master")
resp := session.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, http.StatusOK, resp.Code, "Template should render without errors")
doc := NewHTMLParser(t, resp.Body)
// 1. Repository.Link is used in tag template
tagLinks := doc.doc.Find("a.ui.label.basic[href*='/src/tag/']")
if tagLinks.Length() > 0 {
href, _ := tagLinks.First().Attr("href")
assert.Contains(t, href, "/user2/repo1/src/tag/", "Repository link should be correctly rendered in tag URLs")
}
// 2. Repository.ObjectFormatName is used in the SHA column header
shaHeader := doc.doc.Find("#commits-table thead tr th.sha")
assert.Equal(t, 1, shaHeader.Length(), "SHA column header should exist")
headerText := strings.TrimSpace(shaHeader.Text())
assert.NotEmpty(t, headerText, "SHA column header should have text (ObjectFormatName)")
// Should be uppercase SHA1 or SHA256 depending on the repository format
assert.True(t, headerText == "SHA1" || headerText == "SHA256", "ObjectFormatName should be rendered correctly, got: %s", headerText)
// 3. Repository.ComposeMetas is used for rendering commit messages
commitMessages := doc.doc.Find("#commits-table tbody tr td.message .commit-summary")
assert.Positive(t, commitMessages.Length(), "Should have commit messages rendered")
// 4. RepoLink variable is used throughout
commitLinks := doc.doc.Find("#commits-table tbody tr td.sha a[href*='/commit/']")
assert.Positive(t, commitLinks.Length(), "Should have commit links")
firstCommitLink, _ := commitLinks.First().Attr("href")
assert.Contains(t, firstCommitLink, "/user2/repo1/commit/", "RepoLink should be correctly used in commit URLs")
// 5. CommitTagsMap is used for tag rendering
// If $.CommitTagsMap is mistyped, the template would fail with a 500 error
// (for detailed tag rendering tests see repo_commits_tags_test.go)
tagLabels := doc.doc.Find("#commits-table tbody tr td.message a.ui.label.basic")
if tagLabels.Length() > 0 {
assert.NotContains(t, tagLabels.First().Text(), "{{", "Tags should be properly rendered without template syntax")
}
}