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