radicle_cli/commands/
ls.rs

1use std::ffi::OsString;
2
3use radicle::storage::{ReadStorage, RepositoryInfo};
4
5use crate::terminal as term;
6use crate::terminal::args::{Args, Error, Help};
7
8use term::Element;
9
10pub const HELP: Help = Help {
11    name: "ls",
12    description: "List repositories",
13    version: env!("RADICLE_VERSION"),
14    usage: r#"
15Usage
16
17    rad ls [<option>...]
18
19    By default, this command shows you all repositories that you have forked or initialized.
20    If you wish to see all seeded repositories, use the `--seeded` option.
21
22Options
23
24    --private       Show only private repositories
25    --public        Show only public repositories
26    --seeded, -s    Show all seeded repositories
27    --all, -a       Show all repositories in storage
28    --verbose, -v   Verbose output
29    --help          Print help
30"#,
31};
32
33pub struct Options {
34    #[allow(dead_code)]
35    verbose: bool,
36    public: bool,
37    private: bool,
38    all: bool,
39    seeded: bool,
40}
41
42impl Args for Options {
43    fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
44        use lexopt::prelude::*;
45
46        let mut parser = lexopt::Parser::from_args(args);
47        let mut verbose = false;
48        let mut private = false;
49        let mut public = false;
50        let mut all = false;
51        let mut seeded = false;
52
53        while let Some(arg) = parser.next()? {
54            match arg {
55                Long("help") | Short('h') => {
56                    return Err(Error::Help.into());
57                }
58                Long("all") | Short('a') => {
59                    all = true;
60                }
61                Long("seeded") | Short('s') => {
62                    seeded = true;
63                }
64                Long("private") => {
65                    private = true;
66                }
67                Long("public") => {
68                    public = true;
69                }
70                Long("verbose") | Short('v') => verbose = true,
71                _ => anyhow::bail!(arg.unexpected()),
72            }
73        }
74
75        Ok((
76            Options {
77                verbose,
78                private,
79                public,
80                all,
81                seeded,
82            },
83            vec![],
84        ))
85    }
86}
87
88pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
89    let profile = ctx.profile()?;
90    let storage = &profile.storage;
91    let repos = storage.repositories()?;
92    let policy = profile.policies()?;
93    let mut table = term::Table::new(term::TableOptions::bordered());
94    let mut rows = Vec::new();
95
96    if repos.is_empty() {
97        return Ok(());
98    }
99
100    for RepositoryInfo {
101        rid,
102        head,
103        doc,
104        refs,
105        ..
106    } in repos
107    {
108        if doc.is_public() && options.private && !options.public {
109            continue;
110        }
111        if !doc.is_public() && !options.private && options.public {
112            continue;
113        }
114        if refs.is_none() && !options.all && !options.seeded {
115            continue;
116        }
117        let seeded = policy.is_seeding(&rid)?;
118
119        if !seeded && !options.all {
120            continue;
121        }
122        if !seeded && options.seeded {
123            continue;
124        }
125        let proj = match doc.project() {
126            Ok(p) => p,
127            Err(e) => {
128                log::error!(target: "cli", "Error loading project payload for {rid}: {e}");
129                continue;
130            }
131        };
132        let head = term::format::oid(head).into();
133
134        rows.push([
135            term::format::bold(proj.name().to_owned()),
136            term::format::tertiary(rid.urn()),
137            if seeded {
138                term::format::visibility(doc.visibility()).into()
139            } else {
140                term::format::dim("local").into()
141            },
142            term::format::secondary(head),
143            term::format::italic(proj.description().to_owned()),
144        ]);
145    }
146    rows.sort();
147
148    if rows.is_empty() {
149        term::print(term::format::italic("Nothing to show."));
150    } else {
151        table.header([
152            "Name".into(),
153            "RID".into(),
154            "Visibility".into(),
155            "Head".into(),
156            "Description".into(),
157        ]);
158        table.divider();
159        table.extend(rows);
160        table.print();
161    }
162
163    Ok(())
164}