Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions e2e/cli/test_doctor_dotfiles_history_disabled
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# doctor still shows where dotfiles live when history.enabled = false

git init -q --bare -b main "$PWD/origin.git"
echo original >"$HOME/.native"
assert_succeed "mise bootstrap dotfiles track ~/.native --no-autosave"
assert_succeed "mise bootstrap dotfiles origin set file://$PWD/origin.git --sync manual --yes"

assert_contains "mise doctor 2>&1 || true" "1 tracked entry, watcher"

export MISE_HISTORY_ENABLED=0
assert_contains "mise doctor 2>&1 || true" "1 tracked entry, history disabled (history.enabled = false): nothing is saved or synced"
assert_contains "mise doctor 2>&1 || true" "repo: "
assert_contains "mise doctor 2>&1 || true" "origin.git (branch main, sync manual)"
assert "mise doctor --json 2>/dev/null | jq -r '.dotfiles.history_enabled, .dotfiles.tracked, .dotfiles.origin.url'" "false
1
file://$PWD/origin.git"

# nothing tracked, no history, no setup repository: no section
export MISE_STATE_DIR="$PWD/empty-state"
export MISE_CONFIG_DIR="$PWD/empty-config"
assert_not_contains "mise doctor 2>&1 || true" "dotfiles:"

# a declaration that cannot be read is reported, not taken for none
mkdir -p "$MISE_CONFIG_DIR"
cat >"$MISE_CONFIG_DIR/config.toml" <<'TOML'
[history.encryption]
recipients = []
[dotfiles]
"~/.private-value" = { mode = "track", encrypt = true }
TOML
assert_contains "mise doctor 2>&1 || true" "tracked entries could not be read ([history.encryption].recipients must not be empty"
97 changes: 81 additions & 16 deletions src/cli/doctor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,16 @@ enum SystemDefaultsDiagnosis {
/// outcome of the `[bootstrap.user].login_shell` doctor check
#[derive(serde::Serialize)]
struct DotfilesDiagnosis {
/// `history.enabled`: when false, nothing is saved or synced and only
/// `repo`, `origin`, and `tracked` are reported.
history_enabled: bool,
/// The local bare repository that holds the history.
repo: PathBuf,
/// The connected setup repository, if any.
origin: Option<DotfilesOriginDiagnosis>,
tracked: usize,
/// Why the tracked entries could not be read, when history is disabled.
tracking_error: Option<String>,
watcher: String,
stale: bool,
health_age_secs: Option<u64>,
Expand All @@ -111,6 +116,59 @@ struct DotfilesOriginDiagnosis {
sync: String,
}

/// The connected setup repository, if any.
fn dotfiles_origin() -> Option<DotfilesOriginDiagnosis> {
crate::system::history::sync::run::origin()
.ok()
.map(|origin| DotfilesOriginDiagnosis {
url: origin.url,
branch: origin.branch,
sync: crate::system::history::sync::SyncMode::current()
.map(|mode| mode.as_str().to_string())
.unwrap_or_else(|_| "unknown".to_string()),
})
}

/// Where the dotfiles history would live with `history.enabled = false`:
/// the store is closed, so nothing about capture or sync health applies.
/// Reads the declared tracking only, never opening or creating the store.
/// Returns `None` when nothing is tracked, no history exists, and no setup
/// repository is connected.
async fn check_disabled_dotfiles() -> Option<DotfilesDiagnosis> {
let config = Config::get().await.ok()?;
// a declaration that cannot be read is reported, not taken for none
let (tracked, tracking_error) =
match crate::system::history::tracked::TrackedSet::from_config(&config) {
Ok(set) => (set.entries.len(), None),
Err(err) => (0, Some(format!("{err:#}"))),
};
let repo =
crate::system::history::store::repo_dir_in(&crate::system::history::store::state_dir());
let origin = dotfiles_origin();
if tracked == 0 && tracking_error.is_none() && !repo.is_dir() && origin.is_none() {
return None;
}
Some(DotfilesDiagnosis {
history_enabled: false,
repo,
origin,
tracked,
tracking_error,
watcher: "disabled".to_string(),
stale: false,
health_age_secs: None,
unavailable: None,
last_error: None,
degraded: vec![],
throttled: vec![],
sync_conflicts: vec![],
sync_error: None,
sync_failing_for_secs: None,
sync_failures: 0,
notifications: "disabled".to_string(),
})
}

/// How long syncs have been failing: since the current run of failures
/// began, or, for a record from before that was kept, since the last
/// success. `None` when nothing is known.
Expand Down Expand Up @@ -778,7 +836,7 @@ impl Doctor {
use crate::system::history::health;
use crate::system::history::tracked::TrackedSet;
if !crate::config::Settings::get().history.enabled {
return None;
return check_disabled_dotfiles().await;
}
let tracked = TrackedSet::effective().await.ok()?;
let watcher = crate::cli::dotfiles::capture_health::watcher().await.ok()?;
Expand All @@ -799,19 +857,12 @@ impl Doctor {
.map(|d| d.as_secs())
.unwrap_or(600);
let stale = running && age.is_some_and(|age| reconcile > 0 && age > reconcile * 2);
let origin = crate::system::history::sync::run::origin()
.ok()
.map(|origin| DotfilesOriginDiagnosis {
url: origin.url,
branch: origin.branch,
sync: crate::system::history::sync::SyncMode::current()
.map(|mode| mode.as_str().to_string())
.unwrap_or_else(|_| "unknown".to_string()),
});
let mut diagnosis = DotfilesDiagnosis {
history_enabled: true,
repo: crate::system::history::store::repo_dir_in(&state_dir),
origin,
origin: dotfiles_origin(),
tracked: tracks,
tracking_error: None,
watcher: watcher.as_str().to_string(),
stale,
health_age_secs: age,
Expand Down Expand Up @@ -950,19 +1001,33 @@ impl Doctor {
let Some(diagnosis) = self.check_dotfiles().await else {
return Ok(());
};
let mut lines = vec![format!(
"{} tracked entr{}, watcher {}",
let entries = format!(
"{} tracked entr{}",
diagnosis.tracked,
if diagnosis.tracked == 1 { "y" } else { "ies" },
diagnosis.watcher
)];
if diagnosis.tracked == 1 { "y" } else { "ies" }
);
let mut lines = vec![if diagnosis.history_enabled {
format!("{entries}, watcher {}", diagnosis.watcher)
} else if let Some(error) = &diagnosis.tracking_error {
format!(
"tracked entries could not be read ({error}); history disabled (history.enabled = false): nothing is saved or synced"
)
} else {
format!(
"{entries}, history disabled (history.enabled = false): nothing is saved or synced"
)
}];
lines.push(format!("repo: {}", display_path(&diagnosis.repo)));
if let Some(origin) = &diagnosis.origin {
lines.push(format!(
"origin: {} (branch {}, sync {})",
origin.url, origin.branch, origin.sync
));
}
if !diagnosis.history_enabled {
info::section("dotfiles", lines.join("\n"))?;
return Ok(());
}
if diagnosis.stale {
lines.push(format!(
"health information is stale (last update {} ago); the watcher may be stuck",
Expand Down
Loading