modificata logica per creare differ senza creare nuova baseline, cambiata colorazione tabella

This commit is contained in:
VALLONGOL 2025-12-01 14:08:33 +01:00
parent 6476810001
commit d211a7d354
5 changed files with 313 additions and 42 deletions

View File

@ -257,6 +257,18 @@ def analyze_file_counts(path: Path) -> Dict[str, Any]:
}
)
# Sanity check: if pygount reports zero physical lines but file
# actually contains bytes, treat this as an error and fall back
# to the simple reader. This guards against encoding/pygount
# failures that return bogus zeroed results.
try:
norm_len = len(norm) if 'norm' in locals() and norm is not None else 0
if norm_len > 0 and int(result.get("physical_lines", 0)) == 0:
raise RuntimeError("pygount produced zero lines for non-empty file")
except Exception:
# Force fallback path by raising to outer except
raise
# Cache the result (using only normalized hash as key)
if content_hash:
with _CACHE_LOCK:

View File

@ -415,6 +415,7 @@ class ActionHandlers:
# Show summary dialog
self._show_differ_summary_dialog(result, baseline_id, bdir)
self.app.log(f"New baseline created: {baseline_id}", level="INFO")
except Exception:
self.app._set_phase("Idle")
self.app._current_task_id = None
@ -428,29 +429,16 @@ class ActionHandlers:
level="INFO",
)
self.app.export_btn.config(state="normal")
self.app._set_phase("Idle")
self.app._current_task_id = None
self.app._enable_action_buttons()
# Show summary dialog first (non-blocking)
self._show_differ_summary_with_baseline_prompt(result, bm, project, ignore_patterns, profile_name, max_keep, _on_create_done)
# Create new baseline after successful diff
try:
self.app._set_phase("Creating baseline...")
self.app._current_task_id = self.app.worker.submit(
bm.create_baseline_from_dir,
project,
None,
True,
True,
ignore_patterns,
profile_name,
max_keep,
kind="thread",
on_done=_on_create_done,
)
self.app.cancel_btn.config(state="normal")
except Exception:
self.app._set_phase("Idle")
self.app._current_task_id = None
self.app._enable_action_buttons()
except Exception as e:
messagebox.showerror("Differ Error", str(e))
self.app._set_phase("Idle")
self.app._enable_action_buttons()
# If no baseline exists, create the first baseline without diffing
@ -1159,16 +1147,26 @@ class ActionHandlers:
def _configure_tree_tags(self):
"""Configure Treeview tags for coloring delta values."""
# Positive changes (increases) - green tones
# Positive changes (increases) - green tones with light green background
self.app.results_tree.tag_configure(
"positive", foreground="#007700", font=("TkDefaultFont", 9, "bold")
"positive",
foreground="#006600",
background="#e8f5e8",
font=("TkDefaultFont", 9, "bold")
)
# Negative changes (decreases) - red tones
# Negative changes (decreases) - red tones with light red background
self.app.results_tree.tag_configure(
"negative", foreground="#cc0000", font=("TkDefaultFont", 9, "bold")
"negative",
foreground="#cc0000",
background="#ffe8e8",
font=("TkDefaultFont", 9, "bold")
)
# Zero/neutral - default gray
self.app.results_tree.tag_configure(
"neutral",
foreground="#666666",
background="#f5f5f5"
)
# Zero/neutral - default
self.app.results_tree.tag_configure("neutral", foreground="#666666")
def _apply_delta_tags(self, item_id, countings_delta, metrics_delta, counts=None):
"""
@ -1269,8 +1267,119 @@ class ActionHandlers:
for c in self.app.results_tree.get_children(""):
self.app.results_tree.delete(c)
def _show_differ_summary_with_baseline_prompt(self, result, bm, project, ignore_patterns, profile_name, max_keep, on_create_done_callback):
"""Show summary dialog and then ask if user wants to create a new baseline."""
import subprocess
dlg = tk.Toplevel(self.app)
dlg.title("Differ Summary")
dlg.geometry("700x650")
dlg.transient(self.app)
# Center dialog
dlg.update_idletasks()
pw = self.app.winfo_width()
ph = self.app.winfo_height()
px = self.app.winfo_rootx()
py = self.app.winfo_rooty()
dw = 700
dh = 650
x = px + (pw - dw) // 2
y = py + (ph - dh) // 2
dlg.geometry(f"{dw}x{dh}+{x}+{y}")
# Title
title_frame = ttk.Frame(dlg)
title_frame.pack(fill="x", padx=10, pady=10)
ttk.Label(
title_frame,
text="Differ Summary",
font=("Arial", 12, "bold"),
).pack()
# Text widget with scrollbar for summary
text_frame = ttk.Frame(dlg)
text_frame.pack(fill="both", expand=True, padx=10, pady=(0, 10))
scrollbar = ttk.Scrollbar(text_frame)
scrollbar.pack(side="right", fill="y")
summary_text = tk.Text(
text_frame,
wrap="none",
font=("Courier", 9),
yscrollcommand=scrollbar.set,
height=25,
width=80,
)
summary_text.pack(side="left", fill="both", expand=True)
scrollbar.config(command=summary_text.yview)
# Generate summary content
baseline_id = result.get("baseline_id", "unknown")
summary_lines = self._generate_summary_text(result, baseline_id)
summary_text.insert("1.0", "\n".join(summary_lines))
summary_text.config(state="disabled")
# Store summary for clipboard
summary_content = "\n".join(summary_lines)
# Buttons frame at bottom
btn_frame = ttk.Frame(dlg)
btn_frame.pack(fill="x", padx=10, pady=(0, 10))
def copy_to_clipboard():
self.app.clipboard_clear()
self.app.clipboard_append(summary_content)
messagebox.showinfo("Copied", "Summary copied to clipboard!", parent=dlg)
def close_and_ask_baseline():
dlg.destroy()
# Ask user if they want to save as new baseline
response = messagebox.askyesno(
"Save as Baseline",
"Do you want to save the current state as a new baseline?\n\n"
"• Yes: Create a new baseline for future comparisons\n"
"• No: Keep only the analysis results without saving",
icon="question"
)
if response:
# User chose to create a new baseline
try:
self.app._set_phase("Creating baseline...")
self.app._disable_action_buttons()
self.app._current_task_id = self.app.worker.submit(
bm.create_baseline_from_dir,
project,
None,
True,
True,
ignore_patterns,
profile_name,
max_keep,
kind="thread",
on_done=on_create_done_callback,
)
self.app.cancel_btn.config(state="normal")
except Exception as e:
self.app.log(f"Failed to create baseline: {e}", level="ERROR")
self.app._set_phase("Idle")
self.app._current_task_id = None
self.app._enable_action_buttons()
else:
# User chose not to create a baseline
self.app.log("Differ analysis completed without creating new baseline", level="INFO")
ttk.Button(
btn_frame, text="📋 Copy to Clipboard", command=copy_to_clipboard
).pack(side="left", padx=5)
ttk.Button(btn_frame, text="✅ Close", command=close_and_ask_baseline).pack(
side="right", padx=5
)
def _show_differ_summary_dialog(self, result, baseline_id, baseline_dir):
"""Show summary dialog with differ results."""
"""Show summary dialog with differ results (after baseline creation)."""
import subprocess
dlg = tk.Toplevel(self.app)

View File

@ -98,10 +98,12 @@ class App(tk.Tk):
self.results_tree = ttk.Treeview(
results_frame, columns=self.results_columns, show="headings"
)
self.results_tree.heading("name", text="File")
self.results_tree.heading("path", text="Path")
self.results_tree.column("name", width=400, anchor="w")
self.results_tree.column("path", width=600, anchor="w")
# initialize sort state
self._sort_column = None
self._sort_reverse = False
# Configure columns via helper to enable clickable headings
self._set_results_columns(self.results_columns)
self.results_tree.grid(row=0, column=0, sticky="nsew")
vsb_r = ttk.Scrollbar(
results_frame, orient="vertical", command=self.results_tree.yview
@ -273,7 +275,10 @@ class App(tk.Tk):
self.results_tree.config(columns=cols)
self._column_tooltips = tooltips or {}
for cid in cols:
self.results_tree.heading(cid, text=cid.title())
# set heading text and attach click handler for sorting
self.results_tree.heading(
cid, text=cid.title(), command=lambda c=cid: self._on_heading_click(c)
)
self.results_tree.column(cid, width=120, anchor="w")
def _on_tree_motion(self, event):
@ -324,6 +329,66 @@ class App(tk.Tk):
self._column_tooltip.destroy()
self._column_tooltip = None
def _on_heading_click(self, col_id):
"""Handle click on a column heading: sort rows by that column.
Click toggles between ascending and descending when repeated on same column.
"""
cols = list(self.results_tree["columns"])
try:
col_index = cols.index(col_id)
except ValueError:
return
# toggle or set sort order
if self._sort_column == col_id:
self._sort_reverse = not self._sort_reverse
else:
self._sort_column = col_id
self._sort_reverse = False
# collect items and values
items = list(self.results_tree.get_children(""))
def convert_val(v):
if v is None:
return ""
try:
# try integer
return int(v)
except Exception:
try:
return float(v)
except Exception:
return str(v).lower()
decorated = []
for it in items:
try:
val = self.results_tree.set(it, col_id)
except Exception:
val = ""
decorated.append((convert_val(val), it))
decorated.sort(key=lambda x: x[0], reverse=self._sort_reverse)
# reposition items
for index, (_val, it) in enumerate(decorated):
try:
self.results_tree.move(it, "", index)
except Exception:
pass
# update heading visuals (arrow)
for cid in cols:
txt = cid.title()
if cid == self._sort_column:
txt += "" if not self._sort_reverse else ""
try:
self.results_tree.heading(cid, text=txt, command=lambda c=cid: self._on_heading_click(c))
except Exception:
pass
def _on_results_double_click(self, event):
"""Gestisce il doppio click su una riga della tabella results."""
# Ottieni la riga selezionata

21
todo.md
View File

@ -1,6 +1,6 @@
# TODO List
- [ ] creare report veloce che fornisca alla fine dell'analisi i seguenti dati inviare per kpi: linee logiche codice totali, quelle cancellate, quelle modificate, quelle nuove
- [x] creare report veloce che fornisca alla fine dell'analisi i seguenti dati inviare per kpi: linee logiche codice totali, quelle cancellate, quelle modificate, quelle nuove
- [x] barra di progresso lunga tutta la finestra
- [x] aggiungere i contatori in basso sotto la tabella di result
- [x] la barra di progresso deve stare nella parte bassa della schermata, in una status bar con indicata anche la fase in corso
@ -11,16 +11,17 @@
- [x] la baseline per il differ dovrebbero essere salvate in una cartella locale dove si trova PYUcc e non nella cartella del progetto, per evitare di sporcare il contenuto di repository ed altro., Quindi secondo me sarebbe utile permettere all'utente di configurare una cartella di destinazione, di default la puoiò creare nella cartella dove gira il software che si chiama "baseline"
- [x] fare in modo di avere nella cartelkle delle baseline al massimo le ultime x baseline per ogni progetto
- [x] il file delle baseline deve avere un nome che contiene il nome del profilo di definizione.
- [ ] mettere una hint sulle colonne che se mi muovo su di una determinata colonna mi dica di cosa si tratta
- [ ] poter ordinare la tabella cliccando sulla colonna sia in ordine crescente che descrescente
- [x] mettere una hint sulle colonne che se mi muovo su di una determinata colonna mi dica di cosa si tratta
- [x] poter ordinare la tabella cliccando sulla colonna sia in ordine crescente che descrescente
- [ ] aggiungere schermata di debug dove provare le singole funzioni su singolo file.
# FIXME List
- [ ] il contatore non viene aggiornato quando uso la funzione di scanner
- [ ] il contatore di file deve essere azzerato al lancio di ogni funzione
- [x] il contatore non viene aggiornato quando uso la funzione di scanner
- [x] il contatore di file deve essere azzerato al lancio di ogni funzione
- [x] eliminare i log troppo verboso
- [ ] completare la tabella anche con le metriche
- [ ] colorare la tabella
- [ ] salvare il file delle diff in automatico
- [ ] mettere le hint per spiegare i vari parametri cosa sono
- [ ] ordinare le righe selezionando la colonna sia in ordine screscente che descrescente
- [x] completare la tabella anche con le metriche
- [x] colorare la tabella
- [x] salvare il file delle diff in automatico
- [x] mettere le hint per spiegare i vari parametri cosa sono
- [x] ordinare le righe selezionando la colonna sia in ordine screscente che descrescente

84
tools/inspect_counts.py Normal file
View File

@ -0,0 +1,84 @@
"""Inspect counting results and normalized hashes for two files.
Usage:
python tools/inspect_counts.py <path_to_current_file> <path_to_baseline_file>
Example:
python tools/inspect_counts.py pyucc/gui/gui.py baseline/pyucc__20251128T121455_local/files/pyucc/gui/gui.py
"""
from pathlib import Path
import hashlib
import sys
import json
def normalize_bytes(b: bytes) -> bytes:
if b.startswith(b"\xef\xbb\xbf"):
b = b[3:]
b = b.replace(b"\r\n", b"\n")
b = b.replace(b"\r", b"\n")
return b
def md5(b: bytes) -> str:
return hashlib.md5(b).hexdigest()
def phys_lines(b: bytes) -> int:
nb = normalize_bytes(b)
if len(nb) == 0:
return 0
return nb.count(b"\n") + (0 if nb.endswith(b"\n") else 1)
def load_bytes(p: Path):
try:
with p.open("rb") as fh:
return fh.read()
except Exception as e:
print(f"Failed to read {p}: {e}")
return None
def analyze_path(p: Path):
b = load_bytes(p)
if b is None:
return None
nb = normalize_bytes(b)
return {
"path": str(p),
"size": len(b),
"raw_md5": md5(b),
"norm_md5": md5(nb),
"phys_lines": phys_lines(b),
}
def print_countings(p: Path):
try:
from pyucc.core.countings_impl import analyze_file_counts
except Exception as e:
print(f"Cannot import analyze_file_counts: {e}")
analyze_file_counts = None
info = analyze_path(p)
if info is None:
print(f"No info for {p}")
return
print(json.dumps(info, indent=2))
if analyze_file_counts is not None:
try:
c = analyze_file_counts(p)
print("countings:", json.dumps(c, indent=2))
except Exception as e:
print(f"analyze_file_counts failed: {e}")
def main():
if len(sys.argv) < 2:
print("Usage: python tools/inspect_counts.py <current_path> [<baseline_path>]")
sys.exit(2)
cur = Path(sys.argv[1])
base = Path(sys.argv[2]) if len(sys.argv) > 2 else None
print("\n=== Current file ===")
print_countings(cur)
if base:
print("\n=== Baseline file ===")
print_countings(base)
if __name__ == "__main__":
main()