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
//! What:
//! The shadow ref store: a snapshot of every real ref,
//! the private target ref,
//! symbolic refs,
//! and the private `HEAD`.
//! Why:
//! Hooks resolve tags, other branches and remote-tracking refs as they stood at invocation.
//! With the files backend the snapshot is one `packed-refs` file and the private target is a loose ref,
//! which takes precedence over a packed entry;
//! with reftable the snapshot is one `update-ref --stdin` transaction
//! (`src/shadow-repository/shadow-refs.ts`).
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! await writeShadowRefs({ gitPath, cwd, shadowPath, capture });
//! ```
/// One shadow Git start, one real Git start, and their failure text.
use super::shadow_git::shadow_request;
use super::transaction_git::{GitContext, GitFailure, GitOutput, GitRequest, run_git_checked};
/// Private file writes for the snapshot.
use super::private_storage::write_private_file;
/// Journal enums: the ref backend, the base and the symbolic `HEAD` at invocation.
use super::transaction_journal::{Base, RefFormat, SymbolicHead};
/// `Path`/`PathBuf` are borrowed/owned filesystem paths.
use std::path::{Path, PathBuf};
/// Header Git writes for a sorted `packed-refs` file without peeled lines.
/// The trailing space matches Git's own writer, which older readers require.
pub const PACKED_REFS_HEADER: &str = "# pack-refs with: sorted \n";
/// What:
/// One real ref at invocation.
/// Why:
/// Symbolic refs and direct refs take different snapshot forms.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type RefSnapshotEntry = { name: string; oid: string; symref: string };
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RefSnapshotEntry {
/// Full ref name.
pub name: String,
/// Object the ref resolves to.
pub oid: String,
/// Symbolic target, or empty for a direct ref.
pub symref: String,
}
/// What:
/// Lists every real ref of the owning worktree in byte order.
/// Why:
/// Sorting by bytes matches the incumbent's `Buffer.compare` order in the packed file.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await listRealRefs({ gitPath, cwd });
/// ```
pub fn list_real_refs(context: &GitContext, cwd: &Path) -> Result<Vec<RefSnapshotEntry>, GitFailure> {
let mut request: GitRequest = GitRequest::new(
cwd,
&["for-each-ref", "--format=%(refname)%00%(objectname)%00%(symref)"],
);
request.without_prefix = true;
let output: GitOutput = run_git_checked(context, &request)?;
let text: &str = std::str::from_utf8(output.stdout.as_slice())
.map_err(|_| { return GitFailure(String::from("git for-each-ref returned non-UTF-8 output.")); })?;
let mut refs: Vec<RefSnapshotEntry> = Vec::new();
for line in text.split('\n').filter(|line| { return !line.is_empty(); }) {
let fields: Vec<&str> = line.split('\0').collect();
let [name, oid, symref] = fields.as_slice() else {
return Err(GitFailure(format!(
"git for-each-ref returned a malformed line: {line:?}"
)));
};
refs.push(RefSnapshotEntry {
name: String::from(*name),
oid: String::from(*oid),
symref: String::from(*symref),
});
}
refs.sort_by(|left, right| { return left.name.as_bytes().cmp(right.name.as_bytes()); });
return Ok(refs);
}
/// What:
/// Formats the files-backend snapshot of every direct ref except the private target.
/// Why:
/// The target is written loose, so a packed copy would only be shadowed.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// formatPackedRefs({ refs, targetRef });
/// ```
pub fn format_packed_refs(refs: &[RefSnapshotEntry], target_ref: &str) -> String {
let mut text: String = String::from(PACKED_REFS_HEADER);
for entry in refs
.iter()
.filter(|entry| { return entry.symref.is_empty() && entry.name != target_ref; })
{
text.push_str(format!("{} {}\n", entry.oid, entry.name).as_str());
}
return text;
}
/// What:
/// Points the shadow `HEAD` at the private target:
/// symbolic to the branch, or detached at the base.
/// Why:
/// A detached `HEAD` needs a base, and an unborn branch has no object to write.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await writeShadowHead({ gitPath, shadowPath, symbolicHead, base });
/// ```
fn write_shadow_head(
context: &GitContext,
shadow: &Path,
symbolic_head: &SymbolicHead,
base: &Base,
) -> Result<(), GitFailure> {
match symbolic_head {
SymbolicHead::Branch(reference) => {
if let Base::Commit(oid) = base {
run_git_checked(
context,
&shadow_request(shadow, shadow, &["update-ref", reference.as_str(), oid.as_str()]),
)?;
}
run_git_checked(
context,
&shadow_request(shadow, shadow, &["symbolic-ref", "HEAD", reference.as_str()]),
)?;
return Ok(());
}
SymbolicHead::Detached => {
let Base::Commit(oid) = base else {
return Err(GitFailure(String::from(
"A detached HEAD has no unborn preparation base.",
)));
};
run_git_checked(
context,
&shadow_request(shadow, shadow, &["update-ref", "--no-deref", "HEAD", oid.as_str()]),
)?;
return Ok(());
}
}
}
/// What:
/// Writes the shadow ref store:
/// the snapshot, the symbolic refs, the private target ref and `HEAD`.
/// Why:
/// The order matches the incumbent's:
/// symbolic refs come after their targets exist, and `HEAD` comes last.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await writeShadowRefs({ gitPath, cwd, shadowPath, capture });
/// ```
pub fn write_shadow_refs(
context: &GitContext,
cwd: &Path,
shadow: &Path,
ref_format: RefFormat,
target_ref: &str,
symbolic_head: &SymbolicHead,
base: &Base,
) -> Result<(), GitFailure> {
let refs: Vec<RefSnapshotEntry> = list_real_refs(context, cwd)?;
match ref_format {
RefFormat::Files => {
write_private_file(
PathBuf::from(shadow).join("packed-refs").as_path(),
format_packed_refs(&refs, target_ref).as_bytes(),
)
.map_err(|error| { return GitFailure(format!("writing packed-refs failed: {error}")); })?;
}
RefFormat::Reftable => {
let mut lines: String = String::new();
for entry in refs
.iter()
.filter(|entry| { return entry.symref.is_empty() && entry.name != target_ref; })
{
lines.push_str(format!("create {} {}\n", entry.name, entry.oid).as_str());
}
let mut request: GitRequest = shadow_request(shadow, shadow, &["update-ref", "--stdin"]);
request.input = Some(lines.into_bytes());
run_git_checked(context, &request)?;
}
}
for entry in refs.iter().filter(|entry| { return !entry.symref.is_empty(); }) {
run_git_checked(
context,
&shadow_request(shadow, shadow, &["symbolic-ref", entry.name.as_str(), entry.symref.as_str()]),
)?;
}
return write_shadow_head(context, shadow, symbolic_head, base);
}
/// Shadow ref controls stay out of the release executable.
#[cfg(test)]
#[path = "shadow_refs_tests.rs"]
mod tests;