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
//! What:
//! Carrying back what Git's apply of a merge's autostash left in the commit's scratch repository when the apply conflicted:
//! the stash entry with its `autostash` reflog line, and the unmerged index entries of the files it conflicted in.
//! Why:
//! The commit that concludes a merge is built by real Git in a scratch repository, and Git's `apply_autostash` runs inside it
//! (`builtin/commit.c` line 1982). A stash that applies cleanly leaves nothing behind; one that conflicts is stored by `git stash
//! store` in the scratch repository's own `refs/stash`, and its conflict stages stay in the commit's private index. Both were
//! thrown away with the scratch directory, so Git's promise ("Your changes are safe in the stash") was false in the real
//! repository. The objects need no copying: the scratch repository writes into the real object store. The stash entry is stored
//! in the real repository with Git's own words, and the stages are put into the index the landing installs, under the lock it holds.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! if (await scratchStash(cleanup) === copiedAutostash(cleanup)) { await storeStash(repository, oid); stages = await unmergedStages(cleanup); }
//! ```
/// The fail-closed recovery failure, for the listing that cannot be read.
use super::recovery_files::read_optional;
/// Scratch repository commands.
use super::shadow_git::shadow_request;
/// The record of the commit's scratch directory.
use super::service_protocol::Cleanup;
/// Running real Git.
use super::transaction_git::{GitContext, GitRequest, run_git};
/// Diagnostics for the log.
use super::diagnostic_log::{debug, warn};
/// Borrowed filesystem paths.
use std::path::{Path, PathBuf};
/// The file of the conclusion state a merge's autostash is recorded in.
const AUTOSTASH_FILE: &str = "MERGE_AUTOSTASH";
/// The words Git's own `apply_autostash` gives the stash entry it stores (`sequencer.c`, `git stash store -m autostash`).
const STASH_MESSAGE: &str = "autostash";
/// What:
/// The stash a conflicted apply left in the scratch repository, when the commit consumed a merge's autostash.
/// Why:
/// The copy of `MERGE_AUTOSTASH` made when the commit was prepared names the stash; the scratch repository's `refs/stash` names
/// it only when Git's apply failed and stored it. Both naming the same object is the one case worth carrying.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async function conflictedAutostash(git, cleanup): Promise<string | undefined>;
/// ```
fn conflicted_autostash(git: &GitContext, cleanup: &Cleanup) -> Option<String> {
let copy: Vec<u8> = read_optional(cleanup.scratch.join("conclusion").join(AUTOSTASH_FILE).as_path()).ok().flatten()?;
let recorded: String = String::from_utf8_lossy(copy.as_slice()).trim().to_owned();
let shadow: PathBuf = cleanup.scratch.join("shadow");
let stored = run_git(git, &shadow_request(shadow.as_path(), shadow.as_path(), &["rev-parse", "--verify", "--quiet", "refs/stash"])).ok()?;
if !stored.succeeded() {
return None;
}
let kept: String = String::from_utf8_lossy(stored.stdout.as_slice()).trim().to_owned();
if recorded.is_empty() || kept != recorded {
return None;
}
return Some(recorded);
}
/// What:
/// Store the conflicted autostash of the commit in the real repository's stash list, once.
/// Why:
/// The landing's cleanup may run again after a restart; a stash already in the list is not stored a second time.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async function carryStash(git, repository, cleanup): Promise<boolean>;
/// ```
pub fn carry_stash(git: &GitContext, repository: &Path, cleanup: &Cleanup) -> bool {
let Some(stash) = conflicted_autostash(git, cleanup) else { return true };
let listed = run_git(git, &GitRequest::new(repository, &["stash", "list", "--format=%H"]));
if let Ok(output) = &listed
&& output.succeeded()
&& String::from_utf8_lossy(output.stdout.as_slice()).lines().any(|line| { return line == stash; })
{
return true;
}
return match run_git(git, &GitRequest::new(repository, &["stash", "store", "-m", STASH_MESSAGE, stash.as_str()])) {
Ok(output) if output.succeeded() => {
debug("autostashCarry", format!("the stash {stash} the apply conflicted on was stored in the repository").as_str());
true
}
other => {
warn("autostashCarry", format!("the stash {stash} could not be stored in the repository: {other:?}").as_str());
false
}
};
}
/// What:
/// Put the conflict stages the apply left in the commit's private index into `built`, the index the landing is about to install.
/// Why:
/// The files the stash conflicted in must be unmerged in the real index as they are after Git's own apply. A path is removed
/// first, because a stage entry does not replace a merged one; the stages come as `git ls-files --unmerged` prints them, which
/// is what `update-index --index-info` reads.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async function carryStages(git, repository, cleanup, built): Promise<boolean>;
/// ```
pub fn carry_stages(git: &GitContext, repository: &Path, cleanup: &Cleanup, built: &Path) -> bool {
if conflicted_autostash(git, cleanup).is_none() || !built.exists() {
return true;
}
let mut listing: GitRequest = GitRequest::new(repository, &["ls-files", "--unmerged", "-z"]);
listing.index = Some(cleanup.scratch.join("commit.index"));
let Ok(stages) = run_git(git, &listing) else { return false };
if !stages.succeeded() {
return false;
}
if stages.stdout.is_empty() {
return true;
}
// Each entry is `mode oid stage TAB path`, ended by a NUL; the first entry's object name gives the hash length.
let entries: Vec<&[u8]> = stages.stdout.split(|byte| { return *byte == 0; }).filter(|entry| { return !entry.is_empty(); }).collect();
let length: usize = entries[0].split(|byte| { return *byte == b' '; }).nth(1).map_or(0, <[u8]>::len);
let mut paths: Vec<&[u8]> = entries.iter().filter_map(|entry| { return entry.split(|byte| { return *byte == b'\t'; }).nth(1); }).collect();
paths.dedup();
let mut input: Vec<u8> = Vec::new();
for path in &paths {
input.extend_from_slice(format!("0 {}\t", "0".repeat(length)).as_bytes());
input.extend_from_slice(path);
input.push(0);
}
for entry in &entries {
input.extend_from_slice(entry);
input.push(0);
}
let mut placing: GitRequest = GitRequest::new(repository, &["update-index", "-z", "--index-info"]);
placing.index = Some(built.to_path_buf());
placing.input = Some(input);
return match run_git(git, &placing) {
Ok(output) if output.succeeded() => true,
other => {
warn("autostashCarry", format!("the conflict stages of the stash could not be put into the index: {other:?}").as_str());
false
}
};
}