SXXXXXXX_GitUtility/gitutility/core/submodule_handler.py
2025-07-29 08:41:44 +02:00

215 lines
9.9 KiB
Python

# --- FILE: gitsync_tool/core/submodule_handler.py ---
import os
import datetime
from typing import Tuple, List, Optional
# Import using absolute path from the package
from gitutility.logging_setup import log_handler
from ..commands.git_commands import GitCommands, GitCommandError
class SubmoduleHandler:
"""
Handles high-level logic for Git submodule operations, orchestrating
multiple GitCommands to perform complex workflows like syncing or removing submodules.
"""
def __init__(self, git_commands: GitCommands):
"""
Initializes the SubmoduleHandler.
Args:
git_commands: An instance for executing low-level Git commands.
"""
if not isinstance(git_commands, GitCommands):
raise TypeError("SubmoduleHandler requires a GitCommands instance.")
self.git_commands: GitCommands = git_commands
log_handler.log_debug("SubmoduleHandler initialized.", func_name="__init__")
def execute_add_submodule(
self, repo_path: str, submodule_url: str, submodule_path: str
) -> Tuple[bool, str]:
"""
Adds a new submodule and commits the change in the parent repository.
Args:
repo_path: The path to the parent repository.
submodule_url: The URL of the submodule repository to add.
submodule_path: The local path within the parent repo for the submodule.
Returns:
A tuple (success, message).
"""
func_name = "execute_add_submodule"
log_handler.log_info(
f"Executing add submodule: URL='{submodule_url}', Path='{submodule_path}'",
func_name=func_name
)
try:
# Step 1: Add the submodule. This stages .gitmodules and the new path.
self.git_commands.submodule_add(repo_path, submodule_url, submodule_path)
# Step 2: Commit the addition of the submodule.
commit_message = f"Feat: Add submodule '{submodule_path}'"
log_handler.log_info(f"Committing submodule addition with message: '{commit_message}'", func_name=func_name)
commit_made = self.git_commands.git_commit(repo_path, commit_message)
if not commit_made:
# This should not happen if submodule add succeeded, but as a safeguard:
raise GitCommandError("Submodule was added but failed to create a commit for it.")
success_message = f"Submodule '{submodule_path}' added and committed successfully."
log_handler.log_info(success_message, func_name=func_name)
return True, success_message
except (GitCommandError, ValueError) as e:
log_handler.log_error(f"Failed to add submodule '{submodule_path}': {e}", func_name=func_name)
return False, f"Failed to add submodule: {e}"
def execute_sync_all_submodules(self, repo_path: str) -> Tuple[bool, str, bool]:
"""
Initializes, fetches latest, and updates all submodules.
If any submodule commit pointer changes, it creates a commit in the parent repo.
Args:
repo_path: The path to the parent repository.
Returns:
A tuple (success, message, commit_was_made).
"""
func_name = "execute_sync_all_submodules"
log_handler.log_info(f"Executing sync for all submodules in '{repo_path}'", func_name=func_name)
try:
# Step 1: Get the state of submodules BEFORE the update
log_handler.log_debug("Getting pre-sync submodule status...", func_name=func_name)
status_before = {s["path"]: s["commit"] for s in self.git_commands.submodule_status(repo_path)}
# Step 2: Perform the update (initialize, fetch remote, merge)
log_handler.log_info("Running 'git submodule update --init --remote --merge'...", func_name=func_name)
self.git_commands.submodule_update(
working_directory=repo_path,
init=True,
remote=True,
recursive=True,
merge=True
)
log_handler.log_info("Submodule update command finished.", func_name=func_name)
# Step 3: Get the state AFTER the update
log_handler.log_debug("Getting post-sync submodule status...", func_name=func_name)
status_after = {s["path"]: s["commit"] for s in self.git_commands.submodule_status(repo_path)}
# Step 4: Compare states to see if anything changed
changed_submodules = []
for path, new_commit in status_after.items():
old_commit = status_before.get(path)
if old_commit != new_commit:
changed_submodules.append((path, old_commit, new_commit))
# Step 5: If there are changes, stage them and commit in the parent repo
if not changed_submodules:
msg = "All submodules are already up-to-date."
log_handler.log_info(msg, func_name=func_name)
return True, msg, False
log_handler.log_info(f"Found {len(changed_submodules)} updated submodules. Staging changes...", func_name=func_name)
commit_body_lines = []
for path, old_commit, new_commit in changed_submodules:
self.git_commands.add_file(repo_path, path)
log_handler.log_debug(f"Staged submodule path: '{path}'", func_name=func_name)
commit_body_lines.append(f"- Update {path} from {old_commit[:7]} to {new_commit[:7]}")
commit_message = "Chore: Sync submodules\n\n" + "\n".join(commit_body_lines)
log_handler.log_info(f"Committing submodule updates with message:\n{commit_message}", func_name=func_name)
commit_made = self.git_commands.git_commit(repo_path, commit_message)
if commit_made:
msg = f"Successfully synced {len(changed_submodules)} submodule(s) and created commit."
log_handler.log_info(msg, func_name=func_name)
return True, msg, True
else:
msg = "Submodules were updated, but the parent repo commit failed unexpectedly."
log_handler.log_error(msg, func_name=func_name)
return False, msg, False
except (GitCommandError, ValueError) as e:
log_handler.log_error(f"Failed to sync submodules: {e}", func_name=func_name)
return False, f"Failed to sync submodules: {e}", False
def execute_init_missing_submodules(self, repo_path: str) -> Tuple[bool, str]:
"""
Initializes and clones any submodules that are registered but not yet cloned.
Args:
repo_path: The path to the parent repository.
Returns:
A tuple (success, message).
"""
func_name = "execute_init_missing_submodules"
log_handler.log_info(f"Initializing missing submodules in '{repo_path}'", func_name=func_name)
try:
self.git_commands.submodule_update(
working_directory=repo_path,
init=True,
remote=False, # No need to fetch remote here, just init
recursive=True,
merge=False
)
msg = "Missing submodules initialized successfully."
log_handler.log_info(msg, func_name=func_name)
return True, msg
except (GitCommandError, ValueError) as e:
log_handler.log_error(f"Failed to initialize submodules: {e}", func_name=func_name)
return False, f"Failed to initialize submodules: {e}"
def execute_remove_submodule(self, repo_path: str, submodule_path: str) -> Tuple[bool, str]:
"""
Completely removes a submodule from the repository. This is a multi-step process.
Args:
repo_path: The path to the parent repository.
submodule_path: The local path of the submodule to remove.
Returns:
A tuple (success, message).
"""
func_name = "execute_remove_submodule"
log_handler.log_warning(
f"Executing REMOVAL of submodule '{submodule_path}' from '{repo_path}'",
func_name=func_name
)
try:
# Step 1: De-initialize the submodule. This removes the entry from .git/config.
log_handler.log_debug(f"De-initializing submodule '{submodule_path}'...", func_name=func_name)
self.git_commands.submodule_deinit(repo_path, submodule_path)
# Step 2: Remove the submodule from the index and .gitmodules file.
log_handler.log_debug(f"Running 'git rm' for submodule '{submodule_path}'...", func_name=func_name)
self.git_commands.git_rm(repo_path, submodule_path)
# The submodule's directory is now removed.
# The changes to .gitmodules and the removed path are now staged.
# Step 3: Commit the removal.
commit_message = f"Refactor: Remove submodule '{submodule_path}'"
log_handler.log_info(f"Committing submodule removal with message: '{commit_message}'", func_name=func_name)
commit_made = self.git_commands.git_commit(repo_path, commit_message)
if not commit_made:
raise GitCommandError("Submodule was removed from index but the final commit failed.")
# Note: The empty directory .git/modules/<submodule_name> may remain,
# which is standard Git behavior and generally harmless.
success_message = f"Submodule '{submodule_path}' removed successfully."
log_handler.log_info(success_message, func_name=func_name)
return True, success_message
except (GitCommandError, ValueError) as e:
log_handler.log_error(f"Failed to remove submodule '{submodule_path}': {e}", func_name=func_name)
return False, f"Failed to remove submodule: {e}"