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}