gitgrip 1.0.0

Multi-repo workflow tool - manage multiple git repositories as one
Documentation
//! Disposable playground harness for binary-level CLI flows.
//!
//! This is the proving-ground layer for future team-workspace work. It starts
//! from plain local git repos, initializes a workspace via `gr init --from-dirs`,
//! and then drives real `gr` commands against that disposable workspace.

use std::path::{Path, PathBuf};
use tempfile::TempDir;

use super::git_helpers;

pub struct PlaygroundHarness {
    pub _temp: TempDir,
    pub workspace_root: PathBuf,
    pub remotes_dir: PathBuf,
    pub repo_names: Vec<String>,
}

impl PlaygroundHarness {
    pub fn new(repo_names: &[&str]) -> Self {
        let temp = TempDir::new().expect("failed to create temp dir");
        let workspace_root = temp.path().join("playground");
        let remotes_dir = temp.path().join("remotes");
        std::fs::create_dir_all(&workspace_root).expect("failed to create workspace root");
        std::fs::create_dir_all(&remotes_dir).expect("failed to create remotes dir");

        for repo_name in repo_names {
            let bare_path = remotes_dir.join(format!("{}.git", repo_name));
            let staging = temp.path().join(format!("staging-{}", repo_name));
            let repo_path = workspace_root.join(repo_name);

            git_helpers::init_bare_repo(&bare_path);
            git_helpers::init_repo(&staging);
            git_helpers::commit_file(
                &staging,
                "README.md",
                &format!("# {}\n", repo_name),
                "Initial commit",
            );

            let remote_url = format!("file://{}", bare_path.display());
            git_helpers::add_remote(&staging, "origin", &remote_url);
            git_helpers::push_upstream(&staging, "origin", "main");
            git_helpers::clone_repo(&remote_url, &repo_path);
        }

        Self {
            _temp: temp,
            workspace_root,
            remotes_dir,
            repo_names: repo_names.iter().map(|name| (*name).to_string()).collect(),
        }
    }

    pub fn repo_path(&self, name: &str) -> PathBuf {
        self.workspace_root.join(name)
    }

    pub fn remote_url(&self, name: &str) -> String {
        format!(
            "file://{}",
            self.remotes_dir.join(format!("{}.git", name)).display()
        )
    }

    pub fn init_from_dirs(&self) {
        self.run_from_manifest_dir([
            "init",
            "--from-dirs",
            "-p",
            self.workspace_root
                .to_str()
                .expect("workspace path should be utf-8"),
        ]);
        self.ensure_repo_identities();
    }

    pub fn run_in_workspace<I, S>(&self, args: I)
    where
        I: IntoIterator<Item = S>,
        S: AsRef<str>,
    {
        self.run(args, &self.workspace_root);
    }

    pub fn run_from_manifest_dir<I, S>(&self, args: I)
    where
        I: IntoIterator<Item = S>,
        S: AsRef<str>,
    {
        self.run(args, Path::new(env!("CARGO_MANIFEST_DIR")));
    }

    pub fn run_in_workspace_output<I, S>(&self, args: I) -> std::process::Output
    where
        I: IntoIterator<Item = S>,
        S: AsRef<str>,
    {
        self.run_output(args, &self.workspace_root)
    }

    pub fn ensure_repo_identities(&self) {
        for repo_name in &self.repo_names {
            let repo_path = self.repo_path(repo_name);
            if repo_path.join(".git").exists() {
                git_helpers::configure_identity(&repo_path);
            }
        }
    }

    fn run<I, S>(&self, args: I, current_dir: &Path)
    where
        I: IntoIterator<Item = S>,
        S: AsRef<str>,
    {
        let args_vec: Vec<String> = args.into_iter().map(|s| s.as_ref().to_string()).collect();
        let output = self.run_output(args_vec.iter().map(String::as_str), current_dir);
        assert!(
            output.status.success(),
            "gr {:?} failed in {}:\nstdout:\n{}\nstderr:\n{}",
            args_vec,
            current_dir.display(),
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr)
        );
    }

    fn run_output<I, S>(&self, args: I, current_dir: &Path) -> std::process::Output
    where
        I: IntoIterator<Item = S>,
        S: AsRef<str>,
    {
        let args_vec: Vec<String> = args.into_iter().map(|s| s.as_ref().to_string()).collect();
        let mut cmd = std::process::Command::new(assert_cmd::cargo::cargo_bin!("gr"));
        cmd.current_dir(current_dir)
            .args(args_vec.iter().map(String::as_str));
        cmd.output().expect("failed to run gr binary")
    }
}