mod common;
use std::fs;
use std::path::Path;
use serde_yaml::Value;
use common::assertions;
use common::fixtures::WorkspaceBuilder;
use common::git_helpers;
fn set_default_branch(manifest_path: &Path, repo: &str, branch: &str) {
let content = fs::read_to_string(manifest_path).unwrap();
let mut doc: Value = serde_yaml::from_str(&content).unwrap();
assert!(
!doc["repos"][repo].is_null(),
"repo {} missing from manifest",
repo
);
doc["repos"][repo]["default_branch"] = Value::String(branch.to_string());
fs::write(manifest_path, serde_yaml::to_string(&doc).unwrap()).unwrap();
}
#[test]
fn test_tree_add_creates_worktrees() {
let ws = WorkspaceBuilder::new()
.add_repo("app")
.add_repo("lib")
.build();
let manifest = ws.load_manifest();
let result = gitgrip::cli::commands::tree::run_tree_add(
&ws.workspace_root,
&manifest,
"feat/new-feature",
);
assert!(
result.is_ok(),
"tree add should succeed: {:?}",
result.err()
);
let tree_path = ws.workspace_root.parent().unwrap().join("feat-new-feature");
assertions::assert_file_exists(&tree_path);
let pointer_path = tree_path.join(".griptree");
assertions::assert_file_exists(&pointer_path);
let pointer: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&pointer_path).unwrap()).unwrap();
assert_eq!(pointer["branch"], "feat/new-feature");
assert_eq!(
pointer["mainWorkspace"],
ws.workspace_root.to_string_lossy().as_ref()
);
assert!(!pointer["locked"].as_bool().unwrap());
let registry_path = ws.workspace_root.join(".gitgrip").join("griptrees.json");
assertions::assert_file_exists(®istry_path);
let registry: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(®istry_path).unwrap()).unwrap();
assert!(
registry["griptrees"]["feat/new-feature"].is_object(),
"registry should contain the new griptree"
);
assertions::assert_file_exists(&tree_path.join("app"));
assertions::assert_file_exists(&tree_path.join("lib"));
assertions::assert_on_branch(&tree_path.join("app"), "feat/new-feature");
assertions::assert_on_branch(&tree_path.join("lib"), "feat/new-feature");
}
#[test]
fn test_tree_add_writes_repo_upstreams() {
let ws = WorkspaceBuilder::new()
.add_repo("app")
.add_repo("lib")
.build();
git_helpers::create_branch(&ws.repo_path("lib"), "dev");
git_helpers::commit_file(&ws.repo_path("lib"), "dev.txt", "dev", "Add dev");
git_helpers::push_branch(&ws.repo_path("lib"), "origin", "dev");
git_helpers::checkout(&ws.repo_path("lib"), "main");
let manifest_path = ws
.workspace_root
.join(".gitgrip")
.join("manifests")
.join("manifest.yaml");
set_default_branch(&manifest_path, "lib", "dev");
let manifest = ws.load_manifest();
let result =
gitgrip::cli::commands::tree::run_tree_add(&ws.workspace_root, &manifest, "feat/upstream");
assert!(
result.is_ok(),
"tree add should succeed: {:?}",
result.err()
);
let tree_path = ws.workspace_root.parent().unwrap().join("feat-upstream");
let config_path = tree_path.join(".gitgrip").join("griptree.json");
let config: serde_json::Value =
serde_json::from_str(&fs::read_to_string(&config_path).unwrap()).unwrap();
assert_eq!(config["repoUpstreams"]["app"], "origin/main");
assert_eq!(config["repoUpstreams"]["lib"], "origin/dev");
}
#[test]
fn test_tree_add_with_manifest_repo() {
let ws = WorkspaceBuilder::new()
.add_repo("app")
.with_manifest_repo()
.build();
let manifest = ws.load_manifest();
let result = gitgrip::cli::commands::tree::run_tree_add(
&ws.workspace_root,
&manifest,
"feat/with-manifest",
);
assert!(
result.is_ok(),
"tree add with manifest repo should succeed: {:?}",
result.err()
);
let tree_path = ws
.workspace_root
.parent()
.unwrap()
.join("feat-with-manifest");
let tree_manifest_dir = tree_path.join(".gitgrip").join("manifests");
assertions::assert_file_exists(&tree_manifest_dir);
assertions::assert_file_exists(&tree_manifest_dir.join("manifest.yaml"));
let pointer_path = tree_path.join(".griptree");
let pointer: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&pointer_path).unwrap()).unwrap();
assert!(
pointer["manifestBranch"].is_string(),
"pointer should have manifestBranch"
);
}
#[test]
fn test_tree_add_duplicate_fails() {
let ws = WorkspaceBuilder::new().add_repo("app").build();
let manifest = ws.load_manifest();
gitgrip::cli::commands::tree::run_tree_add(&ws.workspace_root, &manifest, "feat/dup")
.expect("first tree add should succeed");
let result =
gitgrip::cli::commands::tree::run_tree_add(&ws.workspace_root, &manifest, "feat/dup");
assert!(result.is_err(), "duplicate tree add should fail");
let err = result.unwrap_err().to_string();
assert!(
err.contains("already exists"),
"error should mention 'already exists': {}",
err
);
}
#[test]
fn test_tree_list_empty() {
let ws = WorkspaceBuilder::new().add_repo("app").build();
let result = gitgrip::cli::commands::tree::run_tree_list(&ws.workspace_root);
assert!(
result.is_ok(),
"tree list with no griptrees should succeed: {:?}",
result.err()
);
}
#[test]
fn test_tree_list_after_add() {
let ws = WorkspaceBuilder::new().add_repo("app").build();
let manifest = ws.load_manifest();
gitgrip::cli::commands::tree::run_tree_add(&ws.workspace_root, &manifest, "feat/listed")
.expect("tree add should succeed");
let result = gitgrip::cli::commands::tree::run_tree_list(&ws.workspace_root);
assert!(
result.is_ok(),
"tree list should succeed: {:?}",
result.err()
);
let registry_path = ws.workspace_root.join(".gitgrip").join("griptrees.json");
let registry: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(®istry_path).unwrap()).unwrap();
assert!(registry["griptrees"]["feat/listed"].is_object());
}
#[test]
fn test_tree_remove() {
let ws = WorkspaceBuilder::new().add_repo("app").build();
let manifest = ws.load_manifest();
gitgrip::cli::commands::tree::run_tree_add(&ws.workspace_root, &manifest, "feat/removeme")
.expect("tree add should succeed");
let tree_path = ws.workspace_root.parent().unwrap().join("feat-removeme");
assert!(
tree_path.exists(),
"griptree dir should exist before remove"
);
let result =
gitgrip::cli::commands::tree::run_tree_remove(&ws.workspace_root, "feat/removeme", false);
assert!(
result.is_ok(),
"tree remove should succeed: {:?}",
result.err()
);
assert!(!tree_path.exists(), "griptree directory should be removed");
let registry_path = ws.workspace_root.join(".gitgrip").join("griptrees.json");
let registry: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(®istry_path).unwrap()).unwrap();
assert!(
registry["griptrees"]["feat/removeme"].is_null(),
"registry should not contain removed griptree"
);
}
#[test]
fn test_tree_remove_nonexistent_fails() {
let ws = WorkspaceBuilder::new().add_repo("app").build();
let config_path = ws.workspace_root.join(".gitgrip").join("griptrees.json");
std::fs::write(&config_path, r#"{"griptrees":{}}"#).unwrap();
let result =
gitgrip::cli::commands::tree::run_tree_remove(&ws.workspace_root, "feat/ghost", false);
assert!(result.is_err(), "removing nonexistent griptree should fail");
let err = result.unwrap_err().to_string();
assert!(
err.contains("not found"),
"error should mention 'not found': {}",
err
);
}
#[test]
fn test_tree_lock_prevents_remove() {
let ws = WorkspaceBuilder::new().add_repo("app").build();
let manifest = ws.load_manifest();
gitgrip::cli::commands::tree::run_tree_add(&ws.workspace_root, &manifest, "feat/locked")
.expect("tree add should succeed");
let lock_result = gitgrip::cli::commands::tree::run_tree_lock(
&ws.workspace_root,
"feat/locked",
Some("important work"),
);
assert!(
lock_result.is_ok(),
"tree lock should succeed: {:?}",
lock_result.err()
);
let registry_path = ws.workspace_root.join(".gitgrip").join("griptrees.json");
let registry: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(®istry_path).unwrap()).unwrap();
assert!(
registry["griptrees"]["feat/locked"]["locked"]
.as_bool()
.unwrap(),
"registry should show locked"
);
assert_eq!(
registry["griptrees"]["feat/locked"]["lock_reason"],
"important work"
);
let tree_path = ws.workspace_root.parent().unwrap().join("feat-locked");
let pointer: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(tree_path.join(".griptree")).unwrap())
.unwrap();
assert!(
pointer["locked"].as_bool().unwrap(),
"pointer should show locked"
);
let remove_result =
gitgrip::cli::commands::tree::run_tree_remove(&ws.workspace_root, "feat/locked", false);
assert!(
remove_result.is_err(),
"removing locked griptree should fail"
);
let err = remove_result.unwrap_err().to_string();
assert!(
err.contains("locked"),
"error should mention 'locked': {}",
err
);
let force_result =
gitgrip::cli::commands::tree::run_tree_remove(&ws.workspace_root, "feat/locked", true);
assert!(
force_result.is_ok(),
"force remove should succeed: {:?}",
force_result.err()
);
assert!(
!tree_path.exists(),
"griptree dir should be removed after force"
);
}
#[test]
fn test_tree_unlock_allows_remove() {
let ws = WorkspaceBuilder::new().add_repo("app").build();
let manifest = ws.load_manifest();
gitgrip::cli::commands::tree::run_tree_add(&ws.workspace_root, &manifest, "feat/unlock-me")
.expect("tree add should succeed");
gitgrip::cli::commands::tree::run_tree_lock(&ws.workspace_root, "feat/unlock-me", None)
.expect("lock should succeed");
let unlock_result =
gitgrip::cli::commands::tree::run_tree_unlock(&ws.workspace_root, "feat/unlock-me");
assert!(
unlock_result.is_ok(),
"unlock should succeed: {:?}",
unlock_result.err()
);
let registry_path = ws.workspace_root.join(".gitgrip").join("griptrees.json");
let registry: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(®istry_path).unwrap()).unwrap();
assert!(
!registry["griptrees"]["feat/unlock-me"]["locked"]
.as_bool()
.unwrap(),
"registry should show unlocked"
);
let remove_result =
gitgrip::cli::commands::tree::run_tree_remove(&ws.workspace_root, "feat/unlock-me", false);
assert!(
remove_result.is_ok(),
"remove after unlock should succeed: {:?}",
remove_result.err()
);
}
#[test]
fn test_tree_add_branch_name_normalization() {
let ws = WorkspaceBuilder::new().add_repo("app").build();
let manifest = ws.load_manifest();
let result = gitgrip::cli::commands::tree::run_tree_add(
&ws.workspace_root,
&manifest,
"fix/bug/critical",
);
assert!(
result.is_ok(),
"tree add with nested slashes should succeed: {:?}",
result.err()
);
let tree_path = ws.workspace_root.parent().unwrap().join("fix-bug-critical");
assertions::assert_file_exists(&tree_path);
assertions::assert_on_branch(&tree_path.join("app"), "fix/bug/critical");
}
#[test]
fn test_tree_original_repos_unaffected() {
let ws = WorkspaceBuilder::new()
.add_repo("app")
.add_repo("lib")
.build();
let manifest = ws.load_manifest();
assertions::assert_on_branch(&ws.repo_path("app"), "main");
assertions::assert_on_branch(&ws.repo_path("lib"), "main");
gitgrip::cli::commands::tree::run_tree_add(
&ws.workspace_root,
&manifest,
"feat/no-side-effects",
)
.expect("tree add should succeed");
assertions::assert_on_branch(&ws.repo_path("app"), "main");
assertions::assert_on_branch(&ws.repo_path("lib"), "main");
}