gitoxide_core/repository/
clone.rs1use 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}