GitCommand.cs 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. // Copyright (c) 2025 TerraByte Inc.
  2. //
  3. // A new helper class that abstracts away the boilerplate of running external
  4. // command-line processes, specifically Git and VS Code, using CliWrap.
  5. using System;
  6. using CliWrap;
  7. using System.IO;
  8. using System.Linq;
  9. using System.Text;
  10. using UnityEngine.Scripting;
  11. using System.Threading.Tasks;
  12. using System.Runtime.InteropServices;
  13. namespace Terra.Arbitrator.Services
  14. {
  15. /// <summary>
  16. /// An internal helper class for executing Git commands.
  17. /// It centralizes the logic for finding executables and running them via CliWrap.
  18. /// </summary>
  19. [Preserve]
  20. internal static class GitCommand
  21. {
  22. private static string _projectRoot;
  23. private static string ProjectRoot => _projectRoot ??= MainThreadDataCache.ProjectRoot;
  24. /// <summary>
  25. /// Runs a git command asynchronously.
  26. /// </summary>
  27. /// <param name="execPath">Executable path like git or vs code to perform terminal-based command action</param>
  28. /// <param name="log">A StringBuilder to capture command output for logging.</param>
  29. /// <param name="progress">A delegate to report real-time standard error lines.</param>
  30. /// <param name="args">The arguments to pass to the git command.</param>
  31. /// <param name="acceptableExitCodes">A list of exit codes that should not be treated as errors.</param>
  32. private static async Task RunAsync(string execPath, StringBuilder log, IProgress<string> progress, string[] args, params int[] acceptableExitCodes)
  33. {
  34. var stdOutBuffer = new StringBuilder();
  35. var stdErrBuffer = new StringBuilder();
  36. var argumentsString = string.Join(" ", args);
  37. log?.AppendLine($"\n--- Executing: git {argumentsString} ---");
  38. // Pipe stderr to a delegate that both captures the full output and reports each line for progress.
  39. var stdErrPipe = PipeTarget.ToDelegate(line => {
  40. stdErrBuffer.AppendLine(line);
  41. progress?.Report(line); // Report progress for each line received.
  42. });
  43. var stdOutPipe = PipeTarget.ToDelegate(line => {
  44. stdOutBuffer.AppendLine(line);
  45. });
  46. var command = Cli.Wrap(execPath)
  47. .WithArguments(args)
  48. .WithWorkingDirectory(ProjectRoot)
  49. .WithValidation(CommandResultValidation.None) // We handle validation manually
  50. | (stdOutPipe, stdErrPipe);
  51. var result = await command.ExecuteAsync();
  52. log?.AppendLine($"Exit Code: {result.ExitCode}");
  53. if (stdOutBuffer.Length > 0) log?.AppendLine($"StdOut: {stdOutBuffer}");
  54. if (stdErrBuffer.Length > 0) log?.AppendLine($"StdErr: {stdErrBuffer}");
  55. // Default to 0 if no specific codes are provided
  56. if (acceptableExitCodes.Length == 0)
  57. {
  58. acceptableExitCodes = new[] { 0 };
  59. }
  60. if (!acceptableExitCodes.Contains(result.ExitCode))
  61. {
  62. throw new Exception($"Command 'git {argumentsString}' failed with unexpected exit code {result.ExitCode}. Error: {stdErrBuffer}");
  63. }
  64. }
  65. public static Task RunGitAsync(StringBuilder log, string[] args, params int[] acceptableExitCodes)
  66. {
  67. return RunAsync(FindGitExecutable(), log, null, args, acceptableExitCodes);
  68. }
  69. public static Task RunGitAsync(StringBuilder log, string[] args, IProgress<string> progress, params int[] acceptableExitCodes)
  70. {
  71. return RunAsync(FindGitExecutable(), log, progress, args, acceptableExitCodes);
  72. }
  73. public static Task RunVsCodeAsync(StringBuilder log, string[] args, params int[] acceptableExitCodes)
  74. {
  75. return RunAsync(FindVsCodeExecutable(), log, null, args, acceptableExitCodes);
  76. }
  77. /// <summary>
  78. /// Finds the absolute path to a given executable.
  79. /// </summary>
  80. private static string FindExecutable(string name)
  81. {
  82. if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
  83. {
  84. // CliWrap handles PATH search on Windows automatically.
  85. return name;
  86. }
  87. // For macOS/Linux, we need to be more explicit due to Unity's sandboxing.
  88. string[] searchPaths = { "/usr/local/bin", "/usr/bin", "/bin", "/opt/homebrew/bin" };
  89. foreach (var path in searchPaths)
  90. {
  91. var fullPath = Path.Combine(path, name);
  92. if (File.Exists(fullPath))
  93. {
  94. return fullPath;
  95. }
  96. }
  97. throw new FileNotFoundException($"Could not find executable '{name}'. Please ensure it is installed and in your system's PATH.");
  98. }
  99. private static string FindVsCodeExecutable() => FindExecutable("code");
  100. private static string FindGitExecutable() => FindExecutable("git");
  101. }
  102. }