radicle_cli/commands/
checkout.rs1#![allow(clippy::box_default)]
2use std::ffi::OsString;
3use std::path::PathBuf;
4
5use anyhow::anyhow;
6use anyhow::Context as _;
7
8use radicle::git;
9use radicle::node::AliasStore;
10use radicle::prelude::*;
11use radicle::storage::git::transport;
12
13use crate::project;
14use crate::terminal as term;
15use crate::terminal::args::{Args, Error, Help};
16
17pub const HELP: Help = Help {
18 name: "checkout",
19 description: "Checkout a repository into the local directory",
20 version: env!("RADICLE_VERSION"),
21 usage: r#"
22Usage
23
24 rad checkout <rid> [--remote <did>] [<option>...]
25
26 Creates a working copy from a repository in local storage.
27
28Options
29
30 --remote <did> Remote peer to checkout
31 --no-confirm Don't ask for confirmation during checkout
32 --help Print help
33"#,
34};
35
36pub struct Options {
37 pub id: RepoId,
38 pub remote: Option<Did>,
39}
40
41impl Args for Options {
42 fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
43 use lexopt::prelude::*;
44
45 let mut parser = lexopt::Parser::from_args(args);
46 let mut id = None;
47 let mut remote = None;
48
49 while let Some(arg) = parser.next()? {
50 match arg {
51 Long("no-confirm") => {
52 }
54 Long("help") | Short('h') => return Err(Error::Help.into()),
55 Long("remote") => {
56 let val = parser.value().unwrap();
57 remote = Some(term::args::did(&val)?);
58 }
59 Value(val) if id.is_none() => {
60 id = Some(term::args::rid(&val)?);
61 }
62 _ => anyhow::bail!(arg.unexpected()),
63 }
64 }
65
66 Ok((
67 Options {
68 id: id.ok_or_else(|| anyhow!("a repository to checkout must be provided"))?,
69 remote,
70 },
71 vec![],
72 ))
73 }
74}
75
76pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
77 let profile = ctx.profile()?;
78 execute(options, &profile)?;
79
80 Ok(())
81}
82
83fn execute(options: Options, profile: &Profile) -> anyhow::Result<PathBuf> {
84 let id = options.id;
85 let storage = &profile.storage;
86 let remote = options.remote.unwrap_or(profile.did());
87 let doc = storage
88 .repository(id)?
89 .identity_doc()
90 .context("repository could not be found in local storage")?;
91 let payload = doc.project()?;
92 let path = PathBuf::from(payload.name());
93
94 transport::local::register(storage.clone());
95
96 if path.exists() {
97 anyhow::bail!("the local path {:?} already exists", path.as_path());
98 }
99
100 let mut spinner = term::spinner("Performing checkout...");
101 let repo = match radicle::rad::checkout(options.id, &remote, path.clone(), &storage, false) {
102 Ok(repo) => repo,
103 Err(err) => {
104 spinner.failed();
105 term::blank();
106
107 return Err(err.into());
108 }
109 };
110 spinner.message(format!(
111 "Repository checkout successful under ./{}",
112 term::format::highlight(path.file_name().unwrap_or_default().to_string_lossy())
113 ));
114 spinner.finish();
115
116 let remotes = doc
117 .delegates()
118 .clone()
119 .into_iter()
120 .map(|did| *did)
121 .filter(|id| id != profile.id())
122 .collect::<Vec<_>>();
123
124 setup_remotes(
126 project::SetupRemote {
127 rid: id,
128 tracking: Some(payload.default_branch().clone()),
129 repo: &repo,
130 fetch: true,
131 },
132 &remotes,
133 profile,
134 )?;
135
136 Ok(path)
137}
138
139pub fn setup_remotes(
141 setup: project::SetupRemote,
142 remotes: &[NodeId],
143 profile: &Profile,
144) -> anyhow::Result<()> {
145 let aliases = profile.aliases();
146
147 for remote_id in remotes {
148 if let Err(e) = setup_remote(&setup, remote_id, None, &aliases) {
149 term::warning(format!("Failed to setup remote for {remote_id}: {e}").as_str());
150 }
151 }
152 Ok(())
153}
154
155pub fn setup_remote(
157 setup: &project::SetupRemote,
158 remote_id: &NodeId,
159 remote_name: Option<git::RefString>,
160 aliases: &impl AliasStore,
161) -> anyhow::Result<git::RefString> {
162 let remote_name = if let Some(name) = remote_name {
163 name
164 } else {
165 let name = if let Some(alias) = aliases.alias(remote_id) {
166 format!("{alias}@{remote_id}")
167 } else {
168 remote_id.to_human()
169 };
170 git::RefString::try_from(name.as_str())
171 .map_err(|_| anyhow!("invalid remote name: '{name}'"))?
172 };
173 let (remote, branch) = setup.run(&remote_name, *remote_id)?;
174
175 term::success!("Remote {} added", term::format::tertiary(remote.name));
176
177 if let Some(branch) = branch {
178 term::success!(
179 "Remote-tracking branch {} created for {}",
180 term::format::tertiary(branch),
181 term::format::tertiary(term::format::node_id_human(remote_id))
182 );
183 }
184 Ok(remote_name)
185}