@@ -23,6 +23,11 @@ interface RunGitCommandOptions extends RunGitTextOptions {
2323 acceptedExitCodes ?: number [ ] ;
2424}
2525
26+ export interface GitColorMovedOptions {
27+ mode : string ;
28+ whitespaceMode ?: string ;
29+ }
30+
2631/** Append Git pathspec arguments only when the caller requested them. */
2732export function appendGitPathspecs ( args : string [ ] , pathspecs ?: string [ ] ) {
2833 if ( ! pathspecs || pathspecs . length === 0 ) {
@@ -46,13 +51,58 @@ const DIFF_PREFIX_NORMALIZATION_ARGS = [
4651 "diff.dstPrefix=b/" ,
4752] ;
4853
54+ const GIT_MOVED_LINE_COLOR_CONFIG = [
55+ "-c" ,
56+ "color.diff.oldMoved=magenta bold" ,
57+ "-c" ,
58+ "color.diff.oldMovedAlternative=magenta bold" ,
59+ "-c" ,
60+ "color.diff.oldMovedDimmed=magenta dim" ,
61+ "-c" ,
62+ "color.diff.oldMovedAlternativeDimmed=magenta dim" ,
63+ "-c" ,
64+ "color.diff.newMoved=cyan bold" ,
65+ "-c" ,
66+ "color.diff.newMovedAlternative=cyan bold" ,
67+ "-c" ,
68+ "color.diff.newMovedDimmed=cyan dim" ,
69+ "-c" ,
70+ "color.diff.newMovedAlternativeDimmed=cyan dim" ,
71+ ] ;
72+
4973function withNormalizedDiffPrefixes ( args : string [ ] ) {
5074 return [ ...DIFF_PREFIX_NORMALIZATION_ARGS , ...args ] ;
5175}
5276
77+ /** Return Git color flags for patch commands, enabling ANSI only when Hunk needs move classes. */
78+ function gitPatchColorArgs ( colorMoved : GitColorMovedOptions | null ) {
79+ if ( ! colorMoved ) {
80+ return [ "--no-color" ] ;
81+ }
82+
83+ return [
84+ "--color=always" ,
85+ `--color-moved=${ colorMoved . mode } ` ,
86+ ...( colorMoved . whitespaceMode ? [ `--color-moved-ws=${ colorMoved . whitespaceMode } ` ] : [ ] ) ,
87+ ] ;
88+ }
89+
90+ /** Add deterministic moved-line colors so the parser can classify Git's ANSI output reliably. */
91+ function withGitMovedLineColorConfig ( args : string [ ] , colorMoved : GitColorMovedOptions | null ) {
92+ if ( ! colorMoved ) {
93+ return args ;
94+ }
95+
96+ return [ ...GIT_MOVED_LINE_COLOR_CONFIG , ...args ] ;
97+ }
98+
5399/** Build the exact `git diff` arguments used for the shared working-tree and range review path. */
54- export function buildGitDiffArgs ( input : VcsCommandInput , excludedPathspecs : string [ ] = [ ] ) {
55- const args = [ "diff" , "--no-ext-diff" , "--find-renames" , "--no-color" ] ;
100+ export function buildGitDiffArgs (
101+ input : VcsCommandInput ,
102+ excludedPathspecs : string [ ] = [ ] ,
103+ colorMoved : GitColorMovedOptions | null = null ,
104+ ) {
105+ const args = [ "diff" , "--no-ext-diff" , "--find-renames" , ...gitPatchColorArgs ( colorMoved ) ] ;
56106
57107 if ( input . staged ) {
58108 args . push ( "--staged" ) ;
@@ -72,7 +122,7 @@ export function buildGitDiffArgs(input: VcsCommandInput, excludedPathspecs: stri
72122 appendGitPathspecs ( args , input . pathspecs ) ;
73123 }
74124
75- return withNormalizedDiffPrefixes ( args ) ;
125+ return withNormalizedDiffPrefixes ( withGitMovedLineColorConfig ( args , colorMoved ) ) ;
76126}
77127
78128/** Build the cheap tracked-file stats query used to skip huge file diffs before patch output. */
@@ -115,26 +165,45 @@ function buildGitNewFileDiffArgs(filePath: string) {
115165}
116166
117167/** Build the exact `git show` arguments used for commit review. */
118- export function buildGitShowArgs ( input : ShowCommandInput ) {
119- const args = [ "show" , "--format=" , "--no-ext-diff" , "--find-renames" , "--no-color" ] ;
168+ export function buildGitShowArgs (
169+ input : ShowCommandInput ,
170+ colorMoved : GitColorMovedOptions | null = null ,
171+ ) {
172+ const args = [
173+ "show" ,
174+ "--format=" ,
175+ "--no-ext-diff" ,
176+ "--find-renames" ,
177+ ...gitPatchColorArgs ( colorMoved ) ,
178+ ] ;
120179
121180 if ( input . ref ) {
122181 args . push ( input . ref ) ;
123182 }
124183
125184 appendGitPathspecs ( args , input . pathspecs ) ;
126- return withNormalizedDiffPrefixes ( args ) ;
185+ return withNormalizedDiffPrefixes ( withGitMovedLineColorConfig ( args , colorMoved ) ) ;
127186}
128187
129188/** Build the exact `git stash show -p` arguments used for stash review. */
130- export function buildGitStashShowArgs ( input : StashShowCommandInput ) {
131- const args = [ "stash" , "show" , "-p" , "--no-ext-diff" , "--find-renames" , "--no-color" ] ;
189+ export function buildGitStashShowArgs (
190+ input : StashShowCommandInput ,
191+ colorMoved : GitColorMovedOptions | null = null ,
192+ ) {
193+ const args = [
194+ "stash" ,
195+ "show" ,
196+ "-p" ,
197+ "--no-ext-diff" ,
198+ "--find-renames" ,
199+ ...gitPatchColorArgs ( colorMoved ) ,
200+ ] ;
132201
133202 if ( input . ref ) {
134203 args . push ( input . ref ) ;
135204 }
136205
137- return withNormalizedDiffPrefixes ( args ) ;
206+ return withNormalizedDiffPrefixes ( withGitMovedLineColorConfig ( args , colorMoved ) ) ;
138207}
139208
140209export function formatGitCommandLabel ( input : GitBackedInput ) {
@@ -330,6 +399,72 @@ export function runGitText(options: RunGitTextOptions) {
330399 return runGitCommand ( options ) . stdout ;
331400}
332401
402+ const GIT_BOOLEAN_TRUE_VALUES = new Set ( [ "true" , "yes" , "on" , "1" , "always" ] ) ;
403+ const GIT_BOOLEAN_FALSE_VALUES = new Set ( [ "false" , "no" , "off" , "0" , "never" ] ) ;
404+
405+ /** Read an optional Git config value without treating an unset key as an error. */
406+ function readOptionalGitConfig (
407+ input : GitBackedInput ,
408+ key : string ,
409+ options : Omit < RunGitTextOptions , "input" | "args" > = { } ,
410+ ) {
411+ const result = runGitCommand ( {
412+ input,
413+ args : [ "config" , "--get" , key ] ,
414+ ...options ,
415+ acceptedExitCodes : [ 0 , 1 ] ,
416+ } ) ;
417+
418+ if ( result . exitCode !== 0 ) {
419+ return undefined ;
420+ }
421+
422+ return result . stdout . trim ( ) || undefined ;
423+ }
424+
425+ /** Normalize Git's diff.colorMoved config into the mode Hunk should request from Git. */
426+ function normalizeGitColorMovedMode ( value : string | undefined ) {
427+ if ( ! value ) {
428+ return undefined ;
429+ }
430+
431+ const normalized = value . toLowerCase ( ) ;
432+ if ( GIT_BOOLEAN_FALSE_VALUES . has ( normalized ) || normalized === "no" ) {
433+ return null ;
434+ }
435+
436+ if ( GIT_BOOLEAN_TRUE_VALUES . has ( normalized ) ) {
437+ return "zebra" ;
438+ }
439+
440+ return value ;
441+ }
442+
443+ /** Resolve whether Hunk should ask Git to color moved lines for this patch command. */
444+ export function resolveGitColorMovedOptions (
445+ input : GitBackedInput ,
446+ options : Omit < RunGitTextOptions , "input" | "args" > = { } ,
447+ ) : GitColorMovedOptions | null {
448+ const gitMode = normalizeGitColorMovedMode (
449+ readOptionalGitConfig ( input , "diff.colorMoved" , options ) ,
450+ ) ;
451+
452+ if ( gitMode === null ) {
453+ return null ;
454+ }
455+
456+ const mode = gitMode ?? ( input . options . colorMoved ? "zebra" : undefined ) ;
457+ if ( ! mode ) {
458+ return null ;
459+ }
460+
461+ const whitespaceMode = readOptionalGitConfig ( input , "diff.colorMovedWS" , options ) ;
462+ return {
463+ mode,
464+ whitespaceMode,
465+ } ;
466+ }
467+
333468/**
334469 * Return whether one `hunk diff` input still compares against the live working tree.
335470 *
0 commit comments