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