1use std::fs;
15use std::io::Write;
16use std::path::{Path, PathBuf};
17
18use crate::{fsync, GitError, Result};
19
20pub fn lock_path_for(path: &Path) -> Result<PathBuf> {
23 let Some(file_name) = path.file_name() else {
24 return Err(GitError::InvalidPath(format!(
25 "path has no filename: {}",
26 path.display()
27 )));
28 };
29 let mut lock_name = file_name.to_os_string();
30 lock_name.push(".lock");
31 Ok(path.with_file_name(lock_name))
32}
33
34#[derive(Debug)]
40pub struct LockFile {
41 path: PathBuf,
42 target: Option<PathBuf>,
45 file: Option<fs::File>,
46 armed: bool,
47}
48
49impl LockFile {
50 pub fn create(path: PathBuf) -> Result<Self> {
54 let file = fs::OpenOptions::new()
55 .write(true)
56 .create_new(true)
57 .open(&path)?;
58 Ok(Self {
59 path,
60 target: None,
61 file: Some(file),
62 armed: true,
63 })
64 }
65
66 pub fn acquire(target: &Path) -> Result<Self> {
68 let mut lock = Self::create(lock_path_for(target)?)?;
69 lock.target = Some(target.to_path_buf());
70 Ok(lock)
71 }
72
73 pub fn path(&self) -> &Path {
75 &self.path
76 }
77
78 pub fn target(&self) -> Option<&Path> {
80 self.target.as_deref()
81 }
82
83 pub fn file_mut(&mut self) -> Option<&mut fs::File> {
85 self.file.as_mut()
86 }
87
88 pub fn write_all(&mut self, bytes: &[u8]) -> Result<()> {
90 let Some(file) = self.file.as_mut() else {
91 return Err(GitError::Io("lock file is already closed".into()));
92 };
93 file.write_all(bytes)?;
94 Ok(())
95 }
96
97 pub fn sync(&mut self, policy: &fsync::Policy, component: fsync::FsyncComponents) -> Result<()> {
101 let Some(file) = self.file.as_mut() else {
102 return Err(GitError::Io("lock file is already closed".into()));
103 };
104 policy.apply(file, component)?;
105 Ok(())
106 }
107
108 pub fn persist(self) -> Result<()> {
111 let Some(target) = self.target.clone() else {
112 return Err(GitError::Io(format!(
113 "lock file {} has no publication target",
114 self.path.display()
115 )));
116 };
117 self.persist_into(&target)
118 }
119
120 pub fn persist_into(mut self, target: &Path) -> Result<()> {
124 self.armed = false;
125 let _ = self.file.take();
126 match fs::rename(&self.path, target) {
127 Ok(()) => Ok(()),
128 Err(err) => {
129 let _ = fs::remove_file(&self.path);
130 Err(GitError::Io(err.to_string()))
131 }
132 }
133 }
134
135 pub fn persist_racy(mut self, target: &Path) -> Result<()> {
141 self.armed = false;
142 let _ = self.file.take();
143 match fs::rename(&self.path, target) {
144 Ok(()) => Ok(()),
145 Err(_) if target.exists() => {
146 let _ = fs::remove_file(&self.path);
147 Ok(())
148 }
149 Err(err) => {
150 let _ = fs::remove_file(&self.path);
151 Err(GitError::Io(err.to_string()))
152 }
153 }
154 }
155
156 pub fn keep(mut self) -> (PathBuf, Option<fs::File>) {
159 self.armed = false;
160 (std::mem::take(&mut self.path), self.file.take())
161 }
162}
163
164impl Drop for LockFile {
165 fn drop(&mut self) {
166 if self.armed {
167 self.armed = false;
168 let _ = self.file.take();
169 let _ = fs::remove_file(&self.path);
170 }
171 }
172}
173
174pub fn atomic_write(path: &Path, contents: &[u8]) -> Result<()> {
178 atomic_write_with(path, |file| {
179 file.write_all(contents)?;
180 Ok(())
181 })
182}
183
184pub fn atomic_write_with(
187 path: &Path,
188 write: impl FnOnce(&mut fs::File) -> Result<()>,
189) -> Result<()> {
190 let mut lock = LockFile::acquire(path)?;
191 match lock.file_mut() {
192 Some(file) => write(file)?,
193 None => return Err(GitError::Io("lock file is already closed".into())),
194 }
195 lock.persist()
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use std::sync::atomic::{AtomicU32, Ordering};
202
203 static COUNTER: AtomicU32 = AtomicU32::new(0);
204
205 fn scratch_dir(name: &str) -> PathBuf {
206 let dir = std::env::temp_dir().join(format!(
207 "sley-core-atomic-{name}-{}-{}",
208 std::process::id(),
209 COUNTER.fetch_add(1, Ordering::Relaxed)
210 ));
211 fs::create_dir_all(&dir).expect("create scratch dir");
212 dir
213 }
214
215 #[test]
216 fn lock_path_for_appends_lock_suffix() {
217 assert_eq!(
218 lock_path_for(Path::new("refs/heads/main")).expect("lock path"),
219 PathBuf::from("refs/heads/main.lock")
220 );
221 assert!(lock_path_for(Path::new("")).is_err());
222 }
223
224 #[test]
225 fn acquire_write_persist_replaces_target_atomically() {
226 let dir = scratch_dir("persist");
227 let target = dir.join("packed-refs");
228 fs::write(&target, b"old").expect("seed target");
229
230 let mut lock = LockFile::acquire(&target).expect("acquire");
231 assert_eq!(lock.path(), target.with_file_name("packed-refs.lock"));
232 lock.write_all(b"new").expect("write");
233 lock.persist().expect("persist");
234
235 assert_eq!(fs::read(&target).expect("read target"), b"new");
236 assert!(!target.with_file_name("packed-refs.lock").exists());
237
238 let mut again = LockFile::acquire(&target).expect("re-acquire");
240 again.write_all(b"newer").expect("write");
241 again.persist().expect("persist");
242 assert_eq!(fs::read(&target).expect("read target"), b"newer");
243
244 fs::remove_dir_all(dir).expect("cleanup");
245 }
246
247 #[test]
248 fn dropped_guard_removes_lock_and_atomic_write_fails_when_locked() {
249 let dir = scratch_dir("drop");
250 let target = dir.join("HEAD");
251 let guard = LockFile::acquire(&target).expect("acquire");
252 let lock_path = guard.path().to_path_buf();
253 drop(guard);
254 assert!(!lock_path.exists(), "dropped guard must remove its lock");
255
256 let held = LockFile::acquire(&target).expect("hold");
258 let err = atomic_write(&target, b"x").expect_err("second writer must fail");
259 assert_eq!(err.io_kind(), Some(std::io::ErrorKind::AlreadyExists));
260 drop(held);
261
262 atomic_write(&target, b"payload").expect("atomic write after release");
263 assert_eq!(fs::read(&target).expect("read"), b"payload");
264 fs::remove_dir_all(dir).expect("cleanup");
265 }
266
267 #[test]
268 fn create_keeps_exact_temp_names_for_mkstemp_style_callers() {
269 let dir = scratch_dir("keep");
270 let temp = dir.join("tmp_obj_42_7");
271 let lock = LockFile::create(temp.clone()).expect("create");
272 assert!(temp.exists());
273 let (kept_path, _) = lock.keep();
274 assert_eq!(kept_path, temp);
275 assert!(temp.exists(), "keep() must not delete the file");
276
277 let err = LockFile::create(temp).expect_err("exclusive create");
279 assert_eq!(err.io_kind(), Some(std::io::ErrorKind::AlreadyExists));
280 fs::remove_dir_all(dir).expect("cleanup");
281 }
282
283 #[test]
284 fn persist_racy_succeeds_when_another_writer_landed_first() {
285 let dir = scratch_dir("racy");
286 let target = dir.join("objects/ab/cdef");
287 fs::create_dir_all(target.parent().expect("parent")).expect("fanout");
288 fs::write(&target, b"theirs").expect("concurrent winner");
289
290 let temp = dir.join("tmp_obj_racy");
291 let mut lock = LockFile::create(temp).expect("create temp");
292 lock.write_all(b"ours").expect("write");
293 lock.persist_racy(&target)
296 .expect("racy publish must tolerate the concurrent winner");
297 let contents = fs::read(&target).expect("read");
298 assert!(contents == b"theirs" || contents == b"ours");
299 fs::remove_dir_all(dir).expect("cleanup");
300 }
301}