use std::path::PathBuf;
use std::time::Duration;
use haematite::store::NodeCacheBudget;
use super::{DEFAULT_LONG_WAIT_INTERVAL, parse_long_wait_interval, read_config};
use crate::serve::contracts::placeable;
use crate::serve::protocol::{BranchDiscard, Change, Fork, Mutation, Selection, Version};
use crate::serve::storage::{Captured, Storage};
pub const BRANCH_RENAME_USAGE: &str = "usage: haematite branch-rename --config <file.json> \
--from <name> --to <name> (--check | --apply) [--long-wait-interval <duration>] \
(Unix; offline; example: --long-wait-interval 30s)";
#[derive(Debug, PartialEq, Eq)]
struct Request {
config: PathBuf,
from: String,
to: String,
apply: bool,
long_wait_interval: Duration,
}
pub fn branch_rename(args: impl Iterator<Item = String>) -> Result<(), String> {
let request = parse(args)?;
if request.from == "main" {
return Err(
"main_protected: main cannot be renamed because it cannot be discarded".to_owned(),
);
}
placeable(&request.to).map_err(|failure| failure.to_string())?;
let config = read_config(&request.config)?;
let mut storage = Storage::open_kept(
&config.data_dir,
NodeCacheBudget::Bytes(config.branch_cache_bytes_per_shard),
config.kept_limit()?,
request.long_wait_interval,
)
.map_err(|error| error.to_string())?;
let source = capture(&storage, &request.from)?
.ok_or_else(|| format!("branch_absent: no branch is named {:?}", request.from))?;
let renamed = capture(&storage, &request.to)?;
let fork_id = request_id("fork", &request);
if let Some(renamed) = &renamed
&& !forked_from(&source, renamed)
{
return Err(format!(
"branch_exists: {:?} already names a branch not forked from {:?}'s head",
request.to, request.from
));
}
if !request.apply {
println!(
"would fork {:?} from {:?} at {}.{}, check its roots on {} shards, then discard {:?}",
request.to,
request.from,
source.version.generation,
source.version.sequence,
source.shards.len(),
request.from
);
return Ok(());
}
storage
.mutate(&Mutation {
request_id: fork_id,
branch: request.from.clone(),
expected: source.version,
change: Change::Fork(Fork {
name: request.to.clone(),
from: source.version,
}),
})
.map_err(|failure| failure.to_string())?;
let forked = capture(&storage, &request.to)?.ok_or_else(|| {
format!(
"rename_roots_differ: {:?} is absent after its fork",
request.to
)
})?;
if !forked_from(&source, &forked) {
return Err(format!(
"rename_roots_differ: {:?} was not forked from {:?}'s head; {:?} is kept",
request.to, request.from, request.from
));
}
storage
.mutate(&Mutation {
request_id: request_id("discard", &request),
branch: request.from.clone(),
expected: source.version,
change: Change::Discard(BranchDiscard {
name: request.from.clone(),
}),
})
.map_err(|failure| failure.to_string())?;
println!(
"renamed {:?} to {:?} at {}.{}",
request.from, request.to, forked.version.generation, forked.version.sequence
);
Ok(())
}
fn capture(storage: &Storage, branch: &str) -> Result<Option<Captured>, String> {
let head: Option<Version> = storage
.catalogue()
.head(branch)
.map_err(|failure| failure.to_string())?;
head.map(|version| {
storage
.capture(&Selection {
branch: branch.to_owned(),
version: Some(version),
})
.map_err(|failure| failure.to_string())
})
.transpose()
}
fn forked_from(source: &Captured, forked: &Captured) -> bool {
source.shards.len() == forked.shards.len()
&& source
.shards
.iter()
.zip(&forked.shards)
.all(|(source, forked)| {
(source.shard_id, source.head) == (forked.shard_id, forked.fork_anchor)
})
}
fn request_id(step: &str, request: &Request) -> String {
format!(
"branch-rename {step} {:?} to {:?}",
request.from, request.to
)
}
fn parse(mut args: impl Iterator<Item = String>) -> Result<Request, String> {
let (mut config, mut from, mut to, mut apply, mut long_wait) = (None, None, None, None, None);
while let Some(arg) = args.next() {
match arg.as_str() {
"--check" if apply.is_none() => apply = Some(false),
"--apply" if apply.is_none() => apply = Some(true),
"--config" | "--from" | "--to" | "--long-wait-interval" => {
let value = args.next().ok_or(BRANCH_RENAME_USAGE)?;
let slot = match arg.as_str() {
"--config" => &mut config,
"--from" => &mut from,
"--to" => &mut to,
_ => &mut long_wait,
};
if slot.replace(value).is_some() {
return Err(format!("{arg} is repeated\n{BRANCH_RENAME_USAGE}"));
}
}
_ => {
return Err(format!(
"unknown or repeated argument {arg:?}\n{BRANCH_RENAME_USAGE}"
));
}
}
}
let from = from.ok_or(BRANCH_RENAME_USAGE)?;
let to = to.ok_or(BRANCH_RENAME_USAGE)?;
if from == to {
return Err(format!(
"--from and --to name the same branch\n{BRANCH_RENAME_USAGE}"
));
}
Ok(Request {
config: PathBuf::from(config.ok_or(BRANCH_RENAME_USAGE)?),
from,
to,
apply: apply.ok_or(BRANCH_RENAME_USAGE)?,
long_wait_interval: long_wait
.as_deref()
.map(parse_long_wait_interval)
.transpose()?
.unwrap_or(DEFAULT_LONG_WAIT_INTERVAL),
})
}
#[cfg(test)]
#[path = "branch_rename_tests.rs"]
mod tests;