634 lines
25 KiB
Python
634 lines
25 KiB
Python
# projectutility/core/git_manager.py
|
|
|
|
import os
|
|
import sys # Needed for path calculation
|
|
import logging
|
|
import shutil
|
|
from typing import Optional, Dict, Any, Tuple, Callable
|
|
|
|
# --- Constants ---
|
|
# Git Status Strings (used for reporting status consistently)
|
|
GIT_STATUS_UP_TO_DATE = "Up-to-date"
|
|
GIT_STATUS_BEHIND = "Behind"
|
|
GIT_STATUS_AHEAD = "Ahead"
|
|
GIT_STATUS_DIVERGED = "Diverged"
|
|
GIT_STATUS_NOT_CLONED = "Not Cloned"
|
|
GIT_STATUS_ERROR = "Error" # Generic Git or manager error
|
|
GIT_STATUS_CHECKING = "Checking..." # UI transient state
|
|
GIT_STATUS_UPDATING = "Updating..." # UI transient state
|
|
GIT_STATUS_GITPYTHON_MISSING = "GitPython Missing" # Specific error if library unavailable
|
|
|
|
# --- Optional GitPython Import --- (unchanged)
|
|
try:
|
|
import git
|
|
from git.exc import GitCommandError, InvalidGitRepositoryError, NoSuchPathError
|
|
from git import RemoteProgress
|
|
|
|
GITPYTHON_AVAILABLE = True
|
|
logger = logging.getLogger(__name__)
|
|
logger.info("GitPython library loaded successfully.")
|
|
except ImportError:
|
|
GITPYTHON_AVAILABLE = False
|
|
logger = logging.getLogger(__name__)
|
|
logger.warning(
|
|
"GitPython library not found. Git-related features will be disabled. "
|
|
"Install it using: pip install GitPython"
|
|
)
|
|
|
|
class GitCommandError(Exception):
|
|
pass
|
|
|
|
class InvalidGitRepositoryError(Exception):
|
|
pass
|
|
|
|
class NoSuchPathError(Exception):
|
|
pass
|
|
|
|
class RemoteProgress:
|
|
pass
|
|
|
|
class Repo:
|
|
pass # Dummy
|
|
|
|
# ... (altri dummy se necessario)
|
|
|
|
# --- Internal Model Import --- (unchanged)
|
|
try:
|
|
from projectutility.core.registry_models import ToolRegistryEntry
|
|
except ImportError:
|
|
logger.critical(
|
|
"Failed to import .registry_models. GitManager functions may fail!",
|
|
exc_info=True,
|
|
)
|
|
from dataclasses import dataclass
|
|
|
|
@dataclass
|
|
class ToolRegistryEntry:
|
|
id: str = "dummy_import_failed"
|
|
type: str = "unknown"
|
|
git_url: Optional[str] = None
|
|
git_ref: str = "main"
|
|
local_dir_name: Optional[str] = None # Aggiunto per coerenza con l'uso
|
|
display_name: str = "Dummy" # Aggiunto per coerenza
|
|
|
|
|
|
# --- Constants --- (unchanged)
|
|
GIT_STATUS_UP_TO_DATE = "Up-to-date"
|
|
GIT_STATUS_BEHIND = "Behind"
|
|
# ... (altri status)
|
|
GIT_STATUS_GITPYTHON_MISSING = "GitPython Missing"
|
|
|
|
# --- Determine if Running as Frozen Executable ---
|
|
IS_FROZEN = getattr(sys, "frozen", False)
|
|
|
|
# --- Calculate Base Paths ---
|
|
# APP_ROOT_DIR is the base for MANAGED_TOOLS_DIR.
|
|
# In frozen mode, it's the directory containing the executable.
|
|
# In script mode, it's the repository root.
|
|
_app_root_dir_calculated: str
|
|
try:
|
|
if IS_FROZEN:
|
|
_app_root_dir_calculated = os.path.dirname(sys.executable)
|
|
logger.debug(
|
|
f"Git Manager (FROZEN): Executable directory (used as App Root): {_app_root_dir_calculated}"
|
|
)
|
|
else:
|
|
_current_file_path = os.path.abspath(__file__) # .../core/git_manager.py
|
|
_core_dir = os.path.dirname(_current_file_path) # .../core
|
|
_app_source_root = os.path.dirname(_core_dir) # .../projectutility (package)
|
|
_app_root_dir_calculated = os.path.dirname(_app_source_root) # Repository Root
|
|
logger.debug(
|
|
f"Git Manager (SCRIPT): Repository root: {_app_root_dir_calculated}"
|
|
)
|
|
|
|
APP_ROOT_DIR = _app_root_dir_calculated
|
|
MANAGED_TOOLS_DIR = os.path.join(APP_ROOT_DIR, "managed_tools")
|
|
|
|
logger.debug(
|
|
f"Git Manager: Final APP_ROOT_DIR (Repo Root or Exe Dir): {APP_ROOT_DIR}"
|
|
)
|
|
logger.debug(
|
|
f"Git Manager: Final Managed tools directory (MANAGED_TOOLS_DIR): {MANAGED_TOOLS_DIR}"
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.critical(
|
|
f"Failed to calculate base paths in git_manager: {e}. "
|
|
f"Managed tools path will be incorrect.",
|
|
exc_info=True,
|
|
)
|
|
APP_ROOT_DIR = os.getcwd() # Fallback
|
|
MANAGED_TOOLS_DIR = os.path.join(APP_ROOT_DIR, "managed_tools") # Fallback
|
|
logger.warning(f"Using fallback managed tools path: {MANAGED_TOOLS_DIR}")
|
|
|
|
|
|
# --- Progress Reporting Class --- (unchanged)
|
|
class CloneProgressReporter(RemoteProgress):
|
|
def __init__(
|
|
self,
|
|
tool_id: str,
|
|
progress_callback: Optional[Callable[[str, str], None]] = None,
|
|
):
|
|
super().__init__()
|
|
self.tool_id = tool_id
|
|
self.progress_callback = progress_callback
|
|
self._last_message = ""
|
|
|
|
def update(self, op_code, cur_count, max_count=None, message=""):
|
|
if self.progress_callback and message:
|
|
progress_text = f"[{self.tool_id}] Git: {message.strip()}"
|
|
try:
|
|
self.progress_callback("git_progress", progress_text)
|
|
except Exception as e:
|
|
logger.error(
|
|
f"Error occurred within the progress callback "
|
|
f"function for tool '{self.tool_id}': {e}",
|
|
exc_info=True,
|
|
)
|
|
|
|
|
|
# --- Helper Functions --- (get_local_repo_path usa MANAGED_TOOLS_DIR che ora è frozen-aware)
|
|
# (Le funzioni _get_repo_object e _fetch_origin rimangono invariate nella logica interna)
|
|
|
|
|
|
def get_local_repo_path(tool_id: str, local_dir_name: Optional[str] = None) -> str:
|
|
dir_name = local_dir_name if local_dir_name else tool_id
|
|
absolute_path = os.path.abspath(
|
|
os.path.join(MANAGED_TOOLS_DIR, dir_name)
|
|
) # MANAGED_TOOLS_DIR è ora corretto
|
|
# logger.debug(f"Resolved local repo path for '{tool_id}': {absolute_path}") # Già loggato
|
|
return absolute_path
|
|
|
|
|
|
def _get_repo_object(
|
|
local_path: str,
|
|
) -> Optional["Repo"]: # "Repo" tra virgolette per forward declaration
|
|
if not GITPYTHON_AVAILABLE:
|
|
# logger.error("Cannot get Repo object: GitPython library is not available.") # Log ripetitivo
|
|
return None
|
|
if not os.path.isdir(local_path):
|
|
# logger.debug(f"Directory does not exist, cannot be a repo: {local_path}")
|
|
return None
|
|
try:
|
|
repo = git.Repo(local_path)
|
|
# logger.debug(f"Successfully accessed Git repository object at: {local_path}")
|
|
return repo
|
|
except InvalidGitRepositoryError:
|
|
logger.warning(
|
|
f"Directory exists but is not a valid Git repository: {local_path}"
|
|
)
|
|
return None
|
|
except NoSuchPathError:
|
|
logger.error(
|
|
f"GitPython reported 'NoSuchPathError' for existing (?) directory: {local_path}"
|
|
)
|
|
return None
|
|
except Exception as e:
|
|
logger.exception(f"Unexpected error accessing repository at {local_path}: {e}")
|
|
return None
|
|
|
|
|
|
def _fetch_origin(
|
|
repo: "Repo", # "Repo" tra virgolette
|
|
tool_id: str,
|
|
progress_callback: Optional[Callable[[str, str], None]] = None,
|
|
) -> bool:
|
|
if not GITPYTHON_AVAILABLE:
|
|
# logger.error(f"[{tool_id}] Cannot fetch: GitPython is not available.") # Log ripetitivo
|
|
return False
|
|
logger.info(f"[{tool_id}] Attempting to fetch updates from 'origin'...")
|
|
if progress_callback:
|
|
try:
|
|
progress_callback("git_status", f"[{tool_id}] Fetching from remote...")
|
|
except Exception as cb_err:
|
|
logger.error(
|
|
f"Error in progress callback (pre-fetch): {cb_err}", exc_info=True
|
|
)
|
|
try:
|
|
if not repo.remotes or "origin" not in [r.name for r in repo.remotes]:
|
|
logger.error(
|
|
f"[{tool_id}] Cannot fetch: Remote 'origin' not found or no remotes configured."
|
|
)
|
|
return False
|
|
origin = repo.remotes.origin
|
|
progress_reporter = (
|
|
CloneProgressReporter(tool_id, progress_callback)
|
|
if progress_callback
|
|
else None
|
|
)
|
|
fetch_info_list = origin.fetch(prune=True, progress=progress_reporter)
|
|
if logger.isEnabledFor(logging.DEBUG):
|
|
for info in fetch_info_list:
|
|
logger.debug(
|
|
f"[{tool_id}] Fetch info: {info.name}, {info.flags}, {info.note}"
|
|
)
|
|
logger.info(f"[{tool_id}] Fetch operation completed successfully.")
|
|
if progress_callback:
|
|
try:
|
|
progress_callback("git_status", f"[{tool_id}] Fetch complete.")
|
|
except Exception as cb_err:
|
|
logger.error(
|
|
f"Error in progress callback (post-fetch): {cb_err}", exc_info=True
|
|
)
|
|
return True
|
|
except GitCommandError as e:
|
|
error_output = e.stderr.strip() if e.stderr else str(e)
|
|
logger.error(
|
|
f"[{tool_id}] Git fetch command failed. Error: {error_output}",
|
|
exc_info=True,
|
|
)
|
|
if progress_callback:
|
|
try:
|
|
progress_callback(
|
|
"git_error", f"[{tool_id}] Fetch failed: {error_output}"
|
|
)
|
|
except Exception as cb_err:
|
|
logger.error(
|
|
f"Error in progress callback (fetch error): {cb_err}", exc_info=True
|
|
)
|
|
return False
|
|
except AttributeError: # E.g., repo.remotes.origin non esiste
|
|
logger.error(f"[{tool_id}] Failed to access remote 'origin' during fetch.")
|
|
return False
|
|
except Exception as e:
|
|
logger.exception(f"[{tool_id}] An unexpected error occurred during fetch: {e}")
|
|
if progress_callback:
|
|
try:
|
|
progress_callback(
|
|
"git_error", f"[{tool_id}] Unexpected fetch error: {e}"
|
|
)
|
|
except Exception as cb_err:
|
|
logger.error(
|
|
f"Error in progress callback (fetch unexpected error): {cb_err}",
|
|
exc_info=True,
|
|
)
|
|
return False
|
|
|
|
|
|
# --- Core Git Operations ---
|
|
# (ensure_repository_cloned, get_repository_status, _get_local_ref_name, update_repository)
|
|
# La logica interna di queste funzioni rimane invariata, ma beneficiano dei percorsi corretti.
|
|
|
|
|
|
def ensure_repository_cloned(
|
|
tool_entry: ToolRegistryEntry,
|
|
progress_callback: Optional[Callable[[str, str], None]] = None,
|
|
) -> Tuple[Optional["Repo"], str]: # "Repo"
|
|
if not GITPYTHON_AVAILABLE:
|
|
return None, GIT_STATUS_GITPYTHON_MISSING
|
|
if tool_entry.type != "git" or not tool_entry.git_url:
|
|
return None, "Tool not configured for Git or missing URL."
|
|
|
|
tool_id = tool_entry.id
|
|
local_path = get_local_repo_path(tool_id, tool_entry.local_dir_name)
|
|
repo = _get_repo_object(local_path)
|
|
if repo:
|
|
return repo, "Repository exists locally."
|
|
|
|
logger.info(
|
|
f"[{tool_id}] Repository not found. Cloning from '{tool_entry.git_url}' into '{local_path}'..."
|
|
)
|
|
if progress_callback:
|
|
try:
|
|
# Usa tool_entry.display_name per un messaggio più user-friendly
|
|
progress_callback(
|
|
"git_status",
|
|
f"[{tool_id}] Cloning {tool_entry.display_name or tool_id}...",
|
|
)
|
|
except Exception as cb_err:
|
|
logger.error(
|
|
f"Error in progress callback (pre-clone): {cb_err}", exc_info=True
|
|
)
|
|
try:
|
|
os.makedirs(
|
|
MANAGED_TOOLS_DIR, exist_ok=True
|
|
) # MANAGED_TOOLS_DIR è ora corretto
|
|
except OSError as e:
|
|
logger.error(
|
|
f"[{tool_id}] Failed to create parent directory '{MANAGED_TOOLS_DIR}': {e}.",
|
|
exc_info=True,
|
|
)
|
|
return None, f"Failed to create base directory: {e}"
|
|
|
|
progress_reporter = (
|
|
CloneProgressReporter(tool_id, progress_callback) if progress_callback else None
|
|
)
|
|
cloned_repo: Optional["Repo"] = None # "Repo"
|
|
clone_failed_msg = "Clone operation failed." # Default
|
|
try:
|
|
cloned_repo = git.Repo.clone_from(
|
|
url=tool_entry.git_url, to_path=local_path, progress=progress_reporter
|
|
)
|
|
logger.info(f"[{tool_id}] Repository cloned successfully to {local_path}.")
|
|
if progress_callback:
|
|
try:
|
|
progress_callback("git_status", f"[{tool_id}] Clone successful.")
|
|
except Exception as cb_err:
|
|
logger.error(
|
|
f"Error in progress callback (post-clone success): {cb_err}",
|
|
exc_info=True,
|
|
)
|
|
return cloned_repo, "Repository cloned successfully."
|
|
except GitCommandError as e:
|
|
error_output = e.stderr.strip() if e.stderr else str(e)
|
|
logger.error(
|
|
f"[{tool_id}] Failed to clone from '{tool_entry.git_url}'. Error: {error_output}",
|
|
exc_info=True,
|
|
)
|
|
clone_failed_msg = f"Failed to clone: {error_output}"
|
|
if progress_callback:
|
|
try:
|
|
progress_callback("git_error", f"[{tool_id}] {clone_failed_msg}")
|
|
except Exception as cb_err:
|
|
logger.error(
|
|
f"Error in progress callback (clone GitCommandError): {cb_err}",
|
|
exc_info=True,
|
|
)
|
|
except Exception as e:
|
|
logger.exception(f"[{tool_id}] An unexpected error occurred during clone: {e}")
|
|
clone_failed_msg = f"Unexpected clone error: {e}"
|
|
if progress_callback:
|
|
try:
|
|
progress_callback("git_error", f"[{tool_id}] {clone_failed_msg}")
|
|
except Exception as cb_err:
|
|
logger.error(
|
|
f"Error in progress callback (clone unexpected error): {cb_err}",
|
|
exc_info=True,
|
|
)
|
|
|
|
if os.path.exists(local_path): # Cleanup on failure
|
|
logger.warning(
|
|
f"[{tool_id}] Cleaning up incomplete clone directory: {local_path}"
|
|
)
|
|
try:
|
|
shutil.rmtree(local_path)
|
|
logger.info(f"[{tool_id}] Successfully removed incomplete clone directory.")
|
|
except OSError as rm_e:
|
|
logger.error(
|
|
f"[{tool_id}] Failed to remove incomplete clone directory '{local_path}': {rm_e}",
|
|
exc_info=True,
|
|
)
|
|
except Exception as rm_e: # Catch broader exceptions
|
|
logger.exception(
|
|
f"[{tool_id}] Unexpected error removing directory '{local_path}': {rm_e}"
|
|
)
|
|
return None, clone_failed_msg
|
|
|
|
|
|
def get_repository_status(tool_entry: ToolRegistryEntry) -> Dict[str, Any]:
|
|
status_result: Dict[str, Any] = {
|
|
"status": GIT_STATUS_ERROR,
|
|
"local_hash": None,
|
|
"remote_hash": None,
|
|
"local_ref": None,
|
|
"remote_ref": None,
|
|
"message": "Init error.",
|
|
}
|
|
if not GITPYTHON_AVAILABLE:
|
|
status_result.update(
|
|
{
|
|
"status": GIT_STATUS_GITPYTHON_MISSING,
|
|
"message": "GitPython not installed.",
|
|
}
|
|
)
|
|
return status_result
|
|
if tool_entry.type != "git" or not tool_entry.git_url:
|
|
status_result.update(
|
|
{"status": GIT_STATUS_ERROR, "message": "Not Git tool or no URL."}
|
|
)
|
|
return status_result
|
|
|
|
tool_id = tool_entry.id
|
|
local_path = get_local_repo_path(tool_id, tool_entry.local_dir_name)
|
|
repo = _get_repo_object(local_path)
|
|
if not repo:
|
|
status_result.update(
|
|
{"status": GIT_STATUS_NOT_CLONED, "message": "Not cloned."}
|
|
)
|
|
return status_result
|
|
|
|
logger.info(f"[{tool_id}] Checking repo status: {local_path}...")
|
|
if not _fetch_origin(repo, tool_id, progress_callback=None):
|
|
status_result.update({"status": GIT_STATUS_ERROR, "message": "Fetch failed."})
|
|
try: # Try to get local info even if fetch fails
|
|
status_result["local_hash"] = repo.head.commit.hexsha
|
|
status_result["local_ref"] = _get_local_ref_name(repo)
|
|
except Exception:
|
|
pass
|
|
return status_result
|
|
|
|
try:
|
|
local_commit: "Commit" = repo.head.commit # "Commit"
|
|
status_result["local_hash"] = local_commit.hexsha
|
|
status_result["local_ref"] = _get_local_ref_name(repo)
|
|
remote_ref_name = f"origin/{tool_entry.git_ref}"
|
|
status_result["remote_ref"] = remote_ref_name
|
|
remote_commit: Optional["Commit"] = None # "Commit"
|
|
try:
|
|
remote_commit = repo.commit(remote_ref_name)
|
|
status_result["remote_hash"] = remote_commit.hexsha
|
|
except (git.BadName, GitCommandError) as e:
|
|
logger.warning(f"[{tool_id}] Remote ref '{remote_ref_name}' not found: {e}")
|
|
status_result.update(
|
|
{
|
|
"status": GIT_STATUS_ERROR,
|
|
"remote_hash": None,
|
|
"message": f"Remote ref '{remote_ref_name}' not found.",
|
|
}
|
|
)
|
|
return status_result
|
|
|
|
if local_commit == remote_commit:
|
|
status_result.update(
|
|
{"status": GIT_STATUS_UP_TO_DATE, "message": "Up-to-date."}
|
|
)
|
|
elif repo.is_ancestor(local_commit.hexsha, remote_commit.hexsha):
|
|
status_result.update(
|
|
{"status": GIT_STATUS_BEHIND, "message": "Behind remote."}
|
|
)
|
|
elif repo.is_ancestor(remote_commit.hexsha, local_commit.hexsha):
|
|
status_result.update(
|
|
{"status": GIT_STATUS_AHEAD, "message": "Ahead of remote."}
|
|
)
|
|
else:
|
|
status_result.update(
|
|
{"status": GIT_STATUS_DIVERGED, "message": "Diverged from remote."}
|
|
)
|
|
logger.info(f"[{tool_id}] Status: {status_result['status']}")
|
|
except GitCommandError as e:
|
|
error_output = e.stderr.strip() if e.stderr else str(e)
|
|
logger.error(
|
|
f"[{tool_id}] Git error during status check: {error_output}", exc_info=True
|
|
)
|
|
status_result.update(
|
|
{"status": GIT_STATUS_ERROR, "message": f"Git error: {error_output}"}
|
|
)
|
|
except Exception as e:
|
|
logger.exception(f"[{tool_id}] Unexpected error during status check: {e}")
|
|
status_result.update(
|
|
{"status": GIT_STATUS_ERROR, "message": f"Unexpected error: {e}"}
|
|
)
|
|
return status_result
|
|
|
|
|
|
def _get_local_ref_name(repo: "Repo") -> str: # "Repo"
|
|
if not GITPYTHON_AVAILABLE:
|
|
return "N/A"
|
|
try:
|
|
head = repo.head
|
|
if head.is_detached:
|
|
return f"Detached@{head.commit.hexsha[:7]}"
|
|
else:
|
|
return head.ref.name
|
|
except Exception as e:
|
|
logger.warning(f"Could not determine local ref name: {e}")
|
|
return "Unknown"
|
|
|
|
|
|
def update_repository(
|
|
tool_entry: ToolRegistryEntry,
|
|
progress_callback: Optional[Callable[[str, str], None]] = None,
|
|
) -> Tuple[bool, str]:
|
|
if not GITPYTHON_AVAILABLE:
|
|
return False, GIT_STATUS_GITPYTHON_MISSING
|
|
if tool_entry.type != "git" or not tool_entry.git_url:
|
|
return False, "Not Git tool or no URL."
|
|
|
|
tool_id = tool_entry.id
|
|
logger.info(f"[{tool_id}] Starting repo update...")
|
|
repo, clone_msg = ensure_repository_cloned(tool_entry, progress_callback)
|
|
if not repo:
|
|
logger.error(f"[{tool_id}] Update failed (clone): {clone_msg}")
|
|
return False, f"Update failed (clone): {clone_msg}"
|
|
if not _fetch_origin(repo, tool_id, progress_callback):
|
|
logger.error(f"[{tool_id}] Update failed (fetch).")
|
|
return False, "Update failed (fetch from remote)."
|
|
|
|
target_ref = tool_entry.git_ref
|
|
remote_target_ref_name = f"origin/{target_ref}"
|
|
logger.info(f"[{tool_id}] Checking out target ref: '{target_ref}'")
|
|
if progress_callback:
|
|
try:
|
|
progress_callback(
|
|
"git_status", f"[{tool_id}] Checking out '{target_ref}'..."
|
|
)
|
|
except Exception as cb_err:
|
|
logger.error(
|
|
f"Error in progress callback (pre-checkout): {cb_err}", exc_info=True
|
|
)
|
|
try:
|
|
target_commit_on_remote: Optional["Commit"] = None # "Commit"
|
|
try:
|
|
target_commit_on_remote = repo.commit(remote_target_ref_name)
|
|
logger.debug(
|
|
f"[{tool_id}] Verified remote ref '{remote_target_ref_name}' (Commit: {target_commit_on_remote.hexsha[:7]})"
|
|
)
|
|
except (git.BadName, GitCommandError):
|
|
logger.error(
|
|
f"[{tool_id}] Update failed: Ref '{target_ref}' (as '{remote_target_ref_name}') not on remote 'origin'."
|
|
)
|
|
return False, f"Update failed: Ref '{target_ref}' not on remote 'origin'."
|
|
|
|
if repo.head.commit.hexsha == target_commit_on_remote.hexsha:
|
|
logger.info(
|
|
f"[{tool_id}] Already at target commit for '{target_ref}'. Checking out to ensure correct state."
|
|
)
|
|
repo.git.checkout(target_ref) # Checkout the ref (branch, tag, commit)
|
|
logger.info(f"[{tool_id}] Checked out '{target_ref}'.")
|
|
if progress_callback:
|
|
try:
|
|
progress_callback("git_status", f"[{tool_id}] Checkout complete.")
|
|
except Exception as cb_err:
|
|
logger.error(
|
|
f"Error in progress callback (post-checkout): {cb_err}",
|
|
exc_info=True,
|
|
)
|
|
|
|
if not repo.head.is_detached:
|
|
current_branch_name = repo.head.ref.name
|
|
logger.info(
|
|
f"[{tool_id}] On branch '{current_branch_name}'. Attempting ff-only pull..."
|
|
)
|
|
if progress_callback:
|
|
try:
|
|
progress_callback(
|
|
"git_status",
|
|
f"[{tool_id}] Pulling '{current_branch_name}' (ff-only)...",
|
|
)
|
|
except Exception as cb_err:
|
|
logger.error(
|
|
f"Error in progress callback (pre-pull): {cb_err}",
|
|
exc_info=True,
|
|
)
|
|
try:
|
|
repo.git.pull("origin", current_branch_name, "--ff-only")
|
|
logger.info(
|
|
f"[{tool_id}] FF-only pull successful for '{current_branch_name}'."
|
|
)
|
|
if progress_callback:
|
|
try:
|
|
progress_callback("git_status", f"[{tool_id}] Pull successful.")
|
|
except Exception as cb_err:
|
|
logger.error(
|
|
f"Error in progress callback (post-pull success): {cb_err}",
|
|
exc_info=True,
|
|
)
|
|
return (
|
|
True,
|
|
f"Repo updated. On branch '{current_branch_name}' tracking '{target_ref}'.",
|
|
)
|
|
except GitCommandError as pull_err:
|
|
error_output = (
|
|
pull_err.stderr.strip() if pull_err.stderr else str(pull_err)
|
|
)
|
|
logger.warning(
|
|
f"[{tool_id}] FF-only pull failed for '{current_branch_name}'. Git: {error_output}"
|
|
)
|
|
status_now = get_repository_status(tool_entry) # Re-check status
|
|
if status_now["status"] == GIT_STATUS_UP_TO_DATE:
|
|
logger.info(
|
|
f"[{tool_id}] Repo up-to-date despite ff-pull fail. Update OK."
|
|
)
|
|
if progress_callback:
|
|
try:
|
|
progress_callback(
|
|
"git_status",
|
|
f"[{tool_id}] Pull failed (ff-only), but repo is Up-to-date.",
|
|
)
|
|
except Exception as cb_err:
|
|
logger.error(
|
|
f"Error in progress callback (pull fail but ok): {cb_err}",
|
|
exc_info=True,
|
|
)
|
|
return (
|
|
True,
|
|
f"Repo up-to-date at '{target_ref}' (ff-pull failed/unnecessary).",
|
|
)
|
|
else:
|
|
logger.error(
|
|
f"[{tool_id}] Update failed: FF-pull failed for '{current_branch_name}', not up-to-date (Status: {status_now['status']}). Error: {error_output}"
|
|
)
|
|
return (
|
|
False,
|
|
f"Update failed: Pull error on '{current_branch_name}' ({status_now['status']}). Check logs.",
|
|
)
|
|
else: # Detached HEAD
|
|
detached_commit_sha = repo.head.commit.hexsha[:7]
|
|
logger.info(
|
|
f"[{tool_id}] Checkout to detached HEAD at {detached_commit_sha} ('{target_ref}'). Update complete."
|
|
)
|
|
return (
|
|
True,
|
|
f"Repo at '{target_ref}' (Detached HEAD {detached_commit_sha}).",
|
|
)
|
|
except GitCommandError as e:
|
|
error_output = e.stderr.strip() if e.stderr else str(e)
|
|
logger.error(
|
|
f"[{tool_id}] Git error updating to '{target_ref}'. Code: {e.status}. Output: {error_output}",
|
|
exc_info=True,
|
|
)
|
|
return False, f"Update failed: Git error (Code: {e.status}). Check logs."
|
|
except Exception as e:
|
|
logger.exception(f"[{tool_id}] Unexpected error during repo update: {e}")
|
|
return False, f"Update failed: Unexpected error - {e}"
|