diff --git a/Sources/GitKit/Repository+Push.swift b/Sources/GitKit/Repository+Push.swift index 5c171e5..0f2b5d9 100644 --- a/Sources/GitKit/Repository+Push.swift +++ b/Sources/GitKit/Repository+Push.swift @@ -209,6 +209,11 @@ extension Repository { } private func qualifiedPushSource(_ src: String) throws -> (String, String) { + if src == "HEAD" { + guard let branch = try currentBranch() else { return (src, "") } + return ("refs/heads/\(branch)", "refs/heads/") + } + let branch = "refs/heads/\(src)" let tag = "refs/tags/\(src)" let hasBranch = try referenceExists(branch) diff --git a/Tests/GitKitTests/RepositoryTests.swift b/Tests/GitKitTests/RepositoryTests.swift index d2b7e63..6374e95 100644 --- a/Tests/GitKitTests/RepositoryTests.swift +++ b/Tests/GitKitTests/RepositoryTests.swift @@ -164,6 +164,70 @@ struct RepositoryTests { #expect(remoteSHA == localSHA) } + @Test("push accepts detached HEAD and retains fast-forward protection") + func pushDetachedHEAD() throws { + let dir = try makeFixtureRepo() + defer { try? FileManager.default.removeItem(at: dir) } + let origin = try makeBareOrigin() + defer { try? FileManager.default.removeItem(at: origin) } + + let headSHA = try runGit(["rev-parse", "HEAD"], in: dir) + .trimmingCharacters(in: .whitespacesAndNewlines) + let repo = try Repository.open(at: dir) + try repo.addRemote(name: "origin", url: origin) + try repo.checkout(ref: headSHA) + #expect(try repo.currentBranch() == nil) + #expect(throws: Libgit2Error.self) { + try repo.push( + remote: "origin", refspec: "HEAD:review-fixes", + setUpstream: false, progress: { _ in }) + } + try repo.push( + remote: "origin", refspec: "HEAD:refs/heads/review-fixes", + setUpstream: false, progress: { _ in }) + + let pushedSHA = try runGit(["rev-parse", "refs/heads/review-fixes"], in: origin) + .trimmingCharacters(in: .whitespacesAndNewlines) + #expect(pushedSHA == headSHA) + + try Data("advanced\n".utf8).write(to: dir.appendingPathComponent("README.md")) + try runGit(["commit", "-am", "advance remote"], in: dir) + try runGit(["push", "origin", "HEAD:refs/heads/review-fixes"], in: dir) + let advancedSHA = try runGit(["rev-parse", "HEAD"], in: dir) + .trimmingCharacters(in: .whitespacesAndNewlines) + try repo.checkout(ref: headSHA) + + #expect(throws: Libgit2Error.self) { + try repo.push( + remote: "origin", refspec: "HEAD:refs/heads/review-fixes", + setUpstream: false, progress: { _ in }) + } + let protectedSHA = try runGit(["rev-parse", "refs/heads/review-fixes"], in: origin) + .trimmingCharacters(in: .whitespacesAndNewlines) + #expect(protectedSHA == advancedSHA) + } + + @Test("push HEAD sets upstream for the attached branch") + func pushAttachedHEADSetUpstream() throws { + let dir = try makeFixtureRepo() + defer { try? FileManager.default.removeItem(at: dir) } + let origin = try makeBareOrigin() + defer { try? FileManager.default.removeItem(at: origin) } + + let repo = try Repository.open(at: dir) + try repo.addRemote(name: "origin", url: origin) + try repo.push( + remote: "origin", refspec: "HEAD:refs/heads/review-fixes", + setUpstream: true, progress: { _ in }) + + let upstreamRemote = try runGit(["config", "branch.main.remote"], in: dir) + .trimmingCharacters(in: .whitespacesAndNewlines) + let upstreamMerge = try runGit(["config", "branch.main.merge"], in: dir) + .trimmingCharacters(in: .whitespacesAndNewlines) + #expect(upstreamRemote == "origin") + #expect(upstreamMerge == "refs/heads/review-fixes") + } + @Test("push accepts a short tag refspec") func pushShortTag() throws { let dir = try makeFixtureRepo()