123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114 |
- // 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 UnityEngine.Scripting;
- using System.Threading.Tasks;
- using System.Runtime.InteropServices;
- namespace Terra.Arbitrator.Services
- {
- /// <summary>
- /// An internal helper class for executing Git commands.
- /// It centralizes the logic for finding executables and running them via CliWrap.
- /// </summary>
- [Preserve]
- internal static class GitCommand
- {
- private static string _projectRoot;
- private static string ProjectRoot => _projectRoot ??= Directory.GetParent(Application.dataPath)?.FullName;
- /// <summary>
- /// Runs a git command asynchronously.
- /// </summary>
- /// <param name="execPath">Executable path like git or vs code to perform terminal based command action</param>
- /// <param name="log">A StringBuilder to capture command output for logging.</param>
- /// <param name="progress">A delegate to report real-time standard error lines.</param>
- /// <param name="args">The arguments to pass to the git command.</param>
- /// <param name="acceptableExitCodes">A list of exit codes that should not be treated as errors.</param>
- public static async Task RunAsync(string execPath, StringBuilder log, IProgress<string> progress, string[] args, params int[] acceptableExitCodes)
- {
- var stdOutBuffer = new StringBuilder();
- var stdErrBuffer = new StringBuilder();
- var argumentsString = string.Join(" ", args);
- log?.AppendLine($"\n--- Executing: git {argumentsString} ---");
-
- // Pipe stderr to a delegate that both captures the full output and reports each line for progress.
- var stdErrPipe = PipeTarget.ToDelegate(line => {
- stdErrBuffer.AppendLine(line);
- progress?.Report(line); // Report progress for each line received.
- });
- var command = Cli.Wrap(execPath)
- .WithArguments(args)
- .WithWorkingDirectory(ProjectRoot)
- .WithValidation(CommandResultValidation.None) // We handle validation manually
- | (PipeTarget.ToDelegate(x => stdOutBuffer.Append(x)), stdErrPipe);
- 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}");
- }
- }
-
- public static Task RunGitAsync(StringBuilder log, string[] args, params int[] acceptableExitCodes)
- {
- return RunAsync(FindGitExecutable(), log, null, args, acceptableExitCodes);
- }
-
- public static Task RunGitAsync(StringBuilder log, string[] args, IProgress<string> progress, params int[] acceptableExitCodes)
- {
- return RunAsync(FindGitExecutable(), log, progress, args, acceptableExitCodes);
- }
-
- public static Task RunVsCodeAsync(StringBuilder log, string[] args, params int[] acceptableExitCodes)
- {
- return RunAsync(FindVsCodeExecutable(), log, null, args, acceptableExitCodes);
- }
-
- /// <summary>
- /// Finds the absolute path to a given executable.
- /// </summary>
- 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.");
- }
-
- private static string FindVsCodeExecutable() => FindExecutable("code");
- private static string FindGitExecutable() => FindExecutable("git");
- }
- }
|