use crate::auto_push::push_once;
use crate::journald_native::Priority;
use crate::auto_push_single_flight::PushGit;
use crate::child_environment::{environment_with_overlay, lockfile_pid_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_failure_notice::{clear_push_failures_before, record_push_failure, record_push_skip};
use crate::push_retry::{Due, PushRetries, RetryChange, Retrying, final_failure, pause_before, plain_words, unrecognised_rejection};
use crate::repo_read_config_text::{last_entry, parse_bool, parse_config_text};
use crate::service_claim_renewal::{ClaimRenewal, HeldClaims, Renewal, held_claims, renewal_interval};
use crate::service_review::{options_for, review_item, review_variables};
use crate::parallel_budget::HostResources;
use crate::service_review_key::ReviewKey;
use crate::service_round::{Assignment, RoundAnswer, RoundDrain, drain_directories};
use crate::service_timing::log_entry;
use crate::standards_review_run::ReviewOptions;
use crate::transaction_git::GitContext;
use crate::work_item::{Claimed, DONE, FAILED, WORK_DIRECTORY, WorkItem, WorkKind, claim_next_of, next_claim_expiry, requeue_expired_reporting};
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 git_dir.join(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.bytes().all(|byte| { return byte.is_ascii_hexdigit(); });
}
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}");
}
}
}
fn still_held(held: &HeldClaims, work_dir: &Path, claimed: Vec<Claimed>) -> Vec<Claimed> {
let (mine, lost): (Vec<Claimed>, Vec<Claimed>) = claimed.into_iter().partition(|item| { return held.is_held_on_disk(work_dir, item.id.as_str()); });
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;
}
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 = last_entry(entries.as_slice(), "extensions.worktreeconfig").is_some_and(|entry| { return 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 last_entry(entries.as_slice(), "core.bare").is_some_and(|entry| { return parse_bool(entry.value.as_deref()) == Some(true); });
}
fn tell_about_given_up(git_dir: &Path, given_up: &[WorkItem]) {
for item in given_up.iter().filter(|item| { return item.kind == WorkKind::Push; }) {
let reason: String = format!(
"cli-git: 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>,
}
struct Replanning {
first: Option<Duration>,
changes: Vec<RetryChange>,
}
impl Replanning {
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() });
}
}
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!(
"cli-git: 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}");
}
fn run_due_push_retries(git_dir: &Path, handlers: &dyn WorkHandlers, candidates: &[Due], replanning: &mut Replanning) {
for due in candidates.iter().filter(|candidate| { return candidate.git_dir == git_dir; }) {
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}");
}
}
}
}
}
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);
}
fn drain_pending(git_dir: &Path, handlers: &dyn WorkHandlers, lease: Duration, renewal: Renewal<'_>, replanning: &mut Replanning) {
let work_dir: PathBuf = work_directory(git_dir);
let (held, renewer) = held_claims(work_dir.as_path());
let _renewing: ClaimRenewal = match renewal {
Renewal::Timed => ClaimRenewal::start(renewer, renewal_interval(lease)),
Renewal::Driven(driver) => ClaimRenewal::driven(renewer, driver),
};
loop {
let mut claimed: Vec<Claimed> = Vec::new();
loop {
match claim_next_of(work_dir.as_path(), &ClaimTime::read(), &SERVICE_KINDS) {
Ok(Some(item)) => {
held.hold(&item);
claimed.push(item);
}
Ok(None) => break,
Err(reason) => {
crate::service_error!("{reason}");
break;
}
}
}
if claimed.is_empty() {
return;
}
let (acceptable, refused): (Vec<_>, Vec<_>) = claimed.into_iter().partition(|item| { return is_acceptable(&item.item, git_dir); });
settle_all(work_dir.as_path(), &held, &refused, FAILED);
let (pushes, rest): (Vec<_>, Vec<_>) = acceptable.into_iter().partition(|item| { return item.item.kind == WorkKind::Push; });
let (trashes, after_trash): (Vec<_>, Vec<_>) = rest.into_iter().partition(|item| { return item.item.kind == WorkKind::Trash; });
let (reviews, upkeep): (Vec<_>, Vec<_>) = after_trash.into_iter().partition(|item| { return item.item.kind == WorkKind::Review; });
let mut groups: Vec<((String, String), Vec<Claimed>)> = Vec::new();
for item in pushes {
let key = (item.item.repository.clone(), item.item.branch_ref.clone());
match groups.iter_mut().find(|(existing, _)| { return *existing == key; }) {
Some((_, members)) => members.push(item),
None => groups.push((key, vec![item])),
}
}
for (_, claimed_members) in groups {
let members: Vec<Claimed> = still_held(&held, work_dir.as_path(), claimed_members);
if members.is_empty() {
continue;
}
let items: Vec<WorkItem> = members.iter().map(|member| { return member.item.clone(); }).collect();
let pushing_since: std::time::SystemTime = std::time::SystemTime::now();
let pushing: Instant = Instant::now();
let verdict: Verdict = handlers.push(items.as_slice());
if let Some(newest) = members.last() {
let (priority, outcome, said): (Priority, &str, &str) = match &verdict {
Verdict::Done => (Priority::Info, "done", ""),
Verdict::Failed(text) => (Priority::Warning, "failed", text.as_str()),
Verdict::Retry(text) => (Priority::Warning, "retrying", text.as_str()),
Verdict::Skipped(text) => (Priority::Info, "skipped", text.as_str()),
};
let fields: [(&str, String); 3] = [
("CLI_GIT_BRANCH", newest.item.branch_ref.clone()),
("CLI_GIT_OID", newest.item.oid.clone()),
("CLI_GIT_OUTCOME", outcome.to_string()),
];
log_entry(
priority,
format!("pushed branch={:?} oid={} items={} outcome={outcome} push_ms={}{}{said}", newest.item.branch_ref, newest.item.oid, members.len(), pushing.elapsed().as_millis(), if said.is_empty() { "" } else { ": " }).as_str(),
&fields,
);
}
match verdict {
Verdict::Done => {
if let Some(newest) = members.last() {
clear_push_failures_before(git_dir, newest.item.branch_ref.as_str(), pushing_since);
replanning.forget(git_dir, newest.item.branch_ref.as_str());
}
settle_all(work_dir.as_path(), &held, &members, DONE);
}
Verdict::Retry(text) => {
let said: String = match (members.last(), replanning.first) {
(Some(newest), Some(first)) => planned_retry(git_dir, replanning, first, &newest.item, 1, text.as_str()),
_ => text,
};
if let Some(newest) = members.last()
&& let Err(reason) = record_push_failure(git_dir, newest.item.branch_ref.as_str(), newest.item.oid.as_str(), said.as_str())
{
crate::service_error!("the failed push of {} could not be recorded for the person: {reason}", newest.item.oid);
}
settle_all(work_dir.as_path(), &held, &members, FAILED);
}
Verdict::Failed(text) => {
if let Some(newest) = members.last() {
replanning.forget(git_dir, newest.item.branch_ref.as_str());
}
if let Some(newest) = members.last()
&& let Err(reason) = record_push_failure(git_dir, newest.item.branch_ref.as_str(), newest.item.oid.as_str(), text.as_str())
{
crate::service_error!("the failed push of {} could not be recorded for the person: {reason}", newest.item.oid);
}
settle_all(work_dir.as_path(), &held, &members, FAILED);
}
Verdict::Skipped(text) => {
let recorded: bool = match members.last() {
Some(newest) if !text.is_empty() => match record_push_skip(git_dir, newest.item.branch_ref.as_str(), newest.item.oid.as_str(), text.as_str()) {
Ok(()) => true,
Err(reason) => {
crate::service_error!("the skipped push of {} could not be recorded for the person: {reason}", newest.item.oid);
false
}
},
_ => true,
};
settle_all(work_dir.as_path(), &held, &members, if recorded { DONE } else { FAILED });
}
}
}
let mut upkept: Vec<String> = Vec::new();
for item in still_held(&held, work_dir.as_path(), upkeep) {
if !upkept.contains(&item.item.repository) {
upkept.push(item.item.repository.clone());
let upkeep_began: Instant = Instant::now();
match handlers.maintenance(&item.item) {
Verdict::Failed(reason) | Verdict::Retry(reason) => crate::service_warn!("automatic maintenance did not run: {reason}"),
Verdict::Done | Verdict::Skipped(_) => crate::service_log!("automatic maintenance ran maintenance_ms={}", upkeep_began.elapsed().as_millis()),
};
}
settle_all(work_dir.as_path(), &held, std::slice::from_ref(&item), DONE);
}
for item in still_held(&held, work_dir.as_path(), trashes) {
match handlers.trash(&item.item) {
Verdict::Done => settle_all(work_dir.as_path(), &held, std::slice::from_ref(&item), DONE),
Verdict::Failed(reason) | Verdict::Retry(reason) => {
eprintln!("cli-git-service: the trash {} could not be emptied: {reason}", item.item.parent);
settle_all(work_dir.as_path(), &held, std::slice::from_ref(&item), FAILED);
}
Verdict::Skipped(reason) => {
eprintln!("cli-git-service: the trash {} was left as it is: {reason}", item.item.parent);
settle_all(work_dir.as_path(), &held, std::slice::from_ref(&item), DONE);
}
}
}
for item in still_held(&held, work_dir.as_path(), reviews) {
let review_began: Instant = Instant::now();
let verdict: Verdict = handlers.review(&item.item);
if matches!(verdict, Verdict::Done) {
crate::service_log!("the review of {} is done review_ms={}", item.item.oid, review_began.elapsed().as_millis());
}
match verdict {
Verdict::Done => settle_all(work_dir.as_path(), &held, std::slice::from_ref(&item), DONE),
Verdict::Failed(reason) | Verdict::Retry(reason) => {
crate::service_warn!("the review of {} failed: {reason}", item.item.oid);
settle_all(work_dir.as_path(), &held, std::slice::from_ref(&item), FAILED);
}
Verdict::Skipped(reason) => {
crate::service_log!("the review of {} was skipped: {reason}", item.item.oid);
settle_all(work_dir.as_path(), &held, std::slice::from_ref(&item), DONE);
}
}
}
}
}
pub fn run_round(wakes: Vec<Wake>, handlers: &GitHandlers, retries: &mut PushRetries, now: &ClaimTime) -> Vec<(PathBuf, Duration)> {
return run_round_on(&HostResources::read(), wakes, handlers, retries, now);
}
pub fn run_round_on(host: &HostResources, wakes: Vec<Wake>, handlers: &GitHandlers, retries: &mut PushRetries, now: &ClaimTime) -> Vec<(PathBuf, Duration)> {
let mut directories: Vec<PathBuf> = Vec::new();
let mut keys: std::collections::HashMap<PathBuf, ReviewKey> = std::collections::HashMap::new();
for (directory, review_key) in wakes {
if let Some(key) = review_key {
keys.insert(directory.clone(), key);
}
if !directories.contains(&directory) {
directories.push(directory);
}
}
let round: GitHandlers = handlers.for_round(keys);
return run_planned_round(host, directories.as_slice(), &round, retries, now);
}
pub fn run_planned_round<H: WorkHandlers + Sync>(host: &HostResources, directories: &[PathBuf], handlers: &H, retries: &mut PushRetries, now: &ClaimTime) -> Vec<(PathBuf, Duration)> {
let taken_at: Instant = Instant::now();
let first: Duration = retries.first();
let assignments: Vec<Assignment> = directories
.iter()
.map(|directory| { return Assignment { directory: directory.clone(), retrying: Retrying { first: Some(first), due: retries.take_due(directory.as_path(), taken_at) } }; })
.collect();
let answered: RoundAnswer = drain_directories(&RoundDrain { directories: assignments.as_slice(), handlers, now, host });
let mut named: Vec<PathBuf> = directories.to_vec();
for change in &answered.changes {
if let RetryChange::Plan { git_dir, .. } = change
&& !named.contains(git_dir)
{
named.push(git_dir.clone());
}
}
retries.apply(answered.changes);
let applied_at: Instant = Instant::now();
let mut asked: Vec<(PathBuf, Duration)> = Vec::new();
for directory in named {
let lease: Option<Duration> = answered.waits.iter().find(|(named_directory, _)| { return *named_directory == directory; }).map(|(_, wait)| { return *wait; });
let planned: Option<Duration> = retries.next_wait(directory.as_path(), applied_at);
if let Some(wait) = [lease, planned].into_iter().flatten().min() {
asked.push((directory, wait));
}
}
return asked;
}
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(), lockfile_pid_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;