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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
//! What:
//! The wait of a command that is about to change the checkout for any program's hold on Git's `index.lock`: the commit
//! service's, a foreign `git`'s, or a lock nothing proves held. Event-driven, with a notice that names what is waited for
//! 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
//! both waited for with no bound, because the person's own `git commit` with its editor open holds the lock with no proof;
//! 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
//! a process that is gone ends the wait at once with a diagnostic.
//! Why:
//! Native Git never waits for `index.lock`: a merge that finds it held dies (`Unable to create ... index.lock` or, from the
//! stash that every merge commit takes first, `could not write index` and `stash failed`). Other sessions commit into a
//! shared checkout all the time, so a held lock is ordinary and passes by itself. This wait is the sibling of the wait for
//! the commit service's own hold (`service_index_wait.rs`), which returns at once for a foreign holder because a plain
//! forwarded writer keeps the behavior of native Git; a merge is held to the owner's rule that merges never fail
//! (`SPEC.md`, "Merges and commits never fail"). The wait blocks on an inotify watch set before the first look, so a
//! release between the look and the wait is not missed; where no watch can be set it looks again after a short pause.
//! It never removes a lock.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! const outcome = await waitForIndexRelease({ realIndex, command: 'merge', cadence, stderr });
//! ```
/// Evidence about the holder of a lock.
use super::index_lock_evidence::{
Evidence, Verdict, blocker_words, ceiling_line, classify_index_lock, dead_owner_diagnostic, gather_with, proven_holder_line, proven_holder_progress_line, unproven_wait_line,
};
/// The bounded scan of open descriptors.
use super::index_lock_holders::scan_platform_holders_bounded;
/// The start time of a process.
use super::process_start::resolve_process_start;
/// The place of the lock and the watch on its directory.
use super::service_index_lock::{Watch, lock_path_of};
/// The longest a wait lasts.
use super::wait_ceilings::LOCK_CEILING;
/// How long a wait without a watch sleeps between looks.
use super::service_index_watch::POLL_PAUSE;
use super::wait_progress::{Cadence, Ticker};
/// Writing the notices.
use std::io::Write;
/// `Path` is a borrowed filesystem path.
use std::path::Path;
/// Time spans and the monotonic clock.
use std::time::{Duration, Instant};
/// What came of the wait.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum IndexWait {
/// Nobody held the lock when the command asked.
NotHeld,
/// A holder had the lock and let it go.
Released(
/// How long the wait lasted.
Duration,
),
/// The lock's PID file names a process that no longer runs, so nothing will release it; the diagnostic was written,
/// nothing was changed and the lock was left in place.
OwnerGone(
/// How long the wait lasted.
Duration,
),
/// The lock was still held at the ceiling; the line naming the blocker was written, nothing was changed and the lock was left
/// in place.
GaveUp(
/// How long the wait lasted.
Duration,
),
}
/// What the last look at the lock found.
enum Proof {
/// A holder is proven alive.
Alive(Evidence, i64, bool, Option<String>),
/// The PID file names a process that is gone.
Dead(i64),
/// Nothing proves a holder alive or dead.
Nothing(Option<Evidence>),
}
/// What the evidence for `real_index` proves now.
fn look_at(real_index: &Path, gather: &dyn Fn(&Path) -> std::io::Result<Option<Evidence>>) -> Proof {
let Ok(Some(evidence)) = gather(real_index) else {
return Proof::Nothing(None);
};
return match classify_index_lock(&evidence) {
Verdict::ProvenAlive { pid, by_descriptor, command } => Proof::Alive(evidence, pid, by_descriptor, command),
Verdict::Dead(pid) => Proof::Dead(pid),
Verdict::EvidenceFree => Proof::Nothing(Some(evidence)),
};
}
/// The evidence of the host: the Git lock PID file and the bounded scan of open descriptors.
fn gather_here(real_index: &Path) -> std::io::Result<Option<Evidence>> {
return gather_with(real_index, resolve_process_start, scan_platform_holders_bounded);
}
/// The notice: what the command waits for, why it is safe to wait, then the holder.
fn notice(command: &str, real_index: &Path, waited: Duration, proof: &Proof, first: bool) -> String {
let tail: String = match proof {
Proof::Alive(evidence, pid, by_descriptor, holder_command) => {
let line: String = if first {
proven_holder_line(*pid, *by_descriptor, holder_command.as_deref(), evidence.lock_path.as_path())
} else {
proven_holder_progress_line(*pid, *by_descriptor, holder_command.as_deref(), evidence.lock_path.as_path(), waited.as_secs())
};
line
}
Proof::Nothing(found) => unproven_wait_line(lock_path_of(real_index).as_path(), found.as_ref()),
Proof::Dead(_) => unproven_wait_line(lock_path_of(real_index).as_path(), None),
};
return format!(
"cli-git: git {command} has changed nothing yet and waits for the index lock of this repository; {}",
tail.strip_prefix("cli-git: ").unwrap_or(tail.as_str())
);
}
/// What:
/// Wait while any program holds `index.lock` of `real_index`, telling `out` what is waited for.
/// Why:
/// Returns after one metadata read when nothing holds the lock. The first line comes at once for a holder that is already
/// known when the wait begins and then every notice interval, so a queued command can be told from a hung one. A holder
/// proven alive and an owner nothing proves alive are waited for with no bound; a dead owner ends the wait at once.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await waitForIndexRelease(realIndex, 'merge', cadence, stderr)
/// ```
pub fn wait_for_index_release(real_index: &Path, command: &str, cadence: Cadence, out: &mut dyn Write) -> IndexWait {
return wait_for_index_release_within(real_index, command, LOCK_CEILING, cadence, out);
}
/// `wait_for_index_release` that gives up after `ceiling`.
pub fn wait_for_index_release_within(real_index: &Path, command: &str, ceiling: Duration, cadence: Cadence, out: &mut dyn Write) -> IndexWait {
return wait_with_watches(real_index, command, ceiling, cadence, out, &Watch::on);
}
/// `wait_for_index_release` with the function that sets a watch on the lock's directory, so a control can hand it none.
fn wait_with_watches(real_index: &Path, command: &str, ceiling: Duration, cadence: Cadence, out: &mut dyn Write, set_watch: &dyn Fn(&Path) -> Option<Watch>) -> IndexWait {
let lock = lock_path_of(real_index);
// The watch comes before the first look: a release between the look and the wait is then seen as an event.
let mut watch: Option<Watch> = lock.parent().and_then(set_watch);
let lock_name: std::ffi::OsString = lock.file_name().map(std::ffi::OsStr::to_os_string).unwrap_or_default();
let consequence: String = format!("git {command} stops waiting and reports what Git says");
let began: Instant = Instant::now();
let mut ticker: Ticker = Ticker::announced_at_start(cadence);
let mut held: bool = false;
let mut announced: bool = false;
let mut proof: Proof = Proof::Nothing(None);
loop {
if std::fs::symlink_metadata(&lock).is_err() {
return if held { IndexWait::Released(began.elapsed()) } else { IndexWait::NotHeld };
}
held = true;
let elapsed: Duration = began.elapsed();
// 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
// for every wake of the watch.
let due: bool = !announced || ticker.due(elapsed);
if due || elapsed >= ceiling {
proof = look_at(real_index, &gather_here);
}
if let Proof::Dead(pid) = &proof {
let _noted = out.write_all(dead_owner_diagnostic(lock.as_path(), *pid, consequence.as_str()).as_bytes()).and_then(|()| { return out.flush(); });
return IndexWait::OwnerGone(elapsed);
}
if due {
// A closed terminal is not a reason to stop the command the person asked for.
let _noted = out.write_all(notice(command, real_index, elapsed, &proof, !announced).as_bytes()).and_then(|()| { return out.flush(); });
announced = true;
}
// The owner, 2026-11-03: "nothing should ever wait forever or indefinitely". At the ceiling the wait names the blocker and ends;
// the merge then runs into Git's own refusal and exits non-zero, with nothing changed.
if elapsed >= ceiling {
let blocker: String = match &proof {
Proof::Alive(evidence, ..) => blocker_words(evidence),
Proof::Nothing(Some(evidence)) => blocker_words(evidence),
Proof::Nothing(None) | Proof::Dead(_) => String::from("an owner whose evidence could not be read"),
};
let line: String = ceiling_line(lock.as_path(), blocker.as_str(), elapsed.as_secs(), consequence.as_str());
let _noted = out.write_all(line.as_bytes()).and_then(|()| { return out.flush(); });
return IndexWait::GaveUp(elapsed);
}
let pause: Duration = ticker.next_in(elapsed).min(ceiling.saturating_sub(elapsed)).max(Duration::from_millis(1));
match watch.as_ref() {
Some(active) => {
if !active.wait(pause, lock_name.as_os_str()) {
// The watch ended with its directory: the ended watch is dropped first, because a new one may need the
// descriptor and the quota it holds, then the lock is looked at again with a new watch set.
drop(watch.take());
watch = lock.parent().and_then(set_watch);
}
}
None => std::thread::sleep(pause.min(POLL_PAUSE)),
}
}
}
/// `wait_for_index_release` with the notice cadence this process runs under, on standard error.
pub fn wait_for_index_release_here(real_index: &Path, command: &str) -> IndexWait {
return wait_for_index_release(real_index, command, Cadence::from_environment(), &mut std::io::stderr());
}
/// The wait is controlled in the test module.
#[cfg(test)]
#[path = "index_holder_wait_tests.rs"]
mod tests;