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,
|
||||
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,
|
||||
clean_invalid_submodule_cb=lambda: self.automation_handler.clean_invalid_submodule() if self.automation_handler else None,
|
||||
browse_folder_cb=self.browse_folder,
|
||||
update_svn_status_cb=self.update_svn_status_indicator,
|
||||
open_gitignore_editor_cb=self.open_gitignore_editor,
|
||||
@ -203,6 +204,7 @@ class GitSvnSyncApp:
|
||||
auto_tab = self.main_frame.automation_tab
|
||||
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.clean_invalid_submodule_callback = self.automation_handler.clean_invalid_submodule
|
||||
|
||||
def _handle_fatal_error(self, message: str):
|
||||
log_handler.log_critical(message, func_name="_handle_fatal_error")
|
||||
|
||||
@ -15,6 +15,7 @@ from typing import (
|
||||
Optional,
|
||||
Set,
|
||||
)
|
||||
import re
|
||||
|
||||
from gitutility.logging_setup import log_handler
|
||||
from gitutility.commands.git_commands import GitCommandError
|
||||
@ -88,6 +89,8 @@ class AsyncResultHandler:
|
||||
"sync_submodules": self._handle_sync_all_submodules_result,
|
||||
"remove_submodule": self._handle_remove_submodule_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)
|
||||
@ -450,12 +453,74 @@ class AsyncResultHandler:
|
||||
return False, False
|
||||
|
||||
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"):
|
||||
return True, True
|
||||
elif result_data.get("status") == "error":
|
||||
self.main_frame.show_error("Sync Submodules Error", f"Failed:\n{result_data.get('message')}")
|
||||
status = result_data.get("status")
|
||||
message = result_data.get("message")
|
||||
exception = result_data.get("exception")
|
||||
|
||||
# --- 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
|
||||
|
||||
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]:
|
||||
if result_data.get("status") == "success":
|
||||
return True, True
|
||||
@ -470,6 +535,16 @@ class AsyncResultHandler:
|
||||
self.main_frame.show_error("Initialize Submodules Error", f"Failed:\n{result_data.get('message')}")
|
||||
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]:
|
||||
status, message = result_data.get("status"), result_data.get("message", "Operation finished.")
|
||||
if status == "error":
|
||||
|
||||
@ -6,6 +6,7 @@ import logging # Usato solo per i livelli di logging (es. logging.INFO)
|
||||
import datetime
|
||||
from typing import List, Dict, Any, Tuple, Optional, Set
|
||||
import subprocess
|
||||
import re
|
||||
|
||||
# Importa usando il percorso assoluto dal pacchetto
|
||||
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)
|
||||
result_payload: Dict[str, Any] = {"status": "error", "message": "Submodule sync failed.", "exception": None, "committed": False}
|
||||
try:
|
||||
# Questa chiamata ora solleva un'eccezione in caso di errore
|
||||
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:
|
||||
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}")
|
||||
@ -1148,3 +1159,43 @@ def run_init_submodules_async(
|
||||
finally:
|
||||
results_queue.put(result_payload)
|
||||
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"
|
||||
git_file_path = file_path.replace(os.path.sep, "/")
|
||||
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}"
|
||||
log_handler.log_debug(
|
||||
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]]:
|
||||
"""
|
||||
Gets the status of all submodules recursively and parses the output.
|
||||
Output format: ' ' (up-to-date), '-' (not initialized), '+' (commit mismatch), 'U' (conflict)
|
||||
Example line: '-b82312... gitsync_tool/logging_setup (unspecified)'
|
||||
Handles multiple output formats from the 'git submodule status' command.
|
||||
"""
|
||||
func_name = "submodule_status"
|
||||
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)
|
||||
|
||||
submodules = []
|
||||
# Regex to parse the status line
|
||||
status_regex = re.compile(r"^\s*([ U+-])([0-9a-fA-F]+)\s(.*?)\s\((.*?)\)\s*$")
|
||||
# This regex is now more robust and handles two main formats:
|
||||
# 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():
|
||||
line = line.strip()
|
||||
@ -2238,11 +2248,15 @@ class GitCommands:
|
||||
match = status_regex.match(line)
|
||||
if match:
|
||||
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({
|
||||
"status_char": status_char,
|
||||
"commit": commit,
|
||||
"status_char": final_status_char,
|
||||
"commit": commit.strip(),
|
||||
"path": path.strip(),
|
||||
"description": description.strip(),
|
||||
"description": (description or "N/A").strip(),
|
||||
})
|
||||
else:
|
||||
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
|
||||
cmd = ["git", "submodule", "deinit", "-f", "--", path]
|
||||
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 datetime
|
||||
import shutil
|
||||
import time
|
||||
import stat
|
||||
from typing import Tuple, List, Optional
|
||||
|
||||
# Import using absolute path from the package
|
||||
@ -70,22 +73,27 @@ class SubmoduleHandler:
|
||||
"""
|
||||
Initializes, fetches latest, and updates all submodules.
|
||||
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:
|
||||
repo_path: The path to the parent repository.
|
||||
|
||||
Returns:
|
||||
A tuple (success, message, commit_was_made).
|
||||
|
||||
Raises:
|
||||
GitCommandError: If the submodule update command fails.
|
||||
"""
|
||||
func_name = "execute_sync_all_submodules"
|
||||
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
|
||||
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)
|
||||
# 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)
|
||||
self.git_commands.submodule_update(
|
||||
working_directory=repo_path,
|
||||
@ -94,20 +102,20 @@ class SubmoduleHandler:
|
||||
recursive=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
|
||||
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
|
||||
# Step 4: Compare states
|
||||
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
|
||||
# Step 5: If there are changes, stage and commit
|
||||
if not changed_submodules:
|
||||
msg = "All submodules are already up-to-date."
|
||||
log_handler.log_info(msg, func_name=func_name)
|
||||
@ -118,13 +126,12 @@ class SubmoduleHandler:
|
||||
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)
|
||||
commit_made = self.git_commands.git_commit(repo_path, commit_message, stage_all_first=False)
|
||||
|
||||
if commit_made:
|
||||
msg = f"Successfully synced {len(changed_submodules)} submodule(s) and created commit."
|
||||
@ -133,11 +140,8 @@ class SubmoduleHandler:
|
||||
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
|
||||
# Questo è un caso strano, ma lo trattiamo come un successo parziale senza commit
|
||||
return True, msg, False
|
||||
|
||||
def execute_init_missing_submodules(self, repo_path: str) -> Tuple[bool, str]:
|
||||
"""
|
||||
@ -213,3 +217,143 @@ class SubmoduleHandler:
|
||||
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}"
|
||||
|
||||
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
|
||||
import os
|
||||
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) ---
|
||||
|
||||
@ -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 ---
|
||||
|
||||
@ -26,6 +26,7 @@ class AutomationTab(ttk.Frame):
|
||||
# Store callbacks
|
||||
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.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)
|
||||
self.main_frame = self.master.master
|
||||
@ -90,6 +91,31 @@ class AutomationTab(ttk.Frame):
|
||||
"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:
|
||||
"""
|
||||
Sets the state of all action widgets in this tab.
|
||||
@ -109,6 +135,7 @@ class AutomationTab(ttk.Frame):
|
||||
widgets = [
|
||||
self.update_wiki_button,
|
||||
self.analyze_history_button,
|
||||
self.clean_submodule_button,
|
||||
]
|
||||
|
||||
for widget in widgets:
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
# --- FILE: gitsync_tool/logic/automation_handler.py ---
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from tkinter import simpledialog
|
||||
|
||||
from gitutility.async_tasks import async_workers
|
||||
from gitutility.logging_setup import log_handler
|
||||
from gitutility.config.config_manager import DEFAULT_REMOTE_NAME
|
||||
from gitutility.gui.dialogs import SelectSubmoduleDialog
|
||||
|
||||
# Forward reference for type hinting
|
||||
if TYPE_CHECKING:
|
||||
@ -78,3 +80,50 @@ class AutomationHandler:
|
||||
"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.logging_setup import log_handler
|
||||
from gitutility.gui.dialogs import AddSubmoduleDialog
|
||||
|
||||
# Forward reference for type hinting to avoid circular import
|
||||
if TYPE_CHECKING:
|
||||
@ -51,8 +52,11 @@ class SubmoduleLogicHandler:
|
||||
self.main_frame.show_error("Action Failed", "Repository is not ready.")
|
||||
return
|
||||
|
||||
# Use the dialog method on MainFrame to get user input
|
||||
details: Optional[Tuple[str, str]] = self.main_frame.ask_new_submodule_details()
|
||||
# --- MODIFICA: Usa il nuovo dialogo personalizzato ---
|
||||
dialog = AddSubmoduleDialog(self.app.master)
|
||||
details = dialog.result
|
||||
# ---------------------------------------------------
|
||||
|
||||
if not details:
|
||||
self.main_frame.update_status_bar("Add submodule cancelled.")
|
||||
return
|
||||
@ -60,7 +64,7 @@ class SubmoduleLogicHandler:
|
||||
submodule_url, submodule_path = details
|
||||
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(
|
||||
worker_func=async_workers.run_add_submodule_async,
|
||||
args_tuple=args,
|
||||
@ -126,3 +130,5 @@ class SubmoduleLogicHandler:
|
||||
args_tuple=args,
|
||||
context_dict={"context": "init_submodules", "status_msg": "Initializing missing submodules..."},
|
||||
)
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user