Skip to content

Commit 7c52da2

Browse files
authored
feat: format bulk commit from other sources in release notes (#2665)
Fixes #2599
1 parent a636f7c commit 7c52da2

File tree

5 files changed

+474
-32
lines changed

5 files changed

+474
-32
lines changed

internal/config/state.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import (
2626
const (
2727
StatusNew = "new"
2828
StatusExisting = "existing"
29+
// BulkChangeThreshold is a threshold to determine whether a commit is a bulk change.
30+
BulkChangeThreshold = 10
2931
)
3032

3133
// LibrarianState defines the contract for the state.yaml file.
@@ -161,7 +163,7 @@ type Commit struct {
161163
// IsBulkCommit returns true if the commit is associated with 10 or more
162164
// libraries.
163165
func (c *Commit) IsBulkCommit() bool {
164-
return len(strings.Split(c.LibraryIDs, ",")) >= 10
166+
return len(strings.Split(c.LibraryIDs, ",")) >= BulkChangeThreshold
165167
}
166168

167169
var (

internal/librarian/release_init.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ func (r *initRunner) updateLibrary(library *config.LibraryState, commits []*gitr
277277

278278
// Update the previous version, we need this value when creating release note.
279279
library.PreviousVersion = library.Version
280-
library.Changes = toCommit(commits)
280+
library.Changes = toCommit(commits, library.ID)
281281
library.Version = nextVersion
282282
library.ReleaseTriggered = true
283283
return nil
@@ -310,16 +310,24 @@ func (r *initRunner) determineNextVersion(commits []*gitrepo.ConventionalCommit,
310310
// toCommit converts a slice of gitrepo.ConventionalCommit to a slice of config.Commit.
311311
// If the ConventionalCommit has NestedCommits, they are also extracted and
312312
// converted.
313-
func toCommit(c []*gitrepo.ConventionalCommit) []*config.Commit {
313+
// Set LibraryIDs to the given libraryID if the conventional commit doesn't have key `Library-IDs` in the Footers;
314+
// otherwise use the value in the Footers as LibraryIDs.
315+
func toCommit(c []*gitrepo.ConventionalCommit, libraryID string) []*config.Commit {
314316
var commits []*config.Commit
315317
for _, cc := range c {
318+
var libraryIDs string
319+
libraryIDs, ok := cc.Footers["Library-IDs"]
320+
if !ok {
321+
libraryIDs = libraryID
322+
}
323+
316324
commits = append(commits, &config.Commit{
317325
Type: cc.Type,
318326
Subject: cc.Subject,
319327
Body: cc.Body,
320328
CommitHash: cc.CommitHash,
321329
PiperCLNumber: cc.Footers["PiperOrigin-RevId"],
322-
LibraryIDs: cc.Footers["Library-IDs"],
330+
LibraryIDs: libraryIDs,
323331
})
324332
}
325333
return commits

internal/librarian/release_init_test.go

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1246,6 +1246,7 @@ func TestRunInitCommand(t *testing.T) {
12461246
Type: "feat",
12471247
Subject: "bump version",
12481248
CommitHash: "1234560000000000000000000000000000000000",
1249+
LibraryIDs: "another-example-id",
12491250
},
12501251
},
12511252
ReleaseTriggered: true,
@@ -1266,6 +1267,7 @@ func TestRunInitCommand(t *testing.T) {
12661267
Type: "feat",
12671268
Subject: "bump version",
12681269
CommitHash: "1234560000000000000000000000000000000000",
1270+
LibraryIDs: "example-id",
12691271
},
12701272
},
12711273
ReleaseTriggered: true,
@@ -1556,14 +1558,16 @@ func TestUpdateLibrary(t *testing.T) {
15561558
PreviousVersion: "1.2.3",
15571559
Changes: []*config.Commit{
15581560
{
1559-
Type: "fix",
1560-
Subject: "change a typo",
1561+
Type: "fix",
1562+
Subject: "change a typo",
1563+
LibraryIDs: "one-id",
15611564
},
15621565
{
15631566
Type: "feat",
15641567
Subject: "add a config file",
15651568
Body: "This is the body.",
15661569
PiperCLNumber: "12345",
1570+
LibraryIDs: "one-id",
15671571
},
15681572
},
15691573
ReleaseTriggered: true,
@@ -1594,14 +1598,16 @@ func TestUpdateLibrary(t *testing.T) {
15941598
PreviousVersion: "1.2.3",
15951599
Changes: []*config.Commit{
15961600
{
1597-
Type: "fix",
1598-
Subject: "change a typo",
1601+
Type: "fix",
1602+
Subject: "change a typo",
1603+
LibraryIDs: "one-id",
15991604
},
16001605
{
16011606
Type: "feat",
16021607
Subject: "add a config file",
16031608
Body: "This is the body.",
16041609
PiperCLNumber: "12345",
1610+
LibraryIDs: "one-id",
16051611
},
16061612
},
16071613
ReleaseTriggered: true,
@@ -1686,13 +1692,15 @@ func TestUpdateLibrary(t *testing.T) {
16861692
PreviousVersion: "1.2.3",
16871693
Changes: []*config.Commit{
16881694
{
1689-
Type: "feat",
1690-
Subject: "add another config file",
1691-
Body: "This is the body",
1695+
Type: "feat",
1696+
Subject: "add another config file",
1697+
Body: "This is the body",
1698+
LibraryIDs: "one-id",
16921699
},
16931700
{
1694-
Type: "feat",
1695-
Subject: "change a typo",
1701+
Type: "feat",
1702+
Subject: "change a typo",
1703+
LibraryIDs: "one-id",
16961704
},
16971705
},
16981706
ReleaseTriggered: true,
@@ -1746,8 +1754,9 @@ func TestUpdateLibrary(t *testing.T) {
17461754
ReleaseTriggered: true,
17471755
Changes: []*config.Commit{
17481756
{
1749-
Type: "chore",
1750-
Subject: "a chore",
1757+
Type: "chore",
1758+
Subject: "a chore",
1759+
LibraryIDs: "one-id",
17511760
},
17521761
},
17531762
},

internal/librarian/release_notes.go

Lines changed: 90 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -176,24 +176,22 @@ type commitSection struct {
176176
// formatReleaseNotes generates the body for a release pull request.
177177
func formatReleaseNotes(state *config.LibrarianState, ghRepo *github.Repository) (string, error) {
178178
librarianVersion := cli.Version()
179+
// Separate commits to bulk changes (affects multiple libraries) or library-specific changes because they
180+
// appear in different section in the release notes.
181+
bulkChangesMap, libraryChanges := separateCommits(state)
182+
// Process library specific changes.
179183
var releaseSections []*releaseNoteSection
180-
// create a map to deduplicate bulk changes based on their commit hash
181-
// and subject
182-
bulkChangesMap := make(map[string]*config.Commit)
183184
for _, library := range state.Libraries {
184185
if !library.ReleaseTriggered {
185186
continue
186187
}
187-
188-
for _, commit := range library.Changes {
189-
if commit.IsBulkCommit() {
190-
bulkChangesMap[commit.CommitHash+commit.Subject] = commit
191-
}
192-
}
193-
194-
section := formatLibraryReleaseNotes(library)
188+
// No need to check the existence of the key, library.ID, because a library without library-specific changes
189+
// may appear in the release notes, i.e., in the bulk changes section.
190+
commits := libraryChanges[library.ID]
191+
section := formatLibraryReleaseNotes(library, commits)
195192
releaseSections = append(releaseSections, section)
196193
}
194+
// Process bulk changes
197195
var bulkChanges []*config.Commit
198196
for _, commit := range bulkChangesMap {
199197
bulkChanges = append(bulkChanges, commit)
@@ -222,18 +220,19 @@ func formatReleaseNotes(state *config.LibrarianState, ghRepo *github.Repository)
222220

223221
// formatLibraryReleaseNotes generates release notes in Markdown format for a single library.
224222
// It returns the generated release notes and the new version string.
225-
func formatLibraryReleaseNotes(library *config.LibraryState) *releaseNoteSection {
223+
func formatLibraryReleaseNotes(library *config.LibraryState, commits []*config.Commit) *releaseNoteSection {
226224
// The version should already be updated to the next version.
227225
newVersion := library.Version
228226
tagFormat := config.DetermineTagFormat(library.ID, library, nil)
229227
newTag := config.FormatTag(tagFormat, library.ID, newVersion)
230228
previousTag := config.FormatTag(tagFormat, library.ID, library.PreviousVersion)
231229

230+
sort.Slice(commits, func(i, j int) bool {
231+
return commits[i].CommitHash < commits[j].CommitHash
232+
})
232233
commitsByType := make(map[string][]*config.Commit)
233-
for _, commit := range library.Changes {
234-
if !commit.IsBulkCommit() {
235-
commitsByType[commit.Type] = append(commitsByType[commit.Type], commit)
236-
}
234+
for _, commit := range commits {
235+
commitsByType[commit.Type] = append(commitsByType[commit.Type], commit)
237236
}
238237

239238
var sections []*commitSection
@@ -259,3 +258,78 @@ func formatLibraryReleaseNotes(library *config.LibraryState) *releaseNoteSection
259258

260259
return section
261260
}
261+
262+
// separateCommits analyzes all commits associated with triggered releases in the
263+
// given state and categorizes them into two groups:
264+
//
265+
// 1. Bulk Changes: Commits that affect multiple libraries. This includes:
266+
// - Commits identified by IsBulkCommit() (e.g., librarian generation PRs).
267+
// - Commits that appear in multiple libraries' change sets but are not
268+
// marked as bulk commits (e.g., dependency updates, README changes).
269+
// The Library-IDs for these are concatenated.
270+
//
271+
// 2. Library Changes: Commits that are unique to a single library.
272+
//
273+
// It returns two maps:
274+
// - The first map contains bulk changes, keyed by a composite of commit hash and subject.
275+
// - The second map contains library-specific changes, keyed by LibraryID.
276+
func separateCommits(state *config.LibrarianState) (map[string]*config.Commit, map[string][]*config.Commit) {
277+
maybeBulkChanges := make(map[string][]*config.Commit)
278+
for _, library := range state.Libraries {
279+
if !library.ReleaseTriggered {
280+
continue
281+
}
282+
283+
for _, commit := range library.Changes {
284+
key := commit.CommitHash + commit.Subject
285+
maybeBulkChanges[key] = append(maybeBulkChanges[key], commit)
286+
}
287+
}
288+
289+
bulkChanges := make(map[string]*config.Commit)
290+
libraryChanges := make(map[string][]*config.Commit)
291+
for key, commits := range maybeBulkChanges {
292+
// A commit has multiple library IDs in the footer, this should come from librarian generation PR.
293+
// All commits should be identical.
294+
if commits[0].IsBulkCommit() {
295+
bulkChanges[key] = commits[0]
296+
continue
297+
}
298+
// More than ten commits have the same commit subject and sha, this should come from other sources,
299+
// e.g., dependency updates, README updates, etc.
300+
// All commits should be identical except for the library id.
301+
// We assume this type of commits has only one library id in Footers and each id is unique among all
302+
// commits.
303+
if len(commits) >= config.BulkChangeThreshold {
304+
bulkChanges[key] = concatenateLibraryIDs(commits)
305+
continue
306+
}
307+
// We assume the rest of commits are library-specific.
308+
for _, commit := range commits {
309+
// Non-bulk commits may have 1 - 9 library IDs.
310+
libraryIDs := strings.Split(commit.LibraryIDs, ",")
311+
for _, libraryID := range libraryIDs {
312+
if libraryID == "" {
313+
continue
314+
}
315+
libraryChanges[libraryID] = append(libraryChanges[libraryID], commit)
316+
}
317+
}
318+
}
319+
320+
return bulkChanges, libraryChanges
321+
}
322+
323+
// concatenateLibraryIDs merges the LibraryIDs from a slice of commits into the first commit.
324+
func concatenateLibraryIDs(commits []*config.Commit) *config.Commit {
325+
var libraryIDs []string
326+
for _, commit := range commits {
327+
libraryIDs = append(libraryIDs, commit.LibraryIDs)
328+
}
329+
330+
sort.Slice(libraryIDs, func(i, j int) bool {
331+
return libraryIDs[i] < libraryIDs[j]
332+
})
333+
commits[0].LibraryIDs = strings.Join(libraryIDs, ",")
334+
return commits[0]
335+
}

0 commit comments

Comments
 (0)