GitService.cs 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. // Copyright (c) 2025 TerraByte Inc.
  2. //
  3. // This script contains the core business logic for interacting with the Git
  4. // repository. All public methods are asynchronous and return a promise.
  5. using System;
  6. using System.IO;
  7. using UnityEngine;
  8. using LibGit2Sharp;
  9. using Terra.Arbitrator.Promises;
  10. using System.Collections.Generic;
  11. namespace Terra.Arbitrator
  12. {
  13. public static class GitService
  14. {
  15. // Public method that returns the promise
  16. public static IPromise<List<GitChange>> CompareLocalToRemote()
  17. {
  18. return new Promise<List<GitChange>>(CompareExecutor);
  19. }
  20. // Public method that returns the promise
  21. public static IPromise<string> CommitAndPush(List<GitChange> changesToCommit, string commitMessage)
  22. {
  23. // Use a lambda here to pass arguments to the executor method
  24. return new Promise<string>((resolve, reject) =>
  25. CommitAndPushExecutor(resolve, reject, changesToCommit, commitMessage));
  26. }
  27. // Creates a promise to revert a single file to its HEAD revision.
  28. public static IPromise<string> ResetFileChanges(GitChange changeToReset)
  29. {
  30. return new Promise<string>((resolve, reject) =>
  31. ResetFileExecutor(resolve, reject, changeToReset));
  32. }
  33. // Creates a promise to get the diff patch for a single file against HEAD.
  34. public static IPromise<string> GetFileDiff(GitChange change)
  35. {
  36. return new Promise<string>((resolve, reject) => GetFileDiffExecutor(resolve, reject, change));
  37. }
  38. /// <summary>
  39. /// The private logic for the CompareLocalToRemote promise.
  40. /// This code is executed on a background thread by the Promise constructor.
  41. /// </summary>
  42. private static void CompareExecutor(Action<List<GitChange>> resolve, Action<Exception> reject)
  43. {
  44. try
  45. {
  46. var changes = new List<GitChange>();
  47. var projectRoot = Directory.GetParent(Application.dataPath)?.FullName;
  48. using var repo = new Repository(projectRoot);
  49. var remote = repo.Network.Remotes["origin"];
  50. if (remote == null) throw new Exception("No remote named 'origin' was found.");
  51. Commands.Fetch(repo, remote.Name, Array.Empty<string>(), new FetchOptions(), "Arbitrator fetch");
  52. var remoteBranch = repo.Head.TrackedBranch;
  53. if (remoteBranch == null || !remoteBranch.IsRemote)
  54. {
  55. throw new Exception($"Current branch '{repo.Head.FriendlyName}' is not tracking a remote branch.");
  56. }
  57. var diff = repo.Diff.Compare<TreeChanges>(remoteBranch.Tip.Tree, DiffTargets.Index | DiffTargets.WorkingDirectory);
  58. foreach (var entry in diff)
  59. {
  60. if (entry.Status == ChangeKind.Added || entry.Status == ChangeKind.Deleted || entry.Status == ChangeKind.Modified)
  61. {
  62. changes.Add(new GitChange(entry.Path, entry.Status));
  63. }
  64. }
  65. resolve(changes); // Success
  66. }
  67. catch (Exception ex)
  68. {
  69. reject(ex); // Failure
  70. }
  71. }
  72. /// <summary>
  73. /// The private logic for the CommitAndPush promise.
  74. /// This code is executed on a background thread.
  75. /// </summary>
  76. private static void CommitAndPushExecutor(Action<string> resolve, Action<Exception> reject, List<GitChange> changesToCommit, string commitMessage)
  77. {
  78. try
  79. {
  80. var projectRoot = Directory.GetParent(Application.dataPath)?.FullName;
  81. using var repo = new Repository(projectRoot);
  82. var remote = repo.Network.Remotes["origin"];
  83. if (remote == null) throw new Exception("No remote named 'origin' found.");
  84. Commands.Fetch(repo, remote.Name, Array.Empty<string>(), new FetchOptions(), "Arbitrator pre-push fetch");
  85. var trackingDetails = repo.Head.TrackingDetails;
  86. if (trackingDetails.BehindBy > 0)
  87. {
  88. throw new Exception($"Push aborted. There are {trackingDetails.BehindBy.Value} incoming changes on the remote. Please pull first.");
  89. }
  90. foreach (var change in changesToCommit)
  91. {
  92. if (change.Status == ChangeKind.Deleted) Commands.Remove(repo, change.FilePath);
  93. else Commands.Stage(repo, change.FilePath);
  94. }
  95. var author = new Signature("Arbitrator Tool User", "arbitrator@terabyte.com", DateTimeOffset.Now);
  96. repo.Commit(commitMessage, author, author);
  97. repo.Network.Push(repo.Head, new PushOptions());
  98. resolve("Successfully committed and pushed changes!"); // Success
  99. }
  100. catch (Exception ex)
  101. {
  102. reject(ex); // Failure
  103. }
  104. }
  105. /// <summary>
  106. /// The private logic for resetting a file's changes.
  107. /// This is executed on a background thread.
  108. /// </summary>
  109. private static void ResetFileExecutor(Action<string> resolve, Action<Exception> reject, GitChange changeToReset)
  110. {
  111. try
  112. {
  113. var projectRoot = Directory.GetParent(Application.dataPath)?.FullName;
  114. using var repo = new Repository(projectRoot);
  115. // For 'Added' files, checking out doesn't remove them. We need to unstage and delete.
  116. if (changeToReset.Status == ChangeKind.Added)
  117. {
  118. // Unstage the file from the index.
  119. Commands.Unstage(repo, changeToReset.FilePath);
  120. // And delete it from the working directory.
  121. if (projectRoot != null)
  122. {
  123. var fullPath = Path.Combine(projectRoot, changeToReset.FilePath);
  124. if (File.Exists(fullPath))
  125. {
  126. File.Delete(fullPath);
  127. }
  128. }
  129. }
  130. else
  131. {
  132. // For Modified or Deleted files, CheckoutPaths is the correct command to revert them.
  133. repo.CheckoutPaths(repo.Head.Tip.Sha, new[] { changeToReset.FilePath }, new CheckoutOptions { CheckoutModifiers = CheckoutModifiers.Force });
  134. }
  135. resolve($"Successfully reset changes for '{changeToReset.FilePath}'");
  136. }
  137. catch (Exception ex)
  138. {
  139. reject(ex);
  140. }
  141. }
  142. private static void GetFileDiffExecutor(Action<string> resolve, Action<Exception> reject, GitChange change)
  143. {
  144. try
  145. {
  146. var projectRoot = Directory.GetParent(Application.dataPath)?.FullName;
  147. using var repo = new Repository(projectRoot);
  148. // Use Compare() against the HEAD commit to get the patch for the specific file.
  149. var diff = repo.Diff.Compare<Patch>(new[] { change.FilePath }, true);
  150. resolve(diff.Content);
  151. }
  152. catch(Exception ex)
  153. {
  154. reject(ex);
  155. }
  156. }
  157. }
  158. }