1use std::path::PathBuf;
2use std::process::ExitCode;
3
4use clap::{Parser, Subcommand};
5
6use crate::channel::{ChannelError, DEFAULT_REVIEW_CHANNEL, ReviewChannel};
7use crate::commands::{
8 DiffTarget, DirtyPathHandling, Gate, MarkOptions, StatusMode, StatusTarget, diff_path,
9 mark_paths, status, sync_notes, unmark_paths,
10};
11use crate::error::AppError;
12use crate::git::Git;
13use crate::notes::{GitNotesStore, NotesStore};
14use crate::sync_progress::SyncProgressReporter;
15
16#[derive(Parser, Debug)]
17#[command(
18 name = "git-vet",
19 version,
20 about = "Track human review state for Git-tracked file contents"
21)]
22pub struct Cli {
23 #[arg(long, global = true)]
25 channel: Option<String>,
26 #[command(subcommand)]
27 command: CommandKind,
28}
29
30#[derive(Subcommand, Debug)]
31enum CommandKind {
32 Mark {
34 #[arg(long)]
36 allow_dirty: bool,
37 #[arg(required = true)]
38 paths: Vec<PathBuf>,
39 },
40 Unmark {
42 #[arg(required = true)]
43 paths: Vec<PathBuf>,
44 },
45 Status {
47 #[arg(long)]
49 json: bool,
50 #[arg(long)]
52 all: bool,
53 #[arg(long)]
55 check: bool,
56 #[arg(long)]
58 workspace: bool,
59 },
60 Diff {
62 #[arg(long)]
64 worktree: bool,
65 path: PathBuf,
66 },
67 Sync {
69 #[arg(long)]
71 remote: Option<String>,
72 },
73 Prune,
75}
76
77pub fn run_cli() -> Result<ExitCode, AppError> {
78 let cli = Cli::parse();
79 let git = Git::discover()?;
80 let channel = review_channel_from_selection(cli.channel.as_deref(), &git)?;
81 let notes = GitNotesStore::new(&git, channel.notes_ref().clone());
82
83 match cli.command {
84 CommandKind::Mark { paths, allow_dirty } => {
85 let dirty_paths = if allow_dirty {
86 DirtyPathHandling::Allow
87 } else {
88 DirtyPathHandling::Prompt
89 };
90 mark_paths(&git, ¬es, &paths, MarkOptions { dirty_paths })?;
91 Ok(ExitCode::SUCCESS)
92 }
93 CommandKind::Unmark { paths } => {
94 unmark_paths(&git, ¬es, &paths)?;
95 Ok(ExitCode::SUCCESS)
96 }
97 CommandKind::Status {
98 json,
99 all,
100 check,
101 workspace,
102 } => {
103 let target = if workspace {
104 StatusTarget::Workspace
105 } else {
106 StatusTarget::Head
107 };
108 match status(
109 &git,
110 ¬es,
111 &channel,
112 StatusMode {
113 json,
114 all,
115 check,
116 target,
117 },
118 )? {
119 Gate::Open => Ok(ExitCode::SUCCESS),
120 Gate::Closed => Ok(ExitCode::from(1)),
121 }
122 }
123 CommandKind::Diff { path, worktree } => {
124 let target = if worktree {
125 DiffTarget::Worktree
126 } else {
127 DiffTarget::Head
128 };
129 diff_path(&git, ¬es, &path, target)?;
130 Ok(ExitCode::SUCCESS)
131 }
132 CommandKind::Sync { remote } => {
133 let remote = git.select_sync_remote(remote.as_deref())?;
134 let mut progress = SyncProgressReporter::from_environment();
135 sync_notes(¬es, &channel, &remote, &mut progress)?;
136 Ok(ExitCode::SUCCESS)
137 }
138 CommandKind::Prune => {
139 notes.prune()?;
140 Ok(ExitCode::SUCCESS)
141 }
142 }
143}
144
145fn review_channel_from_selection(
146 explicit: Option<&str>,
147 git: &Git,
148) -> Result<ReviewChannel, AppError> {
149 match explicit {
150 Some(input) => review_channel_from_input(input, ReviewChannelSource::Cli),
151 None => git.configured_review_channel()?.map_or_else(
152 || {
153 review_channel_from_input(
154 DEFAULT_REVIEW_CHANNEL,
155 ReviewChannelSource::BuiltInDefault,
156 )
157 },
158 |input| review_channel_from_input(&input, ReviewChannelSource::Config),
159 ),
160 }
161}
162
163#[derive(Clone, Copy, Debug)]
164enum ReviewChannelSource {
165 Cli,
166 Config,
167 BuiltInDefault,
168}
169
170fn review_channel_from_input(
171 input: &str,
172 source: ReviewChannelSource,
173) -> Result<ReviewChannel, AppError> {
174 ReviewChannel::new(input).map_err(|error| channel_error_from_source(input, &error, source))
175}
176
177fn channel_error_from_source(
178 input: &str,
179 error: &ChannelError,
180 source: ReviewChannelSource,
181) -> AppError {
182 AppError::InvalidChannel {
183 channel: input.to_owned(),
184 details: details_from_source(error.to_string(), source),
185 }
186}
187
188fn details_from_source(details: String, source: ReviewChannelSource) -> String {
189 match source {
190 ReviewChannelSource::Config => format!("from git config vet.channel: {details}"),
191 ReviewChannelSource::Cli | ReviewChannelSource::BuiltInDefault => details,
192 }
193}