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