diff --git a/models/forgejo_migrations/migrate.go b/models/forgejo_migrations/migrate.go index c5553388ea..71fcf16e7a 100644 --- a/models/forgejo_migrations/migrate.go +++ b/models/forgejo_migrations/migrate.go @@ -117,6 +117,8 @@ var migrations = []*Migration{ NewMigration("Add `resolved_unix` column to `abuse_report` table", AddResolvedUnixToAbuseReport), // v38 -> v39 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. diff --git a/models/forgejo_migrations/v40.go b/models/forgejo_migrations/v40.go new file mode 100644 index 0000000000..11e8fbd85e --- /dev/null +++ b/models/forgejo_migrations/v40.go @@ -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)) +} diff --git a/models/repo/release.go b/models/repo/release.go index 10e9bb259f..b39a1de971 100644 --- a/models/repo/release.go +++ b/models/repo/release.go @@ -77,7 +77,7 @@ type Release struct { Target string TargetBehind string `xorm:"-"` // to handle non-existing or empty target Title string - Sha1 string `xorm:"VARCHAR(64)"` + Sha1 string `xorm:"INDEX VARCHAR(64)"` HideArchiveLinks bool `xorm:"NOT NULL DEFAULT false"` NumCommits int64 NumCommitsBehind int64 `xorm:"-"` @@ -618,3 +618,17 @@ func InsertReleases(ctx context.Context, rels ...*Release) error { 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 +} diff --git a/models/repo/release_test.go b/models/repo/release_test.go index 94dbd6d9d5..69f9333589 100644 --- a/models/repo/release_test.go +++ b/models/repo/release_test.go @@ -49,3 +49,16 @@ func TestReleaseDisplayName(t *testing.T) { release.Title = "Title" 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) +} diff --git a/options/locale_next/locale_en-US.json b/options/locale_next/locale_en-US.json index 8542114100..e730c84726 100644 --- a/options/locale_next/locale_en-US.json +++ b/options/locale_next/locale_en-US.json @@ -111,6 +111,7 @@ "settings.visibility.description": "Profile visibility affects others' ability to access your non-private repositories. Learn more.", "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", + "repo.commit.load_tags_failed": "Load tags failed because of internal error", "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." } diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index f3192266ad..408a2844de 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -85,6 +85,17 @@ func Commits(ctx *context.Context) { } 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["Reponame"] = ctx.Repo.Repository.Name ctx.Data["CommitCount"] = commitsCount diff --git a/templates/repo/commits_list.tmpl b/templates/repo/commits_list.tmpl index 69837bfc1a..2c0a93f2ba 100644 --- a/templates/repo/commits_list.tmpl +++ b/templates/repo/commits_list.tmpl @@ -59,6 +59,14 @@ {{end}} {{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}}
{{RenderCommitBody $.Context .Message ($.Repository.ComposeMetas ctx)}}
{{end}}
diff --git a/templates/repo/graph/commits.tmpl b/templates/repo/graph/commits.tmpl
index 51392e3470..0c4cb49e9d 100644
--- a/templates/repo/graph/commits.tmpl
+++ b/templates/repo/graph/commits.tmpl
@@ -27,9 +27,7 @@
{{end}}
{{else if eq $refGroup "tags"}}
-
- {{svg "octicon-tag"}} {{.ShortName}}
-
+ {{- template "repo/tag/name" dict "RepoLink" $.Repository.Link "TagName" .ShortName -}}
{{else if eq $refGroup "remotes"}}
{{svg "octicon-cross-reference"}} {{.ShortName}}
diff --git a/templates/repo/tag/name.tmpl b/templates/repo/tag/name.tmpl
new file mode 100644
index 0000000000..a0445545e6
--- /dev/null
+++ b/templates/repo/tag/name.tmpl
@@ -0,0 +1,3 @@
+
+{{svg "octicon-tag"}} {{.TagName}}
+
diff --git a/tests/integration/repo_commits_tags_test.go b/tests/integration/repo_commits_tags_test.go
new file mode 100644
index 0000000000..a6797fce94
--- /dev/null
+++ b/tests/integration/repo_commits_tags_test.go
@@ -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")
+ }
+}
diff --git a/tests/integration/repo_commits_template_test.go b/tests/integration/repo_commits_template_test.go
new file mode 100644
index 0000000000..8243ecc62a
--- /dev/null
+++ b/tests/integration/repo_commits_template_test.go
@@ -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")
+ }
+}