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") + } +}