mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-10-07 00:02:06 +00:00
fix(sidebar): stop ungrouping tabs when a git probe fails (#1011)
A tab's auto group comes from probing its cwd for a repository. Any failure of that probe - git failing to spawn, the macOS /usr/bin/git shim dying while Xcode is mid-switch, a remote link dropping - came back as "not a repository", overwrote the tab's remembered group, and was never re-checked while the pane sat idle. Tabs in perfectly good repositories dropped into Ungrouped and stayed there. - Read root, home and branch in-process on this machine with gix-discover instead of three git processes. Only the line counts still come from `git diff --numstat`, so they keep matching the diff views; a detached HEAD is still named by git for the same reason. - Remote hosts keep asking their own git (a new control request would force a dialect bump), but the probe now tells "not a repository" (exit 128, or the directory is gone) apart from a failure. - A failed probe leaves the cache as it was, so a tab keeps its remembered group, and an unanswered cwd is retried every 10s.
This commit is contained in:
@@ -2,7 +2,7 @@ use std::borrow::Cow;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
pub use crate::core::git::{GitStatus, RepoSnapshot, probe};
|
||||
pub use crate::core::git::{GitStatus, RepoProbe, RepoSnapshot, probe_repo};
|
||||
use crate::ui::host_ops::{ByHost, HostId, InFlight};
|
||||
|
||||
/// The spelling a directory is keyed by in here.
|
||||
@@ -213,6 +213,22 @@ impl GitStatusCache {
|
||||
}
|
||||
rerun
|
||||
}
|
||||
|
||||
/// Land a probe that got no answer ([`RepoProbe::Failed`]).
|
||||
///
|
||||
/// Only the bookkeeping moves: the probe is no longer in flight, and the
|
||||
/// throttle counts from now so a retry waits its turn. What the cache
|
||||
/// knew about `cwd` stays exactly as it was — a repository it had found
|
||||
/// is still that repository, and a cwd it had never answered stays
|
||||
/// unanswered, which is what lets a tab keep the group it remembers
|
||||
/// instead of dropping into Ungrouped on a `git` that did not run.
|
||||
pub fn fail_probe(&mut self, host: HostId, cwd: &Path) -> bool {
|
||||
let cwd = key(host, cwd);
|
||||
let rerun = !self.probes.finish(&(host, cwd.to_path_buf()));
|
||||
let throttle = self.throttle_key(host, &cwd).to_path_buf();
|
||||
self.last_probe.insert(host, throttle, Instant::now());
|
||||
rerun
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -403,6 +419,34 @@ mod tests {
|
||||
assert!(cache.status_for(L, b).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_failed_probe_leaves_what_was_known() {
|
||||
let mut cache = GitStatusCache::default();
|
||||
let (repo, unseen) = (Path::new("/repo/a"), Path::new("/repo/b"));
|
||||
cache.finish_probe(L, repo, Some(snap("/repo", "main", Some((1, 0)))));
|
||||
|
||||
assert!(cache.begin_probe(L, repo));
|
||||
assert!(!cache.fail_probe(L, repo));
|
||||
assert_eq!(
|
||||
cache.known_repo_for(L, repo),
|
||||
Some(Some(PathBuf::from("/repo"))),
|
||||
"a git that did not run says nothing about the repository"
|
||||
);
|
||||
assert_eq!(cache.status_for(L, repo).unwrap().branch, "main");
|
||||
|
||||
assert!(cache.begin_probe(L, unseen));
|
||||
cache.fail_probe(L, unseen);
|
||||
assert_eq!(
|
||||
cache.known_repo_for(L, unseen),
|
||||
None,
|
||||
"still unknown, not \"not a repository\""
|
||||
);
|
||||
assert!(
|
||||
!cache.begin_probe_throttled(L, unseen, Duration::from_secs(60)),
|
||||
"the retry waits out the throttle"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_repo_for_is_three_valued() {
|
||||
let mut cache = GitStatusCache::default();
|
||||
|
||||
+33
-3
@@ -661,6 +661,13 @@ const INTEGRATION_NOTICE_TIMEOUT: std::time::Duration = std::time::Duration::fro
|
||||
|
||||
const OPPORTUNISTIC_GIT_GAP: std::time::Duration = std::time::Duration::from_millis(1500);
|
||||
|
||||
/// How long a pane whose repository is still unanswered waits before asking
|
||||
/// again. Only a probe that failed leaves it that way, and whatever broke it
|
||||
/// (a remote link mid-reconnect, a `git` that would not start) is gone in
|
||||
/// seconds, not milliseconds — nor worth a request every poll tick while it
|
||||
/// lasts.
|
||||
const GIT_RETRY_GAP: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
|
||||
/// How long a title the program set has to stand before the tab adopts it.
|
||||
///
|
||||
/// Long enough that a command which is over almost as soon as it started never
|
||||
@@ -705,6 +712,9 @@ const MAX_HISTORY_BYTES: u64 = 4 << 20;
|
||||
enum GitRefresh {
|
||||
Edge,
|
||||
Opportunistic,
|
||||
/// Nothing happened; the cache just has no answer for this pane's cwd
|
||||
/// yet, because the last probe failed. Throttled by [`GIT_RETRY_GAP`].
|
||||
Retry,
|
||||
}
|
||||
|
||||
fn known_pty_shim(fg: &str) -> Option<&'static str> {
|
||||
@@ -2157,6 +2167,20 @@ impl TerminalView {
|
||||
self.git_status_cwd.as_deref()
|
||||
}
|
||||
|
||||
/// This pane sits somewhere the repo cache cannot place yet — neither a
|
||||
/// repository nor "not one". A probe in flight looks the same, and the
|
||||
/// throttle turns the retry away; the case this exists for is a probe that
|
||||
/// failed, which leaves the answer open instead of recording a wrong one,
|
||||
/// and would otherwise wait for the cwd to change or a command to finish
|
||||
/// before anyone asked again.
|
||||
fn git_repo_unanswered(&self, cx: &App) -> bool {
|
||||
let Some(cwd) = self.git_status_cwd.as_deref() else {
|
||||
return false;
|
||||
};
|
||||
cx.try_global::<crate::terminal::git_status::GitStatusCache>()
|
||||
.is_none_or(|cache| cache.known_repo_for(self.host_id, cwd).is_none())
|
||||
}
|
||||
|
||||
/// Plant the cwd the git-status poll would have found. For tests that
|
||||
/// need a pane to look like it is sitting somewhere known — a real poll
|
||||
/// needs a live shell reporting a directory, which a quiet test pane has
|
||||
@@ -4011,6 +4035,8 @@ impl TerminalView {
|
||||
self.refresh_git_status(cwd_now, GitRefresh::Edge, cx);
|
||||
} else if tool_activity {
|
||||
self.refresh_git_status(cwd_now, GitRefresh::Opportunistic, cx);
|
||||
} else if self.git_repo_unanswered(cx) {
|
||||
self.refresh_git_status(cwd_now, GitRefresh::Retry, cx);
|
||||
}
|
||||
|
||||
self.follow_history_scope(cx);
|
||||
@@ -4219,6 +4245,7 @@ impl TerminalView {
|
||||
GitRefresh::Opportunistic => {
|
||||
cache.begin_probe_throttled(id, &cwd, OPPORTUNISTIC_GIT_GAP)
|
||||
}
|
||||
GitRefresh::Retry => cache.begin_probe_throttled(id, &cwd, GIT_RETRY_GAP),
|
||||
});
|
||||
if !claimed {
|
||||
return;
|
||||
@@ -4228,10 +4255,13 @@ impl TerminalView {
|
||||
crate::ui::host_ops::HostOps::run_detached(
|
||||
host,
|
||||
cx,
|
||||
move |h| crate::terminal::git_status::probe(h, &probe_cwd),
|
||||
move |h| crate::terminal::git_status::probe_repo(h, &probe_cwd),
|
||||
move |cx, result| {
|
||||
let rerun = cx.update_global::<GitStatusCache, _>(|cache, _| {
|
||||
cache.finish_probe(id, &cwd, result)
|
||||
use crate::terminal::git_status::RepoProbe;
|
||||
let rerun = cx.update_global::<GitStatusCache, _>(|cache, _| match result {
|
||||
RepoProbe::Repo(snap) => cache.finish_probe(id, &cwd, Some(snap)),
|
||||
RepoProbe::NotARepo => cache.finish_probe(id, &cwd, None),
|
||||
RepoProbe::Failed => cache.fail_probe(id, &cwd),
|
||||
});
|
||||
if rerun {
|
||||
let _ = pane.update(cx, |view, cx| {
|
||||
|
||||
Reference in New Issue
Block a user