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::{
8 DirtyPathHandling, Gate, MarkOptions, StatusMode, diff_path, mark_paths, status, unmark_paths,
9};
10use crate::error::AppError;
11use crate::git::Git;
12use crate::git_ref_format::{CheckRefFormatError, check_ref_format};
13use crate::notes::{GitNotesStore, NotesStore};
14
15#[derive(Parser, Debug)]
16#[command(
17 name = "git-vet",
18 version,
19 about = "Track human review state for Git-tracked file contents"
20)]
21pub struct Cli {
22 #[arg(long, global = true, default_value = DEFAULT_REVIEW_CHANNEL)]
24 channel: String,
25 #[command(subcommand)]
26 command: CommandKind,
27}
28
29#[derive(Subcommand, Debug)]
30enum CommandKind {
31 Mark {
33 #[arg(long)]
35 allow_dirty: bool,
36 #[arg(required = true)]
37 paths: Vec<PathBuf>,
38 },
39 Unmark {
41 #[arg(required = true)]
42 paths: Vec<PathBuf>,
43 },
44 Status {
46 #[arg(long)]
48 json: bool,
49 #[arg(long)]
51 all: bool,
52 #[arg(long)]
54 check: bool,
55 },
56 Diff { path: PathBuf },
58 Sync {
60 #[arg(long)]
62 remote: Option<String>,
63 },
64 Prune,
66}
67
68pub fn run_cli() -> Result<ExitCode, AppError> {
69 let cli = Cli::parse();
70 let channel = review_channel_from_cli(&cli.channel)?;
71 let git = Git::discover()?;
72 let notes = GitNotesStore::new(&git, channel.notes_ref().clone());
73
74 match cli.command {
75 CommandKind::Mark { paths, allow_dirty } => {
76 let dirty_paths = if allow_dirty {
77 DirtyPathHandling::Allow
78 } else {
79 DirtyPathHandling::Prompt
80 };
81 mark_paths(&git, ¬es, &paths, MarkOptions { dirty_paths })?;
82 Ok(ExitCode::SUCCESS)
83 }
84 CommandKind::Unmark { paths } => {
85 unmark_paths(&git, ¬es, &paths)?;
86 Ok(ExitCode::SUCCESS)
87 }
88 CommandKind::Status { json, all, check } => {
89 match status(&git, ¬es, &channel, StatusMode { json, all, check })? {
90 Gate::Open => Ok(ExitCode::SUCCESS),
91 Gate::Closed => Ok(ExitCode::from(1)),
92 }
93 }
94 CommandKind::Diff { path } => {
95 diff_path(&git, ¬es, &path)?;
96 Ok(ExitCode::SUCCESS)
97 }
98 CommandKind::Sync { remote } => {
99 let remote = git.select_sync_remote(remote.as_deref())?;
100 notes.sync(&remote)?;
101 Ok(ExitCode::SUCCESS)
102 }
103 CommandKind::Prune => {
104 notes.prune()?;
105 Ok(ExitCode::SUCCESS)
106 }
107 }
108}
109
110fn review_channel_from_cli(input: &str) -> Result<ReviewChannel, AppError> {
111 let candidate = ReviewChannelCandidate::new(input)?;
112
113 match check_ref_format(candidate.notes_ref_name()) {
117 Ok(()) => Ok(candidate.into_channel_after_git_check_ref_format()),
118 Err(CheckRefFormatError::Rejected { ref_name, details }) => Err(candidate
119 .channel_error(format!(
120 "`git check-ref-format` rejected {ref_name:?}: {details}"
121 ))
122 .into()),
123 Err(CheckRefFormatError::Io(error)) => Err(AppError::Io(error)),
124 }
125}