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(
77        request.format,
78        &references,
79        &refspecs,
80        request.options.auto_follow_tags,
81    )?;
82    if request.options.fetch_all_tags {
83        mark_tag_refspec_updates_not_for_merge(&mut fetched);
84        order_bundle_fetch_all_tags_updates(&mut fetched);
85    }
86    let store = FileRefStore::new(request.git_dir, request.format);
87    if !request.options.fetch_all_tags {
88        retain_missing_auto_follow_tags(&store, &mut fetched)?;
89    }
90    if request.options.dry_run {
91        return Ok(());
92    }
93    if request.options.write_fetch_head {
94        write_fetch_head(
95            request.git_dir,
96            request.bundle_path,
97            &fetched,
98            request.options.append,
99        )?;
100    }
101    let updates = fetched
102        .iter()
103        .filter_map(|fetched| {
104            fetched.dst.as_ref().map(|dst| BundleRefUpdate {
105                name: dst.clone(),
106                oid: fetched.oid,
107            })
108        })
109        .collect::<Vec<_>>();
110    store.apply_bundle_ref_updates(&updates, None)?;
111    Ok(())
112}
113
114fn bundle_default_fetch_reference(references: &[BundleReference]) -> Result<&BundleReference> {
115    references
116        .iter()
117        .find(|reference| reference.name == "HEAD")
118        .ok_or_else(|| GitError::reference_not_found("remote ref HEAD"))
119}
120
121fn write_bundle_default_fetch_head(
122    git_dir: &Path,
123    bundle_path: &str,
124    reference: &BundleReference,
125    append: bool,
126) -> Result<()> {
127    let records = [FetchHeadRecord {
128        oid: reference.oid,
129        not_for_merge: false,
130        description: bundle_path.to_string(),
131    }];
132    write_fetch_head_records(git_dir, &records, append)?;
133    Ok(())
134}
135
136fn bundle_fetch_refs(
137    format: ObjectFormat,
138    references: &[BundleReference],
139    refspecs: &[String],
140    auto_follow_tags: bool,
141) -> Result<Vec<FetchRefUpdate>> {
142    let refs = references
143        .iter()
144        .map(|reference| RefAdvertisement {
145            oid: reference.oid,
146            name: reference.name.clone(),
147            capabilities: Vec::new(),
148        })
149        .collect::<Vec<_>>();
150    let refspecs = refspecs
151        .iter()
152        .map(|refspec| parse_refspec(refspec))
153        .collect::<Result<Vec<_>>>()?;
154    plan_fetch_ref_updates(format, &refs, &refspecs, auto_follow_tags)
155}