@@ -509,6 +509,138 @@ public Result StatusPorcelain()
509509 return this . InvokeGitInWorkingDirectoryRoot ( command , useReadObjectHook : false ) ;
510510 }
511511
512+ /// <summary>
513+ /// Returns staged file changes (index vs HEAD) as null-separated pairs of
514+ /// status and path: "A\0path1\0M\0path2\0D\0path3\0".
515+ /// Status codes: A=added, M=modified, D=deleted, R=renamed, C=copied.
516+ /// </summary>
517+ /// <param name="pathspecs">Inline pathspecs to scope the diff, or null for all.</param>
518+ /// <param name="pathspecFromFile">
519+ /// Path to a file containing additional pathspecs (one per line), forwarded
520+ /// as --pathspec-from-file to git. Null if not used.
521+ /// </param>
522+ /// <param name="pathspecFileNul">
523+ /// When true and pathspecFromFile is set, pathspec entries in the file are
524+ /// separated by NUL instead of newline (--pathspec-file-nul).
525+ /// </param>
526+ public Result DiffCachedNameStatus ( string [ ] pathspecs = null , string pathspecFromFile = null , bool pathspecFileNul = false )
527+ {
528+ string command = "diff --cached --name-status -z --no-renames" ;
529+
530+ if ( pathspecFromFile != null )
531+ {
532+ command += " --pathspec-from-file=" + QuoteGitPath ( pathspecFromFile ) ;
533+ if ( pathspecFileNul )
534+ {
535+ command += " --pathspec-file-nul" ;
536+ }
537+ }
538+
539+ if ( pathspecs != null && pathspecs . Length > 0 )
540+ {
541+ command += " -- " + string . Join ( " " , pathspecs . Select ( p => QuoteGitPath ( p ) ) ) ;
542+ }
543+
544+ return this . InvokeGitInWorkingDirectoryRoot ( command , useReadObjectHook : false ) ;
545+ }
546+
547+ /// <summary>
548+ /// Writes the staged (index) version of the specified files to the working
549+ /// tree with correct line endings and attributes. Batches multiple paths into
550+ /// a single git process invocation where possible, respecting the Windows
551+ /// command line length limit.
552+ /// </summary>
553+ public List < Result > CheckoutIndexForFiles ( IEnumerable < string > paths )
554+ {
555+ // Windows command line limit is 32,767 characters. Leave headroom for
556+ // the base command and other arguments.
557+ const int MaxCommandLength = 30000 ;
558+ const string BaseCommand = "-c core.hookspath= checkout-index --force --" ;
559+
560+ List < Result > results = new List < Result > ( ) ;
561+ StringBuilder command = new StringBuilder ( BaseCommand ) ;
562+ foreach ( string path in paths )
563+ {
564+ string quotedPath = " " + QuoteGitPath ( path ) ;
565+
566+ if ( command . Length + quotedPath . Length > MaxCommandLength && command . Length > BaseCommand . Length )
567+ {
568+ // Flush current batch
569+ results . Add ( this . InvokeGitInWorkingDirectoryRoot ( command . ToString ( ) , useReadObjectHook : false ) ) ;
570+ command . Clear ( ) ;
571+ command . Append ( BaseCommand ) ;
572+ }
573+
574+ command . Append ( quotedPath ) ;
575+ }
576+
577+ // Flush remaining paths
578+ if ( command . Length > BaseCommand . Length )
579+ {
580+ results . Add ( this . InvokeGitInWorkingDirectoryRoot ( command . ToString ( ) , useReadObjectHook : false ) ) ;
581+ }
582+
583+ return results ;
584+ }
585+
586+ /// <summary>
587+ /// Wraps a path in double quotes for use as a git command argument,
588+ /// escaping any embedded double quotes and any backslashes that
589+ /// immediately precede a double quote (to prevent them from being
590+ /// interpreted as escape characters by the Windows C runtime argument
591+ /// parser). Lone backslashes used as path separators are left as-is.
592+ /// </summary>
593+ public static string QuoteGitPath ( string path )
594+ {
595+ StringBuilder sb = new StringBuilder ( path . Length + 4 ) ;
596+ sb . Append ( '"' ) ;
597+
598+ for ( int i = 0 ; i < path . Length ; i ++ )
599+ {
600+ if ( path [ i ] == '"' )
601+ {
602+ sb . Append ( '\\ ' ) ;
603+ sb . Append ( '"' ) ;
604+ }
605+ else if ( path [ i ] == '\\ ' )
606+ {
607+ // Count consecutive backslashes
608+ int backslashCount = 0 ;
609+ while ( i < path . Length && path [ i ] == '\\ ' )
610+ {
611+ backslashCount ++ ;
612+ i ++ ;
613+ }
614+
615+ if ( i < path . Length && path [ i ] == '"' )
616+ {
617+ // Backslashes before a quote: double them all, then escape the quote
618+ sb . Append ( '\\ ' , backslashCount * 2 ) ;
619+ sb . Append ( '\\ ' ) ;
620+ sb . Append ( '"' ) ;
621+ }
622+ else if ( i == path . Length )
623+ {
624+ // Backslashes at end of string (before closing quote): double them
625+ sb . Append ( '\\ ' , backslashCount * 2 ) ;
626+ }
627+ else
628+ {
629+ // Backslashes not before a quote: keep as-is (path separators)
630+ sb . Append ( '\\ ' , backslashCount ) ;
631+ i -- ; // Re-process current non-backslash char
632+ }
633+ }
634+ else
635+ {
636+ sb . Append ( path [ i ] ) ;
637+ }
638+ }
639+
640+ sb . Append ( '"' ) ;
641+ return sb . ToString ( ) ;
642+ }
643+
512644 public Result SerializeStatus ( bool allowObjectDownloads , string serializePath )
513645 {
514646 // specify ignored=matching and --untracked-files=complete
0 commit comments