GitCommand.cs 3.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
  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 UnityEngine;
  10. using System.Text;
  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. internal static class GitCommand
  20. {
  21. private static string _projectRoot;
  22. private static string ProjectRoot => _projectRoot ??= Directory.GetParent(Application.dataPath)?.FullName;
  23. /// <summary>
  24. /// Runs a git command asynchronously.
  25. /// </summary>
  26. /// <param name="log">A StringBuilder to capture command output for logging.</param>
  27. /// <param name="args">The arguments to pass to the git command.</param>
  28. /// <param name="acceptableExitCodes">A list of exit codes that should not be treated as errors.</param>
  29. public static async Task RunAsync(StringBuilder log, string[] args, params int[] acceptableExitCodes)
  30. {
  31. var stdOutBuffer = new StringBuilder();
  32. var stdErrBuffer = new StringBuilder();
  33. var argumentsString = string.Join(" ", args);
  34. log?.AppendLine($"\n--- Executing: git {argumentsString} ---");
  35. var command = Cli.Wrap(FindGitExecutable())
  36. .WithArguments(args)
  37. .WithWorkingDirectory(ProjectRoot)
  38. .WithValidation(CommandResultValidation.None) // We handle validation manually
  39. | (PipeTarget.ToDelegate(x => stdOutBuffer.Append(x)), PipeTarget.ToDelegate(x => stdErrBuffer.Append(x)));
  40. var result = await command.ExecuteAsync();
  41. log?.AppendLine($"Exit Code: {result.ExitCode}");
  42. if (stdOutBuffer.Length > 0) log?.AppendLine($"StdOut: {stdOutBuffer}");
  43. if (stdErrBuffer.Length > 0) log?.AppendLine($"StdErr: {stdErrBuffer}");
  44. // Default to 0 if no specific codes are provided
  45. if (acceptableExitCodes.Length == 0)
  46. {
  47. acceptableExitCodes = new[] { 0 };
  48. }
  49. if (!acceptableExitCodes.Contains(result.ExitCode))
  50. {
  51. throw new Exception($"Command 'git {argumentsString}' failed with unexpected exit code {result.ExitCode}. Error: {stdErrBuffer}");
  52. }
  53. }
  54. /// <summary>
  55. /// Finds the absolute path to a given executable.
  56. /// </summary>
  57. private static string FindExecutable(string name)
  58. {
  59. if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
  60. {
  61. // CliWrap handles PATH search on Windows automatically.
  62. return name;
  63. }
  64. // For macOS/Linux, we need to be more explicit due to Unity's sandboxing.
  65. string[] searchPaths = { "/usr/local/bin", "/usr/bin", "/bin", "/opt/homebrew/bin" };
  66. foreach (var path in searchPaths)
  67. {
  68. var fullPath = Path.Combine(path, name);
  69. if (File.Exists(fullPath))
  70. {
  71. return fullPath;
  72. }
  73. }
  74. throw new FileNotFoundException($"Could not find executable '{name}'. Please ensure it is installed and in your system's PATH.");
  75. }
  76. public static string FindVsCodeExecutable() => FindExecutable("code");
  77. private static string FindGitExecutable() => FindExecutable("git");
  78. }
  79. }