-
-
Notifications
You must be signed in to change notification settings - Fork 6.3k
feat: automatic generation of release notes #35977
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
dawidgora
wants to merge
30
commits into
go-gitea:main
Choose a base branch
from
dawidgora:feat/automatic-generation-of-release-notes
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
30 commits
Select commit
Hold shift + click to select a range
4fce573
feat: automatic generation of release notes
dawidgora 9c9f348
improve links generation
dawidgora 8da1948
apply CR suggestions from @silverwind
dawidgora 61afa90
update previous adjustment
dawidgora 5c99e40
apply @wxiaoguang suggestion
dawidgora 9fbb2b9
remove unused method
dawidgora 6b93144
skip contributors with posterID = 0
dawidgora 9cee5e1
adjustments for CR
dawidgora bf46d53
improvement: detect previous published release for notes base
dawidgora 9604e6b
make new logic less defensive
dawidgora 1d5e8ea
load shared data on edit (without it, tags list was empty on release …
dawidgora 498646e
cleanup changes
dawidgora 89d93a8
remove unnecessary check
dawidgora ffcc2c4
Update web_src/js/features/repo-release.ts
silverwind 86e0035
revert previous commit
silverwind 6f73691
Merge branch 'main' into feat/automatic-generation-of-release-notes
dawidgora 0cebfd6
Merge branch 'main' into feat/automatic-generation-of-release-notes
dawidgora 0546bf5
proposal to https://github.com/go-gitea/gitea/pull/35977#discussion_r…
dawidgora dbe3419
fix fmt
dawidgora c19ea2b
Merge branch 'main' into feat/automatic-generation-of-release-notes
wxiaoguang cf11756
improve
wxiaoguang d368900
fix dropdown
wxiaoguang 355c649
fix broken UI
wxiaoguang 8a48faf
Merge branch 'main' into feat/automatic-generation-of-release-notes
dawidgora f45bfb6
Merge remote-tracking branch 'origin/main' into feat/automatic-genera…
dawidgora 614cff0
adjustments after merge
dawidgora 7444449
Merge branch 'main' into feat/automatic-generation-of-release-notes
wxiaoguang e8b379a
fix
wxiaoguang ab82186
Merge branch 'main' into feat-automatic-generation-of-release-notes
wxiaoguang 47d9c5e
fix
wxiaoguang File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,188 @@ | ||
| // Copyright 2025 The Gitea Authors. All rights reserved. | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| package release | ||
|
|
||
| import ( | ||
| "cmp" | ||
| "context" | ||
| "fmt" | ||
| "slices" | ||
| "strings" | ||
|
|
||
| issues_model "code.gitea.io/gitea/models/issues" | ||
| repo_model "code.gitea.io/gitea/models/repo" | ||
| user_model "code.gitea.io/gitea/models/user" | ||
| "code.gitea.io/gitea/modules/container" | ||
| "code.gitea.io/gitea/modules/git" | ||
| "code.gitea.io/gitea/modules/util" | ||
| ) | ||
|
|
||
| // GenerateReleaseNotesOptions describes how to build release notes content. | ||
| type GenerateReleaseNotesOptions struct { | ||
| TagName string | ||
| TagTarget string | ||
| PreviousTag string | ||
| } | ||
|
|
||
| // GenerateReleaseNotes builds the markdown snippet for release notes. | ||
| func GenerateReleaseNotes(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, opts GenerateReleaseNotesOptions) (string, error) { | ||
| headCommit, err := resolveHeadCommit(gitRepo, opts.TagName, opts.TagTarget) | ||
| if err != nil { | ||
| 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 | ||
| } | ||
|
|
||
| baseCommit, err := gitRepo.GetCommit(opts.PreviousTag) | ||
| if err != nil { | ||
| return "", util.ErrorWrapTranslatable(util.ErrNotExist, "repo.release.generate_notes_tag_not_found", opts.TagName) | ||
| } | ||
|
|
||
| commits, err := gitRepo.CommitsBetweenIDs(headCommit.ID.String(), baseCommit.ID.String()) | ||
| if err != nil { | ||
| return "", fmt.Errorf("CommitsBetweenIDs: %w", err) | ||
| } | ||
|
|
||
| prs, err := collectPullRequestsFromCommits(ctx, repo.ID, commits) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| contributors, newContributors, err := collectContributors(ctx, repo.ID, prs) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| content := buildReleaseNotesContent(ctx, repo, opts.TagName, opts.PreviousTag, prs, contributors, newContributors) | ||
| return content, nil | ||
| } | ||
|
|
||
| func resolveHeadCommit(gitRepo *git.Repository, tagName, tagTarget string) (*git.Commit, error) { | ||
| ref := tagName | ||
| if !gitRepo.IsTagExist(tagName) { | ||
| ref = tagTarget | ||
| } | ||
|
|
||
| commit, err := gitRepo.GetCommit(ref) | ||
| if err != nil { | ||
| return nil, util.ErrorWrapTranslatable(util.ErrNotExist, "repo.release.generate_notes_target_not_found", ref) | ||
| } | ||
| return commit, nil | ||
| } | ||
|
|
||
| func collectPullRequestsFromCommits(ctx context.Context, repoID int64, commits []*git.Commit) ([]*issues_model.PullRequest, error) { | ||
| prs := make([]*issues_model.PullRequest, 0, len(commits)) | ||
|
|
||
| for _, commit := range commits { | ||
| pr, err := issues_model.GetPullRequestByMergedCommit(ctx, repoID, commit.ID.String()) | ||
| if err != nil { | ||
| if issues_model.IsErrPullRequestNotExist(err) { | ||
| continue | ||
| } | ||
| return nil, fmt.Errorf("GetPullRequestByMergedCommit: %w", err) | ||
| } | ||
|
|
||
| if err = pr.LoadIssue(ctx); err != nil { | ||
| return nil, fmt.Errorf("LoadIssue: %w", err) | ||
| } | ||
| if err = pr.Issue.LoadAttributes(ctx); err != nil { | ||
| return nil, fmt.Errorf("LoadIssueAttributes: %w", err) | ||
| } | ||
|
|
||
| prs = append(prs, pr) | ||
| } | ||
|
|
||
| slices.SortFunc(prs, func(a, b *issues_model.PullRequest) int { | ||
| if cmpRes := cmp.Compare(b.MergedUnix, a.MergedUnix); cmpRes != 0 { | ||
| return cmpRes | ||
| } | ||
| return cmp.Compare(b.Issue.Index, a.Issue.Index) | ||
| }) | ||
|
|
||
| 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 { | ||
| var builder strings.Builder | ||
| builder.WriteString("## What's Changed\n") | ||
|
|
||
| for _, pr := range prs { | ||
| prURL := pr.Issue.HTMLURL(ctx) | ||
| builder.WriteString(fmt.Sprintf("* %s in [#%d](%s)\n", pr.Issue.Title, pr.Issue.Index, prURL)) | ||
| } | ||
|
|
||
| builder.WriteString("\n") | ||
|
|
||
| if len(contributors) > 0 { | ||
| builder.WriteString("## Contributors\n") | ||
| for _, contributor := range contributors { | ||
| builder.WriteString(fmt.Sprintf("* @%s\n", contributor.Name)) | ||
| } | ||
| builder.WriteString("\n") | ||
| } | ||
|
|
||
| if len(newContributors) > 0 { | ||
| builder.WriteString("## New Contributors\n") | ||
| for _, contributor := range newContributors { | ||
| prURL := contributor.Issue.HTMLURL(ctx) | ||
| builder.WriteString(fmt.Sprintf("* @%s made their first contribution in [#%d](%s)\n", contributor.Issue.Poster.Name, contributor.Issue.Index, prURL)) | ||
| } | ||
| builder.WriteString("\n") | ||
| } | ||
|
|
||
| builder.WriteString("**Full Changelog**: ") | ||
| compareURL := fmt.Sprintf("%s/compare/%s...%s", repo.HTMLURL(ctx), util.PathEscapeSegments(baseRef), util.PathEscapeSegments(tagName)) | ||
| builder.WriteString(fmt.Sprintf("[%s...%s](%s)", baseRef, tagName, compareURL)) | ||
| builder.WriteByte('\n') | ||
| return builder.String() | ||
| } | ||
|
|
||
| func collectContributors(ctx context.Context, repoID int64, prs []*issues_model.PullRequest) ([]*user_model.User, []*issues_model.PullRequest, error) { | ||
| contributors := make([]*user_model.User, 0, len(prs)) | ||
| newContributors := make([]*issues_model.PullRequest, 0, len(prs)) | ||
| seenContributors := container.Set[int64]{} | ||
| seenNew := container.Set[int64]{} | ||
|
|
||
| for _, pr := range prs { | ||
| poster := pr.Issue.Poster | ||
| posterID := poster.ID | ||
|
|
||
| if posterID == 0 { | ||
| // Migrated PRs may not have a linked local user (PosterID == 0). Skip them for now. | ||
| continue | ||
| } | ||
|
|
||
| if !seenContributors.Contains(posterID) { | ||
| contributors = append(contributors, poster) | ||
| seenContributors.Add(posterID) | ||
| } | ||
|
|
||
| if seenNew.Contains(posterID) { | ||
| continue | ||
| } | ||
|
|
||
| isFirst, err := isFirstContribution(ctx, repoID, posterID, pr) | ||
| if err != nil { | ||
| return nil, nil, err | ||
| } | ||
| if isFirst { | ||
| seenNew.Add(posterID) | ||
| newContributors = append(newContributors, pr) | ||
| } | ||
| } | ||
|
|
||
| return contributors, newContributors, nil | ||
| } | ||
|
|
||
| func isFirstContribution(ctx context.Context, repoID, posterID int64, pr *issues_model.PullRequest) (bool, error) { | ||
| hasMergedBefore, err := issues_model.HasMergedPullRequestInRepoBefore(ctx, repoID, posterID, pr.MergedUnix, pr.ID) | ||
| if err != nil { | ||
| return false, fmt.Errorf("check merged PRs for contributor: %w", err) | ||
| } | ||
| return !hasMergedBefore, nil | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.