Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@
# them to avoid a spurious whole-file diff every time the solution is opened.
*.sln text eol=crlf

# Captured git diff output, kept exactly as git emitted it. A patch fixture mixes LF
# structural lines with CRLF content lines by design, testing that the parser leaves
# carriage returns alone, so eol normalization above would corrupt the very bytes the
# test asserts on.
GitIntegration.Test/Fixtures/patch-*.txt -text

###############################
# Git Large File System (LFS) #
###############################
Expand Down
78 changes: 78 additions & 0 deletions GitIntegration.Test/Builders/GitApplyBuilderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.GitIntegration.Test;

using System;
using System.IO;
using System.Threading.Tasks;

[TestClass]
public class GitApplyBuilderTests
{
[TestMethod]
public void MapsTheOptionFlags()
{
RecordingGitProcessRunner runner = new();

GitApplyBuilder index = new(runner, TestPaths.Root, "patch");
_ = index.ToIndex();
Assert.IsTrue(index.BuildArguments().Contains("--cached"));

Check warning on line 19 in GitIntegration.Test/Builders/GitApplyBuilderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'Assert.IsTrue'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitIntegration&issues=AaDTbW1kFZv4oPou5j8t&open=AaDTbW1kFZv4oPou5j8t&pullRequest=119

GitApplyBuilder reversed = new(runner, TestPaths.Root, "patch");
_ = reversed.Reversed();
Assert.IsTrue(reversed.BuildArguments().Contains("--reverse"));

Check warning on line 23 in GitIntegration.Test/Builders/GitApplyBuilderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'Assert.IsTrue'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitIntegration&issues=AaDTbW1kFZv4oPou5j8u&open=AaDTbW1kFZv4oPou5j8u&pullRequest=119

GitApplyBuilder checkOnly = new(runner, TestPaths.Root, "patch");
_ = checkOnly.Checked();
Assert.IsTrue(checkOnly.BuildArguments().Contains("--check"));

Check warning on line 27 in GitIntegration.Test/Builders/GitApplyBuilderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'Assert.IsTrue'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitIntegration&issues=AaDTbW1kFZv4oPou5j8v&open=AaDTbW1kFZv4oPou5j8v&pullRequest=119
}

[TestMethod]
public void AlwaysSuppressesWhitespaceWarnings()
{
RecordingGitProcessRunner runner = new();
GitApplyBuilder builder = new(runner, TestPaths.Root, "patch");

Assert.IsTrue(
builder.BuildArguments().Contains("--whitespace=nowarn"),
"Staging content already on disk is not the moment to enforce a whitespace policy the user configured for authoring.");

Check warning on line 38 in GitIntegration.Test/Builders/GitApplyBuilderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'Assert.IsTrue'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitIntegration&issues=AaDTbW1kFZv4oPou5j8w&open=AaDTbW1kFZv4oPou5j8w&pullRequest=119
}

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

_ = Assert.ThrowsExactly<ArgumentException>(() => _ = repository.Apply(" "));
}

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

ArgumentException thrown = Assert.ThrowsExactly<ArgumentException>(
() => _ = repository.Apply("@@ -1 +1 @@\n-caf\uFFFD\n+cafe\n"),
"Bytes that are not valid UTF-8 decode to U+FFFD, and with ASCII context around them the patch applies and stages the replacement character while the working tree keeps the original bytes.");

StringAssert.Contains(thrown.Message, "U+FFFD", StringComparison.Ordinal);

Check warning on line 60 in GitIntegration.Test/Builders/GitApplyBuilderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'StringAssert.Contains'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitIntegration&issues=AaDTbW1kFZv4oPou5j8x&open=AaDTbW1kFZv4oPou5j8x&pullRequest=119
}

[TestMethod]
public async Task DeletesTheTemporaryFileEvenWhenGitFailsAsync()
{
RecordingGitProcessRunner runner = new() { ExitCode = 1, StandardError = "error: corrupt patch" };
GitApplyBuilder builder = new(runner, TestPaths.Root, "not a patch\n");

_ = await Assert.ThrowsExactlyAsync<GitCommandException>(
async () => await builder.ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false);

Check warning on line 70 in GitIntegration.Test/Builders/GitApplyBuilderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Consider using the overload that accepts a CancellationToken and pass 'TestContext.CancellationToken'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitIntegration&issues=AaDTbW1kFZv4oPou5j8y&open=AaDTbW1kFZv4oPou5j8y&pullRequest=119

string path = runner.LastArguments![^1];

Assert.IsFalse(
File.Exists(path),
"A failing patch must not leave files behind, and the failure the caller sees is git's, not the cleanup's.");
}
}
2 changes: 1 addition & 1 deletion GitIntegration.Test/Builders/GitBranchListBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public void PinsTheExactFormatStringSentToGit()
{
// Asserted literally rather than through GitOutputFormats. The leading %(refname) is what
// lets the parser tell a local branch from a remote-tracking one and drop the remote HEAD
// symbolic reference, so silently losing it would break both behaviours at once.
// symbolic reference, so silently losing it would break both behaviors at once.
RecordingGitProcessRunner runner = new();
GitBranchListBuilder builder = new(runner, TestPaths.Root);

Expand Down
2 changes: 1 addition & 1 deletion GitIntegration.Test/Builders/GitCommandBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace ktsu.GitIntegration.Test;
[TestClass]
public class GitCommandBuilderTests
{
/// <summary>A minimal concrete builder, exercising only the base class behaviour.</summary>
/// <summary>A minimal concrete builder, exercising only the base class behavior.</summary>
private sealed class EchoBuilder(IGitProcessRunner runner, AbsoluteDirectoryPath? repositoryPath)
: GitCommandBuilder<string>(runner, repositoryPath)
{
Expand Down
2 changes: 1 addition & 1 deletion GitIntegration.Test/Builders/GitInitBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public void RejectsNullArguments()
}

[TestMethod]
public async Task ProbesBeforeInitialisingAndReportsAFreshRepositoryAsync()
public async Task ProbesBeforeInitializingAndReportsAFreshRepositoryAsync()
{
ScriptedGitProcessRunner runner = new ScriptedGitProcessRunner()
.Then(standardError: "fatal: not a git repository (or any of the parent directories): .git\n", exitCode: 128)
Expand Down
119 changes: 119 additions & 0 deletions GitIntegration.Test/Builders/GitPatchBuilderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.GitIntegration.Test;

using System;
using System.Collections.Generic;
using System.Linq;

[TestClass]
public class GitPatchBuilderTests
{
[TestMethod]
public void BuildsTheDefaultPatchVector()
{
RecordingGitProcessRunner runner = new();
GitPatchBuilder builder = new(runner, TestPaths.Root);

string[] expectedArguments =
[
"-C", TestPaths.Root.WeakString,
"--no-pager",
"-c", "core.quotepath=false",
"-c", "color.ui=false",
"-c", "diff.suppressBlankEmpty=false",
"diff",
"--no-ext-diff",
"--no-textconv",
"--no-color",
"--src-prefix=a/",
"--dst-prefix=b/",
];

Assert.AreSequenceEqual(expectedArguments, builder.BuildArguments());
}

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

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

Assert.IsTrue(
arguments.Contains("--src-prefix=a/"),
"diff.noprefix emits 'diff --git f.txt f.txt' and diff.mnemonicPrefix emits 'diff --git i/f.txt w/f.txt'. Neither header carries a new-side path this parser can read, and neither patch applies without -p0.");

Check warning on line 46 in GitIntegration.Test/Builders/GitPatchBuilderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'Assert.IsTrue'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitIntegration&issues=AaDTbW6vFZv4oPou5j84&open=AaDTbW6vFZv4oPou5j84&pullRequest=119
Assert.IsTrue(
arguments.Contains("--dst-prefix=b/"),
"diff.noprefix emits 'diff --git f.txt f.txt' and diff.mnemonicPrefix emits 'diff --git i/f.txt w/f.txt'. Neither header carries a new-side path this parser can read, and neither patch applies without -p0.");
}

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

List<string> arguments = [.. builder.BuildArguments()];
int setting = arguments.IndexOf("diff.suppressBlankEmpty=false");

Assert.IsTrue(
setting > 0 && arguments[setting - 1] == "-c",
"diff.suppressBlankEmpty prints an empty context line as a bare newline, which ends the hunk body early and truncates the text a round trip depends on.");
Assert.IsTrue(
setting < arguments.IndexOf("diff"),
"Git reads -c only before the subcommand.");

Check warning on line 66 in GitIntegration.Test/Builders/GitPatchBuilderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.IsLessThan' instead of 'Assert.IsTrue'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitIntegration&issues=AaDTbW6vFZv4oPou5j86&open=AaDTbW6vFZv4oPou5j86&pullRequest=119
}

[TestMethod]
public void MapsTheOptionFlags()
{
RecordingGitProcessRunner runner = new();

GitPatchBuilder staged = new(runner, TestPaths.Root);
_ = staged.Staged();
Assert.IsTrue(staged.BuildArguments().Contains("--cached"));

Check warning on line 76 in GitIntegration.Test/Builders/GitPatchBuilderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'Assert.IsTrue'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitIntegration&issues=AaDTbW6vFZv4oPou5j87&open=AaDTbW6vFZv4oPou5j87&pullRequest=119

GitPatchBuilder context = new(runner, TestPaths.Root);
_ = context.WithContext(7);
Assert.IsTrue(context.BuildArguments().Contains("-U7"));

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'Assert.IsTrue'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitIntegration&issues=AaDTbW6vFZv4oPou5j88&open=AaDTbW6vFZv4oPou5j88&pullRequest=119

GitPatchBuilder renames = new(runner, TestPaths.Root);
_ = renames.DetectRenames();
Assert.IsTrue(renames.BuildArguments().Contains("--find-renames"));

Check warning on line 84 in GitIntegration.Test/Builders/GitPatchBuilderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'Assert.IsTrue'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitIntegration&issues=AaDTbW6vFZv4oPou5j89&open=AaDTbW6vFZv4oPou5j89&pullRequest=119
}

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

Assert.IsFalse(
builder.BuildArguments().Contains("-z"),
"Patch format is line-based, and -z changes only the name-status framing this builder does not use.");

Check warning on line 95 in GitIntegration.Test/Builders/GitPatchBuilderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.DoesNotContain' instead of 'Assert.IsFalse'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitIntegration&issues=AaDTbW6vFZv4oPou5j8-&open=AaDTbW6vFZv4oPou5j8-&pullRequest=119
}

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

_ = Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => _ = builder.WithContext(-1));
}

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

ArgumentOutOfRangeException thrown = Assert.ThrowsExactly<ArgumentOutOfRangeException>(
() => _ = builder.WithContext(0),
"git apply refuses a zero-context patch without --unidiff-zero, which IGitApplyBuilder does not offer, so this is the one pairing of the library's own two verbs that could never work.");

StringAssert.Contains(thrown.Message, "--unidiff-zero", StringComparison.Ordinal);
}
}
2 changes: 1 addition & 1 deletion GitIntegration.Test/Builders/GitPullBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ public async Task ThrowsConflictWhenTheMergeLeavesConflictsAsync()
}

[TestMethod]
public async Task RecognisesARebaseConflictTooAsync()
public async Task RecognizesARebaseConflictTooAsync()
{
// A rebase reports its conflicts with different prose but the same "CONFLICT" marker, and
// leaves the repository mid-rebase rather than mid-merge. Both are conflicts to a caller.
Expand Down
87 changes: 87 additions & 0 deletions GitIntegration.Test/Builders/GitRestoreBuilderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.GitIntegration.Test;

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

using ktsu.Semantics.Paths;
using ktsu.Semantics.Strings;

[TestClass]
public class GitRestoreBuilderTests
{
[TestMethod]
public void BuildsTheRestoreVectorOnAModernGit()
{
RecordingGitProcessRunner runner = new();
GitRestoreBuilder builder = new(runner, TestPaths.Root, "f.txt".As<RelativeFilePath>());

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

Assert.IsTrue(arguments.Contains("restore"));

Check warning on line 23 in GitIntegration.Test/Builders/GitRestoreBuilderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'Assert.IsTrue'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitIntegration&issues=AaDTbW6lFZv4oPou5j8z&open=AaDTbW6lFZv4oPou5j8z&pullRequest=119
Assert.IsTrue(arguments.Contains("--staged"));
Assert.IsTrue(arguments.Contains("f.txt"));
}

[TestMethod]
public void SeparatesThePathWithABareDoubleDash()
{
RecordingGitProcessRunner runner = new();
GitRestoreBuilder builder = new(runner, TestPaths.Root, "f.txt".As<RelativeFilePath>());

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

Assert.AreSequenceEqual(
["restore", "--staged", "--", "f.txt"],
arguments[^4..],
"--end-of-options arrived in git 2.24, one release after restore, so the very versions this builder's reset fallback serves would read it as a pathspec and fail.");
Assert.IsFalse(
arguments.Contains("--end-of-options"),
"--end-of-options arrived in git 2.24, one release after restore, so the very versions this builder's reset fallback serves would read it as a pathspec and fail.");
}

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

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

[TestMethod]
public async Task FallsBackToResetOnAGitOlderThanRestoreAsync()
{
// git restore arrived in 2.23. Below that, unstaging goes through reset HEAD instead, which
// every supported git understands.
ScriptedGitProcessRunner runner = new ScriptedGitProcessRunner()
.Then(standardOutput: "git version 2.22.0\n")
.Then(standardOutput: string.Empty);
GitRestoreBuilder builder = new(runner, TestPaths.Root, "f.txt".As<RelativeFilePath>());

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

Check warning on line 64 in GitIntegration.Test/Builders/GitRestoreBuilderTests.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=AaDTbW6lFZv4oPou5j82&open=AaDTbW6lFZv4oPou5j82&pullRequest=119

string[] arguments = [.. runner.Invocations[1]];
Assert.AreSequenceEqual(["reset", "HEAD", "--", "f.txt"], arguments[^4..]);
}

[TestMethod]
public async Task TreatsExactlyTwoTwentyThreeAsSupportedAsync()
{
// The documented floor, asserted exactly: an off-by-one here silently falls back to reset
// for every user on the first version that supports restore.
ScriptedGitProcessRunner runner = new ScriptedGitProcessRunner()
.Then(standardOutput: "git version 2.23.0\n")
.Then(standardOutput: string.Empty);
GitRestoreBuilder builder = new(runner, TestPaths.Root, "f.txt".As<RelativeFilePath>());

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

Check warning on line 80 in GitIntegration.Test/Builders/GitRestoreBuilderTests.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=AaDTbW6lFZv4oPou5j83&open=AaDTbW6lFZv4oPou5j83&pullRequest=119

string[] arguments = [.. runner.Invocations[1]];
Assert.AreSequenceEqual(["restore", "--staged", "--", "f.txt"], arguments[^4..]);
}

public TestContext TestContext { get; set; } = null!;
}
2 changes: 1 addition & 1 deletion GitIntegration.Test/Builders/GitStatusBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ public void ConfigurationMethodsReturnTheSameBuilderForChaining()
}

[TestMethod]
public void RejectsAnUnrecognisedUntrackedFilesMode()
public void RejectsAnUnrecognizedUntrackedFilesMode()
{
RecordingGitProcessRunner runner = new();
GitStatusBuilder builder = new(runner, TestPaths.Root);
Expand Down
12 changes: 6 additions & 6 deletions GitIntegration.Test/Builders/GitSubmoduleBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,9 @@ public async Task KeepsTheRecordedGitlinkApartFromTheCheckedOutCommitAsync()
}

[TestMethod]
public async Task ReportsNoCheckedOutCommitForAnUninitialisedSubmoduleAsync()
public async Task ReportsNoCheckedOutCommitForAnUninitializedSubmoduleAsync()
{
// git prints the recorded gitlink again for an uninitialised submodule, which would make it
// git prints the recorded gitlink again for an uninitialized submodule, which would make it
// indistinguishable from a synchronised one if it were reported verbatim. Nothing is checked
// out there, so nothing is reported. Note there is no describe suffix either.
ScriptedGitProcessRunner runner = new ScriptedGitProcessRunner()
Expand All @@ -107,7 +107,7 @@ public async Task ReportsNoCheckedOutCommitForAnUninitialisedSubmoduleAsync()
IReadOnlyList<GitSubmodule> submodules =
await builder.ExecuteAsync(TestContext.CancellationTokenSource.Token).ConfigureAwait(false);

Assert.AreEqual(GitSubmoduleState.Uninitialised, submodules[0].State);
Assert.AreEqual(GitSubmoduleState.Uninitialized, submodules[0].State);
Assert.IsNull(submodules[0].CheckedOutSha);
Assert.IsNull(submodules[0].Describe);
}
Expand Down Expand Up @@ -326,7 +326,7 @@ public void MapsEveryOptionToItsFlag()
RecordingGitProcessRunner runner = new();
GitSubmoduleUpdateBuilder builder = new(runner, TestPaths.Root);

_ = builder.Initialise().Recursive().FromRemote().Force().WithDepth(1);
_ = builder.Initialize().Recursive().FromRemote().Force().WithDepth(1);

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

Expand Down Expand Up @@ -363,7 +363,7 @@ public void ConfigurationMethodsReturnTheSameBuilderForChaining()
RecordingGitProcessRunner runner = new();
GitSubmoduleUpdateBuilder builder = new(runner, TestPaths.Root);

Assert.AreSame(builder, builder.Initialise().Recursive().FromRemote().Force().WithDepth(1));
Assert.AreSame(builder, builder.Initialize().Recursive().FromRemote().Force().WithDepth(1));
}
}

Expand Down Expand Up @@ -434,7 +434,7 @@ public void PushEmitsItsOwnValueSet()
}

[TestMethod]
public void RejectsAnUnrecognisedEnumValue()
public void RejectsAnUnrecognizedEnumValue()
{
RecordingGitProcessRunner runner = new();

Expand Down
Loading
Loading