Skip to content

Instantly share code, notes, and snippets.

@serrasqueiro
Last active January 2, 2026 17:05
Show Gist options
  • Select an option

  • Save serrasqueiro/3fff85f0fca07f36d7ead661aa6274a5 to your computer and use it in GitHub Desktop.

Select an option

Save serrasqueiro/3fff85f0fca07f36d7ead661aa6274a5 to your computer and use it in GitHub Desktop.
MIT license
MIT License
Copyright (c) 2026 Trisconta (Henrique Moreira)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
#!/usr/bin/env python
""" submodulecleaner.py -- for Windows/ Linux usage
(c)2026 Henrique Moreira (part of Trisconta)
"""
import sys
import os
import subprocess
from git import Repo
def main():
""" Just call the right 'test'
"""
do_script(sys.argv[1:])
def do_script(args):
param = args if args else ["src/packages/xlrd"]
subm = param[0]
if len(param) != 1 or subm.startswith("-"):
print(f"""Usage:
{__file__} submodule
submodule is a GIT submodule path within current repo.
""")
obj = SubmoduleHandler()
for key, item in obj.my_submodules().items():
print(key, item, end="\n\n")
return None
isok = submodule_delete(subm)
print("Deleted submodule(s):", "OK" if isok else "NotOk", subm)
return isok
def submodule_delete(subm):
obj = GitSubmoduleCleaner(subm)
inited = obj.submodule_initialized()
print(obj.path, "; submodule_initialized() ?", obj.subm_name, inited)
#print(obj.repo)
print("Submodule:", obj.is_there())
return obj.clean()
class GenericRun:
""" Abstract class for running commands. """
def __init__(self, path, name="R"):
self._name, self.path = name, path
self._linux = os.name != "nt"
def get_name(self):
return self._name
def a_path(self, path):
if self._linux:
assert "\\" not in path, f"Bad path: {path}"
return path
return path.replace("/", "\\")
def do_confirm(self, msg):
if msg:
print(msg)
answer = input("Proceed? [y/N]: ").strip().lower()
return answer == "y"
class SubmoduleHandler(GenericRun):
""" GIT submodule handler class. """
def __init__(self, submodule="", path=""):
super().__init__(path if path else p_path(os.getcwd()))
repo = Repo(self.path)
self.repo = repo
self.subm_name = submodule
self._submodule = repo.submodule(submodule) if submodule else None
self._subm_dct = {
sm.path: (sm.branch_path, is_initialized(self.repo, sm.name), sm) for sm in self.repo.submodules
}
def still_there(self):
""" Whether the submodule is there. """
submods = {sm.path for sm in self.repo.submodules}
#print(f"Debug: submodule={self._submodule.path}, listed: {submods}")
return self._submodule.path in submods
def my_submodules(self):
return self._subm_dct
def is_there(self) -> tuple:
""" Returns (True, <PATH>) if submodule path is there. """
full = os.path.join(self.repo.git_dir, "modules", self._submodule.path)
return os.path.isdir(full), p_path(full)
def submodule_initialized(self) -> bool:
""" Returns True if submodule path is initialized (and there as well). """
if not self.still_there():
return False # Nothing to 'deinit'
isit = is_initialized(self.repo, self.subm_name)
isok, full = self.is_there()
if isit:
assert isok, f"Missing: {full}"
return True
return False
class GitSubmoduleCleaner(SubmoduleHandler):
""" Submodule cleaner for Windows/ Linux """
def __init__(self, submodule, path=""):
""" Normalize to an absolute path with forward slashes """
super().__init__(submodule, os.path.abspath(path) if path else "")
def clear_readonly(self, path):
assert path, "clear_readonly()"
if self._linux:
return True
pack_path = f"{path}/objects/pack/*"
subprocess.run(
["attrib", "-R", self.a_path(pack_path), "/S"],
shell=True,
check=False,
)
return True
def remove_directory(self, path):
assert path, "remove_directory()"
assert "/.git/" in path, path
if self._linux:
subprocess.run(
["rm", "-rf", path],
shell=False,
check=False,
)
else:
subprocess.run(
["rmdir", "/S", "/Q", self.a_path(path)],
shell=True,
check=False,
)
def clean(self, confirm=True):
""" Clean submodule and storing .git dir! """
isok, full = self.is_there()
if not isok:
print(f"Could not find: {full}")
return False
if self.submodule_initialized():
print(f"""Cowardly refusing to clean an initialized submodule: {full}\n
Do first:
git submodule deinit -f {self.subm_name}""")
return False
if confirm:
if not self.do_confirm(f"About to delete: {full}"):
return False
self.clear_readonly(full)
self.remove_directory(full)
isok = not self.is_there()[0]
return isok
def p_path(path):
""" Normalized name to contain only slashes. """
return path.replace("\\", "/")
def is_initialized(repo, submodule_path):
""" A submodule is considered deinitialized if:
- It exists in .gitmodules
- But is NOT present in repo.submodules (GitPython only lists initialized ones)
"""
cfg = repo.config_reader()
section = f'submodule "{submodule_path}"'
return cfg.has_section(section)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment