radicle_cli/commands/
checkout.rs

1#![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                    // Ignored for now.
53                }
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 remote tracking branches for project delegates.
125    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
139/// Setup a remote and tracking branch for each given remote.
140pub 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
155/// Setup a remote and tracking branch for the given remote.
156pub 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}