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:
parent
fbaaac9c14
commit
623bb81bb9
@ -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()
|
||||
}
|
||||
|
||||
@ -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",
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user