@@ -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