use crate::stack::apply::{checkout_force, cherry_pick, rebase, url_matches, CherryPickResult};
use crate::stack::{save, GitRef, StackSpec};
use crate::test_support::with_isolated_home;
use std::fs;
use std::process::Command;
mod support;
use support::{commit_file, git, init_repo, rev_parse};
#[test]
fn cherry_pick_succeeds_picked() {
let (dir, path) = init_repo();
git(&path, &["checkout", "-q", "-b", "feature"]);
let sha = commit_file(&dir, &path, "a.txt", "feature change\n", "feature commit");
git(&path, &["checkout", "-q", "main"]);
let result = cherry_pick(&path, &sha).expect("cherry_pick");
assert!(
matches!(result, CherryPickResult::Picked),
"expected Picked, got {:?}",
result
);
let status = Command::new("git")
.args(["status", "--porcelain=v1"])
.current_dir(&path)
.output()
.unwrap();
assert!(status.stdout.is_empty(), "working tree should be clean");
}
#[test]
fn cherry_pick_skips_empty_when_change_already_in_base() {
let (dir, path) = init_repo();
let sha = commit_file(&dir, &path, "a.txt", "shared change\n", "shared commit");
git(&path, &["checkout", "-q", "-b", "feature"]);
let result = cherry_pick(&path, &sha).expect("cherry_pick");
assert!(
matches!(result, CherryPickResult::Empty),
"expected Empty (already-applied), got {:?}",
result
);
let status = Command::new("git")
.args(["status", "--porcelain=v1"])
.current_dir(&path)
.output()
.unwrap();
assert!(
status.stdout.is_empty(),
"working tree should be clean after empty-pick skip; got: {}",
String::from_utf8_lossy(&status.stdout)
);
}
#[test]
fn cherry_pick_returns_conflict_with_message() {
let (dir, path) = init_repo();
commit_file(&dir, &path, "f.txt", "main version\n", "main edit");
git(&path, &["checkout", "-q", "-b", "feature", "HEAD~1"]);
let conflict_sha = commit_file(&dir, &path, "f.txt", "feature version\n", "feature edit");
git(&path, &["checkout", "-q", "main"]);
let result = cherry_pick(&path, &conflict_sha).expect("cherry_pick");
match result {
CherryPickResult::Conflict(msg) => {
assert!(!msg.is_empty(), "conflict message should not be empty");
}
other => panic!("expected Conflict, got {:?}", other),
}
let _ = Command::new("git")
.args(["cherry-pick", "--abort"])
.current_dir(&path)
.output();
}
#[test]
fn checkout_force_recreates_branch_from_base() {
let (dir, path) = init_repo();
commit_file(&dir, &path, "x.txt", "x\n", "x");
commit_file(&dir, &path, "y.txt", "y\n", "y");
git(&path, &["tag", "base"]);
git(&path, &["checkout", "-q", "-b", "target"]);
commit_file(&dir, &path, "stale.txt", "stale\n", "stale on target");
checkout_force(&path, "target", "base").expect("checkout_force");
assert_eq!(rev_parse(&path, "HEAD"), rev_parse(&path, "base"));
assert!(
!dir.path().join("stale.txt").exists(),
"stale file should be removed by force-checkout"
);
}
#[test]
fn rebase_rebuilds_target_without_editing_spec() {
with_isolated_home(|home| {
let (dir, path) = init_repo();
git(&path, &["remote", "add", "origin", &path]);
commit_file(&dir, &path, "base.txt", "base\n", "base commit");
git(&path, &["checkout", "-q", "-b", "stack-target"]);
commit_file(&dir, &path, "stale.txt", "stale\n", "stale target commit");
git(&path, &["checkout", "-q", "main"]);
let spec = StackSpec {
id: "rebase-no-edit".to_string(),
description: "prove rebase does not mutate specs".to_string(),
component: "homeboy".to_string(),
component_path: path.clone(),
base: GitRef {
remote: "origin".to_string(),
branch: "main".to_string(),
},
target: GitRef {
remote: "origin".to_string(),
branch: "stack-target".to_string(),
},
prs: Vec::new(),
};
save(&spec).expect("save stack spec");
let spec_path = home
.path()
.join(".config/homeboy/stacks/rebase-no-edit.json");
let before = fs::read_to_string(&spec_path).expect("read spec before rebase");
let output = rebase(&spec).expect("rebase stack");
assert!(output.success);
assert_eq!(output.picked_count, 0);
assert_eq!(output.skipped_count, 0);
let after = fs::read_to_string(&spec_path).expect("read spec after rebase");
assert_eq!(after, before, "stack rebase must not edit the spec file");
assert_eq!(
rev_parse(&path, "stack-target"),
rev_parse(&path, "origin/main")
);
assert!(
!dir.path().join("stale.txt").exists(),
"rebase should recreate target from base and remove stale files"
);
});
}
#[test]
fn url_matches_https_with_and_without_dot_git() {
assert!(url_matches(
"https://github.com/Automattic/studio.git",
"https://github.com/Automattic/studio"
));
assert!(url_matches(
"https://github.com/Automattic/studio",
"https://github.com/Automattic/studio.git"
));
}
#[test]
fn url_matches_https_vs_ssh() {
assert!(url_matches(
"https://github.com/Automattic/studio.git",
"git@github.com:Automattic/studio.git"
));
}
#[test]
fn url_matches_case_insensitive() {
assert!(url_matches(
"https://github.com/automattic/STUDIO.git",
"https://github.com/Automattic/studio"
));
}
#[test]
fn url_matches_rejects_different_repos() {
assert!(!url_matches(
"https://github.com/Automattic/studio",
"https://github.com/Automattic/playground"
));
}
#[test]
fn url_matches_rejects_non_github_urls() {
assert!(!url_matches(
"https://gitlab.com/foo/bar",
"https://gitlab.com/foo/bar"
));
}