Skip to main content

git_policy_cli/lock/
index_holder_wait.rs

1//! What:
2//!  The wait of a command that is about to change the checkout for any program's hold on Git's `index.lock`: the commit
3//!  service's, a foreign `git`'s, or a lock nothing proves held. Event-driven, with a notice that names what is waited for
4//!  and the holder, and over as soon as the lock is gone. A holder proven alive and an owner that cannot be proven alive are
5//!  both waited for with no bound, because the person's own `git commit` with its editor open holds the lock with no proof;
6//!  the line for the second says what is known and how to remove the lock if no program uses the repository. A PID file naming
7//!  a process that is gone ends the wait at once with a diagnostic.
8//! Why:
9//!  Native Git never waits for `index.lock`: a merge that finds it held dies (`Unable to create ... index.lock` or, from the
10//!  stash that every merge commit takes first, `could not write index` and `stash failed`). Other sessions commit into a
11//!  shared checkout all the time, so a held lock is ordinary and passes by itself. This wait is the sibling of the wait for
12//!  the commit service's own hold (`service_index_wait.rs`), which returns at once for a foreign holder because a plain
13//!  forwarded writer keeps the behavior of native Git; a merge is held to the owner's rule that merges never fail
14//!  (`SPEC.md`, "Merges and commits never fail"). The wait blocks on an inotify watch set before the first look, so a
15//!  release between the look and the wait is not missed; where no watch can be set it looks again after a short pause.
16//!  It never removes a lock.
17//!
18//! In TS you'd write (pseudocode):
19//! ```ts
20//! const outcome = await waitForIndexRelease({ realIndex, command: 'merge', cadence, stderr });
21//! ```
22
23/// Evidence about the holder of a lock.
24use crate::index_lock_evidence::{
25    Evidence, Verdict, blocker_words, ceiling_line, classify_index_lock, dead_owner_diagnostic, gather_with, proven_holder_line, proven_holder_progress_line, unproven_wait_line,
26};
27/// The bounded scan of open descriptors.
28use crate::index_lock_holders::scan_platform_holders_bounded;
29/// The start time of a process.
30use crate::process_start::resolve_process_start;
31/// The place of the lock and the watch on its directory.
32use crate::service_index_lock::{Watch, lock_path_of};
33/// The longest a wait lasts.
34use crate::wait_ceilings::LOCK_CEILING;
35/// How long a wait without a watch sleeps between looks.
36use crate::service_index_watch::POLL_PAUSE;
37use crate::wait_progress::{Cadence, Ticker};
38/// Writing the notices.
39use std::io::Write;
40/// `Path` is a borrowed filesystem path.
41use std::path::Path;
42/// Time spans and the monotonic clock.
43use std::time::{Duration, Instant};
44
45/// What came of the wait.
46#[derive(Clone, Copy, Debug, Eq, PartialEq)]
47pub enum IndexWait {
48    /// Nobody held the lock when the command asked.
49    NotHeld,
50    /// A holder had the lock and let it go.
51    Released(
52        /// How long the wait lasted.
53        Duration,
54    ),
55    /// The lock's PID file names a process that no longer runs, so nothing will release it; the diagnostic was written,
56    ///  nothing was changed and the lock was left in place.
57    OwnerGone(
58        /// How long the wait lasted.
59        Duration,
60    ),
61    /// The lock was still held at the ceiling; the line naming the blocker was written, nothing was changed and the lock was left
62    ///  in place.
63    GaveUp(
64        /// How long the wait lasted.
65        Duration,
66    ),
67}
68
69/// What the last look at the lock found.
70enum Proof {
71    /// A holder is proven alive.
72    Alive(Evidence, i64, bool, Option<String>),
73    /// The PID file names a process that is gone.
74    Dead(i64),
75    /// Nothing proves a holder alive or dead.
76    Nothing(Option<Evidence>),
77}
78
79/// What the evidence for `real_index` proves now.
80fn look_at(real_index: &Path, gather: &dyn Fn(&Path) -> std::io::Result<Option<Evidence>>) -> Proof {
81    let Ok(Some(evidence)) = gather(real_index) else {
82        return Proof::Nothing(None);
83    };
84    return match classify_index_lock(&evidence) {
85        Verdict::ProvenAlive { pid, by_descriptor, command } => Proof::Alive(evidence, pid, by_descriptor, command),
86        Verdict::Dead(pid) => Proof::Dead(pid),
87        Verdict::EvidenceFree => Proof::Nothing(Some(evidence)),
88    };
89}
90
91/// The evidence of the host: the Git lock PID file and the bounded scan of open descriptors.
92fn gather_here(real_index: &Path) -> std::io::Result<Option<Evidence>> {
93    return gather_with(real_index, resolve_process_start, scan_platform_holders_bounded);
94}
95
96/// The notice: what the command waits for, why it is safe to wait, then the holder.
97fn notice(command: &str, real_index: &Path, waited: Duration, proof: &Proof, first: bool) -> String {
98    let tail: String = match proof {
99        Proof::Alive(evidence, pid, by_descriptor, holder_command) => {
100            let line: String = if first {
101                proven_holder_line(*pid, *by_descriptor, holder_command.as_deref(), evidence.lock_path.as_path())
102            } else {
103                proven_holder_progress_line(*pid, *by_descriptor, holder_command.as_deref(), evidence.lock_path.as_path(), waited.as_secs())
104            };
105            line
106        }
107        Proof::Nothing(found) => unproven_wait_line(lock_path_of(real_index).as_path(), found.as_ref()),
108        Proof::Dead(_) => unproven_wait_line(lock_path_of(real_index).as_path(), None),
109    };
110    return format!(
111        "cli-git: git {command} has changed nothing yet and waits for the index lock of this repository; {}",
112        tail.strip_prefix("cli-git: ").unwrap_or(tail.as_str())
113    );
114}
115
116/// What:
117///  Wait while any program holds `index.lock` of `real_index`, telling `out` what is waited for.
118/// Why:
119///   Returns after one metadata read when nothing holds the lock. The first line comes at once for a holder that is already
120///  known when the wait begins and then every notice interval, so a queued command can be told from a hung one. A holder
121///  proven alive and an owner nothing proves alive are waited for with no bound; a dead owner ends the wait at once.
122///
123/// In TS you'd write (pseudocode):
124/// ```ts
125/// await waitForIndexRelease(realIndex, 'merge', cadence, stderr)
126/// ```
127pub fn wait_for_index_release(real_index: &Path, command: &str, cadence: Cadence, out: &mut dyn Write) -> IndexWait {
128    return wait_for_index_release_within(real_index, command, LOCK_CEILING, cadence, out);
129}
130
131/// `wait_for_index_release` that gives up after `ceiling`.
132pub fn wait_for_index_release_within(real_index: &Path, command: &str, ceiling: Duration, cadence: Cadence, out: &mut dyn Write) -> IndexWait {
133    return wait_with_watches(real_index, command, ceiling, cadence, out, &Watch::on);
134}
135
136/// `wait_for_index_release` with the function that sets a watch on the lock's directory, so a control can hand it none.
137fn wait_with_watches(real_index: &Path, command: &str, ceiling: Duration, cadence: Cadence, out: &mut dyn Write, set_watch: &dyn Fn(&Path) -> Option<Watch>) -> IndexWait {
138    let lock = lock_path_of(real_index);
139    // The watch comes before the first look: a release between the look and the wait is then seen as an event.
140    let mut watch: Option<Watch> = lock.parent().and_then(set_watch);
141    let lock_name: std::ffi::OsString = lock.file_name().map(std::ffi::OsStr::to_os_string).unwrap_or_default();
142    let consequence: String = format!("git {command} stops waiting and reports what Git says");
143    let began: Instant = Instant::now();
144    let mut ticker: Ticker = Ticker::announced_at_start(cadence);
145    let mut held: bool = false;
146    let mut announced: bool = false;
147    let mut proof: Proof = Proof::Nothing(None);
148    loop {
149        if std::fs::symlink_metadata(&lock).is_err() {
150            return if held { IndexWait::Released(began.elapsed()) } else { IndexWait::NotHeld };
151        }
152        held = true;
153        let elapsed: Duration = began.elapsed();
154        // The evidence is read when the first line is due and when a repeat is due, so a scan of the open descriptors is not made
155        //  for every wake of the watch.
156        let due: bool = !announced || ticker.due(elapsed);
157        if due || elapsed >= ceiling {
158            proof = look_at(real_index, &gather_here);
159        }
160        if let Proof::Dead(pid) = &proof {
161            let _noted = out.write_all(dead_owner_diagnostic(lock.as_path(), *pid, consequence.as_str()).as_bytes()).and_then(|()| { return out.flush(); });
162            return IndexWait::OwnerGone(elapsed);
163        }
164        if due {
165            // A closed terminal is not a reason to stop the command the person asked for.
166            let _noted = out.write_all(notice(command, real_index, elapsed, &proof, !announced).as_bytes()).and_then(|()| { return out.flush(); });
167            announced = true;
168        }
169        // The owner, 2026-11-03: "nothing should ever wait forever or indefinitely". At the ceiling the wait names the blocker and ends;
170        //  the merge then runs into Git's own refusal and exits non-zero, with nothing changed.
171        if elapsed >= ceiling {
172            let blocker: String = match &proof {
173                Proof::Alive(evidence, ..) => blocker_words(evidence),
174                Proof::Nothing(Some(evidence)) => blocker_words(evidence),
175                Proof::Nothing(None) | Proof::Dead(_) => String::from("an owner whose evidence could not be read"),
176            };
177            let line: String = ceiling_line(lock.as_path(), blocker.as_str(), elapsed.as_secs(), consequence.as_str());
178            let _noted = out.write_all(line.as_bytes()).and_then(|()| { return out.flush(); });
179            return IndexWait::GaveUp(elapsed);
180        }
181        let pause: Duration = ticker.next_in(elapsed).min(ceiling.saturating_sub(elapsed)).max(Duration::from_millis(1));
182        match watch.as_ref() {
183            Some(active) => {
184                if !active.wait(pause, lock_name.as_os_str()) {
185                    // The watch ended with its directory: the ended watch is dropped first, because a new one may need the
186                    //  descriptor and the quota it holds, then the lock is looked at again with a new watch set.
187                    drop(watch.take());
188                    watch = lock.parent().and_then(set_watch);
189                }
190            }
191            None => std::thread::sleep(pause.min(POLL_PAUSE)),
192        }
193    }
194}
195
196/// `wait_for_index_release` with the notice cadence this process runs under, on standard error.
197pub fn wait_for_index_release_here(real_index: &Path, command: &str) -> IndexWait {
198    return wait_for_index_release(real_index, command, Cadence::from_environment(), &mut std::io::stderr());
199}
200
201/// The wait is controlled in the test module.
202#[cfg(test)]
203#[path = "index_holder_wait_tests.rs"]
204mod tests;