Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ jobs:
hk install --mise

- name: Run hk ci workflow
run: hk ci
run: hk run ci

security_audit:
name: Security Audit
Expand Down
4 changes: 2 additions & 2 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#![doc = r#"
#![doc = r"
Configuration types and utilities for submod.

Defines serializable wrappers for git submodule options, project-level defaults, and submodule
Expand All @@ -18,7 +18,7 @@ Features:

TODO:
- Add validation for config values when loading from file.
"#]
"]

use anyhow::{Context, Result};
use bstr::BStr;
Expand Down
43 changes: 40 additions & 3 deletions src/gitoxide_manager.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#![doc = r#"
#![doc = r"
# Gitoxide-Based Submodule Manager

Provides core logic for managing git submodules using the [`gitoxide`](https://github.com/Byron/gitoxide) library, with fallbacks to `git2` and the Git CLI when needed. Supports advanced features like sparse checkout and TOML-based configuration.
Expand Down Expand Up @@ -42,7 +42,7 @@ All operations return [`SubmoduleError`](src/gitoxide_manager.rs:14) for consist
## Usage

Use this module as the backend for CLI commands to manage submodules in a repository. See the project [README](README.md) for usage examples and configuration details.
"#]
"]

use crate::config::{Config, SubmoduleConfig, SubmoduleGitOptions};
use gix::Repository;
Expand Down Expand Up @@ -385,9 +385,46 @@ impl GitoxideSubmoduleManager {
.output()
.map_err(SubmoduleError::IoError)?;

// Clean up any existing broken submodule state
let target_path = std::path::Path::new(workdir).join(path);

// Always try to clean up, even if the directory doesn't exist
// because there might be git metadata left behind

// Try to deinitialize the submodule first
let _ = Command::new("git")
.args(["submodule", "deinit", "-f", path])
.current_dir(workdir)
.output();

// Remove the submodule from .gitmodules and .git/config
let _ = Command::new("git")
.args(["rm", "-f", path])
.current_dir(workdir)
.output();

// Remove the directory if it exists
if target_path.exists() {
let _ = std::fs::remove_dir_all(&target_path);
}

// Clean up git modules directory
let git_modules_path = std::path::Path::new(workdir)
.join(".git/modules")
.join(path);
if git_modules_path.exists() {
let _ = std::fs::remove_dir_all(&git_modules_path);
}

// Also try to clean up parent directories in git modules if they're empty
if let Some(parent) = git_modules_path.parent() {
let _ = std::fs::remove_dir(parent); // This will only succeed if empty
}

// Use --force to ensure git overwrites any stale state
// Explicitly specify the main branch to avoid default branch issues
let output = Command::new("git")
.args(["submodule", "add", "--force", url, path])
.args(["submodule", "add", "--force", "--branch", "main", url, path])
.current_dir(workdir)
.output()
.map_err(SubmoduleError::IoError)?;
Expand Down
4 changes: 2 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#![doc = r#"
#![doc = r"
Main entry point for the submod CLI tool.

Parses command-line arguments and dispatches submodule management commands using the
Expand All @@ -15,7 +15,7 @@ and syncing submodules with advanced features like sparse checkout.
- `sync`: Run check, init, and update in sequence.

Exits with an error if any operation fails.
"#]
"]

mod commands;
mod config;
Expand Down
28 changes: 26 additions & 2 deletions tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ impl TestHarness {
return Err(format!("Failed to init git repo: {stderr}").into());
}

// Ensure we're on the main branch
Command::new("git")
.args(["checkout", "-b", "main"])
.current_dir(&self.work_dir)
.output()?;

// Configure git user for tests
Command::new("git")
.args(["config", "user.name", "Test User"])
Expand Down Expand Up @@ -128,13 +134,25 @@ impl TestHarness {
.arg(&remote_dir)
.output()?;

// Set the default branch to main for the bare repository
Command::new("git")
.args(["symbolic-ref", "HEAD", "refs/heads/main"])
.current_dir(&remote_dir)
.output()?;

// Create a working copy to add content
let work_copy = self.temp_dir.path().join(format!("{name}_work"));
Command::new("git")
.args(["init"])
.arg(&work_copy)
.output()?;

// Set the default branch to main for the working copy
Command::new("git")
.args(["checkout", "-b", "main"])
.current_dir(&work_copy)
.output()?;

// Set up remote
Command::new("git")
.args(["remote", "add", "origin", remote_dir.to_str().unwrap()])
Expand Down Expand Up @@ -181,11 +199,17 @@ impl TestHarness {
.current_dir(&work_copy)
.output()?;

Command::new("git")
.args(["push", "origin", "main"])
let push_output = Command::new("git")
.args(["push", "--no-verify", "origin", "main"])
.current_dir(&work_copy)
.output()?;

// Check if push was successful
if !push_output.status.success() {
let stderr = String::from_utf8_lossy(&push_output.stderr);
return Err(format!("Failed to push to remote: {stderr}").into());
}

Ok(remote_dir)
}

Expand Down
Loading