use std::path::PathBuf;
use super::super::{DefaultBranchName, WorktreeInfo, finalize_worktree};
#[test]
fn test_parse_worktree_list() {
let output = "worktree /path/to/main
HEAD abcd1234
branch refs/heads/main
worktree /path/to/feature
HEAD efgh5678
branch refs/heads/feature
";
let worktrees = WorktreeInfo::parse_porcelain_list(output).unwrap();
let [main_wt, feature_wt]: [WorktreeInfo; 2] = worktrees.try_into().unwrap();
assert_eq!(main_wt.path, PathBuf::from("/path/to/main"));
assert_eq!(main_wt.head, "abcd1234");
assert_eq!(main_wt.branch, Some("main".to_string()));
assert!(!main_wt.bare);
assert!(!main_wt.detached);
assert_eq!(feature_wt.path, PathBuf::from("/path/to/feature"));
assert_eq!(feature_wt.head, "efgh5678");
assert_eq!(feature_wt.branch, Some("feature".to_string()));
}
#[test]
fn test_parse_detached_worktree() {
let output = "worktree /path/to/detached
HEAD abcd1234
detached
";
let worktrees = WorktreeInfo::parse_porcelain_list(output).unwrap();
let [wt]: [WorktreeInfo; 1] = worktrees.try_into().unwrap();
assert!(wt.detached);
assert_eq!(wt.branch, None);
}
#[test]
fn test_finalize_worktree_with_branch() {
let wt = WorktreeInfo {
path: PathBuf::from("/path/to/worktree"),
head: "abcd1234".to_string(),
branch: Some("feature".to_string()),
bare: false,
detached: false,
locked: None,
prunable: None,
};
let finalized = finalize_worktree(wt.clone());
assert_eq!(finalized.branch, Some("feature".to_string()));
}
#[test]
fn test_finalize_worktree_detached_with_branch() {
let wt = WorktreeInfo {
path: PathBuf::from("/path/to/worktree"),
head: "abcd1234".to_string(),
branch: Some("feature".to_string()),
bare: false,
detached: true,
locked: None,
prunable: None,
};
let finalized = finalize_worktree(wt.clone());
assert_eq!(finalized.branch, Some("feature".to_string()));
}
#[test]
fn test_finalize_worktree_detached_no_branch() {
let wt = WorktreeInfo {
path: PathBuf::from("/nonexistent/path"),
head: "abcd1234".to_string(),
branch: None,
bare: false,
detached: true,
locked: None,
prunable: None,
};
let finalized = finalize_worktree(wt);
assert_eq!(finalized.branch, None);
}
#[test]
fn test_parse_locked_worktree() {
let output = "worktree /path/to/locked
HEAD abcd1234
branch refs/heads/main
locked reason for lock
";
let worktrees = WorktreeInfo::parse_porcelain_list(output).unwrap();
let [wt]: [WorktreeInfo; 1] = worktrees.try_into().unwrap();
assert_eq!(wt.locked, Some("reason for lock".to_string()));
}
#[test]
fn test_parse_bare_worktree() {
let output = "worktree /path/to/bare
HEAD abcd1234
bare
";
let worktrees = WorktreeInfo::parse_porcelain_list(output).unwrap();
let [wt]: [WorktreeInfo; 1] = worktrees.try_into().unwrap();
assert!(wt.bare);
}
#[test]
fn test_parse_local_default_branch_with_prefix() {
let output = "origin/main\n";
let branch = DefaultBranchName::from_local("origin", output)
.map(DefaultBranchName::into_string)
.unwrap();
assert_eq!(branch, "main");
}
#[test]
fn test_parse_local_default_branch_without_prefix() {
let output = "main\n";
let branch = DefaultBranchName::from_local("origin", output)
.map(DefaultBranchName::into_string)
.unwrap();
assert_eq!(branch, "main");
}
#[test]
fn test_parse_local_default_branch_master() {
let output = "origin/master\n";
let branch = DefaultBranchName::from_local("origin", output)
.map(DefaultBranchName::into_string)
.unwrap();
assert_eq!(branch, "master");
}
#[test]
fn test_parse_local_default_branch_custom_name() {
let output = "origin/develop\n";
let branch = DefaultBranchName::from_local("origin", output)
.map(DefaultBranchName::into_string)
.unwrap();
assert_eq!(branch, "develop");
}
#[test]
fn test_parse_local_default_branch_custom_remote() {
let output = "upstream/main\n";
let branch = DefaultBranchName::from_local("upstream", output)
.map(DefaultBranchName::into_string)
.unwrap();
assert_eq!(branch, "main");
}
#[test]
fn test_parse_local_default_branch_empty() {
let output = "";
let result =
DefaultBranchName::from_local("origin", output).map(DefaultBranchName::into_string);
assert!(result.is_err());
}
#[test]
fn test_parse_local_default_branch_whitespace_only() {
let output = " \n ";
let result =
DefaultBranchName::from_local("origin", output).map(DefaultBranchName::into_string);
assert!(result.is_err());
}
#[test]
fn test_parse_remote_default_branch_main() {
let output = "ref: refs/heads/main\tHEAD
85a1ce7c7182540f9c02453441cb3e8bf0ced214\tHEAD
";
let branch = DefaultBranchName::from_remote(output)
.map(DefaultBranchName::into_string)
.unwrap();
assert_eq!(branch, "main");
}
#[test]
fn test_parse_remote_default_branch_master() {
let output = "ref: refs/heads/master\tHEAD
abcd1234567890abcd1234567890abcd12345678\tHEAD
";
let branch = DefaultBranchName::from_remote(output)
.map(DefaultBranchName::into_string)
.unwrap();
assert_eq!(branch, "master");
}
#[test]
fn test_parse_remote_default_branch_custom() {
let output = "ref: refs/heads/develop\tHEAD
1234567890abcdef1234567890abcdef12345678\tHEAD
";
let branch = DefaultBranchName::from_remote(output)
.map(DefaultBranchName::into_string)
.unwrap();
assert_eq!(branch, "develop");
}
#[test]
fn test_parse_remote_default_branch_only_symref_line() {
let output = "ref: refs/heads/main\tHEAD\n";
let branch = DefaultBranchName::from_remote(output)
.map(DefaultBranchName::into_string)
.unwrap();
assert_eq!(branch, "main");
}
#[test]
fn test_parse_remote_default_branch_missing_symref() {
let output = "85a1ce7c7182540f9c02453441cb3e8bf0ced214\tHEAD\n";
let result = DefaultBranchName::from_remote(output).map(DefaultBranchName::into_string);
assert!(result.is_err());
}
#[test]
fn test_parse_remote_default_branch_empty() {
let output = "";
let result = DefaultBranchName::from_remote(output).map(DefaultBranchName::into_string);
assert!(result.is_err());
}
#[test]
fn test_parse_remote_default_branch_malformed_ref() {
let output = "ref: main\tHEAD\n";
let result = DefaultBranchName::from_remote(output).map(DefaultBranchName::into_string);
assert!(result.is_err());
}
#[test]
fn test_parse_remote_default_branch_with_spaces() {
let output = "ref: refs/heads/main HEAD\n";
let result = DefaultBranchName::from_remote(output).map(DefaultBranchName::into_string);
assert!(result.is_err());
}
#[test]
fn test_parse_remote_default_branch_branch_with_slash() {
let output = "ref: refs/heads/feature/new-ui\tHEAD\n";
let branch = DefaultBranchName::from_remote(output)
.map(DefaultBranchName::into_string)
.unwrap();
assert_eq!(branch, "feature/new-ui");
}
use super::ResolvedWorktree;
#[test]
fn test_resolved_worktree_clone() {
let wt = ResolvedWorktree::Worktree {
path: PathBuf::from("/path/to/worktree"),
branch: Some("feature".to_string()),
};
let cloned = wt.clone();
if let ResolvedWorktree::Worktree { path, branch } = cloned {
assert_eq!(path, PathBuf::from("/path/to/worktree"));
assert_eq!(branch, Some("feature".to_string()));
} else {
panic!("Expected Worktree variant");
}
}
#[test]
fn test_resolved_worktree_none_branch() {
let wt = ResolvedWorktree::Worktree {
path: PathBuf::from("/path/to/worktree"),
branch: None,
};
if let ResolvedWorktree::Worktree { path, branch } = wt {
assert_eq!(path, PathBuf::from("/path/to/worktree"));
assert!(branch.is_none());
} else {
panic!("Expected Worktree variant");
}
}
#[test]
fn test_worktree_locked_empty_reason() {
let output = "worktree /path/to/locked
HEAD abcd1234
branch refs/heads/main
locked
";
let worktrees = WorktreeInfo::parse_porcelain_list(output).unwrap();
let [wt]: [WorktreeInfo; 1] = worktrees.try_into().unwrap();
assert_eq!(wt.locked, Some(String::new()));
}
#[test]
fn test_worktree_prunable() {
let output = "worktree /path/to/prunable
HEAD abcd1234
detached
prunable gitdir file points to non-existent location
";
let worktrees = WorktreeInfo::parse_porcelain_list(output).unwrap();
let [wt]: [WorktreeInfo; 1] = worktrees.try_into().unwrap();
assert!(wt.prunable.is_some());
assert!(wt.prunable.as_ref().unwrap().contains("non-existent"));
}
#[test]
fn test_parse_multiple_worktrees() {
let output = "worktree /main
HEAD 1111111111111111111111111111111111111111
branch refs/heads/main
worktree /feature-a
HEAD 2222222222222222222222222222222222222222
branch refs/heads/feature-a
worktree /feature-b
HEAD 3333333333333333333333333333333333333333
branch refs/heads/feature-b
worktree /detached
HEAD 4444444444444444444444444444444444444444
detached
";
let worktrees = WorktreeInfo::parse_porcelain_list(output).unwrap();
let [main_wt, feature_a, feature_b, detached_wt]: [WorktreeInfo; 4] =
worktrees.try_into().unwrap();
assert_eq!(main_wt.branch, Some("main".to_string()));
assert_eq!(feature_a.branch, Some("feature-a".to_string()));
assert_eq!(feature_b.branch, Some("feature-b".to_string()));
assert!(detached_wt.detached);
assert_eq!(detached_wt.branch, None);
}
#[test]
fn test_default_branch_name_display() {
let cases = [
("origin/main\n", "main"),
("upstream/develop\n", "develop"),
("origin/master\n", "master"),
];
for (input, expected) in cases {
let remote = input.split('/').next().unwrap();
let branch = DefaultBranchName::from_local(remote, input)
.map(DefaultBranchName::into_string)
.unwrap();
assert_eq!(branch, expected);
}
}
#[test]
fn repo_path_error_when_is_bare_fails() {
use super::RepoCache;
use std::sync::Arc;
let repo = super::Repository {
discovery_path: PathBuf::from("/nonexistent/repo"),
git_common_dir: PathBuf::from("/nonexistent/.git"),
cache: Arc::new(RepoCache::default()),
};
let err = repo.repo_path().unwrap_err();
let msg = format!("{err:#}");
assert!(
msg.starts_with("failed to read git config: "),
"unexpected error message: {msg}"
);
}
#[test]
fn parse_config_list_z_basic() {
let input = b"core.bare\nfalse\0remote.origin.url\nhttps://example.com/a.git\0";
let map = super::parse_config_list_z(input);
assert_eq!(map["core.bare"], vec!["false"]);
assert_eq!(map["remote.origin.url"], vec!["https://example.com/a.git"]);
}
#[test]
fn parse_config_list_z_multivar() {
let input =
b"remote.origin.fetch\n+refs/heads/*:refs/remotes/origin/*\0remote.origin.fetch\n+refs/tags/*:refs/tags/*\0";
let map = super::parse_config_list_z(input);
assert_eq!(
map["remote.origin.fetch"],
vec![
"+refs/heads/*:refs/remotes/origin/*",
"+refs/tags/*:refs/tags/*"
]
);
}
#[test]
fn parse_config_list_z_newline_in_value() {
let input = b"commit.template\nline1\nline2\0core.bare\nfalse\0";
let map = super::parse_config_list_z(input);
assert_eq!(map["commit.template"], vec!["line1\nline2"]);
assert_eq!(map["core.bare"], vec!["false"]);
}
#[test]
fn parse_config_list_z_equals_in_value() {
let input = b"user.email\nme=you@example.com\0";
let map = super::parse_config_list_z(input);
assert_eq!(map["user.email"], vec!["me=you@example.com"]);
}
#[test]
fn parse_config_list_z_empty() {
let map = super::parse_config_list_z(b"");
assert!(map.is_empty());
}
#[test]
fn parse_config_list_z_entry_without_newline_tolerates_key_only() {
let input = b"core.bare\0other.key\nfalse\0";
let map = super::parse_config_list_z(input);
assert_eq!(map["core.bare"], vec![""]);
assert_eq!(map["other.key"], vec!["false"]);
}
#[test]
fn canonical_config_key_cases() {
assert_eq!(
super::canonical_config_key("init.defaultBranch"),
"init.defaultbranch"
);
assert_eq!(
super::canonical_config_key("checkout.defaultRemote"),
"checkout.defaultremote"
);
assert_eq!(super::canonical_config_key("core.Bare"), "core.bare");
assert_eq!(
super::canonical_config_key("remote.MyFork.url"),
"remote.MyFork.url"
);
assert_eq!(
super::canonical_config_key("branch.MyBranch.pushRemote"),
"branch.MyBranch.pushremote"
);
assert_eq!(
super::canonical_config_key("worktrunk.state.MyBranch.marker"),
"worktrunk.state.MyBranch.marker"
);
}
#[test]
fn parse_git_bool_variants() {
for truthy in ["true", "TRUE", "True", "1", "yes", "YES", "on", "ON"] {
assert!(super::parse_git_bool(truthy), "{truthy} should be true");
}
for falsy in ["false", "0", "no", "off", "", "anything-else"] {
assert!(!super::parse_git_bool(falsy), "{falsy} should be false");
}
}
#[test]
fn extract_failed_command_from_stream_error() {
use super::StreamCommandError;
let err: anyhow::Error = StreamCommandError {
output: "fatal: ref exists".into(),
command: "git worktree add /path".into(),
exit_info: "exit code 128".into(),
}
.into();
let (output, cmd) = super::Repository::extract_failed_command(&err);
assert_eq!(output, "fatal: ref exists");
let cmd = cmd.unwrap();
assert_eq!(cmd.command, "git worktree add /path");
assert_eq!(cmd.exit_info, "exit code 128");
}
#[test]
fn extract_failed_command_from_other_error() {
let err = anyhow::anyhow!("some other error");
let (output, cmd) = super::Repository::extract_failed_command(&err);
assert_eq!(output, "some other error");
assert!(cmd.is_none());
}
#[test]
fn is_builtin_fsmonitor_enabled_variants() {
use super::RepoCache;
use indexmap::IndexMap;
use std::sync::{Arc, RwLock};
fn repo_with_fsmonitor(value: Option<&str>) -> super::Repository {
let cache = RepoCache::default();
let mut map: IndexMap<String, Vec<String>> = IndexMap::new();
if let Some(v) = value {
map.insert("core.fsmonitor".to_string(), vec![v.to_string()]);
}
cache.all_config.set(RwLock::new(map)).unwrap();
super::Repository {
discovery_path: PathBuf::from("/nonexistent/repo"),
git_common_dir: PathBuf::from("/nonexistent/.git"),
cache: Arc::new(cache),
}
}
for truthy in ["true", "1", "yes", "on", "TRUE"] {
assert!(
repo_with_fsmonitor(Some(truthy)).is_builtin_fsmonitor_enabled(),
"{truthy} should enable builtin fsmonitor"
);
}
assert!(
!repo_with_fsmonitor(Some("/usr/local/bin/git-fsmonitor-watchman.sh"))
.is_builtin_fsmonitor_enabled()
);
for falsy in ["false", "0", "no", "off"] {
assert!(
!repo_with_fsmonitor(Some(falsy)).is_builtin_fsmonitor_enabled(),
"{falsy} should disable builtin fsmonitor"
);
}
assert!(!repo_with_fsmonitor(None).is_builtin_fsmonitor_enabled());
}