From 7c09f2d8cef12d71697af127e0eb6522bc83baa2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 15:35:35 +0000 Subject: [PATCH 1/2] Pin diff --submodule=short so Patch reports a moved submodule as its gitlink [patch] With diff.submodule=log in the user's config, git prints a moved submodule as a summary with no diff --git block, so Patch() dropped the change; with =diff it prints the submodule's own files as superproject paths, which Apply then refuses. Pin the short form next to the other output pins. Fixes #124 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WAdbE4VMmkBh1tzMGpC9Wy --- .../Builders/GitPatchBuilderTests.cs | 1 + .../Integration/GitSubmoduleTests.cs | 48 +++++++++++++++++++ GitIntegration/Builders/GitPatchBuilder.cs | 6 +++ 3 files changed, 55 insertions(+) diff --git a/GitIntegration.Test/Builders/GitPatchBuilderTests.cs b/GitIntegration.Test/Builders/GitPatchBuilderTests.cs index 1193da3..daf23f1 100644 --- a/GitIntegration.Test/Builders/GitPatchBuilderTests.cs +++ b/GitIntegration.Test/Builders/GitPatchBuilderTests.cs @@ -28,6 +28,7 @@ public void BuildsTheDefaultPatchVector() "--no-color", "--src-prefix=a/", "--dst-prefix=b/", + "--submodule=short", "-U3", "--no-renames", ]; diff --git a/GitIntegration.Test/Integration/GitSubmoduleTests.cs b/GitIntegration.Test/Integration/GitSubmoduleTests.cs index 084a319..ed23aaa 100644 --- a/GitIntegration.Test/Integration/GitSubmoduleTests.cs +++ b/GitIntegration.Test/Integration/GitSubmoduleTests.cs @@ -3,6 +3,7 @@ namespace ktsu.GitIntegration.Test; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -164,6 +165,53 @@ await IntegrationGitFixture.ConfigureIdentityAsync(checkout, AuthorName, AuthorE Assert.AreNotEqual(after[0].Sha, after[0].CheckedOutSha); } + [TestMethod] + [DataRow("log")] + [DataRow("diff")] + public async Task PatchReportsAMovedSubmoduleAsItsGitlinkWhateverDiffSubmoduleSaysAsync(string format) + { + CancellationToken cancellationToken = TestContext.CancellationTokenSource.Token; + await IntegrationGitFixture.RequireGitAsync(cancellationToken).ConfigureAwait(false); + + using TemporaryRepository subDirectory = new(); + using TemporaryRepository superDirectory = new(); + + GitRepository sub = await CreateRepositoryAsync(subDirectory, "s.txt", cancellationToken).ConfigureAwait(false); + GitRepository super = await CreateRepositoryAsync(superDirectory, "m.txt", cancellationToken).ConfigureAwait(false); + + await AddSubmoduleAsync(super, sub.LocalPath!, "libs/sub", cancellationToken).ConfigureAwait(false); + + // Move the submodule's checkout on by a commit, so the superproject sees its gitlink change. + GitRepository checkout = new() + { + LocalPath = System.IO.Path.Join(superDirectory.RootPath, "libs", "sub").As(), + ProcessRunner = super.ProcessRunner, + }; + + await IntegrationGitFixture.ConfigureIdentityAsync(checkout, AuthorName, AuthorEmail, cancellationToken) + .ConfigureAwait(false); + + superDirectory.WriteFile("libs/sub/s.txt", "two\n"); + _ = await checkout.Add().All().ExecuteAsync(cancellationToken).ConfigureAwait(false); + _ = await checkout.Commit("c2".As()).ExecuteAsync(cancellationToken).ConfigureAwait(false); + + // Written into the superproject's own config, which is the nearest scope git reads it from. + _ = await new GitTextBuilder(super.ProcessRunner!, super.LocalPath, "config", "diff.submodule", format) + .ExecuteAsync(cancellationToken).ConfigureAwait(false); + + GitPatch patch = await super.Patch().ExecuteAsync(cancellationToken).ConfigureAwait(false); + + // "log" left the change out entirely, and "diff" reported libs/sub/s.txt in its place + // (ktsu-dev/GitIntegration#124). + GitFilePatch file = patch.Files.Single(); + Assert.AreEqual("libs/sub", file.Path.WeakString); + + _ = await super.Apply(file.PatchFor(file.Hunks)).ToIndex().ExecuteAsync(cancellationToken).ConfigureAwait(false); + + GitPatch staged = await super.Patch().Staged().ExecuteAsync(cancellationToken).ConfigureAwait(false); + Assert.AreEqual("libs/sub", staged.Files.Single().Path.WeakString); + } + [TestMethod] public async Task ReportsAnUninitializedSubmoduleAndThenUpdatesItAsync() { diff --git a/GitIntegration/Builders/GitPatchBuilder.cs b/GitIntegration/Builders/GitPatchBuilder.cs index 927961e..c4ee8d1 100644 --- a/GitIntegration/Builders/GitPatchBuilder.cs +++ b/GitIntegration/Builders/GitPatchBuilder.cs @@ -178,6 +178,12 @@ protected override void AppendVerbArguments(ICollection arguments) arguments.Add("--src-prefix=a/"); arguments.Add("--dst-prefix=b/"); + // diff.submodule=log prints a moved submodule as a "Submodule sub a..b:" summary with no + // diff --git block, so the change is missing from the patch, and =diff prints the + // submodule's own files as if they were the superproject's, which apply then refuses. + // Only the short form is a gitlink patch that stages (ktsu-dev/GitIntegration#124). + arguments.Add("--submodule=short"); + if (_staged) { arguments.Add("--cached"); From c711bbad9558a2efe9847a0fabc9b5b4baf30c23 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 15:39:33 +0000 Subject: [PATCH 2/2] Compare the submodule patch path as a RelativeFilePath so the test passes on Windows WeakString carries the platform separator, so the raw "libs/sub" literal failed on windows-latest; the other submodule tests compare typed paths. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WAdbE4VMmkBh1tzMGpC9Wy --- GitIntegration.Test/Integration/GitSubmoduleTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GitIntegration.Test/Integration/GitSubmoduleTests.cs b/GitIntegration.Test/Integration/GitSubmoduleTests.cs index ed23aaa..566e3d4 100644 --- a/GitIntegration.Test/Integration/GitSubmoduleTests.cs +++ b/GitIntegration.Test/Integration/GitSubmoduleTests.cs @@ -204,12 +204,12 @@ await IntegrationGitFixture.ConfigureIdentityAsync(checkout, AuthorName, AuthorE // "log" left the change out entirely, and "diff" reported libs/sub/s.txt in its place // (ktsu-dev/GitIntegration#124). GitFilePatch file = patch.Files.Single(); - Assert.AreEqual("libs/sub", file.Path.WeakString); + Assert.AreEqual("libs/sub".As(), file.Path); _ = await super.Apply(file.PatchFor(file.Hunks)).ToIndex().ExecuteAsync(cancellationToken).ConfigureAwait(false); GitPatch staged = await super.Patch().Staged().ExecuteAsync(cancellationToken).ConfigureAwait(false); - Assert.AreEqual("libs/sub", staged.Files.Single().Path.WeakString); + Assert.AreEqual("libs/sub".As(), staged.Files.Single().Path); } [TestMethod]