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
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
//! Helpers for the `--diff` difftool presentation: reading the two
//! sides, deciding whether to render them as Markdown, and naming the
//! session in the status bar.
//!
//! They live here rather than in `main.rs` for one reason: `main`
//! deliberately declares no modules (see its header comment), so a
//! function defined there is reachable by neither `cargo test --lib`
//! nor the integration tests. [`diff_label`] in particular is four
//! branches of path handling — a rename, a delete, an add, and the
//! degenerate pair — and every one of them is a string a user reads.
use Path;
use ;
use is_markdown_path;
/// Exit status used when a difftool session is ended by the user.
///
/// 128 + `SIGINT`, the conventional status for an interrupted program —
/// and on Unix what we actually exit with is that signal itself, so this
/// is only reached on a platform with no process groups to signal.
const EXIT_INTERRUPTED: i32 = 130;
/// Read one side of a `--diff` pair.
///
/// git passes `/dev/null` for the missing side of an add or a delete,
/// which reads as empty — exactly the right input, so no special case.
///
/// The error carries the path because both sides are usually temp files
/// the user never typed: "failed to read" without a name is unactionable
/// in the middle of a `git difftool` walk. A non-UTF-8 file (git will
/// happily invoke a difftool on a binary) fails here rather than being
/// rendered as mojibake.
/// Whether a `--diff` pair is Markdown, and so whether there is a
/// review to open at all.
///
/// **Either side naming a Markdown file is enough.** git passes
/// `/dev/null` for the missing half of an add or a delete, so asking
/// only one side would decline every added and every deleted file.
///
/// The negative case is the reason this exists, and it is what lets the
/// documented `git difftool` recipe drop its pathspec: git invokes a
/// difftool on *every* changed file, and edamame has nothing to offer a
/// `.rs` or a `.png`. Rendering one as Markdown would turn a `#`
/// comment into a heading; rendering it as plain text is worse than the
/// colored diff `git diff` already prints, and costs an `Esc` per file
/// to page past. `main` declines the pair instead — before reading
/// either side, so a binary file is turned away by its name rather than
/// by a UTF-8 decode error.
/// Status-bar label for a difftool session (see `App::diff_label`).
///
/// The new side's file name, because git difftool reproduces the
/// repository's directory structure under its temp dir — so even a
/// commit-to-commit review carries the real basename. Falls back to the
/// old side for a delete (where the new side is `/dev/null`), and names
/// both when they differ, which is what a rename looks like.
///
/// `null` is dropped as a name rather than a path, so it also covers the
/// `/dev/null` git substitutes on both an add and a delete. A real file
/// named `null` is therefore labelled by its other side — a trade taken
/// deliberately: the git case is routine and the collision is not.
/// True when *git* is running us as an external diff over a list of
/// paths, rather than our having been typed by hand.
///
/// **Despite the name, this is not a `git difftool` test, and cannot be
/// made into one.** `GIT_DIFF_PATH_TOTAL` (and its companion
/// `GIT_DIFF_PATH_COUNTER`) are exported by git's external-diff
/// machinery, not by `git difftool--helper` — so a plain `git diff`
/// running a gitattributes `diff.<driver>.command` sets them too, which
/// is the second recipe in `docs/editing.md`. Only `GIT_DIFFTOOL_EXTCMD`
/// is difftool's own, and only under the `-x` form. Measured on git
/// 2.50.1:
///
/// | invocation | `PATH_TOTAL` | `EXTCMD` |
/// |---|---|---|
/// | `git diff` + gitattributes driver | set | unset |
/// | `git difftool -x <cmd>` | set | set |
/// | `git difftool -t <tool>` | set | unset |
///
/// Rows one and three are therefore indistinguishable, and the check
/// cannot be narrowed to `EXTCMD`: the `-t` form — the alias recipe in
/// the docs — sets none. Either variable is enough.
///
/// This gates [`stop_walk`], and the breadth is *right* rather than
/// merely tolerable, because the fact the gate needs is the one all
/// three rows share: git is driving us over a list of paths, so ending
/// our process group ends the list. What it rules out is the case that
/// would be indefensible — a hand-typed `edamame --diff a.md b.md`,
/// where the group is whatever shell or script invoked us and nothing
/// there asked to be interrupted.
/// The decision behind [`under_git_difftool`], split out so it can be
/// tested without touching the process environment (see
/// `crate::test_env`).
/// End a `git difftool` walk the way `Ctrl-C` would have, and never
/// return.
///
/// **Why a signal rather than an exit code.** A diff tool's only channel
/// back to git is its exit status, and `git difftool--helper` discards
/// it: the script ends in a bare `exit 0` unless
/// `GIT_DIFFTOOL_TRUST_EXIT_CODE` is `true`, which is not git's default.
/// So a tool that merely exits — with any status — cannot stop the walk,
/// and the user is left pressing `Esc` once per remaining file.
///
/// What *does* stop it is the signal a `Ctrl-C` at the terminal would have
/// raised — and a full-screen program is the one thing that prevents the
/// terminal raising it, since raw mode clears `ISIG` and the byte arrives as
/// a keystroke instead. So edamame delivers it by hand when the user quits.
/// git is in our process group, dies alongside us, cleans up its temp
/// directory on the way out (it installs handlers for exactly this), and
/// prints nothing — no `fatal: external diff died`, which is what an
/// exit-code stop produces and what makes a deliberate quit read as a
/// crash.
///
/// The key that gets here is edamame's ordinary `Quit` binding (`Ctrl-Q` by
/// default, rebindable like any other). `Ctrl-C` is deliberately *not* a
/// second door: it is `Action::Copy` everywhere in edamame, and a mode that
/// quietly redefined it would be the inconsistency, not the convenience.
///
/// **The caller must have restored the terminal first.** We are about to
/// die from a signal, so no cleanup after this point runs.
///
/// Gated on [`under_git_difftool`] by its caller — which establishes
/// that git is driving us over a list of paths, not that the driver is
/// `difftool` specifically (see there). Ungated, a hand-typed
/// `edamame --diff` would signal whatever shell or script invoked it.
!