187 lines
6.6 KiB
Python
Executable File
187 lines
6.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
Clean up git repositories and directories that are not listed in .gitmodules file.
|
|
This script will find all git repositories and potential submodule directories in the project
|
|
and remove those that are not listed as submodules in the .gitmodules file.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import shutil
|
|
import subprocess
|
|
import re
|
|
import argparse
|
|
from pathlib import Path
|
|
|
|
def get_submodule_paths(gitmodules_path):
|
|
"""
|
|
Parse .gitmodules file to extract all submodule paths.
|
|
|
|
Args:
|
|
gitmodules_path: Path to .gitmodules file
|
|
|
|
Returns:
|
|
A set of normalized paths for all submodules
|
|
"""
|
|
if not os.path.exists(gitmodules_path):
|
|
return set()
|
|
|
|
submodule_paths = set()
|
|
|
|
with open(gitmodules_path, 'r') as f:
|
|
content = f.read()
|
|
|
|
# Find all path entries in the .gitmodules file
|
|
path_matches = re.findall(r'^\s*path\s*=\s*(.+)$', content, re.MULTILINE)
|
|
|
|
for path in path_matches:
|
|
path = path.strip()
|
|
# Normalize path to ensure consistent comparison
|
|
normalized_path = os.path.normpath(path)
|
|
submodule_paths.add(normalized_path)
|
|
|
|
return submodule_paths
|
|
|
|
def is_git_repo(path):
|
|
"""
|
|
Check if a directory is a git repository.
|
|
|
|
Args:
|
|
path: Path to the directory
|
|
|
|
Returns:
|
|
True if the directory is a git repository, False otherwise
|
|
"""
|
|
# Check if .git directory exists
|
|
git_dir = os.path.join(path, '.git')
|
|
if os.path.exists(git_dir) and os.path.isdir(git_dir):
|
|
return True
|
|
|
|
# Check if .git file exists (for submodules)
|
|
git_file = os.path.join(path, '.git')
|
|
if os.path.exists(git_file) and os.path.isfile(git_file):
|
|
return True
|
|
|
|
return False
|
|
|
|
def load_submodule_whitelist(base_dir):
|
|
"""Load whitelist paths from .gitmodules in base_dir. Paths are normalized."""
|
|
gm_path = os.path.join(base_dir, '.gitmodules')
|
|
return get_submodule_paths(gm_path)
|
|
|
|
def scan_unexpected_git_repos(root_dir):
|
|
"""
|
|
Walk the tree from root_dir. For any git repo encountered under a base directory,
|
|
compare against that base's .gitmodules whitelist. If a child repo path relative to base
|
|
is not listed, mark it as unexpected. When a listed submodule is encountered, treat it
|
|
as a new base and use its own .gitmodules for deeper comparisons.
|
|
Returns list of paths (relative to root_dir) to remove.
|
|
"""
|
|
# Maintain mapping of base absolute path -> whitelist set
|
|
base_whitelists = {}
|
|
root_abs = os.path.abspath(root_dir)
|
|
base_whitelists[root_abs] = load_submodule_whitelist(root_abs)
|
|
|
|
def find_base_for(path_abs):
|
|
# choose deepest base that is a prefix of path_abs
|
|
candidates = [b for b in base_whitelists.keys() if path_abs == b or path_abs.startswith(b + os.sep)]
|
|
if not candidates:
|
|
return root_abs
|
|
return max(candidates, key=len)
|
|
|
|
unexpected = []
|
|
|
|
for dirpath, dirnames, filenames in os.walk(root_abs):
|
|
# skip hidden directories at traversal level
|
|
dirnames[:] = [d for d in dirnames if not d.startswith('.')]
|
|
|
|
if not is_git_repo(dirpath):
|
|
continue
|
|
|
|
current_base = find_base_for(dirpath)
|
|
rel_to_base = os.path.relpath(dirpath, current_base)
|
|
|
|
# If this is exactly the base repo directory, allow and ensure its whitelist is loaded
|
|
if rel_to_base == '.':
|
|
# already loaded for root; for nested bases, load when we encounter them as listed modules below
|
|
continue
|
|
|
|
whitelist = base_whitelists.get(current_base, set())
|
|
|
|
if rel_to_base in whitelist:
|
|
# It's a declared submodule under current_base; treat it as a new base and load its whitelist
|
|
sub_base_abs = dirpath
|
|
if sub_base_abs not in base_whitelists:
|
|
base_whitelists[sub_base_abs] = load_submodule_whitelist(sub_base_abs)
|
|
# continue walking inside (allowed)
|
|
continue
|
|
else:
|
|
# Not declared in the nearest base's .gitmodules -> unexpected
|
|
rel_to_root = os.path.relpath(dirpath, root_abs)
|
|
unexpected.append(rel_to_root)
|
|
# no need to descend into this unexpected repo
|
|
dirnames[:] = []
|
|
|
|
return unexpected, base_whitelists[root_abs]
|
|
|
|
def main():
|
|
# Parse command line arguments
|
|
parser = argparse.ArgumentParser(description='Clean up git repositories that are not listed in .gitmodules file.')
|
|
parser.add_argument('-y', '--yes', action='store_true', help='Automatically confirm removal without prompting')
|
|
parser.add_argument('--dry-run', action='store_true', help='Only show what would be removed, but do not actually remove anything')
|
|
parser.add_argument('--root-path', type=str, help='Project root path containing .gitmodules', default=os.getcwd())
|
|
args = parser.parse_args()
|
|
|
|
root_dir = args.root_path
|
|
# Get the project root directory (where .gitmodules is located)
|
|
gitmodules_path = os.path.join(root_dir, '.gitmodules')
|
|
|
|
# Get all submodule paths from root .gitmodules
|
|
root_submodule_paths = get_submodule_paths(gitmodules_path)
|
|
print(f"Found {len(root_submodule_paths)} submodules in .gitmodules:")
|
|
for path in sorted(root_submodule_paths):
|
|
print(f" - {path}")
|
|
|
|
# Scan with hierarchical .gitmodules
|
|
dirs_to_remove, _ = scan_unexpected_git_repos(root_dir)
|
|
|
|
if not dirs_to_remove:
|
|
print("\nNo unexpected module directories found. Nothing to clean up.")
|
|
return
|
|
|
|
print(f"\nFound {len(dirs_to_remove)} git repositories that are not in .gitmodules:")
|
|
for dir_path in dirs_to_remove:
|
|
print(f" - {dir_path}")
|
|
|
|
# Check if this is a dry run
|
|
if args.dry_run:
|
|
print("\nDry run: No repositories will be removed.")
|
|
return
|
|
|
|
# Ask for confirmation before removing (unless --yes flag is provided)
|
|
if not args.yes:
|
|
confirm = input("\nDo you want to remove these git repositories? [y/N]: ")
|
|
if confirm.lower() != 'y':
|
|
print("Operation cancelled.")
|
|
return
|
|
else:
|
|
print("\nAutomatic confirmation enabled. Proceeding with removal...")
|
|
|
|
# Remove the directories
|
|
for dir_path in dirs_to_remove:
|
|
full_path = os.path.join(root_dir, dir_path)
|
|
print(f"Removing {full_path}...")
|
|
|
|
try:
|
|
shutil.rmtree(full_path)
|
|
print(f" [OK] Successfully removed {dir_path}")
|
|
except Exception as e:
|
|
print(f" [FAILED] Failed to remove {dir_path}: {str(e)}")
|
|
|
|
print("\nCleanup completed.")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|