From b8757372b37ad39de6f366f5bb673d3a36ee90ec Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Sun, 9 Aug 2026 00:34:59 +0800 Subject: [PATCH] feat(git): give network operations the long deadline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- crates/tty7-core/src/core/git/mod.rs | 11 ++++-- crates/tty7-core/src/core/git/ops.rs | 57 +++++----------------------- 2 files changed, 18 insertions(+), 50 deletions(-) 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 [