pub use crate::service_work_handlers_replanning_and_acceptance::*;
pub(crate) use crate::service_work_drain_pending_claims::drain_pending;
use crate::auto_push::push_once;
use crate::auto_push_single_flight::PushGit;
use crate::child_environment::{environment_with_overlay, service_child_overlay};
use crate::config_snapshot::read_config_snapshot;
use crate::lfs_pre_push::uses_lfs;
use crate::maintenance_run::{AutoMaintenanceOutcome, run_auto_maintenance_with};
use crate::auto_push::AutoPushOutcome;
use crate::claim_time::ClaimTime;
use crate::push_retry::{Retrying, final_failure, plain_words, unrecognised_rejection};
use crate::service_claim_renewal::Renewal;
use crate::service_review::{options_for, review_item, review_variables};
use crate::service_review_key::ReviewKey;
use crate::standards_review_run::ReviewOptions;
use crate::transaction_git::GitContext;
use crate::work_item::WorkItem;
use crate::work_item_recovery::{next_claim_expiry, requeue_expired_reporting};
use std::path::{Path, PathBuf};
use std::time::Duration;
pub const CLAIM_LEASE: Duration = Duration::from_secs(120);
pub fn drain(git_dir: &Path, handlers: &dyn WorkHandlers, now: &ClaimTime) -> Option<Duration> {
return drain_with_lease(git_dir, handlers, now, CLAIM_LEASE);
}
pub fn drain_with_lease(git_dir: &Path, handlers: &dyn WorkHandlers, now: &ClaimTime, lease: Duration) -> Option<Duration> {
return drain_with_renewal(git_dir, handlers, now, lease, Renewal::Timed);
}
pub fn drain_with_renewal(git_dir: &Path, handlers: &dyn WorkHandlers, now: &ClaimTime, lease: Duration, renewal: Renewal<'_>) -> Option<Duration> {
return drain_planned_with(git_dir, handlers, now, lease, renewal, &Retrying::none()).wait;
}
pub fn drain_planned(git_dir: &Path, handlers: &dyn WorkHandlers, now: &ClaimTime, retrying: &Retrying) -> Drained {
return drain_planned_with(git_dir, handlers, now, CLAIM_LEASE, Renewal::Timed, retrying);
}
pub fn drain_planned_with(git_dir: &Path, handlers: &dyn WorkHandlers, now: &ClaimTime, lease: Duration, renewal: Renewal<'_>, retrying: &Retrying) -> Drained {
let work_dir: PathBuf = work_directory(git_dir);
let mut replanning: Replanning = Replanning { first: retrying.first, changes: Vec::new() };
tell_about_given_up(git_dir, requeue_expired_reporting(work_dir.as_path(), now, lease, MAX_TRIES).given_up.as_slice());
run_due_push_retries(git_dir, handlers, retrying.due.as_slice(), &mut replanning);
drain_pending(git_dir, handlers, lease, renewal, &mut replanning);
let wait: Option<Duration> = next_claim_expiry(work_dir.as_path(), &ClaimTime::read(), lease);
return Drained { wait, changes: replanning.changes };
}
pub fn drain_common_records(git_dir: &Path, handlers: &dyn WorkHandlers, now: &ClaimTime) -> Option<Duration> {
return drain_common_records_with_lease(git_dir, handlers, now, CLAIM_LEASE);
}
pub fn drain_common_records_with_lease(git_dir: &Path, handlers: &dyn WorkHandlers, now: &ClaimTime, lease: Duration) -> Option<Duration> {
return drain_common_records_planned(git_dir, handlers, now, lease, None).wait;
}
pub fn drain_common_records_planned(git_dir: &Path, handlers: &dyn WorkHandlers, now: &ClaimTime, lease: Duration, first: Option<Duration>) -> Drained {
let Some(common) = common_directory_of(git_dir) else {
return Drained::default();
};
if common == git_dir || !is_bare_common_directory(common.as_path()) || !work_directory(common.as_path()).is_dir() {
return Drained::default();
}
let mut replanning: Replanning = Replanning { first, changes: Vec::new() };
tell_about_given_up(common.as_path(), requeue_expired_reporting(work_directory(common.as_path()).as_path(), now, lease, MAX_TRIES).given_up.as_slice());
drain_pending(common.as_path(), handlers, lease, Renewal::Timed, &mut replanning);
let wait: Option<Duration> = next_claim_expiry(work_directory(common.as_path()).as_path(), &ClaimTime::read(), lease);
return Drained { wait, changes: replanning.changes };
}
pub(crate) fn common_directory_of(git_dir: &Path) -> Option<PathBuf> {
let text: String = std::fs::read_to_string(git_dir.join("commondir")).ok()?;
let named: &Path = Path::new(text.trim_end_matches(['\n', '\r']));
if named.as_os_str().is_empty() {
return None;
}
let mut resolved: PathBuf = PathBuf::new();
for part in git_dir.join(named).components() {
match part {
std::path::Component::ParentDir => {
if !resolved.pop() {
return None;
}
}
std::path::Component::CurDir => {}
other => resolved.push(other.as_os_str()),
}
}
return Some(resolved);
}
pub struct GitHandlers {
pub context: GitContext,
review_keys: std::collections::HashMap<PathBuf, ReviewKey>,
#[cfg(test)]
review_endpoint: Option<String>,
#[cfg(test)]
push_environment: Vec<(std::ffi::OsString, std::ffi::OsString)>,
}
impl GitHandlers {
pub fn new(context: GitContext) -> GitHandlers {
return GitHandlers {
context,
review_keys: std::collections::HashMap::new(),
#[cfg(test)]
review_endpoint: None,
#[cfg(test)]
push_environment: Vec::new(),
};
}
#[cfg(test)]
pub fn with_push_environment(mut self, variables: &[(&str, &str)]) -> GitHandlers {
self.push_environment = variables.iter().map(|(name, value)| { return (std::ffi::OsString::from(name), std::ffi::OsString::from(value)); }).collect();
return self;
}
#[cfg(test)]
pub fn with_review_endpoint(mut self, endpoint: &str) -> GitHandlers {
self.review_endpoint = Some(endpoint.to_string());
return self;
}
pub fn for_round(&self, keys: std::collections::HashMap<PathBuf, ReviewKey>) -> GitHandlers {
return GitHandlers {
context: self.context.clone(),
review_keys: keys,
#[cfg(test)]
review_endpoint: self.review_endpoint.clone(),
#[cfg(test)]
push_environment: self.push_environment.clone(),
};
}
fn review_key_of(&self, git_dir: &Path) -> Option<ReviewKey> {
return self.review_keys.get(git_dir).cloned();
}
fn push_git(&self, item: &WorkItem) -> PushGit {
let cwd: PathBuf = PathBuf::from(item.repository.as_str());
#[cfg_attr(not(test), expect(unused_mut, reason = "only a test adds variables"))]
let mut inherited: Vec<(std::ffi::OsString, std::ffi::OsString)> = std::env::vars_os().collect();
#[cfg(test)]
inherited.extend(self.push_environment.iter().cloned());
return PushGit {
real_git: self.context.real_git.clone(),
lfs: read_config_snapshot(&self.context, cwd.as_path()).as_ref().is_none_or(uses_lfs),
cwd,
environment: environment_with_overlay(inherited.as_slice(), service_child_overlay(inherited.as_slice()).as_slice()),
};
}
}
impl WorkHandlers for GitHandlers {
fn push(&self, items: &[WorkItem]) -> Verdict {
let Some(newest) = items.last() else { return Verdict::Done };
let git: PushGit = self.push_git(newest);
let mut output: Vec<u8> = Vec::new();
return match push_once(&git, newest.branch_ref.as_str(), newest.oid.as_str(), &mut output) {
Ok(result) if result.outcome == AutoPushOutcome::Skipped => Verdict::Skipped(result.shown),
Ok(result) if result.exit_code == 0 => Verdict::Done,
Ok(result) => match final_failure(result.shown.as_str()) {
Some(cause) => Verdict::Failed(match plain_words(cause) {
Some(words) => format!("{words}\n{}", result.shown),
None => result.shown,
}),
None => {
if let Some(line) = unrecognised_rejection(result.shown.as_str()) {
crate::service_warn!("the server rejected the push for a reason this release does not know, so it is tried again: {line}");
}
Verdict::Retry(result.shown)
}
},
Err(error) => Verdict::Retry(format!("{error}")),
};
}
fn trash(&self, item: &WorkItem) -> Verdict {
return crate::worktree_trash::empty_trash(item);
}
fn maintenance(&self, item: &WorkItem) -> Verdict {
return match run_auto_maintenance_with(&self.context, Path::new(item.repository.as_str()), None) {
Ok(AutoMaintenanceOutcome::Exited(code)) if code != 0 => Verdict::Failed(format!("it exited with status {code}")),
Ok(_) => Verdict::Done,
Err(error) => Verdict::Failed(format!("{error:?}")),
};
}
fn review(&self, item: &WorkItem) -> Verdict {
let key: Option<ReviewKey> = self.review_key_of(Path::new(item.git_dir.as_str()));
#[cfg_attr(not(test), expect(unused_mut, reason = "only a test replaces the endpoint"))]
let mut options: ReviewOptions = options_for(item, &review_variables(key.as_ref()));
#[cfg(test)]
if let Some(endpoint) = self.review_endpoint.as_ref() {
options.endpoint = endpoint.clone();
}
return review_item(&self.context, item, options);
}
}
#[cfg(test)]
#[path = "service_work_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "service_push_retry_tests.rs"]
mod push_retry_tests;