gitwatch_rs/
cli.rs

1use std::path::PathBuf;
2
3use clap::{Parser, ValueEnum};
4use clap_complete::Shell;
5use log::LevelFilter;
6use regex::Regex;
7use serde::Deserialize;
8
9#[derive(Parser)]
10#[command(
11    name = "gitwatch",
12    about = "CLI to watch a git repo and automatically commit changes"
13)]
14pub struct Cli {
15    #[command(subcommand)]
16    pub command: Commands,
17}
18
19#[derive(Parser)]
20pub enum Commands {
21    /// Watch a repository and commit changes
22    Watch(CliOptions),
23
24    /// Generate shell completion scripts
25    Completion {
26        /// The shell to generate completions for
27        #[arg(value_enum)]
28        shell: Shell,
29    },
30}
31
32#[derive(Parser)]
33pub struct CliOptions {
34    /// Path to the Git repository to monitor for changes
35    #[clap(default_value = ".")]
36    pub repository: PathBuf,
37
38    #[clap(flatten)]
39    pub commit_message: CommitMessageOptions,
40
41    /// Automatically commit any existing changes on start
42    #[clap(long = "commit-on-start", default_value = "true")]
43    pub commit_on_start: std::primitive::bool,
44
45    /// Number of seconds to wait before processing multiple changes to the same file.
46    /// Higher values reduce commit frequency but group more changes together.
47    #[clap(long = "debounce-seconds", default_value = "1", verbatim_doc_comment)]
48    pub debounce_seconds: u64,
49
50    /// Run without performing actual Git operations (staging, committing, etc.)
51    #[clap(long = "dry-run", default_value = "false")]
52    pub dry_run: bool,
53
54    /// Regular expression pattern for files to exclude from watching.
55    /// Matching is performed against repository-relative file paths.
56    /// Note: the .git folder & gitignored files are ignored by default.
57    /// Example: "\.tmp$" to ignore temporary files.
58    #[clap(short = 'i', long = "ignore-regex", verbatim_doc_comment)]
59    pub ignore_regex: Option<Regex>,
60
61    /// Set the log level
62    #[arg(long, value_enum, default_value_t = LogLevel::Info)]
63    pub log_level: LogLevel,
64
65    /// Name of the remote to push to (if specified).
66    /// Example: "origin".
67    #[clap(short = 'r', long = "remote", verbatim_doc_comment)]
68    pub remote: Option<String>,
69
70    /// Number of retry attempts when errors occur.
71    /// Use -1 for infinite retries.
72    #[clap(long = "retries", default_value = "3", verbatim_doc_comment)]
73    pub retries: i32,
74
75    /// Enable continuous monitoring of filesystem changes.
76    /// Set to false for one-time commit of current changes.
77    #[clap(
78        short = 'w',
79        long = "watch",
80        default_value = "true",
81        verbatim_doc_comment
82    )]
83    pub watch: std::primitive::bool,
84}
85
86#[derive(Clone, Debug, clap::Args)]
87#[group(multiple = false)]
88pub struct CommitMessageOptions {
89    #[clap(short = 'm', long = "commit-message")]
90    /// Static commit message to use for all commits
91    pub message: Option<String>,
92
93    /// Path to executable script that generates commit messages.
94    /// The path can be absolute or relative to the repository.
95    /// The script is executed with the repository as working directory
96    /// and must output the message to stdout.
97    #[clap(long = "commit-message-script", verbatim_doc_comment)]
98    pub script: Option<PathBuf>,
99}
100
101#[derive(Copy, Clone, Debug, Default, Deserialize, ValueEnum)]
102pub enum LogLevel {
103    Trace,
104    Debug,
105    #[default]
106    Info,
107    Warn,
108    Error,
109}
110
111impl From<LogLevel> for LevelFilter {
112    fn from(level: LogLevel) -> Self {
113        match level {
114            LogLevel::Trace => LevelFilter::Trace,
115            LogLevel::Debug => LevelFilter::Debug,
116            LogLevel::Info => LevelFilter::Info,
117            LogLevel::Warn => LevelFilter::Warn,
118            LogLevel::Error => LevelFilter::Error,
119        }
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn test_loglevel_conversion() {
129        let conversions = [
130            (LogLevel::Trace, LevelFilter::Trace),
131            (LogLevel::Debug, LevelFilter::Debug),
132            (LogLevel::Info, LevelFilter::Info),
133            (LogLevel::Warn, LevelFilter::Warn),
134            (LogLevel::Error, LevelFilter::Error),
135        ];
136
137        for (input, expected) in conversions {
138            assert_eq!(LevelFilter::from(input), expected);
139        }
140    }
141}