Skip to main content

zoi_cli/cmd/
repo.rs

1use std::collections::HashSet;
2
3use anyhow::{Result, anyhow};
4use clap::{Parser, Subcommand};
5use colored::Colorize;
6use comfy_table::Table;
7use comfy_table::presets::UTF8_FULL;
8
9use crate::pkg::config;
10
11/// Arguments for the `repo` command.
12#[derive(Parser)]
13pub struct RepoCommand {
14    /// Automatically answer yes to all prompts
15    #[arg(
16        short = 'y',
17        long,
18        help = "Automatically answer yes to all prompts",
19        global = true
20    )]
21    yes: bool,
22    /// The repository sub-command to run.
23    #[command(subcommand)]
24    command: Commands
25}
26
27/// Available repository sub-commands.
28#[derive(Subcommand)]
29enum Commands {
30    /// Add a repository to the configuration or clone from a git URL
31    #[command(alias = "a")]
32    Add {
33        /// The name of the repository to add or a git URL to clone
34        repo_or_url: Option<String>
35    },
36    /// Remove a repository from the active configuration
37    #[command(alias = "rm")]
38    Remove {
39        /// The name of the repository to remove
40        repo_name: String
41    },
42    /// List repositories (active by default); use `list all` to show all
43    #[command(alias = "ls")]
44    List {
45        /// Which repositories to list
46        #[command(subcommand)]
47        which: Option<ListSub>
48    },
49    /// Manage cloned git repositories
50    #[command(subcommand)]
51    Git(GitCommand)
52}
53
54/// Runs the `repo` command.
55///
56/// # Errors
57///
58/// This function returns an error if:
59/// - A repository name or URL is missing when running non-interactively with
60///   `--yes`.
61/// - Adding, cloning, or removing a repository fails.
62/// - The configuration file cannot be read or modified.
63/// # Errors
64///
65/// Returns an error if the repository operation fails.
66pub fn run(args: RepoCommand) -> Result<()> {
67    let yes = args.yes;
68    match args.command {
69        Commands::Add { repo_or_url } => {
70            if let Some(val) = repo_or_url {
71                if val.starts_with("http://")
72                    || val.starts_with("https://")
73                    || std::path::Path::new(&val)
74                        .extension()
75                        .is_some_and(|ext| ext.eq_ignore_ascii_case("git"))
76                {
77                    config::clone_git_repo(&val)?;
78                } else {
79                    config::add_repo(&val)?;
80                    println!(
81                        "Repository '{}' added successfully.",
82                        val.green()
83                    );
84                }
85            } else if yes {
86                return Err(anyhow!(
87                    "A repository name or URL is required when using --yes."
88                ));
89            } else {
90                config::interactive_add_repo()?;
91            }
92        }
93        Commands::Remove { repo_name } => {
94            config::remove_repo(&repo_name)?;
95            println!(
96                "Repository '{}' removed successfully.",
97                repo_name.green()
98            );
99        }
100        Commands::List { which } => match which {
101            None => run_list_active()?,
102            Some(ListSub::All) => run_list_all()?
103        },
104        Commands::Git(cmd) => match cmd {
105            GitCommand::List => run_list_git_only()?,
106            GitCommand::Rm { repo_name } => {
107                config::remove_git_repo(&repo_name)?;
108            }
109        }
110    }
111    Ok(())
112}
113
114/// Lists active repositories.
115fn run_list_active() -> Result<()> {
116    let config = config::read_config()?;
117    if config.repos.is_empty() {
118        println!("No active repositories.");
119        return Ok(());
120    }
121
122    println!("{} Active repositories:", "::".bold().blue());
123    let mut table = Table::new();
124    table.load_style(UTF8_FULL).set_header(vec!["Repository"]);
125    for repo in config.repos {
126        table.add_row(vec![repo]);
127    }
128    println!("{table}");
129    Ok(())
130}
131
132/// Lists all available repositories.
133fn run_list_all() -> Result<()> {
134    let active_repos = config::read_config()?
135        .repos
136        .into_iter()
137        .collect::<HashSet<_>>();
138    let all_repos = config::get_all_repos()?;
139
140    println!("{} All available repositories:", "::".bold().blue());
141    let mut table = Table::new();
142    table
143        .load_style(UTF8_FULL)
144        .set_header(vec!["Status", "Repository"]);
145
146    for repo in all_repos {
147        let status = if active_repos.contains(&repo.to_lowercase()) {
148            "Added"
149        } else {
150            ""
151        };
152        table.add_row(vec![status.to_string(), repo]);
153    }
154    println!("{table}");
155    Ok(())
156}
157
158/// Options for listing repositories.
159#[derive(Subcommand)]
160enum ListSub {
161    /// Show all available repositories (active + discovered)
162    All
163}
164
165/// Available git repository sub-commands.
166#[derive(Subcommand)]
167enum GitCommand {
168    /// Show only cloned git repositories (~/.zoi/pkgs/git)
169    #[command(alias = "ls")]
170    List,
171    /// Remove a cloned git repository directory (~/.zoi/pkgs/git/<repo-name>)
172    Rm {
173        /// The name of the repository to remove
174        repo_name: String
175    }
176}
177
178/// Lists only cloned git repositories.
179fn run_list_git_only() -> Result<()> {
180    let repos = config::list_git_repos()?;
181    if repos.is_empty() {
182        println!("No cloned git repositories.");
183        return Ok(());
184    }
185
186    println!(
187        "{} Cloned git repositories (~/.zoi/pkgs/git):",
188        "::".bold().blue()
189    );
190    let mut table = Table::new();
191    table.load_style(UTF8_FULL).set_header(vec!["Repository"]);
192    for repo in repos {
193        table.add_row(vec![repo]);
194    }
195    println!("{table}");
196    Ok(())
197}