@@ -28,8 +28,9 @@ import { FeatureUseCase } from '../models/constants'
2828import { ProjectZipError } from '../../amazonqTest/error'
2929import { normalize } from '../../shared/utilities/pathUtils'
3030import { ZipStream } from '../../shared/utilities/zipStream'
31- import { getTextContent } from '../../shared/utilities/vsCodeUtils'
32- import { getGitDiffContent } from '../../amazonq/util/gitDiffUtils'
31+ import { getTextContent } from '../../shared/utilities/textDocumentUtilities'
32+ import { ChildProcess , ChildProcessOptions } from '../../shared/utilities/processUtils'
33+ import { removeAnsi } from '../../shared/utilities/textUtilities'
3334
3435export interface ZipMetadata {
3536 rootDir : string
@@ -217,7 +218,7 @@ export class ZipUtil {
217218 filePath ?: string ,
218219 scope ?: CodeWhispererConstants . CodeAnalysisScope
219220 ) {
220- const gitDiffContent = await getGitDiffContent ( projectPaths , filePath )
221+ const gitDiffContent = await getGitDiffContentForProjects ( projectPaths , filePath )
221222 if ( gitDiffContent ) {
222223 zip . writeString ( gitDiffContent , ZipConstants . codeDiffFilePath )
223224 }
@@ -451,3 +452,173 @@ export class ZipUtil {
451452 logger . verbose ( `Complete cleaning up temporary files.` )
452453 }
453454}
455+
456+ // TODO: port this to its own utility with tests.
457+ interface GitDiffOptions {
458+ projectPath : string
459+ projectName : string
460+ filepath ?: string
461+ scope ?: CodeWhispererConstants . CodeAnalysisScope
462+ }
463+
464+ async function getGitDiffContentForProjects (
465+ projectPaths : string [ ] ,
466+ filepath ?: string ,
467+ scope ?: CodeWhispererConstants . CodeAnalysisScope
468+ ) {
469+ let gitDiffContent = ''
470+ for ( const projectPath of projectPaths ) {
471+ const projectName = path . basename ( projectPath )
472+ gitDiffContent += await getGitDiffContent ( {
473+ projectPath,
474+ projectName,
475+ filepath,
476+ scope,
477+ } )
478+ }
479+ return gitDiffContent
480+ }
481+
482+ async function getGitDiffContent ( options : GitDiffOptions ) : Promise < string > {
483+ const { projectPath, projectName, filepath : filePath , scope } = options
484+ const isProjectScope = scope === CodeWhispererConstants . CodeAnalysisScope . PROJECT
485+
486+ const untrackedFilesString = await getGitUntrackedFiles ( projectPath )
487+ const untrackedFilesArray = untrackedFilesString ?. trim ( ) ?. split ( '\n' ) ?. filter ( Boolean )
488+
489+ if ( isProjectScope && untrackedFilesArray && ! untrackedFilesArray . length ) {
490+ return await generateHeadDiff ( projectPath , projectName )
491+ }
492+
493+ let diffContent = ''
494+
495+ if ( isProjectScope ) {
496+ diffContent = await generateHeadDiff ( projectPath , projectName )
497+
498+ if ( untrackedFilesArray ) {
499+ const untrackedDiffs = await Promise . all (
500+ untrackedFilesArray . map ( ( file ) => generateNewFileDiff ( projectPath , projectName , file ) )
501+ )
502+ diffContent += untrackedDiffs . join ( '' )
503+ }
504+ } else if ( ! isProjectScope && filePath ) {
505+ const relativeFilePath = path . relative ( projectPath , filePath )
506+
507+ const newFileDiff = await generateNewFileDiff ( projectPath , projectName , relativeFilePath )
508+ diffContent = rewriteDiff ( newFileDiff )
509+ }
510+ return diffContent
511+ }
512+
513+ async function getGitUntrackedFiles ( projectPath : string ) : Promise < string | undefined > {
514+ const checkNewFileArgs = [ 'ls-files' , '--others' , '--exclude-standard' ]
515+ const checkProcess = new ChildProcess ( 'git' , checkNewFileArgs )
516+
517+ try {
518+ let output = ''
519+ await checkProcess . run ( {
520+ rejectOnError : true ,
521+ rejectOnErrorCode : true ,
522+ onStdout : ( text ) => {
523+ output += text
524+ } ,
525+ spawnOptions : {
526+ cwd : projectPath ,
527+ } ,
528+ } )
529+ return output
530+ } catch ( err ) {
531+ getLogger ( ) . warn ( `Failed to check if file is new: ${ err } ` )
532+ return undefined
533+ }
534+ }
535+
536+ async function generateHeadDiff ( projectPath : string , projectName : string , relativePath ?: string ) : Promise < string > {
537+ let diffContent = ''
538+
539+ const gitArgs = [
540+ 'diff' ,
541+ 'HEAD' ,
542+ `--src-prefix=a/${ projectName } /` ,
543+ `--dst-prefix=b/${ projectName } /` ,
544+ ...( relativePath ? [ relativePath ] : [ ] ) ,
545+ ]
546+
547+ const childProcess = new ChildProcess ( 'git' , gitArgs )
548+
549+ const runOptions : ChildProcessOptions = {
550+ rejectOnError : true ,
551+ rejectOnErrorCode : true ,
552+ onStdout : ( text ) => {
553+ diffContent += text
554+ getLogger ( ) . verbose ( removeAnsi ( text ) )
555+ } ,
556+ onStderr : ( text ) => {
557+ getLogger ( ) . error ( removeAnsi ( text ) )
558+ } ,
559+ spawnOptions : {
560+ cwd : projectPath ,
561+ } ,
562+ }
563+
564+ try {
565+ await childProcess . run ( runOptions )
566+ return diffContent
567+ } catch ( err ) {
568+ getLogger ( ) . warn ( `Failed to run command \`${ childProcess . toString ( ) } \`: ${ err } ` )
569+ return ''
570+ }
571+ }
572+
573+ async function generateNewFileDiff ( projectPath : string , projectName : string , relativePath : string ) : Promise < string > {
574+ let diffContent = ''
575+
576+ const gitArgs = [
577+ 'diff' ,
578+ '--no-index' ,
579+ `--src-prefix=a/${ projectName } /` ,
580+ `--dst-prefix=b/${ projectName } /` ,
581+ '/dev/null' , // Use /dev/null as the old file
582+ relativePath ,
583+ ]
584+
585+ const childProcess = new ChildProcess ( 'git' , gitArgs )
586+ const runOptions : ChildProcessOptions = {
587+ rejectOnError : false ,
588+ rejectOnErrorCode : false ,
589+ onStdout : ( text ) => {
590+ diffContent += text
591+ getLogger ( ) . verbose ( removeAnsi ( text ) )
592+ } ,
593+ onStderr : ( text ) => {
594+ getLogger ( ) . error ( removeAnsi ( text ) )
595+ } ,
596+ spawnOptions : {
597+ cwd : projectPath ,
598+ } ,
599+ }
600+
601+ try {
602+ await childProcess . run ( runOptions )
603+ return diffContent
604+ } catch ( err ) {
605+ getLogger ( ) . warn ( `Failed to run diff command: ${ err } ` )
606+ return ''
607+ }
608+ }
609+
610+ function rewriteDiff ( inputStr : string ) : string {
611+ const lines = inputStr . split ( '\n' )
612+ const rewrittenLines = lines . slice ( 0 , 5 ) . map ( ( line ) => {
613+ line = line . replace ( / \\ \\ / g, '/' )
614+ line = line . replace ( / ( " a \/ [ ^ " ] * ) / g, ( match , p1 ) => p1 )
615+ line = line . replace ( / ( " b \/ [ ^ " ] * ) / g, ( match , p1 ) => p1 )
616+ line = line . replace ( / " / g, '' )
617+
618+ return line
619+ } )
620+ const outputLines = [ ...rewrittenLines , ...lines . slice ( 5 ) ]
621+ const outputStr = outputLines . join ( '\n' )
622+
623+ return outputStr
624+ }
0 commit comments