use crate::daemon::Daemon;
use crate::error::Error;
use crate::lock;
use crate::paths::Tree;
use crate::settings::{self, Change, Settings};
use crate::state::{State, branch_refusal};
pub async fn set_branch<D: Daemon>(
daemon: &D,
tree: &Tree,
branch: &str,
) -> Result<(Option<String>, State), Error> {
let sheep = tree.sheep();
if let Some(why) = branch_refusal(branch) {
return Err(Error::Config(format!("{sheep}: `branch` {why}")));
}
let held = settings::read(daemon, sheep).await?;
if State::read(&tree.state_file())?.deployed.is_none() {
return Err(Error::Config(format!(
"{sheep} has a deploy tree but was never cut over to it, so a branch set now \
would not describe the release the cutover lands. Finish the cutover instead - \
remove {root} and run `shep-deploy setup {sheep}`.",
root = tree.root().display(),
)));
}
settings::change(daemon, sheep, [Change::Branch(branch.to_owned())]).await?;
let _record = lock::hold_record(tree)?;
let mut state = State::read(&tree.state_file())?;
state.adopt(
sheep,
&Settings {
branch: Some(branch.to_owned()),
..held.clone()
},
)?;
state.failed = None;
state.write(&tree.state_file())?;
Ok((held.branch, state))
}
pub async fn branch_once<D: Daemon>(daemon: &D, tree: &Tree, branch: &str) -> Result<u8, Error> {
let (was, state) = set_branch(daemon, tree, branch).await?;
println!(
"{} branch: {} -> {}, still deployed at {}",
tree.sheep(),
was.as_deref().unwrap_or("none"),
state.branch,
state.deployed.as_deref().unwrap_or("nothing yet")
);
Ok(0)
}
#[cfg(test)]
mod tests {
use crate::error::Error;
use crate::paths::Tree;
use crate::state::{State, Verify, Watch};
use crate::{fixtures, lock};
use super::*;
const TABLE: &str = "branch = 'main'\nwatch = 'auto'\nverify = 'alive'\ncommand = 'make'\n";
fn deployed_tree(home: &std::path::Path) -> (Tree, fixtures::Sheep) {
let tree = Tree::for_sheep(home, "web");
std::fs::create_dir_all(tree.root()).expect("tree");
State {
deployed: Some(fixtures::SHA.to_owned()),
failed: Some(fixtures::OTHER_SHA.to_owned()),
..fixtures::state()
}
.write(&tree.state_file())
.expect("the record before");
(tree, fixtures::Sheep::of("web", TABLE))
}
#[tokio::test]
async fn a_branch_is_written_to_the_table_and_the_hold_is_lifted() {
let home = tempfile::tempdir().expect("tempdir");
let (tree, sheep) = deployed_tree(home.path());
let (was, after) = set_branch(&sheep, &tree, "release").await.expect("sets");
assert_eq!(was.as_deref(), Some("main"), "what the table held before");
let held = sheep.settings();
assert_eq!(held.branch.as_deref(), Some("release"));
assert_eq!(held.watch, Watch::Auto);
assert_eq!(held.verify, Verify::Alive);
assert_eq!(held.command.as_deref(), Some("make"));
let written = State::read(&tree.state_file()).expect("reads");
assert_eq!(written.failed, None);
assert_eq!(written.deployed.as_deref(), Some(fixtures::SHA));
assert_eq!(after.branch, "release", "the record returned is adopted");
assert_eq!(after.failed, None);
let on_disk = std::fs::read_to_string(tree.state_file()).expect("reads");
assert!(!on_disk.contains("release"), "{on_disk}");
}
#[tokio::test]
async fn a_full_ref_is_written_as_given() {
let home = tempfile::tempdir().expect("tempdir");
let (tree, sheep) = deployed_tree(home.path());
set_branch(&sheep, &tree, "refs/pull/1/head")
.await
.expect("sets");
assert_eq!(sheep.settings().branch.as_deref(), Some("refs/pull/1/head"));
}
#[tokio::test]
async fn a_branch_that_cannot_be_followed_is_refused_and_nothing_is_written() {
let home = tempfile::tempdir().expect("tempdir");
let (tree, sheep) = deployed_tree(home.path());
for name in ["--upload-pack=x", "", " ", "refs/heads/a..b"] {
let err = set_branch(&sheep, &tree, name).await.expect_err("refuses");
assert!(matches!(err, Error::Config(_)), "{name:?}: {err}");
}
assert_eq!(sheep.settings().branch.as_deref(), Some("main"));
let on_disk = State::read(&tree.state_file()).expect("still reads");
assert_eq!(on_disk.failed.as_deref(), Some(fixtures::OTHER_SHA));
}
#[tokio::test]
async fn a_tree_the_cutover_never_landed_on_cannot_be_retargeted() {
let home = tempfile::tempdir().expect("tempdir");
let tree = Tree::for_sheep(home.path(), "web");
std::fs::create_dir_all(tree.root()).expect("tree");
fixtures::state()
.write(&tree.state_file())
.expect("the record before");
let sheep = fixtures::Sheep::of("web", TABLE);
let err = set_branch(&sheep, &tree, "release")
.await
.expect_err("refuses");
let shown = err.to_string();
assert!(matches!(err, Error::Config(_)), "{shown}");
assert!(shown.contains("never cut over"), "{shown}");
assert!(shown.contains("shep-deploy setup web"), "{shown}");
assert_eq!(sheep.settings().branch.as_deref(), Some("main"));
}
#[test]
fn a_branch_waits_for_the_record_lock() {
let home = tempfile::tempdir().expect("tempdir");
let (tree, _) = deployed_tree(home.path());
let held = lock::hold_record(&tree).expect("the deploy's hold");
let (tx, rx) = std::sync::mpsc::channel();
let operator = {
let home = home.path().to_owned();
std::thread::spawn(move || {
let sheep = fixtures::Sheep::of("web", TABLE);
let result = tokio::runtime::Builder::new_current_thread()
.build()
.expect("runtime")
.block_on(set_branch(
&sheep,
&Tree::for_sheep(&home, "web"),
"release",
));
tx.send(()).expect("report");
result
})
};
assert!(
rx.recv_timeout(std::time::Duration::from_millis(300))
.is_err(),
"set_branch must wait for the record lock"
);
drop(held);
rx.recv_timeout(std::time::Duration::from_secs(5))
.expect("set_branch lands");
operator.join().expect("thread").expect("sets");
}
}