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
//! What:
//! The refusals of an isolated merge, as the finding events a caller reads, each naming the paths involved and
//! every valid way forward.
//! Why:
//! A merge that cannot proceed changes nothing, and the person (or the agent) who ran it must learn which files
//! stand in the way and what to do next without reading the wrapper's source.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! renderPolicyEvents(1, [{ kind: 'core-finding', coreId: 'merge-isolation', code: 'staged-overlap', message }]);
//! ```
/// The path list of an overlap report: a few names, then a count.
use super::auto_push_dirty_paths::listed;
/// The finding event and its rendering.
use super::policy_events::{PolicyEvent, render_policy_events};
/// The finding family of every refusal here.
pub const CORE_ID: &str = "merge-isolation";
/// Code of the refusal when the merge changes a path another session holds changes in.
pub const OVERLAP_CODE: &str = "paths-in-use";
/// Code of the refusal when the merge conflicts textually.
pub const CONFLICT_CODE: &str = "merge-conflict";
/// Code of the refusal when Git refused the last step.
pub const APPLY_REFUSED_CODE: &str = "not-applied";
/// What:
/// Render one finding as a JSONL line.
/// Why:
/// Every refusal is a core finding of the merge-isolation family; the message carries the ways forward.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// renderPolicyEvents(1, [finding])
/// ```
fn render(code: &'static str, message: String) -> String {
return render_policy_events(
1,
&[PolicyEvent::CoreFinding {
core_id: CORE_ID,
code,
message,
}],
);
}
/// What:
/// The refusal for a merge that changes paths which the checkout holds staged or modified.
/// Why:
/// Those paths belong to another session's work; writing the merge over them would lose it, and Git itself refuses
/// the same overlap.
/// Every way out is named, including the one that needs no cooperation: a clean linked worktree.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function overlapFinding({ reference, paths }): string;
/// ```
pub fn overlap_finding(reference: &str, paths: &[Vec<u8>]) -> String {
return render(
OVERLAP_CODE,
format!(
"git merge {reference} was not run: the merge changes {}, which this checkout holds staged or modified \
(probably another session's work in progress). Writing the merge there would overwrite that work, so \
nothing was changed. Valid ways forward: wait until the session that owns those files has committed or \
restored them, then merge again; or merge in a clean linked worktree \
(`git worktree add -b merge-work ../merge-work <branch>`, then `git merge {reference}` there, and bring \
the result back with `git merge --ff-only merge-work` here once those paths are free); or, when the \
files are yours, commit them (`git commit -- <path>`) or set them aside (`git stash push -- <path>`) and \
run the merge again.",
listed(paths)
),
);
}
/// What:
/// The refusal for a merge that conflicts textually.
/// Why:
/// A conflict needs a worktree to resolve in, and this checkout's worktree and index hold other work; resolving
/// there would mix the two.
/// The merge was computed from objects alone, so nothing was changed.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function conflictFinding({ reference, paths }): string;
/// ```
pub fn conflict_finding(reference: &str, paths: &[String]) -> String {
return render(
CONFLICT_CODE,
format!(
"git merge {reference} was not run: merging it into the current branch conflicts in {}. Conflicts have \
to be resolved in a worktree, and this checkout holds staged changes that are not yours, so nothing was \
changed. Valid ways forward: merge in a clean linked worktree (`git worktree add -b merge-work \
../merge-work <this branch>`, then `git merge {reference}` there, resolve, commit, and bring the result \
back with `git merge --ff-only merge-work` here); or resolve in a clean checkout of the repository; or \
rebase or adjust {reference} so that it no longer conflicts.",
paths.join(", ")
),
);
}
/// What:
/// The refusal for a merge commit that was made but that Git refused to apply to the worktree.
/// Why:
/// The last step is Git's own, and it can still refuse (an untracked file in the way, a path changed after the
/// check); the branch, the index and the worktree are then as they were, and the commit stays unreachable.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function notAppliedFinding({ reference, detail }): string;
/// ```
pub fn not_applied_finding(reference: &str, detail: &str) -> String {
return render(
APPLY_REFUSED_CODE,
format!(
"git merge {reference} was not applied: Git refused to move the checkout to the merge ({detail}). The \
branch, the index and the worktree are as they were. Valid ways forward: remove or move the files Git \
names and merge again; or merge in a clean linked worktree and bring the result back with \
`git merge --ff-only`."
),
);
}
/// Finding text and the paths it names.
#[cfg(test)]
#[path = "merge_isolation_report_tests.rs"]
mod tests;