Skip to main content

girt/fetch/
workflow.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::io::{Read, Write};
3use std::num::NonZeroU32;
4use std::ops::ControlFlow;
5use std::path::Path;
6use std::sync::atomic::AtomicBool;
7
8use super::{
9    Advertisement, FetchError, FetchInstalled, FetchLimits, FetchUpdateLimits, KnownHistory,
10    ReceivedFetch,
11};
12use crate::refs::{Expected, RefEdit, RefEditOutcome, RefName, ReferenceError, Reflog, Target};
13use crate::remote::{Direction, Mapping, MappingError, Refspecs};
14use crate::transport::TransportControl;
15use crate::{ObjectId, Repository};
16
17/// A synchronous destination snapshot and explicit policy for one fetch.
18///
19/// # Workflow
20///
21/// [`Self::prepare`] captures destination values. Choose optional depth, pruning, and reflog
22/// policy before [`Self::receive_local`] or a network adapter reads the actual advertisement.
23/// [`FetchReady::finish`] installs validated objects and publishes eligible refs; inspect
24/// [`FetchReport`] or [`FetchFinishError`] for completed effects and retry decisions. Use
25/// [`Self::plan`] only to preview how an advertisement would map, not to reserve its tips.
26///
27/// Construct with a named remote's [`crate::remote::Remote::fetch_refspecs`] or an explicit list.
28/// Endpoint selection and credentials remain separate transport arguments. Only `refs/remotes/*`
29/// and `refs/tags/*` destinations are supported by default. A caller may add tag-like namespaces
30/// with [`Self::with_tag_destination_namespace`]. Branch and symbolic destinations are rejected.
31/// Remote-tracking refs accept all validated object kinds. When both old/new tips peel to commits,
32/// non-fast-forward updates require force intent and name authorization; other kind replacements
33/// need neither. Replacing a tag requires both `+` and its name in `authorized_force`.
34/// Neither source-only selection nor an unchanged tag requires force authorization.
35///
36/// Preparation performs filesystem work; transport selection subsequently maps the actual
37/// advertisement using only this owned snapshot. HTTP/SSH downloads retain owned validation state
38/// for caller-controlled worker handoff. No runtime, endpoint discovery or worker is created here.
39///
40/// Callers must exclude checkout, symbolic-HEAD/branch changes, worktree registration changes and
41/// GC/pruning from preparation through publication. Ordinary destination writers may run
42/// concurrently: changed destinations use exact expected values in a reference transaction.
43/// Unchanged destinations are reported but not written or locked, and may change concurrently after
44/// the snapshot. HEAD chains in the main and registered linked worktrees must stay within
45/// `refs/heads/*` (or be detached), so rejecting branch destinations also protects checked-out
46/// references. Unusual HEAD chains and inaccessible worktree metadata fail closed, even for
47/// source-only fetches.
48/// Verified known history must include exactly the destination's captured shallow roots, so a
49/// depth response cannot silently drop boundaries belonging to other local references.
50///
51/// Exact selectors missing from the advertisement are reported while other valid selections
52/// continue. [`Self::with_prune`] permits scoped remote-tracking and authorized tag-namespace
53/// deletion. Shallow depth changes
54/// use a conditional metadata lock and are reported separately from object/ref effects.
55///
56/// This workflow does not write `FETCH_HEAD`, infer tags, clone, check out, edit
57/// configuration, discover credentials, or implement the full Git CLI. See
58/// `examples/fetch_remote.rs`.
59///
60/// # Examples
61///
62/// Preview what a fetch would do, using an advertisement from [`super::discover_local`]. The
63/// preview is only advisory: the transfer replans from the advertisement it actually receives.
64///
65/// ```
66/// use std::collections::BTreeSet;
67/// use std::sync::atomic::AtomicBool;
68///
69/// use girt::fetch::{FetchLimits, FetchRequest, FetchUpdateKind, discover_local};
70/// use girt::refs::{RefName, Reflog};
71/// use girt::remote::Remote;
72/// use girt::transport::TransportControl;
73/// # let (_directory, upstream, repo, main, _) = include!("../doctest/remote.rs");
74///
75/// let origin = Remote::find(repo.config(), b"origin")?.expect("configured");
76/// let request = FetchRequest::prepare(
77///     repo.clone(),
78///     origin.fetch_refspecs().clone(),
79///     BTreeSet::new(),
80///     Reflog::Preserve,
81/// )?;
82///
83/// let cancel = AtomicBool::new(false);
84/// let discovery = discover_local(
85///     upstream.git_dir(),
86///     FetchLimits::default(),
87///     TransportControl::new(&cancel),
88/// )?;
89///
90/// let updates = request.plan(&discovery.advertisement)?;
91/// assert_eq!(updates.len(), 1);
92/// assert_eq!(
93///     updates[0].mapping.destination,
94///     Some(RefName::new("refs/remotes/origin/main")?)
95/// );
96/// assert_eq!(updates[0].kind, FetchUpdateKind::Create);
97/// assert_eq!(
98///     updates[0].mapping.source.as_ref().map(|source| source.id),
99///     Some(main)
100/// );
101/// # Ok::<(), Box<dyn std::error::Error>>(())
102/// ```
103#[derive(Debug)]
104pub struct FetchRequest {
105    repository: Repository,
106    specs: Refspecs,
107    snapshot: BTreeMap<RefName, Target>,
108    authorized_force: BTreeSet<RefName>,
109    reflog: Reflog,
110    namespace_reflogs: Vec<(RefName, Reflog, Reflog)>,
111    kind_reflogs: Vec<(RefName, FetchUpdateKind, Reflog)>,
112    additional_tag_namespaces: Vec<RefName>,
113    retained_missing_sources: BTreeSet<Vec<u8>>,
114    prune: bool,
115    pub(super) depth: Option<NonZeroU32>,
116    shallow_before: Vec<ObjectId>,
117}
118
119impl FetchRequest {
120    /// Captures destination values and validates the supported worktree layout without writing.
121    ///
122    /// `authorized_force` authorizes replacement only for those exact destination names; it does
123    /// not turn an unforced refspec into a forced one. `reflog` chooses the default action for
124    /// changed refs; [`Self::with_reflog_for_namespace`] may override it. Reference enumeration has
125    /// its existing unbounded metadata allocation contract; transfer and object budgets are
126    /// supplied separately.
127    ///
128    /// # Errors
129    ///
130    /// Rejects push refspecs, unsupported HEAD chains, inaccessible worktrees, or reference errors.
131    /// Mapping-specific failures are returned when the transfer advertisement is available.
132    ///
133    /// # Examples
134    ///
135    /// A `+` refspec states the intent to force; `authorized_force` grants it. When upstream
136    /// rewrites `main`, the fetch refuses to move `refs/remotes/origin/main` backwards until the
137    /// caller authorizes that destination:
138    ///
139    /// ```
140    /// use std::collections::BTreeSet;
141    /// use std::sync::atomic::AtomicBool;
142    ///
143    /// use girt::fetch::{
144    ///     FetchFinishFailure, FetchLimits, FetchRequest, FetchUpdateError, FetchUpdateKind,
145    ///     FetchUpdateLimits, KnownHistory, LocalFetchObserver,
146    /// };
147    /// use girt::refs::{Expected, RefName, Reflog, Target};
148    /// use girt::remote::Remote;
149    /// use girt::transport::TransportControl;
150    /// # let (_directory, upstream, repo, main, _) = include!("../doctest/remote.rs");
151    /// # let commit = include!("../doctest/commit.rs");
152    ///
153    /// let cancel = AtomicBool::new(false);
154    /// let origin = Remote::find(repo.config(), b"origin")?.expect("configured");
155    /// let fetch = |authorized: BTreeSet<RefName>| -> Result<_, Box<dyn std::error::Error>> {
156    ///     let request = FetchRequest::prepare(
157    ///         repo.clone(),
158    ///         origin.fetch_refspecs().clone(),
159    ///         authorized,
160    ///         Reflog::Preserve,
161    ///     )?;
162    ///
163    ///     let ready = request.receive_local(
164    ///         upstream.git_dir(),
165    ///         &KnownHistory::default(),
166    ///         FetchLimits::default(),
167    ///         TransportControl::new(&cancel),
168    ///         LocalFetchObserver::default(),
169    ///     )?;
170    ///     Ok(ready.finish(FetchUpdateLimits::default(), &cancel))
171    /// };
172    /// fetch(BTreeSet::new())??;
173    ///
174    /// // Upstream replaces `main` with an unrelated commit.
175    /// let rewritten = commit(&upstream, &[], "Rewritten history")?;
176    /// let main_ref = RefName::new("refs/heads/main")?;
177    /// upstream.references()?.update_without_reflog(
178    ///     &main_ref,
179    ///     Target::Direct(rewritten),
180    ///     Expected::Value(Target::Direct(main)),
181    /// )?;
182    ///
183    /// // `+` alone is not enough.
184    /// let error = fetch(BTreeSet::new())?.unwrap_err();
185    /// assert!(matches!(
186    ///     *error.source,
187    ///     FetchFinishFailure::Update(FetchUpdateError::NonFastForward(_))
188    /// ));
189    ///
190    /// let tracking = RefName::new("refs/remotes/origin/main")?;
191    /// let report = fetch(BTreeSet::from([tracking.clone()]))??;
192    /// assert_eq!(report.updates[0].kind, FetchUpdateKind::ForcedTracking);
193    /// assert_eq!(
194    ///     repo.references()?.read(&tracking)?,
195    ///     Some(Target::Direct(rewritten))
196    /// );
197    /// # Ok::<(), Box<dyn std::error::Error>>(())
198    /// ```
199    pub fn prepare(
200        repository: Repository,
201        specs: Refspecs,
202        authorized_force: BTreeSet<RefName>,
203        reflog: Reflog,
204    ) -> Result<Self, FetchPlanError> {
205        Self::prepare_from(repository, specs, authorized_force, reflog, |repository| {
206            Ok(repository.references()?.list()?)
207        })
208    }
209
210    /// Prepares like [`Self::prepare`] from a reference listing the caller already took.
211    ///
212    /// The listing becomes the snapshot every edit is conditional on, so it must come from this
213    /// repository, and the caller must take it no earlier than anything else it derived from it.
214    pub(crate) fn prepare_listed(
215        repository: Repository,
216        specs: Refspecs,
217        authorized_force: BTreeSet<RefName>,
218        reflog: Reflog,
219        references: Vec<crate::refs::Reference>,
220    ) -> Result<Self, FetchPlanError> {
221        Self::prepare_from(repository, specs, authorized_force, reflog, |_| {
222            Ok(references)
223        })
224    }
225
226    fn prepare_from(
227        repository: Repository,
228        specs: Refspecs,
229        authorized_force: BTreeSet<RefName>,
230        reflog: Reflog,
231        list: impl FnOnce(&Repository) -> Result<Vec<crate::refs::Reference>, FetchPlanError>,
232    ) -> Result<Self, FetchPlanError> {
233        if specs.direction() != Direction::Fetch {
234            return Err(FetchPlanError::Mapping(MappingError::Direction));
235        }
236        super::worktree::check(&repository)?;
237        let references = list(&repository)?;
238        let snapshot = references
239            .into_iter()
240            .map(|reference| (reference.name, reference.target))
241            .collect();
242        let shallow_before = repository.shallow_roots().iter().collect();
243        Ok(Self {
244            repository,
245            specs,
246            snapshot,
247            authorized_force,
248            reflog,
249            namespace_reflogs: Vec::new(),
250            kind_reflogs: Vec::new(),
251            additional_tag_namespaces: Vec::new(),
252            retained_missing_sources: BTreeSet::new(),
253            prune: false,
254            depth: None,
255            shallow_before,
256        })
257    }
258
259    /// Allows tag-like destinations below one caller-owned reference namespace.
260    ///
261    /// Only descendants of `namespace` are added; the namespace itself remains unsupported.
262    /// Replacements require both force intent and authorization, as for `refs/tags/*`. When
263    /// pruning is enabled, absent sources can prune these destinations. The caller must choose
264    /// a namespace it owns and keep unrelated references outside it.
265    pub fn with_tag_destination_namespace(mut self, namespace: RefName) -> Self {
266        self.additional_tag_namespaces.push(namespace);
267        self
268    }
269
270    /// Chooses reflog actions for changed destinations below one reference namespace.
271    ///
272    /// `update` applies to creates and updates and must not be [`Reflog::Delete`]. `prune` applies
273    /// to deletions requested by
274    /// [`Self::with_prune`]. The most specific matching namespace wins. Names outside configured
275    /// namespaces keep the policy passed to [`Self::prepare`]. This does not authorize a
276    /// destination or alter its update rules. Use [`Reflog::Delete`] for `prune` when a removed
277    /// reference should lose its log, as Git does for pruned remote-tracking refs.
278    pub fn with_reflog_for_namespace(
279        mut self,
280        namespace: RefName,
281        update: Reflog,
282        prune: Reflog,
283    ) -> Self {
284        self.namespace_reflogs.push((namespace, update, prune));
285        self
286    }
287
288    /// Overrides a namespace's reflog action for one changed update kind.
289    ///
290    /// This permits different messages for creation, fast-forward, forced update and other
291    /// outcomes. The caller supplies the complete message and identity; this workflow does not
292    /// synthesize Git CLI command text. A kind-specific action takes priority over a matching
293    /// [`Self::with_reflog_for_namespace`] action, and the most specific matching namespace wins.
294    /// `SourceOnly` and `Unchanged` never write a reference or a reflog.
295    pub fn with_reflog_for_update_kind(
296        mut self,
297        namespace: RefName,
298        kind: FetchUpdateKind,
299        reflog: Reflog,
300    ) -> Self {
301        self.kind_reflogs.push((namespace, kind, reflog));
302        self
303    }
304
305    fn reflog_for(&self, name: &RefName, kind: FetchUpdateKind) -> Reflog {
306        if let Some((_, _, reflog)) = self
307            .kind_reflogs
308            .iter()
309            .filter(|(namespace, selected, _)| *selected == kind && is_descendant(name, namespace))
310            .max_by_key(|(namespace, _, _)| namespace.as_bytes().len())
311        {
312            return reflog.clone();
313        }
314        self.namespace_reflogs
315            .iter()
316            .filter(|(namespace, _, _)| is_descendant(name, namespace))
317            .max_by_key(|(namespace, _, _)| namespace.as_bytes().len())
318            .map(|(_, update, prune)| {
319                if kind == FetchUpdateKind::Prune {
320                    prune
321                } else {
322                    update
323                }
324            })
325            .unwrap_or(&self.reflog)
326            .clone()
327    }
328
329    fn additional_tag_destination(&self, name: &RefName) -> bool {
330        self.additional_tag_namespaces
331            .iter()
332            .any(|namespace| is_descendant(name, namespace))
333    }
334
335    /// Requests a positive commit depth on HTTP/SSH upload-pack or depth one from a native local
336    /// source. Local depth requires one selected commit tip and a nonshallow source. This permits
337    /// a resulting shallow-boundary change during coordinated publication.
338    /// Network validation materializes any reachable known-local dependencies in a nonempty
339    /// received pack. An empty object transfer verifies local dependencies during publication;
340    /// the caller must continue excluding concurrent collection for that existing history.
341    ///
342    /// # Examples
343    ///
344    /// Fetch only the newest commit of `main`, like `git fetch --depth 1`:
345    ///
346    /// ```
347    /// use std::collections::BTreeSet;
348    /// use std::num::NonZeroU32;
349    /// use std::sync::atomic::AtomicBool;
350    ///
351    /// use girt::fetch::{FetchLimits, FetchRequest, FetchUpdateLimits, KnownHistory, LocalFetchObserver};
352    /// use girt::refs::{Expected, RefName, Reflog, Target};
353    /// use girt::remote::Remote;
354    /// use girt::transport::TransportControl;
355    /// # let (_directory, upstream, repo, main, _) = include!("../doctest/remote.rs");
356    /// # let commit = include!("../doctest/commit.rs");
357    /// // Upstream has two commits on `main`.
358    /// let second = commit(&upstream, &[main], "Second commit")?;
359    /// # upstream.references()?.update_without_reflog(&RefName::new("refs/heads/main")?, Target::Direct(second), Expected::Value(Target::Direct(main)))?;
360    ///
361    /// let origin = Remote::find(repo.config(), b"origin")?.expect("configured");
362    /// let request =
363    ///     FetchRequest::prepare(repo.clone(), origin.fetch_refspecs().clone(), BTreeSet::new(), Reflog::Preserve)?
364    ///         .with_depth(NonZeroU32::MIN);
365    ///
366    /// let cancel = AtomicBool::new(false);
367    /// let ready = request.receive_local(
368    ///     upstream.git_dir(),
369    ///     &KnownHistory::default(),
370    ///     FetchLimits::default(),
371    ///     TransportControl::new(&cancel),
372    ///     LocalFetchObserver::default(),
373    /// )?;
374    ///
375    /// let report = ready.finish(FetchUpdateLimits::default(), &cancel)?;
376    /// assert!(report.shallow_published);
377    ///
378    /// // `second` is now a shallow boundary: its parent was not fetched.
379    /// let repo = girt::Repository::open(repo.git_dir())?;
380    /// assert!(repo.shallow_roots().iter().any(|root| root == second));
381    /// # Ok::<(), Box<dyn std::error::Error>>(())
382    /// ```
383    pub fn with_depth(mut self, depth: NonZeroU32) -> Self {
384        self.depth = Some(depth);
385        self
386    }
387
388    /// Enables deletion of stale direct remote-tracking and added tag-namespace destinations
389    /// owned by these refspecs. Exact snapshot values are required at publication. Built-in
390    /// `refs/tags/*` destinations and excluded sources are retained.
391    ///
392    /// Choose what happens to a pruned reference's reflog with
393    /// [`Self::with_reflog_for_update_kind`] and [`FetchUpdateKind::Prune`]; Git deletes it
394    /// ([`Reflog::Delete`]).
395    ///
396    /// # Examples
397    ///
398    /// ```
399    /// use std::collections::BTreeSet;
400    /// use std::sync::atomic::AtomicBool;
401    ///
402    /// use girt::fetch::{
403    ///     FetchLimits, FetchRequest, FetchUpdateKind, FetchUpdateLimits, KnownHistory,
404    ///     LocalFetchObserver,
405    /// };
406    /// use girt::refs::{Expected, RefName, Reflog, Target};
407    /// use girt::remote::Remote;
408    /// use girt::transport::TransportControl;
409    /// # let (_directory, upstream, repo, main, _) = include!("../doctest/remote.rs");
410    ///
411    /// let cancel = AtomicBool::new(false);
412    /// let origin = Remote::find(repo.config(), b"origin")?.expect("configured");
413    /// let fetch = || -> Result<_, Box<dyn std::error::Error>> {
414    ///     let request = FetchRequest::prepare(
415    ///         repo.clone(),
416    ///         origin.fetch_refspecs().clone(),
417    ///         BTreeSet::new(),
418    ///         Reflog::Preserve,
419    ///     )?
420    ///     .with_prune();
421    ///
422    ///     let ready = request.receive_local(
423    ///         upstream.git_dir(),
424    ///         &KnownHistory::default(),
425    ///         FetchLimits::default(),
426    ///         TransportControl::new(&cancel),
427    ///         LocalFetchObserver::default(),
428    ///     )?;
429    ///     Ok(ready.finish(FetchUpdateLimits::default(), &cancel)?)
430    /// };
431    ///
432    /// fetch()?;
433    ///
434    /// // `main` disappears upstream, so the next fetch prunes its remote-tracking reference.
435    /// let main_ref = RefName::new("refs/heads/main")?;
436    /// upstream
437    ///     .references()?
438    ///     .delete_without_reflog(&main_ref, Expected::Value(Target::Direct(main)))?;
439    ///
440    /// let report = fetch()?;
441    /// assert_eq!(report.updates[0].kind, FetchUpdateKind::Prune);
442    /// assert_eq!(
443    ///     repo.references()?
444    ///         .read(&RefName::new("refs/remotes/origin/main")?)?,
445    ///     None
446    /// );
447    /// # Ok::<(), Box<dyn std::error::Error>>(())
448    /// ```
449    pub fn with_prune(mut self) -> Self {
450        self.prune = true;
451        self
452    }
453
454    /// Retains destinations for absent exact sources while pruning other stale refs.
455    ///
456    /// The caller supplies only exact positive source names whose cached destinations Git would
457    /// keep after a missing-ref retry. This applies to the advertisement from this transfer, so
458    /// no second discovery or fetch is needed. The source remains selected if it is advertised.
459    pub fn with_retained_missing_sources(
460        mut self,
461        sources: impl IntoIterator<Item = RefName>,
462    ) -> Self {
463        self.retained_missing_sources
464            .extend(sources.into_iter().map(|source| source.as_bytes().to_vec()));
465        self
466    }
467
468    /// Maps an advertisement against the captured destination values, without I/O or mutation.
469    ///
470    /// Useful for previewing policy. Transfer adapters always recompute this plan from their own
471    /// advertisement; a preview cannot substitute IDs from an earlier server connection.
472    ///
473    /// # Errors
474    ///
475    /// Rejects mapping collisions/missing sources, unsupported destinations, symbolic stored
476    /// values, and tag replacements lacking both force intent and authorization. No objects are
477    /// requested.
478    pub fn plan(&self, advertisement: &Advertisement) -> Result<Vec<FetchUpdate>, FetchPlanError> {
479        let mut updates: Vec<FetchUpdate> = self
480            .specs
481            .map_advertisement_with_missing(advertisement)?
482            .0
483            .into_iter()
484            .map(|mapping| {
485                let Some(name) = &mapping.destination else {
486                    return Ok(FetchUpdate {
487                        mapping,
488                        previous: None,
489                        kind: FetchUpdateKind::SourceOnly,
490                    });
491                };
492                let bytes = name.as_bytes();
493                let tag_destination =
494                    bytes.starts_with(b"refs/tags/") || self.additional_tag_destination(name);
495                if !bytes.starts_with(b"refs/remotes/") && !tag_destination {
496                    return Err(FetchPlanError::Destination(name.clone()));
497                }
498                let previous = match self.snapshot.get(name) {
499                    Some(Target::Symbolic(_)) => {
500                        return Err(FetchPlanError::Symbolic(name.clone()));
501                    }
502                    Some(Target::Direct(id)) => Some(*id),
503                    None => None,
504                };
505                let new = mapping
506                    .source
507                    .as_ref()
508                    .expect("fetch mapping has a source")
509                    .id;
510                let kind = update_kind(
511                    name,
512                    previous,
513                    new,
514                    mapping.force == crate::remote::ForceIntent::Requested,
515                    self.authorized_force.contains(name),
516                    tag_destination,
517                )?;
518                Ok(FetchUpdate {
519                    mapping,
520                    previous,
521                    kind,
522                })
523            })
524            .collect::<Result<_, _>>()?;
525        if self.prune {
526            let advertised: BTreeSet<_> = advertisement
527                .refs
528                .iter()
529                .filter(|reference| reference.kind == crate::fetch::AdvertisementKind::Tip)
530                .map(|reference| reference.name.as_bytes().to_vec())
531                .collect();
532            let selected: BTreeSet<_> = updates
533                .iter()
534                .filter_map(|update| update.mapping.destination.as_ref())
535                .cloned()
536                .collect();
537            for (name, target) in &self.snapshot {
538                if !(name.as_bytes().starts_with(b"refs/remotes/")
539                    || self.additional_tag_destination(name))
540                    || selected.contains(name)
541                {
542                    continue;
543                }
544                let sources = self.specs.prune_sources(name);
545                if sources.is_empty()
546                    || sources.iter().any(|source| {
547                        advertised.contains(source)
548                            || self.retained_missing_sources.contains(source.as_slice())
549                    })
550                {
551                    continue;
552                }
553                let Target::Direct(previous) = target else {
554                    // Remote HEAD aliases are local symbolic policy, not stale remote tips.
555                    continue;
556                };
557                updates.push(FetchUpdate {
558                    mapping: Mapping {
559                        source: None,
560                        destination: Some(name.clone()),
561                        force: crate::remote::ForceIntent::Normal,
562                    },
563                    previous: Some(*previous),
564                    kind: FetchUpdateKind::Prune,
565                });
566            }
567        }
568        Ok(updates)
569    }
570
571    /// Transfers and validates synchronously using a caller-selected local upload-pack endpoint.
572    ///
573    /// Pass explicit verified `known` history for incremental negotiation, or an empty history for
574    /// a full transfer. The transport control bounds network/process lifetime, not later install.
575    /// Mapping failures select no wants, then return the saved planning error; no storage changes.
576    ///
577    /// # Errors
578    ///
579    /// Returns mapping/policy failures or [`super::receive_local`]'s transfer failures.
580    pub fn receive_local(
581        self,
582        source: impl AsRef<Path>,
583        known: &KnownHistory,
584        limits: FetchLimits,
585        control: TransportControl<'_>,
586        mut observer: super::LocalFetchObserver<'_>,
587    ) -> Result<FetchReady, FetchWorkflowError> {
588        self.receive_local_observed(
589            source,
590            known,
591            limits,
592            control,
593            |message| {
594                observer
595                    .messages
596                    .as_mut()
597                    .map_or(ControlFlow::Continue(()), |notify| notify(message))
598            },
599            |event| {
600                if let Some(notify) = observer.construction.as_mut() {
601                    notify(event);
602                }
603            },
604            |event| {
605                if let Some(notify) = observer.validation.as_mut() {
606                    notify(event);
607                }
608            },
609        )
610    }
611
612    #[expect(
613        clippy::too_many_arguments,
614        reason = "independent construction and validation observers"
615    )]
616    fn receive_local_observed(
617        self,
618        source: impl AsRef<Path>,
619        known: &KnownHistory,
620        limits: FetchLimits,
621        control: TransportControl<'_>,
622        progress: impl FnMut(&[u8]) -> ControlFlow<()>,
623        mut observe: impl FnMut(super::LocalFetchProgress),
624        validation: impl FnMut(super::ValidationProgress),
625    ) -> Result<FetchReady, FetchWorkflowError> {
626        self.check_known(known)?;
627        let mut plan = None;
628        let received = super::local::receive_local_observed(
629            source,
630            |advertisement| select(self.plan(advertisement), &mut plan),
631            known,
632            self.depth,
633            limits,
634            control,
635            progress,
636            |event| {
637                if event != super::LocalFetchProgress::Complete {
638                    observe(event);
639                }
640            },
641            validation,
642        );
643        let updates = selected(plan, &received)?;
644        let ready = FetchReady {
645            request: self,
646            updates,
647            received: received?,
648        };
649        control.check().map_err(FetchError::from)?;
650        observe(super::LocalFetchProgress::Complete);
651        Ok(ready)
652    }
653
654    /// Transfers from a caller-owned v0/v1 upload-pack stream using this request's depth policy.
655    /// The caller owns process/network lifetime and must close and reap it on every outcome.
656    ///
657    /// # Errors
658    ///
659    /// Returns planning or wire/pack validation errors without destination changes.
660    ///
661    /// # Examples
662    ///
663    /// Run a fetch over a `git upload-pack` process. It needs a Git executable, so it is not run:
664    ///
665    /// ```no_run
666    /// use std::collections::BTreeSet;
667    /// use std::ops::ControlFlow;
668    /// use std::process::{Command, Stdio};
669    /// use std::sync::atomic::AtomicBool;
670    ///
671    /// use girt::Repository;
672    /// use girt::fetch::{FetchLimits, FetchRequest, FetchUpdateLimits, KnownHistory};
673    /// use girt::refs::Reflog;
674    /// use girt::remote::Remote;
675    ///
676    /// let repo = Repository::open("project")?;
677    /// let origin = Remote::find(repo.config(), b"origin")?.ok_or("no origin")?;
678    /// let request = FetchRequest::prepare(
679    ///     repo.clone(),
680    ///     origin.fetch_refspecs().clone(),
681    ///     BTreeSet::new(),
682    ///     Reflog::Preserve,
683    /// )?;
684    ///
685    /// let mut server = Command::new("git")
686    ///     .args(["upload-pack", "/srv/git/project.git"])
687    ///     .stdin(Stdio::piped())
688    ///     .stdout(Stdio::piped())
689    ///     .spawn()?;
690    ///
691    /// let mut input = server.stdin.take().expect("piped");
692    /// let mut output = server.stdout.take().expect("piped");
693    ///
694    /// let cancel = AtomicBool::new(false);
695    /// let result = request.receive_session(
696    ///     &mut output,
697    ///     &mut input,
698    ///     &KnownHistory::default(),
699    ///     FetchLimits::default(),
700    ///     &cancel,
701    ///     |_message| ControlFlow::Continue(()),
702    /// );
703    ///
704    /// // Close the pipes and reap the process whatever happened; stop it first if the fetch failed.
705    /// drop((input, output));
706    /// if result.is_err() {
707    ///     server.kill().ok();
708    /// }
709    ///
710    /// let status = server.wait()?;
711    /// let ready = result?;
712    ///
713    /// // Publish only if the server also reported success.
714    /// if !status.success() {
715    ///     return Err(format!("git upload-pack failed: {status}").into());
716    /// }
717    /// ready.finish(FetchUpdateLimits::default(), &cancel)?;
718    /// # Ok::<(), Box<dyn std::error::Error>>(())
719    /// ```
720    pub fn receive_session(
721        self,
722        reader: &mut impl Read,
723        writer: &mut impl Write,
724        known: &KnownHistory,
725        limits: FetchLimits,
726        cancel: &AtomicBool,
727        progress: impl FnMut(&[u8]) -> ControlFlow<()>,
728    ) -> Result<FetchReady, FetchWorkflowError> {
729        self.check_known(known)?;
730        let mut plan = None;
731        let received = super::receive_with_known_depth(
732            reader,
733            writer,
734            |advertisement| select(self.plan(advertisement), &mut plan),
735            known,
736            super::FetchOptions {
737                limits,
738                depth: self.depth,
739            },
740            cancel,
741            progress,
742        );
743        let updates = selected(plan, &received)?;
744        Ok(FetchReady {
745            request: self,
746            updates,
747            received: received?,
748        })
749    }
750
751    pub(super) fn check_known(&self, known: &KnownHistory) -> Result<(), FetchError> {
752        if known.shallow != self.shallow_before {
753            return Err(FetchError::Unsupported(
754                "known shallow boundaries differ from destination",
755            ));
756        }
757        Ok(())
758    }
759}
760
761fn is_descendant(name: &RefName, namespace: &RefName) -> bool {
762    name.as_bytes()
763        .strip_prefix(namespace.as_bytes())
764        .is_some_and(|suffix| suffix.starts_with(b"/") && suffix.len() > 1)
765}
766
767fn update_kind(
768    name: &RefName,
769    previous: Option<ObjectId>,
770    new: ObjectId,
771    force: bool,
772    authorized: bool,
773    tag_destination: bool,
774) -> Result<FetchUpdateKind, FetchPlanError> {
775    match previous {
776        None => Ok(FetchUpdateKind::Create),
777        Some(old) if old == new => Ok(FetchUpdateKind::Unchanged),
778        Some(_) if tag_destination => {
779            if !force || !authorized {
780                return Err(FetchPlanError::TagReplacement(name.clone()));
781            }
782            Ok(FetchUpdateKind::ForcedTag)
783        }
784        Some(_) => Ok(FetchUpdateKind::Replace),
785    }
786}
787
788pub(super) fn select(
789    result: Result<Vec<FetchUpdate>, FetchPlanError>,
790    saved: &mut Option<Result<Vec<FetchUpdate>, FetchPlanError>>,
791) -> Vec<ObjectId> {
792    let wants = result
793        .as_ref()
794        .map(|updates| {
795            updates
796                .iter()
797                .filter_map(|update| update.mapping.source.as_ref().map(|s| s.id))
798                .collect()
799        })
800        .unwrap_or_default();
801    *saved = Some(result);
802    wants
803}
804
805pub(super) fn selected<T>(
806    plan: Option<Result<Vec<FetchUpdate>, FetchPlanError>>,
807    transfer: &Result<T, FetchError>,
808) -> Result<Vec<FetchUpdate>, FetchWorkflowError> {
809    match plan {
810        Some(plan) => Ok(plan?),
811        None if transfer.is_err() => Ok(Vec::new()),
812        None => Err(FetchError::Protocol("selection callback was not invoked").into()),
813    }
814}
815
816/// A validated transfer tied to its actual advertisement, destination snapshot and repository.
817///
818/// Owns all state needed for synchronous installation/publication; can move to a caller's worker.
819/// Dropping it has no storage side effects. No public API permits substituting a different
820/// transfer, destination or mapping after validation.
821///
822/// # Examples
823///
824/// Inspect a validated transfer before deciding to publish it:
825///
826/// ```
827/// use std::collections::BTreeSet;
828/// use std::sync::atomic::AtomicBool;
829///
830/// use girt::fetch::{
831///     FetchLimits, FetchRequest, FetchUpdateLimits, KnownHistory, LocalFetchObserver,
832/// };
833/// use girt::refs::Reflog;
834/// use girt::remote::Remote;
835/// use girt::transport::TransportControl;
836/// # let (_directory, upstream, repo, _, _) = include!("../doctest/remote.rs");
837///
838/// let origin = Remote::find(repo.config(), b"origin")?.expect("configured");
839/// let request = FetchRequest::prepare(
840///     repo.clone(),
841///     origin.fetch_refspecs().clone(),
842///     BTreeSet::new(),
843///     Reflog::Preserve,
844/// )?;
845///
846/// let cancel = AtomicBool::new(false);
847/// let ready = request.receive_local(
848///     upstream.git_dir(),
849///     &KnownHistory::default(),
850///     FetchLimits::default(),
851///     TransportControl::new(&cancel),
852///     LocalFetchObserver::default(),
853/// )?;
854///
855/// // A commit and its empty tree, nothing written yet.
856/// assert_eq!(ready.received().object_count(), 2);
857/// assert!(repo.references()?.list()?.is_empty());
858///
859/// let report = ready.finish(FetchUpdateLimits::default(), &cancel)?;
860/// assert!(report.installed.is_some());
861/// assert_eq!(report.references.len(), 1);
862/// # Ok::<(), Box<dyn std::error::Error>>(())
863/// ```
864#[derive(Debug)]
865pub struct FetchReady {
866    pub(super) request: FetchRequest,
867    pub(super) updates: Vec<FetchUpdate>,
868    pub(super) received: ReceivedFetch,
869}
870
871impl FetchReady {
872    // Clone supplies a fresh repository; use only the actual transfer advertisement.
873    pub(crate) fn for_clone(
874        request: FetchRequest,
875        received: ReceivedFetch,
876    ) -> Result<Self, FetchPlanError> {
877        let updates = request.plan(received.advertisement())?;
878        Ok(Self {
879            request,
880            updates,
881            received,
882        })
883    }
884
885    /// The mapping and decisions derived from the advertisement used by this transfer.
886    pub fn updates(&self) -> &[FetchUpdate] {
887        &self.updates
888    }
889
890    /// Validated object response, available for inspecting advertisement and transfer statistics.
891    pub fn received(&self) -> &ReceivedFetch {
892        &self.received
893    }
894
895    /// Installs a self-contained pack under retention before publishing shallow metadata or refs.
896    ///
897    /// Supports self-contained complete and depth-limited transfers, including refresh of an
898    /// existing shallow destination. Known-local dependencies and empty transfers are refused.
899    /// Holds the conditional shallow lock from installation through the returned phase's
900    /// publication, including depth requests whose response has no boundaries. An existing pack or
901    /// index is refused because a collector may already have selected it for removal.
902    ///
903    /// The returned phase permits caller-specific object checks before any shallow or reference
904    /// publication. Call [`RetainedFetchReady::finish`] to publish, or
905    /// [`RetainedFetchReady::into_recovery`] to abandon the phase with its effects and retention.
906    /// Dropping the phase releases its shallow lock but leaves the retention marker on disk.
907    /// The caller still owns the GC and HEAD/worktree exclusions on [`FetchRequest`]; retention
908    /// protects only this newly installed pack. No references or reflogs change in this method.
909    ///
910    /// # Errors
911    ///
912    /// Returns validation, shallow-lock, or installation failures with completed effects. Once
913    /// retention is acquired, errors retain its handle; a failed installation can leave partial
914    /// pack artifacts even when the report has no successful installation. Do not retry through
915    /// another fetch implementation after acquiring retention without inspecting those effects.
916    pub fn install_retained(
917        mut self,
918        limits: FetchUpdateLimits,
919        cancel: &AtomicBool,
920    ) -> Result<RetainedFetchReady, RetainedFetchFinishError> {
921        let mut report = self.take_report();
922        let prepare = || {
923            let checksum = self
924                .received
925                .retention_checksum(&self.request.repository, cancel)
926                .map_err(FetchFinishFailure::Installation)?;
927            let shallow_lock = self.lock_shallow(true, cancel)?;
928            Ok((checksum, shallow_lock))
929        };
930        let (checksum, shallow_lock) = match prepare() {
931            Ok(prepared) => prepared,
932            Err(source) => {
933                return Err(RetainedFetchFinishError {
934                    report: Box::new(report),
935                    source,
936                    retention: None,
937                });
938            }
939        };
940        let installation =
941            super::retention::install_with(&self.request.repository, checksum, || {
942                self.received.install_for_workflow(
943                    &self.request.repository,
944                    limits.snapshot,
945                    cancel,
946                    shallow_lock.is_some(),
947                )
948            });
949        let (installed, retention) = match installation {
950            Ok(result) => result,
951            Err(error) => {
952                return Err(RetainedFetchFinishError {
953                    report: Box::new(report),
954                    source: FetchFinishFailure::Installation(*error.source).into(),
955                    retention: error.retention,
956                });
957            }
958        };
959        report.installed = Some(installed);
960        Ok(RetainedFetchReady {
961            ready: self,
962            report,
963            limits,
964            shallow_lock,
965            retention,
966        })
967    }
968
969    /// Installs validated objects and shallow metadata, then publishes changed destinations.
970    ///
971    /// Rechecks known-local dependencies during installation, installed selected-tip readability,
972    /// update ancestry and supported HEAD
973    /// layout before publication. All changed destinations use the exact values captured by
974    /// preparation, including absence. Source-only and unchanged entries produce no reference
975    /// edits or reflog entries. Object installation can succeed even when later checks or
976    /// reference publication fail. A shallow metadata change can also remain after a later
977    /// reference failure. Cancellation is checked before installation and before the
978    /// transaction, but does not interrupt a started reference transaction. Observe completion
979    /// before releasing worker resources.
980    ///
981    /// The caller must coordinate GC and worktree/HEAD changes as described on [`FetchRequest`].
982    /// Transaction locks do not protect objects from external deletion. Readers can observe partial
983    /// reference publication; neither whole-operation rollback nor crash durability is promised.
984    ///
985    /// # Errors
986    ///
987    /// The returned report distinguishes installation success from publication. An installation
988    /// failure can leave an unindexed pack (see [`ReceivedFetch::install`]); no ref edits were
989    /// made. After successful installation, all installed objects remain on error. Transaction
990    /// errors retain exact per-ref/per-log partial effects. No automatic cleanup, rollback or
991    /// retry occurs.
992    pub fn finish(
993        mut self,
994        limits: FetchUpdateLimits,
995        cancel: &AtomicBool,
996    ) -> Result<FetchReport, FetchFinishError> {
997        #[cfg(feature = "tracing")]
998        let span = tracing::debug_span!(
999            target: "girt",
1000            "fetch.finish",
1001            outcome = "incomplete",
1002            failure_class = tracing::field::Empty,
1003            effects = tracing::field::Empty,
1004        );
1005
1006        let operation = || {
1007            let mut report = self.take_report();
1008            let result = self.install_publish(limits, cancel, &mut report);
1009            match result {
1010                Ok(()) => Ok(report),
1011                Err(source) => Err(FetchFinishError {
1012                    report: Box::new(report),
1013                    source,
1014                }),
1015            }
1016        };
1017        #[cfg(feature = "tracing")]
1018        let result = span.in_scope(operation);
1019        #[cfg(not(feature = "tracing"))]
1020        let result = { operation }();
1021        #[cfg(feature = "tracing")]
1022        crate::trace::finish(&span, &result, |error| {
1023            crate::trace::fetch_finish(error, &span)
1024        });
1025
1026        result
1027    }
1028
1029    fn install_publish(
1030        &self,
1031        limits: FetchUpdateLimits,
1032        cancel: &AtomicBool,
1033        report: &mut FetchReport,
1034    ) -> Result<(), Box<FetchFinishFailure>> {
1035        let mut shallow_lock = self.lock_shallow(false, cancel)?;
1036        report.installed = Some(
1037            self.received
1038                .install_for_workflow(
1039                    &self.request.repository,
1040                    limits.snapshot,
1041                    cancel,
1042                    shallow_lock.is_some(),
1043                )
1044                .map_err(FetchFinishFailure::Installation)?,
1045        );
1046        self.publish_installed(limits, cancel, &mut shallow_lock, report)
1047    }
1048
1049    fn take_report(&mut self) -> FetchReport {
1050        FetchReport {
1051            updates: std::mem::take(&mut self.updates),
1052            pack_bytes: self.received.pack_bytes(),
1053            objects: self.received.object_count(),
1054            installed: None,
1055            references: Vec::new(),
1056            shallow_published: false,
1057            missing: self
1058                .request
1059                .specs
1060                .map_advertisement_with_missing(self.received.advertisement())
1061                .expect("validated fetch mapping")
1062                .1,
1063        }
1064    }
1065
1066    fn lock_shallow(
1067        &self,
1068        retained: bool,
1069        cancel: &AtomicBool,
1070    ) -> Result<Option<super::shallow::Lock>, Box<FetchFinishFailure>> {
1071        let resulting_roots = self.received.shallow_roots();
1072        if self.received.wants().is_empty() && self.request.shallow_before != resulting_roots {
1073            return Err(FetchFinishFailure::Safety(FetchPlanError::Shallow).into());
1074        }
1075        if self.request.depth.is_none() && self.request.shallow_before != resulting_roots {
1076            return Err(FetchFinishFailure::Safety(FetchPlanError::Shallow).into());
1077        }
1078        let shallow_lock =
1079            if retained || !self.request.shallow_before.is_empty() || !resulting_roots.is_empty() {
1080                Some(
1081                    super::shallow::Lock::acquire(
1082                        self.request.repository.common_dir(),
1083                        self.request.repository.object_format(),
1084                        &self.request.shallow_before,
1085                        cancel,
1086                    )
1087                    .map_err(FetchFinishFailure::Shallow)?,
1088                )
1089            } else {
1090                None
1091            };
1092        Ok(shallow_lock)
1093    }
1094
1095    fn publish_installed(
1096        &self,
1097        limits: FetchUpdateLimits,
1098        cancel: &AtomicBool,
1099        shallow_lock: &mut Option<super::shallow::Lock>,
1100        report: &mut FetchReport,
1101    ) -> Result<(), Box<FetchFinishFailure>> {
1102        let resulting_roots = self.received.shallow_roots();
1103        if !self.received.wants().is_empty() && self.request.shallow_before != resulting_roots {
1104            let objects = self
1105                .request
1106                .repository
1107                .objects_with(limits.snapshot)
1108                .map_err(|error| {
1109                    FetchFinishFailure::BeforePublication(FetchError::Destination(error))
1110                })?;
1111            let mut verify_roots = self.received.wants().to_vec();
1112            verify_roots.extend(self.request.shallow_before.iter().copied());
1113            let verified = KnownHistory::new_with_boundaries(
1114                &objects,
1115                &verify_roots,
1116                resulting_roots,
1117                limits.verification,
1118                cancel,
1119            )
1120            .map_err(FetchFinishFailure::BeforePublication)?;
1121            if resulting_roots
1122                .iter()
1123                .any(|root| !verified.objects.contains_key(root))
1124            {
1125                return Err(FetchFinishFailure::BeforePublication(FetchError::Protocol(
1126                    "unrelated shallow boundary",
1127                ))
1128                .into());
1129            }
1130        }
1131        if let Some(lock) = shallow_lock {
1132            report.shallow_published = lock
1133                .publish(resulting_roots)
1134                .map_err(FetchFinishFailure::Shallow)?;
1135        }
1136        super::check_cancelled(cancel).map_err(FetchFinishFailure::BeforePublication)?;
1137        super::worktree::check(&self.request.repository).map_err(FetchFinishFailure::Safety)?;
1138        if !self.received.wants().is_empty() {
1139            let reopened = Repository::open(self.request.repository.git_dir())
1140                .map_err(FetchFinishFailure::Reopen)?;
1141            let objects = reopened.objects_with(limits.snapshot).map_err(|error| {
1142                FetchFinishFailure::BeforePublication(FetchError::Destination(error))
1143            })?;
1144            // Prove the received representation is still installed, and honor destination lookup
1145            // precedence before any ref can name this graph. Different copies stay checked.
1146            // History reachable from references that existed before the fetch was verified when
1147            // those references were published; only new history is walked. Validation found the
1148            // complete dependencies through verified edges from those same references.
1149            let mut verify_roots = self.received.wants().to_vec();
1150            verify_roots.extend(self.request.shallow_before.iter().copied());
1151            let mut published: crate::IdSet = self
1152                .request
1153                .snapshot
1154                .values()
1155                .filter_map(|target| match target {
1156                    Target::Direct(id) => Some(*id),
1157                    Target::Symbolic(_) => None,
1158                })
1159                .collect();
1160            // Stop only where the representation lookup now selects has the kind that evidence
1161            // requires; anything else fails before a reference can name this graph.
1162            for (&id, &required) in self.received.complete_dependencies() {
1163                super::check_cancelled(cancel).map_err(FetchFinishFailure::BeforePublication)?;
1164                let located = objects
1165                    .locate(id, limits.verification.known_read, cancel)
1166                    .map_err(|source| {
1167                        FetchFinishFailure::BeforePublication(FetchError::LocalRead { id, source })
1168                    })?;
1169                match located {
1170                    None => {
1171                        return Err(
1172                            FetchFinishFailure::BeforePublication(FetchError::Missing(id)).into(),
1173                        );
1174                    }
1175                    Some((kind, _)) if kind != required => {
1176                        return Err(
1177                            FetchFinishFailure::BeforePublication(FetchError::Kind(id)).into()
1178                        );
1179                    }
1180                    Some(_) => {
1181                        published.insert(id);
1182                    }
1183                }
1184            }
1185            let shallow: Vec<_> = objects.shallow_roots().iter().collect();
1186            let verified =
1187                super::verified::InstalledObjects::bind(&objects, &self.received, cancel)
1188                    .map_err(FetchFinishFailure::BeforePublication)?;
1189            KnownHistory::verify_installed(
1190                &verified,
1191                &verify_roots,
1192                self.received.dependencies(),
1193                &shallow,
1194                &published,
1195                limits.verification,
1196                cancel,
1197            )
1198            .map_err(FetchFinishFailure::BeforePublication)?;
1199            let validation = super::update::validate(
1200                &mut report.updates,
1201                &verified,
1202                &self.request.authorized_force,
1203                limits,
1204                cancel,
1205            );
1206            validation.map_err(FetchFinishFailure::Update)?;
1207        }
1208        super::check_cancelled(cancel).map_err(FetchFinishFailure::BeforePublication)?;
1209        let edits: Vec<_> = report
1210            .updates
1211            .iter()
1212            .filter(|update| update.kind.changes_ref())
1213            .map(|update| {
1214                let name = update
1215                    .mapping
1216                    .destination
1217                    .as_ref()
1218                    .expect("changed destination");
1219                RefEdit {
1220                    name: name.clone(),
1221                    dereference: crate::refs::Dereference::Stored,
1222                    target: update
1223                        .mapping
1224                        .source
1225                        .as_ref()
1226                        .map(|source| Target::Direct(source.id)),
1227                    expected: update
1228                        .previous
1229                        .map(|id| Expected::Value(Target::Direct(id)))
1230                        .unwrap_or(Expected::Absent),
1231                    reflog: self.request.reflog_for(name, update.kind),
1232                }
1233            })
1234            .collect();
1235        let refs = self
1236            .request
1237            .repository
1238            .references()
1239            .map_err(FetchPlanError::from)
1240            .map_err(FetchFinishFailure::Safety)?;
1241        // Like `git fetch --prune`, prune before updating so a pruned name can become a
1242        // namespace for new references (or the reverse).
1243        let (prunes, updates): (Vec<_>, Vec<_>) =
1244            edits.into_iter().partition(|edit| edit.target.is_none());
1245        report.references = refs
1246            .transaction(&prunes)
1247            .map_err(FetchFinishFailure::Publication)?;
1248        match refs.transaction(&updates) {
1249            Ok(outcomes) => report.references.extend(outcomes),
1250            Err(crate::refs::TransactionError::Prepare { source, .. })
1251                if !report.references.is_empty() =>
1252            {
1253                return Err(Box::new(FetchFinishFailure::Publication(
1254                    crate::refs::TransactionError::Publish {
1255                        outcomes: std::mem::take(&mut report.references),
1256                        source,
1257                    },
1258                )));
1259            }
1260            Err(crate::refs::TransactionError::Publish {
1261                mut outcomes,
1262                source,
1263            }) => {
1264                let mut all = std::mem::take(&mut report.references);
1265                all.append(&mut outcomes);
1266                return Err(Box::new(FetchFinishFailure::Publication(
1267                    crate::refs::TransactionError::Publish {
1268                        outcomes: all,
1269                        source,
1270                    },
1271                )));
1272            }
1273            Err(error) => return Err(Box::new(FetchFinishFailure::Publication(error))),
1274        }
1275        Ok(())
1276    }
1277}
1278
1279/// An installed pack retained through caller validation and shallow/reference publication.
1280///
1281/// Owns the same shallow lock acquired before installation. No shallow metadata, refs, or reflogs
1282/// have changed yet. Read [`Self::updates`] and installed objects to apply caller-specific checks,
1283/// then consume this phase with [`Self::finish`] or [`Self::into_recovery`]. Dropping it releases
1284/// the lock but deliberately leaves its Git `.keep` marker for recovery.
1285///
1286/// # Examples
1287///
1288/// Install the pack, check it before publishing anything, then publish and release the
1289/// retention marker:
1290///
1291/// ```
1292/// use std::collections::BTreeSet;
1293/// use std::sync::atomic::AtomicBool;
1294///
1295/// use girt::fetch::{
1296///     FetchLimits, FetchRequest, FetchUpdateLimits, KnownHistory, LocalFetchObserver,
1297/// };
1298/// use girt::refs::{RefName, Reflog, Target};
1299/// use girt::remote::Remote;
1300/// use girt::transport::TransportControl;
1301/// # let (_directory, upstream, repo, main, _) = include!("../doctest/remote.rs");
1302///
1303/// let origin = Remote::find(repo.config(), b"origin")?.expect("configured");
1304/// let request = FetchRequest::prepare(
1305///     repo.clone(),
1306///     origin.fetch_refspecs().clone(),
1307///     BTreeSet::new(),
1308///     Reflog::Preserve,
1309/// )?;
1310///
1311/// let cancel = AtomicBool::new(false);
1312/// let ready = request.receive_local(
1313///     upstream.git_dir(),
1314///     &KnownHistory::default(),
1315///     FetchLimits::default(),
1316///     TransportControl::new(&cancel),
1317///     LocalFetchObserver::default(),
1318/// )?;
1319///
1320/// let retained = ready.install_retained(FetchUpdateLimits::default(), &cancel)?;
1321/// // The pack is installed and protected; no reference has moved yet.
1322/// assert!(retained.report().installed.is_some());
1323/// let tracking = RefName::new("refs/remotes/origin/main")?;
1324/// assert_eq!(repo.references()?.read(&tracking)?, None);
1325///
1326/// // ... the application's own checks of the installed objects go here ...
1327///
1328/// let (report, retention) = retained.finish(&cancel)?;
1329/// assert_eq!(report.references.len(), 1);
1330/// assert_eq!(
1331///     repo.references()?.read(&tracking)?,
1332///     Some(Target::Direct(main))
1333/// );
1334/// retention.release()?;
1335/// # Ok::<(), Box<dyn std::error::Error>>(())
1336/// ```
1337#[derive(Debug)]
1338#[must_use = "publish references or recover the retained installation explicitly"]
1339pub struct RetainedFetchReady {
1340    ready: FetchReady,
1341    report: FetchReport,
1342    limits: FetchUpdateLimits,
1343    shallow_lock: Option<super::shallow::Lock>,
1344    retention: super::FetchRetention,
1345}
1346
1347impl RetainedFetchReady {
1348    /// Original planned updates, before final object-kind and ancestry validation.
1349    pub fn updates(&self) -> &[FetchUpdate] {
1350        &self.report.updates
1351    }
1352
1353    /// Completed installation effects; shallow and reference publication have not started.
1354    pub fn report(&self) -> &FetchReport {
1355        &self.report
1356    }
1357
1358    /// Owned pack retention, available for recording its recovery path.
1359    pub fn retention(&self) -> &super::FetchRetention {
1360        &self.retention
1361    }
1362
1363    /// Publishes shallow boundaries and conditional refs while holding the installation's lock.
1364    ///
1365    /// Performs the same installed-object, ancestry, worktree and reference checks as
1366    /// [`FetchReady::finish`], without installing again or reacquiring the shallow lock.
1367    /// Returns retention for explicit release after persistent roots are established. Source-only
1368    /// callers must establish their own roots before releasing it.
1369    ///
1370    /// # Errors
1371    ///
1372    /// Returns completed installation/shallow effects, exact transaction failure details, and
1373    /// retention. Objects and published boundaries remain on failure; dropping the error leaves
1374    /// the marker intact. No rollback or retry occurs. See [`FetchReady::finish`] for the
1375    /// reference transaction's cancellation and partial-publication contract.
1376    pub fn finish(
1377        mut self,
1378        cancel: &AtomicBool,
1379    ) -> Result<(FetchReport, super::FetchRetention), RetainedFetchFinishError> {
1380        let result = self.ready.publish_installed(
1381            self.limits,
1382            cancel,
1383            &mut self.shallow_lock,
1384            &mut self.report,
1385        );
1386        match result {
1387            Ok(()) => Ok((self.report, self.retention)),
1388            Err(source) => Err(RetainedFetchFinishError {
1389                report: Box::new(self.report),
1390                source,
1391                retention: Some(self.retention),
1392            }),
1393        }
1394    }
1395
1396    /// Abandons publication and releases the shallow lock, preserving effects and pack retention.
1397    ///
1398    /// No shallow metadata or references have changed. The caller can retain the returned marker
1399    /// while inspecting objects, or explicitly release it when abandoning the installation.
1400    pub fn into_recovery(self) -> (FetchReport, super::FetchRetention) {
1401        (self.report, self.retention)
1402    }
1403}
1404
1405/// Retained workflow failure with completed effects and ownership of any acquired marker.
1406///
1407/// Like [`FetchFinishError`], with one addition: if the `.keep` marker was acquired,
1408/// `retention` owns it. Dropping the error leaves the marker on disk, protecting the installed
1409/// pack from repacking. Release it with [`FetchRetention::release`](super::FetchRetention::release)
1410/// once references that keep the objects reachable are published, or when abandoning them.
1411#[derive(Debug, thiserror::Error)]
1412#[error("fetch finish failed")]
1413#[non_exhaustive]
1414pub struct RetainedFetchFinishError {
1415    /// Completed effects. No successful installation does not exclude partial pack artifacts.
1416    pub report: Box<FetchReport>,
1417    /// Failed phase, including exact partial reference and reflog effects on transaction failure.
1418    #[source]
1419    pub source: Box<FetchFinishFailure>,
1420    /// Acquired retention; dropping this error leaves its marker on disk for recovery.
1421    pub retention: Option<super::FetchRetention>,
1422}
1423
1424/// One advertised source and its proposed local effect, preserving refspec order.
1425#[derive(Clone, Debug)]
1426#[non_exhaustive]
1427pub struct FetchUpdate {
1428    /// Actual advertised source, optional destination and force intent.
1429    pub mapping: Mapping,
1430    /// Captured direct destination value, or absence (also `None` for source-only selections).
1431    pub previous: Option<ObjectId>,
1432    /// Update rule applied to the captured values.
1433    pub kind: FetchUpdateKind,
1434}
1435
1436/// Decision based on the advertised identity and captured destination, before publication.
1437#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1438#[non_exhaustive]
1439pub enum FetchUpdateKind {
1440    /// Objects selected without a reference destination; no `FETCH_HEAD` is written.
1441    SourceOnly,
1442    /// Destination was absent.
1443    Create,
1444    /// Destination already named the advertised ID; it will not be locked or rewritten.
1445    Unchanged,
1446    /// Remote-tracking replacement awaiting object checks; after successful finish, at least one
1447    /// tip does not peel to a commit, so no ancestry rule applies.
1448    Replace,
1449    /// Both tips peel to commits and the old commit is an ancestor of the new commit.
1450    FastForward,
1451    /// Non-fast-forward commit-valued remote-tracking replacement with intent and authorization.
1452    ForcedTracking,
1453    /// Explicitly authorized replacement of a tag with a forced refspec.
1454    ForcedTag,
1455    /// Delete a stale direct remote-tracking reference in the requested prune scope.
1456    Prune,
1457}
1458impl FetchUpdateKind {
1459    fn changes_ref(self) -> bool {
1460        matches!(
1461            self,
1462            Self::Create
1463                | Self::Replace
1464                | Self::FastForward
1465                | Self::ForcedTracking
1466                | Self::ForcedTag
1467                | Self::Prune
1468        )
1469    }
1470}
1471
1472/// Transfer statistics and separately observable installation/publication effects.
1473///
1474/// # Examples
1475///
1476/// Summarize a fetch the way `git fetch` reports it:
1477///
1478/// ```
1479/// use std::collections::BTreeSet;
1480/// use std::sync::atomic::AtomicBool;
1481///
1482/// use girt::fetch::{
1483///     FetchLimits, FetchRequest, FetchUpdateKind, FetchUpdateLimits, KnownHistory,
1484///     LocalFetchObserver,
1485/// };
1486/// use girt::refs::Reflog;
1487/// use girt::remote::Remote;
1488/// use girt::transport::TransportControl;
1489/// # let (_directory, upstream, repo, main, _) = include!("../doctest/remote.rs");
1490///
1491/// let origin = Remote::find(repo.config(), b"origin")?.expect("configured");
1492/// let request = FetchRequest::prepare(
1493///     repo.clone(),
1494///     origin.fetch_refspecs().clone(),
1495///     BTreeSet::new(),
1496///     Reflog::Preserve,
1497/// )?;
1498///
1499/// let cancel = AtomicBool::new(false);
1500/// let ready = request.receive_local(
1501///     upstream.git_dir(),
1502///     &KnownHistory::default(),
1503///     FetchLimits::default(),
1504///     TransportControl::new(&cancel),
1505///     LocalFetchObserver::default(),
1506/// )?;
1507///
1508/// let report = ready.finish(FetchUpdateLimits::default(), &cancel)?;
1509///
1510/// for update in &report.updates {
1511///     let Some(destination) = &update.mapping.destination else {
1512///         continue;
1513///     };
1514///
1515///     let marker = match update.kind {
1516///         FetchUpdateKind::Create => "* [new]",
1517///         FetchUpdateKind::FastForward => "  [updated]",
1518///         FetchUpdateKind::ForcedTracking | FetchUpdateKind::ForcedTag => "+ [forced]",
1519///         FetchUpdateKind::Prune => "- [deleted]",
1520///         _ => continue,
1521///     };
1522///     println!(
1523///         "{marker} {}",
1524///         String::from_utf8_lossy(destination.as_bytes())
1525///     );
1526/// }
1527///
1528/// assert_eq!(report.updates[0].kind, FetchUpdateKind::Create);
1529/// assert_eq!(report.objects, 2);
1530/// # let _ = main;
1531/// # Ok::<(), Box<dyn std::error::Error>>(())
1532/// ```
1533#[derive(Debug)]
1534#[non_exhaustive]
1535pub struct FetchReport {
1536    /// Decisions against the captured local values; not evidence of publication by themselves.
1537    pub updates: Vec<FetchUpdate>,
1538    /// Validated pack bytes, zero for an empty or known-only selection.
1539    pub pack_bytes: usize,
1540    /// Verified received objects, including extras; not a count of newly stored objects.
1541    pub objects: usize,
1542    /// Successful installation/reuse, including known-only installation with no pack.
1543    /// `None` on failure does not exclude an unindexed residual pack.
1544    pub installed: Option<FetchInstalled>,
1545    /// Shallow metadata was replaced after object installation, before reference publication.
1546    pub shallow_published: bool,
1547    /// Successful transaction effects for changed entries only. On transaction failure, consult
1548    /// [`FetchFinishFailure::Publication`] for partial effects instead.
1549    pub references: Vec<RefEditOutcome>,
1550    /// Exact positive sources absent from the advertisement; other valid selections continue.
1551    pub missing: Vec<Vec<u8>>,
1552}
1553
1554/// Planning failed without changing repository contents.
1555///
1556/// # Examples
1557///
1558/// A fetch may not write local branches, so a refspec that maps into `refs/heads/` is refused
1559/// once the advertisement is known, before anything is transferred:
1560///
1561/// ```
1562/// use std::collections::BTreeSet;
1563/// use std::sync::atomic::AtomicBool;
1564///
1565/// use girt::fetch::{
1566///     FetchLimits, FetchPlanError, FetchRequest, FetchWorkflowError, KnownHistory,
1567///     LocalFetchObserver,
1568/// };
1569/// use girt::refs::Reflog;
1570/// use girt::remote::{Direction, Refspecs};
1571/// use girt::transport::TransportControl;
1572/// # let (_directory, upstream, repo, _, _) = include!("../doctest/remote.rs");
1573///
1574/// let specs = Refspecs::parse(Direction::Fetch, ["refs/heads/main:refs/heads/main"])?;
1575/// let request = FetchRequest::prepare(repo.clone(), specs, BTreeSet::new(), Reflog::Preserve)?;
1576///
1577/// let cancel = AtomicBool::new(false);
1578/// let result = request.receive_local(
1579///     upstream.git_dir(),
1580///     &KnownHistory::default(),
1581///     FetchLimits::default(),
1582///     TransportControl::new(&cancel),
1583///     LocalFetchObserver::default(),
1584/// );
1585///
1586/// assert!(matches!(
1587///     result,
1588///     Err(FetchWorkflowError::Plan(FetchPlanError::Destination(_)))
1589/// ));
1590/// # Ok::<(), Box<dyn std::error::Error>>(())
1591/// ```
1592#[derive(Debug, thiserror::Error)]
1593#[non_exhaustive]
1594pub enum FetchPlanError {
1595    /// Received boundaries differ without an explicit depth request.
1596    #[error("shallow boundary change requires explicit depth request")]
1597    Shallow,
1598    /// Invalid or ambiguous advertisement/refspec mapping.
1599    #[error(transparent)]
1600    Mapping(#[from] MappingError),
1601    /// A destination is outside the supported remote-tracking and tag namespaces.
1602    #[error("unsupported fetch destination: {0:?}")]
1603    Destination(RefName),
1604    /// Updating a symbolic destination could bypass namespace rules.
1605    #[error("symbolic fetch destination: {0:?}")]
1606    Symbolic(RefName),
1607    /// Existing tag replacement requires both force intent and explicit name authorization.
1608    #[error("tag replacement requires force intent and authorization: {0:?}")]
1609    TagReplacement(RefName),
1610    /// Reference metadata could not be read.
1611    #[error(transparent)]
1612    Reference(#[from] ReferenceError),
1613    /// A main/linked worktree could not be opened; stale registrations fail closed.
1614    #[error(transparent)]
1615    Worktree(#[from] crate::OpenError),
1616    /// Worktree enumeration failed.
1617    #[error("worktree enumeration")]
1618    Io(#[from] std::io::Error),
1619    /// HEAD or its symbolic branch chain escapes the protected branch namespace.
1620    #[error("unsupported HEAD chain at {0:?}")]
1621    Head(RefName),
1622}
1623
1624/// A failure before any installation or reference publication.
1625///
1626/// The repository is unchanged, so the fetch can be retried as is.
1627/// [`Plan`](Self::Plan) means the refspecs or destination policy cannot apply to what the remote
1628/// advertised; retrying won't help without changing them. [`Transfer`](Self::Transfer) covers
1629/// the transport, the protocol, validation, limits, cancellation and deadlines; see
1630/// [`FetchError`] for each cause.
1631#[derive(Debug, thiserror::Error)]
1632#[non_exhaustive]
1633pub enum FetchWorkflowError {
1634    /// Mapping or destination policy rejected the actual advertisement.
1635    #[error(transparent)]
1636    Plan(#[from] FetchPlanError),
1637    /// Transfer or synchronous validation failed, with no installed objects.
1638    #[error(transparent)]
1639    Transfer(#[from] FetchError),
1640}
1641
1642/// Installation/publication failure retaining completed effects.
1643///
1644/// Returned by [`FetchReady::finish`] after the transfer was validated. Nothing is rolled back,
1645/// so inspect `report`, and [`FetchFinishFailure`] for how far the fetch got, before deciding
1646/// what to do:
1647///
1648/// - `report.installed` is `Some` only if the pack was installed; a failed installation can leave
1649///   an unindexed pack file behind.
1650/// - `report.shallow_published` says whether `.git/shallow` changed.
1651/// - A [`FetchFinishFailure::Publication`] error carries the reference and reflog edits that were
1652///   published before the failure.
1653///
1654/// Fetching again with a fresh [`FetchRequest`] is always safe. See [`FetchRequest::prepare`] for
1655/// an example that matches a refused non-fast-forward update.
1656#[derive(Debug, thiserror::Error)]
1657#[error("fetch finish failed")]
1658#[non_exhaustive]
1659pub struct FetchFinishError {
1660    /// Transfer and installation effects before failure; installed objects are never rolled back.
1661    pub report: Box<FetchReport>,
1662    /// Failed phase, including the existing transaction's partial-effect contract.
1663    /// Boxed to keep the returned error compact across platforms.
1664    #[source]
1665    pub source: Box<FetchFinishFailure>,
1666}
1667
1668/// Failed phase after validated transfer.
1669///
1670/// [`FetchReady::finish`] takes the shallow lock if the boundaries change or the destination is
1671/// already shallow, installs the pack,
1672/// verifies the installed history, publishes the shallow file, rechecks worktrees, checks the
1673/// update rules and finally publishes references. The variant says which step failed; the report
1674/// in [`FetchFinishError`] has the exact effects.
1675///
1676/// | Variant | Pack installed | `.git/shallow` | References |
1677/// | --- | --- | --- | --- |
1678/// | [`Installation`](Self::Installation) | no, but maybe an unindexed pack | unchanged | unchanged |
1679/// | [`Shallow`](Self::Shallow) | no if the lock was refused, yes if publishing failed | unchanged | unchanged |
1680/// | [`Safety`](Self::Safety) | no for a refused shallow change, which is checked first; otherwise yes | maybe changed | unchanged |
1681/// | [`BeforePublication`](Self::BeforePublication) | yes | maybe changed | unchanged |
1682/// | [`Reopen`](Self::Reopen) | yes | maybe changed | unchanged |
1683/// | [`Update`](Self::Update) | yes | maybe changed | unchanged |
1684/// | [`Publication`](Self::Publication) | yes | maybe changed | possibly some; see the transaction error |
1685///
1686/// Fetching again with a new [`FetchRequest`] is safe after any of them. Before retrying,
1687/// address the cause: [`Safety`](Self::Safety) means a worktree's `HEAD` or registration changed
1688/// during the fetch, or the response would change shallow boundaries the request did not ask to
1689/// change, [`Update`](Self::Update) is usually a refused non-fast-forward, and
1690/// [`Publication`](Self::Publication) usually means another writer moved a destination.
1691#[derive(Debug, thiserror::Error)]
1692#[non_exhaustive]
1693pub enum FetchFinishFailure {
1694    /// Installation failed; refs unchanged, possibly leaving an unindexed pack.
1695    #[error("fetch installation")]
1696    Installation(#[source] FetchError),
1697    /// Shallow lock, snapshot comparison, or metadata publication failed.
1698    #[error("fetch shallow publication")]
1699    Shallow(#[from] super::FetchShallowError),
1700    /// Repository could not be reopened with freshly published shallow boundaries.
1701    #[error("fetch repository reopen")]
1702    Reopen(#[from] crate::OpenError),
1703    /// Cancellation or installed-object verification failed before the reference transaction.
1704    #[error("before fetch publication")]
1705    BeforePublication(#[source] FetchError),
1706    /// Worktree/HEAD safety recheck failed after installation; refs unchanged.
1707    #[error("fetch publication safety")]
1708    Safety(#[from] FetchPlanError),
1709    /// Object-kind/ancestry validation or update authorization failed; installed objects remain,
1710    /// but no refs have changed.
1711    #[error("fetch update validation")]
1712    Update(#[from] super::FetchUpdateError),
1713    /// Exact transaction preparation or partial publication failure; objects remain installed.
1714    #[error("fetch reference publication")]
1715    Publication(#[from] crate::refs::TransactionError),
1716}
1717
1718#[cfg(test)]
1719#[path = "workflow_tests.rs"]
1720mod tests;
1721
1722#[cfg(test)]
1723#[path = "single_pass_tests.rs"]
1724mod single_pass_tests;