fix import, add wiki export

This commit is contained in:
VALLONGOL 2025-07-10 15:38:26 +02:00
parent c262ab7680
commit 8e68e15937
5 changed files with 563 additions and 219 deletions

View File

@ -1,10 +1,10 @@
{
"export_timestamp": "2025-07-10T09:38:38.293280+00:00",
"export_timestamp": "2025-07-10T11:39:41.502153+00:00",
"source_server_url": "http://192.168.100.10:3000",
"repositories": {
"BackupTools": {
"bundle_file": "BackupTools.bundle",
"last_update": "2025-07-10T09:38:38.292950+00:00",
"last_update": "2025-07-10T11:28:29.616675+00:00",
"branches": {
"master": "481c8e89ce0d153080ac220bafcdb19db95cf9f5"
},
@ -12,6 +12,380 @@
"private": false,
"owner": "vallongol",
"clone_url": "http://192.168.100.10:3000/vallongol/BackupTools.git"
},
"BackupTools (Wiki)": {
"bundle_file": "BackupTools.wiki.bundle",
"last_update": "2025-07-10T11:28:29.857379+00:00",
"branches": {
"main": "45d45f62b9110ff2ce20c3394430e998050e7db6"
},
"description": "Wiki for the 'BackupTools' repository",
"private": false,
"owner": "vallongol",
"clone_url": "http://192.168.100.10:3000/vallongol/BackupTools.wiki.git"
},
"ControlPanel": {
"bundle_file": "ControlPanel.bundle",
"last_update": "2025-07-10T11:28:30.228095+00:00",
"branches": {
"SarRawData": "a441ced1e4a34bdac34a6bbd826947872bf7f2cb"
},
"description": "Control panel for view mfd and sar images from radar",
"private": false,
"owner": "vallongol",
"clone_url": "http://192.168.100.10:3000/vallongol/ControlPanel.git"
},
"ControlPanel (Wiki)": {
"bundle_file": "ControlPanel.wiki.bundle",
"last_update": "2025-07-10T11:28:30.506731+00:00",
"branches": {
"main": "f24d2dd58849b3f9a59baed5317f21e7c04bf6f1"
},
"description": "Wiki for the 'ControlPanel' repository",
"private": false,
"owner": "vallongol",
"clone_url": "http://192.168.100.10:3000/vallongol/ControlPanel.wiki.git"
},
"CreateIconFromFilesPng": {
"bundle_file": "CreateIconFromFilesPng.bundle",
"last_update": "2025-07-10T11:28:30.769289+00:00",
"branches": {
"master": "f317b9c3128e8c4be3b4cd2dad8906b8492e85f4"
},
"description": "Create icon files from png with alpha channel",
"private": false,
"owner": "vallongol",
"clone_url": "http://192.168.100.10:3000/vallongol/CreateIconFromFilesPng.git"
},
"DependencyAnalyzer": {
"bundle_file": "DependencyAnalyzer.bundle",
"last_update": "2025-07-10T11:28:31.139590+00:00",
"branches": {
"master": "7d572b882269090ff34f6c16e6e753f8553963d5"
},
"description": "Un'applicazione GUI per analizzare le dipendenze di progetti Python, generare requirements.txt, scaricare pacchetti offline e confrontare ambienti.",
"private": false,
"owner": "vallongol",
"clone_url": "http://192.168.100.10:3000/vallongol/DependencyAnalyzer.git"
},
"FlightMonitor": {
"bundle_file": "FlightMonitor.bundle",
"last_update": "2025-07-10T11:28:33.708421+00:00",
"branches": {
"master": "82bf0afce4099cd4dc1bbbeb67b7eabde07d6b5b"
},
"description": "Monitoraggio e salvataggio dei dati dei voli presenti in una determinata zona geografica",
"private": false,
"owner": "vallongol",
"clone_url": "http://192.168.100.10:3000/vallongol/FlightMonitor.git"
},
"FlightMonitor (Wiki)": {
"bundle_file": "FlightMonitor.wiki.bundle",
"last_update": "2025-07-10T11:28:34.027118+00:00",
"branches": {
"main": "b2e1764fa8a83d599ed87d08a53a0425a3951974"
},
"description": "Wiki for the 'FlightMonitor' repository",
"private": false,
"owner": "vallongol",
"clone_url": "http://192.168.100.10:3000/vallongol/FlightMonitor.wiki.git"
},
"GUI_g_reconverter": {
"bundle_file": "GUI_g_reconverter.bundle",
"last_update": "2025-07-10T11:28:34.363159+00:00",
"branches": {
"master": "0fbac920cae7250117793ba61796f0d3e7811669"
},
"description": "Questo tool fornisce un'interfaccia grafica intuitiva (GUI) per configurare e lanciare conversioni, eliminando la necessit\u00e0 di gestire manualmente complesse righe di comando.",
"private": false,
"owner": "vallongol",
"clone_url": "http://192.168.100.10:3000/vallongol/GUI_g_reconverter.git"
},
"GUI_g_reconverter (Wiki)": {
"bundle_file": "GUI_g_reconverter.wiki.bundle",
"last_update": "2025-07-10T11:28:34.608094+00:00",
"branches": {
"main": "081464e735819a6b9674eef729ecf400e34fafd2"
},
"description": "Wiki for the 'GUI_g_reconverter' repository",
"private": false,
"owner": "vallongol",
"clone_url": "http://192.168.100.10:3000/vallongol/GUI_g_reconverter.wiki.git"
},
"GeoElevation": {
"bundle_file": "GeoElevation.bundle",
"last_update": "2025-07-10T11:28:52.994901+00:00",
"branches": {
"master": "43a13a065844d05137f4b6c6e46132c0956e0a2f"
},
"description": "find elevation for a specific lat/lon and region",
"private": false,
"owner": "vallongol",
"clone_url": "http://192.168.100.10:3000/vallongol/GeoElevation.git"
},
"GeoElevation (Wiki)": {
"bundle_file": "GeoElevation.wiki.bundle",
"last_update": "2025-07-10T11:28:53.320234+00:00",
"branches": {
"main": "7785ea7b11bf63571e607bb7010c31cc79fc48f7"
},
"description": "Wiki for the 'GeoElevation' repository",
"private": false,
"owner": "vallongol",
"clone_url": "http://192.168.100.10:3000/vallongol/GeoElevation.wiki.git"
},
"GitUtility": {
"bundle_file": "GitUtility.bundle",
"last_update": "2025-07-10T11:28:54.537948+00:00",
"branches": {
"master": "16fe8af667ab9f88322986cf0e7c55079008e94e"
},
"description": "Utility per la gestione delle operazione su repository git",
"private": false,
"owner": "vallongol",
"clone_url": "http://192.168.100.10:3000/vallongol/GitUtility.git"
},
"GitUtility (Wiki)": {
"bundle_file": "GitUtility.wiki.bundle",
"last_update": "2025-07-10T11:28:54.804705+00:00",
"branches": {
"main": "784d043c8b5e9e47b9743efcf73952b2cf7df28f"
},
"description": "Wiki for the 'GitUtility' repository",
"private": false,
"owner": "vallongol",
"clone_url": "http://192.168.100.10:3000/vallongol/GitUtility.wiki.git"
},
"LauncherTool": {
"bundle_file": "LauncherTool.bundle",
"last_update": "2025-07-10T11:28:55.131784+00:00",
"branches": {
"master": "a04c599ced0e64f8f4b3c15e541ca97002484d8c"
},
"description": "lanciare applicazione in sequenza configurando opportuni parametri",
"private": false,
"owner": "vallongol",
"clone_url": "http://192.168.100.10:3000/vallongol/LauncherTool.git"
},
"LauncherTool (Wiki)": {
"bundle_file": "LauncherTool.wiki.bundle",
"last_update": "2025-07-10T11:28:55.426585+00:00",
"branches": {
"main": "c3844f16a26730d4783b9cef0813be90b3459af2"
},
"description": "Wiki for the 'LauncherTool' repository",
"private": false,
"owner": "vallongol",
"clone_url": "http://192.168.100.10:3000/vallongol/LauncherTool.wiki.git"
},
"MarkdownConverter": {
"bundle_file": "MarkdownConverter.bundle",
"last_update": "2025-07-10T11:28:55.799643+00:00",
"branches": {
"master": "db1e2cee0a8619fa5ede5b91d639061438d1beaf"
},
"description": "Conversione di file di tipo markdown in docx e pdf, anche con l'applicazione di template",
"private": false,
"owner": "vallongol",
"clone_url": "http://192.168.100.10:3000/vallongol/MarkdownConverter.git"
},
"MarkdownConverter (Wiki)": {
"bundle_file": "MarkdownConverter.wiki.bundle",
"last_update": "2025-07-10T11:28:56.046003+00:00",
"branches": {
"main": "690a2b1cdab203023c6379bced02d8e13372997a"
},
"description": "Wiki for the 'MarkdownConverter' repository",
"private": false,
"owner": "vallongol",
"clone_url": "http://192.168.100.10:3000/vallongol/MarkdownConverter.wiki.git"
},
"ProfileAnalyzer": {
"bundle_file": "ProfileAnalyzer.bundle",
"last_update": "2025-07-10T11:28:56.873273+00:00",
"branches": {
"master": "ce5ec4981c5ca791af743eebae75f5d9571f7555"
},
"description": "Analisi dei file di profilatura delle esecuzioni degli script python.",
"private": false,
"owner": "vallongol",
"clone_url": "http://192.168.100.10:3000/vallongol/ProfileAnalyzer.git"
},
"ProfileAnalyzer (Wiki)": {
"bundle_file": "ProfileAnalyzer.wiki.bundle",
"last_update": "2025-07-10T11:28:57.151129+00:00",
"branches": {
"main": "a8554d94d9c17748c42f31e9c4d9e7eec830c562"
},
"description": "Wiki for the 'ProfileAnalyzer' repository",
"private": false,
"owner": "vallongol",
"clone_url": "http://192.168.100.10:3000/vallongol/ProfileAnalyzer.wiki.git"
},
"ProjectInitializer": {
"bundle_file": "ProjectInitializer.bundle",
"last_update": "2025-07-10T11:28:57.476569+00:00",
"branches": {
"master": "126431a49a6a156146aaa562ed32b942972ed31c"
},
"description": "A Python application to initialize standard project structures for new Python projects, suitable for use with Git.",
"private": false,
"owner": "vallongol",
"clone_url": "http://192.168.100.10:3000/vallongol/ProjectInitializer.git"
},
"ProjectInitializer (Wiki)": {
"bundle_file": "ProjectInitializer.wiki.bundle",
"last_update": "2025-07-10T11:28:57.767439+00:00",
"branches": {
"main": "1db5644983f8e51255a8f7b5cf6967cb467c8033"
},
"description": "Wiki for the 'ProjectInitializer' repository",
"private": false,
"owner": "vallongol",
"clone_url": "http://192.168.100.10:3000/vallongol/ProjectInitializer.wiki.git"
},
"ProjectUtility": {
"bundle_file": "ProjectUtility.bundle",
"last_update": "2025-07-10T11:28:58.141516+00:00",
"branches": {
"master": "d063b16ec99f7d451256cb35db15eab3c72730c3"
},
"description": "Framework per la gestione e lancio di singoli script con varie funzionalit\u00e0, scaricate da git",
"private": false,
"owner": "vallongol",
"clone_url": "http://192.168.100.10:3000/vallongol/ProjectUtility.git"
},
"ProjectUtility (Wiki)": {
"bundle_file": "ProjectUtility.wiki.bundle",
"last_update": "2025-07-10T11:28:58.468499+00:00",
"branches": {
"main": "fcae03bcef6a061d5a2137d1469c2122fe7f0be7"
},
"description": "Wiki for the 'ProjectUtility' repository",
"private": false,
"owner": "vallongol",
"clone_url": "http://192.168.100.10:3000/vallongol/ProjectUtility.wiki.git"
},
"PyInstallerGUIWrapper": {
"bundle_file": "PyInstallerGUIWrapper.bundle",
"last_update": "2025-07-10T11:28:58.827435+00:00",
"branches": {
"master": "c1f368fe07977a4755f08422e9b70bfa81d62dc4"
},
"description": "Creazione di eseguibili e librerie che \u00e8 possibile utilizzare senza dover aver installato python e le sue dipendenze",
"private": false,
"owner": "vallongol",
"clone_url": "http://192.168.100.10:3000/vallongol/PyInstallerGUIWrapper.git"
},
"PyInstallerGUIWrapper (Wiki)": {
"bundle_file": "PyInstallerGUIWrapper.wiki.bundle",
"last_update": "2025-07-10T11:28:59.075697+00:00",
"branches": {
"main": "7b2367baf1594227f94e78b5095d67631998a9a1"
},
"description": "Wiki for the 'PyInstallerGUIWrapper' repository",
"private": false,
"owner": "vallongol",
"clone_url": "http://192.168.100.10:3000/vallongol/PyInstallerGUIWrapper.wiki.git"
},
"Radalyze": {
"bundle_file": "Radalyze.bundle",
"last_update": "2025-07-10T11:28:59.317702+00:00",
"branches": {
"master": "2073a820bb191862237d80bfa44b0f885a4b4f31"
},
"description": "",
"private": false,
"owner": "vallongol",
"clone_url": "http://192.168.100.10:3000/vallongol/Radalyze.git"
},
"Radian": {
"bundle_file": "Radian.bundle",
"last_update": "2025-07-10T11:28:59.663951+00:00",
"branches": {
"master": "a09ff98bfbf96c61c27b4ce5dd55d7d0961c616c"
},
"description": "Framework per l'analisi dei dati di radar e simulazione",
"private": false,
"owner": "vallongol",
"clone_url": "http://192.168.100.10:3000/vallongol/Radian.git"
},
"RepoSync": {
"bundle_file": "RepoSync.bundle",
"last_update": "2025-07-10T11:29:00.046837+00:00",
"branches": {
"master": "c66e1affe482e187ab37693ba1213f4a5f144148"
},
"description": "Software per la sincronizzazione di repository tra vari sistemi di gestione come gitea, github, ecc",
"private": false,
"owner": "vallongol",
"clone_url": "http://192.168.100.10:3000/vallongol/RepoSync.git"
},
"SamCtrlM_PSM_PX_SVN": {
"bundle_file": "SamCtrlM_PSM_PX_SVN.bundle",
"last_update": "2025-07-10T11:29:01.323377+00:00",
"branches": {
"master": "1c46bb8730c032daba185d2fff4036289385211a"
},
"description": "",
"private": false,
"owner": "vallongol",
"clone_url": "http://192.168.100.10:3000/vallongol/SamCtrlM_PSM_PX_SVN.git"
},
"SecureTransferTool": {
"bundle_file": "SecureTransferTool.bundle",
"last_update": "2025-07-10T11:29:01.810288+00:00",
"branches": {
"master": "8d91bf8575fd6f4ef0c878832f314561287fb75d"
},
"description": "",
"private": false,
"owner": "vallongol",
"clone_url": "http://192.168.100.10:3000/vallongol/SecureTransferTool.git"
},
"WorkspaceInspector": {
"bundle_file": "WorkspaceInspector.bundle",
"last_update": "2025-07-10T11:29:02.218689+00:00",
"branches": {
"master": "664b4692092044466950bcd94fb2a6a4e357d1ab"
},
"description": "",
"private": false,
"owner": "vallongol",
"clone_url": "http://192.168.100.10:3000/vallongol/WorkspaceInspector.git"
},
"cpp_python_debug": {
"bundle_file": "cpp_python_debug.bundle",
"last_update": "2025-07-10T11:31:22.744936+00:00",
"branches": {
"master": "6c9ec6060e298409ea1e0073fbea864067c2db11"
},
"description": "debug di applicazioni c++ mediante l'utilizzo di gdb.exe orchestrato da python che fornisce l'interfaccia",
"private": false,
"owner": "vallongol",
"clone_url": "http://192.168.100.10:3000/vallongol/cpp_python_debug.git"
},
"cpp_python_debug (Wiki)": {
"bundle_file": "cpp_python_debug.wiki.bundle",
"last_update": "2025-07-10T11:31:23.062025+00:00",
"branches": {
"main": "69c3557e4e8fbbb086ec02392577aef4e124391b"
},
"description": "Wiki for the 'cpp_python_debug' repository",
"private": false,
"owner": "vallongol",
"clone_url": "http://192.168.100.10:3000/vallongol/cpp_python_debug.wiki.git"
},
"radar_data_reader (Wiki)": {
"bundle_file": "radar_data_reader.wiki.bundle",
"last_update": "2025-07-10T11:39:41.497173+00:00",
"branches": {
"main": "d7b277b87efd441a6bed5d526ce421730ce5fc07"
},
"description": "Wiki for the 'radar_data_reader' repository",
"private": false,
"owner": "vallongol",
"clone_url": "http://192.168.100.10:3000/vallongol/radar_data_reader.wiki.git"
}
}
}

View File

@ -69,7 +69,7 @@ class GitManager:
except Exception as e:
self.logger.error(f"Failed to parse and inject token into URL: {e}. Using original URL.")
command = ["git", "clone", "--progress", url_to_clone, local_path]
self._execute(command, working_directory=parent_dir, timeout=300)
self._execute(command, working_directory=parent_dir, timeout=900)
self.logger.info(f"Repository cloned successfully to '{local_path}'.")
def create_git_bundle(self, repo_path: str, bundle_path: str):

View File

@ -1,10 +1,5 @@
# RepoSync/core/gitea_client.py
"""
Gitea-specific implementation of the BaseVCSClient.
This module handles all interactions with a Gitea instance via its REST API.
"""
# (File completo - versione corretta e semplificata)
import logging
import requests
@ -22,164 +17,112 @@ class GiteaClient(BaseVCSClient):
Implements the abstract methods defined in BaseVCSClient.
"""
def __init__(self, api_url: str, token: str, logger: logging.Logger):
"""
Initializes the GiteaClient.
"""
super().__init__(api_url, token)
self.logger = logger
self.logger.debug(f"GiteaClient initialized for URL: {self.api_url}")
def _api_request(
self,
method: str,
endpoint: str,
params: Optional[Dict[str, Any]] = None,
json_data: Optional[Dict[str, Any]] = None,
) -> Any:
"""
A helper method to make requests to the Gitea API.
"""
def _api_request(self, method: str, endpoint: str, params: Optional[Dict[str, Any]] = None, json_data: Optional[Dict[str, Any]] = None, raise_on_404: bool = True) -> Any:
url = f"{self.api_url}/api/v1{endpoint}"
self.logger.debug(f"Making API call: {method} {url}")
try:
response = requests.request(
method=method,
url=url,
headers=self.headers,
params=params,
json=json_data,
timeout=30
)
response = requests.request(method=method, url=url, headers=self.headers, params=params, json=json_data, timeout=30)
if not raise_on_404 and response.status_code == 404: return None
response.raise_for_status()
if response.status_code == 204:
return None
return response.json()
return None if response.status_code == 204 else response.json()
except requests.exceptions.RequestException as e:
self.logger.error(f"Gitea API request failed: {e}")
raise GiteaAPIError(f"Failed to communicate with Gitea API: {e}") from e
def get_repositories(self) -> List[Dict[str, Any]]:
"""
Retrieves a list of all repositories accessible by the user, handling pagination.
"""
self.logger.info("Fetching list of repositories from Gitea...")
self.logger.info("Fetching list of repositories and wikis from Gitea...")
all_repos = []
page = 1
limit = 50
while True:
self.logger.debug(f"Fetching page {page} of repositories...")
params = {"page": page, "limit": limit}
repos_page = self._api_request("GET", "/user/repos", params=params)
if not repos_page:
break
if not repos_page: break
for repo in repos_page:
standardized_repo = {
"name": repo.get("name"),
"owner": repo.get("owner", {}).get("login"),
"clone_url": repo.get("clone_url"),
"description": repo.get("description"),
"private": repo.get("private"),
"size_kb": repo.get("size", 0)
}
all_repos.append(standardized_repo)
all_repos.append({
"name": repo.get("name"), "owner": repo.get("owner", {}).get("login"),
"clone_url": repo.get("clone_url"), "description": repo.get("description"),
"private": repo.get("private"), "size_kb": repo.get("size", 0), "is_wiki": False
})
# We trust the 'has_wiki' flag provided by the API.
if repo.get("has_wiki"):
repo_name, clone_url = repo.get("name"), repo.get("clone_url")
if clone_url and clone_url.endswith(".git"):
wiki_clone_url = clone_url[:-4] + ".wiki.git"
all_repos.append({
"name": f"{repo_name} (Wiki)", "owner": repo.get("owner", {}).get("login"),
"clone_url": wiki_clone_url, "description": f"Wiki for the '{repo_name}' repository",
"private": repo.get("private"), "size_kb": 0, "is_wiki": True
})
page += 1
self.logger.info(f"Found {len(all_repos)} repositories.")
self.logger.info(f"Found {len(all_repos)} items (repositories and wikis).")
return all_repos
def get_repository(self, repo_name: str) -> Optional[Dict[str, Any]]:
"""
Retrieves a single repository by its name using the search endpoint.
"""
self.logger.info(f"Searching for repository '{repo_name}'...")
params = {"q": repo_name, "limit": 10}
self.logger.info(f"Searching for item '{repo_name}'...")
is_wiki, base_repo_name = repo_name.endswith(" (Wiki)"), repo_name.removesuffix(" (Wiki)").strip()
params = {"q": base_repo_name, "limit": 10}
try:
search_result = self._api_request("GET", "/repos/search", params=params)
except GiteaAPIError as e:
self.logger.warning(f"Repo search failed: {e}. This may be a permissions issue.")
return None
self.logger.warning(f"Repo search failed: {e}."); return None
if not search_result or not search_result.get("data"):
self.logger.info(f"Repository '{repo_name}' not found via search.")
return None
self.logger.info(f"Item '{base_repo_name}' not found via search."); return None
for repo in search_result["data"]:
if repo.get("name").lower() == repo_name.lower():
self.logger.info(f"Found exact match for repository '{repo_name}'.")
return {
"name": repo.get("name"),
"owner": repo.get("owner", {}).get("login"),
"clone_url": repo.get("clone_url"),
"description": repo.get("description"),
"private": repo.get("private"),
"size_kb": repo.get("size", 0)
}
if repo.get("name").lower() == base_repo_name.lower():
self.logger.info(f"Found base repository '{base_repo_name}'.")
owner, clone_url = repo.get("owner", {}).get("login"), repo.get("clone_url")
if is_wiki:
return {"name": repo_name, "owner": owner, "clone_url": (clone_url[:-4] + ".wiki.git" if clone_url and clone_url.endswith(".git") else None), "is_wiki": True}
else:
return {"name": repo.get("name"), "owner": owner, "clone_url": clone_url, "description": repo.get("description"), "private": repo.get("private"), "size_kb": repo.get("size", 0), "is_wiki": False}
self.logger.info(f"No exact match for '{base_repo_name}' found in search results."); return None
self.logger.info(f"No exact match for repository '{repo_name}' found in search results.")
return None
def create_repository(self, name: str, description: str = "", private: bool = True) -> Dict[str, Any]:
"""
Creates a new repository on the Gitea platform.
"""
self.logger.info(f"Creating new repository '{name}'...")
payload = {"name": name, "description": description, "private": private}
new_repo = self._api_request("POST", "/user/repos", json_data=payload)
self.logger.info(f"Successfully created repository '{name}'.")
return {
"name": new_repo.get("name"),
"owner": new_repo.get("owner", {}).get("login"),
"clone_url": new_repo.get("clone_url"),
"description": new_repo.get("description"),
"private": new_repo.get("private"),
"size_kb": new_repo.get("size", 0)
}
def create_repository(self, name: str, description: str = "", private: bool = True, is_wiki: bool = False) -> Dict[str, Any]:
self.logger.info(f"Request to create item: '{name}' (is_wiki={is_wiki})")
base_repo_name = name.removesuffix(" (Wiki)").strip()
if is_wiki:
base_repo = self.get_repository(base_repo_name)
if not base_repo: raise GiteaAPIError(f"Cannot create wiki for non-existent repository '{base_repo_name}'.")
self.enable_wiki(base_repo["owner"], base_repo_name)
return self.get_repository(name)
else:
payload = {"name": name, "description": description, "private": private}
new_repo = self._api_request("POST", "/user/repos", json_data=payload)
self.logger.info(f"Successfully created repository '{name}'.")
return {"name": new_repo.get("name"), "owner": new_repo.get("owner", {}).get("login"), "clone_url": new_repo.get("clone_url"), "description": new_repo.get("description"), "private": new_repo.get("private"), "size_kb": new_repo.get("size", 0), "is_wiki": False}
def get_repository_branches(self, owner: str, repo_name: str) -> Dict[str, str]:
"""
Retrieves a dictionary of branches and their head commit hashes.
"""
self.logger.info(f"Fetching branches for '{owner}/{repo_name}' from Gitea...")
endpoint = f"/repos/{owner}/{repo_name}/branches"
api_repo_name = repo_name.removesuffix(" (Wiki)").strip()
if repo_name.endswith(" (Wiki)"): api_repo_name += ".wiki"
endpoint = f"/repos/{owner}/{api_repo_name}/branches"
try:
branches_data = self._api_request("GET", endpoint)
branches = {}
if branches_data:
for branch in branches_data:
branch_name = branch.get("name")
commit_hash = branch.get("commit", {}).get("id")
if branch_name and commit_hash:
branches[branch_name] = commit_hash
self.logger.debug(f"Found branches for '{repo_name}': {list(branches.keys())}")
return branches
branches_data = self._api_request("GET", endpoint, raise_on_404=False)
if branches_data is None: return {}
return {b.get("name"): b.get("commit", {}).get("id") for b in branches_data if b.get("name") and b.get("commit", {}).get("id")}
except GiteaAPIError as e:
self.logger.error(f"Failed to fetch branches for '{repo_name}': {e}")
return {}
self.logger.error(f"Failed to fetch branches for '{repo_name}': {e}"); return {}
def set_default_branch(self, owner: str, repo_name: str, branch_name: str):
"""
Sets the default branch for a repository via the API.
"""
self.logger.info(f"Setting default branch for '{owner}/{repo_name}' to '{branch_name}'...")
endpoint = f"/repos/{owner}/{repo_name}"
payload = {
"default_branch": branch_name
}
api_repo_name = repo_name.removesuffix(" (Wiki)").strip()
endpoint = f"/repos/{owner}/{api_repo_name}"
payload = {"default_branch": branch_name} if not repo_name.endswith(" (Wiki)") else {"wiki": {"default_branch": branch_name}}
try:
self._api_request("PATCH", endpoint, json_data=payload)
self.logger.info("Default branch set successfully.")
except GiteaAPIError as e:
self.logger.error(f"Failed to set default branch for '{repo_name}': {e}")
# This is not a critical failure, so we log it but don't re-raise.
self.logger.info(f"Default branch for '{repo_name}' set to '{branch_name}'.")
except GiteaAPIError as e: self.logger.error(f"Failed to set default branch for '{repo_name}': {e}")
def enable_wiki(self, owner: str, repo_name: str):
self.logger.info(f"Ensuring wiki is enabled for '{owner}/{repo_name}'...")
endpoint = f"/repos/{owner}/{repo_name}"
payload = {"has_wiki": True}
try:
self._api_request("PATCH", endpoint, json_data=payload)
self.logger.info(f"Wiki enabled successfully for '{owner}/{repo_name}'.")
except GiteaAPIError as e: self.logger.error(f"Failed to enable wiki for '{repo_name}': {e}"); raise

View File

@ -10,6 +10,7 @@ import logging
import os
import shutil
import stat
import time # Import the time module for sleep
from enum import Enum, auto
from pathlib import Path
from typing import List, Dict, Any, Optional, Callable
@ -17,62 +18,94 @@ from typing import List, Dict, Any, Optional, Callable
from .base_vcs_client import BaseVCSClient
from .git_manager import GitManager, GitCommandError
def _remove_readonly(func, path, exc_info):
def robust_rmtree(path, max_retries=3, delay=0.1):
"""
A robust version of shutil.rmtree that handles read-only files and
retries on PermissionError (file in use on Windows).
"""
for i in range(max_retries):
try:
shutil.rmtree(path, onerror=_remove_readonly_onerror)
return # Success
except PermissionError as e:
if "used by another process" in str(e) and i < max_retries - 1:
time.sleep(delay) # Wait and retry
else:
raise # Re-raise the final exception
except FileNotFoundError:
return # Path already gone, which is fine
def _remove_readonly_onerror(func, path, exc_info):
"""
Error handler for shutil.rmtree. It's called when rmtree fails.
It changes the file permissions to writable and retries the deletion.
"""
if not os.access(path, os.W_OK):
os.chmod(path, stat.S_IWRITE)
func(path)
else:
raise
# If it's not a read-only error, just re-raise
# The robust_rmtree will catch PermissionError
raise exc_info[1]
class SyncState(Enum):
IDENTICAL = auto(); AHEAD = auto(); BEHIND = auto(); DIVERGED = auto(); NEW_REPO = auto(); ORPHANED_BUNDLE = auto(); ERROR = auto()
class SyncManager:
def __init__(self, git_manager: GitManager, vcs_client: BaseVCSClient, sync_bundle_path: Path, logger: logging.Logger):
self.git_manager = git_manager; self.vcs_client = vcs_client; self.sync_bundle_path = sync_bundle_path; self.logger = logger
self.temp_clone_path = self.sync_bundle_path / "temp_clones"; self.manifest_path = self.sync_bundle_path / "manifest.json"
self.git_manager, self.vcs_client, self.sync_bundle_path, self.logger = git_manager, vcs_client, sync_bundle_path, logger
self.temp_clone_path, self.manifest_path = self.sync_bundle_path / "temp_clones", self.sync_bundle_path / "manifest.json"
self.logger.debug("SyncManager for offline sync initialized.")
def _write_manifest(self, manifest_data: Dict[str, Any]):
self.logger.info(f"Writing manifest file to: {self.manifest_path}")
try:
with open(self.manifest_path, "w", encoding="utf-8") as f: json.dump(manifest_data, f, indent=4)
self.logger.info("Manifest file created successfully.")
self.logger.info(f"Manifest file written successfully to: {self.manifest_path}")
except IOError as e: self.logger.error(f"Failed to write manifest file: {e}"); raise
def export_repositories_to_bundles(self, source_repos: List[Dict], progress_callback: Optional[Callable[[int, int, int], None]] = None):
self.logger.info(f"Starting export of {len(source_repos)} repositories...")
self.sync_bundle_path.mkdir(exist_ok=True); self.temp_clone_path.mkdir(exist_ok=True)
manifest_repos = {}; total_repos = len(source_repos); successes = 0
self.logger.info(f"Starting export of {len(source_repos)} items..."); self.sync_bundle_path.mkdir(exist_ok=True); self.temp_clone_path.mkdir(exist_ok=True)
manifest_repos, total_repos, successes = {}, len(source_repos), 0
for i, repo in enumerate(source_repos):
repo_name = repo["name"]; local_repo_path = self.temp_clone_path / repo_name
bundle_file_path = self.sync_bundle_path / f"{repo_name}.bundle"; bundle_size_bytes = 0
self.logger.info(f"--- Exporting '{repo_name}' ({i+1}/{total_repos}) ---")
repo_name, local_repo_path = repo["name"], self.temp_clone_path / repo["name"]
bundle_file_name = repo["name"].replace(" (Wiki)", ".wiki") + ".bundle"
bundle_file_path, bundle_size_bytes, is_wiki = self.sync_bundle_path / bundle_file_name, 0, repo.get("is_wiki", False)
self.logger.info(f"--- Processing '{repo_name}' ({i+1}/{total_repos}) ---")
try:
if local_repo_path.exists(): shutil.rmtree(local_repo_path, onerror=_remove_readonly)
# Always clean up before starting
if local_repo_path.exists(): robust_rmtree(local_repo_path)
self.git_manager.clone(remote_url=repo["clone_url"], local_path=str(local_repo_path), token=self.vcs_client.token)
self.git_manager.create_git_bundle(str(local_repo_path), str(bundle_file_path))
if bundle_file_path.exists(): bundle_size_bytes = bundle_file_path.stat().st_size
branches = self.git_manager.get_branches_with_heads(str(local_repo_path))
manifest_repos[repo_name] = {"bundle_file": bundle_file_path.name, "last_update": datetime.datetime.now(datetime.timezone.utc).isoformat(), "branches": branches, "description": repo.get("description", ""), "private": repo.get("private", True), "owner": repo.get("owner"), "clone_url": repo.get("clone_url")}
successes += 1
except (GitCommandError, Exception) as e:
self.logger.error(f"Failed to process repository '{repo_name}': {e}", exc_info=True)
except GitCommandError as e:
# --- NEW LOGIC: Handle empty/uninitialized wikis gracefully ---
if is_wiki and ("Could not read from remote repository" in str(e) or "does not appear to be a git repository" in str(e)):
self.logger.warning(f"Skipping empty or uninitialized wiki for '{repo_name}'. It will not be included in the manifest.")
else:
# For all other critical errors, log them fully
self.logger.error(f"Failed to process repository '{repo_name}': {e}", exc_info=True)
except Exception as e:
# Catch any other unexpected errors
self.logger.error(f"An unexpected error occurred while processing '{repo_name}': {e}", exc_info=True)
finally:
if local_repo_path.exists(): robust_rmtree(local_repo_path)
if progress_callback: progress_callback(i + 1, total_repos, bundle_size_bytes)
# --- NEW LOGIC: Only write manifest and cleanup if there were successes ---
if successes > 0:
full_manifest = {"export_timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(), "source_server_url": self.vcs_client.api_url, "repositories": manifest_repos}
self._write_manifest(full_manifest)
else:
self.logger.error("Export process finished with no successful exports. Manifest file was NOT written.")
# --- NEW LOGIC: Final cleanup of the entire temp directory ---
if self.temp_clone_path.exists():
self.logger.info("Performing final cleanup of temporary clone directory...")
shutil.rmtree(self.temp_clone_path, onerror=_remove_readonly)
self.logger.error("Export process finished with no successful exports. Manifest file was NOT written or updated.")
self.logger.info("Export process finished.")
@ -81,20 +114,17 @@ class SyncManager:
if not self.manifest_path.exists(): self.logger.warning("manifest.json not found. Cannot compare state."); return []
try:
with open(self.manifest_path, "r", encoding="utf-8") as f: manifest = json.load(f)
except (json.JSONDecodeError, IOError) as e:
self.logger.error(f"Could not read or parse manifest file: {e}"); return []
comparison_results = []; manifest_repos = manifest.get("repositories", {})
except (json.JSONDecodeError, IOError) as e: self.logger.error(f"Could not read or parse manifest file: {e}"); return []
comparison_results, manifest_repos = [], manifest.get("repositories", {})
for repo_name, bundle_info in manifest_repos.items():
self.logger.debug(f"Comparing state for '{repo_name}'...")
remote_repo = self.vcs_client.get_repository(repo_name)
if not remote_repo:
comparison_results.append({"name": repo_name, "state": SyncState.NEW_REPO, "bundle_info": bundle_info}); continue
if not remote_repo: comparison_results.append({"name": repo_name, "state": SyncState.NEW_REPO, "bundle_info": bundle_info}); continue
remote_owner = remote_repo.get("owner")
if not remote_owner:
comparison_results.append({"name": repo_name, "state": SyncState.ERROR, "details": "Could not determine remote owner."}); continue
if not remote_owner: comparison_results.append({"name": repo_name, "state": SyncState.ERROR, "details": "Could not determine remote owner."}); continue
remote_branches = self.vcs_client.get_repository_branches(remote_owner, repo_name)
bundle_branches = bundle_info.get("branches", {})
all_branches = set(bundle_branches.keys()) | set(remote_branches.keys()); is_identical, is_ahead, is_behind = True, False, False
all_branches, is_identical, is_ahead, is_behind = set(bundle_branches.keys()) | set(remote_branches.keys()), True, False, False
for branch in all_branches:
bundle_hash, remote_hash = bundle_branches.get(branch), remote_branches.get(branch)
if bundle_hash != remote_hash:
@ -116,46 +146,46 @@ class SyncManager:
for repo_info in repos_to_import:
repo_name, bundle_info = repo_info["name"], repo_info["bundle_info"]
bundle_file, local_repo_path = self.sync_bundle_path / bundle_info["bundle_file"], self.temp_clone_path / repo_name
is_wiki = " (Wiki)" in repo_name
if not bundle_file.exists(): self.logger.error(f"Bundle file {bundle_file} for '{repo_name}' not found. Skipping."); continue
self.logger.info(f"--- Importing '{repo_name}' from bundle ---")
try:
is_new_repo = False
dest_repo = self.vcs_client.get_repository(repo_name)
if not dest_repo:
is_new_repo = True
self.logger.info(f"'{repo_name}' not found on destination, creating...")
dest_repo = self.vcs_client.create_repository(name=repo_name, description=bundle_info.get("description", ""), private=bundle_info.get("private", True))
base_repo_name = repo_name.removesuffix(" (Wiki)").strip()
dest_repo = self.vcs_client.get_repository(base_repo_name)
# --- NEW IMPORT LOGIC ---
if not dest_repo:
self.logger.info(f"Base repository '{base_repo_name}' not found, creating...")
dest_repo = self.vcs_client.create_repository(name=base_repo_name, description=bundle_info.get("description", ""), private=bundle_info.get("private", True))
is_new_repo = True
if is_wiki:
self.logger.info(f"Ensuring wiki is enabled for '{base_repo_name}'...")
self.vcs_client.enable_wiki(dest_repo["owner"], base_repo_name)
if local_repo_path.exists(): robust_rmtree(local_repo_path)
self.logger.info(f"Cloning '{repo_name}' directly from bundle file...")
if local_repo_path.exists(): shutil.rmtree(local_repo_path, onerror=_remove_readonly)
self.git_manager.clone_from_bundle(str(bundle_file), str(local_repo_path))
clone_url = dest_repo.get("clone_url")
if not clone_url:
owner = dest_repo.get('owner') or bundle_info.get('owner')
clone_url = f"{self.vcs_client.api_url}/{owner}/{repo_name}.git"
self.logger.warning(f"Could not find clone_url, constructed manually: {clone_url}")
dest_owner = dest_repo.get("owner")
dest_base_url = self.vcs_client.api_url
dest_clone_url = f"{dest_base_url}/{dest_owner}/{base_repo_name}.wiki.git" if is_wiki else f"{dest_base_url}/{dest_owner}/{base_repo_name}.git"
self.logger.info(f"Setting remote 'origin' to destination URL: {clone_url}")
auth_clone_url = self.git_manager.get_url_with_token(clone_url, self.vcs_client.token)
self.logger.info(f"Setting remote 'origin' to destination URL: {dest_clone_url}")
auth_clone_url = self.git_manager.get_url_with_token(dest_clone_url, self.vcs_client.token)
self.git_manager.set_remote_url(str(local_repo_path), "origin", auth_clone_url)
self.logger.info(f"Pushing all content to destination for '{repo_name}'...")
self.git_manager.push(str(local_repo_path), "origin", all_branches=True, all_tags=True)
if is_new_repo:
if is_new_repo and not is_wiki:
bundle_branches = bundle_info.get("branches", {})
default_branch = "main" if "main" in bundle_branches else "master"
if default_branch in bundle_branches:
owner = dest_repo.get("owner")
self.vcs_client.set_default_branch(owner, repo_name, default_branch)
else:
self.logger.warning(f"Could not determine a default branch for '{repo_name}'. Please set it manually.")
self.vcs_client.set_default_branch(dest_repo["owner"], base_repo_name, default_branch)
else: self.logger.warning(f"Could not determine a default branch for '{base_repo_name}'. Please set it manually.")
except (GitCommandError, Exception) as e:
self.logger.error(f"Failed to import repository '{repo_name}': {e}", exc_info=True)
raise
self.logger.error(f"Failed to import repository '{repo_name}': {e}", exc_info=True); raise
finally:
if local_repo_path.exists(): shutil.rmtree(local_repo_path, onerror=_remove_readonly)
if local_repo_path.exists(): robust_rmtree(local_repo_path)
self.logger.info("Import process finished.")

View File

@ -23,6 +23,8 @@ from ..utility import logger as logger_util
def format_size(size_kb: int) -> str:
"""Formats size in kilobytes to a human-readable string (KB, MB, GB)."""
if not size_kb or size_kb <= 0:
return "N/A"
if size_kb < 1024:
return f"{size_kb} KB"
elif size_kb < 1024 * 1024:
@ -105,7 +107,7 @@ class BaseTab(ttk.Frame):
class ExportTab(BaseTab):
"""Tab for fetching repos from a server and exporting them to bundles."""
"""Tab for fetching repos and wikis from a server and exporting them."""
def __init__(self, parent, app_instance, **kwargs):
super().__init__(parent, app_instance, "ExportTab", **kwargs)
self.repositories = []
@ -121,23 +123,28 @@ class ExportTab(BaseTab):
top_frame.pack(side="top", fill="x")
selector_frame = self.create_profile_selector(top_frame, "Export from Server:")
selector_frame.pack(side="left")
self.fetch_button = ttk.Button(top_frame, text="Fetch Repositories", command=self._start_repo_fetch)
self.fetch_button = ttk.Button(top_frame, text="Fetch Items", command=self._start_repo_fetch)
self.fetch_button.pack(side="left", padx=10)
self.fetch_button.state(['disabled'])
self.export_button = ttk.Button(top_frame, text="Export Selected", command=self._start_export)
self.export_button.pack(side="left", padx=5)
self.export_button.state(['disabled'])
tree_frame = ttk.Frame(self)
tree_frame.pack(expand=True, fill="both", pady=5)
columns = ("name", "size", "description", "private")
columns = ("name", "type", "size", "description")
self.repo_tree = ttk.Treeview(tree_frame, columns=columns, show="headings", selectmode="extended")
self.repo_tree.heading("name", text="Name"); self.repo_tree.heading("size", text="Size (est.)"); self.repo_tree.heading("description", text="Description"); self.repo_tree.heading("private", text="Private")
self.repo_tree.column("name", width=200, stretch=tk.NO); self.repo_tree.column("size", width=100, stretch=tk.NO, anchor="e"); self.repo_tree.column("description", width=500); self.repo_tree.column("private", width=80, stretch=tk.NO, anchor="center")
self.repo_tree.heading("name", text="Name"); self.repo_tree.heading("type", text="Type"); self.repo_tree.heading("size", text="Size (est.)"); self.repo_tree.heading("description", text="Description")
self.repo_tree.column("name", width=250, stretch=tk.NO); self.repo_tree.column("type", width=80, stretch=tk.NO, anchor="center"); self.repo_tree.column("size", width=100, stretch=tk.NO, anchor="e"); self.repo_tree.column("description", width=500)
vsb = ttk.Scrollbar(tree_frame, orient="vertical", command=self.repo_tree.yview)
self.repo_tree.configure(yscrollcommand=vsb.set)
vsb.pack(side="right", fill="y")
self.repo_tree.pack(side="left", expand=True, fill="both")
self.repo_tree.bind("<<TreeviewSelect>>", self._update_selection_info)
info_frame = ttk.LabelFrame(self, text="Selection Info", padding="5")
info_frame.pack(side="bottom", fill="x", pady=(5, 0))
selection_buttons_frame = ttk.Frame(info_frame)
@ -168,7 +175,7 @@ class ExportTab(BaseTab):
def _start_repo_fetch(self):
if not self.active_client: return
self.logger.info(f"Fetching repository list from '{self.selected_profile_name.get()}'...")
self.logger.info(f"Fetching items list from '{self.selected_profile_name.get()}'...")
self.fetch_button.state(['disabled']); self.export_button.state(['disabled'])
threading.Thread(target=self._load_repositories_thread, daemon=True).start()
@ -178,16 +185,24 @@ class ExportTab(BaseTab):
self.repo_data_map = {repo['name']: repo for repo in self.repositories}
self.app.root.after(10, self._update_repo_treeview)
except Exception as e:
self.logger.error(f"Failed to fetch repositories: {e}", exc_info=True)
self.app.root.after(10, lambda: messagebox.showerror("Fetch Error", f"Failed to fetch repositories:\n{e}"))
self.logger.error(f"Failed to fetch items: {e}", exc_info=True)
self.app.root.after(10, lambda: messagebox.showerror("Fetch Error", f"Failed to fetch items:\n{e}"))
finally:
self.app.root.after(10, lambda: self.fetch_button.state(['!disabled']))
def _update_repo_treeview(self):
self.repo_tree.delete(*self.repo_tree.get_children())
for repo in self.repositories:
self.repo_tree.insert("", "end", values=(repo["name"], format_size(repo.get("size_kb", 0)), repo.get("description", ""), "Yes" if repo.get("private") else "No"), iid=repo["name"])
self.logger.info(f"Displayed {len(self.repositories)} repositories.")
# Sort to show wikis directly after their parent repository
sorted_repos = sorted(self.repositories, key=lambda x: x['name'].replace(" (Wiki)", " (Wiki)Z"))
for repo in sorted_repos:
repo_type = "Wiki" if repo.get("is_wiki") else "Repository"
self.repo_tree.insert("", "end", values=(
repo["name"],
repo_type,
format_size(repo.get("size_kb", 0)),
repo.get("description", "")
), iid=repo["name"])
self.logger.info(f"Displayed {len(self.repositories)} items.")
self.export_button.state(['!disabled'] if self.repositories else ['disabled'])
self._update_selection_info()
@ -195,10 +210,10 @@ class ExportTab(BaseTab):
selected_iids = self.repo_tree.selection()
if not selected_iids: return
if not self.sync_bundle_path: return
repos_to_export = [repo for repo in self.repositories if repo["name"] in selected_iids]
repos_to_export = [self.repo_data_map[iid] for iid in selected_iids if iid in self.repo_data_map]
sync_manager = SyncManager(self.app.git_manager, self.active_client, self.sync_bundle_path, logging.getLogger("SyncManager"))
self.logger.info(f"Starting export for {len(repos_to_export)} repositories...")
self.logger.info(f"Starting export for {len(repos_to_export)} selected items...")
self.export_button.state(['disabled']); self.fetch_button.state(['disabled'])
self.total_export_size = 0
self.app.status_bar_progress.config(value=0)
@ -229,12 +244,12 @@ class ExportTab(BaseTab):
class ImportTab(BaseTab):
# This class remains unchanged functionally
def __init__(self, parent, app_instance, **kwargs):
super().__init__(parent, app_instance, "ImportTab", **kwargs)
self.comparison_results = []
self._create_widgets()
self.update_profile_list()
def _create_widgets(self):
top_frame = ttk.Frame(self, padding="5"); top_frame.pack(side="top", fill="x")
selector_frame = self.create_profile_selector(top_frame, "Import to Server:"); selector_frame.pack(side="left")
@ -246,57 +261,39 @@ class ImportTab(BaseTab):
self.bundle_tree.column("name", width=300); self.bundle_tree.column("status", width=200, anchor="center")
for state in SyncState: self.bundle_tree.tag_configure(state.name, background=self._get_color_for_state(state))
vsb = ttk.Scrollbar(tree_frame, orient="vertical", command=self.bundle_tree.yview); self.bundle_tree.configure(yscrollcommand=vsb.set); vsb.pack(side="right", fill="y"); self.bundle_tree.pack(side="left", expand=True, fill="both")
def _get_color_for_state(self, state): return {SyncState.AHEAD: "lightblue", SyncState.NEW_REPO: "lightgreen", SyncState.BEHIND: "khaki", SyncState.DIVERGED: "salmon", SyncState.ERROR: "lightgrey", SyncState.IDENTICAL: "white"}.get(state, "white")
def _on_profile_select(self, event=None): super()._on_profile_select(event); self.check_status_button.state(['!disabled'] if self.active_client else ['disabled']); self.import_button.state(['disabled']); self.bundle_tree.delete(*self.bundle_tree.get_children())
def _start_bundle_check(self):
if not self.active_client or not self.sync_bundle_path: return
sync_manager = SyncManager(self.app.git_manager, self.active_client, self.sync_bundle_path, logging.getLogger("SyncManager"))
self.logger.info("Checking bundle status..."); self.check_status_button.state(['disabled'])
threading.Thread(target=self._check_status_thread, args=(sync_manager,), daemon=True).start()
def _check_status_thread(self, sync_manager):
try:
self.comparison_results = sync_manager.compare_bundles_with_remote()
self.app.root.after(10, self._update_bundle_treeview)
except Exception as e:
self.logger.error(f"Failed to compare bundle status: {e}", exc_info=True)
self.app.root.after(10, lambda: messagebox.showerror("Comparison Error", f"Failed to compare bundle status:\n{e}"))
finally:
self.app.root.after(10, lambda: self.check_status_button.state(['!disabled']))
except Exception as e: self.logger.error(f"Failed to compare bundle status: {e}", exc_info=True); self.app.root.after(10, lambda: messagebox.showerror("Comparison Error", f"Failed to compare bundle status:\n{e}"))
finally: self.app.root.after(10, lambda: self.check_status_button.state(['!disabled']))
def _update_bundle_treeview(self):
self.bundle_tree.delete(*self.bundle_tree.get_children())
for result in self.comparison_results:
self.bundle_tree.insert("", "end", values=(result["name"], result["state"].name), tags=(result["state"].name,), iid=result["name"])
for result in self.comparison_results: self.bundle_tree.insert("", "end", values=(result["name"], result["state"].name), tags=(result["state"].name,), iid=result["name"])
self.import_button.state(['!disabled'] if self.comparison_results else ['disabled'])
def _start_import(self):
selected_iids = self.bundle_tree.selection()
if not selected_iids: return
repos_to_import = [r for r in self.comparison_results if r["name"] in selected_iids and r["state"] in [SyncState.AHEAD, SyncState.NEW_REPO]]
if not repos_to_import:
messagebox.showwarning("Invalid Selection", "Please select repositories that are 'AHEAD' or 'NEW_REPO' to import.")
return
if not repos_to_import: messagebox.showwarning("Invalid Selection", "Please select repositories that are 'AHEAD' or 'NEW_REPO' to import."); return
sync_manager = SyncManager(self.app.git_manager, self.active_client, self.sync_bundle_path, logging.getLogger("SyncManager"))
self.logger.info(f"Starting import for {len(repos_to_import)} repositories...")
self.import_button.state(['disabled']); self.check_status_button.state(['disabled'])
# --- MODIFICATION START ---
# Using a lambda in the target to ensure the arguments are passed correctly and clearly.
thread_target = lambda: self._import_thread(sync_manager, repos_to_import)
threading.Thread(target=thread_target, daemon=True).start()
# --- MODIFICATION END ---
def _import_thread(self, sync_manager, repos_to_import):
try:
sync_manager.import_repositories_from_bundles(repos_to_import)
self.app.root.after(0, lambda: messagebox.showinfo("Success", "Import process completed."))
except Exception as e:
self.logger.error(f"Import failed: {e}", exc_info=True)
# Pass the actual exception to the messagebox for a more informative error.
self.app.root.after(0, lambda: messagebox.showerror("Import Error", f"An error occurred: {e}"))
finally:
self.app.root.after(0, lambda: self.import_button.state(['!disabled']))