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
98 changes: 93 additions & 5 deletions Sources/GitKit/CallbacksPayload.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,89 @@ import CGitKit
final class CallbackBox {
let credentialsProvider: CredentialProvider?
var reporter: ProgressReporter?
let pushLease: PushLeaseCheck?

init(credentials: CredentialProvider?, reporter: ProgressReporter?) {
init(
credentials: CredentialProvider?,
reporter: ProgressReporter?,
pushLease: PushLeaseCheck? = nil
) {
self.credentialsProvider = credentials
self.reporter = reporter
self.pushLease = pushLease
}
}

final class PushLeaseCheck {
let remoteRef: String
let expectedOID: git_oid?
var failure: Libgit2Error?

init(remoteRef: String, expectedOID: git_oid?) {
self.remoteRef = remoteRef
self.expectedOID = expectedOID
}

func verify(
updates: UnsafeMutablePointer<UnsafePointer<git_push_update>?>?,
count: Int
) -> Int32 {
guard let updates else {
failure = leaseError(message: "force-with-lease rejected \(remoteRef): push did not negotiate updates")
return GIT_EMODIFIED.rawValue
}

for i in 0..<count {
guard let update = updates[i],
let dstRefname = update.pointee.dst_refname
else { continue }

let candidate = String(cString: dstRefname)
guard candidate == remoteRef else { continue }

var actual = update.pointee.src
var desired = update.pointee.dst
if git_oid_cmp(&actual, &desired) == 0 { return 0 }

if let expectedOID {
var expected = expectedOID
if git_oid_cmp(&actual, &expected) == 0 { return 0 }
} else if git_oid_is_zero(&actual) != 0 {
Comment thread
odrobnik marked this conversation as resolved.
return 0
}

failure = leaseError(
message: "force-with-lease rejected \(remoteRef): expected "
+ "\(expectedDescription), found \(oidDescription(&actual))")
return GIT_EMODIFIED.rawValue
}

failure = leaseError(message: "force-with-lease rejected \(remoteRef): push did not update that ref")
return GIT_EMODIFIED.rawValue
}

private var expectedDescription: String {
guard var expected = expectedOID else { return "missing" }
return oidDescription(&expected)
}

private func leaseError(message: String) -> Libgit2Error {
Libgit2Error(
code: GIT_EMODIFIED.rawValue,
klass: Int32(GIT_ERROR_REFERENCE.rawValue),
message: message)
}
}

private func oidDescription(_ oid: inout git_oid) -> String {
if git_oid_is_zero(&oid) != 0 { return "missing" }
let buf = UnsafeMutablePointer<CChar>.allocate(capacity: 41)
defer { buf.deallocate() }
buf.initialize(repeating: 0, count: 41)
_ = git_oid_tostr(buf, 41, &oid)
return String(cString: buf)
}

/// Run `body` with a shared raw payload pointer plus the C trampolines
/// each callback slot wants. The reporter's final state is synced back
/// to `outReporter` so the caller can flush pending lines.
Expand All @@ -25,6 +101,7 @@ final class CallbackBox {
func withCallbacksPayload<T>(
credentials: CredentialProvider?,
reporter: ProgressReporter?,
pushLease: PushLeaseCheck? = nil,
_ body: (
_ credentialsCB: git_credential_acquire_cb?,
_ sidebandCB: git_transport_message_cb?,
Expand All @@ -35,14 +112,16 @@ func withCallbacksPayload<T>(
_ pushRefCB: git_push_update_reference_cb?,
_ packCB: git_packbuilder_progress?,
_ pushTransferCB: git_push_transfer_progress_cb?,
_ pushNegotiationCB: git_push_negotiation?,
_ payload: UnsafeMutableRawPointer?
) throws -> T,
outReporter: (inout ProgressReporter) -> Void = { _ in }
) rethrows -> T {
if credentials == nil && reporter == nil {
return try body(nil, nil, nil, nil, nil, nil, nil, nil)
if credentials == nil && reporter == nil && pushLease == nil {
return try body(nil, nil, nil, nil, nil, nil, nil, nil, nil)
}
let box = CallbackBox(credentials: credentials, reporter: reporter)
let box = CallbackBox(
credentials: credentials, reporter: reporter, pushLease: pushLease)
let raw = Unmanaged.passRetained(box).toOpaque()
defer {
if var r = box.reporter { outReporter(&r); box.reporter = r }
Expand All @@ -56,8 +135,9 @@ func withCallbacksPayload<T>(
let pushRefCB = reporter != nil ? combinedPushRefTrampoline : nil
let packCB = reporter != nil ? combinedPackProgressTrampoline : nil
let pushTransferCB = reporter != nil ? combinedPushTransferTrampoline : nil
let pushNegotiationCB = pushLease != nil ? combinedPushNegotiationTrampoline : nil
return try body(credCB, sidebandCB, transferCB, updateCB, pushRefCB,
packCB, pushTransferCB, raw)
packCB, pushTransferCB, pushNegotiationCB, raw)
}

// MARK: Single-branch clone
Expand Down Expand Up @@ -376,6 +456,14 @@ private let combinedPushTransferTrampoline: git_push_transfer_progress_cb = {
return 0
}

private let combinedPushNegotiationTrampoline: git_push_negotiation = {
updates, len, payload in
guard let payload else { return 0 }
let box = Unmanaged<CallbackBox>.fromOpaque(payload).takeUnretainedValue()
guard let pushLease = box.pushLease else { return 0 }
return pushLease.verify(updates: updates, count: Int(len))
}

private func humanBytes(_ bytes: Int) -> String {
let kib = 1024.0
let mib = kib * 1024.0
Expand Down
4 changes: 2 additions & 2 deletions Sources/GitKit/Repository+Clone.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ extension Repository {
func runClone() throws {
try withCallbacksPayload(
credentials: credentials, reporter: reporter,
{ credCB, sidebandCB, transferCB, _, _, _, _, payload in
{ credCB, sidebandCB, transferCB, _, _, _, _, _, payload in
opts.fetch_opts.callbacks.credentials = credCB
opts.fetch_opts.callbacks.sideband_progress = sidebandCB
opts.fetch_opts.callbacks.transfer_progress = transferCB
Expand Down Expand Up @@ -176,7 +176,7 @@ extension Repository {
opts.depth = rawDepth
try withCallbacksPayload(
credentials: credentials, reporter: reporter,
{ credCB, sidebandCB, transferCB, updateCB, _, _, _, payload in
{ credCB, sidebandCB, transferCB, updateCB, _, _, _, _, payload in
opts.callbacks.credentials = credCB
opts.callbacks.sideband_progress = sidebandCB
opts.callbacks.transfer_progress = transferCB
Expand Down
Loading
Loading