Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .mailmap
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ matt-edmondson <matthew.edmondson@gmail.com>
matt-edmondson <matthew.edmondson@gmail.com> <Matt@thethreethousands.com>
matt-edmondson <matthew.edmondson@gmail.com> <matt@thethreethousands.com>
matt-edmondson <matthew.edmondson@gmail.com> <matt-edmondson@users.noreply.github.com>
matt-edmondson <matthew.edmondson@gmail.com> <matt@Mac.home>
matt-edmondson <matthew.edmondson@gmail.com> <matt@MattBookPro.local>
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> <dependabot@ktsu.dev>
github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> <github@ktsu.dev>
github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> <actions@users.noreply.github.com>
Expand Down
198 changes: 198 additions & 0 deletions GitIntegration.Test/Builders/GitWorktreeBuilderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.GitIntegration.Test;

using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

using ktsu.Semantics.Strings;

[TestClass]
public sealed class GitWorktreeBuilderTests
{
private static readonly string[] GlobalArguments =
[
"-C", TestPaths.Root.WeakString,
"--no-pager",
"-c", "core.quotepath=false",
"-c", "color.ui=false",
];

private static string[] Expect(params string[] verbArguments) =>
[.. GlobalArguments, .. verbArguments];

[TestMethod]
public void BuildsTheWorktreeListVector()
{
RecordingGitProcessRunner runner = new();
GitWorktreeListBuilder builder = new(runner, TestPaths.Root);

IReadOnlyList<string> arguments = builder.BuildArguments();

CollectionAssert.AreEqual(Expect("worktree", "list", "--porcelain"), arguments.ToArray());

Check warning on line 33 in GitIntegration.Test/Builders/GitWorktreeBuilderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.AreSequenceEqual' instead of 'CollectionAssert.AreEqual'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitIntegration&issues=AaDHVd40fWaLT-9t-jM6&open=AaDHVd40fWaLT-9t-jM6&pullRequest=118
}

[TestMethod]
public void BuildsTheMinimalWorktreeAddVector()
{
RecordingGitProcessRunner runner = new();
GitWorktreeAddBuilder builder = new(runner, TestPaths.Root, TestPaths.Worktree);

CollectionAssert.AreEqual(
Expect("worktree", "add", "--end-of-options", TestPaths.Worktree.WeakString),
builder.BuildArguments().ToArray());

Check warning on line 44 in GitIntegration.Test/Builders/GitWorktreeBuilderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.AreSequenceEqual' instead of 'CollectionAssert.AreEqual'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitIntegration&issues=AaDHVd40fWaLT-9t-jM7&open=AaDHVd40fWaLT-9t-jM7&pullRequest=118
}

[TestMethod]
public void PutsACheckedOutBranchInTheCommitIshOperand()
{
RecordingGitProcessRunner runner = new();
GitWorktreeAddBuilder builder = new(runner, TestPaths.Root, TestPaths.Worktree);
_ = builder.CheckingOut("feature".As<GitBranchName>());

CollectionAssert.AreEqual(
Expect("worktree", "add", "--end-of-options", TestPaths.Worktree.WeakString, "feature"),
builder.BuildArguments().ToArray());
}

[TestMethod]
public void EmitsTheCreateBranchOptionBeforeThePath()
{
RecordingGitProcessRunner runner = new();
GitWorktreeAddBuilder builder = new(runner, TestPaths.Root, TestPaths.Worktree);
_ = builder.CreatingBranch("feature".As<GitBranchName>());

CollectionAssert.AreEqual(
Expect("worktree", "add", "-b", "feature", "--end-of-options", TestPaths.Worktree.WeakString),
builder.BuildArguments().ToArray());
}

[TestMethod]
public void EmitsTheResettingCreateBranchOption()
{
RecordingGitProcessRunner runner = new();
GitWorktreeAddBuilder builder = new(runner, TestPaths.Root, TestPaths.Worktree);
_ = builder.CreatingOrResettingBranch("feature".As<GitBranchName>());

CollectionAssert.AreEqual(
Expect("worktree", "add", "-B", "feature", "--end-of-options", TestPaths.Worktree.WeakString),
builder.BuildArguments().ToArray());

Check warning on line 80 in GitIntegration.Test/Builders/GitWorktreeBuilderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.AreSequenceEqual' instead of 'CollectionAssert.AreEqual'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitIntegration&issues=AaDHVd40fWaLT-9t-jM-&open=AaDHVd40fWaLT-9t-jM-&pullRequest=118
}

[TestMethod]
public void CombinesBranchCreationWithAStartPoint()
{
RecordingGitProcessRunner runner = new();
GitWorktreeAddBuilder builder = new(runner, TestPaths.Root, TestPaths.Worktree);
_ = builder.CreatingBranch("feature".As<GitBranchName>()).From("origin/main".As<GitRefName>());

CollectionAssert.AreEqual(
Expect("worktree", "add", "-b", "feature", "--end-of-options", TestPaths.Worktree.WeakString, "origin/main"),
builder.BuildArguments().ToArray());

Check warning on line 92 in GitIntegration.Test/Builders/GitWorktreeBuilderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.AreSequenceEqual' instead of 'CollectionAssert.AreEqual'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitIntegration&issues=AaDHVd40fWaLT-9t-jM_&open=AaDHVd40fWaLT-9t-jM_&pullRequest=118
}

[TestMethod]
public void CombinesDetachmentWithACommitIsh()
{
RecordingGitProcessRunner runner = new();
GitWorktreeAddBuilder builder = new(runner, TestPaths.Root, TestPaths.Worktree);
_ = builder.Detached().From("v1.2.0".As<GitRefName>());

CollectionAssert.AreEqual(
Expect("worktree", "add", "--detach", "--end-of-options", TestPaths.Worktree.WeakString, "v1.2.0"),
builder.BuildArguments().ToArray());
}

[TestMethod]
public void TheLastModeSelectionWins()
{
// The four modes are one field, resolved rather than accumulated, matching how
// IGitBranchListBuilder's LocalOnly and RemoteOnly already replace each other. A vector
// carrying both -b and --detach is one git rejects.
RecordingGitProcessRunner runner = new();
GitWorktreeAddBuilder builder = new(runner, TestPaths.Root, TestPaths.Worktree);
_ = builder.CreatingBranch("feature".As<GitBranchName>()).Detached();

string[] arguments = [.. builder.BuildArguments()];

CollectionAssert.Contains(arguments, "--detach");

Check warning on line 119 in GitIntegration.Test/Builders/GitWorktreeBuilderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'CollectionAssert.Contains'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitIntegration&issues=AaDHVd40fWaLT-9t-jNB&open=AaDHVd40fWaLT-9t-jNB&pullRequest=118
CollectionAssert.DoesNotContain(arguments, "-b");

Check warning on line 120 in GitIntegration.Test/Builders/GitWorktreeBuilderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.DoesNotContain' instead of 'CollectionAssert.DoesNotContain'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitIntegration&issues=AaDHVd40fWaLT-9t-jNC&open=AaDHVd40fWaLT-9t-jNC&pullRequest=118
CollectionAssert.DoesNotContain(arguments, "feature");
}

[TestMethod]
public void TheLastCommitIshSelectionWins()
{
// CheckingOut and From write the same operand slot, so the same resolution applies.
RecordingGitProcessRunner runner = new();
GitWorktreeAddBuilder builder = new(runner, TestPaths.Root, TestPaths.Worktree);
_ = builder.CheckingOut("feature".As<GitBranchName>()).From("v1.2.0".As<GitRefName>());

string[] arguments = [.. builder.BuildArguments()];

CollectionAssert.Contains(arguments, "v1.2.0");

Check warning on line 134 in GitIntegration.Test/Builders/GitWorktreeBuilderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'CollectionAssert.Contains'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitIntegration&issues=AaDHVd40fWaLT-9t-jNE&open=AaDHVd40fWaLT-9t-jNE&pullRequest=118
CollectionAssert.DoesNotContain(arguments, "feature");
}

[TestMethod]
public void EmitsForceAndNoCheckout()
{
RecordingGitProcessRunner runner = new();
GitWorktreeAddBuilder builder = new(runner, TestPaths.Root, TestPaths.Worktree);
_ = builder.Force().WithoutCheckout();

CollectionAssert.AreEqual(
Expect("worktree", "add", "--force", "--no-checkout", "--end-of-options", TestPaths.Worktree.WeakString),
builder.BuildArguments().ToArray());

Check warning on line 147 in GitIntegration.Test/Builders/GitWorktreeBuilderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.AreSequenceEqual' instead of 'CollectionAssert.AreEqual'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitIntegration&issues=AaDHVd40fWaLT-9t-jNG&open=AaDHVd40fWaLT-9t-jNG&pullRequest=118
}

[TestMethod]
public async Task ExecuteReportsTheArgumentVectorOnSuccessAsync()
{
// git worktree add writes its confirmation to standard error, not a form this library parses,
// so the result carries the vector rather than a parse of that text.
RecordingGitProcessRunner runner = new() { StandardError = "Preparing worktree (new branch 'feature')\n" };
GitWorktreeAddBuilder builder = new(runner, TestPaths.Root, TestPaths.Worktree);

GitCompleted completed = await builder.ExecuteAsync(TestContext.CancellationTokenSource.Token).ConfigureAwait(false);

Check warning on line 158 in GitIntegration.Test/Builders/GitWorktreeBuilderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'TestContext.CancellationToken' instead of 'TestContext.CancellationTokenSource.Token'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitIntegration&issues=AaDHVd40fWaLT-9t-jNI&open=AaDHVd40fWaLT-9t-jNI&pullRequest=118

CollectionAssert.AreEqual(builder.BuildArguments().ToArray(), completed.Arguments.ToArray());

Check warning on line 160 in GitIntegration.Test/Builders/GitWorktreeBuilderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.AreSequenceEqual' instead of 'CollectionAssert.AreEqual'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitIntegration&issues=AaDHVd40fWaLT-9t-jNH&open=AaDHVd40fWaLT-9t-jNH&pullRequest=118
}

[TestMethod]
public void BuildsTheWorktreeRemoveVector()
{
RecordingGitProcessRunner runner = new();
GitWorktreeRemoveBuilder builder = new(runner, TestPaths.Root, TestPaths.Worktree);

CollectionAssert.AreEqual(
Expect("worktree", "remove", "--end-of-options", TestPaths.Worktree.WeakString),
builder.BuildArguments().ToArray());

Check warning on line 171 in GitIntegration.Test/Builders/GitWorktreeBuilderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.AreSequenceEqual' instead of 'CollectionAssert.AreEqual'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitIntegration&issues=AaDHVd40fWaLT-9t-jNJ&open=AaDHVd40fWaLT-9t-jNJ&pullRequest=118
}

[TestMethod]
public void EmitsForceOnRemoveBeforeThePath()
{
RecordingGitProcessRunner runner = new();
GitWorktreeRemoveBuilder builder = new(runner, TestPaths.Root, TestPaths.Worktree);
_ = builder.Force();

CollectionAssert.AreEqual(
Expect("worktree", "remove", "--force", "--end-of-options", TestPaths.Worktree.WeakString),
builder.BuildArguments().ToArray());
}

[TestMethod]
public void BuildsTheWorktreePruneVector()
{
RecordingGitProcessRunner runner = new();
GitWorktreePruneBuilder builder = new(runner, TestPaths.Root);

CollectionAssert.AreEqual(
Expect("worktree", "prune"),
builder.BuildArguments().ToArray());
}

public TestContext TestContext { get; set; } = null!;
}
4 changes: 4 additions & 0 deletions GitIntegration.Test/GitRepositoryMetadataTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
}

[TestMethod]
public void OpenWebClientTolerablyReturnsWhenWebUriIsNull()

Check warning on line 41 in GitIntegration.Test/GitRepositoryMetadataTests.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Add at least one assertion to this test case.

Check warning on line 41 in GitIntegration.Test/GitRepositoryMetadataTests.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Add at least one assertion to this test case.
{
GitRepository repository = new()
{
Expand Down Expand Up @@ -132,4 +132,8 @@
{
public static AbsoluteDirectoryPath Root { get; } =
(OperatingSystem.IsWindows() ? @"C:\" : "/").As<AbsoluteDirectoryPath>();

/// <summary>An absolute directory distinct from <see cref="Root"/>, used as a worktree destination.</summary>
public static AbsoluteDirectoryPath Worktree { get; } =
(OperatingSystem.IsWindows() ? @"C:\project-feature" : "/project-feature").As<AbsoluteDirectoryPath>();
}
40 changes: 40 additions & 0 deletions GitIntegration.Test/GitRepositoryMutatingVerbTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,44 @@ public void ANullArgumentIsReportedBeforeAMissingProcessRunner()
Assert.ThrowsExactly<ArgumentNullException>(() => _ = repository.Checkout(null!));
Assert.ThrowsExactly<ArgumentNullException>(() => _ = repository.AddRemote(null!, Url));
}

[TestMethod]
public void AddWorktreeRejectsANullPath()
{
GitRepository repository = new()
{
LocalPath = TestPaths.Root,
ProcessRunner = new RecordingGitProcessRunner(),
};

_ = Assert.ThrowsExactly<ArgumentNullException>(() => repository.AddWorktree(null!));
}

[TestMethod]
public void AddWorktreeRequiresAProcessRunner()
{
GitRepository repository = new() { LocalPath = TestPaths.Root };

_ = Assert.ThrowsExactly<InvalidOperationException>(() => repository.AddWorktree(TestPaths.Worktree));
}

[TestMethod]
public void RemoveWorktreeRejectsANullPath()
{
GitRepository repository = new()
{
LocalPath = TestPaths.Root,
ProcessRunner = new RecordingGitProcessRunner(),
};

_ = Assert.ThrowsExactly<ArgumentNullException>(() => repository.RemoveWorktree(null!));
}

[TestMethod]
public void PruneWorktreesRequiresAProcessRunner()
{
GitRepository repository = new() { LocalPath = TestPaths.Root };

_ = Assert.ThrowsExactly<InvalidOperationException>(repository.PruneWorktrees);
}
}
18 changes: 18 additions & 0 deletions GitIntegration.Test/GitRepositoryVerbTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ [.. repository.Branches().BuildArguments()],
[.. repository.Remotes().BuildArguments()],
[.. repository.RevParse("HEAD".As<GitRefName>()).BuildArguments()],
[.. repository.Tags().BuildArguments()],
[.. repository.Worktrees().BuildArguments()],
[.. repository.Submodules().BuildArguments()],
[.. repository.UpdateSubmodules().BuildArguments()],
[.. repository.RevList("HEAD".As<GitRefName>()).BuildArguments()],
Expand Down Expand Up @@ -76,6 +77,7 @@ public void VerbsOnAMetadataOnlyRepositoryExplainWhatIsMissing()
_ = Assert.ThrowsExactly<InvalidOperationException>(() => _ = repository.Branches());
_ = Assert.ThrowsExactly<InvalidOperationException>(() => _ = repository.Remotes());
_ = Assert.ThrowsExactly<InvalidOperationException>(() => _ = repository.Tags());
_ = Assert.ThrowsExactly<InvalidOperationException>(() => _ = repository.Worktrees());
_ = Assert.ThrowsExactly<InvalidOperationException>(() => _ = repository.Submodules());
_ = Assert.ThrowsExactly<InvalidOperationException>(() => _ = repository.UpdateSubmodules());
_ = Assert.ThrowsExactly<InvalidOperationException>(() => _ = repository.RevList("HEAD".As<GitRefName>()));
Expand Down Expand Up @@ -145,5 +147,21 @@ public async Task IsClonedReportsFalseForAPathThatIsNotAWorkingTreeAsync()
Assert.IsFalse(isCloned);
}

[TestMethod]
public void WorktreesRequiresAProcessRunner()
{
GitRepository repository = new() { LocalPath = TestPaths.Root };

_ = Assert.ThrowsExactly<InvalidOperationException>(() => _ = repository.Worktrees());
}

[TestMethod]
public void WorktreesRequiresALocalPath()
{
GitRepository repository = new() { ProcessRunner = new RecordingGitProcessRunner() };

_ = Assert.ThrowsExactly<InvalidOperationException>(() => _ = repository.Worktrees());
}

public TestContext TestContext { get; set; } = null!;
}
Loading
Loading