use super::remote::{parse_github_remote, parse_remote_owner_repo};
use super::semver::{compare_prerelease, parse_semver, parse_semver_tag};
use super::tags::{
find_latest_tag_matching, find_latest_tag_matching_with_prefix, find_previous_tag,
get_all_semver_tags, strip_monorepo_prefix,
};
use crate::redact::redact_url_credentials;
#[test]
fn test_parse_semver() {
let v = parse_semver("v1.2.3").unwrap();
assert_eq!(v.major, 1);
assert_eq!(v.minor, 2);
assert_eq!(v.patch, 3);
assert_eq!(v.prerelease, None);
assert_eq!(v.build_metadata, None);
}
#[test]
fn test_parse_semver_prerelease() {
let v = parse_semver("v1.0.0-rc.1").unwrap();
assert_eq!(v.major, 1);
assert_eq!(v.prerelease, Some("rc.1".to_string()));
assert_eq!(v.build_metadata, None);
}
#[test]
fn test_parse_semver_build_metadata() {
let v = parse_semver("v1.0.0+build.42").unwrap();
assert_eq!(v.major, 1);
assert_eq!(v.minor, 0);
assert_eq!(v.patch, 0);
assert_eq!(v.prerelease, None);
assert_eq!(v.build_metadata, Some("build.42".to_string()));
}
#[test]
fn test_parse_semver_prerelease_and_build_metadata() {
let v = parse_semver("v1.0.0-rc.1+build.42").unwrap();
assert_eq!(v.major, 1);
assert_eq!(v.prerelease, Some("rc.1".to_string()));
assert_eq!(v.build_metadata, Some("build.42".to_string()));
}
#[test]
fn test_parse_semver_rejects_prefix() {
assert!(parse_semver("cfgd-core-v2.1.0").is_err());
assert!(parse_semver("release-notes-v1.2.3").is_err());
}
#[test]
fn test_parse_semver_tag_with_prefix() {
let v = parse_semver_tag("cfgd-core-v2.1.0").unwrap();
assert_eq!(v.major, 2);
assert_eq!(v.minor, 1);
assert_eq!(v.patch, 0);
}
#[test]
fn test_parse_semver_tag_plain() {
let v = parse_semver_tag("v1.2.3").unwrap();
assert_eq!(v.major, 1);
assert_eq!(v.minor, 2);
assert_eq!(v.patch, 3);
}
#[test]
fn test_parse_semver_tag_with_prerelease_prefix() {
let v = parse_semver_tag("my-project-v1.0.0-rc.1").unwrap();
assert_eq!(v.major, 1);
assert_eq!(v.prerelease, Some("rc.1".to_string()));
}
#[test]
fn test_is_prerelease() {
assert!(parse_semver("v1.0.0-rc.1").unwrap().is_prerelease());
assert!(!parse_semver("v1.0.0").unwrap().is_prerelease());
assert!(!parse_semver("v1.0.0+build.42").unwrap().is_prerelease());
}
#[test]
fn test_parse_github_remote_https() {
let result = parse_github_remote("https://github.com/tj-smith47/anodizer.git");
assert_eq!(
result,
Some(("tj-smith47".to_string(), "anodizer".to_string()))
);
}
#[test]
fn test_parse_github_remote_https_no_dotgit() {
let result = parse_github_remote("https://github.com/owner/repo");
assert_eq!(result, Some(("owner".to_string(), "repo".to_string())));
}
#[test]
fn test_parse_github_remote_ssh() {
let result = parse_github_remote("git@github.com:owner/repo.git");
assert_eq!(result, Some(("owner".to_string(), "repo".to_string())));
}
#[test]
fn test_parse_github_remote_ssh_no_dotgit() {
let result = parse_github_remote("git@github.com:owner/repo");
assert_eq!(result, Some(("owner".to_string(), "repo".to_string())));
}
#[test]
fn test_parse_github_remote_invalid() {
let result = parse_github_remote("https://gitlab.com/foo/bar.git");
assert_eq!(result, None);
}
#[test]
fn test_parse_github_remote_empty() {
let result = parse_github_remote("");
assert_eq!(result, None);
}
#[test]
fn test_parse_remote_github_https() {
let result = parse_remote_owner_repo("https://github.com/owner/repo.git");
assert_eq!(result, Some(("owner".to_string(), "repo".to_string())));
}
#[test]
fn test_parse_remote_gitlab_https() {
let result = parse_remote_owner_repo("https://gitlab.com/owner/repo.git");
assert_eq!(result, Some(("owner".to_string(), "repo".to_string())));
}
#[test]
fn test_parse_remote_gitea_https() {
let result = parse_remote_owner_repo("https://gitea.example.com/myorg/myapp.git");
assert_eq!(result, Some(("myorg".to_string(), "myapp".to_string())));
}
#[test]
fn test_parse_remote_gitlab_nested_group() {
let result = parse_remote_owner_repo("https://gitlab.com/group/subgroup/repo.git");
assert_eq!(
result,
Some(("group/subgroup".to_string(), "repo".to_string()))
);
}
#[test]
fn test_parse_remote_ssh_gitlab() {
let result = parse_remote_owner_repo("git@gitlab.com:owner/repo.git");
assert_eq!(result, Some(("owner".to_string(), "repo".to_string())));
}
#[test]
fn test_parse_remote_ssh_gitea() {
let result = parse_remote_owner_repo("git@gitea.example.com:org/app.git");
assert_eq!(result, Some(("org".to_string(), "app".to_string())));
}
#[test]
fn test_parse_remote_ssh_nested_group() {
let result = parse_remote_owner_repo("git@gitlab.com:group/subgroup/repo.git");
assert_eq!(
result,
Some(("group/subgroup".to_string(), "repo".to_string()))
);
}
#[test]
fn test_parse_remote_no_dotgit() {
let result = parse_remote_owner_repo("https://gitlab.com/owner/repo");
assert_eq!(result, Some(("owner".to_string(), "repo".to_string())));
}
#[test]
fn test_parse_remote_empty() {
assert_eq!(parse_remote_owner_repo(""), None);
}
#[test]
fn test_parse_remote_http() {
let result = parse_remote_owner_repo("http://gitlab.local/team/project.git");
assert_eq!(result, Some(("team".to_string(), "project".to_string())));
}
#[test]
fn test_strip_url_credentials_with_userinfo() {
assert_eq!(
redact_url_credentials("https://user:token@github.com/owner/repo.git"),
"https://<redacted>@github.com/owner/repo.git"
);
}
#[test]
fn test_strip_url_credentials_no_userinfo() {
assert_eq!(
redact_url_credentials("https://github.com/owner/repo.git"),
"https://github.com/owner/repo.git"
);
}
#[test]
fn test_strip_url_credentials_ssh_unchanged() {
assert_eq!(
redact_url_credentials("git@github.com:owner/repo.git"),
"git@github.com:owner/repo.git"
);
}
#[test]
fn test_strip_url_credentials_user_only() {
assert_eq!(
redact_url_credentials("https://user@github.com/owner/repo.git"),
"https://<redacted>@github.com/owner/repo.git"
);
}
#[test]
fn test_strip_url_credentials_token_with_at_sign_does_not_leak() {
let leaky = "https://user:t@k@n@github.com/owner/repo.git";
let scrubbed = redact_url_credentials(leaky);
assert!(!scrubbed.contains("t@k@n"));
assert_eq!(scrubbed, "https://<redacted>@github.com/owner/repo.git");
}
#[test]
fn test_compare_prerelease_numeric() {
assert_eq!(
compare_prerelease("rc.9", "rc.10"),
std::cmp::Ordering::Less
);
assert_eq!(
compare_prerelease("rc.10", "rc.9"),
std::cmp::Ordering::Greater
);
}
#[test]
fn test_compare_prerelease_numeric_less_than_alpha() {
assert_eq!(compare_prerelease("1", "alpha"), std::cmp::Ordering::Less);
assert_eq!(
compare_prerelease("alpha", "1"),
std::cmp::Ordering::Greater
);
}
#[test]
fn test_compare_prerelease_alpha_lexicographic() {
assert_eq!(
compare_prerelease("alpha", "beta"),
std::cmp::Ordering::Less
);
}
#[test]
fn test_compare_prerelease_shorter_lower_precedence() {
assert_eq!(
compare_prerelease("alpha", "alpha.1"),
std::cmp::Ordering::Less
);
}
#[test]
fn test_compare_prerelease_equal() {
assert_eq!(
compare_prerelease("rc.1", "rc.1"),
std::cmp::Ordering::Equal
);
}
#[test]
fn test_semver_ord_prerelease_less_than_release() {
let pre = parse_semver("v1.0.0-rc.1").unwrap();
let rel = parse_semver("v1.0.0").unwrap();
assert!(pre < rel);
}
#[test]
fn test_semver_ord_prerelease_numeric_sorting() {
let rc9 = parse_semver("v1.0.0-rc.9").unwrap();
let rc10 = parse_semver("v1.0.0-rc.10").unwrap();
assert!(rc9 < rc10);
}
use serial_test::serial;
fn init_repo_with_tags(dir: &std::path::Path, tags: &[&str]) {
use std::process::Command;
let run = |args: &[&str]| {
let out = Command::new("git")
.args(args)
.current_dir(dir)
.env("GIT_AUTHOR_NAME", "test")
.env("GIT_AUTHOR_EMAIL", "test@test.com")
.env("GIT_COMMITTER_NAME", "test")
.env("GIT_COMMITTER_EMAIL", "test@test.com")
.output()
.unwrap();
assert!(
out.status.success(),
"git {:?} failed: {}",
args,
String::from_utf8_lossy(&out.stderr)
);
};
run(&["init"]);
run(&["config", "user.email", "test@test.com"]);
run(&["config", "user.name", "test"]);
std::fs::write(dir.join("README"), "init").unwrap();
run(&["add", "."]);
run(&["commit", "-m", "initial"]);
for tag in tags {
run(&["tag", tag]);
}
}
#[test]
#[serial]
fn test_find_latest_tag_none_config_unchanged_behavior() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
init_repo_with_tags(dir, &["v1.0.0", "v1.1.0", "v2.0.0"]);
let orig = std::env::current_dir().unwrap();
std::env::set_current_dir(dir).unwrap();
let result = find_latest_tag_matching("v{{ .Version }}", None, None).unwrap();
assert_eq!(result, Some("v2.0.0".to_string()));
std::env::set_current_dir(orig).unwrap();
}
#[test]
#[serial]
fn test_get_all_semver_tags_ignore_tags() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
init_repo_with_tags(dir, &["v1.0.0", "v2.0.0", "v3.0.0"]);
let orig = std::env::current_dir().unwrap();
std::env::set_current_dir(dir).unwrap();
let gc = crate::config::GitConfig {
ignore_tags: Some(vec!["v3.0.0".to_string()]),
..Default::default()
};
let tags = get_all_semver_tags("v", Some(&gc), None).unwrap();
assert_eq!(tags, vec!["v2.0.0".to_string(), "v1.0.0".to_string()]);
std::env::set_current_dir(orig).unwrap();
}
#[test]
#[serial]
fn test_get_all_semver_tags_ignore_tag_prefixes() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
init_repo_with_tags(dir, &["v1.0.0", "v2.0.0", "nightly-v3.0.0"]);
let orig = std::env::current_dir().unwrap();
std::env::set_current_dir(dir).unwrap();
let gc = crate::config::GitConfig {
ignore_tag_prefixes: Some(vec!["nightly-".to_string()]),
..Default::default()
};
let tags = get_all_semver_tags("", Some(&gc), None).unwrap();
assert_eq!(tags, vec!["v2.0.0".to_string(), "v1.0.0".to_string()]);
std::env::set_current_dir(orig).unwrap();
}
#[test]
#[serial]
fn test_get_all_semver_tags_no_config_unchanged() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
init_repo_with_tags(dir, &["v1.0.0", "v2.0.0"]);
let orig = std::env::current_dir().unwrap();
std::env::set_current_dir(dir).unwrap();
let tags = get_all_semver_tags("v", None, None).unwrap();
assert_eq!(tags, vec!["v2.0.0".to_string(), "v1.0.0".to_string()]);
std::env::set_current_dir(orig).unwrap();
}
#[test]
#[serial]
fn test_find_latest_tag_ignore_tags_exact_match() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
init_repo_with_tags(dir, &["v1.0.0", "v2.0.0", "v3.0.0"]);
let orig = std::env::current_dir().unwrap();
std::env::set_current_dir(dir).unwrap();
let gc = crate::config::GitConfig {
ignore_tags: Some(vec!["v3.0.0".to_string()]),
..Default::default()
};
let result = find_latest_tag_matching("v{{ .Version }}", Some(&gc), None).unwrap();
assert_eq!(result, Some("v2.0.0".to_string()));
std::env::set_current_dir(orig).unwrap();
}
#[test]
#[serial]
fn test_find_latest_tag_ignore_tags_multiple() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
init_repo_with_tags(dir, &["v1.0.0", "v2.0.0", "v3.0.0"]);
let orig = std::env::current_dir().unwrap();
std::env::set_current_dir(dir).unwrap();
let gc = crate::config::GitConfig {
ignore_tags: Some(vec!["v3.0.0".to_string(), "v2.0.0".to_string()]),
..Default::default()
};
let result = find_latest_tag_matching("v{{ .Version }}", Some(&gc), None).unwrap();
assert_eq!(result, Some("v1.0.0".to_string()));
std::env::set_current_dir(orig).unwrap();
}
#[test]
#[serial]
fn test_find_latest_tag_ignore_tag_prefixes() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
init_repo_with_tags(
dir,
&["v1.0.0", "v2.0.0", "nightly-v3.0.0", "nightly-v4.0.0"],
);
let orig = std::env::current_dir().unwrap();
std::env::set_current_dir(dir).unwrap();
let gc = crate::config::GitConfig {
ignore_tag_prefixes: Some(vec!["nightly-".to_string()]),
..Default::default()
};
let result = find_latest_tag_matching("v{{ .Version }}", Some(&gc), None).unwrap();
assert_eq!(result, Some("v2.0.0".to_string()));
let result_nightly = find_latest_tag_matching("nightly-v{{ .Version }}", None, None).unwrap();
assert_eq!(result_nightly, Some("nightly-v4.0.0".to_string()));
let result_filtered =
find_latest_tag_matching("nightly-v{{ .Version }}", Some(&gc), None).unwrap();
assert_eq!(result_filtered, None);
std::env::set_current_dir(orig).unwrap();
}
#[test]
#[serial]
fn test_find_latest_tag_ignore_all_returns_none() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
init_repo_with_tags(dir, &["v1.0.0", "v2.0.0"]);
let orig = std::env::current_dir().unwrap();
std::env::set_current_dir(dir).unwrap();
let gc = crate::config::GitConfig {
ignore_tags: Some(vec!["v1.0.0".to_string(), "v2.0.0".to_string()]),
..Default::default()
};
let result = find_latest_tag_matching("v{{ .Version }}", Some(&gc), None).unwrap();
assert_eq!(result, None);
std::env::set_current_dir(orig).unwrap();
}
#[test]
#[serial]
fn test_find_latest_tag_ignore_tags_and_prefixes_combined() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
init_repo_with_tags(dir, &["v1.0.0", "v2.0.0", "v3.0.0-beta.1"]);
let orig = std::env::current_dir().unwrap();
std::env::set_current_dir(dir).unwrap();
let gc = crate::config::GitConfig {
ignore_tags: Some(vec!["v2.0.0".to_string()]),
ignore_tag_prefixes: Some(vec!["v3".to_string()]),
..Default::default()
};
let result = find_latest_tag_matching("v{{ .Version }}", Some(&gc), None).unwrap();
assert_eq!(result, Some("v1.0.0".to_string()));
std::env::set_current_dir(orig).unwrap();
}
#[test]
#[serial]
fn test_find_latest_tag_with_prefixed_template() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
init_repo_with_tags(
dir,
&[
"myapp-v1.0.0",
"myapp-v2.0.0",
"myapp-v3.0.0",
"other-v9.0.0",
],
);
let orig = std::env::current_dir().unwrap();
std::env::set_current_dir(dir).unwrap();
let gc = crate::config::GitConfig {
ignore_tags: Some(vec!["myapp-v3.0.0".to_string()]),
..Default::default()
};
let result = find_latest_tag_matching("myapp-v{{ .Version }}", Some(&gc), None).unwrap();
assert_eq!(result, Some("myapp-v2.0.0".to_string()));
std::env::set_current_dir(orig).unwrap();
}
#[test]
#[serial]
fn test_find_latest_tag_default_git_config_same_as_none() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
init_repo_with_tags(dir, &["v1.0.0", "v1.1.0", "v2.0.0"]);
let orig = std::env::current_dir().unwrap();
std::env::set_current_dir(dir).unwrap();
let gc = crate::config::GitConfig::default();
let with_default = find_latest_tag_matching("v{{ .Version }}", Some(&gc), None).unwrap();
let with_none = find_latest_tag_matching("v{{ .Version }}", None, None).unwrap();
assert_eq!(with_default, with_none);
assert_eq!(with_default, Some("v2.0.0".to_string()));
std::env::set_current_dir(orig).unwrap();
}
#[test]
#[serial]
fn test_find_latest_tag_prerelease_suffix_with_default_sort() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
init_repo_with_tags(dir, &["v1.0.0", "v1.1.0", "v1.1.1-rc.1"]);
let orig = std::env::current_dir().unwrap();
std::env::set_current_dir(dir).unwrap();
let result_no_suffix = find_latest_tag_matching("v{{ .Version }}", None, None).unwrap();
assert_eq!(
result_no_suffix,
Some("v1.1.1-rc.1".to_string()),
"without prerelease_suffix, SemVer sort puts v1.1.1-rc.1 highest"
);
let gc = crate::config::GitConfig {
prerelease_suffix: Some("-rc".to_string()),
..Default::default()
};
let result = find_latest_tag_matching("v{{ .Version }}", Some(&gc), None).unwrap();
assert_eq!(
result,
Some("v1.1.1-rc.1".to_string()),
"prerelease_suffix activates git-delegated sort; v1.1.1-rc.1 still highest"
);
let run = |args: &[&str]| {
let out = std::process::Command::new("git")
.args(args)
.current_dir(dir)
.env("GIT_AUTHOR_NAME", "test")
.env("GIT_AUTHOR_EMAIL", "test@test.com")
.env("GIT_COMMITTER_NAME", "test")
.env("GIT_COMMITTER_EMAIL", "test@test.com")
.output()
.unwrap();
assert!(out.status.success());
};
run(&["tag", "v1.1.1"]);
let result_both = find_latest_tag_matching("v{{ .Version }}", Some(&gc), None).unwrap();
assert!(
result_both.is_some(),
"should find a tag with both release and rc present"
);
std::env::set_current_dir(orig).unwrap();
}
#[test]
#[serial]
fn test_find_latest_tag_ignore_tags_template_rendered() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
init_repo_with_tags(dir, &["v1.0.0", "v2.0.0", "v3.0.0"]);
let orig = std::env::current_dir().unwrap();
std::env::set_current_dir(dir).unwrap();
let mut vars = crate::template::TemplateVars::new();
vars.set_env("IGNORE_TAG", "v3.0.0");
let gc = crate::config::GitConfig {
ignore_tags: Some(vec!["{{ .Env.IGNORE_TAG }}".to_string()]),
..Default::default()
};
let result_raw = find_latest_tag_matching("v{{ .Version }}", Some(&gc), None).unwrap();
assert_eq!(result_raw, Some("v3.0.0".to_string()));
let result_rendered =
find_latest_tag_matching("v{{ .Version }}", Some(&gc), Some(&vars)).unwrap();
assert_eq!(result_rendered, Some("v2.0.0".to_string()));
std::env::set_current_dir(orig).unwrap();
}
fn init_repo_with_tagged_commits(dir: &std::path::Path, tags: &[&str]) {
use std::process::Command;
let run = |args: &[&str]| {
let out = Command::new("git")
.args(args)
.current_dir(dir)
.env("GIT_AUTHOR_NAME", "test")
.env("GIT_AUTHOR_EMAIL", "test@test.com")
.env("GIT_COMMITTER_NAME", "test")
.env("GIT_COMMITTER_EMAIL", "test@test.com")
.output()
.unwrap();
assert!(
out.status.success(),
"git {:?} failed: {}",
args,
String::from_utf8_lossy(&out.stderr)
);
};
run(&["init"]);
run(&["config", "user.email", "test@test.com"]);
run(&["config", "user.name", "test"]);
for (i, tag) in tags.iter().enumerate() {
let filename = format!("file_{}", i);
std::fs::write(dir.join(&filename), format!("content {}", i)).unwrap();
run(&["add", "."]);
run(&["commit", "-m", &format!("commit for {}", tag)]);
run(&["tag", tag]);
}
}
#[test]
#[serial]
fn test_find_previous_tag_with_ignore_tags() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
init_repo_with_tagged_commits(dir, &["v1.0.0", "v2.0.0", "v3.0.0"]);
let orig = std::env::current_dir().unwrap();
std::env::set_current_dir(dir).unwrap();
let result = find_previous_tag("v3.0.0", None, None).unwrap();
assert_eq!(result, Some("v2.0.0".to_string()));
let gc = crate::config::GitConfig {
ignore_tags: Some(vec!["v2.0.0".to_string()]),
..Default::default()
};
let result_filtered = find_previous_tag("v3.0.0", Some(&gc), None).unwrap();
assert_eq!(result_filtered, Some("v1.0.0".to_string()));
std::env::set_current_dir(orig).unwrap();
}
#[test]
#[serial]
fn test_find_previous_tag_with_ignore_tag_prefixes() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
init_repo_with_tagged_commits(dir, &["v1.0.0", "nightly-v2.0.0", "v3.0.0"]);
let orig = std::env::current_dir().unwrap();
std::env::set_current_dir(dir).unwrap();
let result = find_previous_tag("v3.0.0", None, None).unwrap();
assert_eq!(result, Some("nightly-v2.0.0".to_string()));
let gc = crate::config::GitConfig {
ignore_tag_prefixes: Some(vec!["nightly-".to_string()]),
..Default::default()
};
let result_filtered = find_previous_tag("v3.0.0", Some(&gc), None).unwrap();
assert_eq!(result_filtered, Some("v1.0.0".to_string()));
std::env::set_current_dir(orig).unwrap();
}
#[test]
#[serial]
fn test_find_previous_tag_no_config_unchanged_behavior() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
init_repo_with_tagged_commits(dir, &["v1.0.0", "v2.0.0"]);
let orig = std::env::current_dir().unwrap();
std::env::set_current_dir(dir).unwrap();
let result = find_previous_tag("v2.0.0", None, None).unwrap();
assert_eq!(result, Some("v1.0.0".to_string()));
std::env::set_current_dir(orig).unwrap();
}
#[test]
fn test_strip_monorepo_prefix_with_match() {
assert_eq!(
strip_monorepo_prefix("subproject1/v1.2.3", "subproject1/"),
"v1.2.3"
);
}
#[test]
fn test_strip_monorepo_prefix_no_match() {
assert_eq!(strip_monorepo_prefix("v1.2.3", "subproject1/"), "v1.2.3");
}
#[test]
fn test_strip_monorepo_prefix_empty_prefix() {
assert_eq!(strip_monorepo_prefix("v1.2.3", ""), "v1.2.3");
}
#[test]
fn test_strip_monorepo_prefix_partial_match() {
assert_eq!(
strip_monorepo_prefix("subproject1/v1.2.3", "sub"),
"project1/v1.2.3"
);
}
#[test]
#[serial]
fn test_find_latest_tag_with_monorepo_prefix_filters_and_returns_full_tag() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
init_repo_with_tags(
dir,
&[
"v1.0.0",
"subproject1/v1.0.0",
"subproject1/v2.0.0",
"subproject2/v3.0.0",
],
);
let orig = std::env::current_dir().unwrap();
std::env::set_current_dir(dir).unwrap();
let result =
find_latest_tag_matching_with_prefix("v{{ .Version }}", None, None, Some("subproject1/"))
.unwrap();
assert_eq!(
result,
Some("subproject1/v2.0.0".to_string()),
"should return the full tag with prefix"
);
std::env::set_current_dir(orig).unwrap();
}
#[test]
#[serial]
fn test_find_latest_tag_with_monorepo_prefix_semver_comparison_uses_stripped_tag() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
init_repo_with_tags(dir, &["myapp/v1.0.0", "myapp/v2.0.0", "myapp/v1.5.0"]);
let orig = std::env::current_dir().unwrap();
std::env::set_current_dir(dir).unwrap();
let result =
find_latest_tag_matching_with_prefix("v{{ .Version }}", None, None, Some("myapp/"))
.unwrap();
assert_eq!(
result,
Some("myapp/v2.0.0".to_string()),
"should pick the highest version based on stripped semver"
);
std::env::set_current_dir(orig).unwrap();
}
#[test]
#[serial]
fn test_find_latest_tag_with_monorepo_prefix_no_matching_tags() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
init_repo_with_tags(dir, &["v1.0.0", "v2.0.0"]);
let orig = std::env::current_dir().unwrap();
std::env::set_current_dir(dir).unwrap();
let result =
find_latest_tag_matching_with_prefix("v{{ .Version }}", None, None, Some("myapp/"))
.unwrap();
assert_eq!(result, None);
std::env::set_current_dir(orig).unwrap();
}
#[test]
#[serial]
fn test_find_latest_tag_with_monorepo_prefix_none_behaves_like_original() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
init_repo_with_tags(dir, &["v1.0.0", "v1.1.0", "v2.0.0"]);
let orig = std::env::current_dir().unwrap();
std::env::set_current_dir(dir).unwrap();
let result_with_prefix =
find_latest_tag_matching_with_prefix("v{{ .Version }}", None, None, None).unwrap();
let result_original = find_latest_tag_matching("v{{ .Version }}", None, None).unwrap();
assert_eq!(result_with_prefix, result_original);
assert_eq!(result_with_prefix, Some("v2.0.0".to_string()));
std::env::set_current_dir(orig).unwrap();
}
#[test]
#[serial]
fn test_find_latest_tag_with_monorepo_prefix_and_prerelease() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
init_repo_with_tags(dir, &["svc/v1.0.0", "svc/v1.1.0-rc.1", "svc/v1.1.0"]);
let orig = std::env::current_dir().unwrap();
std::env::set_current_dir(dir).unwrap();
let result =
find_latest_tag_matching_with_prefix("v{{ .Version }}", None, None, Some("svc/")).unwrap();
assert_eq!(
result,
Some("svc/v1.1.0".to_string()),
"release v1.1.0 should win over v1.1.0-rc.1"
);
std::env::set_current_dir(orig).unwrap();
}
use super::commits::{add_path_in, commit_in};
#[test]
#[serial]
fn test_add_path_in_bail_redacts_token_in_stderr() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
init_repo_with_tags(dir, &[]);
let secret = "ghp_addpathintestSentinel_123456789";
let prev = std::env::var("GITHUB_TOKEN").ok();
unsafe {
std::env::set_var("GITHUB_TOKEN", secret);
}
let nonexistent = dir.join(format!("missing-{secret}.txt"));
let rel = nonexistent.strip_prefix(dir).unwrap();
let err = add_path_in(dir, rel).expect_err("git add must fail on a non-existent path");
let msg = format!("{err:#}");
unsafe {
if let Some(prev) = prev {
std::env::set_var("GITHUB_TOKEN", prev);
} else {
std::env::remove_var("GITHUB_TOKEN");
}
}
assert!(
!msg.contains(secret),
"add_path_in bail leaked GITHUB_TOKEN: {msg}"
);
assert!(
msg.contains("$GITHUB_TOKEN"),
"redaction must substitute $GITHUB_TOKEN: {msg}"
);
}
#[test]
#[serial]
fn test_commit_in_bail_redacts_token_in_stderr() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
init_repo_with_tags(dir, &[]);
let secret = "ghp_commitintestSentinel_987654321";
let prev = std::env::var("GITHUB_TOKEN").ok();
unsafe {
std::env::set_var("GITHUB_TOKEN", secret);
}
let msg_with_secret = format!("release {secret}");
let err = commit_in(dir, &msg_with_secret, false)
.expect_err("commit must fail when nothing is staged");
let msg = format!("{err:#}");
unsafe {
if let Some(prev) = prev {
std::env::set_var("GITHUB_TOKEN", prev);
} else {
std::env::remove_var("GITHUB_TOKEN");
}
}
assert!(
!msg.contains(secret),
"commit_in bail leaked GITHUB_TOKEN: {msg}"
);
}
#[test]
fn test_detect_github_repo_error_strips_url_credentials() {
let leaky = "https://ghp_leakytoken@gitlab.example.com/grp/proj.git";
let scrubbed = redact_url_credentials(leaky);
assert!(!scrubbed.contains("ghp_leakytoken"));
assert_eq!(
scrubbed,
"https://<redacted>@gitlab.example.com/grp/proj.git"
);
}
#[test]
fn short_commit_str_truncates_to_seven_chars_to_match_git_short() {
use super::commits::{SHORT_COMMIT_LEN, short_commit_str};
assert_eq!(SHORT_COMMIT_LEN, 7);
let full = "deadbeef1234567890abcdef";
let short = short_commit_str(full);
assert_eq!(short.len(), 7);
assert_eq!(short, "deadbee");
}
#[test]
fn short_commit_str_passes_short_inputs_through_unchanged() {
use super::commits::short_commit_str;
assert_eq!(short_commit_str("abc"), "abc");
assert_eq!(short_commit_str("abc1234"), "abc1234");
assert_eq!(short_commit_str(""), "");
}
#[test]
fn head_is_at_tag_returns_true_when_head_has_tag() {
use super::tags::head_is_at_tag;
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
init_repo_with_tagged_commits(dir, &["v1.0.0"]);
assert!(
head_is_at_tag(dir).unwrap(),
"HEAD has tag v1.0.0 attached; head_is_at_tag should return true"
);
}
#[test]
fn head_is_at_tag_returns_false_when_head_has_no_tag() {
use super::tags::head_is_at_tag;
use std::process::Command;
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
init_repo_with_tagged_commits(dir, &["v1.0.0"]);
std::fs::write(dir.join("untagged.txt"), "no tag here").unwrap();
Command::new("git")
.current_dir(dir)
.env("GIT_AUTHOR_NAME", "test")
.env("GIT_AUTHOR_EMAIL", "test@test.com")
.env("GIT_COMMITTER_NAME", "test")
.env("GIT_COMMITTER_EMAIL", "test@test.com")
.args(["add", "."])
.output()
.unwrap();
Command::new("git")
.current_dir(dir)
.env("GIT_AUTHOR_NAME", "test")
.env("GIT_AUTHOR_EMAIL", "test@test.com")
.env("GIT_COMMITTER_NAME", "test")
.env("GIT_COMMITTER_EMAIL", "test@test.com")
.args(["commit", "-m", "post-tag commit"])
.output()
.unwrap();
assert!(
!head_is_at_tag(dir).unwrap(),
"HEAD is one commit past v1.0.0; head_is_at_tag should return false"
);
}