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, list_intents_read_only, 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, Record, remove_stale, try_acquire};
use crate::autostash_carry::carry_stages;
use crate::service_land_context::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()) };
}
fn settled_of(finished: bool) -> Settled {
return if finished { Settled::Done } else { Settled::Open };
}
pub(crate) fn cleanup_done(git: &GitContext, repository: &Path, intent: &Intent) -> bool {
return match intent.cleanup.as_ref() {
Some(cleanup) => remove_conclusion_state(git, repository, cleanup),
None => true,
};
}
struct RecordIndexed<'a> {
journal: &'a Path,
intent: &'a Intent,
}
impl Record for RecordIndexed<'_> {
fn record(&self) -> std::io::Result<()> {
return mark_indexed(self.journal, self.intent.seq, self.intent.new.as_str());
}
}
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: RecordIndexed<'_> = RecordIndexed { journal, intent };
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");
if own_index {
match still_checked_out(git, repository, intent.git_dir.as_path(), intent.common_dir.as_path(), intent.reference.as_str()) {
Ok(false) => return settled_of(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 settled_of(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 still_checked_out(git, repository, intent.git_dir.as_path(), intent.common_dir.as_path(), intent.reference.as_str()) {
Ok(true) => {}
Ok(false) => {
drop(held);
return settled_of(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 => settled_of(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
);
settled_of(cleanup_done(git, repository, intent))
}
};
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Handled {
Finished,
StillOpen,
Dropped,
Left,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RecoveryReport {
pub seq: u64,
pub decision: Recovery,
pub handled: Handled,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum JournalOwner {
ThisService,
EarlierHelper,
}
pub fn recover(context: &LandContext) -> Vec<RecoveryReport> {
return recover_selected(context, None, true, JournalOwner::ThisService);
}
pub fn recover_instance(context: &LandContext, instance: &str) -> Vec<RecoveryReport> {
return recover_selected(context, Some(instance), false, JournalOwner::ThisService);
}
pub fn recover_instance_without_drops(context: &LandContext, instance: &str) -> Vec<RecoveryReport> {
return recover_selected(context, Some(instance), false, JournalOwner::EarlierHelper);
}
const NAME_IS_NO_OBJECT: i32 = 1;
const NOT_AN_ANCESTOR: i32 = 1;
fn current_value(git: &GitContext, intent: &Intent) -> Option<String> {
let request: GitRequest = GitRequest::new(intent.repository.as_path(), &["rev-parse", "--verify", "--quiet", intent.reference.as_str()]);
let Ok(output) = run_git(git, &request) else {
return None;
};
if output.succeeded() {
return Some(String::from_utf8_lossy(output.stdout.as_slice()).trim().to_string());
}
if output.code == Some(NAME_IS_NO_OBJECT) && output.stderr.is_empty() {
return Some(String::new());
}
return None;
}
fn landed_below(git: &GitContext, intent: &Intent, current: &str) -> Option<bool> {
let request: GitRequest = GitRequest::new(intent.repository.as_path(), &["merge-base", "--is-ancestor", intent.new.as_str(), current]);
let Ok(output) = run_git(git, &request) else {
return None;
};
if output.succeeded() {
return Some(true);
}
return if output.code == Some(NOT_AN_ANCESTOR) { Some(false) } else { None };
}
fn decision_for(git: &GitContext, intent: &Intent) -> Option<Recovery> {
let current: String = current_value(git, intent)?;
let decided_without_ancestry: bool = current.is_empty() || current == intent.old || current == intent.new;
let ancestor: bool = if decided_without_ancestry { false } else { landed_below(git, intent, current.as_str())? };
return Some(recovery_for(intent, current.as_str(), ancestor));
}
fn recover_selected(context: &LandContext, instance: Option<&str>, remove_unreadable: bool, owner: JournalOwner) -> Vec<RecoveryReport> {
let mut reports: Vec<RecoveryReport> = Vec::new();
let listed: Vec<(std::path::PathBuf, Option<Intent>)> = match owner {
JournalOwner::ThisService => list_intents(context.journal.as_path(), OPEN_EXTENSION),
JournalOwner::EarlierHelper => list_intents_read_only(context.journal.as_path(), OPEN_EXTENSION),
};
for (path, parsed) in listed {
let Some(intent) = parsed else {
if remove_unreadable {
let _removed = std::fs::remove_file(&path);
}
continue;
};
let selected: bool = match instance {
Some(named) => instance_of(intent.git_dir.as_path()) == named,
None => true,
};
if !selected {
continue;
}
let git: GitContext = GitContext { common_dir: Some(intent.common_dir.clone()), ..context.git.clone() };
let Some(decision) = decision_for(&git, &intent) else {
crate::service_error!("batch {} stays open: Git gave no answer about {} in {}, so its intent is kept", intent.seq, intent.reference, intent.repository.display());
continue;
};
let handled: Handled = match (decision, owner) {
(Recovery::Drop, JournalOwner::EarlierHelper) => Handled::Left,
(Recovery::Drop, JournalOwner::ThisService) => {
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);
Handled::Dropped
}
(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);
Handled::Finished
} else {
Handled::StillOpen
}
}
};
reports.push(RecoveryReport { seq: intent.seq, decision, handled });
}
return reports;
}