Skip to main content

omni_dev/cli/
repo_arg.rs

1//! The `-C/--repo` flag, scoped to the commands that act on a local repository.
2//!
3//! Flattened as a subtree-`global = true` arg onto `git`, `coverage`, and
4//! `config scopes` (every leaf below them reads it), and onto the
5//! `worktrees rebase` / `worktrees push` leaves (the only `worktrees`
6//! subcommands that touch the local repository) — per the placement rule in
7//! [`crate::cli::ai_backend_args`]. Everywhere else clap rejects it rather
8//! than silently ignoring it (#1778).
9
10use std::path::{Path, PathBuf};
11
12use clap::Args;
13
14/// `-C/--repo <PATH>`.
15#[derive(Args, Debug, Clone, Default)]
16pub struct RepoArg {
17    /// Run as if omni-dev was started in `<PATH>` instead of the current
18    /// working directory.
19    ///
20    /// Resolved exactly once by the owning command and threaded explicitly to
21    /// its leaves as a parameter; deliberately **not** propagated to an
22    /// environment variable (so, unlike the other scoped flags, `RepoArg` has
23    /// no `apply()`), so the repo location never becomes an ambient global.
24    /// Mirrors `git -C`.
25    #[arg(long = "repo", short = 'C', global = true, value_name = "PATH")]
26    pub repo: Option<PathBuf>,
27}
28
29impl RepoArg {
30    /// The repository location, `None` meaning the current working directory.
31    pub fn path(&self) -> Option<&Path> {
32        self.repo.as_deref()
33    }
34}
35
36#[cfg(test)]
37#[allow(clippy::unwrap_used, clippy::expect_used)]
38mod tests {
39    use crate::cli::{Cli, Commands};
40    use clap::Parser;
41
42    fn git_repo(argv: &[&str]) -> Option<std::path::PathBuf> {
43        let cli = Cli::try_parse_from(argv).unwrap();
44        let Commands::Git(git) = cli.command else {
45            panic!("expected a git command");
46        };
47        git.repo.repo
48    }
49
50    #[test]
51    fn parses_long_and_short_after_the_owning_subcommand() {
52        let view = ["commit", "message", "view", "HEAD"];
53        for flag in ["--repo", "-C"] {
54            // Directly after the owner, and — being subtree-global — after the leaf.
55            let early: Vec<&str> = ["omni-dev", "git", flag, "/tmp/r"]
56                .into_iter()
57                .chain(view)
58                .collect();
59            let late: Vec<&str> = ["omni-dev", "git"]
60                .into_iter()
61                .chain(view)
62                .chain([flag, "/tmp/r"])
63                .collect();
64            for argv in [early, late] {
65                assert_eq!(
66                    git_repo(&argv).as_deref(),
67                    Some(std::path::Path::new("/tmp/r")),
68                    "{argv:?}"
69                );
70            }
71        }
72        assert!(git_repo(&["omni-dev", "git", "commit", "message", "view"]).is_none());
73    }
74
75    /// RULE 3: the repo location is a parameter, never a relocated global.
76    /// Unlike the other scoped-flag groups, `RepoArg` deliberately has no
77    /// `apply()` exporting it to an env var; this guard stops one being added
78    /// "to complete the pattern" by analogy with `AiBackendArgs`/`InstanceArg`.
79    #[test]
80    fn repo_arg_is_never_exported_to_the_environment() {
81        let source = include_str!("repo_arg.rs");
82        let production = source
83            .split("#[cfg(test)]")
84            .next()
85            .expect("split always yields a first piece");
86        for forbidden in ["set_var", "fn apply"] {
87            assert!(
88                !production.contains(forbidden),
89                "repo_arg.rs must not `{forbidden}`: -C/--repo is threaded as a \
90                 parameter, never exported to an env var"
91            );
92        }
93    }
94
95    /// The pre-#1778 placement — before the subcommand — no longer parses.
96    #[test]
97    fn rejected_before_the_subcommand_and_on_non_readers() {
98        for argv in [
99            &[
100                "omni-dev", "-C", "/tmp/r", "git", "commit", "message", "view",
101            ][..],
102            &["omni-dev", "--repo", "/tmp/r", "help-all"],
103            &["omni-dev", "config", "models", "show", "-C", "/tmp/r"],
104            &["omni-dev", "log", "-C", "/tmp/r"],
105        ] {
106            assert!(Cli::try_parse_from(argv).is_err(), "{argv:?}");
107        }
108    }
109}