1use std::path::PathBuf;
2use std::process::ExitCode;
3
4use clap::{Parser, Subcommand};
5
6use crate::channel::{DEFAULT_REVIEW_CHANNEL, ReviewChannel, ReviewChannelCandidate};
7use crate::commands::{Gate, StatusMode, diff_path, mark_paths, status};
8use crate::error::AppError;
9use crate::git::Git;
10use crate::git_ref_format::{CheckRefFormatError, check_ref_format};
11use crate::notes::{GitNotesStore, NotesStore};
12
13#[derive(Parser, Debug)]
14#[command(
15 name = "git-vet",
16 version,
17 about = "Track human review state for Git-tracked file contents"
18)]
19pub struct Cli {
20 #[arg(long, global = true, default_value = DEFAULT_REVIEW_CHANNEL)]
22 channel: String,
23 #[command(subcommand)]
24 command: CommandKind,
25}
26
27#[derive(Subcommand, Debug)]
28enum CommandKind {
29 Mark {
31 #[arg(required = true)]
32 paths: Vec<PathBuf>,
33 },
34 Status {
36 #[arg(long)]
38 json: bool,
39 #[arg(long)]
41 check: bool,
42 },
43 Diff { path: PathBuf },
45 Prune,
47}
48
49pub fn run_cli() -> Result<ExitCode, AppError> {
50 let cli = Cli::parse();
51 let channel = review_channel_from_cli(&cli.channel)?;
52 let git = Git::discover()?;
53 let notes = GitNotesStore::new(&git, channel.notes_ref().clone());
54
55 match cli.command {
56 CommandKind::Mark { paths } => {
57 mark_paths(&git, ¬es, &paths)?;
58 Ok(ExitCode::SUCCESS)
59 }
60 CommandKind::Status { json, check } => {
61 match status(&git, ¬es, &channel, StatusMode { json, check })? {
62 Gate::Open => Ok(ExitCode::SUCCESS),
63 Gate::Closed => Ok(ExitCode::from(1)),
64 }
65 }
66 CommandKind::Diff { path } => {
67 diff_path(&git, ¬es, &path)?;
68 Ok(ExitCode::SUCCESS)
69 }
70 CommandKind::Prune => {
71 notes.prune()?;
72 Ok(ExitCode::SUCCESS)
73 }
74 }
75}
76
77fn review_channel_from_cli(input: &str) -> Result<ReviewChannel, AppError> {
78 let candidate = ReviewChannelCandidate::new(input)?;
79
80 match check_ref_format(candidate.notes_ref_name()) {
84 Ok(()) => Ok(candidate.into_channel_after_git_check_ref_format()),
85 Err(CheckRefFormatError::Rejected { ref_name, details }) => Err(candidate
86 .channel_error(format!(
87 "`git check-ref-format` rejected {ref_name:?}: {details}"
88 ))
89 .into()),
90 Err(CheckRefFormatError::Io(error)) => Err(AppError::Io(error)),
91 }
92}