diff --git a/.gitignore b/.gitignore index ea8c4bf..c35a4ed 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +/work diff --git a/Cargo.lock b/Cargo.lock index b62e2bd..0ea7ce1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1023,6 +1023,15 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "signal-hook-registry" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +dependencies = [ + "libc", +] + [[package]] name = "slab" version = "0.4.11" @@ -1149,6 +1158,7 @@ dependencies = [ "libc", "mio", "pin-project-lite", + "signal-hook-registry", "socket2 0.6.1", "tokio-macros", "windows-sys 0.61.2", diff --git a/Cargo.toml b/Cargo.toml index 234ebc4..b3ff8a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ opentelemetry-otlp = { version = "0.13.0", optional = true } opentelemetry-semantic-conventions = { version = "0.12.0", optional = true } serde = { version = "1.0.228", default-features = false, features = ["std", "derive"] } serde_json = { version = "1.0.149", default-features = false, features = ["std"] } -tokio = { version = "1.49.0", default-features = false, features = ["rt", "rt-multi-thread", "fs", "io-util"] } +tokio = { version = "1.49.0", default-features = false, features = ["rt", "rt-multi-thread", "fs", "io-util", "process"] } toml = { version = "0.9.11", default-features = false, features = ["display", "parse", "serde", "std"] } tracing = { version = "0.1.37", optional = true } tracing-opentelemetry = { version = "0.20.0", optional = true } diff --git a/example_config.toml b/example_config.toml index 2e35173..e752a45 100644 --- a/example_config.toml +++ b/example_config.toml @@ -1,17 +1,17 @@ -work_directory = "/home/nixworker/persist/nix_builder" +# work_directory = "/home/nixworker/persist/nix_builder" [[targets]] -name = "odo" -repo = "https://code.fizz.buzz/talexander/machine_setup.git" -branch = "nix" -path = "nix/configuration" -attr = "odo" + name = "odo" + repo = "https://code.fizz.buzz/talexander/machine_setup.git" + branch = "nix" + path = "nix/configuration" + attr = "odo" [[targets]] -name = "odo_update" -repo = "https://code.fizz.buzz/talexander/machine_setup.git" -branch = "nix" -path = "nix/configuration" -attr = "odo" -update = true -update_branch = "nix_update" + name = "odo_update" + repo = "https://code.fizz.buzz/talexander/machine_setup.git" + branch = "nix" + path = "nix/configuration" + attr = "odo" + update = true + update_branch = "nix_update" diff --git a/src/command/build/runner.rs b/src/command/build/runner.rs index b14d18f..702651a 100644 --- a/src/command/build/runner.rs +++ b/src/command/build/runner.rs @@ -2,6 +2,10 @@ use crate::cli::parameters::BuildArgs; use crate::config::Config; use crate::config::TargetConfig; use crate::error::CustomError; +use crate::fs_util::assert_directory; +use crate::fs_util::is_git_repo; +use crate::git_util::git_force_into_state; +use crate::git_util::git_init_at_rev; pub(crate) async fn run_build(args: BuildArgs) -> Result<(), CustomError> { println!("{:?}", args); @@ -18,12 +22,41 @@ pub(crate) async fn run_build(args: BuildArgs) -> Result<(), CustomError> { } }; - prepare_flake_repo(target_config).await?; + prepare_flake_repo(&config, target_config).await?; } Ok(()) } -async fn prepare_flake_repo(target_config: &TargetConfig) -> Result<(), CustomError> { - todo!() +async fn prepare_flake_repo( + config_root: &Config, + target_config: &TargetConfig, +) -> Result<(), CustomError> { + let flake_directory = target_config.get_flake_directory(config_root)?; + assert_directory!( + &flake_directory, + "Creating flake directory {}", + (&flake_directory).to_string_lossy() + ); + + if is_git_repo(&flake_directory).await? { + // Clean up the existing clone + git_force_into_state( + &flake_directory, + target_config.get_branch()?, + "origin", + target_config.get_repo()?, + target_config.get_revision()?, + ) + .await?; + } else { + git_init_at_rev( + &flake_directory, + target_config.get_branch()?, + target_config.get_repo()?, + target_config.get_revision()?, + ) + .await?; + } + Ok(()) } diff --git a/src/config/target.rs b/src/config/target.rs index 57c0cba..07c4ffa 100644 --- a/src/config/target.rs +++ b/src/config/target.rs @@ -12,16 +12,23 @@ use super::Config; pub(crate) struct TargetConfig { pub(super) name: String, - #[serde(default)] - pub(super) repo: Option, + pub(super) repo: String, + #[serde(default)] pub(super) branch: Option, + + #[serde(default)] + pub(super) revision: Option, + #[serde(default)] pub(super) path: Option, + #[serde(default)] pub(super) attr: Option, + #[serde(default)] pub(super) update: Option, + #[serde(default)] pub(super) update_branch: Option, } @@ -42,4 +49,20 @@ impl TargetConfig { let target_directory = self.get_target_directory(config_root)?; Ok(Cow::Owned(target_directory.join("flake"))) } + + pub(crate) fn get_repo(&'_ self) -> Result<&String, CustomError> { + Ok(&self.repo) + } + + pub(crate) fn get_branch(&'_ self) -> Result, CustomError> { + if let Some(b) = &self.branch { + Ok(Cow::Borrowed(b)) + } else { + Ok(Cow::Borrowed("main")) + } + } + + pub(crate) fn get_revision(&self) -> Result<&Option, CustomError> { + Ok(&self.revision) + } } diff --git a/src/fs_util.rs b/src/fs_util.rs new file mode 100644 index 0000000..013fe4f --- /dev/null +++ b/src/fs_util.rs @@ -0,0 +1,36 @@ +macro_rules! assert_directory { + ($dir:expr, $($params:tt)*) => {{ + let dir = $dir; + let metadata = tokio::fs::metadata(&dir).await; + match metadata { + Ok(metadata) if !metadata.is_dir() => { + tracing::info!($($params)*); + tokio::fs::create_dir_all(dir).await?; + } + Err(_) => { + tracing::info!($($params)*); + tokio::fs::create_dir_all(dir).await?; + } + Ok(_) => {} + }; + }}; +} +use std::path::Path; + +pub(crate) use assert_directory; + +use crate::error::CustomError; + +pub(crate) async fn is_directory>(dir: D) -> Result { + let metadata = tokio::fs::metadata(dir).await; + let result = match metadata { + Ok(metadata) if metadata.is_dir() => true, + _ => false, + }; + Ok(result) +} + +pub(crate) async fn is_git_repo>(dir: D) -> Result { + let dot_git = dir.as_ref().join(".git"); + is_directory(dot_git).await +} diff --git a/src/git_util/core.rs b/src/git_util/core.rs new file mode 100644 index 0000000..127bac3 --- /dev/null +++ b/src/git_util/core.rs @@ -0,0 +1,236 @@ +use tokio::process::Command; + +use crate::error::CustomError; +use std::ffi::OsStr; +use std::path::Path; + +pub(crate) async fn git_init(dest: P, branch: B) -> Result<(), CustomError> +where + P: AsRef, + B: AsRef, +{ + // git -C $dest init --initial-branch=$branch + + let mut command = Command::new("git"); + let dest = AsRef::::as_ref(dest.as_ref()); + command.arg("-C"); + command.arg(dest); + command.args(&["init"]); + command.arg(format!("--initial-branch={}", branch.as_ref())); + command.kill_on_drop(true); + let output = command.output().await?; + if !output.status.success() { + return Err(format!( + "Failing to init git repository: {}", + String::from_utf8(output.stderr)? + ) + .into()); + } + Ok(()) +} + +pub(crate) async fn git_remote_add(dest: P, name: N, url: U) -> Result<(), CustomError> +where + P: AsRef, + N: AsRef, + U: AsRef, +{ + // git -C $dest remote add $name $url + + let dest = AsRef::::as_ref(dest.as_ref()); + let name = name.as_ref(); + let url = url.as_ref(); + let mut command = Command::new("git"); + command.arg("-C"); + command.arg(dest); + command.args(&["remote", "add"]); + command.arg(name); + command.arg(url); + command.kill_on_drop(true); + let output = command.output().await?; + if !output.status.success() { + return Err(format!( + "Failing to add remote to git repo: {}", + String::from_utf8(output.stderr)? + ) + .into()); + } + Ok(()) +} + +pub(crate) async fn git_fetch( + dest: P, + name: N, + rev: R, + depth: Option, + blobless: bool, + treeless: bool, +) -> Result<(), CustomError> +where + P: AsRef, + N: AsRef, + R: AsRef, +{ + // git -C $dest fetch [--depth=$depth] [--filter=blob:none] [--filter=tree:0] $name $rev + + let dest = AsRef::::as_ref(dest.as_ref()); + let name = name.as_ref(); + let mut command = Command::new("git"); + command.arg("-C"); + command.arg(dest); + command.args(&["fetch"]); + if let Some(d) = depth { + command.arg(format!("--depth={}", d)); + } + if blobless { + command.arg("--filter=blob:none"); + } + if treeless { + command.arg("--filter=tree:0"); + } + command.arg(name); + command.arg(rev.as_ref()); + command.kill_on_drop(true); + let output = command.output().await?; + if !output.status.success() { + return Err(format!( + "Failing to git fetch: {}", + String::from_utf8(output.stderr)? + ) + .into()); + } + Ok(()) +} + +pub(crate) async fn git_checkout( + dest: P, + rev: R, + force: bool, + detach: bool, +) -> Result<(), CustomError> +where + P: AsRef, + R: AsRef, +{ + // git -C $dest checkout [--force] [--detach] $rev + + let dest = AsRef::::as_ref(dest.as_ref()); + let mut command = Command::new("git"); + command.arg("-C"); + command.arg(dest); + command.args(&["checkout"]); + if force { + command.arg("--force"); + } + if detach { + command.arg("--detach"); + } + command.arg(rev.as_ref()); + command.kill_on_drop(true); + let output = command.output().await?; + if !output.status.success() { + return Err(format!( + "Failing to git checkout: {}", + String::from_utf8(output.stderr)? + ) + .into()); + } + Ok(()) +} + +pub(crate) async fn git_get_remote(dest: P, remote: R) -> Result +where + P: AsRef, + R: AsRef, +{ + // git -C $dest remote get-url $remote + + let dest = AsRef::::as_ref(dest.as_ref()); + let mut command = Command::new("git"); + command.arg("-C"); + command.arg(dest); + command.args(&["remote", "get-url"]); + command.arg(remote.as_ref()); + command.kill_on_drop(true); + let output = command.output().await?; + if !output.status.success() { + return Err(format!( + "Failing to get git remote: {}", + String::from_utf8(output.stderr)? + ) + .into()); + } + let url = { + let mut url = String::from_utf8(output.stdout)?; + url.truncate(url.trim_ascii_end().len()); + url + }; + Ok(url) +} + +#[allow(dead_code)] +pub(crate) async fn git_reset(dest: P, hard: bool, rev: R) -> Result<(), CustomError> +where + P: AsRef, + R: AsRef, +{ + // git -C $dest reset [--hard] $rev + + let dest = AsRef::::as_ref(dest.as_ref()); + let rev = rev.as_ref(); + let mut command = Command::new("git"); + command.arg("-C"); + command.arg(dest); + command.args(&["reset"]); + if hard { + command.arg("--hard"); + } + command.arg(rev); + command.kill_on_drop(true); + let output = command.output().await?; + if !output.status.success() { + return Err(format!( + "Failing to git reset: {}", + String::from_utf8(output.stderr)? + ) + .into()); + } + Ok(()) +} + +pub(crate) async fn git_clean

( + dest: P, + recurse_into_untracked_directories: bool, // -d + force: bool, // -f + dont_use_ignore_rules: bool, // -x +) -> Result<(), CustomError> +where + P: AsRef, +{ + // git -C $dest clean [-dfx] + + let dest = AsRef::::as_ref(dest.as_ref()); + let mut command = Command::new("git"); + command.arg("-C"); + command.arg(dest); + command.args(&["clean"]); + if recurse_into_untracked_directories { + command.arg("-d"); + } + if force { + command.arg("--force"); + } + if dont_use_ignore_rules { + command.arg("-x"); + } + command.kill_on_drop(true); + let output = command.output().await?; + if !output.status.success() { + return Err(format!( + "Failing to git clean: {}", + String::from_utf8(output.stderr)? + ) + .into()); + } + Ok(()) +} diff --git a/src/git_util/high_level.rs b/src/git_util/high_level.rs new file mode 100644 index 0000000..0048782 --- /dev/null +++ b/src/git_util/high_level.rs @@ -0,0 +1,85 @@ +use crate::error::CustomError; +use crate::fs_util::assert_directory; +use crate::git_util::git_checkout; +use crate::git_util::git_clean; +use crate::git_util::git_fetch; +use crate::git_util::git_get_remote; +use crate::git_util::git_init; +use crate::git_util::git_remote_add; +use std::path::Path; + +pub(crate) async fn git_init_at_rev( + dest: P, + branch: B, + url: U, + rev: &Option, +) -> Result<(), CustomError> +where + P: AsRef, + B: AsRef, + U: AsRef, +{ + // mkdir -p $dest + // git -C $dest init --initial-branch=main + // git -C $dest remote add origin $url + // git -C $dest fetch --depth 1 origin ($rev|$branch) + // git -C $dest checkout FETCH_HEAD + + assert_directory!(&dest, "Cloning git repo into {}", dest.as_ref().display()); + + let fetch_target = rev.as_deref().unwrap_or(branch.as_ref()); + + git_init(&dest, "main").await?; + git_remote_add(&dest, "origin", url).await?; + git_fetch(&dest, "origin", fetch_target, Some(1), false, false).await?; + git_checkout(&dest, "FETCH_HEAD", false, false).await?; + + Ok(()) +} + +pub(crate) async fn git_force_into_state( + dest: P, + branch: B, + remote: R, + url: U, + rev: &Option, +) -> Result<(), CustomError> +where + P: AsRef, + B: AsRef, + R: AsRef, + U: AsRef, +{ + // Maybe instead + // git -C $dest fetch --depth 1 $remote ($rev|$branch) + // git -C $dest checkout --detach --force FETCH_HEAD + // git -C $dest clean -dfx + + let fetch_target = rev.as_deref().unwrap_or(branch.as_ref()); + + check_remote_correct(&dest, &remote, url).await?; + git_fetch(&dest, &remote, fetch_target, Some(1), false, false).await?; + git_checkout(&dest, "FETCH_HEAD", true, true).await?; + git_clean(&dest, true, true, true).await?; + + Ok(()) +} + +async fn check_remote_correct(folder: F, remote: R, url: U) -> Result<(), CustomError> +where + F: AsRef, + R: AsRef, + U: AsRef, +{ + let existing_remote = git_get_remote(folder, &remote).await?; + if existing_remote != url.as_ref() { + return Err(format!( + "Existing remote {} does not have the expected URL {} != {}", + remote.as_ref(), + existing_remote, + url.as_ref() + ) + .into()); + } + Ok(()) +} diff --git a/src/git_util/mod.rs b/src/git_util/mod.rs new file mode 100644 index 0000000..8227dd4 --- /dev/null +++ b/src/git_util/mod.rs @@ -0,0 +1,4 @@ +mod core; +mod high_level; +pub(crate) use core::*; +pub(crate) use high_level::*; diff --git a/src/main.rs b/src/main.rs index 7a508f2..4ea65eb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,8 @@ mod cli; mod command; mod config; mod error; +mod fs_util; +mod git_util; mod init_tracing; fn main() -> Result {