chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
A git killed part way no longer leaves a mirror's refs stuck
.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 | ||