chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
Mirrors keep a commit-graph, so walks of history read one file
crates/knotless/src/git.rs+26−0
| @@ -207,6 +207,32 @@ impl Git { | ||
Ok(()) | ||
} | ||
| ||
/// Whether the mirror has a commit-graph, as [`Git::graph`] writes. | ||
pub fn has_graph(&self) -> bool { | ||
let info = self.dir.join("objects/info"); | ||
info.join("commit-graph").exists() || info.join("commit-graphs/commit-graph-chain").exists() | ||
} | ||
| ||
/// Adds to the commit-graph the commits the refs reach that it does | ||
/// not hold yet: each commit's parents and date in one file, which | ||
/// walks of history read rather than each commit from the packs, many | ||
/// times faster. Split, it writes only what is new, and merges its | ||
/// layers as git does. | ||
pub async fn graph(&self) -> Result<()> { | ||
self.run( | ||
&[ | ||
"commit-graph", | ||
"write", | ||
"--reachable", | ||
"--split", | ||
"--no-progress", | ||
], | ||
None, | ||
) | ||
.await?; | ||
Ok(()) | ||
} | ||
| ||
/// Applies `update-ref --stdin` instructions as one transaction. | ||
pub async fn update_refs(&self, script: &str) -> Result<()> { | ||
if !script.is_empty() { | ||
crates/knotless/src/mirror.rs+5−1
| @@ -98,7 +98,8 @@ impl Mirror { | ||
let mut want = live; | ||
want.sort(); | ||
let local = self.git.refs().await?; | ||
if local != want { | ||
let moved = local != want; | ||
if moved { | ||
let mut script = String::new(); | ||
for (name, sha) in &want { | ||
script.push_str(&format!("update {name} {sha}\n")); | ||
| @@ -111,6 +112,9 @@ impl Mirror { | ||
self.git.update_refs(&script).await?; | ||
} | ||
self.git.tidy(PACKS).await?; | ||
if moved || !self.git.has_graph() { | ||
self.git.graph().await?; | ||
crates/knotless/src/mirror.rsline 116A failed graph write fails the sync even though the refs are already updated and the graph is only an optimisation. Consider logging the error and continuing. | ||
} | ||
if let Some(branch) = snap | ||
.repo | ||
.as_ref() | ||
crates/knotless/tests/library.rs+2−0
| @@ -784,6 +784,8 @@ fn a_push_stops_at_the_rate_limit() { | ||
.await | ||
.unwrap(); | ||
mirror.sync(&r, &snap).await.unwrap(); | ||
// Its history is in a commit-graph, for walks to read. | ||
assert!(mirror.git.has_graph()); | ||
// New refs at a commit the PDS has: no pack, one create each, so | ||
// two calls. | ||
let updates: Vec<Update> = (0..250) | ||
docs/design/storage.md+8−0
| @@ -438,6 +438,14 @@ A mirror is created with git's own gc off (`gc.auto=0`). Instead, a sync | ||
that leaves it holding more than 50 packs repacks them into one, keeping | ||
objects no ref reaches. | ||
| ||
A sync that moves the refs, or finds a mirror with no commit-graph, then | ||
writes one (`git commit-graph write --reachable --split`): each commit's | ||
parents and date in one file beside the packs, which walks of history read | ||
rather than each commit from the packs. On the rust repository's 345,000 | ||
commits, a walk that takes 2.4 seconds without one takes 0.17 with it. Split, | ||
the write adds only the commits the graph does not hold yet, so a push costs | ||
it milliseconds. Only the first, of a mirror's whole history, takes long. | ||
| ||
## The git remote helper | ||
| ||
git runs `git-remote-knotless` for `knotless://` URLs. It lists live refs, | ||