// Copyright (c) 2025 TerraByte Inc. // // A new helper class that abstracts away the boilerplate of running external // command-line processes, specifically Git and VS Code, using CliWrap. using System; using CliWrap; using System.IO; using System.Linq; using UnityEngine; using System.Text; using System.Threading.Tasks; using System.Runtime.InteropServices; namespace Terra.Arbitrator.Services { /// /// An internal helper class for executing Git commands. /// It centralizes the logic for finding executables and running them via CliWrap. /// internal static class GitCommand { private static string _projectRoot; private static string ProjectRoot => _projectRoot ??= Directory.GetParent(Application.dataPath)?.FullName; /// /// Runs a git command asynchronously. /// /// A StringBuilder to capture command output for logging. /// The arguments to pass to the git command. /// A list of exit codes that should not be treated as errors. public static async Task RunAsync(StringBuilder log, string[] args, params int[] acceptableExitCodes) { var stdOutBuffer = new StringBuilder(); var stdErrBuffer = new StringBuilder(); var argumentsString = string.Join(" ", args); log?.AppendLine($"\n--- Executing: git {argumentsString} ---"); var command = Cli.Wrap(FindGitExecutable()) .WithArguments(args) .WithWorkingDirectory(ProjectRoot) .WithValidation(CommandResultValidation.None) // We handle validation manually | (PipeTarget.ToDelegate(x => stdOutBuffer.Append(x)), PipeTarget.ToDelegate(x => stdErrBuffer.Append(x))); var result = await command.ExecuteAsync(); log?.AppendLine($"Exit Code: {result.ExitCode}"); if (stdOutBuffer.Length > 0) log?.AppendLine($"StdOut: {stdOutBuffer}"); if (stdErrBuffer.Length > 0) log?.AppendLine($"StdErr: {stdErrBuffer}"); // Default to 0 if no specific codes are provided if (acceptableExitCodes.Length == 0) { acceptableExitCodes = new[] { 0 }; } if (!acceptableExitCodes.Contains(result.ExitCode)) { throw new Exception($"Command 'git {argumentsString}' failed with unexpected exit code {result.ExitCode}. Error: {stdErrBuffer}"); } } /// /// Finds the absolute path to a given executable. /// private static string FindExecutable(string name) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { // CliWrap handles PATH search on Windows automatically. return name; } // For macOS/Linux, we need to be more explicit due to Unity's sandboxing. string[] searchPaths = { "/usr/local/bin", "/usr/bin", "/bin", "/opt/homebrew/bin" }; foreach (var path in searchPaths) { var fullPath = Path.Combine(path, name); if (File.Exists(fullPath)) { return fullPath; } } throw new FileNotFoundException($"Could not find executable '{name}'. Please ensure it is installed and in your system's PATH."); } public static string FindVsCodeExecutable() => FindExecutable("code"); private static string FindGitExecutable() => FindExecutable("git"); } }