// Copyright (c) 2025 TerraByte Inc. // // A new internal class that contains the concrete implementation logic for // all Git operations. This separates the "how" from the "what" defined // in the public GitService API. using System; using System.IO; using System.Linq; using UnityEngine; using System.Text; using LibGit2Sharp; using System.Globalization; using System.ComponentModel; using UnityEngine.Scripting; using System.Threading.Tasks; using Terra.Arbitrator.Settings; using System.Collections.Generic; using System.Text.RegularExpressions; namespace Terra.Arbitrator.Services { /// /// A simple data container for branch information. /// [Preserve] public class BranchData { public string CurrentBranch { get; set; } public List AllBranches { get; set; } } /// /// Contains the promise executor methods for all Git operations. /// This is an internal implementation detail and is not exposed publicly. /// [Preserve] internal static class GitExecutors { private static string _projectRoot; private static string ProjectRoot => _projectRoot ??= MainThreadDataCache.ProjectRoot; private const string StashMessage = "Better Git Stash"; private static string GetAuthenticatedRemoteUrl() { var authUsername = BetterGitSettings.AuthUsername; var authPassword = BetterGitSettings.AuthPassword; if (string.IsNullOrEmpty(authUsername) || string.IsNullOrEmpty(authPassword)) { return "origin"; } using var repo = new Repository(ProjectRoot); var remote = repo.Network.Remotes["origin"]; if (remote == null) throw new Exception("No remote named 'origin' found."); var originalUrl = remote.Url; var authenticatedUrl = Regex.Replace(originalUrl, "://", $"://{Uri.EscapeDataString(authUsername)}:{Uri.EscapeDataString(authPassword)}@"); return authenticatedUrl; } public static void HasStashExecutor(Action resolve, Action 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 resolve, Action reject, List 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(); 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 { "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 resolve, Action 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> resolve, Action reject) { try { using var repo = new Repository(ProjectRoot); var stash = repo.Stashes.FirstOrDefault(s => s.Message.Contains(StashMessage)); if (stash == null) { resolve(new List()); return; } var changes = new List(); var stashChanges = repo.Diff.Compare(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(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 resolve, Action 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 resolve, Action 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())); return; } var workTreeChanges = repo.Diff.Compare(stash.Base.Tree, stash.WorkTree.Tree).Select(c => c.Path); var indexChanges = repo.Diff.Compare(stash.Base.Tree, stash.Index.Tree).Select(c => c.Path); var stashedChanges = new HashSet(workTreeChanges.Union(indexChanges)); var localChanges = new HashSet(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 resolve, Action reject, List resolutions) { var tempFiles = new Dictionary(); 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 resolve, Action reject) { try { using var repo = new Repository(ProjectRoot); var data = new BranchData { CurrentBranch = repo.Head.FriendlyName, AllBranches = repo.Branches .Where(b => !b.FriendlyName.Contains("HEAD")) .Select(b => b.FriendlyName.Replace("origin/", "")) .Distinct() .OrderBy(name => name) .ToList() }; resolve(data); } catch(Exception ex) { reject(ex); } } public static async void SwitchBranchExecutor(Action resolve, Action reject, string branchName) { try { var log = new StringBuilder(); await GitCommand.RunGitAsync(log, new[] { "checkout", branchName }); resolve($"Successfully switched to branch '{branchName}'."); } catch (Exception ex) { reject(ex); } } public static async void ResetAndSwitchBranchExecutor(Action resolve, Action reject, string branchName) { try { var log = new StringBuilder(); await GitCommand.RunGitAsync(log, new[] { "reset", "--hard", "HEAD" }); await GitCommand.RunGitAsync(log, new[] { "clean", "-fd" }); await GitCommand.RunGitAsync(log, new[] { "checkout", branchName }); resolve($"Discarded local changes and switched to branch '{branchName}'."); } catch (Exception ex) { reject(ex); } } /// /// Synchronous helper to get a GitChange object for a single file. /// This is public so it can be called by the GitService wrapper. /// public static GitChange GetChangeForFile(string filePath) { try { using var repo = new Repository(ProjectRoot); if (repo.Index.Conflicts.Any(c => c.Ours.Path == filePath)) { return new GitChange(filePath, null, ChangeKind.Conflicted); } var statusEntry = repo.RetrieveStatus(filePath); return statusEntry switch { FileStatus.NewInWorkdir or FileStatus.NewInIndex => new GitChange(filePath, null, ChangeKind.Added), FileStatus.ModifiedInWorkdir or FileStatus.ModifiedInIndex => new GitChange(filePath, null, ChangeKind.Modified), FileStatus.DeletedFromWorkdir or FileStatus.DeletedFromIndex => new GitChange(filePath, null, ChangeKind.Deleted), FileStatus.RenamedInWorkdir or FileStatus.RenamedInIndex => new GitChange(filePath, null, ChangeKind.Renamed), _ => null }; } catch { return null; } // Suppress errors if repo is in a unique state } // --- Promise Executor Implementations --- public static async void GetUpstreamAheadByExecutor(Action resolve, Action reject, Action onProgress) { try { string refSpec; using (var tempRepo = new Repository(ProjectRoot)) { var currentBranch = tempRepo.Head; if (currentBranch.TrackedBranch == null) { resolve(0); return; } var branchName = currentBranch.FriendlyName; var remoteName = currentBranch.TrackedBranch.RemoteName; refSpec = $"{branchName}:refs/remotes/{remoteName}/{branchName}"; } var authenticatedUrl = GetAuthenticatedRemoteUrl(); var progressReporter = new Progress(line => ParseProgress(line, onProgress)); await GitCommand.RunGitAsync(new StringBuilder(), new[] { "fetch", authenticatedUrl, refSpec, "--progress" }, progressReporter); using var repo = new Repository(ProjectRoot); resolve(repo.Head.TrackingDetails.BehindBy); } catch (Exception ex) { if (ex.Message.Contains("is not tracking a remote branch")) { resolve(null); } else { reject(ex); } } } public static void GetLocalStatusExecutor(Action> resolve, Action reject) { try { var changes = new List(); using var repo = new Repository(ProjectRoot); var conflictedPaths = new HashSet(repo.Index.Conflicts.Select(c => c.Ours.Path)); var statusOptions = new StatusOptions { IncludeUntracked = true, RecurseUntrackedDirs = true, DetectRenamesInIndex = true, DetectRenamesInWorkDir = true, IncludeIgnored = false, IncludeUnaltered = false }; foreach (var entry in repo.RetrieveStatus(statusOptions)) { if (conflictedPaths.Contains(entry.FilePath)) { if (changes.All(c => c.FilePath != entry.FilePath)) { changes.Add(new GitChange(entry.FilePath, null, ChangeKind.Conflicted)); } continue; } switch(entry.State) { case FileStatus.ModifiedInWorkdir | FileStatus.RenamedInWorkdir: changes.Add(new GitChange(entry.FilePath, null, ChangeKind.Modified)); break; case FileStatus.NewInWorkdir: case FileStatus.NewInIndex: changes.Add(new GitChange(entry.FilePath, null, ChangeKind.Added)); break; case FileStatus.ModifiedInWorkdir: case FileStatus.ModifiedInIndex: changes.Add(new GitChange(entry.FilePath, null, ChangeKind.Modified)); break; case FileStatus.DeletedFromWorkdir: case FileStatus.DeletedFromIndex: changes.Add(new GitChange(entry.FilePath, null, ChangeKind.Deleted)); break; case FileStatus.RenamedInWorkdir: case FileStatus.RenamedInIndex: var renameDetails = entry.HeadToIndexRenameDetails ?? entry.IndexToWorkDirRenameDetails; changes.Add(renameDetails != null ? new GitChange(renameDetails.NewFilePath, renameDetails.OldFilePath, ChangeKind.Renamed) : new GitChange(entry.FilePath, "Unknown", ChangeKind.Renamed)); break; } } resolve(changes); } catch (Exception ex) { reject(ex); } } public static async void CommitAndPushExecutor(Action resolve, Action reject, List changesToCommit, string commitMessage, string username, string email, Action onProgress) { try { if (string.IsNullOrWhiteSpace(email)) { throw new Exception("Author email is missing. Please set your email address in Project Settings > Better Git."); } var authenticatedUrl = GetAuthenticatedRemoteUrl(); await GitCommand.RunGitAsync(new StringBuilder(), new[] { "fetch", authenticatedUrl }); using (var repo = new Repository(ProjectRoot)) { var remote = repo.Network.Remotes["origin"]; if (remote == null) throw new Exception("No remote named 'origin' found."); 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."); } var pathsToStage = new List(); foreach (var change in changesToCommit) { switch (change.Status) { case ChangeKind.Deleted: Commands.Remove(repo, change.FilePath); break; case ChangeKind.Renamed: Commands.Remove(repo, change.OldFilePath); pathsToStage.Add(change.FilePath); break; default: pathsToStage.Add(change.FilePath); break; } } if (pathsToStage.Any()) Commands.Stage(repo, pathsToStage); var status = repo.RetrieveStatus(); if (!status.IsDirty) throw new Exception("No effective changes were staged to commit."); var author = new Signature(username, email, DateTimeOffset.Now); repo.Commit(commitMessage, author, author); } var progressReporter = new Progress(line => ParseProgress(line, onProgress)); using var tempRepo = new Repository(ProjectRoot); var currentBranch = tempRepo.Head.FriendlyName; await GitCommand.RunGitAsync(new StringBuilder(), new[] { "push", "--progress", authenticatedUrl, $"HEAD:{currentBranch}" }, progressReporter, 0, 141); resolve("Successfully committed and pushed changes!"); } catch (Exception ex) { var errorMessage = ex.InnerException?.Message ?? ex.Message; reject(new Exception(errorMessage)); } } private static void ParseProgress(string line, Action onProgress) { if (onProgress == null || string.IsNullOrWhiteSpace(line)) return; line = line.Trim(); var parts = line.Split(new[] { ':' }, 2); if (parts.Length < 2) return; var action = parts[0]; var progressPart = parts[1]; var percentIndex = progressPart.IndexOf('%'); if (percentIndex == -1) return; var percentString = progressPart[..percentIndex].Trim(); if (!float.TryParse(percentString, NumberStyles.Any, CultureInfo.InvariantCulture, out var percentage)) return; var progressValue = percentage / 100.0f; onProgress(progressValue, $"{action}..."); } public static void ResetFileExecutor(Action resolve, Action reject, GitChange changeToReset) { try { using var repo = new Repository(ProjectRoot); switch (changeToReset.Status) { case ChangeKind.Added: { Commands.Unstage(repo, changeToReset.FilePath); var fullPath = Path.Combine(ProjectRoot, changeToReset.FilePath); if (File.Exists(fullPath)) { File.Delete(fullPath); } break; } case ChangeKind.Renamed: { Commands.Unstage(repo, changeToReset.FilePath); var newFullPath = Path.Combine(ProjectRoot, changeToReset.FilePath); if (File.Exists(newFullPath)) File.Delete(newFullPath); repo.CheckoutPaths(repo.Head.Tip.Sha, new[] { changeToReset.OldFilePath }, new CheckoutOptions { CheckoutModifiers = CheckoutModifiers.Force }); break; } default: repo.CheckoutPaths(repo.Head.Tip.Sha, new[] { changeToReset.FilePath }, new CheckoutOptions { CheckoutModifiers = CheckoutModifiers.Force }); break; } resolve($"Successfully reset changes for '{changeToReset.FilePath}'"); } catch (Exception ex) { reject(ex); } } public static async void LaunchExternalDiffExecutor(Action resolve, Action reject, GitChange change) { string fileAPath = null; // Before string fileBPath = null; // After try { using var repo = new Repository(ProjectRoot); string GetFileContentFromHead(string path) { var blob = repo.Head.Tip[path]?.Target as Blob; return blob?.GetContentText() ?? ""; } string CreateTempFile(string originalPath, string content) { var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + Path.GetExtension(originalPath)); File.WriteAllText(tempPath, content); return tempPath; } switch (change.Status) { case ChangeKind.Added: fileAPath = CreateTempFile(change.FilePath, ""); fileBPath = Path.Combine(ProjectRoot, change.FilePath); break; case ChangeKind.Deleted: fileAPath = CreateTempFile(change.FilePath, GetFileContentFromHead(change.FilePath)); fileBPath = CreateTempFile(change.FilePath, ""); break; case ChangeKind.Renamed: fileAPath = CreateTempFile(change.OldFilePath, GetFileContentFromHead(change.OldFilePath)); fileBPath = Path.Combine(ProjectRoot, change.FilePath); break; default: // Modified fileAPath = CreateTempFile(change.FilePath, GetFileContentFromHead(change.FilePath)); fileBPath = Path.Combine(ProjectRoot, change.FilePath); break; } await GitCommand.RunVsCodeAsync(new StringBuilder(), new[] { "--diff", fileAPath, fileBPath }); resolve("Launched external diff tool."); } catch(Win32Exception ex) { reject(new Exception("Could not launch VS Code. Ensure it is installed and the 'code' command is available in your system's PATH.", ex)); } catch(Exception ex) { reject(ex); } finally { try { if (fileAPath != null && fileAPath.Contains(Path.GetTempPath()) && File.Exists(fileAPath)) File.Delete(fileAPath); if (fileBPath != null && fileBPath.Contains(Path.GetTempPath()) && File.Exists(fileBPath)) File.Delete(fileBPath); } catch(Exception cleanupEx) { Debug.LogError($"Failed to clean up temporary diff files: {cleanupEx.Message}"); } } } public static void FileLevelConflictCheckExecutor(Action resolve, Action reject) { try { using var repo = new Repository(ProjectRoot); var result = AnalyzePullConflictsInternal(repo).Result; resolve(result); } catch (Exception ex) { reject(ex); } } private static Task AnalyzePullConflictsInternal(Repository repo) { var remote = repo.Network.Remotes["origin"]; if (remote == null) throw new Exception("No remote named 'origin' was found."); var localBranch = repo.Head; var remoteBranch = repo.Head.TrackedBranch; if (remoteBranch == null) throw new Exception("Current branch is not tracking a remote branch."); var mergeBase = repo.ObjectDatabase.FindMergeBase(localBranch.Tip, remoteBranch.Tip); if (mergeBase == null) throw new Exception("Could not find a common ancestor."); var theirChanges = new HashSet(repo.Diff.Compare(mergeBase.Tree, remoteBranch.Tip.Tree).Select(c => c.Path)); var ourChanges = new HashSet(repo.Diff.Compare(mergeBase.Tree, localBranch.Tip.Tree).Select(c => c.Path)); foreach (var statusEntry in repo.RetrieveStatus()) ourChanges.Add(statusEntry.FilePath); return Task.FromResult(new PullAnalysisResult(ourChanges.Where(theirChanges.Contains).ToList())); } public static async void SafePullExecutor(Action resolve, Action reject) { try { var authenticatedUrl = GetAuthenticatedRemoteUrl(); var log = new StringBuilder(); string currentBranchName; using (var repo = new Repository(ProjectRoot)) { currentBranchName = repo.Head.FriendlyName; } await GitCommand.RunGitAsync(log, new[] { "pull", "--no-rebase", authenticatedUrl, currentBranchName }, 0, 141); resolve(log.ToString()); } catch (Exception ex) { reject(ex); } } public static async void PullAndOverwriteExecutor(Action resolve, Action reject, List resolutions) { var tempFiles = new Dictionary(); var log = new StringBuilder(); try { foreach (var resolution in resolutions.Where(r => r.Resolution == GitChange.ConflictResolution.Mine)) { var fullPath = Path.Combine(ProjectRoot, resolution.FilePath); 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.Length > 0) { repo.CheckoutPaths(repo.Head.Tip.Sha, filesToReset, new CheckoutOptions { CheckoutModifiers = CheckoutModifiers.Force }); } } await GitCommand.RunGitAsync(log, new[] { "pull", "--no-rebase" }, 0, 1, 141); 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 }); } var unresolvedFiles = resolutions.Where(r => r.Resolution == GitChange.ConflictResolution.None).ToList(); if (unresolvedFiles.Any()) { var fileList = string.Join(", ", unresolvedFiles.Select(f => f.FilePath)); resolve($"Pull completed with unresolved conflicts in: {fileList}. Please resolve them manually."); } else { resolve("Pull successful. Your chosen local changes have been preserved."); } } catch (Exception ex) { reject(ex); } finally { foreach (var tempFile in tempFiles.Values.Where(File.Exists)) { File.Delete(tempFile); } } } public static async void LaunchMergeToolExecutor(Action resolve, Action reject, GitChange change) { try { if (change.FilePath == null) { reject(new Exception("Could not find file path.")); return; } var fileExtension = Path.GetExtension(change.FilePath).ToLower(); if (fileExtension is ".prefab" or ".unity") { reject(new Exception("Cannot auto-resolve conflicts for binary files. Please use an external merge tool.")); return; } await GitCommand.RunVsCodeAsync(new StringBuilder(), new[] { "--wait", change.FilePath }, 0, 141); var fullPath = Path.Combine(ProjectRoot, change.FilePath); var fileContent = await File.ReadAllTextAsync(fullPath); if (fileContent.Contains("<<<<<<<")) { resolve($"Conflict in '{change.FilePath}' was not resolved. Please try again."); return; } await GitCommand.RunGitAsync(new StringBuilder(), new[] { "add", change.FilePath }); await GitCommand.RunGitAsync(new StringBuilder(), new[] { "reset", "HEAD", change.FilePath }); resolve($"Successfully resolved conflict in '{change.FilePath}'. The file is now modified and ready for review."); } catch (Win32Exception ex) { reject(new Exception("Could not launch VS Code. Ensure it is installed and the 'code' command is available in your system's PATH.", ex)); } catch (Exception ex) { reject(ex); } } public static async void UnstageAllFilesIfSafeExecutor(Action resolve, Action reject) { try { using var repo = new Repository(ProjectRoot); if (repo.Index.Conflicts.Any()) { resolve(false); return; } var stagedFiles = repo.RetrieveStatus().Count(s => s.State is FileStatus.NewInIndex or FileStatus.ModifiedInIndex or FileStatus.DeletedFromIndex or FileStatus.RenamedInIndex or FileStatus.TypeChangeInIndex); if (stagedFiles == 0) { resolve(false); return; } await GitCommand.RunGitAsync(new StringBuilder(), new[] { "reset" }); resolve(true); } catch (Exception ex) { 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; } } }