@@ -176,24 +176,22 @@ type commitSection struct {
176176// formatReleaseNotes generates the body for a release pull request.
177177func 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