1pub mod args;
2
3use std::path::{Path, PathBuf};
4
5use radicle::issue::cache::Issues as _;
6use radicle::patch::cache::Patches as _;
7use thiserror::Error;
8
9use radicle::git::raw;
10use radicle::identity::doc;
11use radicle::identity::doc::RepoId;
12use radicle::node;
13use radicle::node::policy::Scope;
14use radicle::node::{Handle as _, Node};
15use radicle::prelude::*;
16use radicle::rad;
17use radicle::storage;
18use radicle::storage::RemoteId;
19use radicle::storage::{HasRepoId, RepositoryError};
20
21use crate::commands::checkout;
22use crate::commands::sync;
23use crate::node::SyncSettings;
24use crate::project;
25use crate::terminal as term;
26use crate::terminal::Element as _;
27
28pub use args::Args;
29
30pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
31 let profile = ctx.profile()?;
32 let mut node = radicle::Node::new(profile.socket());
33
34 if !node.is_running() {
35 anyhow::bail!(
36 "to clone a repository, your node must be running. To start it, run `rad node start`"
37 );
38 }
39
40 let Success {
41 working_copy: working,
42 repository: repo,
43 doc,
44 project: proj,
45 } = clone(
46 args.repo,
47 args.directory.clone(),
48 args.scope,
49 SyncSettings::from(args.sync).with_profile(&profile),
50 &mut node,
51 &profile,
52 args.bare,
53 )?
54 .print_or_success()
55 .ok_or_else(|| anyhow::anyhow!("failed to clone {}", args.repo))?;
56 let delegates = doc
57 .delegates()
58 .iter()
59 .map(|d| **d)
60 .filter(|id| id != profile.id())
61 .collect::<Vec<_>>();
62 let default_branch = proj.default_branch().clone();
63 let path = if !args.bare {
64 working.workdir().unwrap()
65 } else {
66 working.path()
67 };
68
69 radicle::git::configure_repository(&working)?;
71 checkout::setup_remotes(
72 project::SetupRemote {
73 rid: args.repo,
74 tracking: Some(default_branch),
75 repo: &working,
76 fetch: true,
77 },
78 &delegates,
79 &profile,
80 )?;
81
82 term::success!(
83 "Repository successfully cloned under {}",
84 term::format::dim(Path::new(".").join(path).display())
85 );
86
87 let mut info: term::Table<1, term::Line> = term::Table::new(term::TableOptions::bordered());
88 info.push([term::format::bold(proj.name()).into()]);
89 info.push([term::format::italic(proj.description()).into()]);
90
91 let issues = term::cob::issues(&profile, &repo)?.counts()?;
92 let patches = term::cob::patches(&profile, &repo)?.counts()?;
93
94 info.push([term::Line::spaced([
95 term::format::tertiary(issues.open).into(),
96 term::format::default("issues").into(),
97 term::format::dim("ยท").into(),
98 term::format::tertiary(patches.open).into(),
99 term::format::default("patches").into(),
100 ])]);
101 info.print();
102
103 let location = args
104 .directory
105 .map_or(proj.name().to_string(), |loc| loc.display().to_string());
106 term::info!(
107 "Run {} to go to the repository directory.",
108 term::format::command(format!("cd ./{location}")),
109 );
110
111 Ok(())
112}
113
114#[derive(Error, Debug)]
115enum CloneError {
116 #[error("node: {0}")]
117 Node(#[from] node::Error),
118 #[error("checkout: {0}")]
119 Checkout(#[from] rad::CheckoutError),
120 #[error("no seeds found for {0}")]
121 NoSeeds(RepoId),
122 #[error("fetch: {0}")]
123 Fetch(#[from] sync::FetchError),
124}
125
126struct Checkout {
127 id: RepoId,
128 remote: RemoteId,
129 path: PathBuf,
130 repository: storage::git::Repository,
131 doc: Doc,
132 project: Project,
133 bare: bool,
134}
135
136impl Checkout {
137 fn new(
138 repository: storage::git::Repository,
139 profile: &Profile,
140 directory: Option<PathBuf>,
141 bare: bool,
142 ) -> Result<Self, CheckoutFailure> {
143 let rid = repository.rid();
144 let doc = repository
145 .identity_doc()
146 .map_err(|err| CheckoutFailure::Identity { rid, err })?;
147 let proj = doc
148 .project()
149 .map_err(|err| CheckoutFailure::Payload { rid, err })?;
150 let path = directory.unwrap_or_else(|| PathBuf::from(proj.name()));
151 if path.exists() && path.read_dir().map_or(true, |mut dir| dir.next().is_some()) {
153 return Err(CheckoutFailure::Exists { rid, path });
154 }
155
156 Ok(Self {
157 id: rid,
158 remote: *profile.id(),
159 path,
160 repository,
161 doc: doc.doc,
162 project: proj,
163 bare,
164 })
165 }
166
167 fn destination(&self) -> &PathBuf {
168 &self.path
169 }
170
171 fn run<S>(self, storage: &S) -> Result<CloneResult, rad::CheckoutError>
172 where
173 S: storage::ReadStorage,
174 {
175 let destination = self.destination().to_path_buf();
176 let mut spinner = term::spinner(format!(
178 "Creating checkout in ./{}..",
179 term::format::tertiary(destination.display())
180 ));
181 match rad::checkout(self.id, &self.remote, self.path, storage, self.bare) {
182 Err(err) => {
183 spinner.message(format!(
184 "Failed to checkout in ./{}",
185 term::format::tertiary(destination.display())
186 ));
187 spinner.failed();
188 Err(err)
189 }
190 Ok(working_copy) => {
191 spinner.finish();
192 Ok(CloneResult::Success(Success {
193 working_copy,
194 repository: self.repository,
195 doc: self.doc,
196 project: self.project,
197 }))
198 }
199 }
200 }
201}
202
203fn clone(
204 id: RepoId,
205 directory: Option<PathBuf>,
206 scope: Scope,
207 settings: SyncSettings,
208 node: &mut Node,
209 profile: &Profile,
210 bare: bool,
211) -> Result<CloneResult, CloneError> {
212 if node.seed(id, scope)? {
214 term::success!(
215 "Seeding policy updated for {} with scope '{scope}'",
216 term::format::tertiary(id)
217 );
218 }
219
220 match profile.storage.repository(id) {
221 Err(_) => {
222 let settings = settings.replicas(node::sync::ReplicationFactor::must_reach(1));
225 let result = sync::fetch(id, settings, node, profile)?;
226 match &result {
227 node::sync::FetcherResult::TargetReached(_) => {
228 profile.storage.repository(id).map_or_else(
229 |err| Ok(CloneResult::RepositoryMissing { rid: id, err }),
230 |repository| Ok(perform_checkout(repository, profile, directory, bare)?),
231 )
232 }
233 node::sync::FetcherResult::TargetError(failure) => {
234 Err(handle_fetch_error(id, failure))
235 }
236 }
237 }
238 Ok(repository) => Ok(perform_checkout(repository, profile, directory, bare)?),
239 }
240}
241
242fn perform_checkout(
243 repository: storage::git::Repository,
244 profile: &Profile,
245 directory: Option<PathBuf>,
246 bare: bool,
247) -> Result<CloneResult, rad::CheckoutError> {
248 Checkout::new(repository, profile, directory, bare).map_or_else(
249 |failure| Ok(CloneResult::Failure(failure)),
250 |checkout| checkout.run(&profile.storage),
251 )
252}
253
254fn handle_fetch_error(id: RepoId, failure: &node::sync::fetch::TargetMissed) -> CloneError {
255 term::warning(format!(
256 "Failed to fetch from {} seed(s).",
257 failure.progress().failed()
258 ));
259 for (node, reason) in failure.fetch_results().failed() {
260 term::warning(format!(
261 "{}: {}",
262 term::format::node_id_human(node),
263 term::format::yellow(reason),
264 ))
265 }
266 CloneError::NoSeeds(id)
267}
268
269enum CloneResult {
270 Success(Success),
271 RepositoryMissing { rid: RepoId, err: RepositoryError },
272 Failure(CheckoutFailure),
273}
274
275struct Success {
276 working_copy: raw::Repository,
277 repository: storage::git::Repository,
278 doc: Doc,
279 project: Project,
280}
281
282impl CloneResult {
283 fn print_or_success(self) -> Option<Success> {
284 match self {
285 CloneResult::Success(success) => Some(success),
286 CloneResult::RepositoryMissing { rid, err } => {
287 term::error(format!(
288 "failed to find repository in storage after fetching: {err}"
289 ));
290 term::hint(format!(
291 "try `rad inspect {rid}` to see if the repository exists"
292 ));
293 None
294 }
295 CloneResult::Failure(failure) => {
296 failure.print();
297 None
298 }
299 }
300 }
301}
302
303#[derive(Debug)]
304pub enum CheckoutFailure {
305 Identity { rid: RepoId, err: RepositoryError },
306 Payload { rid: RepoId, err: doc::PayloadError },
307 Exists { rid: RepoId, path: PathBuf },
308}
309
310impl CheckoutFailure {
311 fn print(&self) {
312 match self {
313 CheckoutFailure::Identity { rid, err } => {
314 term::error(format!(
315 "failed to get the identity document of {rid} after fetching: {err}"
316 ));
317 term::hint(format!(
318 "try `rad inspect {rid} --identity`, if this works then try `rad checkout {rid}`"
319 ));
320 }
321 CheckoutFailure::Payload { rid, err } => {
322 term::error(format!(
323 "failed to get the project payload of {rid} after fetching: {err}"
324 ));
325 term::hint(format!(
326 "try `rad inspect {rid} --payload`, if this works then try `rad checkout {rid}`"
327 ));
328 }
329 CheckoutFailure::Exists { rid, path } => {
330 term::error(format!(
331 "refusing to checkout repository to {}, since it already exists",
332 path.display()
333 ));
334 term::hint(format!("try `rad checkout {rid}` in a new directory"))
335 }
336 }
337 }
338}