1use crate::inner_git_words::record_words;
20use crate::transaction_git::{GitContext, GitRequest, run_git};
21use crate::worktree_copy::GitRunner;
22use crate::worktree_ladder_target::{Target, checkout_environment, run_step};
23use crate::worktree_snapshot_source::{ChangedPaths, SnapshotSource};
24use std::ffi::OsString;
25use std::path::Path;
26
27const TAG: &str = "worktreeDirtyRestore";
29
30pub fn left_out_note(changed: &ChangedPaths) -> String {
40 let paths: &str = if changed.count == 1 { "path" } else { "paths" };
41 return format!("cli-git: the uncommitted changes of the main checkout ({} {paths}) were left out of the new worktree, which holds what Git's own checkout holds.\n", changed.count);
42}
43
44pub fn record_left_out(destination: &Path, changed: &ChangedPaths) {
55 let note: String = left_out_note(changed);
56 let fields: [(&str, String); 3] = [
57 ("CLI_GIT_WORKTREE", destination.to_string_lossy().into_owned()),
58 ("CLI_GIT_DIRTY_PATHS", changed.count.to_string()),
59 ("CLI_GIT_DIRTY_FIRST", changed.first.clone().unwrap_or_default()),
60 ];
61 crate::client_journal::record_entry(crate::journald_native::Priority::Info, note.trim_end(), &fields);
62}
63
64fn ask(context: &GitContext, target: &Target, work_tree: &Path, arguments: &[&str]) -> Result<(Option<i32>, Vec<u8>), String> {
74 let request: GitRequest = GitRequest {
75 cwd: work_tree.to_path_buf(),
76 arguments: arguments.iter().map(|word: &&str| { return OsString::from(*word); }).collect(),
77 without_prefix: true,
78 environment: checkout_environment(target.admin.as_path(), work_tree),
79 ..GitRequest::default()
80 };
81 let output = run_git(context, &request).map_err(|error| { return format!("`git {}` could not start ({error})", arguments.join(" ")); })?;
82 return Ok((output.code, output.stdout));
83}
84
85pub fn prove_restored(context: &GitContext, target: &Target, work_tree: &Path) -> Result<(), String> {
97 let cached: Option<i32> = ask(context, target, work_tree, &["diff-index", "--cached", "--quiet", target.commit.as_str()])?.0;
98 if cached != Some(0) {
99 return Err(String::from("the restored index does not equal the tree of the new HEAD"));
100 }
101 let (listed, untracked) = ask(context, target, work_tree, &["ls-files", "--others", "--exclude-standard", "--directory", "--no-empty-directory", "-z"])?;
102 if listed != Some(0) {
103 return Err(String::from("Git could not list the untracked files of the restored worktree"));
104 }
105 if let Some(name) = untracked.split(|byte: &u8| { return *byte == 0; }).find(|name: &&[u8]| { return !name.is_empty(); }) {
106 return Err(format!("{} is still untracked in the restored worktree", crate::worktree_snapshot_source::plain(String::from_utf8_lossy(name).as_ref())));
107 }
108 let files: Option<i32> = ask(context, target, work_tree, &["diff-files", "--quiet"])?.0;
109 if files != Some(0) {
110 return Err(String::from("a tracked file of the restored worktree differs from its index entry"));
111 }
112 return Ok(());
113}
114
115pub fn restore_dirty(source: &SnapshotSource, target: &Target, stage: &Path, run_git_step: &mut GitRunner, context: &GitContext) -> Result<(), String> {
126 if stage == source.root.as_path() || target.admin == source.git_dir || target.admin == source.common_dir {
127 return Err(String::from("the restore would run in the source's own directory"));
128 }
129 let began: std::time::Instant = std::time::Instant::now();
130 let environment = checkout_environment(target.admin.as_path(), stage);
131 let in_stage = |words: &[&str]| -> Vec<OsString> {
132 let mut arguments: Vec<OsString> = vec![OsString::from("-C"), stage.as_os_str().to_os_string()];
133 arguments.extend(words.iter().map(|word: &&str| { return OsString::from(*word); }));
134 return arguments;
135 };
136 let reset = run_step(run_git_step, in_stage(&["read-tree", "--reset", "-u", target.commit.as_str()]).as_slice(), environment.as_slice())?;
137 if !reset.succeeded() {
138 return Err(format!("`git read-tree --reset -u` ended with {:?}", reset.code));
139 }
140 let reset_ms: u128 = began.elapsed().as_millis();
141 let clean_request: GitRequest = GitRequest {
145 cwd: stage.to_path_buf(),
146 arguments: ["clean", "--force", "-d"].iter().map(OsString::from).collect(),
147 without_prefix: true,
148 environment: environment.clone(),
149 ..GitRequest::default()
150 };
151 let clean = run_git(context, &clean_request).map_err(|error| { return format!("`git clean --force -d` could not start ({error})"); })?;
152 if !clean.succeeded() {
153 return Err(format!("`git clean --force -d` ended with {:?}", clean.code));
154 }
155 record_words(&["clean", "--force", "-d"], &clean);
156 let clean_ms: u128 = began.elapsed().as_millis();
157 prove_restored(context, target, stage)?;
158 crate::diagnostic_log::debug(
159 TAG,
160 format!("restored {} to {} and proved it: reset done at {reset_ms} ms, clean at {clean_ms} ms, proof at {} ms", stage.display(), target.commit, began.elapsed().as_millis()).as_str(),
161 );
162 return Ok(());
163}
164
165#[cfg(test)]
167#[path = "worktree_dirty_restore_tests.rs"]
168mod tests;