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