Skip to main content

git_policy_cli/
service_handoff.rs

1//! What:
2//!  The wrapper's side of after-commit work: leave the push and upkeep records for a commit the service landed and wake
3//!  the service.
4//! Why:
5//!  The commit returns as soon as it is saved. The push (which can take as long as the network does) and the upkeep run
6//!  in the service afterwards; a push that fails is reported by the person's next wrapper command (`push_failure_notice.rs`)
7//!  (`doc/planning/cli-git-resident-service.md`, "After-commit work"). The wrapper starts no program and waits for no
8//!  one: it writes whole files and sends one message.
9//!
10//! In TS you'd write (pseudocode):
11//! ```ts
12//! await enqueue(workDir, push); await enqueue(workDir, maintenance); await wake(commonDir);
13//! ```
14
15use super::iso_time::now_iso;
16use super::service_client::{ClientConfig, wake};
17use super::service_messages::ClientFailure;
18use super::service_work::work_directory;
19use super::service_review_key::{ReviewKey, key_from_environment};
20use super::standards_review_input::has_shadow_log;
21use super::work_item::{WorkItem, WorkKind, enqueue};
22use std::io::Write;
23use std::path::PathBuf;
24
25/// What the wrapper knows about a commit the service landed.
26#[derive(Clone, Debug)]
27pub struct ServiceHandoff {
28    /// How to reach the service.
29    pub config: ClientConfig,
30    /// The worktree root.
31    pub repository: PathBuf,
32    /// The worktree's own Git directory, which holds the work records and names the service instance.
33    pub git_dir: PathBuf,
34    /// The branch the commit went to.
35    pub reference: String,
36    /// The branch value the commit was made on.
37    pub parent: String,
38}
39
40/// The kinds of work a landed commit leaves for the service.
41pub const HANDED_OVER: [WorkKind; 2] = [WorkKind::Push, WorkKind::Maintenance];
42
43/// What:
44///  Writes the work records for a landed commit and wakes the service.
45/// Why:
46///   The records are written before the message is sent, so a service that wakes always finds them; records that were
47///  written but not announced stay pending and are run by the next wake for the repository.
48///
49/// In TS you'd write (pseudocode):
50/// ```ts
51/// await handOver(handoff, landed)
52/// ```
53pub fn hand_over(handoff: &ServiceHandoff, landed: &str, notices: &mut dyn Write) -> Result<(), ClientFailure> {
54    let key: Option<ReviewKey> = key_from_environment(&|name| { return std::env::var(name).ok(); });
55    return hand_over_with_key(handoff, landed, key.as_ref(), notices);
56}
57
58/// What:
59///  `hand_over` with the standards-review credential given. The records written to disk never hold it; the wake message
60///  carries it to the service, which keeps it in memory for the review.
61/// Why:
62///   A systemd user service does not inherit the wrapper's environment, and the credential must not be stored in a work
63///  item, a unit file or the manager's environment. The wrapper therefore sends it over the user-only socket.
64///
65/// In TS you'd write (pseudocode):
66/// ```ts
67/// await handOverWithKey(handoff, landed, key)
68/// ```
69pub fn hand_over_with_key(handoff: &ServiceHandoff, landed: &str, review_key: Option<&ReviewKey>, notices: &mut dyn Write) -> Result<(), ClientFailure> {
70    let work_dir: PathBuf = work_directory(handoff.git_dir.as_path());
71    // A review is left only where the repository keeps a shadow log; the file is the repository's own switch.
72    let review: &[WorkKind] = if has_shadow_log(handoff.repository.as_path()) { &[WorkKind::Review] } else { &[] };
73    for kind in HANDED_OVER.iter().chain(review.iter()).copied() {
74        let item: WorkItem = WorkItem {
75            kind,
76            repository: handoff.repository.display().to_string(),
77            git_dir: handoff.git_dir.display().to_string(),
78            branch_ref: handoff.reference.clone(),
79            oid: landed.to_string(),
80            parent: handoff.parent.clone(),
81            created: now_iso(),
82            tries: 0,
83        };
84        enqueue(work_dir.as_path(), &item).map_err(ClientFailure::WorkNotHandedOver)?;
85    }
86    return wake(&handoff.config, handoff.git_dir.as_path(), review_key, notices);
87}
88
89/// What:
90///  Writes the push record for a branch tip that a command other than a commit moved, and wakes the service.
91/// Why:
92///   A merge, a cherry-pick, a revert, an applied patch or a pull's merge adds commits that the next commit would otherwise
93///  be the first to push. Only the push is left: the review and the upkeep belong to a commit.
94///
95/// In TS you'd write (pseudocode):
96/// ```ts
97/// await handOverPush(handoff, tip)
98/// ```
99pub fn hand_over_push(handoff: &ServiceHandoff, tip: &str, notices: &mut dyn Write) -> Result<(), ClientFailure> {
100    let item: WorkItem = WorkItem {
101        kind: WorkKind::Push,
102        repository: handoff.repository.display().to_string(),
103        git_dir: handoff.git_dir.display().to_string(),
104        branch_ref: handoff.reference.clone(),
105        oid: tip.to_string(),
106        parent: handoff.parent.clone(),
107        created: now_iso(),
108        tries: 0,
109    };
110    enqueue(work_directory(handoff.git_dir.as_path()).as_path(), &item).map_err(ClientFailure::WorkNotHandedOver)?;
111    return wake(&handoff.config, handoff.git_dir.as_path(), None, notices);
112}
113
114/// Handoff controls stay out of the release executable.
115#[cfg(test)]
116#[path = "service_handoff_tests.rs"]
117mod tests;