Skip to main content

sley_remote/
bundle.rs

1//! Callable fetch orchestration for `.bundle` files.
2//!
3//! [`fetch_bundle`] installs objects from a parsed [`Bundle`] and applies the
4//! requested refspec map, mirroring `git fetch` against a bundle path. Everything
5//! is taken as explicit parameters — `git_dir`, the [`ObjectFormat`], the bundle
6//! path (for `FETCH_HEAD` descriptions), the parsed bundle, refspecs, and
7//! [`FetchOptions`] — so it never reads process-global state, parses arguments, or
8//! prints.
9
10use std::path::Path;
11
12use sley_core::{GitError, ObjectFormat, Result};
13use sley_formats::{Bundle, BundleReference};
14use sley_odb::{FileObjectDatabase, install_bundle_pack, verify_bundle_prerequisites};
15use sley_protocol::{
16    FetchHeadRecord, FetchRefUpdate, RefAdvertisement, parse_refspec, plan_fetch_ref_updates,
17};
18use sley_refs::{BundleRefUpdate, FileRefStore};
19
20use crate::fetch::{
21    FetchOptions, fetch_refspecs_for_source, mark_tag_refspec_updates_not_for_merge,
22    order_bundle_fetch_all_tags_updates, retain_missing_auto_follow_tags, write_fetch_head,
23    write_fetch_head_records,
24};
25
26/// Fully resolved inputs for a [`fetch_bundle`] run.
27pub struct FetchBundleRequest<'a> {
28    /// Local repository `$GIT_DIR`.
29    pub git_dir: &'a Path,
30    /// Local repository object format.
31    pub format: ObjectFormat,
32    /// Bundle path or source string used for `FETCH_HEAD` descriptions.
33    pub bundle_path: &'a str,
34    /// Parsed bundle contents.
35    pub bundle: &'a Bundle,
36    /// Refspecs requested by the caller. Empty means fetch the bundle's default
37    /// `HEAD` only (no ref updates).
38    pub refspecs: &'a [String],
39    /// Fetch behavior flags.
40    pub options: &'a FetchOptions,
41}
42
43/// Fetch from a parsed `bundle` into the repository at `git_dir`.
44///
45/// Installs the bundle pack (unless `dry_run`), plans the ref-map for `refspecs`
46/// (empty means write `FETCH_HEAD` for the bundle's `HEAD` only), writes
47/// `FETCH_HEAD` when requested, and applies remote-tracking ref updates. Bundle
48/// fetches have no shallow support; callers should warn-and-ignore a `--depth`
49/// before calling.
50pub fn fetch_bundle(request: FetchBundleRequest<'_>) -> Result<()> {
51    let prerequisite_reader = FileObjectDatabase::from_git_dir(request.git_dir, request.format);
52    let references = if request.options.dry_run {
53        verify_bundle_prerequisites(request.bundle, &prerequisite_reader)?;
54        request.bundle.references.clone()
55    } else {
56        let database = FileObjectDatabase::from_git_dir(request.git_dir, request.format);
57        install_bundle_pack(request.bundle, &prerequisite_reader, &database)?.references
58    };
59    if request.refspecs.is_empty() {
60        if request.options.dry_run {
61            return Ok(());
62        }
63        if request.options.write_fetch_head {
64            let reference = bundle_default_fetch_reference(&references)?;
65            write_bundle_default_fetch_head(
66                request.git_dir,
67                request.bundle_path,
68                reference,
69                request.options.append,
70            )?;
71        }
72        return Ok(());
73    }
74    let refspecs =
75        fetch_refspecs_for_source(Vec::new(), request.refspecs, request.options.fetch_all_tags);
76    let mut fetched = bundle_fetch_refs(&references, &refspecs, request.options.auto_follow_tags)?;
77    if request.options.fetch_all_tags {
78        mark_tag_refspec_updates_not_for_merge(&mut fetched);
79        order_bundle_fetch_all_tags_updates(&mut fetched);
80    }
81    let store = FileRefStore::new(request.git_dir, request.format);
82    if !request.options.fetch_all_tags {
83        retain_missing_auto_follow_tags(&store, &mut fetched)?;
84    }
85    if request.options.dry_run {
86        return Ok(());
87    }
88    if request.options.write_fetch_head {
89        write_fetch_head(
90            request.git_dir,
91            request.bundle_path,
92            &fetched,
93            request.options.append,
94        )?;
95    }
96    let updates = fetched
97        .iter()
98        .filter_map(|fetched| {
99            fetched.dst.as_ref().map(|dst| BundleRefUpdate {
100                name: dst.clone(),
101                oid: fetched.oid,
102            })
103        })
104        .collect::<Vec<_>>();
105    store.apply_bundle_ref_updates(&updates, None)?;
106    Ok(())
107}
108
109fn bundle_default_fetch_reference(references: &[BundleReference]) -> Result<&BundleReference> {
110    references
111        .iter()
112        .find(|reference| reference.name == "HEAD")
113        .ok_or_else(|| GitError::reference_not_found("remote ref HEAD"))
114}
115
116fn write_bundle_default_fetch_head(
117    git_dir: &Path,
118    bundle_path: &str,
119    reference: &BundleReference,
120    append: bool,
121) -> Result<()> {
122    let records = [FetchHeadRecord {
123        oid: reference.oid,
124        not_for_merge: false,
125        description: bundle_path.to_string(),
126    }];
127    write_fetch_head_records(git_dir, &records, append)?;
128    Ok(())
129}
130
131fn bundle_fetch_refs(
132    references: &[BundleReference],
133    refspecs: &[String],
134    auto_follow_tags: bool,
135) -> Result<Vec<FetchRefUpdate>> {
136    let refs = references
137        .iter()
138        .map(|reference| RefAdvertisement {
139            oid: reference.oid,
140            name: reference.name.clone(),
141            capabilities: Vec::new(),
142        })
143        .collect::<Vec<_>>();
144    let refspecs = refspecs
145        .iter()
146        .map(|refspec| parse_refspec(refspec))
147        .collect::<Result<Vec<_>>>()?;
148    plan_fetch_ref_updates(&refs, &refspecs, auto_follow_tags)
149}