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;