Skip to main content

radicle_cli/commands/
inspect.rs

1mod 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                            // For their own refs, be more strict, and interpret
80                            // `FeatureLevel::Parent` at a root commit as
81                            // `FeatureLevel::Root`. This is so that users
82                            // have a chance of detecting that automatic migration
83                            // did not run or is otherwise broken.
84                            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            // Handled above.
210        }
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
239/// Show the list of given git references as a newline terminated tree `String` similar to the tree command.
240fn tree(mut refs: Vec<String>) -> String {
241    refs.sort();
242
243    // List of references with additional unique entries for each 'directory'.
244    //
245    // i.e. "refs/heads/master" becomes ["refs"], ["refs", "heads"], and ["refs", "heads",
246    // "master"].
247    let mut refs_expanded: Vec<Vec<String>> = Vec::new();
248    // Number of entries per Git 'directory'.
249    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        // Better to explode when things do not go as expected.
280        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}