Skip to main content

git_global/
cli.rs

1//! The command line interface for git-global.
2
3use std::io::{Write, stderr, stdout};
4
5use clap::{Arg, ArgAction, ArgMatches, Command, command};
6use serde_json::json;
7
8use crate::config::Config;
9use crate::subcommands;
10
11/// Returns the definitive clap::Command instance for git-global.
12pub fn get_clap_app() -> Command {
13    command!()
14        .arg(
15            Arg::new("config")
16                .short('c')
17                .long("config")
18                .value_name("FILE")
19                .global(true)
20                .help("Read configuration from FILE instead of the default gitconfig."),
21        )
22        .arg(
23            Arg::new("verbose")
24                .short('v')
25                .long("verbose")
26                .action(ArgAction::SetTrue)
27                .global(true)
28                .help("Enable verbose mode."),
29        )
30        .arg(
31            Arg::new("json")
32                .short('j')
33                .long("json")
34                .action(ArgAction::SetTrue)
35                .global(true)
36                .help("Output subcommand results in JSON."),
37        )
38        .arg(
39            Arg::new("untracked")
40                .short('u')
41                .long("untracked")
42                .action(ArgAction::SetTrue)
43                .conflicts_with("nountracked")
44                .global(true)
45                .help("Show untracked files in output."),
46        )
47        .arg(
48            Arg::new("nountracked")
49                .short('t')
50                .long("nountracked")
51                .action(ArgAction::SetTrue)
52                .conflicts_with("untracked")
53                .global(true)
54                .help("Don't show untracked files in output."),
55        )
56        .subcommands(
57            subcommands::get_subcommands()
58                .iter()
59                .map(|(cmd, desc)| {
60                    let mut subcmd = Command::new(*cmd).about(*desc);
61                    if *cmd == "ignore" {
62                        subcmd = subcmd.arg(
63                            Arg::new("pattern")
64                                .help("Pattern to add to global.ignore (matches paths containing this string)")
65                                .required(true)
66                                .index(1),
67                        );
68                    }
69                    if *cmd == "scan" {
70                        subcmd = subcmd.arg(
71                            Arg::new("paths")
72                                .help("Additional directories to scan for git repos")
73                                .num_args(0..)
74                                .value_name("PATH"),
75                        );
76                    }
77                    subcmd
78                }),
79        )
80}
81
82/// Merge command-line arguments from an ArgMatches object with a Config.
83fn merge_args_with_config(config: &mut Config, matches: &ArgMatches) {
84    if matches.get_flag("verbose") {
85        config.verbose = true;
86    }
87    if matches.get_flag("untracked") {
88        config.show_untracked = true;
89    }
90    if matches.get_flag("nountracked") {
91        config.show_untracked = false;
92    }
93}
94
95/// Runs the appropriate git-global subcommand based on command line arguments.
96///
97/// As the effective binary entry point for `git-global`, prints results to
98/// `STDOUT` (or errors to `STDERR`) and returns an exit code.
99pub fn run_from_command_line() -> i32 {
100    let clap_app = get_clap_app();
101    let matches = clap_app.get_matches();
102    let mut config = match matches.get_one::<String>("config") {
103        Some(path) => Config::from_gitconfig(path),
104        None => Config::new(),
105    };
106    merge_args_with_config(&mut config, &matches);
107
108    // Extract additional arguments for subcommands that need them
109    let args: Vec<String> = matches
110        .subcommand()
111        .map(|(name, sub_matches)| match name {
112            "ignore" => sub_matches
113                .get_one::<String>("pattern")
114                .cloned()
115                .into_iter()
116                .collect(),
117            "scan" => sub_matches
118                .get_many::<String>("paths")
119                .map(|v| v.cloned().collect())
120                .unwrap_or_default(),
121            _ => vec![],
122        })
123        .unwrap_or_default();
124
125    let report = subcommands::run(matches.subcommand_name(), config, args);
126    let use_json = matches.get_flag("json");
127    match report {
128        Ok(rep) => {
129            if use_json {
130                rep.print_json(&mut stdout());
131            } else {
132                rep.print(&mut stdout());
133            }
134            0
135        }
136        Err(err) => {
137            if use_json {
138                let out = json!({
139                    "error": true,
140                    "message": format!("{}", err)
141                });
142                writeln!(&mut stderr(), "{:#}", out).unwrap();
143            } else {
144                writeln!(&mut stderr(), "{}", err).unwrap();
145            }
146            1
147        }
148    }
149}