radicle_cli/commands/
remote.rs1pub 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 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 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 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 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}