fix import, add wiki export
This commit is contained in:
parent
c262ab7680
commit
8e68e15937
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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):
|
||||
|
||||
@ -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}'...")
|
||||
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)
|
||||
}
|
||||
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
|
||||
@ -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:
|
||||
|
||||
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.")
|
||||
@ -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']))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user