using Explorer.Application; using Explorer.Domain; namespace Explorer.Application.Tests; public class GitPorcelainParserTests { [Fact] public void Parses_branch_counts_and_ahead_behind() { var status = GitPorcelainParser.Parse(""" # branch.oid abcdef1234567890 # branch.head main # branch.upstream origin/main # branch.ab +2 -1 1 .M N... 100644 100644 100644 a a src/App.cs 1 M. N... 100644 100644 100644 b b README.md 2 R. N... 100644 100644 100644 c c R100 new.txt old.txt ? bin/out.dll ? notes.md ! ignore.me """, @"C:\src"); Assert.NotNull(status); Assert.Equal("main", status.Branch); Assert.Equal(3, status.ModifiedCount); Assert.Equal(2, status.UntrackedCount); Assert.Equal(2, status.Ahead); Assert.Equal(1, status.Behind); Assert.False(status.WorkingTreeClean); Assert.Equal("main · 3 modified · 2 untracked · 2 ahead · 1 behind", status.Badge); Assert.Collection( status.Changes, c => { Assert.Equal(GitChangeState.Staged, c.State); Assert.Equal("old.txt", c.OriginalPath); Assert.Equal("new.txt", c.Path); Assert.Equal("old.txt → new.txt", c.DisplayPath); Assert.Equal("renamed", c.ChangeLabel); }, c => { Assert.Equal(GitChangeState.Staged, c.State); Assert.Equal("README.md", c.Path); Assert.Equal("modified", c.ChangeLabel); }, c => { Assert.Equal(GitChangeState.Unstaged, c.State); Assert.Equal("src/App.cs", c.Path); Assert.Equal("modified", c.ChangeLabel); }, c => { Assert.Equal(GitChangeState.Untracked, c.State); Assert.Equal("bin/out.dll", c.Path); }, c => { Assert.Equal(GitChangeState.Untracked, c.State); Assert.Equal("notes.md", c.Path); }); } [Fact] public void Lists_staged_and_unstaged_for_the_same_path() { var status = GitPorcelainParser.Parse(""" # branch.head main 1 MM N... 100644 100644 100644 a a src/Both.cs """, @"C:\src"); Assert.NotNull(status); Assert.Equal(1, status.ModifiedCount); Assert.Equal(2, status.Changes.Count); Assert.Equal(GitChangeState.Staged, status.Changes[0].State); Assert.Equal(GitChangeState.Unstaged, status.Changes[1].State); Assert.All(status.Changes, c => Assert.Equal("src/Both.cs", c.Path)); } [Fact] public void Lists_unmerged_and_quoted_paths() { var status = GitPorcelainParser.Parse(""" # branch.head topic u UU N... 100644 100644 100644 100644 a b c conflict.txt 1 .M N... 100644 100644 100644 a a "my file.txt" ? docs/a file.md """, @"C:\src"); Assert.NotNull(status); Assert.Equal(2, status.ModifiedCount); Assert.Equal(1, status.UntrackedCount); Assert.Equal(GitChangeState.Unmerged, status.Changes[0].State); Assert.Equal("conflict.txt", status.Changes[0].Path); Assert.Equal("unmerged", status.Changes[0].ChangeLabel); Assert.Equal("my file.txt", status.Changes[1].Path); Assert.Equal("docs/a file.md", status.Changes[2].Path); } [Fact] public void Unquote_decodes_c_escapes() => Assert.Equal("a\"b", GitPorcelainParser.Unquote("\"a\\\"b\"")); [Fact] public void Clean_tree_uses_detached_oid_prefix() { var status = GitPorcelainParser.Parse(""" # branch.oid 0123456789abcdef # branch.head (detached) """, @"F:\repo"); Assert.NotNull(status); Assert.Equal("0123456", status.Branch); Assert.True(status.WorkingTreeClean); Assert.Equal("0123456 · clean", status.Badge); } [Fact] public void Empty_output_is_null() => Assert.Null(GitPorcelainParser.Parse("", @"C:\src")); } public class GitLocatorTests { [Fact] public void Prefers_the_configured_path_when_it_exists() { var path = @"C:\Tools\git.exe"; Assert.Equal(path, GitLocator.Find(path, fileExists: p => p == path, pathVariable: "")); } [Fact] public void Finds_git_on_PATH() { var found = GitLocator.Find( null, fileExists: p => p.Equals(@"D:\bin\git.exe", StringComparison.OrdinalIgnoreCase), pathVariable: @"C:\Windows;D:\bin"); Assert.Equal(@"D:\bin\git.exe", found); } [Fact] public void Returns_null_when_git_is_missing() => Assert.Null(GitLocator.Find(null, fileExists: _ => false, pathVariable: @"C:\none")); } public class GitRepoDetectorTests { [Fact] public void Finds_the_repo_root_from_a_nested_path() { var dirs = new HashSet(StringComparer.OrdinalIgnoreCase) { @"C:\src", @"C:\src\.git", @"C:\src\app" }; var files = new HashSet(StringComparer.OrdinalIgnoreCase); Assert.Equal(@"C:\src", GitRepoDetector.FindRoot(@"C:\src\app\Main.cs", dirs.Contains, files.Contains)); Assert.True(GitRepoDetector.IsRepoRoot(@"C:\src", dirs.Contains, files.Contains)); Assert.False(GitRepoDetector.IsRepoRoot(@"C:\src\app", dirs.Contains, files.Contains)); } [Fact] public void Treats_a_gitfile_as_a_worktree_root() { var dirs = new HashSet(StringComparer.OrdinalIgnoreCase) { @"C:\work" }; var files = new HashSet(StringComparer.OrdinalIgnoreCase) { @"C:\work\.git" }; Assert.Equal(@"C:\work", GitRepoDetector.FindRoot(@"C:\work", dirs.Contains, files.Contains)); } [Fact] public void Virtual_roots_are_not_repos() => Assert.Null(GitRepoDetector.FindRoot(LocationRoots.ThisPc, _ => true, _ => true)); [Fact] public void Finds_gitdir_from_a_directory_and_a_gitfile() { var dirs = new HashSet(StringComparer.OrdinalIgnoreCase) { @"C:\src", @"C:\src\.git", @"C:\work", @"D:\repo\.git\worktrees\topic" }; var files = new HashSet(StringComparer.OrdinalIgnoreCase) { @"C:\work\.git" }; Assert.Equal(@"C:\src\.git", GitRepoDetector.FindGitDir(@"C:\src", dirs.Contains, files.Contains)); Assert.Equal( @"D:\repo\.git\worktrees\topic", GitRepoDetector.FindGitDir( @"C:\work", dirs.Contains, files.Contains, _ => "gitdir: D:/repo/.git/worktrees/topic")); } [Fact] public void Detects_merge_and_rebase_in_progress() { var dirs = new HashSet(StringComparer.OrdinalIgnoreCase) { @"C:\src", @"C:\src\.git", @"C:\src\.git\rebase-merge", @"C:\work", @"D:\repo\.git\worktrees\topic" }; var files = new HashSet(StringComparer.OrdinalIgnoreCase) { @"C:\src\.git\MERGE_HEAD", @"C:\work\.git", @"D:\repo\.git\worktrees\topic\CHERRY_PICK_HEAD" }; Assert.Equal(GitOperationKind.Merge, GitRepoDetector.GetOperation(@"C:\src", dirs.Contains, files.Contains)); Assert.True(GitRepoDetector.IsOperationInProgress(@"C:\src", dirs.Contains, files.Contains)); dirs.Remove(@"C:\src\.git\rebase-merge"); files.Remove(@"C:\src\.git\MERGE_HEAD"); Assert.Equal(GitOperationKind.None, GitRepoDetector.GetOperation(@"C:\src", dirs.Contains, files.Contains)); Assert.False(GitRepoDetector.IsOperationInProgress(@"C:\src", dirs.Contains, files.Contains)); Assert.Equal( GitOperationKind.CherryPick, GitRepoDetector.GetOperation( @"C:\work", dirs.Contains, files.Contains, _ => "gitdir: D:/repo/.git/worktrees/topic")); } }