use crate::client_journal::record_entry;
use crate::diagnostic_log::debug;
use crate::fs_kind::{FilesystemKind, filesystem_kind, is_subvolume_root};
use crate::journald_native::Priority;
use crate::transaction_git::GitContext;
use crate::worktree_add_plan::AddPlan;
use crate::worktree_identity::WorktreeIdentity;
use crate::worktree_clean::CleanRefusalKind;
use crate::worktree_snapshot_source::{ChangedPaths, Refusal, RefusalKind, changed_paths, plain};
use std::io::Write;
use std::path::Path;
const TAG: &str = "worktreeSlowRoute";
pub const LEAD: &str = "cli-git: this worktree was not made as a snapshot, so it took longer, because ";
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SlowRoute {
word: &'static str,
detail: String,
sentence: String,
}
impl SlowRoute {
pub fn word(&self) -> &'static str {
return self.word;
}
pub fn detail(&self) -> &str {
return self.detail.as_str();
}
pub fn line(&self) -> String {
return format!("{LEAD}{}", self.sentence);
}
pub fn tell(&self, plan: &AddPlan) {
let line: String = self.line();
debug(TAG, format!("{}: told ({}; {})", plan.destination.display(), self.word, self.detail).as_str());
if !plan.quiet {
let _written = std::io::stderr().write_all(format!("{line}\n").as_bytes());
}
let fields: [(&str, String); 3] = [
("CLI_GIT_WORKTREE", plan.destination.to_string_lossy().into_owned()),
("CLI_GIT_SLOW_CAUSE", self.word.to_string()),
("CLI_GIT_SLOW_DETAIL", self.detail.clone()),
];
record_entry(Priority::Warning, line.as_str(), &fields);
}
}
fn subject(identity: &WorktreeIdentity) -> &'static str {
return match identity {
WorktreeIdentity::MainWorktree { .. } => "the main checkout",
_ => "this worktree",
};
}
const THEN_QUICK: &str = "and the next add is a snapshot that takes seconds";
fn status_failed(subject: &str, root: &Path, reason: &str) -> SlowRoute {
return SlowRoute {
word: "status-failed",
detail: reason.to_string(),
sentence: format!(
"Git could not list the uncommitted changes of {subject} ({}). Run `git -C {} status` to read its error and fix what it names, or wait if another Git command is running there, {THEN_QUICK}.",
plain(reason),
plain(root.to_string_lossy().as_ref())
),
};
}
fn explain_unclean(context: &GitContext, root: &Path, subject: &str, kind: CleanRefusalKind, reason: &str) -> SlowRoute {
match kind {
CleanRefusalKind::OperationInProgress("index.lock") => {
return SlowRoute { word: "git-running", detail: reason.to_string(), sentence: format!("another Git command is running in {subject} (its index.lock exists). Add again when it has finished, {THEN_QUICK}.") };
}
CleanRefusalKind::OperationInProgress(name) => {
return SlowRoute { word: "operation-unfinished", detail: reason.to_string(), sentence: format!("{subject} is in the middle of an operation ({name} exists in its Git directory). Finish or abort it, {THEN_QUICK}.") };
}
CleanRefusalKind::NotPlain => {
return SlowRoute { word: "not-restorable", detail: reason.to_string(), sentence: format!("{subject} has an index entry that no reset can undo, a skip-worktree, assume-unchanged, intent-to-add, unmerged or submodule entry ({}).", plain(reason)) };
}
CleanRefusalKind::Unreadable => {
return SlowRoute { word: "unproven", detail: reason.to_string(), sentence: format!("{subject} could not be read to prove it unchanged ({}).", plain(reason)) };
}
CleanRefusalKind::StagedChange | CleanRefusalKind::FileChanged | CleanRefusalKind::StatStale | CleanRefusalKind::Racy | CleanRefusalKind::Untracked => {}
}
let listed: ChangedPaths = match changed_paths(context, root) {
Ok(found) => found,
Err(failure) => return status_failed(subject, root, failure.as_str()),
};
if let ChangedPaths { count, first: Some(first), .. } = listed {
let paths: &str = if count == 1 { "path" } else { "paths" };
return SlowRoute {
word: "uncommitted-changes",
detail: reason.to_string(),
sentence: format!("{subject} has uncommitted changes in {count} {paths} (for example {first}). Commit or stash them, {THEN_QUICK}."),
};
}
if matches!(kind, CleanRefusalKind::Racy | CleanRefusalKind::StatStale) {
return SlowRoute {
word: "stale-index",
detail: reason.to_string(),
sentence: format!("Git's record of file times in {subject} is out of date. Run `git status` there once, {THEN_QUICK}."),
};
}
return SlowRoute { word: "unproven", detail: reason.to_string(), sentence: format!("{subject} could not be proven unchanged ({}).", plain(reason)) };
}
pub fn explain_refusal(context: &GitContext, identity: &WorktreeIdentity, refusal: &Refusal) -> Option<SlowRoute> {
let root: &Path = match identity {
WorktreeIdentity::MainWorktree { worktree_root, .. } | WorktreeIdentity::LinkedWorktree { worktree_root, .. } => worktree_root.as_path(),
WorktreeIdentity::BareRepository { .. } | WorktreeIdentity::OutsideWorktree => return None,
};
return Some(match refusal.kind {
RefusalKind::Unclean(kind) => explain_unclean(context, root, subject(identity), kind, refusal.reason.as_str()),
RefusalKind::Unsupported => explain_unsupported(refusal.reason.as_str()),
RefusalKind::StatusFailed => status_failed(subject(identity), root, refusal.reason.as_str()),
});
}
pub fn explain_unsupported(reason: &str) -> SlowRoute {
return SlowRoute { word: "unsupported", detail: reason.to_string(), sentence: format!("{}.", plain(reason)) };
}
pub fn explain_failure(reason: &str) -> SlowRoute {
return SlowRoute { word: "snapshot-failed", detail: reason.to_string(), sentence: format!("the snapshot could not be finished ({}), so Git's own checkout made the worktree.", plain(reason)) };
}
pub fn explain_cheap_refusal(identity: &WorktreeIdentity, reason: &str) -> Option<SlowRoute> {
let root: &Path = match identity {
WorktreeIdentity::MainWorktree { worktree_root, .. } | WorktreeIdentity::LinkedWorktree { worktree_root, .. } => worktree_root.as_path(),
WorktreeIdentity::BareRepository { .. } | WorktreeIdentity::OutsideWorktree => return None,
};
if filesystem_kind(root) != Ok(FilesystemKind::Btrfs) {
return None;
}
let subject: &str = subject(identity);
let command: &str = if matches!(identity, WorktreeIdentity::MainWorktree { .. }) { "migrate-to-subvolume" } else { "convert-worktree-to-subvolume" };
if !is_subvolume_root(root).unwrap_or(false) {
return Some(SlowRoute {
word: "not-subvolume",
detail: reason.to_string(),
sentence: format!("{subject} is not a btrfs subvolume. Run `git cli-git {command}` in it, {THEN_QUICK}."),
});
}
return Some(SlowRoute {
word: "other-filesystem",
detail: reason.to_string(),
sentence: format!("the new worktree is not on the same btrfs filesystem as {subject}. Put it on the same one, {THEN_QUICK}."),
});
}
#[cfg(test)]
#[path = "worktree_slow_route_tests.rs"]
mod tests;