Skip to main content

git_hunk/
cli.rs

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}