use std::path::Path;
use serde_json::json;
use crate::value::VmError;
use super::git::{bare_file_url, GitOps};
use super::manifest::{ScenarioBranch, ScenarioManifest, ScenarioRepo};
use super::state::{
manifest_path, playground_marker_path, PlaygroundMarker, PlaygroundState, PLAYGROUND_TYPE,
};
fn default_seed_files(repo: &ScenarioRepo) -> std::collections::BTreeMap<String, String> {
if !repo.files.is_empty() {
return repo.files.clone();
}
let mut files = std::collections::BTreeMap::new();
files.insert(
"README.md".to_string(),
format!("# {}\n\nMerge Captain playground seed.\n", repo.name),
);
files
}
pub struct InitOptions<'a> {
pub dir: &'a Path,
pub manifest: &'a ScenarioManifest,
pub allow_existing: bool,
}
pub fn init_playground_at(options: InitOptions<'_>) -> Result<PlaygroundState, VmError> {
let dir = options.dir;
let manifest = options.manifest;
let marker = playground_marker_path(dir);
if marker.exists() && !options.allow_existing {
return Err(VmError::Runtime(format!(
"playground already initialized at {} (run `harn merge-captain mock cleanup {0}` first)",
dir.display()
)));
}
std::fs::create_dir_all(dir).map_err(|error| {
VmError::Runtime(format!(
"failed to create playground dir {}: {error}",
dir.display()
))
})?;
std::fs::create_dir_all(dir.join("remotes"))
.map_err(|error| VmError::Runtime(format!("failed to create remotes dir: {error}")))?;
std::fs::create_dir_all(dir.join("working"))
.map_err(|error| VmError::Runtime(format!("failed to create working dir: {error}")))?;
let git = GitOps::default();
let mut state = PlaygroundState::from_manifest(manifest);
for repo in &manifest.repos {
materialize_repo(&git, dir, repo, &mut state)?;
}
let manifest_bytes = serde_json::to_vec_pretty(manifest)
.map_err(|error| VmError::Runtime(format!("failed to serialize manifest copy: {error}")))?;
let mut manifest_with_newline = manifest_bytes;
manifest_with_newline.push(b'\n');
std::fs::write(manifest_path(dir), manifest_with_newline)
.map_err(|error| VmError::Runtime(format!("failed to write manifest copy: {error}")))?;
let marker_value = PlaygroundMarker::new(&manifest.scenario, state.now_ms);
let mut marker_bytes = serde_json::to_vec_pretty(&marker_value).map_err(|error| {
VmError::Runtime(format!("failed to serialize playground marker: {error}"))
})?;
marker_bytes.push(b'\n');
std::fs::write(playground_marker_path(dir), marker_bytes)
.map_err(|error| VmError::Runtime(format!("failed to write playground marker: {error}")))?;
state.record(
"init",
json!({
"scenario": manifest.scenario,
"repos": manifest.repos.iter().map(|r| r.name.clone()).collect::<Vec<_>>(),
"pull_requests": manifest.pull_requests.len(),
}),
);
state.save(dir)?;
Ok(state)
}
fn materialize_repo(
git: &GitOps,
dir: &Path,
repo: &ScenarioRepo,
state: &mut PlaygroundState,
) -> Result<(), VmError> {
let bare = dir.join("remotes").join(format!("{}.git", repo.name));
let working = dir.join("working").join(&repo.name);
git.init_bare(&bare, &repo.default_branch)?;
git.clone(&bare, &working)?;
let seed_files = default_seed_files(repo);
let seed_message = format!("Initial seed for {}", repo.name);
let seed_sha = git.commit_overlay(
&working,
&seed_files,
&[],
&seed_message,
Some(&repo.default_branch),
)?;
for extra in &repo.default_branch_extra_commits {
git.commit_overlay(
&working,
&extra.files_set,
&extra.files_delete,
&extra.message,
Some(&repo.default_branch),
)?;
}
for branch in &repo.branches {
materialize_branch(git, &working, repo, branch, &seed_sha)?;
}
let url = bare_file_url(&bare);
if let Some(repo_state) = state.repos.get_mut(&repo.name) {
repo_state.remote_url = url;
repo_state.remote_path = path_relative_to(&bare, dir);
repo_state.working_path = path_relative_to(&working, dir);
}
for pr in state
.pull_requests
.values_mut()
.filter(|pr| pr.repo == repo.name)
{
if pr.head_sha.is_some() {
continue;
}
let sha = git
.rev_parse(&working, &format!("origin/{}", pr.head_branch))
.ok();
pr.head_sha = sha;
}
Ok(())
}
fn materialize_branch(
git: &GitOps,
working: &Path,
repo: &ScenarioRepo,
branch: &ScenarioBranch,
seed_sha: &str,
) -> Result<String, VmError> {
let base_branch = branch
.base
.clone()
.unwrap_or_else(|| repo.default_branch.clone());
let from_ref = if branch.fork_before_extra_commits {
seed_sha.to_string()
} else {
format!("origin/{base_branch}")
};
git.create_branch(working, &branch.name, &from_ref)?;
let message = branch
.commit_message
.clone()
.unwrap_or_else(|| format!("{} on {}", branch.name, repo.name));
let sha = git.commit_overlay(
working,
&branch.files_set,
&branch.files_delete,
&message,
Some(&branch.name),
)?;
git.checkout(working, &repo.default_branch)?;
Ok(sha)
}
fn path_relative_to(target: &Path, base: &Path) -> String {
target
.strip_prefix(base)
.map(|rel| rel.to_string_lossy().to_string())
.unwrap_or_else(|_| target.to_string_lossy().to_string())
}
pub fn cleanup_playground_at(dir: &Path) -> Result<bool, VmError> {
let marker = playground_marker_path(dir);
if !marker.exists() {
if !dir.exists() {
return Ok(false);
}
if dir
.read_dir()
.map(|mut r| r.next().is_none())
.unwrap_or(true)
{
return Ok(false);
}
return Err(VmError::Runtime(format!(
"{} does not look like a Merge Captain playground (missing playground.json marker); refusing to remove",
dir.display()
)));
}
let bytes = std::fs::read(&marker).map_err(|error| {
VmError::Runtime(format!(
"failed to read playground marker {}: {error}",
marker.display()
))
})?;
let parsed: PlaygroundMarker = serde_json::from_slice(&bytes).map_err(|error| {
VmError::Runtime(format!(
"playground marker {} is malformed: {error}",
marker.display()
))
})?;
if parsed.type_name != PLAYGROUND_TYPE {
return Err(VmError::Runtime(format!(
"playground marker {} has wrong _type {:?}; refusing to remove",
marker.display(),
parsed.type_name
)));
}
std::fs::remove_dir_all(dir).map_err(|error| {
VmError::Runtime(format!(
"failed to remove playground dir {}: {error}",
dir.display()
))
})?;
Ok(true)
}
pub fn load_playground(dir: &Path) -> Result<(PlaygroundState, ScenarioManifest), VmError> {
let marker = playground_marker_path(dir);
if !marker.exists() {
return Err(VmError::Runtime(format!(
"{} is not a Merge Captain playground (missing playground.json)",
dir.display()
)));
}
let manifest_bytes = std::fs::read(manifest_path(dir)).map_err(|error| {
VmError::Runtime(format!(
"failed to read playground manifest {}: {error}",
manifest_path(dir).display()
))
})?;
let manifest: ScenarioManifest = serde_json::from_slice(&manifest_bytes).map_err(|error| {
VmError::Runtime(format!("failed to parse playground manifest copy: {error}"))
})?;
let state = PlaygroundState::load(dir)?;
Ok((state, manifest))
}