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
34 changes: 34 additions & 0 deletions GitIntegration.Test/Builders/GitPatchBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,45 @@
"--no-color",
"--src-prefix=a/",
"--dst-prefix=b/",
"-U3",
"--no-renames",
];

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

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

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

Assert.IsTrue(
arguments.Contains("-U3"),
"Left out, diff.context=0 in the host's config yields zero-context hunks, which Apply can never stage.");

Check warning on line 48 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=AaDjDnB4AduN7hzMOIJR&open=AaDjDnB4AduN7hzMOIJR&pullRequest=128
Assert.IsTrue(
arguments.Contains("--no-renames"),
"Left out, git's default rename detection and diff.renames=copies decide the patch shape instead of DetectRenames.");

Check warning on line 51 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=AaDjDnB4AduN7hzMOIJS&open=AaDjDnB4AduN7hzMOIJS&pullRequest=128
Assert.IsFalse(arguments.Contains("--find-renames"));

Check warning on line 52 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=AaDjDnB4AduN7hzMOIJT&open=AaDjDnB4AduN7hzMOIJT&pullRequest=128
}

[TestMethod]
public void ExplicitContextAndRenamesReplaceThePinnedDefaults()
{
RecordingGitProcessRunner runner = new();
GitPatchBuilder builder = new(runner, TestPaths.Root);
_ = builder.WithContext(7).DetectRenames();

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

Assert.AreEqual(1, arguments.Count(argument => argument.StartsWith("-U", StringComparison.Ordinal)));

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.ContainsSingle' instead of 'Assert.AreEqual'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitIntegration&issues=AaDjDnB4AduN7hzMOIJU&open=AaDjDnB4AduN7hzMOIJU&pullRequest=128
Assert.IsTrue(arguments.Contains("-U7"));

Check warning on line 65 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=AaDjDnB4AduN7hzMOIJV&open=AaDjDnB4AduN7hzMOIJV&pullRequest=128
Assert.IsTrue(arguments.Contains("--find-renames"));

Check warning on line 66 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=AaDjDnB4AduN7hzMOIJW&open=AaDjDnB4AduN7hzMOIJW&pullRequest=128
Assert.IsFalse(arguments.Contains("--no-renames"));

Check warning on line 67 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=AaDjDnB4AduN7hzMOIJX&open=AaDjDnB4AduN7hzMOIJX&pullRequest=128
}

[TestMethod]
public void AlwaysPinsBothPathPrefixes()
{
Expand Down
63 changes: 63 additions & 0 deletions GitIntegration.Test/Integration/GitPatchRoundTripTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,69 @@
"The patch read under this configuration has to apply back cleanly, or the verbs work only for users whose git is configured the way the tests assume.");
}

[TestMethod]
public async Task RoundTripsUnderZeroContextConfigurationAsync()
{
await IntegrationGitFixture.RequireGitAsync(TestContext.CancellationTokenSource.Token).ConfigureAwait(false);

Check warning on line 248 in GitIntegration.Test/Integration/GitPatchRoundTripTests.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=AaDjDnGPAduN7hzMOIJc&open=AaDjDnGPAduN7hzMOIJc&pullRequest=128

using TemporaryRepository repository = new();
GitClient client = IntegrationGitFixture.CreateClient();

// diff.context=0 makes a plain git diff emit zero-context hunks, which git apply refuses
// without --unidiff-zero.
GitRepository seeded = await SeedAsync(client, repository, [("diff.context", "0")]).ConfigureAwait(false);

string[] lines = [.. Enumerable.Range(1, 50).Select(number => $"line {number}")];
repository.WriteFile("f.txt", string.Join('\n', lines) + "\n");
await CommitAllAsync(seeded).ConfigureAwait(false);

lines[24] = "line 25 changed";
repository.WriteFile("f.txt", string.Join('\n', lines) + "\n");

GitRepository opened = await client.OpenAsync(repository.Root).ConfigureAwait(false);

Check warning on line 264 in GitIntegration.Test/Integration/GitPatchRoundTripTests.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=AaDjDnGPAduN7hzMOIJY&open=AaDjDnGPAduN7hzMOIJY&pullRequest=128
GitFilePatch file = (await opened.Patch().ExecuteAsync().ConfigureAwait(false)).Files.Single();

Check warning on line 265 in GitIntegration.Test/Integration/GitPatchRoundTripTests.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=AaDjDnGPAduN7hzMOIJZ&open=AaDjDnGPAduN7hzMOIJZ&pullRequest=128

_ = await opened.Apply(file.PatchFor(file.Hunks)).ToIndex().ExecuteAsync().ConfigureAwait(false);

Check warning on line 267 in GitIntegration.Test/Integration/GitPatchRoundTripTests.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=AaDjDnGPAduN7hzMOIJa&open=AaDjDnGPAduN7hzMOIJa&pullRequest=128

IReadOnlyList<GitDiffEntry> unstaged = await opened.Diff().ExecuteAsync().ConfigureAwait(false);

Check warning on line 269 in GitIntegration.Test/Integration/GitPatchRoundTripTests.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=AaDjDnGPAduN7hzMOIJb&open=AaDjDnGPAduN7hzMOIJb&pullRequest=128

Assert.AreEqual(
0,
unstaged.Count,
"A patch read with no WithContext call must still apply, whatever diff.context the host sets.");

Check warning on line 274 in GitIntegration.Test/Integration/GitPatchRoundTripTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.IsEmpty' instead of 'Assert.AreEqual'

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

[TestMethod]
public async Task ReportsAStagedRenameAsDeleteAndAddUnlessRequestedAsync()
{
await IntegrationGitFixture.RequireGitAsync(TestContext.CancellationTokenSource.Token).ConfigureAwait(false);

Check warning on line 280 in GitIntegration.Test/Integration/GitPatchRoundTripTests.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=AaDjDnGPAduN7hzMOIJh&open=AaDjDnGPAduN7hzMOIJh&pullRequest=128

using TemporaryRepository repository = new();
GitClient client = IntegrationGitFixture.CreateClient();

GitRepository seeded = await SeedAsync(client, repository, [("diff.renames", "copies")]).ConfigureAwait(false);

repository.WriteFile("old.txt", "one\ntwo\nthree\nfour\nfive\n");
await CommitAllAsync(seeded).ConfigureAwait(false);

repository.DeleteFile("old.txt");
repository.WriteFile("new.txt", "one\ntwo\nthree\nfour\nfive\n");
_ = await seeded.Add().All().ExecuteAsync(TestContext.CancellationTokenSource.Token).ConfigureAwait(false);

Check warning on line 292 in GitIntegration.Test/Integration/GitPatchRoundTripTests.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=AaDjDnGPAduN7hzMOIJi&open=AaDjDnGPAduN7hzMOIJi&pullRequest=128

GitRepository opened = await client.OpenAsync(repository.Root).ConfigureAwait(false);

Check warning on line 294 in GitIntegration.Test/Integration/GitPatchRoundTripTests.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=AaDjDnGPAduN7hzMOIJe&open=AaDjDnGPAduN7hzMOIJe&pullRequest=128

GitPatch plain = await opened.Patch().Staged().ExecuteAsync().ConfigureAwait(false);

Check warning on line 296 in GitIntegration.Test/Integration/GitPatchRoundTripTests.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=AaDjDnGPAduN7hzMOIJf&open=AaDjDnGPAduN7hzMOIJf&pullRequest=128

Assert.AreEqual(2, plain.Files.Count, "Without DetectRenames the rename is a delete and an add.");

Check warning on line 298 in GitIntegration.Test/Integration/GitPatchRoundTripTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.HasCount' instead of 'Assert.AreEqual'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitIntegration&issues=AaDjDnGPAduN7hzMOIJj&open=AaDjDnGPAduN7hzMOIJj&pullRequest=128
Assert.IsFalse(plain.Files.Any(file => file.Kind == GitChangeKind.Renamed));

Check warning on line 299 in GitIntegration.Test/Integration/GitPatchRoundTripTests.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=AaDjDnGPAduN7hzMOIJk&open=AaDjDnGPAduN7hzMOIJk&pullRequest=128

GitPatch detected = await opened.Patch().Staged().DetectRenames().ExecuteAsync().ConfigureAwait(false);

Check warning on line 301 in GitIntegration.Test/Integration/GitPatchRoundTripTests.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=AaDjDnGPAduN7hzMOIJg&open=AaDjDnGPAduN7hzMOIJg&pullRequest=128
GitFilePatch renamed = detected.Files.Single();

Assert.AreEqual(GitChangeKind.Renamed, renamed.Kind);
Assert.AreEqual("old.txt", renamed.OriginalPath?.WeakString);
}

/// <summary>
/// Creates an empty repository with the fixture identity and any extra configuration a test
/// needs, before anything is committed.
Expand Down
24 changes: 14 additions & 10 deletions GitIntegration/Builders/GitPatchBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ public interface IGitPatchBuilder : IGitCommandBuilder<GitPatch>
/// rather than the setting that caused it.
/// </remarks>
/// <param name="lines">
/// The number of context lines, at least one. Git's own default applies when this is never
/// called.
/// The number of context lines, at least one. Three, git's stock default, applies when this is
/// never called, whatever <c>diff.context</c> the host has configured.
/// </param>
/// <returns>The same builder, to allow chaining.</returns>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="lines"/> is less than one.</exception>
Expand Down Expand Up @@ -70,6 +70,9 @@ public interface IGitPatchBuilder : IGitCommandBuilder<GitPatch>
public IGitPatchBuilder Between(GitRefName fromRevision, GitRefName toRevision);

/// <summary>Reports a delete and an add of similar content as a rename.</summary>
/// <remarks>
/// Detection is off until this is called, whatever <c>diff.renames</c> the host has configured.
/// </remarks>
/// <returns>The same builder, to allow chaining.</returns>
public IGitPatchBuilder DetectRenames();
}
Expand All @@ -88,6 +91,9 @@ internal sealed class GitPatchBuilder(IGitProcessRunner runner, AbsoluteDirector
// One slot, so Against and Between cannot combine into a three-revision vector git would reject.
private string[] _revisions = [];
private bool _staged;
// Git's stock default, pinned so diff.context in a config file cannot change the hunk shape.
private const int DefaultContextLines = 3;

private int? _contextLines;
private bool _detectRenames;

Expand Down Expand Up @@ -177,15 +183,13 @@ protected override void AppendVerbArguments(ICollection<string> arguments)
arguments.Add("--cached");
}

if (_contextLines is int contextLines)
{
arguments.Add($"-U{contextLines}");
}
// Always emitted. diff.context=0 would otherwise give a zero-context patch, which is the
// shape WithContext refuses because Apply can never stage it.
arguments.Add($"-U{_contextLines ?? DefaultContextLines}");

if (_detectRenames)
{
arguments.Add("--find-renames");
}
// Git detects renames by default, and diff.renames=copies adds copy headers this parser
// does not read, so the host's setting is overridden in both directions.
arguments.Add(_detectRenames ? "--find-renames" : "--no-renames");

if (_revisions.Length > 0)
{
Expand Down
Loading