From af41accbcfba01743ace0e07bf4a7a32f8ee844e Mon Sep 17 00:00:00 2001 From: csjones Date: Tue, 3 Mar 2026 21:18:40 -0800 Subject: [PATCH] fix: improve git subtree pull error messages and limit split info search to HEAD - Changed findSubtreeSplitInfo() to search HEAD instead of --all branches - Enhanced subtreePull() error messages to include both stdout and stderr - Added filtering of empty output lines in error details --- .../SubtreeLib/Utilities/GitOperations.swift | 8 ++- .../StaleTrailerResyncTests.swift | 61 +++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/Sources/SubtreeLib/Utilities/GitOperations.swift b/Sources/SubtreeLib/Utilities/GitOperations.swift index 43aa1f5..989555d 100644 --- a/Sources/SubtreeLib/Utilities/GitOperations.swift +++ b/Sources/SubtreeLib/Utilities/GitOperations.swift @@ -147,7 +147,7 @@ public enum GitOperations { public static func findSubtreeSplitInfo(prefix: String) async throws -> SubtreeSplitInfo? { // Find the most recent commit with a git-subtree-dir trailer matching this prefix let logResult = try await run(arguments: [ - "log", "--all", "--grep=^git-subtree-dir: \(prefix)$", + "log", "HEAD", "--grep=^git-subtree-dir: \(prefix)$", "--format=%H%n%B", "-1" ]) @@ -267,7 +267,11 @@ public enum GitOperations { let result = try await run(arguments: args) guard result.exitCode == 0 else { - throw GitError.commandFailed("git subtree pull failed: \(result.stderr)") + let details = [result.stdout, result.stderr] + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + .joined(separator: "\n") + throw GitError.commandFailed("git subtree pull failed: \(details)") } // Get the commit hash after pull diff --git a/Tests/IntegrationTests/StaleTrailerResyncTests.swift b/Tests/IntegrationTests/StaleTrailerResyncTests.swift index 158dee6..447fde0 100644 --- a/Tests/IntegrationTests/StaleTrailerResyncTests.swift +++ b/Tests/IntegrationTests/StaleTrailerResyncTests.swift @@ -232,4 +232,65 @@ final class StaleTrailerResyncTests { #expect(newCommitCount == 1, "Normal update should create exactly 1 first-parent commit (no resync), got \(newCommitCount)") } + + @Test("update detects stale trailer even when correct trailer exists on another branch") + func testStaleTrailerNotMaskedByOtherBranch() async throws { + // This reproduces the exact CI failure: a previous workflow run pushed a branch + // with the correct resync trailer, but the current branch (from main) still has + // the stale trailer. findSubtreeSplitInfo must only search HEAD, not --all. + + let upstream = try await createUpstreamRepo(tag: "v1.0") + defer { try? upstream.tearDown() } + + let local = try await GitRepositoryFixture() + defer { try? local.tearDown() } + + // Init and add subtree at v1.0 + _ = try await harness.run(arguments: ["init"], workingDirectory: local.path) + let addResult = try await harness.run( + arguments: ["add", "--remote", fileURL(for: upstream), "--name", "lib", + "--prefix", "Vendor/lib", "--ref", "v1.0"], + workingDirectory: local.path + ) + #expect(addResult.exitCode == 0, "Add should succeed: \(addResult.stderr)") + + // Update to v2.0 normally (creates proper trailers) + try await addUpstreamUpdate(repo: upstream, tag: "v2.0", content: "// v2\n") + let update1 = try await harness.run( + arguments: ["update", "lib"], + workingDirectory: local.path + ) + #expect(update1.exitCode == 0, "First update should succeed: \(update1.stdout)\(update1.stderr)") + let v2Commit = try await getTagCommit(repo: upstream, tag: "v2.0") + + // Simulate GitHub squash merge: replace update commit with one that has NO trailers + try await local.runGit(["commit", "--allow-empty", "-m", + "chore(deps): update subtree lib to v2.0 (#123)\n\nSquash merge lost trailers."]) + + // Create a side branch that has the CORRECT trailer (simulates a previous CI run) + try await local.runGit(["checkout", "-b", "other-branch"]) + try await local.runGit(["commit", "--allow-empty", "-m", + "Squashed 'Vendor/lib/' content from commit \(String(v2Commit.prefix(8)))\n\ngit-subtree-dir: Vendor/lib\ngit-subtree-split: \(v2Commit)"]) + + // Go back to main — this branch has the stale v1.0 trailer + try await local.runGit(["checkout", "-"]) + + // Add v3.0 upstream + try await addUpstreamUpdate(repo: upstream, tag: "v3.0", content: "// v3\n") + + // Update — must detect stale trailer on HEAD despite correct trailer on other-branch + let update2 = try await harness.run( + arguments: ["update", "lib"], + workingDirectory: local.path + ) + + #expect(update2.exitCode == 0, + "Update should succeed (resync from HEAD, ignoring other branch): \(update2.stdout)\(update2.stderr)") + #expect(update2.stdout.contains("Updated lib"), + "Should show updated message: \(update2.stdout)") + + // Verify content was actually updated to v3 + let content = try String(contentsOfFile: local.path.appending("Vendor/lib/src/main.c").string) + #expect(content.contains("v3"), "Content should be from v3.0") + } }