1use anyhow::anyhow;
5use gix::{
6 ObjectId,
7 interrupt::IS_INTERRUPTED,
8 progress::Discard,
9 remote::{self, Direction, fetch, fetch::refmap},
10 worktree::state::checkout,
11};
12use std::{num::NonZero, path::Path};
13
14#[derive(thiserror::Error, Debug)]
16pub enum Error {
17 #[error("io error {error} at {path}")]
18 Io {
19 error: std::io::Error,
20 path: std::path::PathBuf,
21 },
22
23 #[error("target `{target}` not found in `{url}`")]
24 TargetNotFound { url: Box<gix::Url>, target: Target },
25
26 #[error("{0:?}")]
27 Internal(#[from] anyhow::Error),
28}
29
30pub type Result<T, E = Error> = std::result::Result<T, E>;
31
32trait IoResultExt<T> {
33 fn with_path<P: AsRef<Path>>(self, path: P) -> Result<T>;
34}
35
36impl<T> IoResultExt<T> for Result<T, std::io::Error> {
37 fn with_path<P: AsRef<Path>>(self, path: P) -> Result<T> {
38 self.map_err(|error| Error::Io {
39 error,
40 path: path.as_ref().to_owned(),
41 })
42 }
43}
44
45trait InternalResultExt<T> {
46 fn wrap_err(self) -> Result<T>;
47}
48
49impl<T, E: Into<anyhow::Error>> InternalResultExt<T> for Result<T, E> {
50 fn wrap_err(self) -> Result<T> {
51 self.map_err(|e| Error::Internal(e.into()))
52 }
53}
54
55#[derive(Clone, Debug, PartialEq, Eq, Hash)]
57pub struct Spec {
58 pub url: gix::Url,
60 pub target: Target,
62}
63
64impl Spec {
65 pub fn head(url: gix::Url) -> Self {
67 Self {
68 url,
69 target: Target::Head,
70 }
71 }
72
73 pub fn commit(url: gix::Url, commit: ObjectId) -> Self {
74 Self {
75 url,
76 target: Target::Commit(commit),
77 }
78 }
79}
80
81#[serde_with::serde_as]
83#[derive(
84 Clone,
85 Debug,
86 PartialEq,
87 Eq,
88 Hash,
89 Default,
90 serde::Serialize,
91 serde::Deserialize,
92 PartialOrd,
93 Ord,
94)]
95pub enum Target {
96 #[default]
98 Head,
99 Branch(String),
101 Tag(String),
103 Commit(#[serde_as(as = "serde_with::DisplayFromStr")] ObjectId),
112}
113
114impl std::fmt::Display for Target {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 match self {
117 Target::Head => write!(f, "HEAD"),
118 Target::Branch(branch) => write!(f, "refs/heads/{branch}"),
119 Target::Tag(tag) => write!(f, "refs/tags/{tag}"),
120 Target::Commit(c) => write!(f, "{c}"),
121 }
122 }
123}
124
125fn source_object_id(source: &refmap::Source) -> Result<ObjectId> {
126 match source {
127 refmap::Source::ObjectId(id) => Ok(*id),
128 refmap::Source::Ref(r) => {
129 let (_name, id, peeled) = r.unpack();
130
131 Ok(peeled
132 .or(id)
133 .ok_or_else(|| anyhow!("unborn reference"))?
134 .to_owned())
135 }
136 }
137}
138
139pub fn fetch(spec: &Spec, dir: impl AsRef<Path>) -> Result<ObjectId> {
149 let dir = dir.as_ref();
150 std::fs::create_dir_all(dir).with_path(dir)?;
151
152 let git_tempdir = tempfile::tempdir().wrap_err()?;
154 let repo = gix::init(git_tempdir.path()).wrap_err()?;
155 let refspec = spec.target.to_string();
156
157 let remote = repo
158 .remote_at(spec.url.clone())
159 .wrap_err()?
160 .with_fetch_tags(fetch::Tags::None)
161 .with_refspecs(Some(refspec.as_str()), Direction::Fetch)
162 .wrap_err()?;
163
164 let connection = remote.connect(Direction::Fetch).wrap_err()?;
169 let outcome = connection
170 .prepare_fetch(&mut Discard, remote::ref_map::Options::default())
171 .wrap_err()?
172 .with_shallow(fetch::Shallow::DepthAtRemote(NonZero::new(1).unwrap()))
175 .receive(&mut Discard, &IS_INTERRUPTED)
176 .map_err(|e| match e {
177 fetch::Error::NoMapping { .. } => Error::TargetNotFound {
178 url: Box::new(spec.url.clone()),
179 target: spec.target.clone(),
180 },
181 fetch::Error::Fetch(gix::protocol::fetch::Error::FetchResponse(
183 gix::protocol::fetch::response::Error::UnknownSectionHeader { .. },
184 )) => Error::TargetNotFound {
185 url: Box::new(spec.url.clone()),
186 target: spec.target.clone(),
187 },
188 _ => Error::Internal(e.into()),
189 })?;
190
191 if outcome.ref_map.mappings.len() > 1 {
192 return Err(anyhow!("we only asked for 1 ref; why did we get more?")).wrap_err();
193 }
194 if outcome.ref_map.mappings.is_empty() {
195 return Err(Error::TargetNotFound {
196 url: Box::new(spec.url.clone()),
197 target: spec.target.clone(),
198 });
199 }
200 let object_id = source_object_id(&outcome.ref_map.mappings[0].remote)?;
201
202 let object = repo.find_object(object_id).wrap_err()?;
203 let tree_id = object.peel_to_tree().wrap_err()?.id();
204 let mut index = repo.index_from_tree(&tree_id).wrap_err()?;
205
206 checkout(
207 &mut index,
208 dir,
209 repo.objects.clone(),
210 &Discard,
211 &Discard,
212 &IS_INTERRUPTED,
213 checkout::Options {
214 overwrite_existing: true,
215 ..Default::default()
216 },
217 )
218 .wrap_err()?;
219 index.write(Default::default()).wrap_err()?;
220
221 Ok(tree_id.detach())
222}