1use std::path::PathBuf;
2
3use clap::{Args, Parser, Subcommand, ValueEnum};
4use serde::{Deserialize, Serialize};
5
6const CLI_AFTER_HELP: &str = "Agent loop:\n 1. git-hunk scan --mode stage --compact --json\n 2. git-hunk resolve --mode stage --snapshot <snapshot-id> --path src/lib.rs --start 42 --json\n 3. git-hunk show --mode stage <change-key> --json\n 4. git-hunk stage --snapshot <snapshot-id> --change-key <change-key> --dry-run --json\n 5. git-hunk stage --snapshot <snapshot-id> --change-key <change-key> --json\n 6. git-hunk scan --mode stage --compact --json";
7
8#[derive(Debug, Parser)]
9#[command(name = "git-hunk")]
10#[command(about = "Non-interactive hunk staging for AI agents")]
11#[command(after_help = CLI_AFTER_HELP)]
12pub struct Cli {
13 #[command(subcommand)]
14 pub command: Command,
15}
16
17impl Cli {
18 pub fn json(&self) -> bool {
19 match &self.command {
20 Command::Scan(args) => args.json,
21 Command::Show(args) => args.json,
22 Command::Resolve(args) => args.json,
23 Command::Validate(args) => args.json,
24 Command::Stage(args) => args.json,
25 Command::Unstage(args) => args.json,
26 Command::Commit(args) => args.json,
27 }
28 }
29}
30
31#[derive(Debug, Subcommand)]
32pub enum Command {
33 #[command(about = "Scan worktree or index changes into a snapshot")]
34 Scan(ScanArgs),
35 #[command(about = "Inspect a hunk, change id, or change key")]
36 Show(ShowArgs),
37 #[command(about = "Resolve a file and line hint into recommended selectors")]
38 Resolve(ResolveArgs),
39 #[command(about = "Validate selectors against the current snapshot and recover change keys")]
40 Validate(ValidateArgs),
41 #[command(about = "Stage an exact selection into the index")]
42 Stage(MutateArgs),
43 #[command(about = "Remove an exact selection from the index")]
44 Unstage(MutateArgs),
45 #[command(about = "Commit an exact selection, optionally with a dry-run preview")]
46 Commit(CommitArgs),
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
50#[serde(rename_all = "snake_case")]
51pub enum Mode {
52 Stage,
53 Unstage,
54}
55
56impl Mode {
57 pub fn as_str(self) -> &'static str {
58 match self {
59 Mode::Stage => "stage",
60 Mode::Unstage => "unstage",
61 }
62 }
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
66#[serde(rename_all = "snake_case")]
67pub enum ResolveSide {
68 Auto,
69 Old,
70 New,
71}
72
73impl ResolveSide {
74 pub fn as_str(self) -> &'static str {
75 match self {
76 ResolveSide::Auto => "auto",
77 ResolveSide::Old => "old",
78 ResolveSide::New => "new",
79 }
80 }
81}
82
83#[derive(Debug, Args)]
84pub struct ScanArgs {
85 #[arg(
86 long,
87 value_enum,
88 help = "Use 'stage' for worktree vs index or 'unstage' for index vs HEAD"
89 )]
90 pub mode: Mode,
91 #[arg(long, help = "Return metadata without full diff lines")]
92 pub compact: bool,
93 #[arg(long, help = "Print structured JSON output")]
94 pub json: bool,
95}
96
97#[derive(Debug, Args)]
98pub struct ShowArgs {
99 #[arg(
100 long,
101 value_enum,
102 help = "Use 'stage' for worktree vs index or 'unstage' for index vs HEAD"
103 )]
104 pub mode: Mode,
105 #[arg(help = "Hunk id, change id, or change key from scan")]
106 pub id: String,
107 #[arg(long, help = "Print structured JSON output")]
108 pub json: bool,
109}
110
111#[derive(Debug, Args)]
112pub struct ResolveArgs {
113 #[arg(
114 long,
115 value_enum,
116 help = "Use 'stage' for worktree vs index or 'unstage' for index vs HEAD"
117 )]
118 pub mode: Mode,
119 #[arg(long, help = "Snapshot id from scan")]
120 pub snapshot: String,
121 #[arg(long, help = "Changed file path to resolve against")]
122 pub path: String,
123 #[arg(long, help = "Starting line number on the requested side")]
124 pub start: u32,
125 #[arg(long, help = "Optional ending line number; defaults to --start")]
126 pub end: Option<u32>,
127 #[arg(
128 long,
129 value_enum,
130 default_value = "auto",
131 help = "Prefer new lines, old lines, or auto-detect the diff side"
132 )]
133 pub side: ResolveSide,
134 #[arg(long, help = "Print structured JSON output")]
135 pub json: bool,
136}
137
138#[derive(Debug, Args)]
139pub struct ValidateArgs {
140 #[arg(
141 long,
142 value_enum,
143 help = "Use 'stage' for worktree vs index or 'unstage' for index vs HEAD"
144 )]
145 pub mode: Mode,
146 #[arg(
147 long,
148 help = "Snapshot id to validate, or omit to validate selectors against the current state"
149 )]
150 pub snapshot: Option<String>,
151 #[arg(
152 long,
153 help = "Selection plan JSON file, or '-' to read the plan from stdin"
154 )]
155 pub plan: Option<PathBuf>,
156 #[arg(
157 long = "hunk",
158 help = "Whole hunk id or <hunk-id>:<old|new>:<start-end>"
159 )]
160 pub hunks: Vec<String>,
161 #[arg(long = "change", help = "Snapshot-local change id from scan")]
162 pub changes: Vec<String>,
163 #[arg(
164 long = "change-key",
165 help = "Rescan-stable change key from scan or resolve"
166 )]
167 pub change_keys: Vec<String>,
168 #[arg(long, help = "Return metadata without full diff lines")]
169 pub compact: bool,
170 #[arg(long, help = "Print structured JSON output")]
171 pub json: bool,
172}
173
174#[derive(Debug, Args)]
175pub struct MutateArgs {
176 #[arg(long, help = "Snapshot id from scan")]
177 pub snapshot: Option<String>,
178 #[arg(
179 long,
180 help = "Selection plan JSON file, or '-' to read the plan from stdin"
181 )]
182 pub plan: Option<PathBuf>,
183 #[arg(
184 long = "hunk",
185 help = "Whole hunk id or <hunk-id>:<old|new>:<start-end>"
186 )]
187 pub hunks: Vec<String>,
188 #[arg(long = "change", help = "Snapshot-local change id from scan")]
189 pub changes: Vec<String>,
190 #[arg(
191 long = "change-key",
192 help = "Rescan-stable change key from scan or resolve"
193 )]
194 pub change_keys: Vec<String>,
195 #[arg(long, help = "Preview the staged result without mutating the repo")]
196 pub dry_run: bool,
197 #[arg(long, help = "Return the next snapshot without full diff lines")]
198 pub compact: bool,
199 #[arg(long, help = "Print structured JSON output")]
200 pub json: bool,
201}
202
203#[derive(Debug, Args)]
204pub struct CommitArgs {
205 #[arg(short = 'm', long = "message", required = true)]
206 pub messages: Vec<String>,
207 #[arg(long, help = "Snapshot id from scan")]
208 pub snapshot: Option<String>,
209 #[arg(
210 long,
211 help = "Selection plan JSON file, or '-' to read the plan from stdin"
212 )]
213 pub plan: Option<PathBuf>,
214 #[arg(
215 long = "hunk",
216 help = "Whole hunk id or <hunk-id>:<old|new>:<start-end>"
217 )]
218 pub hunks: Vec<String>,
219 #[arg(long = "change", help = "Snapshot-local change id from scan")]
220 pub changes: Vec<String>,
221 #[arg(
222 long = "change-key",
223 help = "Rescan-stable change key from scan or resolve"
224 )]
225 pub change_keys: Vec<String>,
226 #[arg(long, help = "Allow an empty commit if nothing is staged")]
227 pub allow_empty: bool,
228 #[arg(long, help = "Preview the exact commit without mutating the repo")]
229 pub dry_run: bool,
230 #[arg(long, help = "Return the next snapshot without full diff lines")]
231 pub compact: bool,
232 #[arg(long, help = "Print structured JSON output")]
233 pub json: bool,
234}