radicle_cli/commands/
remote.rs1#[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 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 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 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 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}