| 100 |
100 |
|
/// How many namespaces annotate each of `targets`, keyed by the hex id the
|
| 101 |
101 |
|
/// caller passed in.
|
| 102 |
102 |
|
///
|
| 103 |
|
- |
/// This is the log-page path, so it is one [`notes::notes_for_many`] call per
|
| 104 |
|
- |
/// namespace per page and never one lookup per row: `notes_for_many` flattens
|
| 105 |
|
- |
/// the notes tree once and answers every row from that. A repository with no
|
| 106 |
|
- |
/// notes costs one ref scan and stops there.
|
|
103 |
+ |
/// This is the log-page path: every row on the page is answered from one
|
|
104 |
+ |
/// flattened notes tree per namespace, never a lookup per row. The flattening
|
|
105 |
+ |
/// goes through `cache`, so a repository whose notes ref has not moved pays for
|
|
106 |
+ |
/// the walk once rather than once per page view. A repository with no notes
|
|
107 |
+ |
/// costs one ref scan and stops there.
|
| 107 |
108 |
|
///
|
| 108 |
109 |
|
/// Targets absent from the result carry no note; the templates ask with `get`.
|
| 109 |
|
- |
pub fn annotation_counts(repo: &gix::Repository, targets: &[String]) -> HashMap<String, usize> {
|
|
110 |
+ |
pub fn annotation_counts(
|
|
111 |
+ |
repo: &gix::Repository,
|
|
112 |
+ |
cache: ¬es::NotesCache,
|
|
113 |
+ |
targets: &[String],
|
|
114 |
+ |
) -> HashMap<String, usize> {
|
| 110 |
115 |
|
let mut counts = HashMap::new();
|
| 111 |
116 |
|
if targets.is_empty() {
|
| 112 |
117 |
|
return counts;
|
| 122 |
127 |
|
}
|
| 123 |
128 |
|
};
|
| 124 |
129 |
|
|
|
130 |
+ |
// The cache key needs the repository as well as the ref tip: a fork shares
|
|
131 |
+ |
// its notes tree with its parent, so the tip alone collides across repos.
|
|
132 |
+ |
let repo_key = repo.path().to_string_lossy().into_owned();
|
|
133 |
+ |
|
| 125 |
134 |
|
// Keep the caller's spelling of each id: the templates look the count up by
|
| 126 |
135 |
|
// the same string they printed, and re-deriving it from the Oid would make
|
| 127 |
136 |
|
// that lookup depend on two hex encoders agreeing.
|
| 128 |
|
- |
let mut by_oid: HashMap<Oid, &String> = HashMap::with_capacity(targets.len());
|
| 129 |
|
- |
for hex in targets {
|
| 130 |
|
- |
if let Ok(oid) = Oid::from_hex(hex.as_bytes()) {
|
| 131 |
|
- |
by_oid.insert(oid, hex);
|
| 132 |
|
- |
}
|
| 133 |
|
- |
}
|
| 134 |
|
- |
let oids: Vec<Oid> = by_oid.keys().copied().collect();
|
|
137 |
+ |
let parsed: Vec<(Oid, &String)> = targets
|
|
138 |
+ |
.iter()
|
|
139 |
+ |
.filter_map(|hex| Oid::from_hex(hex.as_bytes()).ok().map(|oid| (oid, hex)))
|
|
140 |
+ |
.collect();
|
| 135 |
141 |
|
|
| 136 |
142 |
|
for ns in namespaces {
|
| 137 |
|
- |
match notes::notes_for_many(&engine, ns.tip, &oids) {
|
| 138 |
|
- |
Ok(found) => {
|
| 139 |
|
- |
for target in found.keys() {
|
| 140 |
|
- |
if let Some(hex) = by_oid.get(target) {
|
| 141 |
|
- |
*counts.entry((*hex).clone()).or_insert(0) += 1;
|
| 142 |
|
- |
}
|
| 143 |
|
- |
}
|
|
143 |
+ |
let flattened = match cache.flattened(&engine, &repo_key, &ns) {
|
|
144 |
+ |
Ok(map) => map,
|
|
145 |
+ |
Err(e) => {
|
|
146 |
+ |
tracing::warn!(namespace = %ns.name, error = %e, "batch note lookup failed");
|
|
147 |
+ |
continue;
|
|
148 |
+ |
}
|
|
149 |
+ |
};
|
|
150 |
+ |
for (oid, hex) in &parsed {
|
|
151 |
+ |
if flattened.contains_key(oid) {
|
|
152 |
+ |
*counts.entry((*hex).clone()).or_insert(0) += 1;
|
| 144 |
153 |
|
}
|
| 145 |
|
- |
Err(e) => tracing::warn!(namespace = %ns.name, error = %e, "batch note lookup failed"),
|
| 146 |
154 |
|
}
|
| 147 |
155 |
|
}
|
| 148 |
156 |
|
counts
|
| 231 |
239 |
|
// Three ids, so this takes `notes_for_many`'s flattening branch — the
|
| 232 |
240 |
|
// one the log page actually uses.
|
| 233 |
241 |
|
let page = vec![TARGET.to_string(), "0".repeat(40), "1".repeat(40)];
|
| 234 |
|
- |
let counts = annotation_counts(&repo, &page);
|
|
242 |
+ |
let counts = annotation_counts(&repo, ¬es::NotesCache::default(), &page);
|
| 235 |
243 |
|
|
| 236 |
244 |
|
assert_eq!(counts.get(TARGET).copied(), Some(2));
|
| 237 |
245 |
|
assert_eq!(counts.len(), 1, "unannotated commits carry no entry");
|
| 238 |
246 |
|
}
|
| 239 |
247 |
|
|
|
248 |
+ |
#[test]
|
|
249 |
+ |
fn a_second_page_view_is_answered_from_the_cache() {
|
|
250 |
+ |
let (_tmp, repo) = annotated_repo();
|
|
251 |
+ |
let cache = notes::NotesCache::default();
|
|
252 |
+ |
let page = vec![TARGET.to_string()];
|
|
253 |
+ |
|
|
254 |
+ |
let first = annotation_counts(&repo, &cache, &page);
|
|
255 |
+ |
assert_eq!(cache.len(), 2, "one flattened tree per namespace");
|
|
256 |
+ |
|
|
257 |
+ |
let second = annotation_counts(&repo, &cache, &page);
|
|
258 |
+ |
assert_eq!(first, second);
|
|
259 |
+ |
assert_eq!(cache.len(), 2, "the second view walked nothing new");
|
|
260 |
+ |
}
|
|
261 |
+ |
|
| 240 |
262 |
|
#[test]
|
| 241 |
263 |
|
fn a_page_of_nothing_asks_the_repository_nothing() {
|
| 242 |
264 |
|
let (_tmp, repo) = annotated_repo();
|
| 243 |
|
- |
assert!(annotation_counts(&repo, &[]).is_empty());
|
|
265 |
+ |
assert!(annotation_counts(&repo, ¬es::NotesCache::default(), &[]).is_empty());
|
| 244 |
266 |
|
}
|
| 245 |
267 |
|
|
| 246 |
268 |
|
#[test]
|