aggiornarta la funzione di compare e la visualizzazione gui
This commit is contained in:
parent
ae53019b23
commit
3beb36b584
@ -74,7 +74,7 @@ class CodeComparer:
|
|||||||
hash_sha256.update(chunk)
|
hash_sha256.update(chunk)
|
||||||
return hash_sha256.hexdigest()
|
return hash_sha256.hexdigest()
|
||||||
|
|
||||||
def get_relative_files(self, root_path: str) -> Dict[str, str]:
|
def get_relative_files(self, root_path: str, progress_callback=None) -> Dict[str, str]:
|
||||||
"""
|
"""
|
||||||
Walks through the directory and returns a dict of relative paths and hashes.
|
Walks through the directory and returns a dict of relative paths and hashes.
|
||||||
Filters out ignored patterns.
|
Filters out ignored patterns.
|
||||||
@ -89,23 +89,48 @@ class CodeComparer:
|
|||||||
full_path = os.path.join(root, file)
|
full_path = os.path.join(root, file)
|
||||||
relative_path = os.path.relpath(full_path, root_path)
|
relative_path = os.path.relpath(full_path, root_path)
|
||||||
file_map[relative_path] = full_path
|
file_map[relative_path] = full_path
|
||||||
|
# notify progress for files scanned in source
|
||||||
|
try:
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(root_path, full_path, relative_path)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return file_map
|
return file_map
|
||||||
|
|
||||||
def compare(self) -> Tuple[List[str], List[str], List[str]]:
|
def count_files(self, root_path: str) -> int:
|
||||||
|
"""Quickly count non-ignored files under root_path."""
|
||||||
|
total = 0
|
||||||
|
for root, dirs, files in os.walk(root_path):
|
||||||
|
dirs[:] = [d for d in dirs if not self._is_ignored(d)]
|
||||||
|
for file in files:
|
||||||
|
if self._is_ignored(file):
|
||||||
|
continue
|
||||||
|
total += 1
|
||||||
|
return total
|
||||||
|
|
||||||
|
def compare(self, progress_callback=None) -> Tuple[List[str], List[str], List[str]]:
|
||||||
"""
|
"""
|
||||||
Compares source and destination folders to find changes.
|
Compares source and destination folders to find changes.
|
||||||
"""
|
"""
|
||||||
self.added.clear()
|
self.added.clear()
|
||||||
self.modified.clear()
|
self.modified.clear()
|
||||||
self.deleted.clear()
|
self.deleted.clear()
|
||||||
source_files = self.get_relative_files(self.source_path)
|
source_files = self.get_relative_files(self.source_path, progress_callback=progress_callback)
|
||||||
dest_files = self.get_relative_files(self.destination_path)
|
dest_files = self.get_relative_files(self.destination_path, progress_callback=None)
|
||||||
source_keys: Set[str] = set(source_files.keys())
|
source_keys: Set[str] = set(source_files.keys())
|
||||||
dest_keys: Set[str] = set(dest_files.keys())
|
dest_keys: Set[str] = set(dest_files.keys())
|
||||||
self.added = sorted(list(source_keys - dest_keys))
|
self.added = sorted(list(source_keys - dest_keys))
|
||||||
self.deleted = sorted(list(dest_keys - source_keys))
|
self.deleted = sorted(list(dest_keys - source_keys))
|
||||||
common_files = source_keys.intersection(dest_keys)
|
common_files = source_keys.intersection(dest_keys)
|
||||||
|
# perform hashing comparison and report progress per file
|
||||||
for relative_path in common_files:
|
for relative_path in common_files:
|
||||||
|
# notify that we are hashing this file
|
||||||
|
try:
|
||||||
|
if progress_callback:
|
||||||
|
# indicate hashing phase with a simple tuple
|
||||||
|
progress_callback(("hash", relative_path))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
source_hash = self.compute_hash(source_files[relative_path])
|
source_hash = self.compute_hash(source_files[relative_path])
|
||||||
dest_hash = self.compute_hash(dest_files[relative_path])
|
dest_hash = self.compute_hash(dest_files[relative_path])
|
||||||
if source_hash != dest_hash:
|
if source_hash != dest_hash:
|
||||||
|
|||||||
@ -3,6 +3,7 @@ from tkinter import ttk, filedialog, messagebox
|
|||||||
from tkinter.scrolledtext import ScrolledText
|
from tkinter.scrolledtext import ScrolledText
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
import threading
|
||||||
|
|
||||||
# Externals (made importable by __main__.py adding their folders to sys.path)
|
# Externals (made importable by __main__.py adding their folders to sys.path)
|
||||||
try:
|
try:
|
||||||
@ -31,7 +32,13 @@ class MainWindow:
|
|||||||
def __init__(self, root: tk.Tk):
|
def __init__(self, root: tk.Tk):
|
||||||
self.root = root
|
self.root = root
|
||||||
self.root.title("CodeBridge - Codebase Synchronizer")
|
self.root.title("CodeBridge - Codebase Synchronizer")
|
||||||
self.root.geometry("900x600")
|
# Make the window taller by default so status/log area is visible
|
||||||
|
self.root.geometry("1000x760")
|
||||||
|
# Enforce a reasonable minimum size so status bar remains visible
|
||||||
|
try:
|
||||||
|
self.root.minsize(900, 600)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
self.comparer = None
|
self.comparer = None
|
||||||
self.sync_manager = SyncManager()
|
self.sync_manager = SyncManager()
|
||||||
# Initialize optional externals
|
# Initialize optional externals
|
||||||
@ -45,18 +52,19 @@ class MainWindow:
|
|||||||
except Exception:
|
except Exception:
|
||||||
self.logger_sys = None
|
self.logger_sys = None
|
||||||
|
|
||||||
# Resource monitor
|
# Resource monitor (separate var) and progress status var
|
||||||
self._resource_monitor = None
|
self._resource_monitor = None
|
||||||
self.status_var = tk.StringVar()
|
self.resource_var = tk.StringVar()
|
||||||
|
self.progress_status_var = tk.StringVar()
|
||||||
if TkinterResourceMonitor is not None and is_psutil_available():
|
if TkinterResourceMonitor is not None and is_psutil_available():
|
||||||
try:
|
try:
|
||||||
self._resource_monitor = TkinterResourceMonitor(self.root, self.status_var, poll_interval=1.0)
|
self._resource_monitor = TkinterResourceMonitor(self.root, self.resource_var, poll_interval=1.0)
|
||||||
self._resource_monitor.start()
|
self._resource_monitor.start()
|
||||||
except Exception:
|
except Exception:
|
||||||
self._resource_monitor = None
|
self._resource_monitor = None
|
||||||
else:
|
else:
|
||||||
# psutil not available -> show note
|
# psutil not available -> show note on resource area
|
||||||
self.status_var.set("Resource monitor: psutil not installed")
|
self.resource_var.set("Resource monitor: psutil not installed")
|
||||||
|
|
||||||
# Profiles: load/save path in program root
|
# Profiles: load/save path in program root
|
||||||
try:
|
try:
|
||||||
@ -107,15 +115,38 @@ class MainWindow:
|
|||||||
self.dst_path_var = tk.StringVar()
|
self.dst_path_var = tk.StringVar()
|
||||||
self._create_path_row(path_frame, "Source (New):", self.src_path_var, 0)
|
self._create_path_row(path_frame, "Source (New):", self.src_path_var, 0)
|
||||||
self._create_path_row(path_frame, "Destination (Old):", self.dst_path_var, 1)
|
self._create_path_row(path_frame, "Destination (Old):", self.dst_path_var, 1)
|
||||||
# Action buttons
|
# Action buttons inside a labelled frame and filters to the right
|
||||||
btn_frame = ttk.Frame(self.root, padding=5)
|
actions_row = ttk.Frame(self.root, padding=5)
|
||||||
btn_frame.pack(fill="x", padx=10)
|
actions_row.pack(fill="x", padx=10)
|
||||||
compare_btn = ttk.Button(btn_frame, text="🔍 Compare Folders", command=self._run_comparison)
|
actions_frame = ttk.LabelFrame(actions_row, text="Actions", padding=6)
|
||||||
compare_btn.pack(side="left", padx=5)
|
actions_frame.pack(side="left", fill="x", expand=True)
|
||||||
export_btn = ttk.Button(btn_frame, text="📦 Export Changes (ZIP)", command=self._export_changes)
|
self.compare_btn = ttk.Button(actions_frame, text="🔍 Compare Folders", command=self._run_comparison)
|
||||||
export_btn.pack(side="left", padx=5)
|
self.compare_btn.pack(side="left", padx=5)
|
||||||
import_btn = ttk.Button(btn_frame, text="📥 Import Package (ZIP)", command=self._import_package)
|
self.export_btn = ttk.Button(actions_frame, text="📦 Export Changes (ZIP)", command=self._export_changes)
|
||||||
import_btn.pack(side="left", padx=5)
|
self.export_btn.pack(side="left", padx=5)
|
||||||
|
self.import_btn = ttk.Button(actions_frame, text="📥 Import Package (ZIP)", command=self._import_package)
|
||||||
|
self.import_btn.pack(side="left", padx=5)
|
||||||
|
|
||||||
|
# Filter panel on the right: show counts and allow toggling
|
||||||
|
filter_frame = ttk.Frame(actions_row, padding=(6,0))
|
||||||
|
filter_frame.pack(side="right")
|
||||||
|
self.filter_vars = {
|
||||||
|
"added": tk.BooleanVar(value=False),
|
||||||
|
"modified": tk.BooleanVar(value=False),
|
||||||
|
"deleted": tk.BooleanVar(value=False),
|
||||||
|
}
|
||||||
|
# buttons that show counts; initial counts are 0
|
||||||
|
self.filter_buttons = {}
|
||||||
|
self.filter_buttons["added"] = ttk.Button(filter_frame, text="Added (0)", command=lambda: self._toggle_filter("added"))
|
||||||
|
self.filter_buttons["added"].pack(side="left", padx=4)
|
||||||
|
self.filter_buttons["modified"] = ttk.Button(filter_frame, text="Modified (0)", command=lambda: self._toggle_filter("modified"))
|
||||||
|
self.filter_buttons["modified"].pack(side="left", padx=4)
|
||||||
|
self.filter_buttons["deleted"] = ttk.Button(filter_frame, text="Deleted (0)", command=lambda: self._toggle_filter("deleted"))
|
||||||
|
self.filter_buttons["deleted"].pack(side="left", padx=4)
|
||||||
|
# store last results for filtering
|
||||||
|
self.last_added = []
|
||||||
|
self.last_modified = []
|
||||||
|
self.last_deleted = []
|
||||||
# Results treeview
|
# Results treeview
|
||||||
self._setup_treeview()
|
self._setup_treeview()
|
||||||
|
|
||||||
@ -150,6 +181,10 @@ class MainWindow:
|
|||||||
"""
|
"""
|
||||||
tree_frame = ttk.Frame(self.root, padding=10)
|
tree_frame = ttk.Frame(self.root, padding=10)
|
||||||
tree_frame.pack(fill="both", expand=True)
|
tree_frame.pack(fill="both", expand=True)
|
||||||
|
# Progress bar for long-running operations
|
||||||
|
self.progress_var = tk.IntVar(value=0)
|
||||||
|
self.progress = ttk.Progressbar(tree_frame, variable=self.progress_var, maximum=100, mode="determinate")
|
||||||
|
self.progress.pack(fill="x", pady=(0,6))
|
||||||
columns = ("status", "path")
|
columns = ("status", "path")
|
||||||
self.tree = ttk.Treeview(tree_frame, columns=columns, show="headings")
|
self.tree = ttk.Treeview(tree_frame, columns=columns, show="headings")
|
||||||
self.tree.heading("status", text="Status")
|
self.tree.heading("status", text="Status")
|
||||||
@ -167,13 +202,89 @@ class MainWindow:
|
|||||||
# Persistent log box below the treeview
|
# Persistent log box below the treeview
|
||||||
log_frame = ttk.Frame(self.root, padding=(5, 2))
|
log_frame = ttk.Frame(self.root, padding=(5, 2))
|
||||||
log_frame.pack(fill="x")
|
log_frame.pack(fill="x")
|
||||||
self.log_text = ScrolledText(log_frame, height=8, state=tk.DISABLED)
|
# Slightly reduce log height to keep status bar visible on smaller screens
|
||||||
|
self.log_text = ScrolledText(log_frame, height=6, state=tk.DISABLED)
|
||||||
self.log_text.pack(fill="both", expand=True)
|
self.log_text.pack(fill="both", expand=True)
|
||||||
|
|
||||||
# Status bar at bottom
|
# Status bar at bottom: left = progress/status, right = resource monitor
|
||||||
status_frame = ttk.Frame(self.root, padding=(5, 2))
|
status_frame = ttk.Frame(self.root, padding=(5, 2))
|
||||||
status_frame.pack(fill="x", side="bottom")
|
status_frame.pack(fill="x", side="bottom")
|
||||||
ttk.Label(status_frame, textvariable=self.status_var).pack(side="left")
|
ttk.Label(status_frame, textvariable=self.progress_status_var).pack(side="left")
|
||||||
|
ttk.Label(status_frame, textvariable=self.resource_var).pack(side="right")
|
||||||
|
|
||||||
|
def _log(self, message: str) -> None:
|
||||||
|
"""Write message to both logging and the embedded ScrolledText log."""
|
||||||
|
try:
|
||||||
|
import logging
|
||||||
|
logging.getLogger(__name__).info(message)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _update_progress(self, value: int, maximum: int, phase: str, count_display: str = None) -> None:
|
||||||
|
try:
|
||||||
|
self.progress.config(maximum=maximum)
|
||||||
|
self.progress_var.set(value)
|
||||||
|
if count_display:
|
||||||
|
self.progress_status_var.set(f"{phase}: {count_display}")
|
||||||
|
else:
|
||||||
|
self.progress_status_var.set(f"{phase}: {value}/{maximum}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _toggle_filter(self, status: str) -> None:
|
||||||
|
"""Toggle a filter button and refresh the treeview."""
|
||||||
|
if status not in self.filter_vars:
|
||||||
|
return
|
||||||
|
cur = self.filter_vars[status].get()
|
||||||
|
self.filter_vars[status].set(not cur)
|
||||||
|
# update visual style of button to indicate active/inactive
|
||||||
|
try:
|
||||||
|
btn = self.filter_buttons.get(status)
|
||||||
|
if btn:
|
||||||
|
if self.filter_vars[status].get():
|
||||||
|
btn.state(["pressed"])
|
||||||
|
else:
|
||||||
|
btn.state(["!pressed"])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._refresh_tree()
|
||||||
|
|
||||||
|
def _refresh_tree(self) -> None:
|
||||||
|
"""Repopulates the treeview according to current filter selections."""
|
||||||
|
try:
|
||||||
|
for item in self.tree.get_children():
|
||||||
|
self.tree.delete(item)
|
||||||
|
# gather active filters
|
||||||
|
active = {k for k, v in self.filter_vars.items() if v.get()}
|
||||||
|
# if no filter active, show all
|
||||||
|
show_all = len(active) == 0
|
||||||
|
if show_all or "added" in active:
|
||||||
|
for f in self.last_added:
|
||||||
|
self.tree.insert("", "end", values=("Added", f), tags=("added",))
|
||||||
|
if show_all or "modified" in active:
|
||||||
|
for f in self.last_modified:
|
||||||
|
self.tree.insert("", "end", values=("Modified", f), tags=("modified",))
|
||||||
|
if show_all or "deleted" in active:
|
||||||
|
for f in self.last_deleted:
|
||||||
|
self.tree.insert("", "end", values=("Deleted", f), tags=("deleted",))
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"Error refreshing tree: {e}")
|
||||||
|
|
||||||
|
def _update_filter_counts(self) -> None:
|
||||||
|
"""Update the text of filter buttons to reflect current counts."""
|
||||||
|
try:
|
||||||
|
self.filter_buttons["added"].config(text=f"Added ({len(self.last_added)})")
|
||||||
|
self.filter_buttons["modified"].config(text=f"Modified ({len(self.last_modified)})")
|
||||||
|
self.filter_buttons["deleted"].config(text=f"Deleted ({len(self.last_deleted)})")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
self.log_text.config(state=tk.NORMAL)
|
||||||
|
self.log_text.insert("end", message + "\n")
|
||||||
|
self.log_text.see("end")
|
||||||
|
self.log_text.config(state=tk.DISABLED)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# --- Profiles persistence ---
|
# --- Profiles persistence ---
|
||||||
def _load_profiles(self):
|
def _load_profiles(self):
|
||||||
@ -198,11 +309,13 @@ class MainWindow:
|
|||||||
self.profile_cb.set(names[0])
|
self.profile_cb.set(names[0])
|
||||||
|
|
||||||
def _open_manage_profiles(self):
|
def _open_manage_profiles(self):
|
||||||
|
self._log("Opening Manage Profiles dialog")
|
||||||
dlg = ProfileManagerDialog(self.root, self._profiles_path, self.profiles)
|
dlg = ProfileManagerDialog(self.root, self._profiles_path, self.profiles)
|
||||||
self.root.wait_window(dlg)
|
self.root.wait_window(dlg)
|
||||||
# reload and refresh
|
# reload and refresh
|
||||||
self.profiles = self._load_profiles()
|
self.profiles = self._load_profiles()
|
||||||
self._populate_profiles()
|
self._populate_profiles()
|
||||||
|
self._log("Profiles reloaded")
|
||||||
|
|
||||||
def _close_profile_panel(self):
|
def _close_profile_panel(self):
|
||||||
try:
|
try:
|
||||||
@ -226,6 +339,7 @@ class MainWindow:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
self.current_ignore_exts = []
|
self.current_ignore_exts = []
|
||||||
|
self._log("Profile selection cleared")
|
||||||
return
|
return
|
||||||
profile = self.profiles.get(sel)
|
profile = self.profiles.get(sel)
|
||||||
if not profile:
|
if not profile:
|
||||||
@ -249,6 +363,7 @@ class MainWindow:
|
|||||||
self.dst_browse.config(state="disabled")
|
self.dst_browse.config(state="disabled")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
self._log(f"Selected profile: {sel}")
|
||||||
|
|
||||||
def _run_comparison(self) -> None:
|
def _run_comparison(self) -> None:
|
||||||
"""
|
"""
|
||||||
@ -259,16 +374,90 @@ class MainWindow:
|
|||||||
if not src or not dst:
|
if not src or not dst:
|
||||||
messagebox.showwarning("Warning", "Please select both source and destination folders.")
|
messagebox.showwarning("Warning", "Please select both source and destination folders.")
|
||||||
return
|
return
|
||||||
|
self._log(f"Starting comparison: src={src} dst={dst}")
|
||||||
self.comparer = CodeComparer(src, dst, ignore_extensions=self.current_ignore_exts)
|
self.comparer = CodeComparer(src, dst, ignore_extensions=self.current_ignore_exts)
|
||||||
added, modified, deleted = self.comparer.compare()
|
|
||||||
for item in self.tree.get_children():
|
def _disable_ui():
|
||||||
self.tree.delete(item)
|
try:
|
||||||
for f in added:
|
self.compare_btn.config(state="disabled")
|
||||||
self.tree.insert("", "end", values=("Added", f), tags=("added",))
|
self.export_btn.config(state="disabled")
|
||||||
for f in modified:
|
self.import_btn.config(state="disabled")
|
||||||
self.tree.insert("", "end", values=("Modified", f), tags=("modified",))
|
except Exception:
|
||||||
for f in deleted:
|
pass
|
||||||
self.tree.insert("", "end", values=("Deleted", f), tags=("deleted",))
|
|
||||||
|
def _enable_ui():
|
||||||
|
try:
|
||||||
|
self.compare_btn.config(state="normal")
|
||||||
|
self.export_btn.config(state="normal")
|
||||||
|
self.import_btn.config(state="normal")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def worker():
|
||||||
|
try:
|
||||||
|
# disable UI
|
||||||
|
self.root.after(0, _disable_ui)
|
||||||
|
# phase 1: quick count
|
||||||
|
try:
|
||||||
|
total = self.comparer.count_files(src)
|
||||||
|
except Exception:
|
||||||
|
total = 0
|
||||||
|
if total <= 0:
|
||||||
|
total = 1
|
||||||
|
progress_max = total * 2
|
||||||
|
# initialize progress
|
||||||
|
self.root.after(0, lambda: self._update_progress(0, progress_max, "Counting", f"0/{total}"))
|
||||||
|
|
||||||
|
scanned = 0
|
||||||
|
hashed = 0
|
||||||
|
|
||||||
|
def progress_handler(*cb_args):
|
||||||
|
nonlocal scanned, hashed
|
||||||
|
# get_relative_files calls (root_path, full_path, relative_path)
|
||||||
|
if len(cb_args) == 3:
|
||||||
|
root_path, full_path, relative_path = cb_args
|
||||||
|
if root_path == src:
|
||||||
|
scanned += 1
|
||||||
|
# update UI
|
||||||
|
self.root.after(0, lambda s=scanned: self._update_progress(s, progress_max, "Scanning", f"{s}/{total}"))
|
||||||
|
else:
|
||||||
|
# hashing phase: comparer sends a tuple like ("hash", relative_path)
|
||||||
|
arg = cb_args[0] if cb_args else None
|
||||||
|
if isinstance(arg, tuple) and arg and arg[0] == "hash":
|
||||||
|
hashed += 1
|
||||||
|
v = scanned + hashed
|
||||||
|
self.root.after(0, lambda v=v: self._update_progress(v, progress_max, "Hashing", f"{hashed}/{total}"))
|
||||||
|
|
||||||
|
# perform compare with progress handler
|
||||||
|
added, modified, deleted = self.comparer.compare(progress_callback=progress_handler)
|
||||||
|
|
||||||
|
# ensure progress complete
|
||||||
|
self.root.after(0, lambda: self._update_progress(progress_max, progress_max, "Done", f"{total}/{total}"))
|
||||||
|
# populate tree in main thread
|
||||||
|
def finish_ui():
|
||||||
|
# store results and refresh according to filters
|
||||||
|
self.last_added = added
|
||||||
|
self.last_modified = modified
|
||||||
|
self.last_deleted = deleted
|
||||||
|
self._update_filter_counts()
|
||||||
|
self._refresh_tree()
|
||||||
|
self._log(f"Comparison complete. Added={len(added)} Modified={len(modified)} Deleted={len(deleted)}")
|
||||||
|
self.progress_status_var.set("")
|
||||||
|
self.progress_var.set(0)
|
||||||
|
_enable_ui()
|
||||||
|
|
||||||
|
self.root.after(0, finish_ui)
|
||||||
|
except Exception as e:
|
||||||
|
# ensure UI enabled on error
|
||||||
|
try:
|
||||||
|
self.root.after(0, _enable_ui)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._log(f"Error during comparison: {e}")
|
||||||
|
|
||||||
|
threading.Thread(target=worker, daemon=True).start()
|
||||||
|
# Background comparison started; UI will be updated when finished.
|
||||||
|
self._log("Background comparison started")
|
||||||
|
|
||||||
def _on_item_double_click(self, event) -> None:
|
def _on_item_double_click(self, event) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user