Skip to main content

git_cli/
utils.rs

1use crate::{clipboard, util};
2use nils_common::shell::quote_posix_single;
3use std::io::{self, Write};
4use std::process::Output;
5
6pub fn dispatch(cmd: &str, args: &[String]) -> Option<i32> {
7    match cmd {
8        "zip" => Some(zip(args)),
9        "copy-staged" | "copy" => Some(copy_staged(args)),
10        "root" => Some(root(args)),
11        "commit-hash" | "hash" => Some(commit_hash(args)),
12        _ => None,
13    }
14}
15
16fn zip(_args: &[String]) -> i32 {
17    let short = match git_stdout_trimmed(&["rev-parse", "--short", "HEAD"]) {
18        Ok(value) => value,
19        Err(code) => return code,
20    };
21    let filename = format!("backup-{short}.zip");
22    let output = match run_git_output(&["archive", "--format", "zip", "HEAD", "-o", &filename]) {
23        Some(output) => output,
24        None => return 1,
25    };
26    if output.status.success() {
27        0
28    } else {
29        emit_output(&output);
30        exit_code(&output)
31    }
32}
33
34fn copy_staged(args: &[String]) -> i32 {
35    let mut mode = CopyMode::Clipboard;
36    let mut mode_flags = 0usize;
37    let mut unknown_arg: Option<String> = None;
38
39    for arg in args {
40        match arg.as_str() {
41            "--stdout" | "-p" | "--print" => {
42                mode = CopyMode::Stdout;
43                mode_flags += 1;
44            }
45            "--both" => {
46                mode = CopyMode::Both;
47                mode_flags += 1;
48            }
49            "--help" | "-h" => {
50                print_copy_staged_help();
51                return 0;
52            }
53            _ => {
54                if unknown_arg.is_none() {
55                    unknown_arg = Some(arg.to_string());
56                }
57            }
58        }
59    }
60
61    if mode_flags > 1 {
62        eprintln!("❗ Only one output mode is allowed: --stdout or --both");
63        return 1;
64    }
65
66    if let Some(arg) = unknown_arg {
67        eprintln!("❗ Unknown argument: {arg}");
68        eprintln!("Usage: git-copy-staged [--stdout|--both]");
69        return 1;
70    }
71
72    let output = match run_git_output(&["diff", "--cached", "--no-color"]) {
73        Some(output) => output,
74        None => return 1,
75    };
76    if !output.status.success() {
77        emit_output(&output);
78        return exit_code(&output);
79    }
80
81    let diff = trim_trailing_newlines(&String::from_utf8_lossy(&output.stdout)).to_string();
82    if diff.is_empty() {
83        println!("⚠️  No staged changes to copy");
84        return 1;
85    }
86
87    match mode {
88        CopyMode::Stdout => {
89            println!("{diff}");
90            0
91        }
92        CopyMode::Clipboard => {
93            let _ = clipboard::set_clipboard_best_effort(&diff);
94            println!("✅ Staged diff copied to clipboard");
95            0
96        }
97        CopyMode::Both => {
98            let _ = clipboard::set_clipboard_best_effort(&diff);
99            println!("{diff}");
100            println!("✅ Staged diff copied to clipboard");
101            0
102        }
103    }
104}
105
106fn root(args: &[String]) -> i32 {
107    let shell_mode = args.iter().any(|arg| arg == "--shell");
108    let output = match run_git_output(&["rev-parse", "--show-toplevel"]) {
109        Some(output) => output,
110        None => return 1,
111    };
112
113    if !output.status.success() {
114        eprintln!("❌ Not in a git repository");
115        return 1;
116    }
117
118    let root = trim_trailing_newlines(&String::from_utf8_lossy(&output.stdout)).to_string();
119    if shell_mode {
120        println!("cd -- {}", shell_escape(&root));
121        eprintln!("📁 Jumped to Git root: {root}");
122    } else {
123        println!();
124        println!("📁 Jumped to Git root: {root}");
125    }
126    0
127}
128
129fn commit_hash(args: &[String]) -> i32 {
130    let Some(ref_arg) = args.first() else {
131        eprintln!("❌ Missing git ref");
132        return 1;
133    };
134
135    let ref_commit = format!("{ref_arg}^{{commit}}");
136    let output = match run_git_output(&["rev-parse", "--verify", "--quiet", &ref_commit]) {
137        Some(output) => output,
138        None => return 1,
139    };
140    if !output.status.success() {
141        emit_output(&output);
142        return exit_code(&output);
143    }
144
145    let _ = io::stdout().write_all(&output.stdout);
146    0
147}
148
149fn run_git_output(args: &[&str]) -> Option<Output> {
150    match util::run_output("git", args) {
151        Ok(output) => Some(output),
152        Err(err) => {
153            eprintln!("{err}");
154            None
155        }
156    }
157}
158
159fn git_stdout_trimmed(args: &[&str]) -> Result<String, i32> {
160    let output = run_git_output(args).ok_or(1)?;
161    if !output.status.success() {
162        emit_output(&output);
163        return Err(exit_code(&output));
164    }
165    Ok(trim_trailing_newlines(&String::from_utf8_lossy(&output.stdout)).to_string())
166}
167
168fn exit_code(output: &Output) -> i32 {
169    output.status.code().unwrap_or(1)
170}
171
172fn emit_output(output: &Output) {
173    let _ = io::stdout().write_all(&output.stdout);
174    let _ = io::stderr().write_all(&output.stderr);
175}
176
177fn trim_trailing_newlines(input: &str) -> &str {
178    input.trim_end_matches(['\n', '\r'])
179}
180
181fn shell_escape(value: &str) -> String {
182    quote_posix_single(value)
183}
184
185fn print_copy_staged_help() {
186    print!(
187        "Usage: git-copy-staged [--stdout|--both]\n  --stdout   Print staged diff to stdout (no status message)\n  --both     Print to stdout and copy to clipboard\n"
188    );
189}
190
191enum CopyMode {
192    Clipboard,
193    Stdout,
194    Both,
195}