Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion entangle/.chainlink/.cache/last-edit-time
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1779594594.8513
1779595965.963836
Binary file modified entangle/.chainlink/issues.db
Binary file not shown.
2 changes: 1 addition & 1 deletion entangle/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion entangle/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "entangle-mirror"
version = "0.1.0-alpha.1"
version = "0.1.0"
edition = "2024"
description = "Easy setup for mirroring GitHub repos to Tangled.org in one command"
license = "MIT"
Expand Down
59 changes: 53 additions & 6 deletions entangle/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,11 +240,43 @@ impl PartialConfig {
let json = serde_json::to_string_pretty(self)
.expect("PartialConfig serialization should never fail");

std::fs::write(path, json).map_err(ConfigError::CannotWriteFile)?;
atomic_write_config(path, &json)?;
Ok(())
}
}

// ---------------------------------------------------------------------------
// Atomic write helper
// ---------------------------------------------------------------------------

/// Write `content` to `path` atomically using a unique temp file in the same
/// directory, then rename.
///
/// The caller is responsible for ensuring the parent directory already exists
/// (both [`Config::save_to_path`] and [`PartialConfig::save_to_path`] call
/// `create_dir_all` before reaching here). The temp file is created with a
/// `.lock` suffix so tools that watch the config directory can recognise it
/// as an in-progress write. If this function returns an error the temp file
/// is cleaned up automatically by `tempfile`'s `Drop` implementation.
fn atomic_write_config(path: &Path, content: &str) -> Result<(), ConfigError> {
use std::io::Write as _;
let dir = path.parent().ok_or_else(|| {
ConfigError::CannotWriteFile(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"config path has no parent directory",
))
})?;
let mut tmp = tempfile::Builder::new()
.suffix(".lock")
.tempfile_in(dir)
.map_err(ConfigError::CannotWriteFile)?;
tmp.write_all(content.as_bytes())
.map_err(ConfigError::CannotWriteFile)?;
tmp.persist(path)
.map_err(|e| ConfigError::CannotWriteFile(e.error))?;
Ok(())
}

// ---------------------------------------------------------------------------
// Error types
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -489,11 +521,10 @@ impl Config {
let json =
serde_json::to_string_pretty(self).expect("Config serialization should never fail");

// ── 3. Write atomically-ish via a newline-terminated string ──────────
// We write the complete serialized string in one call; partial writes
// are unlikely on local filesystems but the worst case is a
// re-run of `entangle setup`, not data loss in the repo.
std::fs::write(path, json).map_err(ConfigError::CannotWriteFile)?;
// ── 3. Write atomically ──────────────────────────────────────────────
// Uses a unique temp file in the same directory + rename so a crash
// mid-write never leaves the config in a truncated state.
atomic_write_config(path, &json)?;

Ok(())
}
Expand Down Expand Up @@ -747,6 +778,22 @@ mod tests {

// ── Save ─────────────────────────────────────────────────────────────────

#[test]
fn save_leaves_no_lock_file_behind() {
// atomic_write_config uses a temp file with a .lock suffix; it must
// be renamed (not left on disk) after a successful write.
let f = NamedTempFile::new().unwrap();
let path = f.path();
let lock_path = path.with_extension("lock");

valid_config().save_to_path(path).unwrap();

assert!(
!lock_path.exists(),
"no .lock file should remain after a successful save"
);
}

#[test]
fn save_creates_parent_directory_if_missing() {
// Create a temp dir, then point save() at a nested path that doesn't exist yet.
Expand Down
Loading