Skip to main content

git_policy_cli/landing/
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 command that restarts the commit service, so the manager starts the one the installed units name (Linux with systemd).
24pub const RESTART_COMMAND: &str = "systemctl --user restart 'cli-git@*.service'";
25
26/// The commands that show whether the commit was made.
27pub const CHECK_COMMANDS: [&str; 2] = ["git log -1", "git status"];
28
29/// Printed once when the service takes longer than the grace to answer.
30pub const STARTING_NOTICE: &str = "Starting the commit service...";
31
32/// Printed each time the service goes silent while a commit waits, before the same request is sent to it again.
33pub const SILENT_NOTICE: &str = "The commit service stopped answering; asking it again with the same commit, which cannot land twice.";
34
35/// Printed once when the service is being replaced by a newer one.
36pub const UPDATING_NOTICE: &str = "Updating the commit service...";
37
38/// What:
39///  Where the client cannot get a service from the operating system.
40/// Why:
41///   Each platform has its own remedy, and none has a second way to commit.
42///
43/// In TS you'd write (pseudocode):
44/// ```ts
45/// type Platform = 'windows' | 'bsd' | 'linux-without-user-services' | 'other';
46/// ```
47#[derive(Clone, Copy, Debug, Eq, PartialEq)]
48pub enum Platform {
49    /// Windows, where activation is designed but not built.
50    Windows,
51    /// A BSD system.
52    Bsd,
53    /// Linux with no user service manager (minimal containers, WSL 1, other init systems).
54    LinuxWithoutUserServices,
55    /// Any other system.
56    Other,
57}
58
59/// What:
60///  Every failure that reaches the user, with the facts its text names.
61/// Why:
62///   The set is closed, so a test renders all of them.
63///
64/// In TS you'd write (pseudocode):
65/// ```ts
66/// type ClientFailure = { kind: 'not-installed' } | { kind: 'did-not-start' } | ...;
67/// ```
68#[derive(Clone, Debug, Eq, PartialEq)]
69pub enum ClientFailure {
70    /// The service was started and did not come up.
71    DidNotStart,
72    /// The platform cannot start the service on demand.
73    NoActivation(Platform),
74    /// The service on the socket belongs to another account.
75    OtherAccount,
76    /// The service stopped before it confirmed the commit.
77    MayNotHaveLanded,
78    /// The installed service speaks no protocol this wrapper speaks, after a replacement attempt.
79    NoCommonVersion,
80    /// A service started from another release of the wrapper is still running after it was asked to exit.
81    OtherRelease,
82    /// The repository has no room left.
83    RepositoryFull(String),
84    /// The repository cannot be written.
85    RepositoryReadOnly(String),
86    /// The branch changed while the commit was prepared, `times` times.
87    BranchKeepsChanging { branch: String, times: usize },
88    /// The branch was no longer where the amended commit was prepared, so the amend was not made.
89    BranchMoved { branch: String },
90    /// The branch moved while the person wrote a message; the message is saved in `file` for them to reuse.
91    BranchMovedMessageSaved { branch: String, file: String, amend: bool },
92    /// The worktree switched from `branch` to `now` while the commit was prepared; the message the person wrote, if any, is
93    ///  saved in `file`.
94    BranchSwitched { branch: String, now: String, file: Option<String> },
95    /// The service sent nothing for longer than it ever stays silent while it works, on every try to ask it again.
96    ServiceSilent,
97    /// The service kept the request for `minutes` minutes without answering, and the client withdrew it: it will not land.
98    StillWithService { minutes: u64, blocker: Option<String> },
99    /// The client stopped waiting and could not confirm the service withdrew the request; `blocker` is what the service last said it
100    ///  was stuck in, such as a branch write that has not returned.
101    WithdrawalUnconfirmed { blocker: Option<String> },
102    /// A Git step of the landing ran longer than the wrapper allows and was ended; the commit was withdrawn and kept.
103    StepTooSlow,
104    /// The request was refused as malformed or unsafe.
105    Refused,
106    /// Another program holds the repository's index.
107    IndexInUse(String),
108    /// The service failed in a way it could not resolve.
109    Unavailable,
110    /// The service program is not next to the wrapper.
111    ProgramMissing(String),
112    /// Setting up the service failed for a reason the person can act on.
113    CouldNotStart(String),
114    /// A unit file of the same name that something else wrote stands in the way.
115    UnitInTheWay(String),
116    /// The commit is saved but the backup push and upkeep could not be handed over.
117    WorkNotHandedOver(String),
118    /// The content prepared for the commit was removed from the repository before the commit landed, twice.
119    ContentPruned,
120}
121
122/// The sentence every failure ends with when nothing was changed.
123pub const NOTHING_CHANGED: &str = "Nothing in the repository changed.";
124
125/// What:
126///  The text for a failure.
127/// Why:
128///   Plain words, the input named, every remedy; nothing about how the service works.
129///
130/// In TS you'd write (pseudocode):
131/// ```ts
132/// render(failure): string
133/// ```
134pub fn render(failure: &ClientFailure) -> String {
135    return match failure {
136        ClientFailure::DidNotStart => format!(
137            "The commit service did not start, so this commit was not made. Its log shows why: `{LOG_COMMAND}`. \
138             Run the commit again after fixing that. {NOTHING_CHANGED}"
139        ),
140        ClientFailure::NoActivation(platform) => format!(
141            "This computer cannot start the commit service on demand, so this commit was not made. {} {NOTHING_CHANGED}",
142            platform_remedy(*platform)
143        ),
144        ClientFailure::OtherAccount => format!(
145            "Another account runs the commit service here, so this commit was not made. Use your own account, or ask \
146             the other account to stop its service, then run the commit again. {NOTHING_CHANGED}"
147        ),
148        ClientFailure::MayNotHaveLanded => format!(
149            "The commit service stopped before it confirmed this commit. Run `{}` to see whether it was made, and `{}` \
150             to see what is still staged. If it was not made, run the commit again.",
151            CHECK_COMMANDS[0], CHECK_COMMANDS[1]
152        ),
153        ClientFailure::NoCommonVersion => format!(
154            "This git is older than the installed commit service and cannot work with it, so this commit was not made. \
155             Update the git wrapper with the tool that installed it, and run the commit again. {NOTHING_CHANGED}"
156        ),
157        ClientFailure::OtherRelease => format!(
158            "The commit service that is running was started from another release of this git wrapper and did not stop when \
159             asked, so this commit was not made. Restart it with `{RESTART_COMMAND}`, and run the commit again. {NOTHING_CHANGED}"
160        ),
161        ClientFailure::RepositoryFull(path) => format!(
162            "There is no space left for {path}, so this commit was not made. Free some space and run the commit again. \
163             {NOTHING_CHANGED}"
164        ),
165        ClientFailure::RepositoryReadOnly(path) => format!(
166            "{path} cannot be written to, so this commit was not made. Make the repository writable (check its \
167             permissions and whether the disk is mounted read-only) and run the commit again. {NOTHING_CHANGED}"
168        ),
169        ClientFailure::BranchKeepsChanging { branch, times } => format!(
170            "The branch {branch} changed {times} times while this commit was being prepared, so it was not made. Run \
171             the commit again; if other people commit to the same branch at the same moment, it will succeed once they \
172             pause. {NOTHING_CHANGED}"
173        ),
174        ClientFailure::BranchMoved { branch } => format!(
175            "The branch {branch} moved while this commit was being prepared, and a commit that replaces the last one, or one \
176             that was written or signed by hand, cannot be moved onto a different last commit, so it was not made. Run the \
177             commit again to make it on the branch as it is now. {NOTHING_CHANGED}"
178        ),
179        ClientFailure::BranchMovedMessageSaved { branch, file, amend } => format!(
180            "The branch {branch} moved while this commit was being prepared, so it was not made. The message you wrote is saved \
181             in {file}. Run `git commit {}-F {file}` to make the commit on the branch as it is now. {NOTHING_CHANGED}",
182            if *amend { "--amend " } else { "" }
183        ),
184        ClientFailure::BranchSwitched { branch, now, file } => format!(
185            "The worktree switched from {branch} to {now} while this commit was being prepared, so it was not made. {} Switch back \
186             to {branch} and run the commit again, or run it here to commit on {now}. {NOTHING_CHANGED}",
187            match file {
188                Some(saved) => format!("The message you wrote is saved in {saved}; `git commit -F {saved}` uses it."),
189                None => String::new(),
190            }
191        ),
192        ClientFailure::ServiceSilent => format!(
193            "The commit service stopped answering while this commit waited, and it did not answer when asked again, so it is \
194             not known whether the commit was made. Run `{}` to see whether it was made, and `{}` to see what is still \
195             staged. If it was not made, look at why the service is stuck with `{LOG_COMMAND}`, restart it with \
196             `{RESTART_COMMAND}`, and run the commit again.",
197            CHECK_COMMANDS[0], CHECK_COMMANDS[1]
198        ),
199        ClientFailure::StillWithService { minutes, blocker } => format!(
200            "This commit waited {minutes} minutes for the commit service and was withdrawn, so it will not land later by itself.{} \
201             Your staged changes and your worktree are as they were. Run the commit again; if the service is stuck, look at why with \
202             `{LOG_COMMAND}` and restart it with `{RESTART_COMMAND}`. {NOTHING_CHANGED}",
203            blocker.as_ref().map_or_else(String::new, |text| { return format!(" It was waiting for: {text}."); })
204        ),
205        ClientFailure::WithdrawalUnconfirmed { blocker } => format!(
206            "This commit waited for the commit service until the longest wait the wrapper allows, and the service did not confirm \
207             that it withdrew it, so it is not known whether the commit was made.{} Run `{}` to see whether it was made, and `{}` to \
208             see what is still staged. If it was not made, run the commit again; if the service is stuck, look at why with \
209             `{LOG_COMMAND}` and restart it with `{RESTART_COMMAND}`.",
210            blocker.as_ref().map_or_else(String::new, |text| { return format!(" The service had said: {}", text.trim()); }),
211            CHECK_COMMANDS[0], CHECK_COMMANDS[1]
212        ),
213        ClientFailure::StepTooSlow => format!(
214            "A Git step of this commit ran longer than the commit service allows and was ended, so this commit was not made and will not \
215             land later by itself. The step is named above, and the commit built for you is kept on the side ref named there when one \
216             is. Run the commit again; if it happens again, look at why with `{LOG_COMMAND}`. {NOTHING_CHANGED}"
217        ),
218        ClientFailure::Refused => format!(
219            "The commit service refused this commit because it names a path or a value that cannot be committed, so it \
220             was not made. Check the paths you passed to the commit. {NOTHING_CHANGED}"
221        ),
222        ClientFailure::IndexInUse(holder) => format!(
223            "{holder} is using this repository's index, so this commit was not made. Wait for it to finish, or close \
224             it, and run the commit again. {NOTHING_CHANGED}"
225        ),
226        ClientFailure::ProgramMissing(path) => format!(
227            "The commit service program was not found at {path}, so this commit was not made. Reinstall the git wrapper \
228             with the tool that installed it, so that the program sits next to it, then run the commit again. \
229             {NOTHING_CHANGED}"
230        ),
231        ClientFailure::UnitInTheWay(path) => format!(
232            "The file `{path}` has the name of a settings file of the commit service but was not written by the git wrapper, \
233             so the commit service could not be set up and this commit was not made. Move or delete that file, then run the \
234             commit again. {NOTHING_CHANGED}"
235        ),
236        ClientFailure::WorkNotHandedOver(reason) => format!(
237            "Your commit is saved. Its backup push and upkeep could not be handed to the commit service: {reason}. \
238             Run `git push` when ready."
239        ),
240        ClientFailure::ContentPruned => format!(
241            "The files of this commit were removed from the repository while the commit was being made, so it was not made. \
242             Git removes content that no branch or index refers to yet when `git gc --prune=now` or `git prune` runs in the \
243             same repository, and Git's documentation says that `--prune=now` risks failures and corruption when another \
244             process writes to the repository at the same moment. Run the commit again once the cleanup has finished, and avoid `--prune=now` while commits are \
245             being made (the default `git gc` keeps new content for two weeks). If nothing like that was running, run \
246             `git fsck` to check the repository. {NOTHING_CHANGED}"
247        ),
248        ClientFailure::CouldNotStart(reason) => format!(
249            "The commit service could not be set up, so this commit was not made: {reason}. Fix that and run the commit \
250             again. {NOTHING_CHANGED}"
251        ),
252        ClientFailure::Unavailable => format!(
253            "The commit service could not finish this commit, so it was not made. Run the commit again; if it fails \
254             again, look at the service's log with `{LOG_COMMAND}`. {NOTHING_CHANGED}"
255        ),
256    };
257}
258
259/// What:
260///  The remedy sentence for a platform.
261/// Why:
262///   Windows and the BSD systems have no activation yet; Linux without user services can turn them on.
263///
264/// In TS you'd write (pseudocode):
265/// ```ts
266/// platformRemedy(platform)
267/// ```
268pub fn platform_remedy(platform: Platform) -> String {
269    return match platform {
270        Platform::Windows => String::from(
271            "Committing through this wrapper is not supported on Windows yet. Use the Linux or macOS machine for commits \
272             to this repository.",
273        ),
274        Platform::Bsd => String::from(
275            "Committing through this wrapper is not supported on this system yet. Use a Linux or macOS machine for \
276             commits to this repository.",
277        ),
278        Platform::LinuxWithoutUserServices => format!(
279            "This account has no user service manager. Turn it on with `{LINGER_COMMAND}` (the system must run \
280             systemd), or commit from a system that does."
281        ),
282        Platform::Other => String::from(
283            "Committing through this wrapper is not supported on this system. Use a Linux or macOS machine for \
284             commits to this repository.",
285        ),
286    };
287}
288
289/// What:
290///  The notice for a commit that waits behind others.
291/// Why:
292///   The position is the one fact the person needs: how many commits go first.
293///
294/// In TS you'd write (pseudocode):
295/// ```ts
296/// positionNotice(number, of)
297/// ```
298pub fn position_notice(number: usize, of: usize) -> String {
299    let ahead: usize = number.saturating_sub(1);
300    let _ = of;
301    return match ahead {
302        0 => String::from("This commit is next."),
303        1 => String::from("Waiting for 1 earlier commit."),
304        many => format!("Waiting for {many} earlier commits."),
305    };
306}
307
308/// The texts that must never reach a user: implementation terms the system should have resolved itself.
309///  Matched case-insensitively against whole words after the quoted commands are removed.
310pub const DENYLIST: [&str; 22] = [
311    "protocol", "journal", "request id", "daemon", "socket", "errno", "epipe", "econnrefused", "eagain", "flock",
312    "lock file", "peer", "frame", "handshake", "batch", "oid", "fast-import", "unix domain", "abstract", "pid",
313    "uid", "os error",
314];
315
316/// The repository path a failure names, as text.
317pub fn path_text(path: &Path) -> String {
318    return path.display().to_string();
319}
320
321/// Message controls stay out of the release executable.
322#[cfg(test)]
323#[path = "service_messages_tests.rs"]
324mod tests;