use crate::inner_git_words::record_words;
use crate::transaction_git::{GitContext, GitRequest, run_git};
use crate::worktree_copy::GitRunner;
use crate::worktree_ladder_target::{Target, checkout_environment, run_step};
use crate::worktree_snapshot_source::{ChangedPaths, SnapshotSource};
use std::ffi::OsString;
use std::path::Path;
const TAG: &str = "worktreeDirtyRestore";
pub fn left_out_note(changed: &ChangedPaths) -> String {
let paths: &str = if changed.count == 1 { "path" } else { "paths" };
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);
}
pub fn record_left_out(destination: &Path, changed: &ChangedPaths) {
let note: String = left_out_note(changed);
let fields: [(&str, String); 3] = [
("CLI_GIT_WORKTREE", destination.to_string_lossy().into_owned()),
("CLI_GIT_DIRTY_PATHS", changed.count.to_string()),
("CLI_GIT_DIRTY_FIRST", changed.first.clone().unwrap_or_default()),
];
crate::client_journal::record_entry(crate::journald_native::Priority::Info, note.trim_end(), &fields);
}
fn ask(context: &GitContext, target: &Target, work_tree: &Path, arguments: &[&str]) -> Result<(Option<i32>, Vec<u8>), String> {
let request: GitRequest = GitRequest {
cwd: work_tree.to_path_buf(),
arguments: arguments.iter().map(|word: &&str| { return OsString::from(*word); }).collect(),
without_prefix: true,
environment: checkout_environment(target.admin.as_path(), work_tree),
..GitRequest::default()
};
let output = run_git(context, &request).map_err(|error| { return format!("`git {}` could not start ({error})", arguments.join(" ")); })?;
return Ok((output.code, output.stdout));
}
pub fn prove_restored(context: &GitContext, target: &Target, work_tree: &Path) -> Result<(), String> {
let cached: Option<i32> = ask(context, target, work_tree, &["diff-index", "--cached", "--quiet", target.commit.as_str()])?.0;
if cached != Some(0) {
return Err(String::from("the restored index does not equal the tree of the new HEAD"));
}
let (listed, untracked) = ask(context, target, work_tree, &["ls-files", "--others", "--exclude-standard", "--directory", "--no-empty-directory", "-z"])?;
if listed != Some(0) {
return Err(String::from("Git could not list the untracked files of the restored worktree"));
}
if let Some(name) = untracked.split(|byte: &u8| { return *byte == 0; }).find(|name: &&[u8]| { return !name.is_empty(); }) {
return Err(format!("{} is still untracked in the restored worktree", crate::worktree_snapshot_source::plain(String::from_utf8_lossy(name).as_ref())));
}
let files: Option<i32> = ask(context, target, work_tree, &["diff-files", "--quiet"])?.0;
if files != Some(0) {
return Err(String::from("a tracked file of the restored worktree differs from its index entry"));
}
return Ok(());
}
pub fn restore_dirty(source: &SnapshotSource, target: &Target, stage: &Path, run_git_step: &mut GitRunner, context: &GitContext) -> Result<(), String> {
if stage == source.root.as_path() || target.admin == source.git_dir || target.admin == source.common_dir {
return Err(String::from("the restore would run in the source's own directory"));
}
let began: std::time::Instant = std::time::Instant::now();
let environment = checkout_environment(target.admin.as_path(), stage);
let in_stage = |words: &[&str]| -> Vec<OsString> {
let mut arguments: Vec<OsString> = vec![OsString::from("-C"), stage.as_os_str().to_os_string()];
arguments.extend(words.iter().map(|word: &&str| { return OsString::from(*word); }));
return arguments;
};
let reset = run_step(run_git_step, in_stage(&["read-tree", "--reset", "-u", target.commit.as_str()]).as_slice(), environment.as_slice())?;
if !reset.succeeded() {
return Err(format!("`git read-tree --reset -u` ended with {:?}", reset.code));
}
let reset_ms: u128 = began.elapsed().as_millis();
let clean_request: GitRequest = GitRequest {
cwd: stage.to_path_buf(),
arguments: ["clean", "--force", "-d"].iter().map(OsString::from).collect(),
without_prefix: true,
environment: environment.clone(),
..GitRequest::default()
};
let clean = run_git(context, &clean_request).map_err(|error| { return format!("`git clean --force -d` could not start ({error})"); })?;
if !clean.succeeded() {
return Err(format!("`git clean --force -d` ended with {:?}", clean.code));
}
record_words(&["clean", "--force", "-d"], &clean);
let clean_ms: u128 = began.elapsed().as_millis();
prove_restored(context, target, stage)?;
crate::diagnostic_log::debug(
TAG,
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(),
);
return Ok(());
}
#[cfg(test)]
#[path = "worktree_dirty_restore_tests.rs"]
mod tests;