Skip to content

Commit 5c2c8c5

Browse files
feat(submodule): disable command updates .gitmodules active status (#50)
Co-authored-by: bashandbone <89049923+bashandbone@users.noreply.github.com> Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
1 parent 16bdfbe commit 5c2c8c5

7 files changed

Lines changed: 173 additions & 3 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/commands.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,6 @@ pub enum Commands {
289289
name: String,
290290
},
291291

292-
// TODO: Implement this command (use git2). Functionally this changes a module to `active = false` in our config and `.gitmodules`, but does not delete the submodule from the filesystem.
293292
#[command(
294293
name = "disable",
295294
visible_alias = "d",

src/git_manager.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1502,6 +1502,26 @@ impl GitManager {
15021502
.submodules
15031503
.update_entry(name.to_string(), updated);
15041504

1505+
// Update .gitmodules
1506+
if let Ok(mut entries) = self.git_ops.read_gitmodules() {
1507+
// Find by name, or fall back to finding by path
1508+
let gitmodules_name = if entries.get(name).is_some() {
1509+
Some(name.to_string())
1510+
} else {
1511+
entries
1512+
.submodule_iter()
1513+
.find(|(_, e)| e.path.as_deref() == Some(path.as_str()))
1514+
.map(|(n, _)| n.to_string())
1515+
};
1516+
1517+
if let Some(gm_name) = gitmodules_name {
1518+
let mut gitmodules_entry = entries.get(&gm_name).cloned().unwrap();
1519+
gitmodules_entry.active = Some(false);
1520+
entries.update_entry(gm_name, gitmodules_entry);
1521+
let _ = self.git_ops.write_gitmodules(&entries);
1522+
}
1523+
}
1524+
15051525
self.write_full_config()?;
15061526
println!("Disabled submodule '{name}'.");
15071527
Ok(())

src/git_ops/git2_ops.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,10 @@ impl GitOperations for Git2Operations {
246246
};
247247
config.set_str(&format!("submodule.{name}.update"), update_str)?;
248248
}
249+
if let Some(active) = entry.active {
250+
let active_str = if active { "true" } else { "false" };
251+
config.set_str(&format!("submodule.{name}.active"), active_str)?;
252+
}
249253
// Set URL if different
250254
if let Some(url) = &entry.url
251255
&& submodule.url() != Some(url.as_str()) {

src/git_ops/gix_ops.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,15 @@ impl GitOperations for GixOperations {
194194
value.as_bytes().as_bstr(),
195195
)?;
196196
}
197+
if let Some(active) = entry.active {
198+
let value = if active { "true" } else { "false" };
199+
git_config.set_raw_value_by(
200+
"submodule",
201+
Some(subsection_name),
202+
"active",
203+
value.as_bytes().as_bstr(),
204+
)?;
205+
}
197206
}
198207

199208
// Write to .gitmodules file

tests/git_ops_tests.rs

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,9 +390,67 @@ mod git2_ops_tests {
390390
.expect("add submodule");
391391

392392
let mut ops = Git2Operations::new(Some(&harness.work_dir)).expect("ops");
393-
let entries = ops.read_gitmodules().expect("read_gitmodules");
393+
let mut entries = ops.read_gitmodules().expect("read_gitmodules");
394394
// write_gitmodules with the same entries should succeed without error
395395
ops.write_gitmodules(&entries).expect("write_gitmodules");
396+
397+
// Verify that updating `active` sets it in the git configuration
398+
// In .gitmodules the git2 fallback might name the submodule by its path 'lib/writesub'
399+
let name = if entries.get("write-sub").is_some() {
400+
"write-sub"
401+
} else {
402+
"lib/writesub"
403+
};
404+
405+
if let Some(mut entry) = entries.get(name).cloned() {
406+
entry.active = Some(false);
407+
entries.update_entry(name.to_string(), entry);
408+
}
409+
ops.write_gitmodules(&entries).expect("write_gitmodules active false");
410+
411+
// Check git2 config manually or via read_gitmodules? Actually read_gitmodules in git2
412+
// doesn't read active from .git/config, but wait, it is set in `.git/config`!
413+
let config_path = harness.work_dir.join(".git").join("config");
414+
let config_content = std::fs::read_to_string(&config_path).expect("read git config");
415+
assert!(config_content.contains("active = false"), "submodule should be inactive in config");
416+
}
417+
418+
#[test]
419+
fn test_with_submodule_write_gitmodules_active_none() {
420+
let harness = TestHarness::new().expect("harness");
421+
harness.init_git_repo().expect("init repo");
422+
let remote = harness.create_test_remote("g2_write_sub_none").expect("remote");
423+
let remote_url = format!("file://{}", remote.display());
424+
425+
harness
426+
.run_submod_success(&[
427+
"add",
428+
&remote_url,
429+
"--name",
430+
"write-sub-none",
431+
"--path",
432+
"lib/writesubnone",
433+
])
434+
.expect("add submodule");
435+
436+
let mut ops = Git2Operations::new(Some(&harness.work_dir)).expect("ops");
437+
let mut entries = ops.read_gitmodules().expect("read_gitmodules");
438+
439+
let name = if entries.get("write-sub-none").is_some() {
440+
"write-sub-none"
441+
} else {
442+
"lib/writesubnone"
443+
};
444+
445+
if let Some(mut entry) = entries.get(name).cloned() {
446+
entry.active = None;
447+
entries.update_entry(name.to_string(), entry);
448+
}
449+
ops.write_gitmodules(&entries).expect("write_gitmodules active none");
450+
451+
let config_path = harness.work_dir.join(".git").join("config");
452+
let config_content = std::fs::read_to_string(&config_path).expect("read git config");
453+
assert!(!config_content.contains("active ="), "submodule active should be untouched");
396454
}
397455
}
398456

@@ -554,6 +612,46 @@ mod gix_ops_tests {
554612
);
555613
}
556614

615+
#[test]
616+
fn test_write_gitmodules_active_false() {
617+
let harness = TestHarness::new().expect("harness");
618+
harness.init_git_repo().expect("init repo");
619+
let mut ops = GixOperations::new(Some(&harness.work_dir)).expect("ops");
620+
621+
let mut entries = one_entry_entries();
622+
if let Some(mut entry) = entries.get("test-lib").cloned() {
623+
entry.active = Some(false);
624+
entries.update_entry("test-lib".to_string(), entry);
625+
}
626+
627+
ops.write_gitmodules(&entries)
628+
.expect("write_gitmodules should succeed");
629+
630+
let content = std::fs::read_to_string(harness.work_dir.join(".gitmodules"))
631+
.expect("read .gitmodules");
632+
assert!(
633+
content.contains("active = false"),
634+
".gitmodules should contain active = false"
635+
);
636+
}
637+
638+
#[test]
639+
fn test_write_gitmodules_active_none() {
640+
let harness = TestHarness::new().expect("harness");
641+
harness.init_git_repo().expect("init repo");
642+
let mut ops = GixOperations::new(Some(&harness.work_dir)).expect("ops");
643+
644+
let mut entries = one_entry_entries();
645+
if let Some(mut entry) = entries.get("test-lib").cloned() {
646+
entry.active = None;
647+
entries.update_entry("test-lib".to_string(), entry);
648+
}
649+
650+
ops.write_gitmodules(&entries).expect("write_gitmodules");
651+
let content = std::fs::read_to_string(harness.work_dir.join(".gitmodules")).expect("read");
652+
assert!(!content.contains("active ="));
653+
}
654+
557655
#[test]
558656
fn test_write_gitmodules_empty_entries() {
559657
let harness = TestHarness::new().expect("harness");

tests/integration_tests.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,46 @@ active = true
557557
// Config should show active = false
558558
let config = harness.read_config().expect("Failed to read config");
559559
assert!(config.contains("active = false"));
560+
561+
// .gitmodules should show active = false
562+
let gitmodules_path = harness.work_dir.join(".gitmodules");
563+
let gitmodules_content = std::fs::read_to_string(&gitmodules_path)
564+
.expect("Failed to read .gitmodules");
565+
println!("GITMODULES CONTENT:\n{gitmodules_content}");
566+
assert!(gitmodules_content.contains("active = false"));
567+
}
568+
569+
#[test]
570+
fn test_disable_command_matching_name() {
571+
let harness = TestHarness::new().expect("Failed to create test harness");
572+
harness.init_git_repo().expect("Failed to init git repo");
573+
574+
// Manually create a .gitmodules with a matching name
575+
let gitmodules_content = "\
576+
[submodule \"my-lib\"]
577+
\tpath = lib/my
578+
\turl = https://example.com/my-lib.git
579+
";
580+
std::fs::write(harness.work_dir.join(".gitmodules"), gitmodules_content)
581+
.expect("Failed to write .gitmodules");
582+
583+
let config_content = "\
584+
[my-lib]
585+
path = \"lib/my\"
586+
url = \"https://example.com/my-lib.git\"
587+
active = true
588+
";
589+
harness.create_config(config_content).expect("Failed to create config");
590+
591+
let stdout = harness
592+
.run_submod_success(&["disable", "my-lib"])
593+
.expect("Failed to disable submodule");
594+
595+
assert!(stdout.contains("Disabled submodule 'my-lib'"));
596+
597+
let gitmodules_updated = std::fs::read_to_string(harness.work_dir.join(".gitmodules"))
598+
.expect("Failed to read .gitmodules");
599+
assert!(gitmodules_updated.contains("active = false"));
560600
}
561601

562602
#[test]

0 commit comments

Comments
 (0)