radicle_cli/commands/
remote.rs

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