diff --git a/crates/tty7-core/src/core/git/mod.rs b/crates/tty7-core/src/core/git/mod.rs index dcbe19e4..1e377597 100644 --- a/crates/tty7-core/src/core/git/mod.rs +++ b/crates/tty7-core/src/core/git/mod.rs @@ -113,9 +113,14 @@ pub fn git_output(cwd: &Path, args: &[&str]) -> io::Result { 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( diff --git a/crates/tty7-core/src/core/git/ops.rs b/crates/tty7-core/src/core/git/ops.rs index 9a83b8a2..7cda47da 100644 --- a/crates/tty7-core/src/core/git/ops.rs +++ b/crates/tty7-core/src/core/git/ops.rs @@ -514,24 +514,6 @@ fn batched(prefix: &[&str], specs: &[String]) -> Vec> { .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::>() }; - // 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 [