gitoxide_core/repository/
clone.rs

1use crate::OutputFormat;
2
3pub struct Options {
4    pub format: OutputFormat,
5    pub bare: bool,
6    pub handshake_info: bool,
7    pub no_tags: bool,
8    pub shallow: gix::remote::fetch::Shallow,
9    pub ref_name: Option<gix::refs::PartialName>,
10}
11
12pub const PROGRESS_RANGE: std::ops::RangeInclusive<u8> = 1..=3;
13
14pub(crate) mod function {
15    use std::{borrow::Cow, ffi::OsStr};
16
17    use anyhow::{bail, Context};
18    use gix::{bstr::BString, remote::fetch::Status, NestedProgress};
19
20    use super::Options;
21    use crate::{repository::fetch::function::print_updates, OutputFormat};
22
23    pub fn clone<P>(
24        url: impl AsRef<OsStr>,
25        directory: Option<impl Into<std::path::PathBuf>>,
26        overrides: Vec<BString>,
27        mut progress: P,
28        mut out: impl std::io::Write,
29        mut err: impl std::io::Write,
30        Options {
31            format,
32            handshake_info,
33            bare,
34            no_tags,
35            ref_name,
36            shallow,
37        }: Options,
38    ) -> anyhow::Result<()>
39    where
40        P: NestedProgress,
41        P::SubProgress: 'static,
42    {
43        if format != OutputFormat::Human {
44            bail!("JSON output isn't yet supported for fetching.");
45        }
46
47        let url: gix::Url = url.as_ref().try_into()?;
48        let directory = directory.map_or_else(
49            || {
50                let path = gix::path::from_bstr(Cow::Borrowed(url.path.as_ref()));
51                if !bare && path.extension() == Some(OsStr::new("git")) {
52                    path.file_stem().map(Into::into)
53                } else {
54                    path.file_name().map(Into::into)
55                }
56                .context("Filename extraction failed - path too short")
57            },
58            |dir| Ok(dir.into()),
59        )?;
60        let mut prepare = gix::clone::PrepareFetch::new(
61            url,
62            directory,
63            if bare {
64                gix::create::Kind::Bare
65            } else {
66                gix::create::Kind::WithWorktree
67            },
68            gix::create::Options::default(),
69            {
70                let mut opts = gix::open::Options::default().config_overrides(overrides);
71                opts.permissions.config.git_binary = true;
72                opts
73            },
74        )?;
75        if no_tags {
76            prepare = prepare.configure_remote(|r| Ok(r.with_fetch_tags(gix::remote::fetch::Tags::None)));
77        }
78        let (mut checkout, fetch_outcome) = prepare
79            .with_shallow(shallow)
80            .with_ref_name(ref_name.as_ref())?
81            .fetch_then_checkout(&mut progress, &gix::interrupt::IS_INTERRUPTED)?;
82
83        let (repo, outcome) = if bare {
84            (checkout.persist(), None)
85        } else {
86            let (repo, outcome) = checkout.main_worktree(progress, &gix::interrupt::IS_INTERRUPTED)?;
87            (repo, Some(outcome))
88        };
89
90        if handshake_info {
91            writeln!(out, "Handshake Information")?;
92            writeln!(out, "\t{:?}", fetch_outcome.handshake)?;
93        }
94
95        match fetch_outcome.status {
96            Status::NoPackReceived { dry_run, .. } => {
97                assert!(!dry_run, "dry-run unsupported");
98                writeln!(err, "The cloned repository appears to be empty")?;
99            }
100            Status::Change {
101                update_refs, negotiate, ..
102            } => {
103                let remote = repo
104                    .find_default_remote(gix::remote::Direction::Fetch)
105                    .expect("one origin remote")?;
106                let ref_specs = remote.refspecs(gix::remote::Direction::Fetch);
107                print_updates(
108                    &repo,
109                    &negotiate,
110                    update_refs,
111                    ref_specs,
112                    fetch_outcome.ref_map,
113                    &mut out,
114                    &mut err,
115                )?;
116            }
117        }
118
119        if let Some(gix::worktree::state::checkout::Outcome { collisions, errors, .. }) = outcome {
120            if !(collisions.is_empty() && errors.is_empty()) {
121                let mut messages = Vec::new();
122                if !errors.is_empty() {
123                    messages.push(format!("kept going through {} errors(s)", errors.len()));
124                    for record in errors {
125                        writeln!(err, "{}: {}", record.path, record.error).ok();
126                    }
127                }
128                if !collisions.is_empty() {
129                    messages.push(format!("encountered {} collision(s)", collisions.len()));
130                    for col in collisions {
131                        writeln!(err, "{}: collision ({:?})", col.path, col.error_kind).ok();
132                    }
133                }
134                bail!(
135                    "One or more errors occurred - checkout is incomplete: {}",
136                    messages.join(", ")
137                );
138            }
139        }
140        Ok(())
141    }
142}