radicle_cli/commands/
remote.rs

1//! Remote Command implementation
2
3pub mod add;
4pub mod list;
5pub mod rm;
6
7use std::ffi::OsString;
8
9use anyhow::anyhow;
10
11use radicle::git::RefString;
12use radicle::prelude::NodeId;
13use radicle::storage::ReadStorage;
14
15use crate::terminal as term;
16use crate::terminal::args;
17use crate::terminal::{Args, Context, Help};
18
19pub const HELP: Help = Help {
20    name: "remote",
21    description: "Manage a repository's remotes",
22    version: env!("RADICLE_VERSION"),
23    usage: r#"
24Usage
25
26    rad remote [<option>...]
27    rad remote list [--tracked | --untracked | --all] [<option>...]
28    rad remote add (<did> | <nid>) [--name <string>] [<option>...]
29    rad remote rm <name> [<option>...]
30
31List options
32
33    --tracked     Show all remotes that are listed in the working copy
34    --untracked   Show all remotes that are listed in the Radicle storage
35    --all         Show all remotes in both the Radicle storage and the working copy
36
37Add options
38
39    --name        Override the name of the remote that by default is set to the node alias
40    --[no-]fetch  Fetch the remote from local storage (default: fetch)
41    --[no-]sync   Sync the remote refs from the network (default: sync)
42
43Options
44
45    --help        Print help
46"#,
47};
48
49#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
50pub enum OperationName {
51    Add,
52    Rm,
53    #[default]
54    List,
55}
56
57#[derive(Debug)]
58pub enum Operation {
59    Add {
60        id: NodeId,
61        name: Option<RefString>,
62        fetch: bool,
63        sync: bool,
64    },
65    Rm {
66        name: RefString,
67    },
68    List {
69        option: ListOption,
70    },
71}
72
73#[derive(Debug, Default)]
74pub enum ListOption {
75    All,
76    #[default]
77    Tracked,
78    Untracked,
79}
80
81#[derive(Debug)]
82pub struct Options {
83    pub op: Operation,
84}
85
86impl Args for Options {
87    fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
88        use lexopt::prelude::*;
89
90        let mut parser = lexopt::Parser::from_args(args);
91        let mut op: Option<OperationName> = None;
92        let mut id: Option<NodeId> = None;
93        let mut name: Option<RefString> = None;
94        let mut list_op: ListOption = ListOption::default();
95        let mut fetch = true;
96        let mut sync = true;
97
98        while let Some(arg) = parser.next()? {
99            match arg {
100                Long("help") | Short('h') => {
101                    return Err(args::Error::Help.into());
102                }
103                Long("name") | Short('n') => {
104                    let value = parser.value()?;
105                    let value = args::refstring("name", value)?;
106
107                    name = Some(value);
108                }
109                Value(val) if op.is_none() => match val.to_string_lossy().as_ref() {
110                    "a" | "add" => op = Some(OperationName::Add),
111                    "l" | "list" => op = Some(OperationName::List),
112                    "r" | "rm" => op = Some(OperationName::Rm),
113                    unknown => anyhow::bail!("unknown operation '{}'", unknown),
114                },
115
116                // List options
117                Long("all") if op.unwrap_or_default() == OperationName::List => {
118                    list_op = ListOption::All;
119                }
120                Long("tracked") if op.unwrap_or_default() == OperationName::List => {
121                    list_op = ListOption::Tracked;
122                }
123                Long("untracked") if op.unwrap_or_default() == OperationName::List => {
124                    list_op = ListOption::Untracked;
125                }
126
127                // Add options
128                Long("sync") if op == Some(OperationName::Add) => {
129                    sync = true;
130                }
131                Long("no-sync") if op == Some(OperationName::Add) => {
132                    sync = false;
133                }
134                Long("fetch") if op == Some(OperationName::Add) => {
135                    fetch = true;
136                }
137                Long("no-fetch") if op == Some(OperationName::Add) => {
138                    fetch = false;
139                }
140                Value(val) if op == Some(OperationName::Add) && id.is_none() => {
141                    let nid = args::pubkey(&val)?;
142                    id = Some(nid);
143                }
144
145                // Remove options
146                Value(val) if op == Some(OperationName::Rm) && name.is_none() => {
147                    let val = args::string(&val);
148                    let val = RefString::try_from(val)
149                        .map_err(|e| anyhow!("invalid remote name specified: {e}"))?;
150
151                    name = Some(val);
152                }
153                _ => anyhow::bail!(arg.unexpected()),
154            }
155        }
156
157        let op = match op.unwrap_or_default() {
158            OperationName::Add => Operation::Add {
159                id: id.ok_or(anyhow!(
160                    "`DID` required, try running `rad remote add <did>`"
161                ))?,
162                name,
163                fetch,
164                sync,
165            },
166            OperationName::List => Operation::List { option: list_op },
167            OperationName::Rm => Operation::Rm {
168                name: name.ok_or(anyhow!("name required, see `rad remote`"))?,
169            },
170        };
171
172        Ok((Options { op }, vec![]))
173    }
174}
175
176pub fn run(options: Options, ctx: impl Context) -> anyhow::Result<()> {
177    let (working, rid) = radicle::rad::cwd()
178        .map_err(|_| anyhow!("this command must be run in the context of a repository"))?;
179    let profile = ctx.profile()?;
180
181    match options.op {
182        Operation::Add {
183            ref id,
184            name,
185            fetch,
186            sync,
187        } => {
188            let proj = profile.storage.repository(rid)?.project()?;
189            let branch = proj.default_branch();
190
191            self::add::run(
192                rid,
193                id,
194                name,
195                Some(branch.clone()),
196                &profile,
197                &working,
198                fetch,
199                sync,
200            )?
201        }
202        Operation::Rm { ref name } => self::rm::run(name, &working)?,
203        Operation::List { option } => match option {
204            ListOption::All => {
205                let tracked = list::tracked(&working)?;
206                let untracked = list::untracked(rid, &profile, tracked.iter())?;
207                // Only include a blank line if we're printing both tracked and untracked
208                let include_blank_line = !tracked.is_empty() && !untracked.is_empty();
209
210                list::print_tracked(tracked.iter());
211                if include_blank_line {
212                    term::blank();
213                }
214                list::print_untracked(untracked.iter());
215            }
216            ListOption::Tracked => {
217                let tracked = list::tracked(&working)?;
218                list::print_tracked(tracked.iter());
219            }
220            ListOption::Untracked => {
221                let tracked = list::tracked(&working)?;
222                let untracked = list::untracked(rid, &profile, tracked.iter())?;
223                list::print_untracked(untracked.iter());
224            }
225        },
226    };
227    Ok(())
228}