0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-06-05 00:46:47 +02:00

fix(releases): generate notes for initial tag (#37697)

Fixes https://github.com/go-gitea/gitea/issues/37286

Automatic release notes for the first release in a repository were empty
when there was no previous tag.

Before this change, the release notes generator used the tag name to
build the changelog link, but reused that state for pull request
collection. When `PreviousTag` was empty, the PR collection logic did
not scan a useful commit range, so merged pull requests were omitted
from the generated notes.

This pull request fixes that by decoupling the internal PR collection
range from the rendered changelog link:
- when a previous tag exists, behavior stays unchanged
- when no previous tag exists, release notes collect merged pull
requests from the full reachable history up to the target tag
- the displayed full changelog link for the first release still uses the
existing `/commits/tag/{tag}` format

Tests were updated to cover:
- generating notes for a repository with no previous tags
- including merged pull requests before the first tag
- preserving existing behavior when a previous tag exists
This commit is contained in:
Dawid Góra 2026-06-03 18:30:30 +02:00 committed by GitHub
parent fbaaac9c14
commit 623bb81bb9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 84 additions and 21 deletions

View File

@ -10,6 +10,7 @@ import (
"slices"
"strings"
"gitea.dev/models/db"
issues_model "gitea.dev/models/issues"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
@ -32,18 +33,23 @@ func GenerateReleaseNotes(ctx context.Context, repo *repo_model.Repository, gitR
return "", err
}
if opts.PreviousTag == "" {
// no previous tag, usually due to there is no tag in the repo, use the same content as GitHub
content := fmt.Sprintf("**Full Changelog**: %s/commits/tag/%s\n", repo.HTMLURL(ctx), util.PathEscapeSegments(opts.TagName))
return content, nil
isFirstRelease, err := isFirstRelease(ctx, repo.ID)
if err != nil {
return "", fmt.Errorf("isFirstRelease: %w", err)
}
baseCommit, err := gitRepo.GetCommit(opts.PreviousTag)
if err != nil {
baseCommitID := ""
if opts.PreviousTag != "" {
baseCommit, err := gitRepo.GetCommit(opts.PreviousTag)
if err != nil {
return "", util.ErrorWrapTranslatable(util.ErrNotExist, "repo.release.generate_notes_tag_not_found", opts.TagName)
}
baseCommitID = baseCommit.ID.String()
} else if !isFirstRelease {
return "", util.ErrorWrapTranslatable(util.ErrNotExist, "repo.release.generate_notes_tag_not_found", opts.TagName)
}
commits, err := gitRepo.CommitsBetweenIDs(headCommit.ID.String(), baseCommit.ID.String())
commits, err := gitRepo.CommitsBetweenIDs(headCommit.ID.String(), baseCommitID)
if err != nil {
return "", fmt.Errorf("CommitsBetweenIDs: %w", err)
}
@ -58,10 +64,27 @@ func GenerateReleaseNotes(ctx context.Context, repo *repo_model.Repository, gitR
return "", err
}
content := buildReleaseNotesContent(ctx, repo, opts.TagName, opts.PreviousTag, prs, contributors, newContributors)
fullChangelogURL := ""
if isFirstRelease {
// Keep the first-release changelog link aligned with GitHub, while collecting PRs from full history.
fullChangelogURL = fmt.Sprintf("%s/commits/tag/%s", repo.HTMLURL(ctx), util.PathEscapeSegments(opts.TagName))
}
content := buildReleaseNotesContent(ctx, repo, opts.TagName, opts.PreviousTag, prs, contributors, newContributors, fullChangelogURL)
return content, nil
}
func isFirstRelease(ctx context.Context, repoID int64) (bool, error) {
count, err := db.Count[repo_model.Release](ctx, repo_model.FindReleasesOptions{
RepoID: repoID,
IncludeDrafts: false,
})
if err != nil {
return false, err
}
return count == 0, nil
}
func resolveHeadCommit(gitRepo *git.Repository, tagName, tagTarget string) (*git.Commit, error) {
ref := tagName
if !gitRepo.IsTagExist(tagName) {
@ -107,7 +130,7 @@ func collectPullRequestsFromCommits(ctx context.Context, repoID int64, commits [
return prs, nil
}
func buildReleaseNotesContent(ctx context.Context, repo *repo_model.Repository, tagName, baseRef string, prs []*issues_model.PullRequest, contributors []*user_model.User, newContributors []*issues_model.PullRequest) string {
func buildReleaseNotesContent(ctx context.Context, repo *repo_model.Repository, tagName, baseRef string, prs []*issues_model.PullRequest, contributors []*user_model.User, newContributors []*issues_model.PullRequest, fullChangelogURL string) string {
var builder strings.Builder
builder.WriteString("## What's Changed\n")
@ -136,8 +159,12 @@ func buildReleaseNotesContent(ctx context.Context, repo *repo_model.Repository,
}
builder.WriteString("**Full Changelog**: ")
compareURL := fmt.Sprintf("%s/compare/%s...%s", repo.HTMLURL(ctx), util.PathEscapeSegments(baseRef), util.PathEscapeSegments(tagName))
fmt.Fprintf(&builder, "[%s...%s](%s)", baseRef, tagName, compareURL)
if fullChangelogURL != "" {
builder.WriteString(fullChangelogURL)
} else {
compareURL := fmt.Sprintf("%s/compare/%s...%s", repo.HTMLURL(ctx), util.PathEscapeSegments(baseRef), util.PathEscapeSegments(tagName))
fmt.Fprintf(&builder, "[%s...%s](%s)", baseRef, tagName, compareURL)
}
builder.WriteByte('\n')
return builder.String()
}

View File

@ -21,13 +21,14 @@ import (
func TestGenerateReleaseNotes(t *testing.T) {
unittest.PrepareTestEnv(t)
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
gitRepo, err := gitrepo.OpenRepository(t.Context(), repo)
require.NoError(t, err)
t.Run("ChangeLogsWithPRs", func(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
gitRepo, err := gitrepo.OpenRepository(t.Context(), repo)
require.NoError(t, err)
t.Cleanup(func() { gitRepo.Close() })
mergedCommit := "90c1019714259b24fb81711d4416ac0f18667dfa"
createMergedPullRequest(t, repo, mergedCommit, 5)
createMergedPullRequest(t, repo, mergedCommit, 5, "Release notes test pull request")
content, err := GenerateReleaseNotes(t.Context(), repo, gitRepo, GenerateReleaseNotesOptions{
TagName: "v1.2.0",
@ -50,16 +51,51 @@ func TestGenerateReleaseNotes(t *testing.T) {
})
t.Run("NoPreviousTag", func(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 16})
gitRepo, err := gitrepo.OpenRepository(t.Context(), repo)
require.NoError(t, err)
t.Cleanup(func() { gitRepo.Close() })
createMergedPullRequest(t, repo, "69554a64c1e6030f051e5c3f94bfbd773cd6a324", 5, "Initial tag PR 1")
createMergedPullRequest(t, repo, "27566bd5738fc8b4e3fef3c5e72cce608537bd95", 4, "Initial tag PR 2")
createMergedPullRequest(t, repo, "5099b81332712fe655e34e8dd63574f503f61811", 8, "Initial tag PR 3")
content, err := GenerateReleaseNotes(t.Context(), repo, gitRepo, GenerateReleaseNotesOptions{
TagName: "v1.2.0",
TagTarget: "DefaultBranch",
TagName: "v0.1.0",
TagTarget: repo.DefaultBranch,
})
require.NoError(t, err)
assert.Equal(t, "**Full Changelog**: https://try.gitea.io/user2/repo1/commits/tag/v1.2.0\n", content)
assert.Contains(t, content, "## What's Changed\n")
assert.Contains(t, content, "* Initial tag PR 1 in [#")
assert.Contains(t, content, "* Initial tag PR 2 in [#")
assert.Contains(t, content, "* Initial tag PR 3 in [#")
assert.Contains(t, content, "\n## Contributors\n")
assert.Contains(t, content, "* @user5\n")
assert.Contains(t, content, "* @user4\n")
assert.Contains(t, content, "* @user8\n")
assert.Contains(t, content, "\n## New Contributors\n")
assert.Contains(t, content, "* @user5 made their first contribution in [#")
assert.Contains(t, content, "* @user4 made their first contribution in [#")
assert.Contains(t, content, "* @user8 made their first contribution in [#")
assert.Contains(t, content, "**Full Changelog**: https://try.gitea.io/user2/repo16/commits/tag/v0.1.0\n")
})
t.Run("EmptyPreviousTagWithExistingTags", func(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
gitRepo, err := gitrepo.OpenRepository(t.Context(), repo)
require.NoError(t, err)
t.Cleanup(func() { gitRepo.Close() })
_, err = GenerateReleaseNotes(t.Context(), repo, gitRepo, GenerateReleaseNotesOptions{
TagName: "v1.2.0",
TagTarget: repo.DefaultBranch,
})
require.Error(t, err)
})
}
func createMergedPullRequest(t *testing.T, repo *repo_model.Repository, mergeCommit string, posterID int64) *issues_model.PullRequest {
func createMergedPullRequest(t *testing.T, repo *repo_model.Repository, mergeCommit string, posterID int64, title string) *issues_model.PullRequest {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: posterID})
issue := &issues_model.Issue{
@ -67,7 +103,7 @@ func createMergedPullRequest(t *testing.T, repo *repo_model.Repository, mergeCom
Repo: repo,
Poster: user,
PosterID: user.ID,
Title: "Release notes test pull request",
Title: title,
Content: "content",
}