// 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");
}
}