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
//! Partial-clone promisor bookkeeping used by Grit.
//!
//! Git records missing objects via the promisor protocol; Grit uses a marker file
//! (`grit-promisor-missing`) so commands like `rev-list --missing=print` and
//! `backfill` can track which blob OIDs are not present locally.
//!
//! Pack files with a sibling `.promisor` marker (e.g. `pack-abc.promisor` next to
//! `pack-abc.pack`) match Git's promisor packs: objects in those packs are treated as
//! promised by the remote for `fsck` and `rev-list --exclude-promisor-objects`.
use std::collections::{HashSet, VecDeque};
use std::fs;
use std::path::Path;
use crate::config::ConfigSet;
use crate::error::Result;
use crate::objects::{parse_commit, parse_tag, ObjectId, ObjectKind, TreeRefIter};
use crate::odb::Odb;
use crate::repo::Repository;
use std::ops::ControlFlow;
/// Basename of the marker file under the git directory.
pub const PROMISOR_MISSING_FILE: &str = "grit-promisor-missing";
/// Read OIDs listed in the promisor-missing marker (40-char hex lines).
///
/// Order matches the file; duplicate lines are skipped after the first.
#[must_use]
pub fn read_promisor_missing_oids(git_dir: &Path) -> Vec<ObjectId> {
let path = git_dir.join(PROMISOR_MISSING_FILE);
let content = match fs::read_to_string(&path) {
Ok(c) => c,
Err(_) => return Vec::new(),
};
let mut seen = HashSet::new();
let mut out = Vec::new();
for line in content.lines() {
let t = line.trim();
if t.len() != 40 || !t.chars().all(|c| c.is_ascii_hexdigit()) {
continue;
}
if let Ok(oid) = ObjectId::from_hex(t) {
if seen.insert(oid) {
out.push(oid);
}
}
}
out
}
/// Rewrite the promisor-missing marker from a set of OIDs (sorted, one per line).
pub fn write_promisor_marker(git_dir: &Path, oids: &HashSet<ObjectId>) -> Result<()> {
let path = git_dir.join(PROMISOR_MISSING_FILE);
let mut v: Vec<String> = oids.iter().map(|o| o.to_hex()).collect();
v.sort();
if v.is_empty() {
fs::write(&path, b"")?;
} else {
fs::write(&path, format!("{}\n", v.join("\n")))?;
}
Ok(())
}
/// Returns `true` when the repository should honor promisor pack semantics (Git's
/// `repo_has_promisor_remote`): `extensions.partialclone` is set or some
/// `remote.*.promisor=true`.
#[must_use]
pub fn repo_treats_promisor_packs(_git_dir: &Path, config: &ConfigSet) -> bool {
if config.get("extensions.partialclone").is_some() {
return true;
}
config
.entries()
.iter()
.any(|e| e.key.ends_with(".promisor") && e.value.as_deref() == Some("true"))
}
/// All object IDs stored in packfiles that have a sibling `.promisor` marker file.
#[must_use]
pub fn promisor_pack_object_ids(objects_dir: &Path) -> HashSet<ObjectId> {
let odb = Odb::new(objects_dir);
let Ok(primary) = odb.primary() else {
return HashSet::new();
};
let mut ids = HashSet::new();
let _ = primary
.packed_objects()
.for_each_promisor_object(&mut |oid| {
ids.insert(*oid);
ControlFlow::Continue(())
});
ids
}
/// Every `tag.object` OID for tag objects stored in promisor packs (the peeled target).
///
/// Used for `rev-list --exclude-promisor-objects` when a missing commit was promised only via an
/// annotated tag object still in the promisor pack (t0410).
#[must_use]
pub fn promisor_pack_peeled_tag_targets(repo: &Repository) -> HashSet<ObjectId> {
let seeds = promisor_pack_object_ids(repo.odb().objects_dir());
let mut out = HashSet::new();
for oid in seeds {
let Ok(obj) = repo.odb().read(&oid) else {
continue;
};
if obj.kind != ObjectKind::Tag {
continue;
}
let Ok(t) = parse_tag(&obj.data) else {
continue;
};
out.insert(t.object);
}
out
}
/// Promisor pack member OIDs plus peeled tag targets (one hop from each tag in the pack).
pub fn promisor_pack_and_tag_targets(repo: &Repository) -> Result<HashSet<ObjectId>> {
let seeds = promisor_pack_object_ids(repo.odb().objects_dir());
let mut out = seeds.clone();
for oid in seeds {
let obj = match repo.odb().read(&oid) {
Ok(o) => o,
Err(_) => continue,
};
if obj.kind != ObjectKind::Tag {
continue;
}
let t = parse_tag(&obj.data)?;
out.insert(t.object);
}
Ok(out)
}
/// Objects considered "promisor" for traversal, matching Git's `is_promisor_object` set:
/// every object in promisor packs plus referenced OIDs (commit tree/parents, tag targets,
/// tree entry OIDs), and [`read_promisor_missing_oids`] entries. Missing targets
/// remain promises; this classification never fetches from the remote.
///
/// # Errors
///
/// Returns parse errors for invalid available objects and I/O errors for unreadable
/// pack indexes or object storage.
pub fn promisor_expanded_object_ids(repo: &Repository) -> Result<HashSet<ObjectId>> {
maintenance_promise_boundary(repo.odb())
}
/// Classify promises using only available objects. Never hydrate missing targets while
/// deciding which objects maintenance may omit from local packs and reachability walks.
///
/// A custom primary store has no promisor packs, so only the recorded missing ids apply.
pub(crate) fn maintenance_promise_boundary(odb: &Odb) -> Result<HashSet<ObjectId>> {
let mut queue = VecDeque::new();
if odb.uses_files_primary() {
odb.primary()?
.packed_objects()
.for_each_promisor_object(&mut |oid| {
queue.push_back(*oid);
ControlFlow::Continue(())
})?;
}
let mut set: HashSet<ObjectId> = HashSet::new();
while let Some(oid) = queue.pop_front() {
if !set.insert(oid) {
continue;
}
match odb.read_info_without_fetch(&oid) {
Ok(info) if info.kind == ObjectKind::Blob => continue,
Err(crate::error::Error::ObjectNotFound(_)) => continue,
Err(err) => return Err(err),
Ok(_) => {}
}
let Some(obj) = odb.try_read_once(&oid)? else {
continue;
};
match obj.kind {
ObjectKind::Commit => {
let c = parse_commit(&obj.data)?;
queue.push_back(c.tree);
for p in c.parents {
queue.push_back(p);
}
}
ObjectKind::Tag => {
let t = parse_tag(&obj.data)?;
queue.push_back(t.object);
}
ObjectKind::Tree => {
for entry in TreeRefIter::with_oid_len(&obj.data, odb.hash_algo().len()) {
let entry = entry?;
if entry.mode == 0o160000 {
continue;
}
if entry.mode & 0o170000 == 0o040000 {
queue.push_back(entry.oid);
} else {
// Files are leaves: record the promise without probing absent blobs.
set.insert(entry.oid);
}
}
}
ObjectKind::Blob => {}
}
}
if let Some(git_dir) = odb.config_git_dir() {
set.extend(read_promisor_missing_oids(git_dir));
}
Ok(set)
}