mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 00:02:23 +00:00
feat(git): give network operations the long deadline
run_op now routes fetch/pull/push through Host::git_with_deadline. The no-prompt environment turned out not to belong here at all — LocalHost puts it on every call, on both sides of the wire, so ops.rs's own copy of the same four variables was a second definition waiting to drift. Removed it; the conformance case git_terminal_prompt_is_disabled guards the behaviour across local and remote, which the unit test on the constant could not. Also corrects git_output_with_env's doc comment, which claimed read paths must not inherit the no-prompt environment. They do, deliberately: a read path never prompts, and a request arriving over the wire carries no bit saying which kind it is.
This commit is contained in:
@@ -113,9 +113,14 @@ pub fn git_output(cwd: &Path, args: &[&str]) -> io::Result<Output> {
|
||||
git_output_with_env(cwd, args, &[])
|
||||
}
|
||||
|
||||
/// `git_output` plus extra environment. Network operations (`fetch`/`pull`/
|
||||
/// `push`) need to be told they have no terminal to prompt at; read paths must
|
||||
/// *not* inherit that, so the two share a body rather than a config.
|
||||
/// `git_output` plus extra environment.
|
||||
///
|
||||
/// What `LocalHost` passes is the no-prompt set: git and ssh have to fail
|
||||
/// rather than block on a credential prompt nobody is watching. It goes on
|
||||
/// every call, not just `fetch`/`pull`/`push` — a read path never prompts, so
|
||||
/// carrying it there costs nothing, and a remote workspace only inherits it
|
||||
/// because the far side's `LocalHost` applies the same rule to a request that
|
||||
/// arrived over the wire with no "this one is a network op" bit on it.
|
||||
///
|
||||
/// A `None` value removes the variable instead of setting it.
|
||||
pub fn git_output_with_env(
|
||||
|
||||
@@ -514,24 +514,6 @@ fn batched(prefix: &[&str], specs: &[String]) -> Vec<Vec<String>> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The environment a network operation needs, for whoever spawns it.
|
||||
///
|
||||
/// Without `GIT_TERMINAL_PROMPT=0` git opens `/dev/tty` directly when stdin is
|
||||
/// closed, and in some environments it gets one and hangs on "Username for
|
||||
/// ...". A `None` value means the variable is removed.
|
||||
///
|
||||
/// This lives here next to the operations that need it, but only
|
||||
/// `git_output_with_env` can apply it — `Host::git` takes no environment. It is
|
||||
/// the host layer's job to reach for this.
|
||||
pub fn network_env() -> &'static [(&'static str, Option<&'static str>)] {
|
||||
&[
|
||||
("GIT_TERMINAL_PROMPT", Some("0")),
|
||||
("GIT_ASKPASS", None),
|
||||
("SSH_ASKPASS", None),
|
||||
("SSH_ASKPASS_REQUIRE", Some("never")),
|
||||
]
|
||||
}
|
||||
|
||||
/// What a failure means, from git's own words.
|
||||
///
|
||||
/// Substring matching on English output, checked in a fixed order because the
|
||||
@@ -637,11 +619,16 @@ pub fn run_op(
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
// Network operations still take the plain path: they want
|
||||
// `GIT_NETWORK_DEADLINE` and `network_env`, and neither can be
|
||||
// expressed through `Host::git`. Both arrive with the host layer's
|
||||
// `git_with_deadline`.
|
||||
let out = host.git(root, &borrowed).map_err(|err| GitOpError {
|
||||
// A push over a slow link outlives the deadline a `Git` request gets
|
||||
// by default, which is sized for interactive queries. The no-prompt
|
||||
// environment is not this layer's business: `LocalHost` puts it on
|
||||
// every call, on both sides of the wire.
|
||||
let spawned = if op.is_network() {
|
||||
host.git_with_deadline(root, &borrowed, GIT_NETWORK_DEADLINE)
|
||||
} else {
|
||||
host.git(root, &borrowed)
|
||||
};
|
||||
let out = spawned.map_err(|err| GitOpError {
|
||||
op: label,
|
||||
kind: GitOpErrorKind::Spawn,
|
||||
message: err.to_string(),
|
||||
@@ -1353,30 +1340,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_network_environment_closes_every_prompt() {
|
||||
let env = network_env();
|
||||
assert_eq!(
|
||||
env.iter().find(|(k, _)| *k == "GIT_TERMINAL_PROMPT"),
|
||||
Some(&("GIT_TERMINAL_PROMPT", Some("0"))),
|
||||
);
|
||||
for key in ["GIT_ASKPASS", "SSH_ASKPASS"] {
|
||||
assert_eq!(
|
||||
env.iter().find(|(k, _)| *k == key).map(|(_, v)| *v),
|
||||
Some(None),
|
||||
"{key} has to be removed, not set",
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
env.iter().find(|(k, _)| *k == "SSH_ASKPASS_REQUIRE"),
|
||||
Some(&("SSH_ASKPASS_REQUIRE", Some("never"))),
|
||||
);
|
||||
}
|
||||
|
||||
fn kind_of(stderr: &str) -> GitOpErrorKind {
|
||||
classify(stderr, Some(1))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classify_recognizes_every_shape_of_credential_failure() {
|
||||
for stderr in [
|
||||
|
||||
Reference in New Issue
Block a user