rmassive refactor, add submodule, add clean inconsiistence submodule function
This commit is contained in:
parent
c6ccd6e10f
commit
e0fbb13437
@ -117,6 +117,7 @@ class GitSvnSyncApp:
|
|||||||
init_missing_submodules_cb=lambda: self.submodule_logic_handler.init_missing_submodules() if self.submodule_logic_handler else None,
|
init_missing_submodules_cb=lambda: self.submodule_logic_handler.init_missing_submodules() if self.submodule_logic_handler else None,
|
||||||
update_gitea_wiki_cb=lambda: self.automation_handler.update_gitea_wiki() if self.automation_handler else None,
|
update_gitea_wiki_cb=lambda: self.automation_handler.update_gitea_wiki() if self.automation_handler else None,
|
||||||
analyze_and_clean_history_cb=lambda: self.automation_handler.analyze_and_clean_history() if self.automation_handler else None,
|
analyze_and_clean_history_cb=lambda: self.automation_handler.analyze_and_clean_history() if self.automation_handler else None,
|
||||||
|
clean_invalid_submodule_cb=lambda: self.automation_handler.clean_invalid_submodule() if self.automation_handler else None,
|
||||||
browse_folder_cb=self.browse_folder,
|
browse_folder_cb=self.browse_folder,
|
||||||
update_svn_status_cb=self.update_svn_status_indicator,
|
update_svn_status_cb=self.update_svn_status_indicator,
|
||||||
open_gitignore_editor_cb=self.open_gitignore_editor,
|
open_gitignore_editor_cb=self.open_gitignore_editor,
|
||||||
@ -203,6 +204,7 @@ class GitSvnSyncApp:
|
|||||||
auto_tab = self.main_frame.automation_tab
|
auto_tab = self.main_frame.automation_tab
|
||||||
auto_tab.update_gitea_wiki_callback = self.automation_handler.update_gitea_wiki
|
auto_tab.update_gitea_wiki_callback = self.automation_handler.update_gitea_wiki
|
||||||
auto_tab.analyze_and_clean_history_callback = self.automation_handler.analyze_and_clean_history
|
auto_tab.analyze_and_clean_history_callback = self.automation_handler.analyze_and_clean_history
|
||||||
|
auto_tab.clean_invalid_submodule_callback = self.automation_handler.clean_invalid_submodule
|
||||||
|
|
||||||
def _handle_fatal_error(self, message: str):
|
def _handle_fatal_error(self, message: str):
|
||||||
log_handler.log_critical(message, func_name="_handle_fatal_error")
|
log_handler.log_critical(message, func_name="_handle_fatal_error")
|
||||||
|
|||||||
@ -15,6 +15,7 @@ from typing import (
|
|||||||
Optional,
|
Optional,
|
||||||
Set,
|
Set,
|
||||||
)
|
)
|
||||||
|
import re
|
||||||
|
|
||||||
from gitutility.logging_setup import log_handler
|
from gitutility.logging_setup import log_handler
|
||||||
from gitutility.commands.git_commands import GitCommandError
|
from gitutility.commands.git_commands import GitCommandError
|
||||||
@ -88,6 +89,8 @@ class AsyncResultHandler:
|
|||||||
"sync_submodules": self._handle_sync_all_submodules_result,
|
"sync_submodules": self._handle_sync_all_submodules_result,
|
||||||
"remove_submodule": self._handle_remove_submodule_result,
|
"remove_submodule": self._handle_remove_submodule_result,
|
||||||
"init_submodules": self._handle_init_submodules_result,
|
"init_submodules": self._handle_init_submodules_result,
|
||||||
|
"force_clean_submodule": self._handle_force_clean_submodule_result,
|
||||||
|
"clean_submodule": self._handle_clean_submodule_result,
|
||||||
}
|
}
|
||||||
|
|
||||||
handler_method = handler_map.get(task_context)
|
handler_method = handler_map.get(task_context)
|
||||||
@ -450,12 +453,74 @@ class AsyncResultHandler:
|
|||||||
return False, False
|
return False, False
|
||||||
|
|
||||||
def _handle_sync_all_submodules_result(self, result_data: Dict[str, Any], context: Dict[str, Any]) -> Tuple[bool, bool]:
|
def _handle_sync_all_submodules_result(self, result_data: Dict[str, Any], context: Dict[str, Any]) -> Tuple[bool, bool]:
|
||||||
if result_data.get("status") == "success" and result_data.get("committed"):
|
status = result_data.get("status")
|
||||||
return True, True
|
message = result_data.get("message")
|
||||||
elif result_data.get("status") == "error":
|
exception = result_data.get("exception")
|
||||||
self.main_frame.show_error("Sync Submodules Error", f"Failed:\n{result_data.get('message')}")
|
|
||||||
|
# --- NUOVA LOGICA DI RECUPERO (CORRETTA) ---
|
||||||
|
# Controlliamo se è un GitCommandError e se il suo stderr contiene il messaggio specifico
|
||||||
|
if isinstance(exception, GitCommandError) and "untracked working tree" in (exception.stderr or "").lower():
|
||||||
|
|
||||||
|
# Applichiamo la regex a exception.stderr, non a str(exception)
|
||||||
|
stderr_text = exception.stderr or ""
|
||||||
|
submodule_path_match = re.search(r"in submodule path '(.*?)'", stderr_text)
|
||||||
|
|
||||||
|
submodule_path = submodule_path_match.group(1) if submodule_path_match else "an unknown submodule"
|
||||||
|
|
||||||
|
# Chiedi all'utente se vuole pulire
|
||||||
|
if self.main_frame.ask_yes_no(
|
||||||
|
"Sync Failed: Untracked Files",
|
||||||
|
f"The update failed because untracked files in submodule '{submodule_path}' would be overwritten.\n\n"
|
||||||
|
"Do you want to automatically clean these files (git clean -fd) and retry the sync?"
|
||||||
|
):
|
||||||
|
# L'utente ha detto sì, avvia l'operazione di pulizia
|
||||||
|
repo_path = self.app._get_and_validate_svn_path("Submodule Clean")
|
||||||
|
if repo_path:
|
||||||
|
# Se il path è 'unknown', non procediamo per sicurezza
|
||||||
|
if "unknown" in submodule_path:
|
||||||
|
self.main_frame.show_error("Error", "Could not automatically determine the submodule path from the error message. Cannot proceed with automatic clean.")
|
||||||
return False, False
|
return False, False
|
||||||
|
|
||||||
|
args = (self.app.submodule_handler, repo_path, submodule_path)
|
||||||
|
self.app._start_async_operation(
|
||||||
|
async_workers.run_clean_submodule_async,
|
||||||
|
args,
|
||||||
|
{
|
||||||
|
"context": "clean_submodule",
|
||||||
|
"status_msg": f"Cleaning submodule '{submodule_path}'...",
|
||||||
|
"on_success_retry": "sync_all"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return False, False
|
||||||
|
else:
|
||||||
|
# L'utente ha detto no, mostra l'errore originale
|
||||||
|
self.main_frame.show_error("Sync Submodules Error", f"Failed:\n{message}")
|
||||||
|
return False, False
|
||||||
|
|
||||||
|
# --- Logica esistente per altri casi ---
|
||||||
|
elif status == "success" and result_data.get("committed"):
|
||||||
|
return True, True
|
||||||
|
elif status == "error":
|
||||||
|
self.main_frame.show_error("Sync Submodules Error", f"Failed:\n{message}")
|
||||||
|
|
||||||
|
return False, False
|
||||||
|
|
||||||
|
def _handle_clean_submodule_result(self, result_data: Dict[str, Any], context: Dict[str, Any]) -> Tuple[bool, bool]:
|
||||||
|
"""Handles the result of the submodule cleaning operation."""
|
||||||
|
if result_data.get("status") == "success":
|
||||||
|
self.main_frame.show_info("Submodule Cleaned", result_data.get("message"))
|
||||||
|
|
||||||
|
# Controlla se dobbiamo ritentare un'azione
|
||||||
|
retry_action = context.get("on_success_retry")
|
||||||
|
if retry_action == "sync_all":
|
||||||
|
log_handler.log_info("Clean successful, retrying 'Sync/Update All' operation.", func_name="_handle_clean_submodule_result")
|
||||||
|
# Lancia di nuovo l'operazione di sync
|
||||||
|
self.app.submodule_logic_handler.sync_all_submodules()
|
||||||
|
else:
|
||||||
|
self.main_frame.show_error("Clean Failed", f"The submodule cleaning process failed:\n\n{result_data.get('message')}")
|
||||||
|
|
||||||
|
return False, False # Il refresh verrà triggerato dall'azione successiva (o nessuno se fallisce)
|
||||||
|
|
||||||
def _handle_remove_submodule_result(self, result_data: Dict[str, Any], context: Dict[str, Any]) -> Tuple[bool, bool]:
|
def _handle_remove_submodule_result(self, result_data: Dict[str, Any], context: Dict[str, Any]) -> Tuple[bool, bool]:
|
||||||
if result_data.get("status") == "success":
|
if result_data.get("status") == "success":
|
||||||
return True, True
|
return True, True
|
||||||
@ -470,6 +535,16 @@ class AsyncResultHandler:
|
|||||||
self.main_frame.show_error("Initialize Submodules Error", f"Failed:\n{result_data.get('message')}")
|
self.main_frame.show_error("Initialize Submodules Error", f"Failed:\n{result_data.get('message')}")
|
||||||
return False, False
|
return False, False
|
||||||
|
|
||||||
|
def _handle_force_clean_submodule_result(self, result_data: Dict[str, Any], context: Dict[str, Any]) -> Tuple[bool, bool]:
|
||||||
|
"""Handles the result of the submodule cleaning operation."""
|
||||||
|
if result_data.get("status") == "success":
|
||||||
|
self.main_frame.show_info("Operation Successful", result_data.get("message"))
|
||||||
|
# Trigger a full refresh because the index and history have changed
|
||||||
|
return True, True
|
||||||
|
else:
|
||||||
|
self.main_frame.show_error("Clean Failed", f"The submodule cleaning process failed:\n\n{result_data.get('message')}")
|
||||||
|
return False, False
|
||||||
|
|
||||||
def _handle_generic_result(self, result_data: Dict[str, Any], context: Dict[str, Any]) -> Tuple[bool, bool]:
|
def _handle_generic_result(self, result_data: Dict[str, Any], context: Dict[str, Any]) -> Tuple[bool, bool]:
|
||||||
status, message = result_data.get("status"), result_data.get("message", "Operation finished.")
|
status, message = result_data.get("status"), result_data.get("message", "Operation finished.")
|
||||||
if status == "error":
|
if status == "error":
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import logging # Usato solo per i livelli di logging (es. logging.INFO)
|
|||||||
import datetime
|
import datetime
|
||||||
from typing import List, Dict, Any, Tuple, Optional, Set
|
from typing import List, Dict, Any, Tuple, Optional, Set
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import re
|
||||||
|
|
||||||
# Importa usando il percorso assoluto dal pacchetto
|
# Importa usando il percorso assoluto dal pacchetto
|
||||||
from gitutility.logging_setup import log_handler
|
from gitutility.logging_setup import log_handler
|
||||||
@ -1101,8 +1102,18 @@ def run_sync_all_submodules_async(
|
|||||||
log_handler.log_debug(f"[Worker] Started: Sync All Submodules in '{repo_path}'", func_name=func_name)
|
log_handler.log_debug(f"[Worker] Started: Sync All Submodules in '{repo_path}'", func_name=func_name)
|
||||||
result_payload: Dict[str, Any] = {"status": "error", "message": "Submodule sync failed.", "exception": None, "committed": False}
|
result_payload: Dict[str, Any] = {"status": "error", "message": "Submodule sync failed.", "exception": None, "committed": False}
|
||||||
try:
|
try:
|
||||||
|
# Questa chiamata ora solleva un'eccezione in caso di errore
|
||||||
success, message, commit_made = submodule_handler.execute_sync_all_submodules(repo_path)
|
success, message, commit_made = submodule_handler.execute_sync_all_submodules(repo_path)
|
||||||
result_payload.update(status="success" if success else "error", message=message, committed=commit_made)
|
# Se arriviamo qui, ha avuto successo
|
||||||
|
result_payload.update(status="success", message=message, committed=commit_made)
|
||||||
|
except (GitCommandError, ValueError) as e:
|
||||||
|
# Ora questo blocco verrà eseguito correttamente!
|
||||||
|
log_handler.log_error(f"[Worker] Caught expected exception during submodule sync: {e}", func_name=func_name)
|
||||||
|
result_payload.update(
|
||||||
|
status="error",
|
||||||
|
message=f"Failed to sync submodules: {e}",
|
||||||
|
exception=e
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log_handler.log_exception(f"[Worker] EXCEPTION syncing submodules: {e}", func_name=func_name)
|
log_handler.log_exception(f"[Worker] EXCEPTION syncing submodules: {e}", func_name=func_name)
|
||||||
result_payload.update(exception=e, message=f"Error syncing submodules: {e}")
|
result_payload.update(exception=e, message=f"Error syncing submodules: {e}")
|
||||||
@ -1148,3 +1159,43 @@ def run_init_submodules_async(
|
|||||||
finally:
|
finally:
|
||||||
results_queue.put(result_payload)
|
results_queue.put(result_payload)
|
||||||
log_handler.log_debug(f"[Worker] Finished: Initialize Missing Submodules", func_name=func_name)
|
log_handler.log_debug(f"[Worker] Finished: Initialize Missing Submodules", func_name=func_name)
|
||||||
|
|
||||||
|
def run_force_clean_submodule_async(
|
||||||
|
submodule_handler: SubmoduleHandler,
|
||||||
|
repo_path: str,
|
||||||
|
submodule_path: str,
|
||||||
|
results_queue: queue.Queue[Dict[str, Any]],
|
||||||
|
) -> None:
|
||||||
|
"""Worker to perform a deep clean of a submodule's configuration."""
|
||||||
|
func_name = "run_force_clean_submodule_async"
|
||||||
|
log_handler.log_debug(f"[Worker] Started: Force Clean Submodule '{submodule_path}'", func_name=func_name)
|
||||||
|
result_payload: Dict[str, Any] = {"status": "error", "message": "Submodule clean failed.", "exception": None, "committed": False}
|
||||||
|
try:
|
||||||
|
success, message = submodule_handler.execute_force_clean_submodule(repo_path, submodule_path)
|
||||||
|
result_payload.update(status="success" if success else "error", message=message, committed=success)
|
||||||
|
except Exception as e:
|
||||||
|
log_handler.log_exception(f"[Worker] EXCEPTION during force clean submodule: {e}", func_name=func_name)
|
||||||
|
result_payload.update(exception=e, message=f"Error during submodule clean: {e}")
|
||||||
|
finally:
|
||||||
|
results_queue.put(result_payload)
|
||||||
|
log_handler.log_debug(f"[Worker] Finished: Force Clean Submodule", func_name=func_name)
|
||||||
|
|
||||||
|
def run_clean_submodule_async(
|
||||||
|
submodule_handler: SubmoduleHandler,
|
||||||
|
repo_path: str,
|
||||||
|
submodule_path: str,
|
||||||
|
results_queue: queue.Queue[Dict[str, Any]],
|
||||||
|
) -> None:
|
||||||
|
"""Worker to clean the working tree of a single submodule."""
|
||||||
|
func_name = "run_clean_submodule_async"
|
||||||
|
log_handler.log_debug(f"[Worker] Started: Clean Submodule '{submodule_path}'", func_name=func_name)
|
||||||
|
result_payload: Dict[str, Any] = {"status": "error", "message": "Submodule clean failed."}
|
||||||
|
try:
|
||||||
|
success, message = submodule_handler.execute_clean_submodule_working_tree(repo_path, submodule_path)
|
||||||
|
result_payload.update(status="success" if success else "error", message=message)
|
||||||
|
except Exception as e:
|
||||||
|
log_handler.log_exception(f"[Worker] EXCEPTION cleaning submodule: {e}", func_name=func_name)
|
||||||
|
result_payload.update(message=f"Error cleaning submodule: {e}")
|
||||||
|
finally:
|
||||||
|
results_queue.put(result_payload)
|
||||||
|
log_handler.log_debug(f"[Worker] Finished: Clean Submodule", func_name=func_name)
|
||||||
@ -1273,6 +1273,10 @@ class GitCommands:
|
|||||||
func_name = "get_file_content_from_ref"
|
func_name = "get_file_content_from_ref"
|
||||||
git_file_path = file_path.replace(os.path.sep, "/")
|
git_file_path = file_path.replace(os.path.sep, "/")
|
||||||
git_ref = ref.strip() if ref else "HEAD"
|
git_ref = ref.strip() if ref else "HEAD"
|
||||||
|
if git_ref == ":":
|
||||||
|
# La sintassi corretta per l'index è ":<path>", non "::<path>"
|
||||||
|
ref_path_arg = f":{git_file_path}"
|
||||||
|
else:
|
||||||
ref_path_arg = f"{git_ref}:{git_file_path}"
|
ref_path_arg = f"{git_ref}:{git_file_path}"
|
||||||
log_handler.log_debug(
|
log_handler.log_debug(
|
||||||
f"Getting content for file='{git_file_path}' at ref='{git_ref}' (using '{ref_path_arg}') in '{working_directory}'",
|
f"Getting content for file='{git_file_path}' at ref='{git_ref}' (using '{ref_path_arg}') in '{working_directory}'",
|
||||||
@ -2218,8 +2222,7 @@ class GitCommands:
|
|||||||
def submodule_status(self, working_directory: str) -> List[Dict[str, str]]:
|
def submodule_status(self, working_directory: str) -> List[Dict[str, str]]:
|
||||||
"""
|
"""
|
||||||
Gets the status of all submodules recursively and parses the output.
|
Gets the status of all submodules recursively and parses the output.
|
||||||
Output format: ' ' (up-to-date), '-' (not initialized), '+' (commit mismatch), 'U' (conflict)
|
Handles multiple output formats from the 'git submodule status' command.
|
||||||
Example line: '-b82312... gitsync_tool/logging_setup (unspecified)'
|
|
||||||
"""
|
"""
|
||||||
func_name = "submodule_status"
|
func_name = "submodule_status"
|
||||||
log_handler.log_debug(f"Getting submodule status in '{working_directory}'", func_name=func_name)
|
log_handler.log_debug(f"Getting submodule status in '{working_directory}'", func_name=func_name)
|
||||||
@ -2227,8 +2230,15 @@ class GitCommands:
|
|||||||
result = self.log_and_execute(cmd, working_directory, check=True, log_output_level=logging.DEBUG)
|
result = self.log_and_execute(cmd, working_directory, check=True, log_output_level=logging.DEBUG)
|
||||||
|
|
||||||
submodules = []
|
submodules = []
|
||||||
# Regex to parse the status line
|
# This regex is now more robust and handles two main formats:
|
||||||
status_regex = re.compile(r"^\s*([ U+-])([0-9a-fA-F]+)\s(.*?)\s\((.*?)\)\s*$")
|
# 1. [U+- ]<commit> <path> (<description>) <- Status character is present
|
||||||
|
# 2. <commit> <path> (<description>) <- Status character is absent (for clean submodules)
|
||||||
|
status_regex = re.compile(
|
||||||
|
r"^\s*([U +-])?" # 1. Status character (optional)
|
||||||
|
r"([0-9a-fA-F]+)\s" # 2. Commit hash (mandatory)
|
||||||
|
r"(.*?)" # 3. Path (non-greedy)
|
||||||
|
r"(?:\s\((.*?)\))?\s*$" # 4. Description in parentheses (optional)
|
||||||
|
)
|
||||||
|
|
||||||
for line in result.stdout.strip().splitlines():
|
for line in result.stdout.strip().splitlines():
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
@ -2238,11 +2248,15 @@ class GitCommands:
|
|||||||
match = status_regex.match(line)
|
match = status_regex.match(line)
|
||||||
if match:
|
if match:
|
||||||
status_char, commit, path, description = match.groups()
|
status_char, commit, path, description = match.groups()
|
||||||
|
|
||||||
|
# If status_char is None (because it was optional), default to ' ' (clean)
|
||||||
|
final_status_char = status_char.strip() if status_char else ' '
|
||||||
|
|
||||||
submodules.append({
|
submodules.append({
|
||||||
"status_char": status_char,
|
"status_char": final_status_char,
|
||||||
"commit": commit,
|
"commit": commit.strip(),
|
||||||
"path": path.strip(),
|
"path": path.strip(),
|
||||||
"description": description.strip(),
|
"description": (description or "N/A").strip(),
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
log_handler.log_warning(f"Could not parse submodule status line: '{line}'", func_name=func_name)
|
log_handler.log_warning(f"Could not parse submodule status line: '{line}'", func_name=func_name)
|
||||||
@ -2274,3 +2288,207 @@ class GitCommands:
|
|||||||
# Use -f to force deinit even with local changes
|
# Use -f to force deinit even with local changes
|
||||||
cmd = ["git", "submodule", "deinit", "-f", "--", path]
|
cmd = ["git", "submodule", "deinit", "-f", "--", path]
|
||||||
self.log_and_execute(cmd, working_directory, check=True)
|
self.log_and_execute(cmd, working_directory, check=True)
|
||||||
|
|
||||||
|
def remove_from_index(self, working_directory: str, path: str) -> None:
|
||||||
|
"""
|
||||||
|
Forcibly removes a path from the Git index (staging area) only.
|
||||||
|
This does not delete the file from the working directory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
working_directory (str): Path to the repository.
|
||||||
|
path (str): The relative path to the item to remove from the index.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
GitCommandError: If the 'git rm --cached' command fails.
|
||||||
|
"""
|
||||||
|
func_name = "remove_from_index"
|
||||||
|
log_handler.log_info(
|
||||||
|
f"Forcibly removing path from index (cached): '{path}' in '{working_directory}'",
|
||||||
|
func_name=func_name
|
||||||
|
)
|
||||||
|
# Use -f to force removal even if there are local modifications
|
||||||
|
cmd = ["git", "rm", "--cached", "-f", path]
|
||||||
|
try:
|
||||||
|
self.log_and_execute(cmd, working_directory, check=True)
|
||||||
|
log_handler.log_info(f"Path '{path}' removed from index successfully.", func_name=func_name)
|
||||||
|
except GitCommandError as e:
|
||||||
|
log_handler.log_error(f"Failed to 'git rm --cached {path}': {e}", func_name=func_name)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def remove_config_section(self, working_directory: str, section_name: str) -> None:
|
||||||
|
"""
|
||||||
|
Removes a section from the local .git/config file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
working_directory (str): Path to the repository.
|
||||||
|
section_name (str): The full name of the section to remove (e.g., 'submodule.libs/path').
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
GitCommandError: If the command fails with an unexpected error.
|
||||||
|
"""
|
||||||
|
func_name = "remove_config_section"
|
||||||
|
log_handler.log_info(
|
||||||
|
f"Removing config section '{section_name}' from local .git/config",
|
||||||
|
func_name=func_name
|
||||||
|
)
|
||||||
|
cmd = ["git", "config", "-f", ".git/config", "--remove-section", section_name]
|
||||||
|
try:
|
||||||
|
result = self.log_and_execute(cmd, working_directory, check=False)
|
||||||
|
|
||||||
|
# Git returns non-zero if the section doesn't exist. This is not an error for a clean-up operation.
|
||||||
|
# We only raise an error if the command fails for a different reason.
|
||||||
|
if result.returncode != 0 and "no such section" not in (result.stderr or "").lower():
|
||||||
|
raise GitCommandError(
|
||||||
|
f"Git config command failed with an unexpected error (RC {result.returncode})",
|
||||||
|
command=cmd,
|
||||||
|
stderr=result.stderr
|
||||||
|
)
|
||||||
|
|
||||||
|
log_handler.log_info(f"Config section '{section_name}' removed (if it existed).", func_name=func_name)
|
||||||
|
except GitCommandError as e:
|
||||||
|
log_handler.log_error(f"Failed to remove config section '{section_name}': {e}", func_name=func_name)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def get_registered_submodules(self, working_directory: str) -> Dict[str, str]:
|
||||||
|
"""
|
||||||
|
Retrieves the list of registered submodules and their URLs from the .gitmodules file.
|
||||||
|
This reads the configuration, not the on-disk status.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
working_directory (str): Path to the repository.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A dictionary mapping submodule paths to their URLs.
|
||||||
|
"""
|
||||||
|
func_name = "get_registered_submodules"
|
||||||
|
log_handler.log_debug(
|
||||||
|
f"Getting registered submodules from config in '{working_directory}'",
|
||||||
|
func_name=func_name
|
||||||
|
)
|
||||||
|
submodules = {}
|
||||||
|
# The command 'git config --file .gitmodules --get-regexp path' is perfect for this.
|
||||||
|
# It reads .gitmodules and finds all entries that have a 'path' property.
|
||||||
|
cmd = ["git", "config", "--file", ".gitmodules", "--get-regexp", r"submodule\..*\.path"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = self.log_and_execute(cmd, working_directory, check=False)
|
||||||
|
|
||||||
|
# If .gitmodules doesn't exist, the command will fail, which is okay.
|
||||||
|
if result.returncode != 0 or not result.stdout:
|
||||||
|
log_handler.log_info("No .gitmodules file found or it's empty. No registered submodules.", func_name=func_name)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Output is like: submodule.libs/log-handler-lib.path libs/log-handler-lib
|
||||||
|
for line in result.stdout.strip().splitlines():
|
||||||
|
# Extract the section name (e.g., submodule.libs/log-handler-lib) and the path
|
||||||
|
key, path = line.split(maxsplit=1)
|
||||||
|
submodule_name = key.replace("submodule.", "").replace(".path", "")
|
||||||
|
|
||||||
|
# Now get the URL for this submodule
|
||||||
|
url_cmd = ["git", "config", "--file", ".gitmodules", f"submodule.{submodule_name}.url"]
|
||||||
|
url_result = self.log_and_execute(url_cmd, working_directory, check=True)
|
||||||
|
url = url_result.stdout.strip()
|
||||||
|
|
||||||
|
submodules[path] = url
|
||||||
|
|
||||||
|
log_handler.log_info(f"Found {len(submodules)} registered submodules: {list(submodules.keys())}", func_name=func_name)
|
||||||
|
return submodules
|
||||||
|
|
||||||
|
except GitCommandError as e:
|
||||||
|
# This can happen if .gitmodules is malformed.
|
||||||
|
log_handler.log_error(f"Failed to read registered submodules: {e}", func_name=func_name)
|
||||||
|
return {} # Return empty on error
|
||||||
|
|
||||||
|
def discover_submodules_in_any_state(self, working_directory: str) -> List[str]:
|
||||||
|
"""
|
||||||
|
Discovers submodule paths by looking in multiple Git locations:
|
||||||
|
the checked-out .gitmodules, the version of .gitmodules in the index,
|
||||||
|
and the .git/config file. This is useful for finding submodules in
|
||||||
|
an inconsistent state.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
working_directory (str): Path to the repository.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of unique submodule paths found.
|
||||||
|
"""
|
||||||
|
func_name = "discover_submodules_in_any_state"
|
||||||
|
log_handler.log_debug(
|
||||||
|
f"Discovering submodules in any state in '{working_directory}'",
|
||||||
|
func_name=func_name
|
||||||
|
)
|
||||||
|
|
||||||
|
found_paths = set()
|
||||||
|
|
||||||
|
# --- NUOVO STEP FONDAMENTALE: Leggere .gitmodules dall'INDEX ---
|
||||||
|
# Questo trova i submodule che sono stati "aggiunti" ma non ancora committati,
|
||||||
|
# o che sono rimasti nell'index dopo una cancellazione fallita.
|
||||||
|
try:
|
||||||
|
# 'git show :.gitmodules' legge il contenuto del file dall'index
|
||||||
|
gitmodules_content_from_index = self.get_file_content_from_ref(
|
||||||
|
working_directory,
|
||||||
|
file_path=".gitmodules",
|
||||||
|
ref=":" # Il ':' da solo significa "dall'index"
|
||||||
|
)
|
||||||
|
if gitmodules_content_from_index:
|
||||||
|
log_handler.log_info("Found .gitmodules in Git index. Parsing its content.", func_name=func_name)
|
||||||
|
# Usiamo una regex per estrarre tutti i 'path = ...' dal contenuto
|
||||||
|
for match in re.finditer(r"^\s*path\s*=\s*(.*)$", gitmodules_content_from_index, re.MULTILINE):
|
||||||
|
path = match.group(1).strip()
|
||||||
|
if path:
|
||||||
|
found_paths.add(path)
|
||||||
|
except GitCommandError as e:
|
||||||
|
log_handler.log_warning(f"Could not read .gitmodules from index (it may not be staged): {e}", func_name=func_name)
|
||||||
|
except Exception as e_index:
|
||||||
|
log_handler.log_error(f"Error parsing .gitmodules content from index: {e_index}", func_name=func_name)
|
||||||
|
|
||||||
|
# 1. Look in the on-disk .gitmodules file (the standard way)
|
||||||
|
try:
|
||||||
|
cmd_gitmodules = ["git", "config", "--file", ".gitmodules", "--get-regexp", r"submodule\..*\.path"]
|
||||||
|
result_gitmodules = self.log_and_execute(cmd_gitmodules, working_directory, check=False)
|
||||||
|
if result_gitmodules.returncode == 0 and result_gitmodules.stdout:
|
||||||
|
for line in result_gitmodules.stdout.strip().splitlines():
|
||||||
|
path = line.split(maxsplit=1)[1]
|
||||||
|
found_paths.add(path)
|
||||||
|
except GitCommandError as e:
|
||||||
|
log_handler.log_warning(f"Could not read on-disk .gitmodules: {e}", func_name=func_name)
|
||||||
|
|
||||||
|
# 2. Look in .git/config for submodule sections
|
||||||
|
try:
|
||||||
|
cmd_config = ["git", "config", "-f", ".git/config", "--get-regexp", r"submodule\..*\.path"]
|
||||||
|
result_config = self.log_and_execute(cmd_config, working_directory, check=False)
|
||||||
|
if result_config.returncode == 0 and result_config.stdout:
|
||||||
|
for line in result_config.stdout.strip().splitlines():
|
||||||
|
path = line.split(maxsplit=1)[1]
|
||||||
|
found_paths.add(path)
|
||||||
|
except GitCommandError as e:
|
||||||
|
log_handler.log_warning(f"Could not read submodule paths from .git/config: {e}", func_name=func_name)
|
||||||
|
|
||||||
|
sorted_paths = sorted(list(found_paths))
|
||||||
|
log_handler.log_info(f"Discovered {len(sorted_paths)} unique submodule paths: {sorted_paths}", func_name=func_name)
|
||||||
|
return sorted_paths
|
||||||
|
|
||||||
|
def git_clean(self, working_directory: str) -> None:
|
||||||
|
"""
|
||||||
|
Cleans the working tree by recursively removing untracked files
|
||||||
|
from the current directory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
working_directory (str): The directory to clean.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
GitCommandError: If the 'git clean' command fails.
|
||||||
|
"""
|
||||||
|
func_name = "git_clean"
|
||||||
|
log_handler.log_warning(
|
||||||
|
f"Performing 'git clean -fd' in '{working_directory}'. This will delete untracked files.",
|
||||||
|
func_name=func_name
|
||||||
|
)
|
||||||
|
# -f (force) is required. -d removes untracked directories.
|
||||||
|
cmd = ["git", "clean", "-fd"]
|
||||||
|
try:
|
||||||
|
self.log_and_execute(cmd, working_directory, check=True)
|
||||||
|
log_handler.log_info(f"Successfully cleaned working directory: '{working_directory}'", func_name=func_name)
|
||||||
|
except GitCommandError as e:
|
||||||
|
log_handler.log_error(f"Failed to clean directory '{working_directory}': {e}", func_name=func_name)
|
||||||
|
raise
|
||||||
@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import datetime
|
import datetime
|
||||||
|
import shutil
|
||||||
|
import time
|
||||||
|
import stat
|
||||||
from typing import Tuple, List, Optional
|
from typing import Tuple, List, Optional
|
||||||
|
|
||||||
# Import using absolute path from the package
|
# Import using absolute path from the package
|
||||||
@ -70,22 +73,27 @@ class SubmoduleHandler:
|
|||||||
"""
|
"""
|
||||||
Initializes, fetches latest, and updates all submodules.
|
Initializes, fetches latest, and updates all submodules.
|
||||||
If any submodule commit pointer changes, it creates a commit in the parent repo.
|
If any submodule commit pointer changes, it creates a commit in the parent repo.
|
||||||
|
This method is designed to raise GitCommandError on failure to be caught by workers.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
repo_path: The path to the parent repository.
|
repo_path: The path to the parent repository.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A tuple (success, message, commit_was_made).
|
A tuple (success, message, commit_was_made).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
GitCommandError: If the submodule update command fails.
|
||||||
"""
|
"""
|
||||||
func_name = "execute_sync_all_submodules"
|
func_name = "execute_sync_all_submodules"
|
||||||
log_handler.log_info(f"Executing sync for all submodules in '{repo_path}'", func_name=func_name)
|
log_handler.log_info(f"Executing sync for all submodules in '{repo_path}'", func_name=func_name)
|
||||||
|
|
||||||
try:
|
# Non serve più il blocco try...except qui, lasciamo che l'eccezione si propaghi
|
||||||
|
|
||||||
# Step 1: Get the state of submodules BEFORE the update
|
# Step 1: Get the state of submodules BEFORE the update
|
||||||
log_handler.log_debug("Getting pre-sync submodule status...", func_name=func_name)
|
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)}
|
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)
|
# Step 2: Perform the update. This call will RAISE GitCommandError on failure.
|
||||||
log_handler.log_info("Running 'git submodule update --init --remote --merge'...", func_name=func_name)
|
log_handler.log_info("Running 'git submodule update --init --remote --merge'...", func_name=func_name)
|
||||||
self.git_commands.submodule_update(
|
self.git_commands.submodule_update(
|
||||||
working_directory=repo_path,
|
working_directory=repo_path,
|
||||||
@ -94,20 +102,20 @@ class SubmoduleHandler:
|
|||||||
recursive=True,
|
recursive=True,
|
||||||
merge=True
|
merge=True
|
||||||
)
|
)
|
||||||
log_handler.log_info("Submodule update command finished.", func_name=func_name)
|
log_handler.log_info("Submodule update command finished successfully.", func_name=func_name)
|
||||||
|
|
||||||
# Step 3: Get the state AFTER the update
|
# Step 3: Get the state AFTER the update
|
||||||
log_handler.log_debug("Getting post-sync submodule status...", func_name=func_name)
|
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)}
|
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
|
# Step 4: Compare states
|
||||||
changed_submodules = []
|
changed_submodules = []
|
||||||
for path, new_commit in status_after.items():
|
for path, new_commit in status_after.items():
|
||||||
old_commit = status_before.get(path)
|
old_commit = status_before.get(path)
|
||||||
if old_commit != new_commit:
|
if old_commit != new_commit:
|
||||||
changed_submodules.append((path, 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
|
# Step 5: If there are changes, stage and commit
|
||||||
if not changed_submodules:
|
if not changed_submodules:
|
||||||
msg = "All submodules are already up-to-date."
|
msg = "All submodules are already up-to-date."
|
||||||
log_handler.log_info(msg, func_name=func_name)
|
log_handler.log_info(msg, func_name=func_name)
|
||||||
@ -118,13 +126,12 @@ class SubmoduleHandler:
|
|||||||
commit_body_lines = []
|
commit_body_lines = []
|
||||||
for path, old_commit, new_commit in changed_submodules:
|
for path, old_commit, new_commit in changed_submodules:
|
||||||
self.git_commands.add_file(repo_path, path)
|
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_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)
|
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)
|
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)
|
commit_made = self.git_commands.git_commit(repo_path, commit_message, stage_all_first=False)
|
||||||
|
|
||||||
if commit_made:
|
if commit_made:
|
||||||
msg = f"Successfully synced {len(changed_submodules)} submodule(s) and created commit."
|
msg = f"Successfully synced {len(changed_submodules)} submodule(s) and created commit."
|
||||||
@ -133,11 +140,8 @@ class SubmoduleHandler:
|
|||||||
else:
|
else:
|
||||||
msg = "Submodules were updated, but the parent repo commit failed unexpectedly."
|
msg = "Submodules were updated, but the parent repo commit failed unexpectedly."
|
||||||
log_handler.log_error(msg, func_name=func_name)
|
log_handler.log_error(msg, func_name=func_name)
|
||||||
return False, msg, False
|
# Questo è un caso strano, ma lo trattiamo come un successo parziale senza commit
|
||||||
|
return True, 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]:
|
def execute_init_missing_submodules(self, repo_path: str) -> Tuple[bool, str]:
|
||||||
"""
|
"""
|
||||||
@ -213,3 +217,143 @@ class SubmoduleHandler:
|
|||||||
except (GitCommandError, ValueError) as e:
|
except (GitCommandError, ValueError) as e:
|
||||||
log_handler.log_error(f"Failed to remove submodule '{submodule_path}': {e}", func_name=func_name)
|
log_handler.log_error(f"Failed to remove submodule '{submodule_path}': {e}", func_name=func_name)
|
||||||
return False, f"Failed to remove submodule: {e}"
|
return False, f"Failed to remove submodule: {e}"
|
||||||
|
|
||||||
|
def execute_force_clean_submodule(self, repo_path: str, submodule_path: str) -> Tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
Performs a deep clean of a submodule's configuration from the repository.
|
||||||
|
This is a destructive operation intended to fix an inconsistent state.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
repo_path: The path to the parent repository.
|
||||||
|
submodule_path: The local path of the submodule to clean.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A tuple (success, message).
|
||||||
|
"""
|
||||||
|
func_name = "execute_force_clean_submodule"
|
||||||
|
log_handler.log_warning(
|
||||||
|
f"--- DESTRUCTIVE ACTION: Forcing clean of submodule '{submodule_path}' ---",
|
||||||
|
func_name=func_name
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
# 1. De-initialize. We expect this might fail if the submodule was never properly registered.
|
||||||
|
log_handler.log_debug("Step 1: Attempting to de-initialize submodule...", func_name=func_name)
|
||||||
|
try:
|
||||||
|
self.git_commands.submodule_deinit(repo_path, submodule_path)
|
||||||
|
except GitCommandError as deinit_error:
|
||||||
|
if "did not match any file(s)" in (deinit_error.stderr or "").lower():
|
||||||
|
log_handler.log_warning(
|
||||||
|
f"Submodule deinit failed as expected because '{submodule_path}' is not a known submodule. Continuing cleanup.",
|
||||||
|
func_name=func_name
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise deinit_error
|
||||||
|
|
||||||
|
# 2. Forcibly remove submodule path AND .gitmodules from the index.
|
||||||
|
# This is the core of the cleanup for an inconsistent state.
|
||||||
|
# We attempt both and ignore "did not match" errors.
|
||||||
|
paths_to_remove_from_index = [submodule_path, ".gitmodules"]
|
||||||
|
log_handler.log_debug(f"Step 2: Forcibly removing paths from index: {paths_to_remove_from_index}", func_name=func_name)
|
||||||
|
for path in paths_to_remove_from_index:
|
||||||
|
try:
|
||||||
|
self.git_commands.remove_from_index(repo_path, path)
|
||||||
|
except GitCommandError as rm_error:
|
||||||
|
if "did not match any files" in (rm_error.stderr or "").lower():
|
||||||
|
log_handler.log_warning(
|
||||||
|
f"Path '{path}' was not in the index. Continuing cleanup.",
|
||||||
|
func_name=func_name
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise rm_error # Re-raise unexpected errors
|
||||||
|
|
||||||
|
# 3. Forcibly remove the section from .git/config
|
||||||
|
config_section_name = f"submodule.{submodule_path}"
|
||||||
|
log_handler.log_debug(f"Step 3: Removing section '{config_section_name}' from .git/config...", func_name=func_name)
|
||||||
|
self.git_commands.remove_config_section(repo_path, config_section_name)
|
||||||
|
|
||||||
|
# 4. Remove the submodule's directory from .git/modules
|
||||||
|
git_modules_path = os.path.join(repo_path, ".git", "modules", submodule_path)
|
||||||
|
log_handler.log_debug(f"Step 4: Removing internal git directory '{git_modules_path}'...", func_name=func_name)
|
||||||
|
|
||||||
|
def _remove_readonly(func, path, exc_info):
|
||||||
|
"""
|
||||||
|
Error handler for shutil.rmtree. It's called on error.
|
||||||
|
It attempts to remove the readonly bit and re-execute the failing function.
|
||||||
|
"""
|
||||||
|
# exc_info contains type, value, traceback
|
||||||
|
exc_type, exc_value, _ = exc_info
|
||||||
|
# Check if it's a permission error
|
||||||
|
if issubclass(exc_type, PermissionError) or (hasattr(exc_value, 'winerror') and exc_value.winerror == 5):
|
||||||
|
log_handler.log_warning(f"Permission error on '{path}'. Attempting to change permissions and retry...", func_name=func_name)
|
||||||
|
try:
|
||||||
|
# Change permissions to writable and try again
|
||||||
|
os.chmod(path, stat.S_IWRITE)
|
||||||
|
func(path) # Retry the original function (e.g., os.remove)
|
||||||
|
except Exception as e:
|
||||||
|
log_handler.log_error(f"Failed to remove '{path}' even after changing permissions: {e}", func_name=func_name)
|
||||||
|
# We re-raise the original exception to let rmtree know it still failed
|
||||||
|
raise exc_value
|
||||||
|
else:
|
||||||
|
# If it's not a permission error, re-raise it
|
||||||
|
raise exc_value
|
||||||
|
|
||||||
|
if os.path.isdir(git_modules_path):
|
||||||
|
try:
|
||||||
|
# Call rmtree with our custom error handler
|
||||||
|
shutil.rmtree(git_modules_path, onerror=_remove_readonly)
|
||||||
|
log_handler.log_info(f"Removed directory: {git_modules_path}", func_name=func_name)
|
||||||
|
except Exception as e:
|
||||||
|
# If rmtree still fails after all retries from the handler, log it.
|
||||||
|
log_handler.log_error(f"Failed to remove directory '{git_modules_path}' with robust handler: {e}", func_name=func_name)
|
||||||
|
# Re-raise the exception to make the whole operation fail
|
||||||
|
raise e
|
||||||
|
else:
|
||||||
|
log_handler.log_info(f"Internal directory not found, skipping: {git_modules_path}", func_name=func_name)
|
||||||
|
|
||||||
|
# 5. Commit the changes (the removal from the index)
|
||||||
|
commit_message = f"Chore: Force clean inconsistent submodule state for '{submodule_path}'"
|
||||||
|
log_handler.log_info(f"Step 5: Committing cleanup with message: '{commit_message}'", func_name=func_name)
|
||||||
|
# Le operazioni 'git rm --cached' hanno già preparato l'index.
|
||||||
|
# Eseguiamo un commit diretto senza fare un altro 'git add'.
|
||||||
|
commit_made = self.git_commands.git_commit(repo_path, commit_message, stage_all_first=False)
|
||||||
|
|
||||||
|
if not commit_made:
|
||||||
|
log_handler.log_warning("Cleanup actions performed, but no changes were committed. The state might have already been clean.", func_name=func_name)
|
||||||
|
|
||||||
|
success_message = f"Successfully cleaned submodule state for '{submodule_path}'. The repository should now be in a consistent state."
|
||||||
|
log_handler.log_info(success_message, func_name=func_name)
|
||||||
|
return True, success_message
|
||||||
|
|
||||||
|
except (GitCommandError, ValueError, IOError) as e:
|
||||||
|
error_msg = f"Failed to clean submodule state for '{submodule_path}': {e}"
|
||||||
|
log_handler.log_error(error_msg, func_name=func_name)
|
||||||
|
return False, error_msg
|
||||||
|
|
||||||
|
def execute_clean_submodule_working_tree(self, repo_path: str, submodule_path: str) -> Tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
Executes 'git clean -fd' inside a specific submodule's directory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
repo_path: The path to the parent repository.
|
||||||
|
submodule_path: The local path of the submodule to clean.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A tuple (success, message).
|
||||||
|
"""
|
||||||
|
func_name = "execute_clean_submodule_working_tree"
|
||||||
|
full_submodule_path = os.path.join(repo_path, submodule_path)
|
||||||
|
|
||||||
|
if not os.path.isdir(full_submodule_path):
|
||||||
|
msg = f"Cannot clean: Submodule directory not found at '{full_submodule_path}'"
|
||||||
|
log_handler.log_error(msg, func_name=func_name)
|
||||||
|
return False, msg
|
||||||
|
|
||||||
|
log_handler.log_info(f"Executing clean for submodule at '{full_submodule_path}'", func_name=func_name)
|
||||||
|
try:
|
||||||
|
self.git_commands.git_clean(full_submodule_path)
|
||||||
|
success_msg = f"Submodule '{submodule_path}' cleaned successfully."
|
||||||
|
return True, success_msg
|
||||||
|
except GitCommandError as e:
|
||||||
|
error_msg = f"Failed to clean submodule '{submodule_path}': {e}"
|
||||||
|
log_handler.log_error(error_msg, func_name=func_name)
|
||||||
|
return False, error_msg
|
||||||
@ -4,7 +4,7 @@ import tkinter as tk
|
|||||||
from tkinter import ttk, messagebox, simpledialog, filedialog
|
from tkinter import ttk, messagebox, simpledialog, filedialog
|
||||||
import os
|
import os
|
||||||
import re # Per validazione nomi branch/tag
|
import re # Per validazione nomi branch/tag
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple, List
|
||||||
|
|
||||||
# --- Tooltip (non necessario qui, i dialoghi sono semplici) ---
|
# --- Tooltip (non necessario qui, i dialoghi sono semplici) ---
|
||||||
|
|
||||||
@ -324,4 +324,117 @@ class CloneFromRemoteDialog(simpledialog.Dialog):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SelectSubmoduleDialog(simpledialog.Dialog):
|
||||||
|
"""Dialog to select a submodule from a list for cleaning."""
|
||||||
|
|
||||||
|
def __init__(self, parent, title: str = "Select Submodule to Clean", submodule_paths: List[str] = None):
|
||||||
|
self.submodule_paths = submodule_paths if submodule_paths else []
|
||||||
|
self.result: Optional[str] = None
|
||||||
|
self.selected_path_var = tk.StringVar()
|
||||||
|
super().__init__(parent, title=title)
|
||||||
|
|
||||||
|
def body(self, master: tk.Frame) -> Optional[tk.Widget]:
|
||||||
|
frame = ttk.Frame(master, padding="10")
|
||||||
|
frame.pack(fill="both", expand=True)
|
||||||
|
|
||||||
|
ttk.Label(
|
||||||
|
frame,
|
||||||
|
text="Select the submodule with an inconsistent state to clean from the repository:"
|
||||||
|
).pack(pady=(0, 10))
|
||||||
|
|
||||||
|
if not self.submodule_paths:
|
||||||
|
ttk.Label(frame, text="No registered submodules found in .gitmodules.", foreground="red").pack()
|
||||||
|
return None # No items to select
|
||||||
|
|
||||||
|
self.combobox = ttk.Combobox(
|
||||||
|
frame,
|
||||||
|
textvariable=self.selected_path_var,
|
||||||
|
values=self.submodule_paths,
|
||||||
|
state="readonly",
|
||||||
|
width=50
|
||||||
|
)
|
||||||
|
self.combobox.pack(pady=5)
|
||||||
|
if self.submodule_paths:
|
||||||
|
self.combobox.current(0) # Pre-select the first item
|
||||||
|
|
||||||
|
return self.combobox
|
||||||
|
|
||||||
|
def validate(self) -> bool:
|
||||||
|
if not self.selected_path_var.get():
|
||||||
|
messagebox.showwarning("Selection Required", "You must select a submodule path from the list.", parent=self)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def apply(self) -> None:
|
||||||
|
self.result = self.selected_path_var.get()
|
||||||
|
|
||||||
|
class AddSubmoduleDialog(simpledialog.Dialog):
|
||||||
|
"""Dialog to get a new submodule's URL and local path."""
|
||||||
|
|
||||||
|
def __init__(self, parent, title: str = "Add New Submodule"):
|
||||||
|
self.url_var = tk.StringVar()
|
||||||
|
self.path_var = tk.StringVar()
|
||||||
|
self.result: Optional[Tuple[str, str]] = None
|
||||||
|
super().__init__(parent, title=title)
|
||||||
|
|
||||||
|
def body(self, master: tk.Frame) -> Optional[tk.Widget]:
|
||||||
|
"""Creates the dialog body."""
|
||||||
|
frame = ttk.Frame(master, padding="10")
|
||||||
|
frame.pack(fill="x", expand=True)
|
||||||
|
frame.columnconfigure(1, weight=1)
|
||||||
|
|
||||||
|
# Repository URL Row
|
||||||
|
ttk.Label(frame, text="Repository URL:").grid(
|
||||||
|
row=0, column=0, padx=5, pady=5, sticky="w"
|
||||||
|
)
|
||||||
|
self.url_entry = ttk.Entry(frame, textvariable=self.url_var, width=60) #<-- Larghezza aumentata
|
||||||
|
self.url_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
|
||||||
|
|
||||||
|
# Local Path Row
|
||||||
|
ttk.Label(frame, text="Local Path:").grid(
|
||||||
|
row=1, column=0, padx=5, pady=5, sticky="w"
|
||||||
|
)
|
||||||
|
self.path_entry = ttk.Entry(frame, textvariable=self.path_var, width=60) #<-- Larghezza aumentata
|
||||||
|
self.path_entry.grid(row=1, column=1, padx=5, pady=5, sticky="ew")
|
||||||
|
|
||||||
|
ttk.Label(
|
||||||
|
frame,
|
||||||
|
text="(e.g., libs/my-submodule)",
|
||||||
|
font=("Segoe UI", 8),
|
||||||
|
foreground="grey"
|
||||||
|
).grid(row=2, column=1, padx=5, sticky="w")
|
||||||
|
|
||||||
|
|
||||||
|
return self.url_entry # Initial focus
|
||||||
|
|
||||||
|
def validate(self) -> bool:
|
||||||
|
"""Validates the input."""
|
||||||
|
url = self.url_var.get().strip()
|
||||||
|
path = self.path_var.get().strip()
|
||||||
|
|
||||||
|
if not url:
|
||||||
|
messagebox.showwarning("Input Error", "Repository URL cannot be empty.", parent=self)
|
||||||
|
self.url_entry.focus_set()
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not path:
|
||||||
|
messagebox.showwarning("Input Error", "Local Path cannot be empty.", parent=self)
|
||||||
|
self.path_entry.focus_set()
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Basic check for invalid path characters
|
||||||
|
if any(char in path for char in ['<', '>', ':', '"', '|', '?', '*']):
|
||||||
|
messagebox.showwarning("Input Error", f"Local path '{path}' contains invalid characters.", parent=self)
|
||||||
|
self.path_entry.focus_set()
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def apply(self) -> None:
|
||||||
|
"""Stores the validated result."""
|
||||||
|
self.result = (
|
||||||
|
self.url_var.get().strip(),
|
||||||
|
self.path_var.get().strip().replace("\\", "/") # Normalize path separators
|
||||||
|
)
|
||||||
|
|
||||||
# --- END OF FILE gitsync_tool/gui/dialogs.py ---
|
# --- END OF FILE gitsync_tool/gui/dialogs.py ---
|
||||||
|
|||||||
@ -26,6 +26,7 @@ class AutomationTab(ttk.Frame):
|
|||||||
# Store callbacks
|
# Store callbacks
|
||||||
self.update_gitea_wiki_callback = kwargs.get('update_gitea_wiki_cb')
|
self.update_gitea_wiki_callback = kwargs.get('update_gitea_wiki_cb')
|
||||||
self.analyze_and_clean_history_callback = kwargs.get('analyze_and_clean_history_cb')
|
self.analyze_and_clean_history_callback = kwargs.get('analyze_and_clean_history_cb')
|
||||||
|
self.clean_invalid_submodule_callback = kwargs.get('clean_invalid_submodule_cb')
|
||||||
|
|
||||||
# Get a reference to the main frame to access shared variables (like remote_url_var)
|
# Get a reference to the main frame to access shared variables (like remote_url_var)
|
||||||
self.main_frame = self.master.master
|
self.main_frame = self.master.master
|
||||||
@ -90,6 +91,31 @@ class AutomationTab(ttk.Frame):
|
|||||||
"This action can rewrite the entire repository history."
|
"This action can rewrite the entire repository history."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
clean_frame = ttk.LabelFrame(
|
||||||
|
self, text="Submodule Maintenance", padding=(10, 5)
|
||||||
|
)
|
||||||
|
clean_frame.grid(row=2, column=0, sticky="ew")
|
||||||
|
ttk.Label(
|
||||||
|
clean_frame,
|
||||||
|
text="DESTRUCTIVE: Forcibly removes all traces of a submodule from the Git index and config.\n",
|
||||||
|
wraplength=450,
|
||||||
|
justify=tk.LEFT
|
||||||
|
).pack(pady=(0, 10), fill="x", expand=True)
|
||||||
|
|
||||||
|
self.clean_submodule_button = ttk.Button(
|
||||||
|
clean_frame,
|
||||||
|
text="Clean Invalid Submodule...",
|
||||||
|
command=self.clean_invalid_submodule_callback, # Aggiungeremo questo callback
|
||||||
|
state=tk.DISABLED
|
||||||
|
)
|
||||||
|
self.clean_submodule_button.pack(pady=5)
|
||||||
|
Tooltip(
|
||||||
|
self.clean_submodule_button,
|
||||||
|
"DESTRUCTIVE: Forcibly removes all traces of a submodule from the Git index and config.\n"
|
||||||
|
"Use this to fix a repository stuck in an inconsistent state after a failed submodule operation."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def set_action_widgets_state(self, state: str) -> None:
|
def set_action_widgets_state(self, state: str) -> None:
|
||||||
"""
|
"""
|
||||||
Sets the state of all action widgets in this tab.
|
Sets the state of all action widgets in this tab.
|
||||||
@ -109,6 +135,7 @@ class AutomationTab(ttk.Frame):
|
|||||||
widgets = [
|
widgets = [
|
||||||
self.update_wiki_button,
|
self.update_wiki_button,
|
||||||
self.analyze_history_button,
|
self.analyze_history_button,
|
||||||
|
self.clean_submodule_button,
|
||||||
]
|
]
|
||||||
|
|
||||||
for widget in widgets:
|
for widget in widgets:
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
# --- FILE: gitsync_tool/logic/automation_handler.py ---
|
# --- FILE: gitsync_tool/logic/automation_handler.py ---
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
from tkinter import simpledialog
|
||||||
|
|
||||||
from gitutility.async_tasks import async_workers
|
from gitutility.async_tasks import async_workers
|
||||||
from gitutility.logging_setup import log_handler
|
from gitutility.logging_setup import log_handler
|
||||||
from gitutility.config.config_manager import DEFAULT_REMOTE_NAME
|
from gitutility.config.config_manager import DEFAULT_REMOTE_NAME
|
||||||
|
from gitutility.gui.dialogs import SelectSubmoduleDialog
|
||||||
|
|
||||||
# Forward reference for type hinting
|
# Forward reference for type hinting
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@ -78,3 +80,50 @@ class AutomationHandler:
|
|||||||
"repo_path": svn_path,
|
"repo_path": svn_path,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def clean_invalid_submodule(self):
|
||||||
|
"""Handles the workflow for cleaning an invalid submodule state."""
|
||||||
|
func_name = "clean_invalid_submodule"
|
||||||
|
log_handler.log_info(f"--- Action Triggered: Clean Invalid Submodule ---", func_name=func_name)
|
||||||
|
|
||||||
|
svn_path = self.app._get_and_validate_svn_path("Clean Submodule State")
|
||||||
|
if not svn_path or not self.app._is_repo_ready(svn_path):
|
||||||
|
self.main_frame.show_error("Action Failed", "Repository path is invalid or not prepared.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# --- NUOVA LOGICA: Usa il metodo di scoperta potenziato ---
|
||||||
|
discovered_paths = self.app.git_commands.discover_submodules_in_any_state(svn_path)
|
||||||
|
if not discovered_paths:
|
||||||
|
self.main_frame.show_info("No Submodules Found", "No submodule configurations were found in .gitmodules, .git/config, or the Git index.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Mostra la finestra di dialogo con la lista trovata
|
||||||
|
dialog = SelectSubmoduleDialog(self.app.master, submodule_paths=discovered_paths)
|
||||||
|
submodule_path = dialog.result
|
||||||
|
# --------------------------------------------------
|
||||||
|
|
||||||
|
if not submodule_path:
|
||||||
|
self.main_frame.update_status_bar("Clean submodule cancelled.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.main_frame.ask_yes_no(
|
||||||
|
"Confirm Destructive Clean",
|
||||||
|
f"This will forcibly remove all traces of '{submodule_path}' from the Git index and config, then create a commit.\n\n"
|
||||||
|
"This is intended to fix a broken state. Proceed?"
|
||||||
|
):
|
||||||
|
self.main_frame.update_status_bar("Clean submodule cancelled.")
|
||||||
|
return
|
||||||
|
|
||||||
|
args = (self.app.submodule_handler, svn_path, submodule_path)
|
||||||
|
self.app._start_async_operation(
|
||||||
|
worker_func=async_workers.run_force_clean_submodule_async,
|
||||||
|
args_tuple=args,
|
||||||
|
context_dict={
|
||||||
|
"context": "force_clean_submodule",
|
||||||
|
"status_msg": f"Cleaning submodule '{submodule_path}'..."
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log_handler.log_exception(f"Error during submodule clean setup: {e}", func_name=func_name)
|
||||||
|
self.main_frame.show_error("Error", f"Could not prepare submodule clean operation:\n{e}")
|
||||||
@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Optional, Tuple
|
|||||||
|
|
||||||
from gitutility.async_tasks import async_workers
|
from gitutility.async_tasks import async_workers
|
||||||
from gitutility.logging_setup import log_handler
|
from gitutility.logging_setup import log_handler
|
||||||
|
from gitutility.gui.dialogs import AddSubmoduleDialog
|
||||||
|
|
||||||
# Forward reference for type hinting to avoid circular import
|
# Forward reference for type hinting to avoid circular import
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@ -51,8 +52,11 @@ class SubmoduleLogicHandler:
|
|||||||
self.main_frame.show_error("Action Failed", "Repository is not ready.")
|
self.main_frame.show_error("Action Failed", "Repository is not ready.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Use the dialog method on MainFrame to get user input
|
# --- MODIFICA: Usa il nuovo dialogo personalizzato ---
|
||||||
details: Optional[Tuple[str, str]] = self.main_frame.ask_new_submodule_details()
|
dialog = AddSubmoduleDialog(self.app.master)
|
||||||
|
details = dialog.result
|
||||||
|
# ---------------------------------------------------
|
||||||
|
|
||||||
if not details:
|
if not details:
|
||||||
self.main_frame.update_status_bar("Add submodule cancelled.")
|
self.main_frame.update_status_bar("Add submodule cancelled.")
|
||||||
return
|
return
|
||||||
@ -60,7 +64,7 @@ class SubmoduleLogicHandler:
|
|||||||
submodule_url, submodule_path = details
|
submodule_url, submodule_path = details
|
||||||
log_handler.log_info(f"Attempting to add submodule '{submodule_url}' at path '{submodule_path}'", func_name=func_name)
|
log_handler.log_info(f"Attempting to add submodule '{submodule_url}' at path '{submodule_path}'", func_name=func_name)
|
||||||
|
|
||||||
args = (self.submodule_handler, svn_path, submodule_url, submodule_path)
|
args = (self.app.submodule_handler, svn_path, submodule_url, submodule_path)
|
||||||
self.app._start_async_operation(
|
self.app._start_async_operation(
|
||||||
worker_func=async_workers.run_add_submodule_async,
|
worker_func=async_workers.run_add_submodule_async,
|
||||||
args_tuple=args,
|
args_tuple=args,
|
||||||
@ -126,3 +130,5 @@ class SubmoduleLogicHandler:
|
|||||||
args_tuple=args,
|
args_tuple=args,
|
||||||
context_dict={"context": "init_submodules", "status_msg": "Initializing missing submodules..."},
|
context_dict={"context": "init_submodules", "status_msg": "Initializing missing submodules..."},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user