@@ -424,6 +424,208 @@ bool IsDefaultBranch(string candidate) => !StringUtil.IsNullOrWhiteSpace(default
424
424
candidate == $ "{ remoteName } /{ defaultBranch } ") ;
425
425
}
426
426
427
+ /// <summary>
428
+ /// Retrieves commit metadata for the specified <paramref name="commitSha"/>. If the repository is a shallow clone, the
429
+ /// method attempts to un‑shallow it (requires Git >= 2.27) so that the commit information is available.
430
+ /// </summary>
431
+ /// <param name="workingDirectory">Path to the git working directory.</param>
432
+ /// <param name="commitSha">Commit SHA to retrieve.</param>
433
+ /// <returns>A populated <see cref="CommitData"/> on success; <c>null</c> otherwise.</returns>
434
+ public static CommitData ? FetchCommitData ( string workingDirectory , string commitSha )
435
+ {
436
+ try
437
+ {
438
+ // 1. Detect shallow repository
439
+ Log . Debug ( "GitCommandHelper.FetchCommitData: checking if the repository is a shallow clone" ) ;
440
+ var isShallowClone = IsShallowCloneRepository ( workingDirectory ) ;
441
+
442
+ // 2. If shallow, un‑shallow it (git fetch) provided we have a modern git version
443
+ if ( isShallowClone )
444
+ {
445
+ Log . Debug ( "GitCommandHelper.FetchCommitData: checking the git version" ) ;
446
+ var ( major , minor , patch ) = GetGitVersion ( workingDirectory ) ;
447
+ Log . Debug < int , int , int > ( "GitCommandHelper.FetchCommitData: git version detected {Major}.{Minor}.{Patch}" , major , minor , patch ) ;
448
+ if ( major > 2 || ( major == 2 && minor >= 27 ) )
449
+ {
450
+ // Retrieve remote name, fallback to "origin" if not set
451
+ var remoteName = GetRemoteName ( workingDirectory ) ;
452
+ Log . Debug ( "GitCommandHelper.FetchCommitData: remote name: {Remote}" , remoteName ) ;
453
+
454
+ // git fetch --update-shallow --filter="blob:none" --recurse-submodules=no --no-write-fetch-head <remoteName> <commitSha>
455
+ var fetchArgs =
456
+ $ "fetch --update-shallow --filter=\" blob:none\" --recurse-submodules=no --no-write-fetch-head { remoteName } { commitSha } ";
457
+ var fetchOutput = RunGitCommand (
458
+ workingDirectory ,
459
+ fetchArgs ,
460
+ MetricTags . CIVisibilityCommands . Fetch ) ;
461
+
462
+ if ( fetchOutput is null || fetchOutput . ExitCode != 0 )
463
+ {
464
+ Log . Warning ( "GitCommandHelper.FetchCommitData: git fetch failed. Exit={ExitCode}, Error={Error}" , fetchOutput ? . ExitCode , fetchOutput ? . Error ) ;
465
+ return null ;
466
+ }
467
+ }
468
+ else
469
+ {
470
+ return null ;
471
+ }
472
+ }
473
+
474
+ // 3. Get commit details via `git show`
475
+ Log . Debug ( "GitCommandHelper.FetchCommitData: fetching commit details for {Commit}" , commitSha ) ;
476
+ // Example output:
477
+ // '1f808ea4e7c068a149975e1851bd905cef56779c|,|1753691341|,|Tony Redondo|,|tony.redondo@datadoghq.com|,|1753691341|,|GitHub|,|noreply@github.com|,|Merge branch 'master' into tony/topt-get-head-commit-info'
478
+ var showArgs = $ """ show { commitSha } -s --format='%H|,|%at|,|%an|,|%ae|,|%ct|,|%cn|,|%ce|,|%B'""" ;
479
+ var showOutput = RunGitCommand (
480
+ workingDirectory ,
481
+ showArgs ,
482
+ MetricTags . CIVisibilityCommands . GetHead ) ;
483
+
484
+ if ( showOutput is null || showOutput . ExitCode != 0 || string . IsNullOrWhiteSpace ( showOutput . Output ) )
485
+ {
486
+ Log . Warning ( "GitCommandHelper.FetchCommitData: git show failed. Exit={ExitCode}, Error={Error}" , showOutput ? . ExitCode , showOutput ? . Error ) ;
487
+ return null ;
488
+ }
489
+
490
+ // 4. Parse output
491
+ // The delimiter is |,| to avoid issues with commit messages that may contain commas.
492
+ var gitLogDataArray = showOutput . Output . Trim ( ) . Split ( [ "|,|" ] , StringSplitOptions . None ) ;
493
+ if ( gitLogDataArray . Length < 8 )
494
+ {
495
+ Log . Warning < int > ( "GitCommandHelper.FetchCommitData: unexpected git show output – expected ≥ 8 tokens, got {Count}" , gitLogDataArray . Length ) ;
496
+ return null ;
497
+ }
498
+
499
+ // Parse author and committer dates from Unix timestamp
500
+ if ( ! long . TryParse ( gitLogDataArray [ 1 ] , out var authorUnixDate ) )
501
+ {
502
+ Log . Warning ( "Error parsing author date from git log output" ) ;
503
+ return null ;
504
+ }
505
+
506
+ if ( ! long . TryParse ( gitLogDataArray [ 4 ] , out var committerUnixDate ) )
507
+ {
508
+ Log . Warning ( "Error parsing committer date from git log output" ) ;
509
+ return null ;
510
+ }
511
+
512
+ var commit = gitLogDataArray [ 0 ] ;
513
+ if ( commit . StartsWith ( "'" ) )
514
+ {
515
+ commit = commit . Substring ( 1 ) ;
516
+ }
517
+
518
+ // The commit message may contain the `|,|` string , so we join the remaining parts.
519
+ var commitMessage = gitLogDataArray . Length > 8 ? string . Join ( "|,|" , gitLogDataArray . Skip ( 7 ) ) . Trim ( ) : gitLogDataArray [ 7 ] . Trim ( ) ;
520
+ if ( commitMessage . EndsWith ( "'" ) )
521
+ {
522
+ commitMessage = commitMessage . Substring ( 0 , commitMessage . Length - 1 ) . Trim ( ) ;
523
+ }
524
+
525
+ var commitData = new CommitData (
526
+ CommitSha : commit ,
527
+ AuthorDate : DateTimeOffset . FromUnixTimeSeconds ( authorUnixDate ) ,
528
+ AuthorName : gitLogDataArray [ 2 ] ,
529
+ AuthorEmail : gitLogDataArray [ 3 ] ,
530
+ CommitterDate : DateTimeOffset . FromUnixTimeSeconds ( committerUnixDate ) ,
531
+ CommitterName : gitLogDataArray [ 5 ] ,
532
+ CommitterEmail : gitLogDataArray [ 6 ] ,
533
+ CommitMessage : commitMessage ) ;
534
+
535
+ Log . Debug ( "GitCommandHelper.FetchCommitData: completed successfully for {Commit}" , commitSha ) ;
536
+ return commitData ;
537
+ }
538
+ catch ( Exception ex )
539
+ {
540
+ Log . Warning ( ex , "GitCommandHelper.FetchCommitData: unexpected error while fetching data for {Commit}" , commitSha ) ;
541
+ return null ;
542
+ }
543
+ }
544
+
545
+ /// <summary>
546
+ /// Determines whether the repository located at <paramref name="workingDirectory"/> is a shallow clone.
547
+ /// </summary>
548
+ /// <param name="workingDirectory">Path to the git working directory.</param>
549
+ /// <returns>True if the repository is a shallow clone; otherwise, false.</returns>
550
+ public static bool IsShallowCloneRepository ( string workingDirectory )
551
+ {
552
+ // We need to check if the git clone is a shallow one before uploading anything.
553
+ // In the case is a shallow clone we need to reconfigure it to upload the git tree
554
+ // without blobs so no content will be downloaded.
555
+ var gitRevParseShallowOutput = RunGitCommand (
556
+ workingDirectory ,
557
+ "rev-parse --is-shallow-repository" ,
558
+ MetricTags . CIVisibilityCommands . CheckShallow ) ;
559
+ if ( gitRevParseShallowOutput is null || gitRevParseShallowOutput . ExitCode != 0 )
560
+ {
561
+ Log . Warning ( "GitCommandHelper: 'git rev-parse --is-shallow-repository' command is null or exit code is not 0. Exit={ExitCode}" , gitRevParseShallowOutput ? . ExitCode ) ;
562
+ return false ;
563
+ }
564
+
565
+ return gitRevParseShallowOutput . Output . IndexOf ( "true" , StringComparison . OrdinalIgnoreCase ) > - 1 ;
566
+ }
567
+
568
+ /// <summary>
569
+ /// Parses and returns the git version (major, minor, patch) as reported by <c>git --version</c>.
570
+ /// </summary>
571
+ /// <param name="workingDirectory">Path to the git working directory.</param>
572
+ /// <returns>VersionInfo containing the major, minor, and patch version numbers.</returns>
573
+ public static VersionInfo GetGitVersion ( string workingDirectory )
574
+ {
575
+ var output = RunGitCommand (
576
+ workingDirectory ,
577
+ "--version" ,
578
+ MetricTags . CIVisibilityCommands . GetBranch ) ;
579
+
580
+ if ( output is { ExitCode : 0 } && ! string . IsNullOrWhiteSpace ( output . Output ) )
581
+ {
582
+ // Expected format: "git version 2.41.0" or similar
583
+ var span = output . Output . AsSpan ( ) . Trim ( ) ;
584
+ var lastSpace = span . LastIndexOf ( ' ' ) ;
585
+ var versionText = span . Slice ( lastSpace + 1 ) . ToString ( ) ;
586
+ var segments = versionText . Split ( '.' ) ;
587
+ int . TryParse ( segments . ElementAtOrDefault ( 0 ) , out var major ) ;
588
+ int . TryParse ( segments . ElementAtOrDefault ( 1 ) , out var minor ) ;
589
+ int . TryParse ( segments . ElementAtOrDefault ( 2 ) , out var patch ) ;
590
+ return new VersionInfo ( major , minor , patch ) ;
591
+ }
592
+
593
+ return new VersionInfo ( 0 , 0 , 0 ) ;
594
+ }
595
+
596
+ /// <summary>
597
+ /// Attempts to obtain the default remote name for the repository located at <paramref name="workingDirectory"/>.
598
+ /// Falls back to <c>origin</c> when no remote could be detected.
599
+ /// </summary>
600
+ /// <param name="workingDirectory">Path to the git working directory.</param>
601
+ /// <returns>Remote name, or <c>origin</c> if not set.</returns>
602
+ public static string GetRemoteName ( string workingDirectory )
603
+ {
604
+ var output = RunGitCommand (
605
+ workingDirectory ,
606
+ "config --default origin --get clone.defaultRemoteName" ,
607
+ MetricTags . CIVisibilityCommands . GetRemote ) ;
608
+
609
+ return output ? . Output ? . Replace ( "\n " , string . Empty ) . Trim ( ) ?? "origin" ;
610
+ }
611
+
612
+ /// <summary>
613
+ /// Retrieves a list of local commits from the repository located at <paramref name="workingDirectory"/>.
614
+ /// </summary>
615
+ /// <param name="workingDirectory">Path to the git working directory.</param>
616
+ /// <returns>String array containing commit SHAs, or an empty array if no commits are found.</returns>
617
+ public static string [ ] GetLocalCommits ( string workingDirectory )
618
+ {
619
+ var gitLogOutput = RunGitCommand ( workingDirectory , "log --format=%H -n 1000 --since=\" 1 month ago\" " , MetricTags . CIVisibilityCommands . GetLocalCommits ) ;
620
+ if ( gitLogOutput is null )
621
+ {
622
+ Log . Warning ( "TestOptimizationClient: 'git log...' command is null" ) ;
623
+ return [ ] ;
624
+ }
625
+
626
+ return gitLogOutput . Output . Split ( [ "\n " ] , StringSplitOptions . RemoveEmptyEntries ) ;
627
+ }
628
+
427
629
private static void CheckAndFetchBranch ( string workingDirectory , string branch , string remoteName )
428
630
{
429
631
try
@@ -480,4 +682,16 @@ private static string[] SplitLines(string text, StringSplitOptions options = Str
480
682
private readonly record struct LineRange ( int Start , int End ) ;
481
683
482
684
private readonly record struct BranchMetrics ( string Branch , string MergeBaseSha , int Behind , int Ahead ) ;
685
+
686
+ public readonly record struct VersionInfo ( int Major , int Minor , int Patch ) ;
687
+
688
+ public readonly record struct CommitData (
689
+ string CommitSha ,
690
+ DateTimeOffset AuthorDate ,
691
+ string AuthorName ,
692
+ string AuthorEmail ,
693
+ DateTimeOffset CommitterDate ,
694
+ string CommitterName ,
695
+ string CommitterEmail ,
696
+ string CommitMessage ) ;
483
697
}
0 commit comments