1use crate::auto_push::push_once;
19use crate::journald_native::Priority;
20use crate::auto_push_single_flight::PushGit;
21use crate::child_environment::{environment_with_overlay, lockfile_pid_overlay};
22use crate::config_snapshot::read_config_snapshot;
23use crate::lfs_pre_push::uses_lfs;
24use crate::maintenance_run::{AutoMaintenanceOutcome, run_auto_maintenance_with};
25use crate::auto_push::AutoPushOutcome;
26use crate::claim_time::ClaimTime;
27use crate::push_failure_notice::{clear_push_failures_before, record_push_failure, record_push_skip};
28use crate::push_retry::{Due, PushRetries, RetryChange, Retrying, final_failure, pause_before, plain_words, unrecognised_rejection};
29use crate::repo_read_config_text::{last_entry, parse_bool, parse_config_text};
30use crate::service_claim_renewal::{ClaimRenewal, HeldClaims, Renewal, held_claims, renewal_interval};
31use crate::service_review::{options_for, review_item, review_variables};
32use crate::parallel_budget::HostResources;
33use crate::service_review_key::ReviewKey;
34use crate::service_round::{Assignment, RoundAnswer, RoundDrain, drain_directories};
35use crate::service_timing::log_entry;
36use crate::standards_review_run::ReviewOptions;
37use crate::transaction_git::GitContext;
38use crate::work_item::{Claimed, DONE, FAILED, WORK_DIRECTORY, WorkItem, WorkKind, claim_next_of, next_claim_expiry, requeue_expired_reporting};
39use std::path::{Path, PathBuf};
40use std::time::{Duration, Instant};
41
42pub const SERVICE_KINDS: [WorkKind; 4] = [WorkKind::Push, WorkKind::Maintenance, WorkKind::Review, WorkKind::Trash];
44
45pub const MAX_TRIES: u32 = 3;
47
48pub type Wake = (PathBuf, Option<ReviewKey>);
50
51#[derive(Clone, Debug, Eq, PartialEq)]
53pub enum Verdict {
54 Done,
56 Failed(String),
58 Skipped(String),
60 Retry(String),
63}
64
65pub trait WorkHandlers {
67 fn push(&self, items: &[WorkItem]) -> Verdict;
69 fn maintenance(&self, item: &WorkItem) -> Verdict;
71 fn review(&self, item: &WorkItem) -> Verdict;
73 fn trash(&self, _item: &WorkItem) -> Verdict {
75 return Verdict::Skipped(String::from("this handler does not empty trash directories"));
76 }
77}
78
79pub fn work_directory(git_dir: &Path) -> PathBuf {
81 return git_dir.join(WORK_DIRECTORY);
82}
83
84pub fn is_acceptable(item: &WorkItem, git_dir: &Path) -> bool {
95 return Path::new(item.repository.as_str()).is_absolute()
96 && Path::new(item.repository.as_str()).is_dir()
97 && Path::new(item.git_dir.as_str()) == git_dir
98 && item.branch_ref.starts_with("refs/")
99 && !item.branch_ref.contains(['\0', '\n', ' '])
100 && item.oid.len() >= 40
101 && item.oid.bytes().all(|byte| { return byte.is_ascii_hexdigit(); });
102}
103
104fn settle_all(work_dir: &Path, held: &HeldClaims, claimed: &[Claimed], state: &str) {
106 for item in claimed {
107 if let Err(reason) = held.settle(work_dir, item.id.as_str(), state) {
108 crate::service_warn!("{reason}");
109 }
110 }
111}
112
113fn still_held(held: &HeldClaims, work_dir: &Path, claimed: Vec<Claimed>) -> Vec<Claimed> {
124 let (mine, lost): (Vec<Claimed>, Vec<Claimed>) = claimed.into_iter().partition(|item| { return held.is_held_on_disk(work_dir, item.id.as_str()); });
125 for item in lost {
126 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",
127 item.id
128 );
129 held.release(item.id.as_str());
130 }
131 return mine;
132}
133
134fn is_bare_common_directory(common: &Path) -> bool {
151 let Ok(text) = std::fs::read(common.join("config")) else {
152 return false;
153 };
154 let Some(mut entries) = parse_config_text(text.as_slice()) else {
155 return false;
156 };
157 let worktree_config: bool = last_entry(entries.as_slice(), "extensions.worktreeconfig").is_some_and(|entry| { return parse_bool(entry.value.as_deref()) == Some(true); })
158 && last_entry(entries.as_slice(), "core.repositoryformatversion").is_some();
159 if worktree_config {
160 match std::fs::read(common.join("config.worktree")) {
162 Ok(bytes) => match parse_config_text(bytes.as_slice()) {
163 Some(more) => entries.extend(more),
164 None => return false,
165 },
166 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
167 Err(_) => return false,
168 }
169 }
170 return last_entry(entries.as_slice(), "core.bare").is_some_and(|entry| { return parse_bool(entry.value.as_deref()) == Some(true); });
171}
172
173fn tell_about_given_up(git_dir: &Path, given_up: &[WorkItem]) {
185 for item in given_up.iter().filter(|item| { return item.kind == WorkKind::Push; }) {
186 let reason: String = format!(
187 "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.",
188 item.branch_ref
189 );
190 if let Err(problem) = record_push_skip(git_dir, item.branch_ref.as_str(), item.oid.as_str(), reason.as_str()) {
191 crate::service_error!("the abandoned push of {} could not be recorded for the person: {problem}", item.oid);
192 }
193 }
194}
195
196#[derive(Debug, Default)]
208pub struct Drained {
209 pub wait: Option<Duration>,
211 pub changes: Vec<RetryChange>,
213}
214
215struct Replanning {
226 first: Option<Duration>,
228 changes: Vec<RetryChange>,
230}
231
232impl Replanning {
234 fn forget(&mut self, git_dir: &Path, branch_ref: &str) {
236 self.changes.push(RetryChange::Forget { git_dir: git_dir.to_path_buf(), branch_ref: branch_ref.to_string() });
237 }
238}
239
240fn planned_retry(git_dir: &Path, replanning: &mut Replanning, first: Duration, item: &WorkItem, attempt: u32, text: &str) -> String {
253 let pause: Duration = pause_before(attempt, first);
254 replanning.changes.push(RetryChange::Plan { git_dir: git_dir.to_path_buf(), item: item.clone(), attempt, due: Instant::now() + pause });
255 let plan: String = format!(
256 "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.",
257 pause.as_secs(),
258 attempt + 1
259 );
260 let fields: [(&str, String); 3] = [
261 ("CLI_GIT_BRANCH", item.branch_ref.clone()),
262 ("CLI_GIT_OID", item.oid.clone()),
263 ("CLI_GIT_OUTCOME", String::from("retrying")),
264 ];
265 log_entry(
266 Priority::Warning,
267 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(),
268 &fields,
269 );
270 return format!("{plan}\n{text}");
271}
272
273fn run_due_push_retries(git_dir: &Path, handlers: &dyn WorkHandlers, candidates: &[Due], replanning: &mut Replanning) {
286 for due in candidates.iter().filter(|candidate| { return candidate.git_dir == git_dir; }) {
287 let branch: &str = due.item.branch_ref.as_str();
288 let commit: &str = due.item.oid.as_str();
289 if !is_acceptable(&due.item, git_dir) {
290 crate::service_warn!("the push of {branch} is not tried again: its repository or branch record is no longer acceptable");
291 replanning.forget(git_dir, branch);
292 continue;
293 }
294 let pushing_since: std::time::SystemTime = std::time::SystemTime::now();
295 let began: Instant = Instant::now();
296 let verdict: Verdict = handlers.push(std::slice::from_ref(&due.item));
297 match verdict {
298 Verdict::Done => {
299 clear_push_failures_before(git_dir, branch, pushing_since);
300 replanning.forget(git_dir, branch);
301 crate::service_log!("the push of {branch:?} landed on try {} push_ms={}", due.attempt + 1, began.elapsed().as_millis());
302 }
303 Verdict::Retry(text) => {
304 let said: String = match replanning.first {
305 Some(first) => planned_retry(git_dir, replanning, first, &due.item, due.attempt + 1, text.as_str()),
306 None => {
307 replanning.forget(git_dir, branch);
308 text
309 }
310 };
311 if let Err(reason) = record_push_failure(git_dir, branch, commit, said.as_str()) {
312 crate::service_error!("the failed push of {commit} could not be recorded for the person: {reason}");
313 }
314 }
315 Verdict::Failed(text) => {
316 crate::service_warn!("the push of {branch:?} failed on try {} and needs you: {}", due.attempt + 1, text.lines().next().unwrap_or_default());
317 replanning.forget(git_dir, branch);
318 if let Err(reason) = record_push_failure(git_dir, branch, commit, text.as_str()) {
319 crate::service_error!("the failed push of {commit} could not be recorded for the person: {reason}");
320 }
321 }
322 Verdict::Skipped(text) => {
323 replanning.forget(git_dir, branch);
324 if !text.is_empty() && let Err(reason) = record_push_skip(git_dir, branch, commit, text.as_str()) {
325 crate::service_error!("the skipped push of {commit} could not be recorded for the person: {reason}");
326 }
327 }
328 }
329 }
330}
331
332pub const CLAIM_LEASE: Duration = Duration::from_secs(120);
344
345pub fn drain(git_dir: &Path, handlers: &dyn WorkHandlers, now: &ClaimTime) -> Option<Duration> {
362 return drain_with_lease(git_dir, handlers, now, CLAIM_LEASE);
363}
364
365pub fn drain_with_lease(git_dir: &Path, handlers: &dyn WorkHandlers, now: &ClaimTime, lease: Duration) -> Option<Duration> {
367 return drain_with_renewal(git_dir, handlers, now, lease, Renewal::Timed);
368}
369
370pub fn drain_with_renewal(git_dir: &Path, handlers: &dyn WorkHandlers, now: &ClaimTime, lease: Duration, renewal: Renewal<'_>) -> Option<Duration> {
373 return drain_planned_with(git_dir, handlers, now, lease, renewal, &Retrying::none()).wait;
374}
375
376pub fn drain_planned(git_dir: &Path, handlers: &dyn WorkHandlers, now: &ClaimTime, retrying: &Retrying) -> Drained {
379 return drain_planned_with(git_dir, handlers, now, CLAIM_LEASE, Renewal::Timed, retrying);
380}
381
382pub fn drain_planned_with(git_dir: &Path, handlers: &dyn WorkHandlers, now: &ClaimTime, lease: Duration, renewal: Renewal<'_>, retrying: &Retrying) -> Drained {
384 let work_dir: PathBuf = work_directory(git_dir);
385 let mut replanning: Replanning = Replanning { first: retrying.first, changes: Vec::new() };
386 tell_about_given_up(git_dir, requeue_expired_reporting(work_dir.as_path(), now, lease, MAX_TRIES).given_up.as_slice());
387 run_due_push_retries(git_dir, handlers, retrying.due.as_slice(), &mut replanning);
388 drain_pending(git_dir, handlers, lease, renewal, &mut replanning);
389 let wait: Option<Duration> = next_claim_expiry(work_dir.as_path(), &ClaimTime::read(), lease);
390 return Drained { wait, changes: replanning.changes };
391}
392
393pub fn drain_common_records(git_dir: &Path, handlers: &dyn WorkHandlers, now: &ClaimTime) -> Option<Duration> {
409 return drain_common_records_with_lease(git_dir, handlers, now, CLAIM_LEASE);
410}
411
412pub fn drain_common_records_with_lease(git_dir: &Path, handlers: &dyn WorkHandlers, now: &ClaimTime, lease: Duration) -> Option<Duration> {
414 return drain_common_records_planned(git_dir, handlers, now, lease, None).wait;
415}
416
417pub fn drain_common_records_planned(git_dir: &Path, handlers: &dyn WorkHandlers, now: &ClaimTime, lease: Duration, first: Option<Duration>) -> Drained {
421 let Some(common) = common_directory_of(git_dir) else {
422 return Drained::default();
423 };
424 if common == git_dir || !is_bare_common_directory(common.as_path()) || !work_directory(common.as_path()).is_dir() {
425 return Drained::default();
426 }
427 let mut replanning: Replanning = Replanning { first, changes: Vec::new() };
428 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());
429 drain_pending(common.as_path(), handlers, lease, Renewal::Timed, &mut replanning);
430 let wait: Option<Duration> = next_claim_expiry(work_directory(common.as_path()).as_path(), &ClaimTime::read(), lease);
431 return Drained { wait, changes: replanning.changes };
432}
433
434pub(crate) fn common_directory_of(git_dir: &Path) -> Option<PathBuf> {
436 let text: String = std::fs::read_to_string(git_dir.join("commondir")).ok()?;
437 let named: &Path = Path::new(text.trim_end_matches(['\n', '\r']));
438 if named.as_os_str().is_empty() {
439 return None;
440 }
441 let mut resolved: PathBuf = PathBuf::new();
444 for part in git_dir.join(named).components() {
445 match part {
446 std::path::Component::ParentDir => {
447 if !resolved.pop() {
448 return None;
449 }
450 }
451 std::path::Component::CurDir => {}
452 other => resolved.push(other.as_os_str()),
453 }
454 }
455 return Some(resolved);
456}
457
458fn drain_pending(git_dir: &Path, handlers: &dyn WorkHandlers, lease: Duration, renewal: Renewal<'_>, replanning: &mut Replanning) {
461 let work_dir: PathBuf = work_directory(git_dir);
462 let (held, renewer) = held_claims(work_dir.as_path());
463 let _renewing: ClaimRenewal = match renewal {
464 Renewal::Timed => ClaimRenewal::start(renewer, renewal_interval(lease)),
465 Renewal::Driven(driver) => ClaimRenewal::driven(renewer, driver),
466 };
467 loop {
468 let mut claimed: Vec<Claimed> = Vec::new();
469 loop {
470 match claim_next_of(work_dir.as_path(), &ClaimTime::read(), &SERVICE_KINDS) {
473 Ok(Some(item)) => {
474 held.hold(&item);
475 claimed.push(item);
476 }
477 Ok(None) => break,
478 Err(reason) => {
479 crate::service_error!("{reason}");
480 break;
481 }
482 }
483 }
484 if claimed.is_empty() {
485 return;
486 }
487 let (acceptable, refused): (Vec<_>, Vec<_>) = claimed.into_iter().partition(|item| { return is_acceptable(&item.item, git_dir); });
488 settle_all(work_dir.as_path(), &held, &refused, FAILED);
489 let (pushes, rest): (Vec<_>, Vec<_>) = acceptable.into_iter().partition(|item| { return item.item.kind == WorkKind::Push; });
490 let (trashes, after_trash): (Vec<_>, Vec<_>) = rest.into_iter().partition(|item| { return item.item.kind == WorkKind::Trash; });
491 let (reviews, upkeep): (Vec<_>, Vec<_>) = after_trash.into_iter().partition(|item| { return item.item.kind == WorkKind::Review; });
492 let mut groups: Vec<((String, String), Vec<Claimed>)> = Vec::new();
493 for item in pushes {
494 let key = (item.item.repository.clone(), item.item.branch_ref.clone());
495 match groups.iter_mut().find(|(existing, _)| { return *existing == key; }) {
496 Some((_, members)) => members.push(item),
497 None => groups.push((key, vec![item])),
498 }
499 }
500 for (_, claimed_members) in groups {
501 let members: Vec<Claimed> = still_held(&held, work_dir.as_path(), claimed_members);
502 if members.is_empty() {
503 continue;
504 }
505 let items: Vec<WorkItem> = members.iter().map(|member| { return member.item.clone(); }).collect();
506 let pushing_since: std::time::SystemTime = std::time::SystemTime::now();
508 let pushing: Instant = Instant::now();
509 let verdict: Verdict = handlers.push(items.as_slice());
510 if let Some(newest) = members.last() {
511 let (priority, outcome, said): (Priority, &str, &str) = match &verdict {
513 Verdict::Done => (Priority::Info, "done", ""),
514 Verdict::Failed(text) => (Priority::Warning, "failed", text.as_str()),
515 Verdict::Retry(text) => (Priority::Warning, "retrying", text.as_str()),
516 Verdict::Skipped(text) => (Priority::Info, "skipped", text.as_str()),
517 };
518 let fields: [(&str, String); 3] = [
519 ("CLI_GIT_BRANCH", newest.item.branch_ref.clone()),
520 ("CLI_GIT_OID", newest.item.oid.clone()),
521 ("CLI_GIT_OUTCOME", outcome.to_string()),
522 ];
523 log_entry(
524 priority,
525 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(),
526 &fields,
527 );
528 }
529 match verdict {
530 Verdict::Done => {
531 if let Some(newest) = members.last() {
534 clear_push_failures_before(git_dir, newest.item.branch_ref.as_str(), pushing_since);
535 replanning.forget(git_dir, newest.item.branch_ref.as_str());
536 }
537 settle_all(work_dir.as_path(), &held, &members, DONE);
538 }
539 Verdict::Retry(text) => {
540 let said: String = match (members.last(), replanning.first) {
542 (Some(newest), Some(first)) => planned_retry(git_dir, replanning, first, &newest.item, 1, text.as_str()),
543 _ => text,
544 };
545 if let Some(newest) = members.last()
546 && let Err(reason) = record_push_failure(git_dir, newest.item.branch_ref.as_str(), newest.item.oid.as_str(), said.as_str())
547 {
548 crate::service_error!("the failed push of {} could not be recorded for the person: {reason}", newest.item.oid);
549 }
550 settle_all(work_dir.as_path(), &held, &members, FAILED);
551 }
552 Verdict::Failed(text) => {
553 if let Some(newest) = members.last() {
555 replanning.forget(git_dir, newest.item.branch_ref.as_str());
556 }
557 if let Some(newest) = members.last()
559 && let Err(reason) = record_push_failure(git_dir, newest.item.branch_ref.as_str(), newest.item.oid.as_str(), text.as_str())
560 {
561 crate::service_error!("the failed push of {} could not be recorded for the person: {reason}", newest.item.oid);
562 }
563 settle_all(work_dir.as_path(), &held, &members, FAILED);
564 }
565 Verdict::Skipped(text) => {
566 let recorded: bool = match members.last() {
570 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()) {
571 Ok(()) => true,
572 Err(reason) => {
573 crate::service_error!("the skipped push of {} could not be recorded for the person: {reason}", newest.item.oid);
574 false
575 }
576 },
577 _ => true,
578 };
579 settle_all(work_dir.as_path(), &held, &members, if recorded { DONE } else { FAILED });
580 }
581 }
582 }
583 let mut upkept: Vec<String> = Vec::new();
584 for item in still_held(&held, work_dir.as_path(), upkeep) {
585 if !upkept.contains(&item.item.repository) {
586 upkept.push(item.item.repository.clone());
587 let upkeep_began: Instant = Instant::now();
588 match handlers.maintenance(&item.item) {
589 Verdict::Failed(reason) | Verdict::Retry(reason) => crate::service_warn!("automatic maintenance did not run: {reason}"),
590 Verdict::Done | Verdict::Skipped(_) => crate::service_log!("automatic maintenance ran maintenance_ms={}", upkeep_began.elapsed().as_millis()),
591 };
592 }
593 settle_all(work_dir.as_path(), &held, std::slice::from_ref(&item), DONE);
594 }
595 for item in still_held(&held, work_dir.as_path(), trashes) {
598 match handlers.trash(&item.item) {
599 Verdict::Done => settle_all(work_dir.as_path(), &held, std::slice::from_ref(&item), DONE),
600 Verdict::Failed(reason) | Verdict::Retry(reason) => {
601 eprintln!("cli-git-service: the trash {} could not be emptied: {reason}", item.item.parent);
602 settle_all(work_dir.as_path(), &held, std::slice::from_ref(&item), FAILED);
603 }
604 Verdict::Skipped(reason) => {
605 eprintln!("cli-git-service: the trash {} was left as it is: {reason}", item.item.parent);
606 settle_all(work_dir.as_path(), &held, std::slice::from_ref(&item), DONE);
607 }
608 }
609 }
610 for item in still_held(&held, work_dir.as_path(), reviews) {
612 let review_began: Instant = Instant::now();
613 let verdict: Verdict = handlers.review(&item.item);
614 if matches!(verdict, Verdict::Done) {
615 crate::service_log!("the review of {} is done review_ms={}", item.item.oid, review_began.elapsed().as_millis());
616 }
617 match verdict {
618 Verdict::Done => settle_all(work_dir.as_path(), &held, std::slice::from_ref(&item), DONE),
619 Verdict::Failed(reason) | Verdict::Retry(reason) => {
620 crate::service_warn!("the review of {} failed: {reason}", item.item.oid);
621 settle_all(work_dir.as_path(), &held, std::slice::from_ref(&item), FAILED);
622 }
623 Verdict::Skipped(reason) => {
624 crate::service_log!("the review of {} was skipped: {reason}", item.item.oid);
625 settle_all(work_dir.as_path(), &held, std::slice::from_ref(&item), DONE);
626 }
627 }
628 }
629 }
630}
631
632pub fn run_round(wakes: Vec<Wake>, handlers: &GitHandlers, retries: &mut PushRetries, now: &ClaimTime) -> Vec<(PathBuf, Duration)> {
648 return run_round_on(&HostResources::read(), wakes, handlers, retries, now);
649}
650
651pub fn run_round_on(host: &HostResources, wakes: Vec<Wake>, handlers: &GitHandlers, retries: &mut PushRetries, now: &ClaimTime) -> Vec<(PathBuf, Duration)> {
653 let mut directories: Vec<PathBuf> = Vec::new();
654 let mut keys: std::collections::HashMap<PathBuf, ReviewKey> = std::collections::HashMap::new();
655 for (directory, review_key) in wakes {
656 if let Some(key) = review_key {
657 keys.insert(directory.clone(), key);
658 }
659 if !directories.contains(&directory) {
660 directories.push(directory);
661 }
662 }
663 let round: GitHandlers = handlers.for_round(keys);
664 return run_planned_round(host, directories.as_slice(), &round, retries, now);
665}
666
667pub fn run_planned_round<H: WorkHandlers + Sync>(host: &HostResources, directories: &[PathBuf], handlers: &H, retries: &mut PushRetries, now: &ClaimTime) -> Vec<(PathBuf, Duration)> {
681 let taken_at: Instant = Instant::now();
682 let first: Duration = retries.first();
683 let assignments: Vec<Assignment> = directories
684 .iter()
685 .map(|directory| { return Assignment { directory: directory.clone(), retrying: Retrying { first: Some(first), due: retries.take_due(directory.as_path(), taken_at) } }; })
686 .collect();
687 let answered: RoundAnswer = drain_directories(&RoundDrain { directories: assignments.as_slice(), handlers, now, host });
688 let mut named: Vec<PathBuf> = directories.to_vec();
689 for change in &answered.changes {
690 if let RetryChange::Plan { git_dir, .. } = change
691 && !named.contains(git_dir)
692 {
693 named.push(git_dir.clone());
694 }
695 }
696 retries.apply(answered.changes);
697 let applied_at: Instant = Instant::now();
698 let mut asked: Vec<(PathBuf, Duration)> = Vec::new();
699 for directory in named {
700 let lease: Option<Duration> = answered.waits.iter().find(|(named_directory, _)| { return *named_directory == directory; }).map(|(_, wait)| { return *wait; });
701 let planned: Option<Duration> = retries.next_wait(directory.as_path(), applied_at);
702 if let Some(wait) = [lease, planned].into_iter().flatten().min() {
703 asked.push((directory, wait));
704 }
705 }
706 return asked;
707}
708
709pub struct GitHandlers {
711 pub context: GitContext,
713 review_keys: std::collections::HashMap<PathBuf, ReviewKey>,
717 #[cfg(test)]
719 review_endpoint: Option<String>,
720 #[cfg(test)]
722 push_environment: Vec<(std::ffi::OsString, std::ffi::OsString)>,
723}
724
725impl GitHandlers {
727 pub fn new(context: GitContext) -> GitHandlers {
729 return GitHandlers {
730 context,
731 review_keys: std::collections::HashMap::new(),
732 #[cfg(test)]
733 review_endpoint: None,
734 #[cfg(test)]
735 push_environment: Vec::new(),
736 };
737 }
738
739 #[cfg(test)]
741 pub fn with_push_environment(mut self, variables: &[(&str, &str)]) -> GitHandlers {
742 self.push_environment = variables.iter().map(|(name, value)| { return (std::ffi::OsString::from(name), std::ffi::OsString::from(value)); }).collect();
743 return self;
744 }
745
746 #[cfg(test)]
748 pub fn with_review_endpoint(mut self, endpoint: &str) -> GitHandlers {
749 self.review_endpoint = Some(endpoint.to_string());
750 return self;
751 }
752
753 pub fn for_round(&self, keys: std::collections::HashMap<PathBuf, ReviewKey>) -> GitHandlers {
757 return GitHandlers {
758 context: self.context.clone(),
759 review_keys: keys,
760 #[cfg(test)]
761 review_endpoint: self.review_endpoint.clone(),
762 #[cfg(test)]
763 push_environment: self.push_environment.clone(),
764 };
765 }
766
767 fn review_key_of(&self, git_dir: &Path) -> Option<ReviewKey> {
769 return self.review_keys.get(git_dir).cloned();
770 }
771
772 fn push_git(&self, item: &WorkItem) -> PushGit {
773 let cwd: PathBuf = PathBuf::from(item.repository.as_str());
774 #[cfg_attr(not(test), expect(unused_mut, reason = "only a test adds variables"))]
777 let mut inherited: Vec<(std::ffi::OsString, std::ffi::OsString)> = std::env::vars_os().collect();
778 #[cfg(test)]
779 inherited.extend(self.push_environment.iter().cloned());
780 return PushGit {
781 real_git: self.context.real_git.clone(),
782 lfs: read_config_snapshot(&self.context, cwd.as_path()).as_ref().is_none_or(uses_lfs),
783 cwd,
784 environment: environment_with_overlay(inherited.as_slice(), lockfile_pid_overlay(inherited.as_slice()).as_slice()),
785 };
786 }
787}
788
789impl WorkHandlers for GitHandlers {
791 fn push(&self, items: &[WorkItem]) -> Verdict {
792 let Some(newest) = items.last() else { return Verdict::Done };
793 let git: PushGit = self.push_git(newest);
794 let mut output: Vec<u8> = Vec::new();
795 return match push_once(&git, newest.branch_ref.as_str(), newest.oid.as_str(), &mut output) {
796 Ok(result) if result.outcome == AutoPushOutcome::Skipped => Verdict::Skipped(result.shown),
799 Ok(result) if result.exit_code == 0 => Verdict::Done,
800 Ok(result) => match final_failure(result.shown.as_str()) {
803 Some(cause) => Verdict::Failed(match plain_words(cause) {
804 Some(words) => format!("{words}\n{}", result.shown),
805 None => result.shown,
806 }),
807 None => {
808 if let Some(line) = unrecognised_rejection(result.shown.as_str()) {
810 crate::service_warn!("the server rejected the push for a reason this release does not know, so it is tried again: {line}");
811 }
812 Verdict::Retry(result.shown)
813 }
814 },
815 Err(error) => Verdict::Retry(format!("{error}")),
816 };
817 }
818
819 fn trash(&self, item: &WorkItem) -> Verdict {
820 return crate::worktree_trash::empty_trash(item);
821 }
822
823 fn maintenance(&self, item: &WorkItem) -> Verdict {
824 return match run_auto_maintenance_with(&self.context, Path::new(item.repository.as_str()), None) {
825 Ok(AutoMaintenanceOutcome::Exited(code)) if code != 0 => Verdict::Failed(format!("it exited with status {code}")),
826 Ok(_) => Verdict::Done,
827 Err(error) => Verdict::Failed(format!("{error:?}")),
828 };
829 }
830
831 fn review(&self, item: &WorkItem) -> Verdict {
832 let key: Option<ReviewKey> = self.review_key_of(Path::new(item.git_dir.as_str()));
833 #[cfg_attr(not(test), expect(unused_mut, reason = "only a test replaces the endpoint"))]
834 let mut options: ReviewOptions = options_for(item, &review_variables(key.as_ref()));
835 #[cfg(test)]
836 if let Some(endpoint) = self.review_endpoint.as_ref() {
837 options.endpoint = endpoint.clone();
838 }
839 return review_item(&self.context, item, options);
840 }
841}
842
843#[cfg(test)]
845#[path = "service_work_tests.rs"]
846mod tests;
847
848#[cfg(test)]
850#[path = "service_push_retry_tests.rs"]
851mod push_retry_tests;