Skip to main content

git_policy_cli/
service_route.rs

1//! What:
2//!  Which way a commit goes: through the resident service (already listening, or set up first by the wrapper), or the
3//!  way it went before the service existed.
4//! Why:
5//!  There is no switch and no install state for the person to manage (`doc/planning/cli-git-resident-service.md`,
6//!  "Provisioning"). The route follows the machine: a user session bus and a service program next to the wrapper mean the
7//!  wrapper can set the service up itself, and keeps it set up (the socket is the manager's and outlives every upgrade, so
8//!  its presence never says the installed units name this release's program); a socket without them is used as it is;
9//!  anything else keeps the earlier path until that path is retired, after which it is a plain failure. The decision reads
10//!  the file system only and starts no process.
11//!
12//! In TS you'd write (pseudocode):
13//! ```ts
14//! const route = chooseRoute({ runtimeDir, busAddress, configHome, homeDir, ownExecutable, realGit });
15//! ```
16
17use super::repo_read_layout::read_layout_in;
18use super::service_instance::{SOCKET_DIRECTORY, instance_of, socket_for};
19use super::service_provision::{ProvisionHost, RETIREMENT_WAIT, SERVICE_PROGRAM};
20use super::service_server::Bind;
21use super::systemd_dbus::session_bus_path;
22use super::transaction_git::{GitContext, GitRequest, run_git};
23use std::path::{Path, PathBuf};
24
25static OWN_EXECUTABLE: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
26
27/// Remembers where the running wrapper is, once, so the service program can be found beside it.
28///  Identification only: nothing is started from this path (`doc/planning/cli-git-no-self-invocation.md`); the path is
29///  read to name a sibling program in a unit file that the operating system's manager runs.
30pub fn remember_own_executable(path: &Path) {
31    let _first = OWN_EXECUTABLE.set(path.to_path_buf());
32}
33
34/// The wrapper's own path, when the process remembered it.
35pub fn remembered_own_executable() -> Option<&'static Path> {
36    return OWN_EXECUTABLE.get().map(PathBuf::as_path);
37}
38
39/// What the route decision reads.
40#[derive(Clone, Copy, Debug)]
41pub struct RouteInputs<'a> {
42    /// `XDG_RUNTIME_DIR`.
43    pub runtime_dir: Option<&'a Path>,
44    /// `DBUS_SESSION_BUS_ADDRESS`.
45    pub bus_address: Option<&'a str>,
46    /// `XDG_CONFIG_HOME`.
47    pub config_home: Option<&'a Path>,
48    /// `HOME`.
49    pub home: Option<&'a Path>,
50    /// The running wrapper's own executable.
51    pub own_executable: Option<&'a Path>,
52    /// The real Git the service will run.
53    pub real_git: &'a Path,
54    /// The worktree's own Git directory, which names the service instance the commit goes to.
55    pub git_dir: &'a Path,
56}
57
58/// Which way a commit goes.
59#[derive(Clone, Debug, Eq, PartialEq)]
60pub enum Route {
61    /// The earlier path.
62    Earlier,
63    /// Through the service at `address`, after provisioning when `provision` is present.
64    Service {
65        /// Where the service listens.
66        address: Bind,
67        /// What to set up, or to check is set up, first; absent only when a socket exists and the wrapper cannot set up
68        ///  a service itself (no session bus, or no service program beside it).
69        provision: Option<ProvisionHost>,
70    },
71}
72
73/// The directory a user's own unit files live in.
74pub fn user_unit_dir(config_home: Option<&Path>, home: Option<&Path>) -> Option<PathBuf> {
75    return config_home
76        .map(|directory| { return directory.join("systemd").join("user"); })
77        .or_else(|| { return home.map(|directory| { return directory.join(".config").join("systemd").join("user"); }); });
78}
79
80/// What:
81///  Decides the route.
82/// Why:
83///   Provisioning needs the session bus socket and the service program, and it is always handed back when both are there,
84///  even when the socket exists: the socket outlives an upgrade, and the units it activates may name a program the upgrade
85///  removed. Whether anything must be written is decided where the units are read (`provision`), so this decision stays
86///  free of reads. A socket that exists without the means to provision is used as it is, whoever made it; with neither
87///  the socket nor the means, the earlier path stays.
88///
89/// In TS you'd write (pseudocode):
90/// ```ts
91/// chooseRoute(inputs)
92/// ```
93pub fn choose_route(inputs: &RouteInputs<'_>) -> Route {
94    let Some(runtime_dir) = inputs.runtime_dir else { return Route::Earlier };
95    let socket: PathBuf = socket_for(runtime_dir, inputs.git_dir);
96    let socket_exists: bool = socket.exists();
97    let Some(bus) = session_bus_path(inputs.bus_address, Some(runtime_dir)).filter(|path| { return path.exists(); }) else {
98        return if socket_exists { Route::Service { address: Bind::Path(socket), provision: None } } else { Route::Earlier };
99    };
100    let Some(service_binary) = inputs
101        .own_executable
102        .and_then(Path::parent)
103        .map(|directory| { return directory.join(SERVICE_PROGRAM); })
104        .filter(|path| { return path.is_file(); })
105    else {
106        return if socket_exists { Route::Service { address: Bind::Path(socket), provision: None } } else { Route::Earlier };
107    };
108    let host: ProvisionHost = ProvisionHost {
109        runtime_dir: runtime_dir.to_path_buf(),
110        user_unit_dir: user_unit_dir(inputs.config_home, inputs.home),
111        bus,
112        service_binary,
113        git: inputs.real_git.to_path_buf(),
114        instance: instance_of(inputs.git_dir),
115        retirement_wait: RETIREMENT_WAIT,
116    };
117    return Route::Service { address: Bind::Path(socket), provision: Some(host) };
118}
119
120/// What `git cli-git service status` prints and exits with.
121#[derive(Clone, Debug, Eq, PartialEq)]
122pub struct StatusReport {
123    /// Exit status: 0 when the service answered.
124    pub code: i32,
125    /// Standard output.
126    pub stdout: String,
127    /// Standard error.
128    pub stderr: String,
129}
130
131/// What:
132///  The real path of the Git directory of the worktree a command runs in, read from the file system when the layout is a plain
133///  one and from Git otherwise.
134/// Why:
135///   The Git directory names the service instance, so the route cannot be chosen without it; the preparation of the commit
136///  asks the same question later and shares the answer's cost when the file system can give it.
137///
138/// In TS you'd write (pseudocode):
139/// ```ts
140/// worktreeGitDir(context, cwd) // undefined outside a repository
141/// ```
142pub fn worktree_git_dir(context: &GitContext, cwd: &Path) -> Option<PathBuf> {
143    if let Some(layout) = read_layout_in(cwd, context.overlay.as_slice()) {
144        return Some(layout.git_dir);
145    }
146    let output = run_git(context, &GitRequest::new(cwd, &["rev-parse", "--path-format=absolute", "--absolute-git-dir"])).ok()?;
147    if !output.succeeded() {
148        return None;
149    }
150    let text: String = String::from_utf8_lossy(output.stdout.as_slice()).trim().to_string();
151    return if text.is_empty() { None } else { Some(PathBuf::from(text)) };
152}
153
154/// What:
155///  Says whether the commit service of a worktree answers on its socket.
156/// Why:
157///   A diagnostic only: there is nothing to install or start by hand. Connecting is what a commit does, so a service that the
158///  manager would start on demand is started by this check too. Outside a repository there is no worktree to ask for, and the
159///  answer counts the instances that answer.
160///
161/// In TS you'd write (pseudocode):
162/// ```ts
163/// serviceStatus(runtimeDir, gitDir)
164/// ```
165pub fn service_status(runtime_dir: Option<&Path>, git_dir: Option<&Path>) -> StatusReport {
166    let answers = |socket: &Path| -> bool { return std::os::unix::net::UnixStream::connect(socket).is_ok(); };
167    let (answered, what): (bool, &str) = match (runtime_dir, git_dir) {
168        (Some(runtime), Some(git)) => (answers(socket_for(runtime, git).as_path()), "The commit service of this worktree"),
169        (Some(runtime), None) => {
170            let any: bool = std::fs::read_dir(runtime.join(SOCKET_DIRECTORY)).is_ok_and(|entries| {
171                return entries.flatten().any(|entry| { return entry.path().extension().is_some_and(|extension| { return extension == "sock"; }) && answers(entry.path().as_path()); });
172            });
173            (any, "A commit service")
174        }
175        (None, _) => (false, "The commit service"),
176    };
177    return if answered {
178        StatusReport { code: 0, stdout: format!("{what} is running and answers.\n"), stderr: String::new() }
179    } else {
180        StatusReport {
181            code: 1,
182            stdout: String::new(),
183            stderr: format!(
184                "{what} does not answer here. It sets itself up the first time a commit needs it, so there is nothing to \
185                 install; if a commit fails, its message says what to do.\n"
186            ),
187        }
188    };
189}
190
191/// Route controls stay out of the release executable.
192#[cfg(test)]
193#[path = "service_route_tests.rs"]
194mod tests;