~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/knotless

git that lives in your Atmosphere account

knotless.dev
1 collaborator

knotless 0.13.0 Latest 2 packages, 1 image

1
10

A git killed part way no longer leaves a mirror's refs stuck

Merged chadtmiller.com asked to merge into main from chadtmiller.com/knotless:serve/mirror-stale-lock
.changeset/mirror-stale-lock.mdnew+7−0
@@ -0,0 +1,7 @@
---
default: patch
---
# A git killed part way no longer leaves a repository unreadable
A server or command stopped while it set a mirror's refs could leave git's lock file behind, and every later read of that repository then failed with `cannot lock ref`. The lock is now cleared the next time the refs move.
crates/knotless/src/mirror.rs+54−0
@@ -149,6 +149,7 @@ async fn settle(git: Git, live: Vec<(String, String)>, branch: Option<String>) -
let local = git.refs().await?;
let moved = local != want;
if moved {
unstick(&git.dir.join("refs"))?;
let mut script = String::new();
for (name, sha) in &want {
script.push_str(&format!("update {name} {sha}\n"));
@@ -174,10 +175,63 @@ async fn settle(git: Git, live: Vec<(String, String)>, branch: Option<String>) -
Ok(())
}
/// Removes the lock files of a git killed part way, as by its server being
/// stopped, under `dir`. Left there, git refuses every later update of those
/// refs, and the repository cannot be read. Only [`settle`] sets a mirror's
/// refs, and it holds their lock, so no other git is at them, and git takes
/// no ref to be named `.lock`.
fn unstick(dir: &Path) -> Result<()> {
for entry in std::fs::read_dir(dir)? {
let path = entry?.path();
if path.is_dir() {
unstick(&path)?;
} else if path.extension().is_some_and(|x| x == "lock") {
std::fs::remove_file(&path)?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn a_lock_a_killed_git_left_does_not_stop_the_refs_moving() {
let dir = tempfile::tempdir().unwrap();
let git = Git::init_bare(&dir.path().join("m.git")).await.unwrap();
let tree = git.run(&["mktree"], Some(b"")).await.unwrap();
let tree = String::from_utf8(tree).unwrap();
let commit = [
"-c",
"user.name=a",
"-c",
"user.email=a@a",
"commit-tree",
"-m",
"a",
tree.trim(),
];
let sha = git.run(&commit, None).await.unwrap();
let sha = String::from_utf8(sha).unwrap().trim().to_string();
// One beside the branches, one in a folder under them.
let heads = git.dir.join("refs/heads");
std::fs::create_dir_all(heads.join("web")).unwrap();
std::fs::write(heads.join("bytes.lock"), "").unwrap();
std::fs::write(heads.join("web/menu.lock"), "").unwrap();
let live = vec![
("refs/heads/bytes".to_string(), sha.clone()),
("refs/heads/web/menu".to_string(), sha.clone()),
];
settle(git.clone(), live.clone(), None).await.unwrap();
assert_eq!(git.refs().await.unwrap(), live);
assert!(!heads.join("bytes.lock").exists());
// A branch's own file is left, as the refs move on from there.
let fewer = vec![live[0].clone()];
settle(git.clone(), fewer.clone(), None).await.unwrap();
assert_eq!(git.refs().await.unwrap(), fewer);
}
#[tokio::test]
async fn a_tidy_under_way_is_left_to_finish() {
let dir = tempfile::tempdir().unwrap();
docs/design/storage.md+4−1
@@ -499,7 +499,10 @@ go of either when its process ends, so a crashed process cannot block the
mirror. The transaction runs in a task of its own that
holds the lock until git is done. A caller that stops waiting, such as a
request whose asker left, does not end it, so the next sync never finds
git's own lock files on the refs still there. A new mirror is created under a temporary name and
git's own lock files on the refs still there. A git killed part way, as by
its server being stopped, can still leave one, which would refuse every later
update, so the transaction first removes any lock file under `refs/`: it holds
the lock on the refs, so no other git is at them. A new mirror is created under a temporary name and
renamed into place, so racing processes end up with the same one.
A mirror is created with git's own gc off (`gc.auto=0`). Instead, a sync