Skip to main content

git_policy_cli/
service_messages.rs

1//! What:
2//!  Everything the wrapper tells the user when it cannot get a commit landed by the resident service, and the notices
3//!  it prints while it waits.
4//! Why:
5//!  A failure the system can resolve is resolved without the user; the rest ends the command with plain words, the
6//!  affected input named, every remedy given, and no implementation term (`doc/planning/cli-git-resident-service.md`,
7//!  "Failures the client can meet"). The texts live here as constants so a test can check each against a list of terms
8//!  that must never reach a user, and so the text a client prints cannot drift from the text the tests read.
9//!
10//! In TS you'd write (pseudocode):
11//! ```ts
12//! stderr.write(render({ kind: 'not-installed' }));
13//! ```
14
15use std::path::Path;
16
17/// The command that shows why the service did not start (Linux with systemd).
18pub const LOG_COMMAND: &str = "journalctl --user --unit cli-git.service";
19
20/// The command that turns on user services for an account that is not logged in (Linux with systemd).
21pub const LINGER_COMMAND: &str = "loginctl enable-linger";
22
23/// The commands that show whether the commit was made.
24pub const CHECK_COMMANDS: [&str; 2] = ["git log -1", "git status"];
25
26/// Printed once when the service takes longer than the grace to answer.
27pub const STARTING_NOTICE: &str = "Starting the commit service...";
28
29/// Printed once when the service is being replaced by a newer one.
30pub const UPDATING_NOTICE: &str = "Updating the commit service...";
31
32/// What:
33///  Where the client cannot get a service from the operating system.
34/// Why:
35///   Each platform has its own remedy, and none has a second way to commit.
36///
37/// In TS you'd write (pseudocode):
38/// ```ts
39/// type Platform = 'windows' | 'bsd' | 'linux-without-user-services' | 'other';
40/// ```
41#[derive(Clone, Copy, Debug, Eq, PartialEq)]
42pub enum Platform {
43    /// Windows, where activation is designed but not built.
44    Windows,
45    /// A BSD system.
46    Bsd,
47    /// Linux with no user service manager (minimal containers, WSL 1, other init systems).
48    LinuxWithoutUserServices,
49    /// Any other system.
50    Other,
51}
52
53/// What:
54///  Every failure that reaches the user, with the facts its text names.
55/// Why:
56///   The set is closed, so a test renders all of them.
57///
58/// In TS you'd write (pseudocode):
59/// ```ts
60/// type ClientFailure = { kind: 'not-installed' } | { kind: 'did-not-start' } | ...;
61/// ```
62#[derive(Clone, Debug, Eq, PartialEq)]
63pub enum ClientFailure {
64    /// The service was started and did not come up.
65    DidNotStart,
66    /// The platform cannot start the service on demand.
67    NoActivation(Platform),
68    /// The service on the socket belongs to another account.
69    OtherAccount,
70    /// The service stopped before it confirmed the commit.
71    MayNotHaveLanded,
72    /// The installed service speaks no protocol this wrapper speaks, after a replacement attempt.
73    NoCommonVersion,
74    /// The repository has no room left.
75    RepositoryFull(String),
76    /// The repository cannot be written.
77    RepositoryReadOnly(String),
78    /// The branch changed while the commit was prepared, `times` times.
79    BranchKeepsChanging { branch: String, times: usize },
80    /// The commit waited `minutes` minutes behind others.
81    WaitedTooLong { minutes: u64 },
82    /// The request was refused as malformed or unsafe.
83    Refused,
84    /// Another program holds the repository's index.
85    IndexInUse(String),
86    /// The service failed in a way it could not resolve.
87    Unavailable,
88    /// The service program is not next to the wrapper.
89    ProgramMissing(String),
90    /// Setting up the service failed for a reason the person can act on.
91    CouldNotStart(String),
92    /// A unit file of the same name that something else wrote stands in the way.
93    UnitInTheWay(String),
94    /// The commit is saved but the backup push and upkeep could not be handed over.
95    WorkNotHandedOver(String),
96}
97
98/// The sentence every failure ends with when nothing was changed.
99const NOTHING_CHANGED: &str = "Nothing in the repository changed.";
100
101/// What:
102///  The text for a failure.
103/// Why:
104///   Plain words, the input named, every remedy; nothing about how the service works.
105///
106/// In TS you'd write (pseudocode):
107/// ```ts
108/// render(failure): string
109/// ```
110pub fn render(failure: &ClientFailure) -> String {
111    return match failure {
112        ClientFailure::DidNotStart => format!(
113            "The commit service did not start, so this commit was not made. Its log shows why: `{LOG_COMMAND}`. \
114             Run the commit again after fixing that. {NOTHING_CHANGED}"
115        ),
116        ClientFailure::NoActivation(platform) => format!(
117            "This computer cannot start the commit service on demand, so this commit was not made. {} {NOTHING_CHANGED}",
118            platform_remedy(*platform)
119        ),
120        ClientFailure::OtherAccount => format!(
121            "Another account runs the commit service here, so this commit was not made. Use your own account, or ask \
122             the other account to stop its service, then run the commit again. {NOTHING_CHANGED}"
123        ),
124        ClientFailure::MayNotHaveLanded => format!(
125            "The commit service stopped before it confirmed this commit. Run `{}` to see whether it was made, and `{}` \
126             to see what is still staged. If it was not made, run the commit again.",
127            CHECK_COMMANDS[0], CHECK_COMMANDS[1]
128        ),
129        ClientFailure::NoCommonVersion => format!(
130            "This git is older than the installed commit service and cannot work with it, so this commit was not made. \
131             Update the git wrapper with the tool that installed it, and run the commit again. {NOTHING_CHANGED}"
132        ),
133        ClientFailure::RepositoryFull(path) => format!(
134            "There is no space left for {path}, so this commit was not made. Free some space and run the commit again. \
135             {NOTHING_CHANGED}"
136        ),
137        ClientFailure::RepositoryReadOnly(path) => format!(
138            "{path} cannot be written to, so this commit was not made. Make the repository writable (check its \
139             permissions and whether the disk is mounted read-only) and run the commit again. {NOTHING_CHANGED}"
140        ),
141        ClientFailure::BranchKeepsChanging { branch, times } => format!(
142            "The branch {branch} changed {times} times while this commit was being prepared, so it was not made. Run \
143             the commit again; if other people commit to the same branch at the same moment, it will succeed once they \
144             pause. {NOTHING_CHANGED}"
145        ),
146        ClientFailure::WaitedTooLong { minutes } => format!(
147            "This commit waited {minutes} minutes behind other commits and was not made. Run it again later, or raise \
148             the waiting limit in the settings for the wrapper. {NOTHING_CHANGED}"
149        ),
150        ClientFailure::Refused => format!(
151            "The commit service refused this commit because it names a path or a value that cannot be committed, so it \
152             was not made. Check the paths you passed to the commit. {NOTHING_CHANGED}"
153        ),
154        ClientFailure::IndexInUse(holder) => format!(
155            "{holder} is using this repository's index, so this commit was not made. Wait for it to finish, or close \
156             it, and run the commit again. {NOTHING_CHANGED}"
157        ),
158        ClientFailure::ProgramMissing(path) => format!(
159            "The commit service program was not found at {path}, so this commit was not made. Reinstall the git wrapper \
160             with the tool that installed it, so that the program sits next to it, then run the commit again. \
161             {NOTHING_CHANGED}"
162        ),
163        ClientFailure::UnitInTheWay(path) => format!(
164            "The file `{path}` has the name of a settings file of the commit service but was not written by the git wrapper, \
165             so the commit service could not be set up and this commit was not made. Move or delete that file, then run the \
166             commit again. {NOTHING_CHANGED}"
167        ),
168        ClientFailure::WorkNotHandedOver(reason) => format!(
169            "Your commit is saved. Its backup push and upkeep could not be handed to the commit service: {reason}. \
170             Run `git push` when ready."
171        ),
172        ClientFailure::CouldNotStart(reason) => format!(
173            "The commit service could not be set up, so this commit was not made: {reason}. Fix that and run the commit \
174             again. {NOTHING_CHANGED}"
175        ),
176        ClientFailure::Unavailable => format!(
177            "The commit service could not finish this commit, so it was not made. Run the commit again; if it fails \
178             again, look at the service's log with `{LOG_COMMAND}`. {NOTHING_CHANGED}"
179        ),
180    };
181}
182
183/// What:
184///  The remedy sentence for a platform.
185/// Why:
186///   Windows and the BSD systems have no activation yet; Linux without user services can turn them on.
187///
188/// In TS you'd write (pseudocode):
189/// ```ts
190/// platformRemedy(platform)
191/// ```
192pub fn platform_remedy(platform: Platform) -> String {
193    return match platform {
194        Platform::Windows => String::from(
195            "Committing through this wrapper is not supported on Windows yet. Use the Linux or macOS machine for commits \
196             to this repository.",
197        ),
198        Platform::Bsd => String::from(
199            "Committing through this wrapper is not supported on this system yet. Use a Linux or macOS machine for \
200             commits to this repository.",
201        ),
202        Platform::LinuxWithoutUserServices => format!(
203            "This account has no user service manager. Turn it on with `{LINGER_COMMAND}` (the system must run \
204             systemd), or commit from a system that does."
205        ),
206        Platform::Other => String::from(
207            "Committing through this wrapper is not supported on this system. Use a Linux or macOS machine for \
208             commits to this repository.",
209        ),
210    };
211}
212
213/// What:
214///  The notice for a commit that waits behind others.
215/// Why:
216///   The position is the one fact the person needs: how many commits go first.
217///
218/// In TS you'd write (pseudocode):
219/// ```ts
220/// positionNotice(number, of)
221/// ```
222pub fn position_notice(number: usize, of: usize) -> String {
223    let ahead: usize = number.saturating_sub(1);
224    let _ = of;
225    return match ahead {
226        0 => String::from("This commit is next."),
227        1 => String::from("Waiting for 1 earlier commit."),
228        many => format!("Waiting for {many} earlier commits."),
229    };
230}
231
232/// What:
233///  The notice for a backup push that failed after its commit was saved.
234/// Why:
235///   The push runs after the commit returned, so the person learns of a failure at their next commit. The service has
236///  its own environment, which is the first thing to check when a push that works in a terminal fails here.
237///
238/// In TS you'd write (pseudocode):
239/// ```ts
240/// pushFailedNotice(commit, said)
241/// ```
242pub fn push_failed_notice(commit: &str, said: &str) -> String {
243    let short: &str = commit.get(..7).unwrap_or(commit);
244    let detail: String = if said.is_empty() { String::from("Git gave no reason.") } else { format!("Git said: {said}") };
245    return format!(
246        "The backup push of your commit {short} failed, so it exists only on this computer for now. {detail} The commit \
247         service runs with its own settings, not your terminal's, so a push that works in your terminal can fail here \
248         when it needs a variable your terminal has. Run `git push` when ready."
249    );
250}
251
252/// The texts that must never reach a user: implementation terms the system should have resolved itself.
253///  Matched case-insensitively against whole words after the quoted commands are removed.
254pub const DENYLIST: [&str; 22] = [
255    "protocol", "journal", "request id", "daemon", "socket", "errno", "epipe", "econnrefused", "eagain", "flock",
256    "lock file", "peer", "frame", "handshake", "batch", "oid", "fast-import", "unix domain", "abstract", "pid",
257    "uid", "os error",
258];
259
260/// The repository path a failure names, as text.
261pub fn path_text(path: &Path) -> String {
262    return path.display().to_string();
263}
264
265/// Message controls stay out of the release executable.
266#[cfg(test)]
267#[path = "service_messages_tests.rs"]
268mod tests;