radicle_cli/commands/
inspect.rs1mod args;
2
3use std::collections::HashMap;
4use std::path::Path;
5use std::str::FromStr;
6
7use anyhow::Context as _;
8use chrono::prelude::*;
9
10use radicle::identity::RepoId;
11use radicle::identity::{DocAt, Identity};
12use radicle::node::AliasStore as _;
13use radicle::node::policy::SeedingPolicy;
14use radicle::storage::git::{Repository, Storage};
15use radicle::storage::refs::{FeatureLevel, RefsAt, SignedRefs};
16use radicle::storage::{ReadRepository, ReadStorage};
17
18use crate::terminal as term;
19use crate::terminal::Element;
20use crate::terminal::json;
21
22pub use args::Args;
23use args::Target;
24
25pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
26 let rid = match args.repo {
27 Some(rid) => {
28 if let Ok(val) = RepoId::from_str(&rid) {
29 val
30 } else {
31 radicle::rad::at(Path::new(&rid))
32 .map(|(_, id)| id)
33 .context("Supplied argument is not a valid path")?
34 }
35 }
36 None => radicle::rad::cwd()
37 .map(|(_, rid)| rid)
38 .context("Current directory is not a Radicle repository")?,
39 };
40
41 let target = args.target.into();
42
43 if matches!(target, Target::RepoId) {
44 term::info!("{}", term::format::highlight(rid.urn()));
45 return Ok(());
46 }
47
48 let profile = ctx.profile()?;
49 let storage = &profile.storage;
50
51 match target {
52 Target::Refs => {
53 let (repo, _) = repo(rid, storage)?;
54 refs(&repo)?;
55 }
56 Target::Payload => {
57 let (_, doc) = repo(rid, storage)?;
58 json::to_pretty(&doc.payload(), Path::new("radicle.json"))?.print();
59 }
60 Target::Identity => {
61 let (_, doc) = repo(rid, storage)?;
62 json::to_pretty(&*doc, Path::new("radicle.json"))?.print();
63 }
64 Target::Sigrefs => {
65 let (repo, _) = repo(rid, storage)?;
66 for remote in repo.remote_ids()? {
67 let remote = remote?;
68 let refs = RefsAt::new(&repo, remote)?;
69 let sigrefs = SignedRefs::load_at(refs.at, remote, &repo);
70
71 term::println(format_args!(
72 "{:<48} {} {}",
73 term::format::tertiary(remote.to_human()),
74 term::format::secondary(refs.at),
75 match sigrefs {
76 Ok(Some(refs)) => {
77 let mut level = refs.feature_level();
78
79 if &remote == profile.id()
85 && level == FeatureLevel::Parent
86 && refs.parent().is_none()
87 {
88 level = FeatureLevel::Root;
89 }
90
91 let s = level.to_string();
92 match level {
93 FeatureLevel::None => term::format::negative(s),
94 FeatureLevel::Root => term::format::yellow(s),
95 FeatureLevel::Parent => term::format::positive(s),
96 _ => term::format::faint(s),
97 }
98 }
99 Err(err) => {
100 term::format::negative(err.to_string())
101 }
102 Ok(None) => {
103 term::format::negative("missing".to_string())
104 }
105 }
106 ));
107 }
108 }
109 Target::Policy => {
110 let policies = profile.policies()?;
111 let seed = policies.seed_policy(&rid)?;
112 match seed.policy {
113 SeedingPolicy::Allow { scope } => {
114 term::println(format_args!(
115 "Repository {} is {} with scope {}",
116 term::format::tertiary(&rid),
117 term::format::positive("being seeded"),
118 term::format::dim(format!("`{scope}`"))
119 ));
120 }
121 SeedingPolicy::Block => {
122 term::println(format_args!(
123 "Repository {} is {}",
124 term::format::tertiary(&rid),
125 term::format::negative("not being seeded"),
126 ));
127 }
128 }
129 }
130 Target::Delegates => {
131 let (_, doc) = repo(rid, storage)?;
132 let aliases = profile.aliases();
133 for did in doc.delegates().iter() {
134 if let Some(alias) = aliases.alias(did) {
135 term::println(format_args!(
136 "{} {}",
137 term::format::tertiary(&did),
138 term::format::parens(term::format::dim(alias))
139 ));
140 } else {
141 term::println(term::format::tertiary(&did));
142 }
143 }
144 }
145 Target::Visibility => {
146 let (_, doc) = repo(rid, storage)?;
147 term::println(term::format::visibility(doc.visibility()));
148 }
149 Target::History => {
150 let (repo, _) = repo(rid, storage)?;
151 let identity = Identity::load(&repo)?;
152 let head = repo.identity_head()?;
153 let history = repo.revwalk(head)?;
154
155 for oid in history {
156 let oid = oid?.into();
157 let tip = repo.commit(oid)?;
158
159 let Some(revision) = identity.revision(&tip.id().into()) else {
160 continue;
161 };
162 if !revision.is_accepted() {
163 continue;
164 }
165 let doc = &revision.doc;
166 let timezone = if tip.time().sign() == '+' {
167 #[allow(deprecated)]
168 FixedOffset::east(tip.time().offset_minutes() * 60)
169 } else {
170 #[allow(deprecated)]
171 FixedOffset::west(tip.time().offset_minutes() * 60)
172 };
173 let time = DateTime::<Utc>::from(
174 std::time::UNIX_EPOCH
175 + std::time::Duration::from_secs(tip.time().seconds() as u64),
176 )
177 .with_timezone(&timezone)
178 .to_rfc2822();
179
180 term::println(format_args!(
181 "{} {}",
182 term::format::yellow("commit"),
183 term::format::yellow(oid),
184 ));
185 if let Ok(parent) = tip.parent_id(0) {
186 term::println(format_args!("parent {parent}"));
187 }
188 term::println(format_args!("blob {}", revision.blob));
189 term::println(format_args!("date {time}"));
190 term::blank();
191
192 for line in tip.message()?.lines() {
193 if line.is_empty() {
194 term::blank();
195 } else {
196 term::indented(term::format::dim(line));
197 }
198 }
199 term::blank();
200
201 for line in json::to_pretty(&doc, Path::new("radicle.json"))? {
202 term::println(format_args!(" {line}"));
203 }
204
205 term::blank();
206 }
207 }
208 Target::RepoId => {
209 }
211 }
212
213 Ok(())
214}
215
216fn repo(rid: RepoId, storage: &Storage) -> anyhow::Result<(Repository, DocAt)> {
217 let repo = storage
218 .repository(rid)
219 .context("No repository with the given RID exists")?;
220 let doc = repo.identity_doc()?;
221
222 Ok((repo, doc))
223}
224
225fn refs(repo: &radicle::storage::git::Repository) -> anyhow::Result<()> {
226 let mut refs = Vec::new();
227 for r in repo.references()? {
228 let r = r?;
229 if let Some(namespace) = r.namespace {
230 refs.push(format!("{}/{}", namespace, r.name));
231 }
232 }
233
234 term::print(tree(refs));
235
236 Ok(())
237}
238
239fn tree(mut refs: Vec<String>) -> String {
241 refs.sort();
242
243 let mut refs_expanded: Vec<Vec<String>> = Vec::new();
248 let mut ref_entries: HashMap<Vec<String>, usize> = HashMap::new();
250 let mut last: Vec<String> = Vec::new();
251
252 for r in refs {
253 let r: Vec<String> = r.split('/').map(|s| s.to_string()).collect();
254
255 for (i, v) in r.iter().enumerate() {
256 let last_v = last.get(i);
257 if Some(v) != last_v {
258 last = r.clone().iter().take(i + 1).map(String::from).collect();
259
260 refs_expanded.push(last.clone());
261
262 let mut dir = last.clone();
263 dir.pop();
264 if dir.is_empty() {
265 continue;
266 }
267
268 if let Some(num) = ref_entries.get_mut(&dir) {
269 *num += 1;
270 } else {
271 ref_entries.insert(dir, 1);
272 }
273 }
274 }
275 }
276 let mut tree = String::default();
277
278 for mut ref_components in refs_expanded {
279 let name = ref_components.pop().expect("non-empty vector");
281 if ref_components.is_empty() {
282 tree.push_str(&format!("{name}\n"));
283 continue;
284 }
285
286 for i in 1..ref_components.len() {
287 let parent: Vec<String> = ref_components.iter().take(i).cloned().collect();
288
289 let num = ref_entries.get(&parent).unwrap_or(&0);
290 if *num == 0 {
291 tree.push_str(" ");
292 } else {
293 tree.push_str("│ ");
294 }
295 }
296
297 if let Some(num) = ref_entries.get_mut(&ref_components) {
298 if *num == 1 {
299 tree.push_str(&format!("└── {name}\n"));
300 } else {
301 tree.push_str(&format!("├── {name}\n"));
302 }
303 *num -= 1;
304 }
305 }
306
307 tree
308}
309
310#[cfg(test)]
311mod test {
312 use super::*;
313
314 #[test]
315 fn test_tree() {
316 let arg = vec![
317 String::from("z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/refs/heads/master"),
318 String::from("z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/refs/rad/id"),
319 String::from("z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/refs/rad/sigrefs"),
320 ];
321 let exp = r#"
322z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
323└── refs
324 ├── heads
325 │ └── master
326 └── rad
327 ├── id
328 └── sigrefs
329"#
330 .trim_start();
331
332 assert_eq!(tree(arg), exp);
333 assert_eq!(tree(vec![String::new()]), "\n");
334 }
335}