use crate::diagnostic_log::debug;
use crate::diagnostics::{EngineFailureCode, render_engine_failure_with_hint};
use crate::merge_isolation_recovery::{RecoveryPlace, merge_intents_to_recover, recover_interrupted_merges};
use crate::random_id::is_canonical_uuid;
use crate::recovery_error::RecoveryError;
use crate::recovery_files::recovery_path_exists;
use crate::transaction_git::GitContext;
use crate::worktree_identity::WorktreeIdentity;
use std::io::Write;
use std::path::{Path, PathBuf};
pub const REGISTRY_DIRECTORY_NAME: &str = "cli-git-transactions";
pub const LEGACY_JOURNAL_DIRECTORY_NAME: &str = "cli-git-transaction";
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Gate {
Clear,
Stop(String),
}
pub fn legacy_notice(directory: &Path) -> String {
return format!(
"cli-git: {} holds a commit journal in a format this executable does not recover. \
Nothing was changed. Run any git command once with the previous cli-git executable to \
recover it, then run this command again.\n",
directory.display()
);
}
pub fn shell_word(path: &Path) -> String {
if let Some(text) = path.to_str() {
return format!("'{}'", text.replace('\'', "'\\''"));
}
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt;
let mut word: String = String::from("$'");
for byte in path.as_os_str().as_bytes() {
match *byte {
b'\\' => word.push_str("\\\\"),
b'\'' => word.push_str("\\'"),
0x20..=0x7e => word.push(char::from(*byte)),
other => word.push_str(format!("\\{other:03o}").as_str()),
}
}
word.push('\'');
return word;
}
#[cfg(not(unix))]
{
return format!("'{}'", path.to_string_lossy().replace('\'', "'\\''"));
}
}
pub fn registry_note(registry: &Path) -> Option<String> {
let entries: std::fs::ReadDir = std::fs::read_dir(registry).ok()?;
let left: bool = entries.flatten().any(|entry| { return is_canonical_uuid(entry.file_name().to_string_lossy().as_ref()); });
if !left {
return None;
}
return Some(format!(
"cli-git: {} holds commit records an earlier release left unfinished. This release no longer uses that directory and \
does not read it. Run `git log -1` and `git status` to see whether that commit was made. Once you have, remove the \
directory with: rm --recursive --force -- {}\n",
registry.display(),
shell_word(registry)
));
}
fn worktree_directories(identity: &WorktreeIdentity) -> Option<(&Path, &Path)> {
match identity {
WorktreeIdentity::OutsideWorktree | WorktreeIdentity::BareRepository { .. } => return None,
WorktreeIdentity::MainWorktree { git_dir, common_dir, .. } | WorktreeIdentity::LinkedWorktree { git_dir, common_dir, .. } => {
return Some((git_dir.as_path(), common_dir.as_path()));
}
}
}
fn settle_merges(context: &GitContext, git_dir: &Path, common_dir: &Path) -> Result<(), RecoveryError> {
if !merge_intents_to_recover(git_dir)? {
return Ok(());
}
let place: RecoveryPlace = RecoveryPlace {
git: context.clone(),
cwd: PathBuf::from("."),
common_dir: common_dir.to_path_buf(),
};
let outcomes = recover_interrupted_merges(&place, git_dir)?;
if !outcomes.is_empty() {
debug("startupGate", format!("isolated merge recovery outcomes: {outcomes:?}").as_str());
}
return Ok(());
}
pub fn recover_for_command(context: &GitContext, identity: &WorktreeIdentity) -> Gate {
let Some((git_dir, common_dir)) = worktree_directories(identity) else {
return Gate::Clear;
};
let legacy: PathBuf = git_dir.join(LEGACY_JOURNAL_DIRECTORY_NAME);
match recovery_path_exists(legacy.as_path()) {
Ok(true) => return Gate::Stop(legacy_notice(legacy.as_path())),
Ok(false) => {}
Err(error) => return stopped(&error),
}
if let Err(error) = settle_merges(context, git_dir, common_dir) {
return stopped(&error);
}
if let Some(note) = registry_note(git_dir.join(REGISTRY_DIRECTORY_NAME).as_path()) {
let _noted = std::io::stderr().write_all(note.as_bytes());
}
return Gate::Clear;
}
fn stopped(error: &RecoveryError) -> Gate {
return Gate::Stop(render_engine_failure_with_hint(0, EngineFailureCode::ContentUnavailable, error.0.as_str(), None));
}
#[cfg(test)]
#[path = "startup_gate_tests.rs"]
mod tests;