use super::test_support::*;
use super::*;
use git2::Repository;
use std::{fs, path::Path};
#[test]
fn test_dirty_submodule_with_real_git_submodule() {
let (tmp, _sub_source, _sub_repo) = init_repo_with_submodule();
let status = query_status(tmp.path(), &SubmoduleConfig::default()).unwrap();
assert!(status.has_submodules);
assert!(!status.has_dirty_submodules);
assert!(status.submodules.is_empty());
let sub_workdir = tmp.path().join("my-sub");
fs::write(sub_workdir.join("lib.rs"), "fn hello() { /* changed */ }").unwrap();
let status = query_status(tmp.path(), &SubmoduleConfig::default()).unwrap();
assert!(status.has_submodules);
assert!(status.has_dirty_submodules);
assert!(!status.submodules.is_empty());
let sub_info = &status.submodules[0];
assert_eq!(sub_info.path, Path::new("my-sub"));
assert_eq!(sub_info.state, Some(SubmoduleState::Dirty));
let file_entry = status.files.iter().find(|f| f.path == Path::new("my-sub"));
assert!(file_entry.is_some());
let file_entry = file_entry.unwrap();
assert!(file_entry.is_submodule);
assert_eq!(file_entry.submodule_state, Some(SubmoduleState::Dirty));
}
#[test]
fn test_ignore_dirty_subs_hides_submodule_state() {
let (tmp, _sub_source, _sub_repo) = init_repo_with_submodule();
fs::write(tmp.path().join("my-sub/lib.rs"), "fn changed() {}").unwrap();
let status = query_status(
tmp.path(),
&SubmoduleConfig {
ignore_dirty: true,
warn_unpushed: false,
},
)
.unwrap();
assert!(status.has_submodules); assert!(!status.has_dirty_submodules);
assert!(status.submodules.is_empty());
assert!(!status.files.iter().any(|f| f.is_submodule));
}
#[test]
fn test_submodule_modified_pointer() {
let (tmp, _sub_source, _sub_repo) = init_repo_with_submodule();
add_unpushed_commit_in_sub(tmp.path(), "my-sub");
let status = query_status(tmp.path(), &SubmoduleConfig::default()).unwrap();
assert!(status.has_submodules);
assert!(status.has_dirty_submodules);
assert!(!status.submodules.is_empty());
let sub_info = &status.submodules[0];
assert_eq!(sub_info.path, Path::new("my-sub"));
assert!(
sub_info.state == Some(SubmoduleState::Modified)
|| sub_info.state == Some(SubmoduleState::Dirty),
"expected Modified or Dirty, got {:?}",
sub_info.state
);
assert!(sub_info.head_oid.is_some());
assert!(sub_info.workdir_oid.is_some());
assert_ne!(sub_info.head_oid, sub_info.workdir_oid);
}
#[test]
fn test_clean_submodule_not_reported() {
let (tmp, _sub_source, _sub_repo) = init_repo_with_submodule();
let status = query_status(tmp.path(), &SubmoduleConfig::default()).unwrap();
assert!(status.has_submodules);
assert!(!status.has_dirty_submodules);
assert!(status.submodules.is_empty());
assert!(!status.files.iter().any(|f| f.is_submodule));
}
#[test]
fn test_dirty_submodule_makes_repo_dirty() {
let (tmp, _sub_source, _sub_repo) = init_repo_with_submodule();
let status = query_status(tmp.path(), &SubmoduleConfig::default()).unwrap();
assert!(!status.is_dirty);
fs::write(tmp.path().join("my-sub/lib.rs"), "dirty").unwrap();
let status = query_status(tmp.path(), &SubmoduleConfig::default()).unwrap();
assert!(status.is_dirty);
}
#[test]
fn test_submodule_warn_default_is_clean() {
let warn = SubmoduleWarn::default();
assert!(warn.is_clean());
assert_eq!(warn.unpushed_commits, 0);
assert!(!warn.pointer_unreachable);
}
#[test]
fn test_submodule_warn_is_clean_predicate() {
assert!(SubmoduleWarn::default().is_clean());
assert!(
!SubmoduleWarn {
unpushed_commits: 1,
pointer_unreachable: false,
needs_merge_to_default: false,
}
.is_clean()
);
assert!(
!SubmoduleWarn {
unpushed_commits: 0,
pointer_unreachable: true,
needs_merge_to_default: false,
}
.is_clean()
);
assert!(
!SubmoduleWarn {
unpushed_commits: 0,
pointer_unreachable: false,
needs_merge_to_default: true,
}
.is_clean()
);
}
#[test]
fn test_clean_submodule_warn_fields_clean() {
let (tmp, _sub_source, _sub_repo) = init_repo_with_submodule();
let status = query_status(tmp.path(), &SubmoduleConfig::default()).unwrap();
assert!(!status.has_unpushed_submodules);
}
#[test]
fn test_dirty_submodule_warn_fields_stay_clean() {
let (tmp, _sub_source, _sub_repo) = init_repo_with_submodule();
fs::write(tmp.path().join("my-sub/lib.rs"), "dirty").unwrap();
let status = query_status(tmp.path(), &SubmoduleConfig::default()).unwrap();
assert!(status.has_dirty_submodules);
assert!(!status.has_unpushed_submodules);
let sub_info = &status.submodules[0];
assert!(sub_info.warn.is_clean());
let file_entry = status
.files
.iter()
.find(|f| f.path == Path::new("my-sub"))
.unwrap();
assert!(file_entry.submodule_warn.is_clean());
}
#[test]
fn test_warn_unpushed_false_zeros_warn_fields_even_when_dirty() {
let (tmp, _sub_source, _sub_repo) = init_repo_with_submodule();
fs::write(tmp.path().join("my-sub/lib.rs"), "dirty").unwrap();
let cfg = SubmoduleConfig {
ignore_dirty: false,
warn_unpushed: false,
};
let status = query_status(tmp.path(), &cfg).unwrap();
assert!(status.has_dirty_submodules);
assert!(!status.has_unpushed_submodules);
assert!(status.submodules[0].warn.is_clean());
}
#[test]
fn test_ignore_dirty_with_warn_unpushed_iterates_subs() {
let (tmp, _sub_source, _sub_repo) = init_repo_with_submodule();
fs::write(tmp.path().join("my-sub/lib.rs"), "dirty").unwrap();
let cfg = SubmoduleConfig {
ignore_dirty: true,
warn_unpushed: true,
};
let status = query_status(tmp.path(), &cfg).unwrap();
assert!(!status.has_dirty_submodules);
assert!(!status.has_unpushed_submodules);
assert!(status.submodules.is_empty());
}
#[test]
fn test_both_flags_off_skips_submodule_loop_entirely() {
let (tmp, _sub_source, _sub_repo) = init_repo_with_submodule();
fs::write(tmp.path().join("my-sub/lib.rs"), "dirty").unwrap();
let cfg = SubmoduleConfig {
ignore_dirty: true,
warn_unpushed: false,
};
let status = query_status(tmp.path(), &cfg).unwrap();
assert!(status.has_submodules); assert!(!status.has_dirty_submodules);
assert!(!status.has_unpushed_submodules);
assert!(status.submodules.is_empty());
}
#[test]
fn test_uninitialized_submodule_does_not_panic_on_warn_check() {
let (tmp, _sub_source, _sub_repo) = init_repo_with_submodule();
fs::remove_dir_all(tmp.path().join("my-sub")).unwrap();
let status = query_status(tmp.path(), &SubmoduleConfig::default()).unwrap();
assert!(status.has_submodules);
for sub in &status.submodules {
assert!(sub.warn.is_clean());
}
assert!(!status.has_unpushed_submodules);
}
#[test]
fn test_parent_pinning_unpushed_oid_marks_pointer_unreachable() {
let (tmp, _sub_source, _sub_repo) = init_repo_with_submodule();
let unpushed = add_unpushed_commit_in_sub(tmp.path(), "my-sub");
stage_submodule_pointer(tmp.path(), "my-sub");
let status = query_status(tmp.path(), &SubmoduleConfig::default()).unwrap();
assert!(status.has_unpushed_submodules);
let sub_info = status
.submodules
.iter()
.find(|s| s.path == Path::new("my-sub"))
.expect("submodule entry should exist with warn signal");
assert!(
sub_info.warn.pointer_unreachable,
"expected pointer_unreachable=true for staged oid {}, got {:?}",
unpushed, sub_info.warn
);
let file_entry = status
.files
.iter()
.find(|f| f.path == Path::new("my-sub"))
.expect("file entry for my-sub");
assert!(file_entry.is_submodule);
assert!(file_entry.submodule_warn.pointer_unreachable);
}
#[test]
fn test_warn_unpushed_false_zeros_unreachable_pointer() {
let (tmp, _sub_source, _sub_repo) = init_repo_with_submodule();
add_unpushed_commit_in_sub(tmp.path(), "my-sub");
stage_submodule_pointer(tmp.path(), "my-sub");
let cfg = SubmoduleConfig {
ignore_dirty: false,
warn_unpushed: false,
};
let status = query_status(tmp.path(), &cfg).unwrap();
assert!(!status.has_unpushed_submodules);
for sub in &status.submodules {
assert!(
sub.warn.is_clean(),
"warn fields must be zero when warn_unpushed=false, got {:?}",
sub.warn
);
}
}
#[test]
fn test_unpushed_commits_count_when_branch_ahead_of_upstream() {
let (tmp, _sub_source, _sub_repo) = init_repo_with_submodule();
let sub_dir = tmp.path().join("my-sub");
let inner = Repository::open(&sub_dir).unwrap();
let mut remote_branch: Option<String> = None;
if let Ok(branches) = inner.branches(Some(git2::BranchType::Remote)) {
for (b, _) in branches.flatten() {
if let Ok(Some(name)) = b.name()
&& let Some(stripped) = name.strip_prefix("origin/")
&& stripped != "HEAD"
{
remote_branch = Some(stripped.to_string());
break;
}
}
}
let branch = remote_branch.expect("submodule should have an origin/<branch> ref");
let remote_ref = inner
.find_reference(&format!("refs/remotes/origin/{}", branch))
.unwrap();
let remote_oid = remote_ref.target().unwrap();
let remote_commit = inner.find_commit(remote_oid).unwrap();
let mut local_branch = inner
.find_branch(&branch, git2::BranchType::Local)
.or_else(|_| inner.branch(&branch, &remote_commit, false))
.unwrap();
local_branch
.set_upstream(Some(&format!("origin/{}", branch)))
.unwrap();
inner.set_head(&format!("refs/heads/{}", branch)).unwrap();
let mut checkout = git2::build::CheckoutBuilder::new();
checkout.force();
inner.checkout_head(Some(&mut checkout)).unwrap();
add_unpushed_commit_in_sub(tmp.path(), "my-sub");
let status = query_status(tmp.path(), &SubmoduleConfig::default()).unwrap();
assert!(status.has_unpushed_submodules);
let sub_info = status
.submodules
.iter()
.find(|s| s.path == Path::new("my-sub"))
.expect("submodule entry expected");
assert_eq!(
sub_info.warn.unpushed_commits, 1,
"expected 1 unpushed commit, got {:?}",
sub_info.warn
);
assert!(
!sub_info.warn.pointer_unreachable,
"parent's pointer is unchanged and on origin — should be reachable"
);
}
#[test]
fn test_detached_head_at_remote_oid_no_warn() {
let (tmp, _sub_source, _sub_repo) = init_repo_with_submodule();
let status = query_status(tmp.path(), &SubmoduleConfig::default()).unwrap();
assert!(!status.has_unpushed_submodules);
for sub in &status.submodules {
assert!(sub.warn.is_clean());
}
}
#[test]
fn test_needs_merge_to_default_when_pinned_on_side_branch() {
let (tmp, _sub_source, _sub_repo) = init_repo_with_submodule();
let sub_dir = tmp.path().join("my-sub");
let inner = Repository::open(&sub_dir).unwrap();
let a = inner.head().unwrap().peel_to_commit().unwrap().id();
inner
.reference("refs/remotes/origin/main", a, true, "test setup")
.unwrap();
let b = add_commit_on_head(&inner, "feature.rs", "fn f() {}");
inner
.reference("refs/remotes/origin/feature", b, true, "test setup")
.unwrap();
stage_submodule_pointer(tmp.path(), "my-sub");
let status = query_status(tmp.path(), &SubmoduleConfig::default()).unwrap();
let sub = status
.submodules
.iter()
.find(|s| s.path == Path::new("my-sub"))
.expect("submodule entry expected");
assert!(
sub.warn.needs_merge_to_default,
"pinned commit is on origin/feature, not origin/main: {:?}",
sub.warn
);
assert!(
!sub.warn.pointer_unreachable,
"commit is on a remote, so it is reachable: {:?}",
sub.warn
);
assert!(!sub.warn.is_clean());
assert!(status.has_unpushed_submodules);
}
#[test]
fn test_no_merge_warning_when_pinned_on_default_branch() {
let (tmp, _sub_source, _sub_repo) = init_repo_with_submodule();
let sub_dir = tmp.path().join("my-sub");
let inner = Repository::open(&sub_dir).unwrap();
let a = inner.head().unwrap().peel_to_commit().unwrap().id();
inner
.reference("refs/remotes/origin/main", a, true, "test setup")
.unwrap();
let status = query_status(tmp.path(), &SubmoduleConfig::default()).unwrap();
for sub in &status.submodules {
assert!(
!sub.warn.needs_merge_to_default,
"pinned commit is on origin/main: {:?}",
sub.warn
);
}
}