11import { sha256 } from '@env/crypto' ;
22import type { Container } from '../../../container' ;
3- import type { GitCommit } from '../../../git/models/commit' ;
3+ import type { GitCommit , GitCommitIdentityShape } from '../../../git/models/commit' ;
44import type { GitDiff , ParsedGitDiff } from '../../../git/models/diff' ;
55import type { Repository } from '../../../git/models/repository' ;
66import { uncommitted , uncommittedStaged } from '../../../git/models/revision' ;
@@ -129,18 +129,138 @@ export function createCombinedDiffForCommit(hunks: ComposerHunk[]): {
129129 return { patch : commitPatch , filePatches : filePatches } ;
130130}
131131
132+ // Given a group of hunks assigned to a single commit, each with their own author and co-authors, determine a single author and co-authors list for the commit
133+ // based on the amount of changes made by each author, measured in additions + deletions
134+ function getAuthorAndCoAuthorsForCommit ( commitHunks : ComposerHunk [ ] ) : {
135+ author : GitCommitIdentityShape | undefined ;
136+ coAuthors : GitCommitIdentityShape [ ] ;
137+ } {
138+ // Each hunk may or may not have an author. Determine the primary author based on the hunk with the largest diff, then assign the rest as co-authors.
139+ // If there is a tie for largest diff, use the first one.
140+ const authorContributionWeights = new Map < string , number > ( ) ;
141+ const coAuthors = new Map < string , GitCommitIdentityShape > ( ) ;
142+ for ( const hunk of commitHunks ) {
143+ if ( hunk . author == null ) continue ;
144+ coAuthors . set ( hunk . author . name , hunk . author ) ;
145+ hunk . coAuthors ?. forEach ( coAuthor => coAuthors . set ( coAuthor . name , coAuthor ) ) ;
146+ authorContributionWeights . set (
147+ hunk . author . name ,
148+ ( authorContributionWeights . get ( hunk . author . name ) ?? 0 ) + hunk . additions + hunk . deletions ,
149+ ) ;
150+ }
151+
152+ let primary : GitCommitIdentityShape | undefined ;
153+ let primaryScore = 0 ;
154+ for ( const [ author , score ] of authorContributionWeights . entries ( ) ) {
155+ if ( primary == null || score > primaryScore ) {
156+ primary = coAuthors . get ( author ) ;
157+ primaryScore = score ;
158+ }
159+ }
160+
161+ // Remove the primary author from the co-authors, if present
162+ if ( primary != null ) {
163+ coAuthors . delete ( primary . name ) ;
164+ }
165+
166+ return { author : primary , coAuthors : [ ...coAuthors . values ( ) ] } ;
167+ }
168+
169+ function overlap ( range1 : { start : number ; count : number } , range2 : { start : number ; count : number } ) : number {
170+ const end1 = range1 . start + range1 . count ;
171+ const end2 = range2 . start + range2 . count ;
172+ const overlapStart = Math . max ( range1 . start , range2 . start ) ;
173+ const overlapEnd = Math . min ( end1 , end2 ) ;
174+ return Math . max ( 0 , overlapEnd - overlapStart ) ;
175+ }
176+
177+ // Calculates a similarity score between two hunks that touch the same file, based on the overlap between the lines in their hunk headers
178+ function getHunkSimilarityValue ( hunk1 : ComposerHunk , hunk2 : ComposerHunk ) : number {
179+ const oldRange1 = hunk1 . hunkHeader . match ( / @ @ - ( \d + ) , ( \d + ) / ) ;
180+ const newRange1 = hunk1 . hunkHeader . match ( / @ @ - \d + , \d + \+ ( \d + ) , ( \d + ) / ) ;
181+ const oldRange2 = hunk2 . hunkHeader . match ( / @ @ - ( \d + ) , ( \d + ) / ) ;
182+ const newRange2 = hunk2 . hunkHeader . match ( / @ @ - \d + , \d + \+ ( \d + ) , ( \d + ) / ) ;
183+ if ( oldRange1 == null || newRange1 == null || oldRange2 == null || newRange2 == null ) {
184+ return 0 ;
185+ }
186+ return (
187+ overlap (
188+ { start : parseInt ( oldRange1 [ 1 ] , 10 ) , count : parseInt ( oldRange1 [ 2 ] , 10 ) } ,
189+ { start : parseInt ( oldRange2 [ 1 ] , 10 ) , count : parseInt ( oldRange2 [ 2 ] , 10 ) } ,
190+ ) +
191+ overlap (
192+ { start : parseInt ( newRange1 [ 1 ] , 10 ) , count : parseInt ( newRange1 [ 2 ] , 10 ) } ,
193+ { start : parseInt ( newRange2 [ 1 ] , 10 ) , count : parseInt ( newRange2 [ 2 ] , 10 ) } ,
194+ )
195+ ) ;
196+ }
197+
198+ // Given an array of hunks representing commit history between two commits, and a hunk from their combined diff, determine the author and co-authors of the
199+ // combined diff hunk based on similarity to the commit hunks
200+ export function getAuthorAndCoAuthorsForCombinedDiffHunk (
201+ commitHunks : ComposerHunk [ ] ,
202+ combinedDiffHunk : ComposerHunk ,
203+ ) : { author : GitCommitIdentityShape | undefined ; coAuthors : GitCommitIdentityShape [ ] } {
204+ const matches = commitHunks . filter ( commitHunk => {
205+ return (
206+ commitHunk . author != null &&
207+ commitHunk . fileName === combinedDiffHunk . fileName &&
208+ ( ! combinedDiffHunk . isRename || commitHunk . isRename === combinedDiffHunk . isRename )
209+ ) ;
210+ } ) ;
211+
212+ const similarityByHunkAuthor = new Map < string , number > ( ) ;
213+ const coAuthors = new Map < string , GitCommitIdentityShape > ( ) ;
214+ let maxSimilarity = 0 ;
215+ let primaryAuthor : GitCommitIdentityShape | undefined ;
216+ for ( const commitHunk of matches ) {
217+ coAuthors . set ( commitHunk . author ! . name , commitHunk . author ! ) ;
218+ commitHunk . coAuthors ?. forEach ( coAuthor => coAuthors . set ( coAuthor . name , coAuthor ) ) ;
219+ let similarity = getHunkSimilarityValue ( commitHunk , combinedDiffHunk ) ;
220+ if ( similarityByHunkAuthor . has ( commitHunk . author ! . name ) ) {
221+ similarity += similarityByHunkAuthor . get ( commitHunk . author ! . name ) ! ;
222+ }
223+
224+ similarityByHunkAuthor . set ( commitHunk . author ! . name , similarity ) ;
225+ if ( primaryAuthor == null || similarity > maxSimilarity ) {
226+ maxSimilarity = similarity ;
227+ primaryAuthor = commitHunk . author ;
228+ }
229+ }
230+
231+ // Remove the primary author from the co-authors, if present
232+ if ( primaryAuthor != null ) {
233+ coAuthors . delete ( primaryAuthor . name ) ;
234+ }
235+
236+ return { author : primaryAuthor , coAuthors : [ ...coAuthors . values ( ) ] } ;
237+ }
238+
132239export function convertToComposerDiffInfo (
133240 commits : ComposerCommit [ ] ,
134241 hunks : ComposerHunk [ ] ,
135- ) : Array < { message : string ; explanation ?: string ; filePatches : Map < string , string [ ] > ; patch : string } > {
242+ ) : Array < {
243+ message : string ;
244+ explanation ?: string ;
245+ filePatches : Map < string , string [ ] > ;
246+ patch : string ;
247+ author ?: GitCommitIdentityShape ;
248+ } > {
136249 return commits . map ( commit => {
137250 const { patch, filePatches } = createCombinedDiffForCommit ( getHunksForCommit ( commit , hunks ) ) ;
251+ const commitHunks = getHunksForCommit ( commit , hunks ) ;
252+ const { author, coAuthors } = getAuthorAndCoAuthorsForCommit ( commitHunks ) ;
253+ let message = commit . message ;
254+ if ( coAuthors . length > 0 ) {
255+ message += `\n${ coAuthors . map ( a => `\nCo-authored-by: ${ a . name } <${ a . email } >` ) . join ( ) } ` ;
256+ }
138257
139258 return {
140- message : commit . message ,
259+ message : message ,
141260 explanation : commit . aiExplanation ,
142261 filePatches : filePatches ,
143262 patch : patch ,
263+ author : author ,
144264 } ;
145265 } ) ;
146266}
@@ -199,6 +319,8 @@ function convertDiffToComposerHunks(
199319 diff : ParsedGitDiff ,
200320 source : 'staged' | 'unstaged' | 'commits' ,
201321 startingCount : number ,
322+ author ?: GitCommitIdentityShape ,
323+ coAuthors ?: GitCommitIdentityShape [ ] ,
202324) : { hunks : ComposerHunk [ ] ; count : number } {
203325 const hunks : ComposerHunk [ ] = [ ] ;
204326 let counter = startingCount ;
@@ -239,6 +361,8 @@ function convertDiffToComposerHunks(
239361 source : source ,
240362 assigned : false ,
241363 isRename : file . metadata . renamedOrCopied !== false ,
364+ author : author ,
365+ coAuthors : coAuthors ,
242366 } ;
243367
244368 hunks . push ( composerHunk ) ;
@@ -262,6 +386,8 @@ function convertDiffToComposerHunks(
262386 source : source ,
263387 assigned : false ,
264388 isRename : false ,
389+ author : author ,
390+ coAuthors : coAuthors ,
265391 } ;
266392
267393 hunks . push ( composerHunk ) ;
@@ -517,6 +643,22 @@ export async function getBranchCommits(
517643 }
518644}
519645
646+ export function parseCoAuthorsFromGitCommit ( commit : GitCommit ) : GitCommitIdentityShape [ ] {
647+ const coAuthors : GitCommitIdentityShape [ ] = [ ] ;
648+ if ( ! commit . message ) return coAuthors ;
649+
650+ const coAuthorRegex = / ^ C o - a u t h o r e d - b y : \s * ( .+ ?) (?: \s * < ( .+ ?) > ) ? \s * $ / gm;
651+ let match ;
652+ while ( ( match = coAuthorRegex . exec ( commit . message ) ) !== null ) {
653+ const [ , name , email ] = match ;
654+ if ( name ) {
655+ coAuthors . push ( { name : name . trim ( ) , email : email ?. trim ( ) , date : commit . date } ) ;
656+ }
657+ }
658+
659+ return coAuthors ;
660+ }
661+
520662/**
521663 * Creates ComposerCommit array from existing branch commits, preserving order and mapping hunks correctly
522664 */
@@ -525,6 +667,7 @@ export async function createComposerCommitsFromGitCommits(
525667 commits : GitCommit [ ] ,
526668) : Promise < { commits : ComposerCommit [ ] ; hunks : ComposerHunk [ ] } | undefined > {
527669 try {
670+ const currentUser = await repo . git . config . getCurrentUser ( ) ;
528671 const composerCommits : ComposerCommit [ ] = [ ] ;
529672 const allHunks : ComposerHunk [ ] = [ ] ;
530673 let count = 0 ;
@@ -545,8 +688,18 @@ export async function createComposerCommitsFromGitCommits(
545688 // Parse the diff to get hunks
546689 const parsedDiff = parseGitDiff ( diff . contents ) ;
547690 const commitHunkIndices : number [ ] = [ ] ;
691+ const author = {
692+ ...commit . author ,
693+ name : commit . author . name === 'You' ? ( currentUser ?. name ?? commit . author . name ) : commit . author . name ,
694+ } ;
548695
549- const { hunks, count : newCount } = convertDiffToComposerHunks ( parsedDiff , 'commits' , count ) ;
696+ const { hunks, count : newCount } = convertDiffToComposerHunks (
697+ parsedDiff ,
698+ 'commits' ,
699+ count ,
700+ author ,
701+ parseCoAuthorsFromGitCommit ( commit ) ,
702+ ) ;
550703 allHunks . push ( ...hunks ) ;
551704 count = newCount ;
552705 commitHunkIndices . push ( ...hunks . map ( h => h . index ) ) ;
0 commit comments