Skip to content

feat(submodule): add deinit method to Submodule (#2014) - #2129

Merged
Byron merged 2 commits into
gitpython-developers:mainfrom
mvanhorn:feat/2014-submodule-deinit
Jul 16, 2026
Merged

feat(submodule): add deinit method to Submodule (#2014)#2129
Byron merged 2 commits into
gitpython-developers:mainfrom
mvanhorn:feat/2014-submodule-deinit

Conversation

@mvanhorn

Copy link
Copy Markdown
Contributor

Closes #2014.

Adds a `Submodule.deinit(force=False)` method that wraps `git submodule deinit [--force] -- `, paralleling the existing `add` / `update` / `remove` methods. Callers no longer need the `repo.git.submodule('deinit', ...)` workaround mentioned in the issue.

Behavior

  • Delegates to `self.repo.git.submodule('deinit', ...)`, so output, error handling, and subprocess invocation match every other GitPython submodule method.
  • Decorated with `@unbare_repo` to match `remove` and `add` — deinit is meaningless in bare repos.
  • Leaves `.gitmodules` and `.git/modules/` intact. A subsequent `sm.update()` re-initializes from those retained contents, which matches the git CLI semantics.
  • `force=True` passes `--force` so callers can deinit submodules with local modifications (same semantics as `git submodule deinit -f`).

Why a thin wrapper

The full semantics of `git submodule deinit` (recursive children, working-tree checks, .git/modules retention) are already handled correctly by the git porcelain. Re-implementing that logic in Python for parity with the existing `remove` method would duplicate significant behavior. A thin wrapper gives callers a typed entry point without duplicating git's state machine.

Verification

  • `ruff check git/objects/submodule/base.py` → clean.
  • `mypy git/objects/submodule/base.py` → no new errors (the file has pre-existing mypy warnings unrelated to this change).
  • Imported the class after install: `Submodule.deinit` is present with the expected docstring.
  • Tests not added in this PR — the existing `test/test_submodule.py` suite couldn't run cleanly against this checkout because the `git/ext/gitdb/gitdb/ext/smmap` submodule fixture expects `git submodule update --init --recursive` to have completed on the clone, which wasn't the case here. Happy to add a targeted test if you can point me at the right fixture setup, or tell me if you'd rather land this as a wrapper-only change first.

🤖 AI-assisted.

@Byron
Byron requested a review from Copilot April 20, 2026 13:55
@Byron
Byron marked this pull request as draft April 20, 2026 13:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a first-class Submodule.deinit(force=False) API to GitPython’s submodule object model, so callers can deinitialize a submodule without manually invoking repo.git.submodule('deinit', ...).

Changes:

  • Introduces Submodule.deinit(force: bool = False) as a thin wrapper around git submodule deinit [--force] -- <path>.
  • Marks deinit as unsupported for bare repositories via @unbare_repo.
  • Updates typing imports to support the new method’s local argument list construction.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1271 to +1299
@unbare_repo
def deinit(self, force: bool = False) -> "Submodule":
"""Run ``git submodule deinit`` on this submodule.

This is a thin wrapper around ``git submodule deinit <path>``, paralleling
:meth:`add`, :meth:`update`, and :meth:`remove`. It unregisters the
submodule (removes its entry from ``.git/config`` and empties the
working-tree directory) without deleting the submodule from
``.gitmodules`` or its checked-out repository under ``.git/modules/``.
A subsequent :meth:`update` will re-initialize the submodule from the
retained contents.

:param force:
If ``True``, pass ``--force`` to ``git submodule deinit``. This
allows deinitialization even when the submodule's working tree has
local modifications that would otherwise block the command.

:return:
self

:note:
Doesn't work in bare repositories.
"""
args: List[str] = []
if force:
args.append("--force")
args.extend(["--", self.path])
self.repo.git.submodule("deinit", *args)
return self

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new public API Submodule.deinit() isn't covered by tests. Please add a targeted unit test that asserts the underlying git invocation (e.g., by mocking submodule.repo.git.submodule) for both force=False and force=True, including verifying the -- separator and the provided path argument.

Copilot uses AI. Check for mistakes.
@mvanhorn
mvanhorn marked this pull request as ready for review May 5, 2026 16:09
@mvanhorn

mvanhorn commented May 5, 2026

Copy link
Copy Markdown
Contributor Author

Added test_deinit_calls_git_submodule in 8e24b0f -- mocks Git.submodule and asserts both arg orderings:

  • deinit() (default) calls with ("deinit", "--", path)
  • deinit(force=True) calls with ("deinit", "--force", "--", path)

Verified locally with pytest test/test_submodule.py -k deinit -v (passed) plus ruff check and ruff format --check clean. Marking the PR ready for human review.

@Byron
Byron marked this pull request as draft May 6, 2026 03:36
@Byron

Byron commented May 6, 2026

Copy link
Copy Markdown
Member

I set this back to draft while CI fails.

@mvanhorn

Copy link
Copy Markdown
Contributor Author

Fixed in 7b0e1ae. It was a mypy error, not a test failure: self.path is str | PathLike[str] but the deinit args list is List[str], so mypy flagged base.py:1297. Wrapped the path in str() (git submodule expects a string path anyway). Verified locally - mypy clean across all 44 files and the deinit test still passes.

@mvanhorn
mvanhorn marked this pull request as ready for review July 15, 2026 23:47
mvanhorn and others added 2 commits July 16, 2026 06:40
…#2014)

Mirrors the pattern of `Submodule.add`, `Submodule.update`, and
`Submodule.remove` by exposing `git submodule deinit` as a first-class
method, so callers no longer need the `repo.git.submodule('deinit', ...)`
workaround noted in the issue.

The method delegates to `git submodule deinit [--force] -- <path>` via
`self.repo.git.submodule`, decorated with `@unbare_repo` to match the
other mutating Submodule methods. It unregisters the submodule from
`.git/config` and clears the working-tree directory while leaving
`.gitmodules` and `.git/modules/<name>` intact, so a later `update()`
can re-initialize.
@Byron
Byron force-pushed the feat/2014-submodule-deinit branch from 7b0e1ae to 0c0cc8a Compare July 16, 2026 04:45
@Byron
Byron merged commit e87854b into gitpython-developers:main Jul 16, 2026
30 checks passed
@Byron

Byron commented Jul 16, 2026

Copy link
Copy Markdown
Member

Thanks a lot!

@mvanhorn

Copy link
Copy Markdown
Contributor Author

Appreciate the merge, @Byron - deinit rounds out the submodule lifecycle nicely alongside add.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

3 participants