|
@@ -39,6 +39,7 @@ namespace Terra.Arbitrator.Services
|
|
|
{
|
|
|
private static string _projectRoot;
|
|
|
private static string ProjectRoot => _projectRoot ??= MainThreadDataCache.ProjectRoot;
|
|
|
+ private const string StashMessage = "Better Git Stash";
|
|
|
|
|
|
private static string GetAuthenticatedRemoteUrl()
|
|
|
{
|
|
@@ -63,6 +64,274 @@ namespace Terra.Arbitrator.Services
|
|
|
return authenticatedUrl;
|
|
|
}
|
|
|
|
|
|
+ public static void HasStashExecutor(Action<bool> resolve, Action<Exception> reject)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ using var repo = new Repository(ProjectRoot);
|
|
|
+ var stashExists = repo.Stashes.Any(s => s.Message.Contains(StashMessage));
|
|
|
+ resolve(stashExists);
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ reject(ex);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public static async void CreateOrOverwriteStashExecutor(Action<string> resolve, Action<Exception> reject, List<GitChange> changes)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ var log = new StringBuilder();
|
|
|
+
|
|
|
+ using (var repo = new Repository(ProjectRoot))
|
|
|
+ {
|
|
|
+ var existingStash = repo.Stashes.FirstOrDefault(s => s.Message.Contains(StashMessage));
|
|
|
+ if (existingStash != null)
|
|
|
+ {
|
|
|
+ repo.Stashes.Remove(repo.Stashes.ToList().IndexOf(existingStash));
|
|
|
+ log.AppendLine("Dropped existing 'Better Git Stash'.");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ var untrackedFiles = new List<string>();
|
|
|
+
|
|
|
+ using (var repo = new Repository(ProjectRoot))
|
|
|
+ {
|
|
|
+ foreach (var change in changes)
|
|
|
+ {
|
|
|
+ var statusEntry = repo.RetrieveStatus(change.FilePath);
|
|
|
+ if (statusEntry == FileStatus.NewInWorkdir)
|
|
|
+ {
|
|
|
+ untrackedFiles.Add(change.FilePath);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (untrackedFiles.Any())
|
|
|
+ {
|
|
|
+ foreach (var file in untrackedFiles)
|
|
|
+ {
|
|
|
+ await GitCommand.RunGitAsync(log, new[] { "add", file });
|
|
|
+ }
|
|
|
+ log.AppendLine($"Staged {untrackedFiles.Count} untracked files.");
|
|
|
+ }
|
|
|
+
|
|
|
+ var allFiles = changes.Select(c => c.FilePath).ToList();
|
|
|
+ if (allFiles.Any())
|
|
|
+ {
|
|
|
+ var stashArgs = new List<string> { "stash", "push", "-m", StashMessage, "--" };
|
|
|
+ stashArgs.AddRange(allFiles);
|
|
|
+
|
|
|
+ await GitCommand.RunGitAsync(log, stashArgs.ToArray());
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ throw new Exception("No files to stash.");
|
|
|
+ }
|
|
|
+
|
|
|
+ resolve("Successfully created new 'Better Git Stash'.");
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ Debug.LogException(ex);
|
|
|
+ reject(ex);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public static void DropStashExecutor(Action<string> resolve, Action<Exception> reject)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ using var repo = new Repository(ProjectRoot);
|
|
|
+ var stash = repo.Stashes.FirstOrDefault(s => s.Message.Contains(StashMessage));
|
|
|
+ if (stash != null)
|
|
|
+ {
|
|
|
+ repo.Stashes.Remove(repo.Stashes.ToList().IndexOf(stash));
|
|
|
+ resolve("'Better Git Stash' has been discarded.");
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ resolve("No 'Better Git Stash' found to discard.");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ reject(ex);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public static void GetStashedFilesExecutor(Action<List<GitChange>> resolve, Action<Exception> reject)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ using var repo = new Repository(ProjectRoot);
|
|
|
+ var stash = repo.Stashes.FirstOrDefault(s => s.Message.Contains(StashMessage));
|
|
|
+ if (stash == null)
|
|
|
+ {
|
|
|
+ resolve(new List<GitChange>());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ var changes = new List<GitChange>();
|
|
|
+
|
|
|
+ var stashChanges = repo.Diff.Compare<TreeChanges>(stash.Base.Tree, stash.WorkTree.Tree);
|
|
|
+ foreach (var change in stashChanges)
|
|
|
+ {
|
|
|
+ changes.Add(new GitChange(change.Path, change.OldPath, change.Status));
|
|
|
+ }
|
|
|
+
|
|
|
+ var indexChanges = repo.Diff.Compare<TreeChanges>(stash.Base.Tree, stash.Index.Tree);
|
|
|
+ foreach (var change in indexChanges)
|
|
|
+ {
|
|
|
+ if (changes.All(c => c.FilePath != change.Path))
|
|
|
+ {
|
|
|
+ changes.Add(new GitChange(change.Path, change.OldPath, change.Status));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ resolve(changes);
|
|
|
+ }
|
|
|
+ catch(Exception ex)
|
|
|
+ {
|
|
|
+ reject(ex);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public static async void DiffStashedFileExecutor(Action<string> resolve, Action<Exception> reject, GitChange change)
|
|
|
+ {
|
|
|
+ string fileAPath = null;
|
|
|
+ string fileBPath = null;
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ using var repo = new Repository(ProjectRoot);
|
|
|
+ var stash = repo.Stashes.FirstOrDefault(s => s.Message.Contains(StashMessage));
|
|
|
+ if (stash == null) throw new Exception("'Better Git Stash' not found.");
|
|
|
+
|
|
|
+ var stashedTree = stash.WorkTree.Tree;
|
|
|
+ var baseTree = stash.Base.Tree;
|
|
|
+
|
|
|
+ switch (change.Status)
|
|
|
+ {
|
|
|
+ case ChangeKind.Added:
|
|
|
+ fileAPath = CreateTempFileWithContent("", "empty");
|
|
|
+ fileBPath = CreateTempFileFromBlob(stashedTree[change.FilePath]?.Target as Blob, change.FilePath);
|
|
|
+ break;
|
|
|
+
|
|
|
+ case ChangeKind.Deleted:
|
|
|
+ fileAPath = CreateTempFileFromBlob(baseTree[change.FilePath]?.Target as Blob, change.FilePath);
|
|
|
+ fileBPath = CreateTempFileWithContent("", change.FilePath);
|
|
|
+ break;
|
|
|
+
|
|
|
+ case ChangeKind.Renamed:
|
|
|
+ fileAPath = CreateTempFileFromBlob(baseTree[change.OldFilePath]?.Target as Blob, change.OldFilePath);
|
|
|
+ fileBPath = Path.Combine(ProjectRoot, change.FilePath);
|
|
|
+ break;
|
|
|
+
|
|
|
+ default:
|
|
|
+ fileAPath = CreateTempFileFromBlob(stashedTree[change.FilePath]?.Target as Blob, change.FilePath);
|
|
|
+ fileBPath = Path.Combine(ProjectRoot, change.FilePath);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!File.Exists(fileBPath))
|
|
|
+ {
|
|
|
+ fileBPath = CreateTempFileWithContent("", Path.GetFileName(fileBPath));
|
|
|
+ }
|
|
|
+
|
|
|
+ await GitCommand.RunVsCodeAsync(new StringBuilder(), new[] { "--diff", fileAPath, fileBPath });
|
|
|
+ resolve("Launched external diff tool.");
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ reject(ex);
|
|
|
+ }
|
|
|
+ finally
|
|
|
+ {
|
|
|
+ if (fileAPath != null && fileAPath.Contains(Path.GetTempPath())) File.Delete(fileAPath);
|
|
|
+ if (fileBPath != null && fileBPath.Contains(Path.GetTempPath())) File.Delete(fileBPath);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public static void AnalyzeStashConflictsExecutor(Action<PullAnalysisResult> resolve, Action<Exception> reject)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ using var repo = new Repository(ProjectRoot);
|
|
|
+ var stash = repo.Stashes.FirstOrDefault(s => s.Message.Contains(StashMessage));
|
|
|
+ if (stash == null)
|
|
|
+ {
|
|
|
+ resolve(new PullAnalysisResult(new List<string>()));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ var stashedChanges = new HashSet<string>(repo.Diff.Compare<TreeChanges>(stash.Base.Tree, stash.Index.Tree).Select(c => c.Path));
|
|
|
+
|
|
|
+ var localChanges = new HashSet<string>(repo.RetrieveStatus().Where(s => s.State != FileStatus.Ignored).Select(s => s.FilePath));
|
|
|
+
|
|
|
+ var conflictingFiles = stashedChanges.Intersect(localChanges).ToList();
|
|
|
+
|
|
|
+ resolve(new PullAnalysisResult(conflictingFiles));
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ reject(ex);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public static async void ApplyStashAndOverwriteExecutor(Action<string> resolve, Action<Exception> reject, List<GitChange> resolutions)
|
|
|
+ {
|
|
|
+ var tempFiles = new Dictionary<string, string>();
|
|
|
+ var log = new StringBuilder();
|
|
|
+ try
|
|
|
+ {
|
|
|
+ foreach (var resolution in resolutions.Where(r => r.Resolution == GitChange.ConflictResolution.Mine))
|
|
|
+ {
|
|
|
+ var fullPath = Path.Combine(ProjectRoot, resolution.FilePath);
|
|
|
+ if (File.Exists(fullPath))
|
|
|
+ {
|
|
|
+ var tempPath = Path.GetTempFileName();
|
|
|
+ File.Copy(fullPath, tempPath, true);
|
|
|
+ tempFiles[resolution.FilePath] = tempPath;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ using (var repo = new Repository(ProjectRoot))
|
|
|
+ {
|
|
|
+ var filesToReset = resolutions.Where(r => r.Resolution != GitChange.ConflictResolution.None).Select(r => r.FilePath).ToArray();
|
|
|
+ if (filesToReset.Any())
|
|
|
+ {
|
|
|
+ repo.CheckoutPaths(repo.Head.Tip.Sha, filesToReset, new CheckoutOptions { CheckoutModifiers = CheckoutModifiers.Force });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ await GitCommand.RunGitAsync(log, new[] { "stash", "apply" });
|
|
|
+
|
|
|
+ foreach (var entry in tempFiles)
|
|
|
+ {
|
|
|
+ var finalPath = Path.Combine(ProjectRoot, entry.Key);
|
|
|
+ File.Copy(entry.Value, finalPath, true);
|
|
|
+ await GitCommand.RunGitAsync(log, new[] { "add", entry.Key });
|
|
|
+ }
|
|
|
+
|
|
|
+ await GitCommand.RunGitAsync(log, new[] { "stash", "drop" });
|
|
|
+
|
|
|
+ resolve("Stash applied successfully and has been dropped.");
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ reject(new Exception($"Failed to apply stash. You may need to resolve conflicts manually. Details: {ex.Message}"));
|
|
|
+ }
|
|
|
+ finally
|
|
|
+ {
|
|
|
+ foreach (var tempFile in tempFiles.Values.Where(File.Exists))
|
|
|
+ {
|
|
|
+ File.Delete(tempFile);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
public static void GetBranchDataExecutor(Action<BranchData> resolve, Action<Exception> reject)
|
|
|
{
|
|
|
try
|
|
@@ -630,5 +899,19 @@ namespace Terra.Arbitrator.Services
|
|
|
reject(ex);
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ private static string CreateTempFileFromBlob(Blob blob, string fallbackFileName)
|
|
|
+ {
|
|
|
+ var content = blob?.GetContentText() ?? "";
|
|
|
+ return CreateTempFileWithContent(content, fallbackFileName);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static string CreateTempFileWithContent(string content, string originalFileName)
|
|
|
+ {
|
|
|
+ var tempFileName = $"{Path.GetFileName(originalFileName)}-{Path.GetRandomFileName()}";
|
|
|
+ var tempPath = Path.Combine(Path.GetTempPath(), tempFileName);
|
|
|
+ File.WriteAllText(tempPath, content);
|
|
|
+ return tempPath;
|
|
|
+ }
|
|
|
}
|
|
|
}
|