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