girt 0.4.3

An incremental, idiomatic Rust implementation of Git
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
use crate::refs::RefName;
use crate::{ObjectId, PackCompression, PackWriteLimits, ReadLimits};

/// Policy for replacing an existing destination. Server restrictions still apply.
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
pub enum ForcePolicy {
    /// Branch updates require the old commit to be an ancestor of the new commit. Existing tags
    /// can only retain the same ID. Other namespaces use only the exact old-value condition.
    /// Creation is allowed; branches must point directly to commits.
    #[default]
    FastForwardOnly,
    /// Explicitly permit non-fast-forward branch changes or tag replacement, still conditional
    /// on the exact expected old value. This never bypasses server hooks or configuration.
    Allow,
}

/// One full destination name and an exact compare-and-swap expectation.
#[derive(Debug, Clone)]
pub struct PushCommand {
    /// Validated full destination name under `refs/`; `HEAD` is not a push destination.
    pub name: RefName,
    /// `None` requires absence. `Some` requires this exact nonzero value, even with force enabled.
    pub expected: Option<ObjectId>,
    /// Desired tip. The null ID in the repository's format deletes an existing ref. Tags and
    /// non-branch namespaces may point to any supported object kind.
    pub new: ObjectId,
    /// Explicit replacement policy; use the default to protect existing history and tags.
    pub force: ForcePolicy,
}
impl PushCommand {
    /// Returns whether this command deletes its destination.
    pub fn deletes(&self) -> bool {
        self.new.is_null()
    }
}

/// Input, work and output bounds for preparation and a single receive-pack session.
///
/// Packs are spooled to temporary files. Retained memory includes selected structured payloads,
/// object/edge metadata, protocol bytes and the caller's object snapshot. Packed blob payloads
/// are not retained by selection; writing holds one reconstructed object and, for fresh delta
/// search, up to `window * max_object_bytes` internal candidate bytes plus one external candidate.
/// Structured parsing copies payloads.
/// Per-read decoding work is bounded separately and may recur for each selected object.
/// These are not hard heap or wall-clock limits; allocator overhead and server memory are excluded.
#[derive(Debug, Clone, Copy)]
pub struct PushLimits {
    /// Advertisement bytes including pkt-line framing (default 4 MiB).
    pub max_advertisement_bytes: usize,
    /// Advertised reference and `.have` entries (default 100,000).
    pub max_refs: usize,
    /// Explicit command count (default 100,000).
    pub max_commands: usize,
    /// Total encoded command and push-option bytes, including flushes (default 16 MiB).
    pub max_command_bytes: usize,
    /// Status bytes including framing (default 4 MiB).
    pub max_status_bytes: usize,
    /// Edge occurrences from objects selected for sending, including duplicates (default 4
    /// million). Reading known receiver history has a separate allowance of the same size.
    pub max_edges: usize,
    /// Cumulative commit visits and parent edge occurrences across fast-forward proofs
    /// (default 4 million), including duplicates. Both the new and old tips' ancestry count.
    pub max_ancestry_steps: usize,
    /// Per-object storage decoding bounds. Payload reads are additionally capped by remaining
    /// aggregate pack input bytes so preparation cannot retain more than that payload budget.
    pub read: ReadLimits,
    /// Selected count/payload and generated artifact bounds. The index is generated into a sink;
    /// its bound still applies. Count/payload bounds cover every object read as a candidate for
    /// sending, including commits later found in receiver history.
    pub pack: PackWriteLimits,
    /// Compression policy for fresh and reconstructed objects; bounded delta search by default.
    /// Blob and tree candidates may come from verified advertised-root history; their
    /// reconstructed values count against `pack.max_input_bytes`. Insufficient candidate
    /// budget falls back to whole objects. A corrupt offered candidate refuses preparation even
    /// if another candidate could produce a valid delta; candidate integrity is checked strictly.
    /// Existing compressed entries and eligible deltas are reused under either policy.
    /// Transfer deltas use REF_DELTA, requiring no optional OFS capability. Search and copied
    /// chains obey `read.max_delta_depth` and the delta policy's depth bound when enabled.
    /// Offering receiver candidates additionally caps output depth at one so internal bases
    /// always resolve before receiver-assisted external lookup.
    pub compression: PackCompression,
}
impl Default for PushLimits {
    fn default() -> Self {
        Self {
            max_advertisement_bytes: 4 * 1024 * 1024,
            max_refs: 100_000,
            max_commands: 100_000,
            max_command_bytes: 16 * 1024 * 1024,
            max_status_bytes: 4 * 1024 * 1024,
            max_edges: 4_000_000,
            max_ancestry_steps: 4_000_000,
            read: ReadLimits::default(),
            pack: PackWriteLimits::default(),
            compression: PackCompression::Delta(crate::DeltaOptions::DEFAULT),
        }
    }
}

impl PushLimits {
    /// Returns limits for a transfer the caller trusts as Git trusts the user's own remotes.
    ///
    /// Git bounds none of these values. The defaults suit untrusted input; these are large enough
    /// that only available memory bounds the work, while keeping headroom for internal arithmetic.
    ///
    /// The prepared pack is held in a temporary file before sending.
    pub const fn trusted() -> Self {
        const UNBOUNDED: usize = usize::MAX / 4;
        Self {
            max_advertisement_bytes: UNBOUNDED,
            max_refs: UNBOUNDED,
            max_commands: UNBOUNDED,
            max_command_bytes: UNBOUNDED,
            max_status_bytes: UNBOUNDED,
            max_edges: UNBOUNDED,
            max_ancestry_steps: UNBOUNDED,
            read: ReadLimits::trusted(),
            pack: PackWriteLimits::trusted(),
            compression: PackCompression::Delta(crate::DeltaOptions::DEFAULT),
        }
    }
}

/// Server acknowledgement, preserving rejection text without assuming UTF-8.
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Status {
    /// The server reported success.
    Ok,
    /// The server reported failure with this byte-preserving explanation.
    Rejected(Vec<u8>),
}

/// Provenance of a rejected reference update.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum RejectionOrigin {
    /// The native destination's locked reference did not match the command's expected old value.
    ExpectedValue,
    /// A receiver rejected the command for policy, storage, or another reason. Wire status
    /// reports use this variant even when their reason text mentions a stale value: the client
    /// cannot independently prove the receiver's exact comparison.
    Receiver,
}

/// Evidence for one submitted command, in caller command order.
#[derive(Debug, Clone)]
pub struct RefStatus {
    /// The submitted destination, old/new IDs and force policy.
    pub command: PushCommand,
    /// `None` means no valid acknowledgement was received. It does not mean rejection.
    pub status: Option<Status>,
    /// Rejection provenance, present only when `status` is [`Status::Rejected`].
    pub rejection_origin: Option<RejectionOrigin>,
    /// Whether any bytes of this command may have reached the peer, or native publication
    /// reached this command. `false` proves this command was not attempted by this push. A true
    /// value without an acknowledgement is uncertain and requires remote inspection.
    pub attempted: bool,
    /// Native reference and reflog effects, when publication was attempted. Wire servers do not
    /// expose this detail. A published reference can accompany a failed reflog append.
    pub effects: Option<crate::refs::RefEditOutcome>,
    /// Actual destination(s) reported by `report-status-v2` for a successful proc-receive
    /// command. Empty for ordinary updates or v1 servers. Each entry is server evidence, not a
    /// local reference mutation.
    pub rewrites: Vec<RefRewrite>,
    /// Whether all v2 rewrite options for this acknowledgement were terminated by the next
    /// status or report flush. False after an interrupted `ok` packet: the command was
    /// acknowledged, but its actual rewritten destination may still be unknown.
    pub rewrite_complete: bool,
}

/// One server-reported replacement for a successful pseudo-reference command.
#[derive(Debug, Clone, Default, Eq, PartialEq)]
pub struct RefRewrite {
    /// Actual destination. `None` means the submitted name was used.
    pub name: Option<RefName>,
    /// Actual previous ID, when reported by the server.
    pub old: Option<ObjectId>,
    /// Actual new ID, when reported by the server.
    pub new: Option<ObjectId>,
    /// Whether the server marked the update as forced.
    pub forced: bool,
}

/// Server evidence, including individual results for a non-atomic multi-ref push.
///
/// A successful `send` returns a complete report even when unpacking or every reference failed.
/// An uncertain error retains whatever valid status prefix was received. An `ok` is the server's
/// acknowledgement, not a promise of durability or that another writer has not since moved the ref.
#[derive(Debug, Clone)]
pub struct PushReport {
    /// Pack acceptance, or `None` when no unpack result was received (also for an empty push).
    pub unpack: Option<Status>,
    /// Per-ref results in request order. Empty for an empty push.
    pub refs: Vec<RefStatus>,
    /// Bounded channel-2 progress frames when sideband reporting was requested. Bytes are
    /// untrusted and need not be UTF-8. Frames received before failure remain in the report.
    pub progress: Vec<Vec<u8>>,
    /// Native receive warnings that did not reject a reference.
    pub warnings: Vec<Vec<u8>>,
}
impl PushReport {
    pub(super) fn pending(commands: &[PushCommand]) -> Self {
        Self {
            unpack: None,
            progress: vec![],
            warnings: vec![],
            refs: commands
                .iter()
                .cloned()
                .map(|command| RefStatus {
                    command,
                    status: None,
                    rejection_origin: None,
                    attempted: false,
                    effects: None,
                    rewrites: vec![],
                    rewrite_complete: false,
                })
                .collect(),
        }
    }

    pub(super) fn mark_attempted(&mut self, request: &[u8], written: usize) {
        let mut offset = 0usize;
        for reference in &mut self.refs {
            if written > offset {
                reference.attempted = true;
            }
            let header = &request[offset..offset + 4];
            let length =
                usize::from_str_radix(std::str::from_utf8(header).expect("encoded packet"), 16)
                    .expect("encoded packet length");
            offset += length;
        }
    }

    /// Whether every requested update was acknowledged and unpacking succeeded. An empty push
    /// is successful. Inspect individual results even when this returns false.
    pub fn all_succeeded(&self) -> bool {
        (self.refs.is_empty() || self.unpack == Some(Status::Ok))
            && self
                .refs
                .iter()
                .all(|r| r.status == Some(Status::Ok) && r.rewrite_complete)
    }
}

/// Session failure classified by whether commands might have reached the server.
#[derive(Debug, thiserror::Error)]
pub enum PushError {
    /// No update commands were attempted; this operation did not mutate destination refs.
    #[error("push not sent")]
    NotSent(#[source] PushFailure),
    /// Command transmission began. Some or all updates may have happened; do not blindly retry.
    /// Retained acknowledgements are evidence, while missing results require remote inspection.
    #[error("push outcome uncertain")]
    Uncertain {
        /// Transport, protocol, cancellation, resource or process failure.
        #[source]
        cause: PushFailure,
        /// Valid status prefix received before failure; missing entries remain unknown.
        report: Box<PushReport>,
    },
}

/// Preparation or session failure cause. Remote per-ref rejections are [`Status`] values instead.
#[derive(Debug, thiserror::Error)]
pub enum PushFailure {
    /// Native local destination could not be opened.
    #[error("local destination")]
    Destination(#[source] Box<crate::OpenError>),
    /// Native object installation failed before reference publication.
    #[error("local object installation")]
    Install(#[source] Box<crate::fetch::FetchError>),
    /// Native reference publication failed after mutation may have begun.
    #[error("local reference publication")]
    Reference(#[source] Box<crate::refs::ReferenceError>),
    /// Explicit reflog identity is invalid for a new record.
    #[error("invalid push reflog identity")]
    Identity,
    /// A supplied identity uses a different object format from the source repository.
    #[error(transparent)]
    ObjectFormat(#[from] crate::ObjectFormatError),
    /// Sanitized OpenSSH transport or service failure.
    #[cfg(all(feature = "ssh", any(target_os = "macos", target_os = "linux")))]
    #[error(transparent)]
    Ssh(crate::transport::ssh::SshError),
    /// Sanitized smart HTTP exchange failure.
    #[cfg(feature = "http")]
    #[error(transparent)]
    Http(crate::transport::http::HttpError),
    /// I/O failure; protocol interruption is returned without retrying.
    #[error("push I/O")]
    Io(#[source] std::io::Error),
    /// Malformed or unexpected receive-pack framing or state.
    #[error("invalid receive-pack response: {0}")]
    Protocol(&'static str),
    /// Unsupported protocol, format, command namespace or required capability.
    #[error("unsupported push feature: {0}")]
    Unsupported(&'static str),
    /// Peer ERR packet, not a per-reference rejection report.
    #[error("receive-pack error: {0:?}")]
    Remote(Vec<u8>),
    /// Resource bound exhausted.
    #[error("push limit exceeded: {0}")]
    Limit(&'static str),
    /// Cancellation observed between operations.
    #[error("push cancelled")]
    Cancelled,
    /// The owned transport reached its caller-supplied deadline.
    #[error("transport deadline expired")]
    Deadline,
    /// A root used for exclusion is no longer advertised. No commands were sent.
    #[error("receiver history root is not advertised: {0}")]
    KnowledgeChanged(ObjectId),
    /// Duplicate destination, zero ID, or other invalid command.
    #[error("invalid push command: {0}")]
    Command(&'static str),
    /// Non-fast-forward branch update or tag replacement without explicit force.
    #[error("replacement requires explicit force for {0:?}")]
    WouldForce(RefName),
    /// Missing selected tip or reachable object.
    #[error("missing reachable object {0}")]
    Missing(ObjectId),
    /// Branch tip or reachable edge has the wrong object type.
    #[error("wrong reachable object kind for {0}")]
    Kind(ObjectId),
    /// Bounded object storage read failed.
    #[error("push object {id}")]
    Read {
        /// Reachable object being read.
        id: ObjectId,
        /// Concrete storage failure.
        #[source]
        source: crate::ObjectReadError,
    },
    /// Pack construction failed before sending commands.
    #[error("push pack")]
    Pack(#[from] crate::PackWriteError),
    /// Reachable commit payload invalid or unsupported.
    #[error("push commit {id}")]
    Commit {
        /// Object whose payload failed validation.
        id: ObjectId,
        /// Concrete payload validation failure.
        #[source]
        source: crate::CommitError,
    },
    /// Reachable tree payload invalid or unsupported.
    #[error("push tree {id}")]
    Tree {
        /// Object whose payload failed validation.
        id: ObjectId,
        /// Concrete payload validation failure.
        #[source]
        source: crate::TreeError,
    },
    /// Reachable tag payload invalid or unsupported.
    #[error("push tag {id}")]
    Tag {
        /// Object whose payload failed validation.
        id: ObjectId,
        /// Concrete payload validation failure.
        #[source]
        source: crate::TagError,
    },
    /// Local receive-pack exited unsuccessfully, even if some results were acknowledged.
    #[error("local receive-pack exited unsuccessfully: {0}")]
    Process(std::process::ExitStatus),
}
impl PushFailure {
    pub(super) fn destination(error: crate::OpenError) -> Self {
        Self::Destination(Box::new(error))
    }

    pub(super) fn install(error: crate::fetch::FetchError) -> Self {
        Self::Install(Box::new(error))
    }

    pub(super) fn reference(error: crate::refs::ReferenceError) -> Self {
        Self::Reference(Box::new(error))
    }
}
impl From<crate::packet::Error> for PushFailure {
    fn from(error: crate::packet::Error) -> Self {
        match error {
            crate::packet::Error::Io(e) => Self::from(e),
            crate::packet::Error::Protocol(e) => Self::Protocol(e),
            crate::packet::Error::Limit(e) => Self::Limit(e),
            crate::packet::Error::Cancelled => Self::Cancelled,
            crate::packet::Error::Remote(e) => Self::Remote(e),
        }
    }
}

impl From<std::io::Error> for PushFailure {
    fn from(error: std::io::Error) -> Self {
        match crate::transport::interruption(&error) {
            Some(crate::transport::Interruption::Cancelled) => Self::Cancelled,
            Some(crate::transport::Interruption::Deadline) => Self::Deadline,
            None => Self::Io(error),
        }
    }
}

#[cfg(feature = "http")]
impl From<crate::transport::http::HttpError> for PushFailure {
    fn from(error: crate::transport::http::HttpError) -> Self {
        match error {
            crate::transport::http::HttpError::Cancelled => Self::Cancelled,
            crate::transport::http::HttpError::Deadline => Self::Deadline,
            error => Self::Http(error),
        }
    }
}

#[cfg(all(feature = "ssh", any(target_os = "macos", target_os = "linux")))]
impl From<crate::transport::ssh::SshError> for PushFailure {
    fn from(error: crate::transport::ssh::SshError) -> Self {
        match error {
            crate::transport::ssh::SshError::Cancelled => Self::Cancelled,
            crate::transport::ssh::SshError::Deadline => Self::Deadline,
            error => Self::Ssh(error),
        }
    }
}

impl From<crate::edges::Error> for PushFailure {
    fn from(error: crate::edges::Error) -> Self {
        match error {
            crate::edges::Error::Commit { id, source } => Self::Commit { id, source },
            crate::edges::Error::Tree { id, source } => Self::Tree { id, source },
            crate::edges::Error::Tag { id, source } => Self::Tag { id, source },
        }
    }
}