use crate::service_head::still_checked_out;
use crate::service_instance::instance_of;
use crate::service_journal::{INDEXED_EXTENSION, Indexed, Intent, OPEN_EXTENSION, Recovery, Started, indexed_state, list_intents, mark_done, mark_indexed, read_start, recovery_for, remove_start};
use crate::service_land_follow::{IndexPaths, build_index, remove_conclusion_state};
use crate::service_land_start::witness_of;
use crate::service_index_lock::{IndexLock, remove_stale, try_acquire};
use crate::autostash_carry::carry_stages;
use crate::service_land::LandContext;
use crate::service_protocol::FailureCode;
use crate::transaction_git::{GitContext, GitRequest, run_git};
use std::path::Path;
pub(crate) fn intent_paths(intent: &Intent) -> IndexPaths<'_> {
return if intent.derived { IndexPaths::Derived { old: intent.old.as_str() } } else { IndexPaths::Listed(intent.paths.as_slice()) };
}
pub(crate) fn cleanup_done(git: &GitContext, repository: &Path, intent: &Intent) -> bool {
return intent.cleanup.as_ref().is_none_or(|cleanup| { return remove_conclusion_state(git, repository, cleanup); });
}
pub fn follow_up(git: &GitContext, repository: &Path, intent: &Intent, lock: IndexLock, journal: &Path) -> bool {
let cleaned: bool = cleanup_done(git, repository, intent);
if crate::service_fault::fails("build-index") {
return false;
}
let Ok(built) = build_index(git, repository, intent.new.as_str(), intent_paths(intent), &lock, intent.seq) else {
return false;
};
if let Some(cleanup) = intent.cleanup.as_ref() {
built.set_flushed(false);
if !carry_stages(git, repository, cleanup, built.path()) {
return false;
}
}
let recorded = || -> std::io::Result<()> { return mark_indexed(journal, intent.seq, intent.new.as_str()); };
return lock.release_flushed_with_record(built.path(), built.is_flushed(), &recorded).is_ok() && cleaned;
}
pub fn finish_after_move(git: &GitContext, intent: &Intent, lock: IndexLock, journal: &Path, follows_move: Result<bool, FailureCode>) -> bool {
let repository: &Path = intent.repository.as_path();
let own_index: bool = intent.index == intent.git_dir.join("index");
return match follows_move {
Ok(false) if own_index => {
drop(lock);
cleanup_done(git, repository, intent)
}
Ok(_) => follow_up(git, repository, intent, lock, journal),
Err(code) => {
drop(lock);
crate::service_error!("batch {} stays open: the head of the worktree cannot be read ({code:?}), so the index is left as it is", intent.seq);
false
}
};
}
#[derive(Debug, Eq, PartialEq)]
enum Gate {
Replay,
Wait,
Leave(&'static str),
}
fn gate_of(journal: &Path, intent: &Intent) -> Gate {
let recorded = match read_start(journal, intent.seq) {
Started::Absent => return Gate::Leave("no record says which index file the landing saw"),
Started::Unreadable => return Gate::Wait,
Started::Recorded(witness) => witness,
};
return match witness_of(intent.index.as_path()) {
Ok(now) if now == recorded => Gate::Replay,
Ok(_) => Gate::Leave("the index file is not the one the landing saw, so another program may have staged into it"),
Err(_) => Gate::Wait,
};
}
#[derive(Debug, Eq, PartialEq)]
pub(crate) enum Settled {
Done,
Open,
}
pub(crate) fn settle(git: &GitContext, intent: &Intent, journal: &Path) -> Settled {
let repository: &Path = intent.repository.as_path();
let own_index: bool = intent.index == intent.git_dir.join("index");
let head = || { return still_checked_out(git, repository, intent.git_dir.as_path(), intent.common_dir.as_path(), intent.reference.as_str()); };
let done = |finished: bool| -> Settled { return if finished { Settled::Done } else { Settled::Open }; };
if own_index {
match head() {
Ok(false) => return done(cleanup_done(git, repository, intent)),
Ok(true) => {}
Err(code) => {
crate::service_error!("batch {} stays open: the head of the worktree cannot be read ({code:?}), so the index is left as it is", intent.seq);
return Settled::Open;
}
}
}
match indexed_state(journal, intent.seq, intent.new.as_str()) {
Indexed::Installed => return done(cleanup_done(git, repository, intent)),
Indexed::Unreadable => {
crate::service_error!("batch {} stays open: its completion record cannot be read, so the index is left as it is", intent.seq);
return Settled::Open;
}
Indexed::Absent => {}
}
crate::service_fault::reach("restart-unlocked");
let Some(held) = try_acquire(intent.index.as_path(), intent.seq) else {
return Settled::Open;
};
crate::service_fault::reach("restart-locked");
if own_index {
match head() {
Ok(true) => {}
Ok(false) => {
drop(held);
return done(cleanup_done(git, repository, intent));
}
Err(code) => {
drop(held);
crate::service_error!("batch {} stays open: the head of the worktree cannot be read under the lock ({code:?}), so the index is left as it is", intent.seq);
return Settled::Open;
}
}
}
return match gate_of(journal, intent) {
Gate::Replay => done(follow_up(git, repository, intent, held, journal)),
Gate::Wait => {
drop(held);
crate::service_error!("batch {} stays open: the record of the index file its landing saw cannot be read now", intent.seq);
Settled::Open
}
Gate::Leave(reason) => {
drop(held);
crate::service_error!(
"batch {}: the index entries of its paths were not updated and are left as they are, because {reason}; `git status` shows the commit's paths against the index until they are staged again",
intent.seq
);
done(cleanup_done(git, repository, intent))
}
};
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RecoveryReport {
pub seq: u64,
pub decision: Recovery,
}
pub fn recover(context: &LandContext) -> Vec<RecoveryReport> {
return recover_selected(context, &|_| { return true; }, true);
}
pub fn recover_instance(context: &LandContext, instance: &str) -> Vec<RecoveryReport> {
let ours = |intent: &Intent| -> bool { return instance_of(intent.git_dir.as_path()) == instance; };
return recover_selected(context, &ours, false);
}
fn current_value(git: &GitContext, intent: &Intent) -> String {
let request: GitRequest = GitRequest::new(intent.repository.as_path(), &["rev-parse", "--verify", intent.reference.as_str()]);
return match run_git(git, &request) {
Ok(output) if output.succeeded() => String::from_utf8_lossy(output.stdout.as_slice()).trim().to_string(),
_ => String::new(),
};
}
fn landed_below(git: &GitContext, intent: &Intent, current: &str) -> bool {
let request: GitRequest = GitRequest::new(intent.repository.as_path(), &["merge-base", "--is-ancestor", intent.new.as_str(), current]);
return run_git(git, &request).is_ok_and(|output| { return output.succeeded(); });
}
fn recover_selected(context: &LandContext, selected: &dyn Fn(&Intent) -> bool, remove_unreadable: bool) -> Vec<RecoveryReport> {
let mut reports: Vec<RecoveryReport> = Vec::new();
for (path, parsed) in list_intents(context.journal.as_path(), OPEN_EXTENSION) {
let Some(intent) = parsed else {
if remove_unreadable {
let _removed = std::fs::remove_file(&path);
}
continue;
};
if !selected(&intent) {
continue;
}
let git: GitContext = GitContext { common_dir: Some(intent.common_dir.clone()), ..context.git.clone() };
let current: String = current_value(&git, &intent);
let ancestor: bool = landed_below(&git, &intent, current.as_str());
let decision: Recovery = recovery_for(&intent, current.as_str(), ancestor);
match decision {
Recovery::Drop => {
remove_stale(intent.index.as_path(), intent.seq);
let _removed = std::fs::remove_file(&path);
let _record_removed = std::fs::remove_file(crate::service_journal::batch_path(context.journal.as_path(), intent.seq, INDEXED_EXTENSION));
remove_start(context.journal.as_path(), intent.seq);
}
Recovery::Finish => {
remove_stale(intent.index.as_path(), intent.seq);
if matches!(settle(&git, &intent, context.journal.as_path()), Settled::Done) {
let _marked = mark_done(context.journal.as_path(), intent.seq);
}
}
}
reports.push(RecoveryReport { seq: intent.seq, decision });
}
return reports;
}