123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211 |
- // Copyright (c) 2025 TerraByte Inc.
- //
- // This script contains the core business logic for interacting with the Git
- // repository. All public methods are asynchronous and return a promise.
- using System;
- using System.IO;
- using LibGit2Sharp;
- using UnityEngine;
- using Terra.Arbitrator.Promises;
- using System.Collections.Generic;
- namespace Terra.Arbitrator.Services
- {
- public static class GitService
- {
- // Public method that returns the promise
- public static IPromise<List<GitChange>> CompareLocalToRemote()
- {
- return new Promise<List<GitChange>>(CompareExecutor);
- }
-
- // Public method that returns the promise
- public static IPromise<string> CommitAndPush(List<GitChange> changesToCommit, string commitMessage)
- {
- // Use a lambda here to pass arguments to the executor method
- return new Promise<string>((resolve, reject) =>
- CommitAndPushExecutor(resolve, reject, changesToCommit, commitMessage));
- }
-
- // Creates a promise to revert a single file to its HEAD revision.
- public static IPromise<string> ResetFileChanges(GitChange changeToReset)
- {
- return new Promise<string>((resolve, reject) =>
- ResetFileExecutor(resolve, reject, changeToReset));
- }
-
- // Creates a promise to get the diff patch for a single file against HEAD.
- public static IPromise<string> GetFileDiff(GitChange change)
- {
- return new Promise<string>((resolve, reject) => GetFileDiffExecutor(resolve, reject, change));
- }
- /// <summary>
- /// The private logic for the CompareLocalToRemote promise.
- /// This code is executed on a background thread by the Promise constructor.
- /// </summary>
- private static void CompareExecutor(Action<List<GitChange>> resolve, Action<Exception> reject)
- {
- try
- {
- var changes = new List<GitChange>();
- var projectRoot = Directory.GetParent(Application.dataPath)?.FullName;
- using var repo = new Repository(projectRoot);
- var remote = repo.Network.Remotes["origin"];
- if (remote == null) throw new Exception("No remote named 'origin' was found.");
-
- Commands.Fetch(repo, remote.Name, Array.Empty<string>(), new FetchOptions(), "Arbitrator fetch");
-
- var remoteBranch = repo.Head.TrackedBranch;
- if (remoteBranch == null || !remoteBranch.IsRemote)
- {
- throw new Exception($"Current branch '{repo.Head.FriendlyName}' is not tracking a remote branch.");
- }
- var diff = repo.Diff.Compare<TreeChanges>(remoteBranch.Tip.Tree, DiffTargets.Index | DiffTargets.WorkingDirectory);
- foreach (var entry in diff)
- {
- if (entry.Status is ChangeKind.Added or ChangeKind.Deleted or ChangeKind.Modified or ChangeKind.Renamed)
- {
- changes.Add(new GitChange(entry.Path, entry.OldPath, entry.Status));
- }
- }
-
- resolve(changes); // Success
- }
- catch (Exception ex)
- {
- reject(ex); // Failure
- }
- }
- /// <summary>
- /// The private logic for the CommitAndPush promise.
- /// This code is executed on a background thread.
- /// </summary>
- private static void CommitAndPushExecutor(Action<string> resolve, Action<Exception> reject, List<GitChange> changesToCommit, string commitMessage)
- {
- try
- {
- var projectRoot = Directory.GetParent(Application.dataPath)?.FullName;
- using var repo = new Repository(projectRoot);
- var remote = repo.Network.Remotes["origin"];
- if (remote == null) throw new Exception("No remote named 'origin' found.");
-
- Commands.Fetch(repo, remote.Name, Array.Empty<string>(), new FetchOptions(), "Arbitrator pre-push fetch");
- var trackingDetails = repo.Head.TrackingDetails;
- if (trackingDetails.BehindBy > 0)
- {
- throw new Exception($"Push aborted. There are {trackingDetails.BehindBy.Value} incoming changes on the remote. Please pull first.");
- }
- foreach (var change in changesToCommit)
- {
- if (change.Status == ChangeKind.Deleted) Commands.Remove(repo, change.FilePath);
- else Commands.Stage(repo, change.FilePath);
- }
- var author = new Signature("Arbitrator Tool User", "arbitrator@terabyte.com", DateTimeOffset.Now);
- repo.Commit(commitMessage, author, author);
- repo.Network.Push(repo.Head, new PushOptions());
-
- resolve("Successfully committed and pushed changes!"); // Success
- }
- catch (Exception ex)
- {
- reject(ex); // Failure
- }
- }
-
- /// <summary>
- /// The private logic for resetting a file's changes.
- /// This is executed on a background thread.
- /// </summary>
- private static void ResetFileExecutor(Action<string> resolve, Action<Exception> reject, GitChange changeToReset)
- {
- try
- {
- var projectRoot = Directory.GetParent(Application.dataPath)?.FullName;
- using var repo = new Repository(projectRoot);
- // For 'Added' files, checking out doesn't remove them. We need to unstage and delete.
- if (changeToReset.Status == ChangeKind.Added)
- {
- // Unstage the file from the index.
- Commands.Unstage(repo, changeToReset.FilePath);
-
- // And delete it from the working directory.
- if (projectRoot != null)
- {
- var fullPath = Path.Combine(projectRoot, changeToReset.FilePath);
- if (File.Exists(fullPath))
- {
- File.Delete(fullPath);
- }
- }
- }
- else if (changeToReset.Status == ChangeKind.Renamed)
- {
- // 1. Unstage the new path
- Commands.Unstage(repo, changeToReset.FilePath);
- // 2. Delete the new file from the working directory
- if (projectRoot != null)
- {
- var newFullPath = Path.Combine(projectRoot, changeToReset.FilePath);
- if (File.Exists(newFullPath)) File.Delete(newFullPath);
- }
- // 3. Checkout the old path from the HEAD to restore it
- repo.CheckoutPaths(repo.Head.Tip.Sha, new[] { changeToReset.OldFilePath }, new CheckoutOptions { CheckoutModifiers = CheckoutModifiers.Force });
- }
- else
- {
- // For Modified or Deleted files, CheckoutPaths is the correct command to revert them.
- repo.CheckoutPaths(repo.Head.Tip.Sha, new[] { changeToReset.FilePath }, new CheckoutOptions { CheckoutModifiers = CheckoutModifiers.Force });
- }
- resolve($"Successfully reset changes for '{changeToReset.FilePath}'");
- }
- catch (Exception ex)
- {
- reject(ex);
- }
- }
-
- private static void GetFileDiffExecutor(Action<string> resolve, Action<Exception> reject, GitChange change)
- {
- try
- {
- var projectRoot = Directory.GetParent(Application.dataPath)?.FullName;
- using var repo = new Repository(projectRoot);
- // Compare the HEAD tree with the working directory.
- var patch = repo.Diff.Compare<Patch>(repo.Head.Tip.Tree, DiffTargets.WorkingDirectory);
-
- // Find the specific change in the complete patch by its path.
- // For renames, the 'Path' property holds the new path, which is what we need to look up.
- var patchEntry = patch[change.FilePath];
- if (patchEntry == null)
- {
- // This might happen if the change is in the index but not the working dir.
- // Fallback to checking the index as well.
- patch = repo.Diff.Compare<Patch>(repo.Head.Tip.Tree, DiffTargets.Index);
- patchEntry = patch[change.FilePath];
- }
- resolve(patchEntry?.Patch ?? $"No textual changes detected for {change.FilePath}. Status: {change.Status}");
- }
- catch(Exception ex)
- {
- reject(ex);
- }
- }
- }
- }
|