feat: add tag label to commit list view (#8759)
Mainly a port of https://github.com/go-gitea/gitea/pull/31082. closes #3573 ## Screenshots   --- ## 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:
		
					parent
					
						
							
								5294cff95f
							
						
					
				
			
			
				commit
				
					
						b6046c17a1
					
				
			
		
					 11 changed files with 201 additions and 4 deletions
				
			
		| 
						 | 
				
			
			@ -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.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										13
									
								
								models/forgejo_migrations/v40.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								models/forgejo_migrations/v40.go
									
										
									
									
									
										Normal 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))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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>.",
 | 
			
		||||
    "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."
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -59,6 +59,14 @@
 | 
			
		|||
							<button class="ui button js-toggle-commit-body ellipsis-button" aria-expanded="false">...</button>
 | 
			
		||||
							{{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}}
 | 
			
		||||
							<pre class="commit-body tw-hidden">{{RenderCommitBody $.Context .Message ($.Repository.ComposeMetas ctx)}}</pre>
 | 
			
		||||
							{{end}}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -27,9 +27,7 @@
 | 
			
		|||
									</a>
 | 
			
		||||
								{{end}}
 | 
			
		||||
							{{else if eq $refGroup "tags"}}
 | 
			
		||||
								<a class="ui basic button" href="{{$.RepoLink}}/src/tag/{{.ShortName|PathEscape}}">
 | 
			
		||||
									{{svg "octicon-tag"}} {{.ShortName}}
 | 
			
		||||
								</a>
 | 
			
		||||
								{{- template "repo/tag/name" dict "RepoLink" $.Repository.Link "TagName" .ShortName -}}
 | 
			
		||||
							{{else if eq $refGroup "remotes"}}
 | 
			
		||||
								<a class="ui basic button" href="{{$.RepoLink}}/src/commit/{{$commit.Rev|PathEscape}}">
 | 
			
		||||
									{{svg "octicon-cross-reference"}} {{.ShortName}}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										3
									
								
								templates/repo/tag/name.tmpl
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								templates/repo/tag/name.tmpl
									
										
									
									
									
										Normal 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>
 | 
			
		||||
							
								
								
									
										73
									
								
								tests/integration/repo_commits_tags_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								tests/integration/repo_commits_tags_test.go
									
										
									
									
									
										Normal 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")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										61
									
								
								tests/integration/repo_commits_template_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								tests/integration/repo_commits_template_test.go
									
										
									
									
									
										Normal 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")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue