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 #[arg(value_name = "PATHSPEC")]
61 paths: Vec<PathBuf>,
62 },
63 Diff {
65 #[arg(long)]
67 workspace: bool,
68 path: PathBuf,
69 },
70 Sync {
72 #[arg(long)]
74 remote: Option<String>,
75 },
76 Prune,
78}
79
80pub fn run_cli() -> Result<ExitCode, AppError> {
81 let cli = Cli::parse();
82 let git = Git::discover()?;
83 let channel = review_channel_from_selection(cli.channel.as_deref(), &git)?;
84 let notes = GitNotesStore::new(&git, channel.notes_ref().clone());
85
86 match cli.command {
87 CommandKind::Mark { paths, allow_dirty } => {
88 let dirty_paths = if allow_dirty {
89 DirtyPathHandling::Allow
90 } else {
91 DirtyPathHandling::Prompt
92 };
93 mark_paths(&git, ¬es, &paths, MarkOptions { dirty_paths })?;
94 Ok(ExitCode::SUCCESS)
95 }
96 CommandKind::Unmark { paths } => {
97 unmark_paths(&git, ¬es, &paths)?;
98 Ok(ExitCode::SUCCESS)
99 }
100 CommandKind::Status {
101 json,
102 all,
103 check,
104 workspace,
105 paths,
106 } => {
107 let target = if workspace {
108 StatusTarget::Workspace
109 } else {
110 StatusTarget::Head
111 };
112 match status(
113 &git,
114 ¬es,
115 &channel,
116 StatusMode {
117 json,
118 all: all || !paths.is_empty(),
119 check,
120 target,
121 },
122 &paths,
123 )? {
124 Gate::Open => Ok(ExitCode::SUCCESS),
125 Gate::Closed => Ok(ExitCode::from(1)),
126 }
127 }
128 CommandKind::Diff { path, workspace } => {
129 let target = if workspace {
130 DiffTarget::Workspace
131 } else {
132 DiffTarget::Head
133 };
134 diff_path(&git, ¬es, &path, target)?;
135 Ok(ExitCode::SUCCESS)
136 }
137 CommandKind::Sync { remote } => {
138 let remote = git.select_sync_remote(remote.as_deref())?;
139 let mut progress = SyncProgressReporter::from_environment();
140 sync_notes(¬es, &channel, &remote, &mut progress)?;
141 Ok(ExitCode::SUCCESS)
142 }
143 CommandKind::Prune => {
144 notes.prune()?;
145 Ok(ExitCode::SUCCESS)
146 }
147 }
148}
149
150fn review_channel_from_selection(
151 explicit: Option<&str>,
152 git: &Git,
153) -> Result<ReviewChannel, AppError> {
154 match explicit {
155 Some(input) => review_channel_from_input(input, ReviewChannelSource::Cli),
156 None => git.configured_review_channel()?.map_or_else(
157 || {
158 review_channel_from_input(
159 DEFAULT_REVIEW_CHANNEL,
160 ReviewChannelSource::BuiltInDefault,
161 )
162 },
163 |input| review_channel_from_input(&input, ReviewChannelSource::Config),
164 ),
165 }
166}
167
168#[derive(Clone, Copy, Debug)]
169enum ReviewChannelSource {
170 Cli,
171 Config,
172 BuiltInDefault,
173}
174
175fn review_channel_from_input(
176 input: &str,
177 source: ReviewChannelSource,
178) -> Result<ReviewChannel, AppError> {
179 ReviewChannel::new(input).map_err(|error| channel_error_from_source(input, &error, source))
180}
181
182fn channel_error_from_source(
183 input: &str,
184 error: &ChannelError,
185 source: ReviewChannelSource,
186) -> AppError {
187 AppError::InvalidChannel {
188 channel: input.to_owned(),
189 details: details_from_source(error.to_string(), source),
190 }
191}
192
193fn details_from_source(details: String, source: ReviewChannelSource) -> String {
194 match source {
195 ReviewChannelSource::Config => format!("from git config vet.channel: {details}"),
196 ReviewChannelSource::Cli | ReviewChannelSource::BuiltInDefault => details,
197 }
198}