1use crate::client_journal::record_entry;
19use crate::diagnostic_log::debug;
20use crate::fs_kind::{FilesystemKind, filesystem_kind, is_subvolume_root};
21use crate::journald_native::Priority;
22use crate::transaction_git::GitContext;
23use crate::worktree_add_plan::AddPlan;
24use crate::worktree_identity::WorktreeIdentity;
25use crate::worktree_clean::CleanRefusalKind;
26use crate::worktree_snapshot_source::{ChangedPaths, Refusal, RefusalKind, changed_paths, plain};
27use std::io::Write;
28use std::path::Path;
29
30const TAG: &str = "worktreeSlowRoute";
32
33pub const LEAD: &str = "cli-git: this worktree was not made as a snapshot, so it took longer, because ";
35
36#[derive(Clone, Debug, Eq, PartialEq)]
46pub struct SlowRoute {
47 word: &'static str,
49 detail: String,
51 sentence: String,
53}
54
55impl SlowRoute {
56 pub fn word(&self) -> &'static str {
58 return self.word;
59 }
60
61 pub fn detail(&self) -> &str {
63 return self.detail.as_str();
64 }
65
66 pub fn line(&self) -> String {
68 return format!("{LEAD}{}", self.sentence);
69 }
70
71 pub fn tell(&self, plan: &AddPlan) {
82 let line: String = self.line();
83 debug(TAG, format!("{}: told ({}; {})", plan.destination.display(), self.word, self.detail).as_str());
84 if !plan.quiet {
85 let _written = std::io::stderr().write_all(format!("{line}\n").as_bytes());
87 }
88 let fields: [(&str, String); 3] = [
89 ("CLI_GIT_WORKTREE", plan.destination.to_string_lossy().into_owned()),
90 ("CLI_GIT_SLOW_CAUSE", self.word.to_string()),
91 ("CLI_GIT_SLOW_DETAIL", self.detail.clone()),
92 ];
93 record_entry(Priority::Warning, line.as_str(), &fields);
94 }
95}
96
97fn subject(identity: &WorktreeIdentity) -> &'static str {
107 return match identity {
108 WorktreeIdentity::MainWorktree { .. } => "the main checkout",
109 _ => "this worktree",
110 };
111}
112
113const THEN_QUICK: &str = "and the next add is a snapshot that takes seconds";
115
116fn status_failed(subject: &str, root: &Path, reason: &str) -> SlowRoute {
127 return SlowRoute {
128 word: "status-failed",
129 detail: reason.to_string(),
130 sentence: format!(
131 "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}.",
132 plain(reason),
133 plain(root.to_string_lossy().as_ref())
134 ),
135 };
136}
137
138fn explain_unclean(context: &GitContext, root: &Path, subject: &str, kind: CleanRefusalKind, reason: &str) -> SlowRoute {
149 match kind {
150 CleanRefusalKind::OperationInProgress("index.lock") => {
151 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}.") };
152 }
153 CleanRefusalKind::OperationInProgress(name) => {
154 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}.") };
155 }
156 CleanRefusalKind::NotPlain => {
157 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)) };
158 }
159 CleanRefusalKind::Unreadable => {
160 return SlowRoute { word: "unproven", detail: reason.to_string(), sentence: format!("{subject} could not be read to prove it unchanged ({}).", plain(reason)) };
161 }
162 CleanRefusalKind::StagedChange | CleanRefusalKind::FileChanged | CleanRefusalKind::StatStale | CleanRefusalKind::Racy | CleanRefusalKind::Untracked => {}
163 }
164 let listed: ChangedPaths = match changed_paths(context, root) {
165 Ok(found) => found,
166 Err(failure) => return status_failed(subject, root, failure.as_str()),
167 };
168 if let ChangedPaths { count, first: Some(first), .. } = listed {
169 let paths: &str = if count == 1 { "path" } else { "paths" };
170 return SlowRoute {
171 word: "uncommitted-changes",
172 detail: reason.to_string(),
173 sentence: format!("{subject} has uncommitted changes in {count} {paths} (for example {first}). Commit or stash them, {THEN_QUICK}."),
174 };
175 }
176 if matches!(kind, CleanRefusalKind::Racy | CleanRefusalKind::StatStale) {
177 return SlowRoute {
178 word: "stale-index",
179 detail: reason.to_string(),
180 sentence: format!("Git's record of file times in {subject} is out of date. Run `git status` there once, {THEN_QUICK}."),
181 };
182 }
183 return SlowRoute { word: "unproven", detail: reason.to_string(), sentence: format!("{subject} could not be proven unchanged ({}).", plain(reason)) };
184}
185
186pub fn explain_refusal(context: &GitContext, identity: &WorktreeIdentity, refusal: &Refusal) -> Option<SlowRoute> {
197 let root: &Path = match identity {
198 WorktreeIdentity::MainWorktree { worktree_root, .. } | WorktreeIdentity::LinkedWorktree { worktree_root, .. } => worktree_root.as_path(),
199 WorktreeIdentity::BareRepository { .. } | WorktreeIdentity::OutsideWorktree => return None,
200 };
201 return Some(match refusal.kind {
202 RefusalKind::Unclean(kind) => explain_unclean(context, root, subject(identity), kind, refusal.reason.as_str()),
203 RefusalKind::Unsupported => explain_unsupported(refusal.reason.as_str()),
204 RefusalKind::StatusFailed => status_failed(subject(identity), root, refusal.reason.as_str()),
205 });
206}
207
208pub fn explain_unsupported(reason: &str) -> SlowRoute {
219 return SlowRoute { word: "unsupported", detail: reason.to_string(), sentence: format!("{}.", plain(reason)) };
220}
221
222pub fn explain_failure(reason: &str) -> SlowRoute {
232 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)) };
233}
234
235pub fn explain_cheap_refusal(identity: &WorktreeIdentity, reason: &str) -> Option<SlowRoute> {
247 let root: &Path = match identity {
248 WorktreeIdentity::MainWorktree { worktree_root, .. } | WorktreeIdentity::LinkedWorktree { worktree_root, .. } => worktree_root.as_path(),
249 WorktreeIdentity::BareRepository { .. } | WorktreeIdentity::OutsideWorktree => return None,
250 };
251 if filesystem_kind(root) != Ok(FilesystemKind::Btrfs) {
252 return None;
253 }
254 let subject: &str = subject(identity);
255 let command: &str = if matches!(identity, WorktreeIdentity::MainWorktree { .. }) { "migrate-to-subvolume" } else { "convert-worktree-to-subvolume" };
257 if !is_subvolume_root(root).unwrap_or(false) {
258 return Some(SlowRoute {
259 word: "not-subvolume",
260 detail: reason.to_string(),
261 sentence: format!("{subject} is not a btrfs subvolume. Run `git cli-git {command}` in it, {THEN_QUICK}."),
262 });
263 }
264 return Some(SlowRoute {
265 word: "other-filesystem",
266 detail: reason.to_string(),
267 sentence: format!("the new worktree is not on the same btrfs filesystem as {subject}. Put it on the same one, {THEN_QUICK}."),
268 });
269}
270
271#[cfg(test)]
273#[path = "worktree_slow_route_tests.rs"]
274mod tests;