use crate::common::{TestRepo, repo, repo_with_remote};
use rstest::rstest;
use std::fs;
use worktrunk::git::{GitRemoteUrl, Repository};
#[rstest]
fn test_get_default_branch_with_origin_head(#[from(repo_with_remote)] repo: TestRepo) {
assert!(repo.has_origin_head());
let branch = Repository::at(repo.root_path())
.unwrap()
.default_branch()
.unwrap();
assert_eq!(branch, "main");
}
#[rstest]
fn test_get_default_branch_without_origin_head(#[from(repo_with_remote)] repo: TestRepo) {
repo.clear_origin_head();
assert!(!repo.has_origin_head());
let branch = Repository::at(repo.root_path())
.unwrap()
.default_branch()
.unwrap();
assert_eq!(branch, "main");
let cached = repo
.git_command()
.args(["config", "--get", "worktrunk.default-branch"])
.run()
.unwrap();
assert_eq!(String::from_utf8_lossy(&cached.stdout).trim(), "main");
}
#[rstest]
fn test_get_default_branch_caches_result(#[from(repo_with_remote)] repo: TestRepo) {
repo.clear_origin_head();
let _ = repo
.git_command()
.args(["config", "--unset", "worktrunk.default-branch"])
.run();
Repository::at(repo.root_path())
.unwrap()
.default_branch()
.unwrap();
let cached = repo
.git_command()
.args(["config", "--get", "worktrunk.default-branch"])
.run()
.unwrap();
assert!(cached.status.success());
let branch = Repository::at(repo.root_path())
.unwrap()
.default_branch()
.unwrap();
assert_eq!(branch, "main");
}
#[rstest]
fn test_get_default_branch_no_remote(repo: TestRepo) {
repo.run_git(&["remote", "remove", "origin"]);
let result = Repository::at(repo.root_path()).unwrap().default_branch();
assert!(result.is_some());
let inferred_branch = result.unwrap();
let repo_instance = Repository::at(repo.root_path()).unwrap();
let current_branch = repo_instance
.worktree_at(repo.root_path())
.branch()
.unwrap()
.unwrap();
assert_eq!(inferred_branch, current_branch);
}
#[rstest]
fn test_get_default_branch_with_custom_remote(mut repo: TestRepo) {
repo.setup_custom_remote("upstream", "main");
let branch = Repository::at(repo.root_path())
.unwrap()
.default_branch()
.unwrap();
assert_eq!(branch, "main");
}
#[rstest]
fn test_primary_remote_detects_custom_remote(mut repo: TestRepo) {
repo.run_git(&["remote", "remove", "origin"]);
repo.setup_custom_remote("upstream", "main");
let git_repo = Repository::at(repo.root_path()).unwrap();
let remote = git_repo.primary_remote().unwrap();
assert_eq!(remote, "upstream");
}
#[rstest]
fn test_primary_remote_skips_includeif_lines(repo: TestRepo) {
let git_config = repo.root_path().join(".git/config");
let original = fs::read_to_string(&git_config).unwrap();
let patched = format!(
"[includeIf \"hasconfig:remote.*.url:https://github.com/example/other.git\"]\n\
\tpath = /dev/null\n{}",
original
);
fs::write(&git_config, patched).unwrap();
let git_repo = Repository::at(repo.root_path()).unwrap();
let remote = git_repo.primary_remote().unwrap();
assert_eq!(remote, "origin");
}
#[rstest]
fn test_branch_exists_with_custom_remote(mut repo: TestRepo) {
repo.setup_custom_remote("upstream", "main");
let git_repo = Repository::at(repo.root_path()).unwrap();
assert!(git_repo.branch("main").exists().unwrap());
assert!(!git_repo.branch("nonexistent").exists().unwrap());
}
#[rstest]
fn test_get_default_branch_no_remote_common_names_fallback(repo: TestRepo) {
repo.run_git(&["remote", "remove", "origin"]);
repo.git_command()
.args(["branch", "feature"])
.run()
.unwrap();
repo.git_command().args(["branch", "bugfix"]).run().unwrap();
let branch = Repository::at(repo.root_path())
.unwrap()
.default_branch()
.unwrap();
assert_eq!(branch, "main");
}
#[rstest]
fn test_get_default_branch_no_remote_master_fallback(repo: TestRepo) {
repo.run_git(&["remote", "remove", "origin"]);
repo.git_command()
.args(["branch", "-m", "main", "master"])
.run()
.unwrap();
repo.git_command()
.args(["branch", "feature"])
.run()
.unwrap();
repo.git_command().args(["branch", "bugfix"]).run().unwrap();
let branch = Repository::at(repo.root_path())
.unwrap()
.default_branch()
.unwrap();
assert_eq!(branch, "master");
}
#[rstest]
fn test_default_branch_no_remote_uses_init_config(repo: TestRepo) {
repo.run_git(&["remote", "remove", "origin"]);
repo.git_command()
.args(["branch", "-m", "main", "primary"])
.run()
.unwrap();
repo.git_command()
.args(["branch", "feature"])
.run()
.unwrap();
repo.git_command()
.args(["config", "init.defaultBranch", "primary"])
.run()
.unwrap();
let branch = Repository::at(repo.root_path())
.unwrap()
.default_branch()
.unwrap();
assert_eq!(branch, "primary");
}
#[rstest]
fn test_configured_default_branch_is_trusted_without_validation(repo: TestRepo) {
repo.git_command()
.args(["config", "worktrunk.default-branch", "nonexistent-branch"])
.run()
.unwrap();
let result = Repository::at(repo.root_path()).unwrap().default_branch();
assert_eq!(result, Some("nonexistent-branch".to_string()));
}
#[rstest]
fn test_set_config_then_get_mixed_case_variable(repo: TestRepo) {
let r = Repository::at(repo.root_path()).unwrap();
let _ = r.is_bare();
r.set_config("branch.main.pushRemote", "origin").unwrap();
assert_eq!(
r.config_value("branch.main.pushRemote").unwrap(),
Some("origin".to_string())
);
}
#[rstest]
fn test_get_default_branch_no_remote_fails_when_no_match(repo: TestRepo) {
repo.run_git(&["remote", "remove", "origin"]);
repo.git_command()
.args(["branch", "-m", "main", "xyz"])
.run()
.unwrap();
repo.git_command().args(["branch", "abc"]).run().unwrap();
repo.git_command().args(["branch", "def"]).run().unwrap();
let result = Repository::at(repo.root_path()).unwrap().default_branch();
assert!(
result.is_none(),
"Expected None when default branch cannot be determined, got: {:?}",
result
);
}
#[rstest]
fn test_resolve_caret_fails_when_default_branch_unavailable(repo: TestRepo) {
repo.run_git(&["remote", "remove", "origin"]);
repo.git_command()
.args(["branch", "-m", "main", "xyz"])
.run()
.unwrap();
repo.git_command().args(["branch", "abc"]).run().unwrap();
repo.git_command().args(["branch", "def"]).run().unwrap();
let git_repo = Repository::at(repo.root_path()).unwrap();
let result = git_repo.resolve_worktree_name("^");
assert!(
result.is_err(),
"Expected error when resolving ^ without default branch"
);
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("Cannot determine default branch"),
"Error should mention cannot determine default branch, got: {}",
err_msg
);
}
fn setup_insteadof(repo: &TestRepo, remote: &str, custom_url: &str, real_prefix: &str) {
let custom_prefix = custom_url
.rsplit_once('/')
.map(|(p, _)| p)
.unwrap_or(custom_url);
repo.run_git(&["config", &format!("remote.{remote}.url"), custom_url]);
repo.run_git(&[
"config",
&format!("url.{real_prefix}.insteadOf"),
custom_prefix,
]);
}
fn setup_push_tracking(repo: &TestRepo, branch: &str, remote: &str) {
repo.run_git(&["config", &format!("branch.{branch}.remote"), remote]);
repo.run_git(&[
"config",
&format!("branch.{branch}.merge"),
&format!("refs/heads/{branch}"),
]);
repo.run_git(&[
"update-ref",
&format!("refs/remotes/{remote}/{branch}"),
branch,
]);
}
#[rstest]
fn test_effective_remote_url_insteadof(repo: TestRepo) {
setup_insteadof(
&repo,
"origin",
"git@work-ssh:org/repo.git",
"git@github.com:org",
);
let git_repo = Repository::at(repo.root_path()).unwrap();
assert_eq!(
git_repo.remote_url("origin").unwrap(),
"git@work-ssh:org/repo.git"
);
let effective = git_repo.effective_remote_url("origin").unwrap();
assert_eq!(effective, "git@github.com:org/repo.git");
let parsed = GitRemoteUrl::parse(&effective).unwrap();
assert!(parsed.is_github());
assert_eq!(parsed.host(), "github.com");
assert_eq!(parsed.owner(), "org");
assert_eq!(parsed.repo(), "repo");
}
#[rstest]
fn test_effective_remote_url_without_insteadof(repo: TestRepo) {
let git_repo = Repository::at(repo.root_path()).unwrap();
assert_eq!(
git_repo.remote_url("origin").unwrap(),
git_repo.effective_remote_url("origin").unwrap()
);
}
#[rstest]
fn test_effective_remote_url_nonexistent_remote(repo: TestRepo) {
let git_repo = Repository::at(repo.root_path()).unwrap();
assert!(git_repo.effective_remote_url("nonexistent").is_none());
}
#[rstest]
fn test_effective_remote_url_is_cached(repo: TestRepo) {
let git_repo = Repository::at(repo.root_path()).unwrap();
let first = git_repo.effective_remote_url("origin");
let second = git_repo.effective_remote_url("origin");
assert_eq!(first, second);
}
#[rstest]
fn test_find_remote_for_repo_insteadof(repo: TestRepo) {
setup_insteadof(
&repo,
"origin",
"git@work-ssh:org/repo.git",
"git@github.com:org",
);
let git_repo = Repository::at(repo.root_path()).unwrap();
let found = git_repo.find_remote_for_repo(Some("github.com"), "org", "repo");
assert_eq!(found.as_deref(), Some("origin"));
}
#[rstest]
fn test_find_remote_for_repo_insteadof_case_insensitive(repo: TestRepo) {
setup_insteadof(
&repo,
"origin",
"git@work-ssh:MyOrg/MyRepo.git",
"git@github.com:MyOrg",
);
let git_repo = Repository::at(repo.root_path()).unwrap();
let found = git_repo.find_remote_for_repo(Some("github.com"), "myorg", "myrepo");
assert_eq!(found.as_deref(), Some("origin"));
}
#[rstest]
fn test_find_remote_for_repo_insteadof_no_host(repo: TestRepo) {
setup_insteadof(
&repo,
"origin",
"git@work-ssh:org/repo.git",
"git@github.com:org",
);
let git_repo = Repository::at(repo.root_path()).unwrap();
let found = git_repo.find_remote_for_repo(None, "org", "repo");
assert_eq!(found.as_deref(), Some("origin"));
}
#[rstest]
fn test_find_remote_for_repo_insteadof_multiple_remotes(repo: TestRepo) {
setup_insteadof(
&repo,
"origin",
"git@work-ssh:org/repo.git",
"git@github.com:org",
);
repo.run_git(&[
"config",
"remote.upstream.url",
"git@work-ssh-2:upstream-org/repo.git",
]);
repo.run_git(&[
"config",
"url.git@github.com:upstream-org.insteadOf",
"git@work-ssh-2:upstream-org",
]);
let git_repo = Repository::at(repo.root_path()).unwrap();
assert_eq!(
git_repo
.find_remote_for_repo(Some("github.com"), "upstream-org", "repo")
.as_deref(),
Some("upstream")
);
assert_eq!(
git_repo
.find_remote_for_repo(Some("github.com"), "org", "repo")
.as_deref(),
Some("origin")
);
}
#[rstest]
fn test_find_remote_by_url_insteadof(repo: TestRepo) {
setup_insteadof(
&repo,
"origin",
"git@work-ssh:org/repo.git",
"git@github.com:org",
);
let git_repo = Repository::at(repo.root_path()).unwrap();
let found = git_repo.find_remote_by_url("git@github.com:org/repo.git");
assert_eq!(found.as_deref(), Some("origin"));
let found = git_repo.find_remote_by_url("https://github.com/org/repo.git");
assert_eq!(found.as_deref(), Some("origin"));
}
#[rstest]
fn test_github_push_url_insteadof_fallback(repo: TestRepo) {
setup_insteadof(
&repo,
"origin",
"git@work-ssh:org/repo.git",
"git@github.com:org",
);
setup_push_tracking(&repo, "main", "origin");
let git_repo = Repository::at(repo.root_path()).unwrap();
let url = git_repo
.branch("main")
.github_push_url()
.expect("github_push_url should resolve via insteadOf");
let parsed = GitRemoteUrl::parse(&url).unwrap();
assert!(parsed.is_github());
assert_eq!(parsed.host(), "github.com");
}
#[rstest]
fn test_github_push_url_non_github_forge_returns_none(repo: TestRepo) {
repo.run_git(&["config", "remote.origin.url", "git@gitlab.com:org/repo.git"]);
setup_push_tracking(&repo, "main", "origin");
let git_repo = Repository::at(repo.root_path()).unwrap();
assert!(git_repo.branch("main").github_push_url().is_none());
}
#[rstest]
fn test_github_push_url_unknown_host_non_github_insteadof(repo: TestRepo) {
setup_insteadof(
&repo,
"origin",
"git@work-ssh:org/repo.git",
"git@gitlab.com:org",
);
setup_push_tracking(&repo, "main", "origin");
let git_repo = Repository::at(repo.root_path()).unwrap();
assert!(git_repo.branch("main").github_push_url().is_none());
}