use crate::kept_names_in_the_git_directories::path_for_reading;
use crate::journald_native::Priority;
use crate::push_failure_notice::{clear_push_failures_before, record_push_failure, record_push_skip};
use crate::push_retry::{Due, RetryChange, pause_before};
use crate::repo_read_config_text::{last_entry, parse_bool, parse_config_text};
use crate::service_claim_renewal::HeldClaims;
use crate::service_review_key::ReviewKey;
use crate::service_timing::log_entry;
use crate::work_item::{Claimed, WORK_DIRECTORY, WorkItem, WorkKind};
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
pub const SERVICE_KINDS: [WorkKind; 4] = [WorkKind::Push, WorkKind::Maintenance, WorkKind::Review, WorkKind::Trash];
pub const MAX_TRIES: u32 = 3;
pub type Wake = (PathBuf, Option<ReviewKey>);
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Verdict {
Done,
Failed(String),
Skipped(String),
Retry(String),
}
pub trait WorkHandlers {
fn push(&self, items: &[WorkItem]) -> Verdict;
fn maintenance(&self, item: &WorkItem) -> Verdict;
fn review(&self, item: &WorkItem) -> Verdict;
fn trash(&self, _item: &WorkItem) -> Verdict {
return Verdict::Skipped(String::from("this handler does not empty trash directories"));
}
}
pub fn work_directory(git_dir: &Path) -> PathBuf {
return path_for_reading(git_dir, WORK_DIRECTORY);
}
pub fn is_acceptable(item: &WorkItem, git_dir: &Path) -> bool {
return Path::new(item.repository.as_str()).is_absolute()
&& Path::new(item.repository.as_str()).is_dir()
&& Path::new(item.git_dir.as_str()) == git_dir
&& item.branch_ref.starts_with("refs/")
&& !item.branch_ref.contains(['\0', '\n', ' '])
&& item.oid.len() >= 40
&& item.oid.as_bytes().iter().all(u8::is_ascii_hexdigit);
}
pub(crate) fn settle_all(work_dir: &Path, held: &HeldClaims, claimed: &[Claimed], state: &str) {
for item in claimed {
if let Err(reason) = held.settle(work_dir, item.id.as_str(), state) {
crate::service_warn!("{reason}");
}
}
}
pub(crate) fn still_held(held: &HeldClaims, work_dir: &Path, claimed: Vec<Claimed>) -> Vec<Claimed> {
let mut mine: Vec<Claimed> = Vec::new();
let mut lost: Vec<Claimed> = Vec::new();
for item in claimed {
if held.is_held_on_disk(work_dir, item.id.as_str()) {
mine.push(item);
} else {
lost.push(item);
}
}
for item in lost {
crate::service_warn!("the claim on {} ran out before this instance could run it, so another instance took the work over and it is not run here",
item.id
);
held.release(item.id.as_str());
}
return mine;
}
pub(crate) fn is_bare_common_directory(common: &Path) -> bool {
let Ok(text) = std::fs::read(common.join("config")) else {
return false;
};
let Some(mut entries) = parse_config_text(text.as_slice()) else {
return false;
};
let worktree_config: bool = matches!(last_entry(entries.as_slice(), "extensions.worktreeconfig"), Some(entry) if parse_bool(entry.value.as_deref()) == Some(true))
&& last_entry(entries.as_slice(), "core.repositoryformatversion").is_some();
if worktree_config {
match std::fs::read(common.join("config.worktree")) {
Ok(bytes) => match parse_config_text(bytes.as_slice()) {
Some(more) => entries.extend(more),
None => return false,
},
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(_) => return false,
}
}
return matches!(last_entry(entries.as_slice(), "core.bare"), Some(entry) if parse_bool(entry.value.as_deref()) == Some(true));
}
pub(crate) fn tell_about_given_up(git_dir: &Path, given_up: &[WorkItem]) {
for item in given_up {
if item.kind != WorkKind::Push {
continue;
}
let reason: String = format!(
"monochromatic-git-policy-cli: the commit service stopped {MAX_TRIES} times before it finished pushing {} (a restart, a kill or a crash each time), so it gave up on that push.",
item.branch_ref
);
if let Err(problem) = record_push_skip(git_dir, item.branch_ref.as_str(), item.oid.as_str(), reason.as_str()) {
crate::service_error!("the abandoned push of {} could not be recorded for the person: {problem}", item.oid);
}
}
}
#[derive(Debug, Default)]
pub struct Drained {
pub wait: Option<Duration>,
pub changes: Vec<RetryChange>,
}
pub(crate) struct Replanning {
pub(crate) first: Option<Duration>,
pub(crate) changes: Vec<RetryChange>,
}
impl Replanning {
pub(crate) fn forget(&mut self, git_dir: &Path, branch_ref: &str) {
self.changes.push(RetryChange::Forget { git_dir: git_dir.to_path_buf(), branch_ref: branch_ref.to_string() });
}
}
pub(crate) fn planned_retry(git_dir: &Path, replanning: &mut Replanning, first: Duration, item: &WorkItem, attempt: u32, text: &str) -> String {
let pause: Duration = pause_before(attempt, first);
replanning.changes.push(RetryChange::Plan { git_dir: git_dir.to_path_buf(), item: item.clone(), attempt, due: Instant::now() + pause });
let plan: String = format!(
"monochromatic-git-policy-cli: the commit service tries this push again by itself in {} seconds (try {}), and keeps trying with longer pauses until the push lands or a conflict needs you.",
pause.as_secs(),
attempt + 1
);
let fields: [(&str, String); 3] = [
("CLI_GIT_BRANCH", item.branch_ref.clone()),
("CLI_GIT_OID", item.oid.clone()),
("CLI_GIT_OUTCOME", String::from("retrying")),
];
log_entry(
Priority::Warning,
format!("push will be tried again branch={:?} oid={} next_try={} in_seconds={} cause: {}", item.branch_ref, item.oid, attempt + 1, pause.as_secs(), text.lines().next().unwrap_or_default()).as_str(),
&fields,
);
return format!("{plan}\n{text}");
}
pub(crate) fn run_due_push_retries(git_dir: &Path, handlers: &dyn WorkHandlers, candidates: &[Due], replanning: &mut Replanning) {
for due in candidates {
if due.git_dir != git_dir {
continue;
}
let branch: &str = due.item.branch_ref.as_str();
let commit: &str = due.item.oid.as_str();
if !is_acceptable(&due.item, git_dir) {
crate::service_warn!("the push of {branch} is not tried again: its repository or branch record is no longer acceptable");
replanning.forget(git_dir, branch);
continue;
}
let pushing_since: std::time::SystemTime = std::time::SystemTime::now();
let began: Instant = Instant::now();
let verdict: Verdict = handlers.push(std::slice::from_ref(&due.item));
match verdict {
Verdict::Done => {
clear_push_failures_before(git_dir, branch, pushing_since);
replanning.forget(git_dir, branch);
crate::service_log!("the push of {branch:?} landed on try {} push_ms={}", due.attempt + 1, began.elapsed().as_millis());
}
Verdict::Retry(text) => {
let said: String = match replanning.first {
Some(first) => planned_retry(git_dir, replanning, first, &due.item, due.attempt + 1, text.as_str()),
None => {
replanning.forget(git_dir, branch);
text
}
};
if let Err(reason) = record_push_failure(git_dir, branch, commit, said.as_str()) {
crate::service_error!("the failed push of {commit} could not be recorded for the person: {reason}");
}
}
Verdict::Failed(text) => {
crate::service_warn!("the push of {branch:?} failed on try {} and needs you: {}", due.attempt + 1, text.lines().next().unwrap_or_default());
replanning.forget(git_dir, branch);
if let Err(reason) = record_push_failure(git_dir, branch, commit, text.as_str()) {
crate::service_error!("the failed push of {commit} could not be recorded for the person: {reason}");
}
}
Verdict::Skipped(text) => {
replanning.forget(git_dir, branch);
if !text.is_empty() && let Err(reason) = record_push_skip(git_dir, branch, commit, text.as_str()) {
crate::service_error!("the skipped push of {commit} could not be recorded for the person: {reason}");
}
}
}
}
}