modificata logica per creare differ senza creare nuova baseline, cambiata colorazione tabella
This commit is contained in:
parent
6476810001
commit
d211a7d354
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
21
todo.md
@ -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
84
tools/inspect_counts.py
Normal 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()
|
||||
Loading…
Reference in New Issue
Block a user