Coverage for gui/src/version_finder_gui/gui.py: 13%
775 statements
« prev ^ index » next coverage.py v7.7.0, created at 2025-03-18 10:30 +0000
« prev ^ index » next coverage.py v7.7.0, created at 2025-03-18 10:30 +0000
1import customtkinter as ctk
2import argparse
3from pathlib import Path
4from enum import Enum, auto
5import tkinter as tk
6from typing import List, Tuple
7from tkinter import filedialog, messagebox
8import importlib.resources
9import multiprocessing
10import queue
11from version_finder.version_finder import VersionFinder
12from version_finder.common import parse_arguments
13from version_finder.logger import get_logger, configure_logging
14from version_finder_gui.widgets import AutocompleteEntry, CommitListWindow, center_window, LoadingSpinner
15import time
16import os
17from PIL import Image
19logger = get_logger()
20# Define message types for inter-process communication
22class MessageType(Enum):
23 TASK_REQUEST = auto()
24 TASK_RESULT = auto()
25 TASK_ERROR = auto()
26 SHUTDOWN = auto()
29class VersionFinderTasks(Enum):
30 FIND_VERSION = auto()
31 COMMITS_BETWEEN_VERSIONS = auto()
32 COMMITS_BY_TEXT = auto()
34# Worker process function
37def version_finder_worker(request_queue, response_queue, exit_event=None):
38 """Worker process for version finder operations"""
39 logger.info("Worker process started")
41 version_finder = None
42 waiting_for_confirmation = False
43 pending_init_task_id = None
45 while True:
46 try:
47 # Check for exit signal
48 if exit_event and exit_event.is_set():
49 logger.info("Exit event received, shutting down worker")
50 break
52 # Get next task from queue with timeout
53 try:
54 message = request_queue.get(timeout=0.5) # 500ms timeout
55 except queue.Empty:
56 continue
58 # Handle shutdown message
59 if message["type"] == MessageType.SHUTDOWN:
60 logger.info("Shutdown message received")
61 break
63 # Extract task info
64 task = message.get("task")
65 task_id = message.get("task_id")
66 args = message.get("args", {})
68 # Handle user confirmation for repository initialization
69 if task == "confirm_init":
70 if waiting_for_confirmation and pending_init_task_id:
71 logger.info("Received confirmation for repository initialization")
72 # Re-initialize with force=True
73 version_finder = VersionFinder(args["repo_path"], force=True)
74 waiting_for_confirmation = False
76 # Send success response for the original init task
77 response_queue.put({
78 "type": MessageType.TASK_RESULT,
79 "task_id": "init_repo_confirmed",
80 "result": {
81 "status": "success",
82 "original_task_id": pending_init_task_id
83 }
84 })
85 pending_init_task_id = None
86 continue
88 # Process tasks
89 try:
90 # Initialize version finder if needed
91 if task == "init_repo":
92 repo_path = args["repo_path"]
93 user_confirmation = args.get("user_confirmation", False)
94 logger.info(f"Initializing version finder with repo: {repo_path}, force: {user_confirmation}")
96 try:
97 # Initialize version finder with specified force parameter
98 version_finder = VersionFinder(repo_path, force=user_confirmation)
100 response_queue.put({
101 "type": MessageType.TASK_RESULT,
102 "task_id": task_id,
103 "result": {"status": "success"}
104 })
105 except Exception as e:
106 # Handle other initialization errors
107 response_queue.put({
108 "type": MessageType.TASK_ERROR,
109 "task_id": task_id,
110 "error": str(e),
111 "error_type": type(e).__name__
112 })
113 elif task == "update_repository":
114 if not version_finder:
115 raise ValueError("Version finder not initialized")
116 result = version_finder.update_repository(**args)
117 response_queue.put({
118 "type": MessageType.TASK_RESULT,
119 "task_id": task_id,
120 "result": result
121 })
122 elif task == "get_branches":
123 if not version_finder:
124 raise ValueError("Version finder not initialized")
125 result = version_finder.list_branches(), version_finder.updated_branch
126 response_queue.put({
127 "type": MessageType.TASK_RESULT,
128 "task_id": task_id,
129 "result": result
130 })
131 elif task == "get_submodules":
132 if not version_finder:
133 raise ValueError("Version finder not initialized")
134 result = version_finder.list_submodules()
135 response_queue.put({
136 "type": MessageType.TASK_RESULT,
137 "task_id": task_id,
138 "result": result
139 })
140 elif task == "find_version":
141 if not version_finder:
142 raise ValueError("Version finder not initialized")
143 result = version_finder.find_version(**args)
144 response_queue.put({
145 "type": MessageType.TASK_RESULT,
146 "task_id": task_id,
147 "result": result
148 })
149 elif task == "find_all_commits_between_versions":
150 if not version_finder:
151 raise ValueError("Version finder not initialized")
152 result = version_finder.find_commits_between_versions(**args)
153 response_queue.put({
154 "type": MessageType.TASK_RESULT,
155 "task_id": task_id,
156 "result": result
157 })
158 elif task == "find_commit_by_text":
159 if not version_finder:
160 raise ValueError("Version finder not initialized")
161 result = version_finder.find_commits_by_text(**args)
162 response_queue.put({
163 "type": MessageType.TASK_RESULT,
164 "task_id": task_id,
165 "result": result
166 })
167 elif task == "restore_state":
168 # Explicitly restore repository state
169 if version_finder and version_finder.has_saved_state():
170 logger.info("Explicitly restoring repository state on close")
171 result = version_finder.restore_repository_state()
172 # Set flag to prevent destructor from trying to restore again
173 version_finder._state_restored = True
175 # Force the destructor to be called by deleting the reference
176 logger.info("Forcing VersionFinder destructor by deleting reference")
177 version_finder_copy = version_finder
178 version_finder = None
179 del version_finder_copy
181 response_queue.put({
182 "type": MessageType.TASK_RESULT,
183 "task_id": task_id,
184 "result": {"status": "success", "restored": result, "forced_destructor": True}
185 })
186 else:
187 logger.info("No saved state to restore")
188 response_queue.put({
189 "type": MessageType.TASK_RESULT,
190 "task_id": task_id,
191 "result": {"status": "success", "restored": False}
192 })
193 else:
194 raise ValueError(f"Unknown task: {task}")
195 except Exception as e:
196 # Get full traceback for better debugging
197 import traceback
198 error_traceback = traceback.format_exc()
199 logger.error(f"Error executing task {task}: {str(e)}\n{error_traceback}")
201 # Send error back
202 response_queue.put({
203 "type": MessageType.TASK_ERROR,
204 "task_id": task_id,
205 "error": str(e),
206 "error_type": type(e).__name__,
207 "traceback": error_traceback
208 })
209 except Exception as e:
210 # Get full traceback for better debugging
211 import traceback
212 error_traceback = traceback.format_exc()
213 logger.error(f"Unexpected error in worker process: {str(e)}\n{error_traceback}")
215 # Send error back to main process
216 response_queue.put({
217 "type": MessageType.TASK_ERROR,
218 "task_id": None, # No specific task ID
219 "error": f"Unexpected worker error: {str(e)}",
220 "error_type": type(e).__name__,
221 "traceback": error_traceback
222 })
224 logger.info("Worker process terminated")
227ctk.set_default_color_theme("green")
230class VersionFinderGUI(ctk.CTk):
231 def __init__(self, path: str = ''):
232 super().__init__()
233 self.repo_path = Path(path).resolve() if path else path
234 self.title("Version Finder")
235 self.version_finder: VersionFinder = None
236 self.selected_branch: str = ''
237 self.selected_submodule: str = ''
239 # Setup multiprocessing
240 self.worker_process = None
241 self.request_queue = None
242 self.response_queue = None
243 self.task_callbacks = {}
244 self.next_task_id = 0
245 self.waiting_for_confirmation = False
247 # Initialize UI
248 self._setup_window()
249 self._create_window_layout()
250 self._setup_icon()
251 self._show_find_version()
253 # Center window on screen
254 center_window(self)
256 # Focus on window
257 self.focus_force()
259 # Start checking for worker responses
260 self.after(100, self._check_worker_responses)
262 # If path is provided, update the entry field and initialize
263 if self.repo_path:
264 # Update the entry field after UI is created
265 self.after(100, lambda: self._update_repo_path_entry())
267 def _update_repo_path_entry(self):
268 """Update the repository path entry field with the current path"""
269 if hasattr(self, 'dir_entry') and self.repo_path:
270 self.dir_entry.delete(0, "end")
271 self.dir_entry.insert(0, str(self.repo_path))
272 self._initialize_version_finder()
274 def _start_worker_process(self):
275 """Start the worker process for background operations"""
276 if self.worker_process is not None and self.worker_process.is_alive():
277 return
279 self.request_queue = multiprocessing.Queue()
280 self.response_queue = multiprocessing.Queue()
281 self.exit_event = multiprocessing.Event()
282 self.worker_process = multiprocessing.Process(
283 target=version_finder_worker,
284 args=(self.request_queue, self.response_queue, self.exit_event),
285 daemon=False # Use non-daemon process for proper cleanup
286 )
287 self.worker_process.start()
288 logger.info(f"Started worker process with PID: {self.worker_process.pid}")
290 def _stop_worker_process(self):
291 """Stop the worker process"""
292 if self.worker_process is not None and self.worker_process.is_alive():
293 try:
294 # First try to signal exit via event
295 logger.info("Setting exit event for worker process")
296 self.exit_event.set()
298 # Give the process a moment to shut down gracefully
299 self.worker_process.join(timeout=2)
301 # If still alive, try shutdown message
302 if self.worker_process.is_alive():
303 logger.info("Worker still alive, sending shutdown message")
304 self.request_queue.put({"type": MessageType.SHUTDOWN})
305 self.worker_process.join(timeout=2)
307 # If still alive, terminate
308 if self.worker_process.is_alive():
309 logger.warning("Worker process did not exit, terminating forcefully")
310 self.worker_process.terminate()
311 else:
312 logger.info("Worker process shut down gracefully")
313 except Exception as e:
314 logger.error(f"Error stopping worker process: {str(e)}")
316 def _check_worker_responses(self):
317 """Check for responses from the worker process"""
318 if self.response_queue is not None:
319 try:
320 # Non-blocking check for messages
321 while True:
322 try:
323 message = self.response_queue.get_nowait()
324 self._handle_worker_message(message)
325 except queue.Empty:
326 break
328 # Check for timed-out tasks
329 self._check_task_timeouts()
331 except Exception as e:
332 logger.error(f"Error checking worker responses: {str(e)}")
334 # Schedule the next check
335 self.after(100, self._check_worker_responses)
337 def _check_task_timeouts(self):
338 """Check for tasks that have timed out"""
339 current_time = time.time()
340 timed_out_tasks = []
342 # Find timed-out tasks
343 for task_id, callback_info in self.task_callbacks.items():
344 start_time = callback_info.get("start_time", 0)
345 timeout = callback_info.get("timeout", 30)
347 if current_time - start_time > timeout:
348 timed_out_tasks.append(task_id)
350 # Handle timed-out tasks
351 for task_id in timed_out_tasks:
352 callback_info = self.task_callbacks.pop(task_id)
353 callback = callback_info["callback"]
354 spinner = callback_info.get("spinner")
356 # Stop the spinner
357 if spinner is not None:
358 spinner.stop()
360 # Log timeout error
361 error_msg = f"Task timed out after {callback_info.get('timeout', 30)} seconds"
362 logger.error(f"Task timeout: {error_msg}")
364 # Display error in UI
365 self._log_error(f"Timeout: {error_msg}")
367 # Call callback with error
368 if callback is not None:
369 callback(None, error=error_msg)
371 def _handle_worker_message(self, message):
372 """Handle a message from the worker process"""
373 message_type = message["type"]
374 task_id = message.get("task_id")
376 # Handle initial repository state message
377 if message_type == MessageType.TASK_RESULT and task_id == "initial_state":
378 self._handle_initial_repo_state(message["result"])
379 return
381 # Handle repository initialization confirmation
382 if message_type == MessageType.TASK_RESULT and task_id == "init_repo_confirmed":
383 self._log_output("Repository initialization confirmed")
385 # Check if we have the original task ID
386 original_task_id = message.get("result", {}).get("original_task_id")
388 if original_task_id is not None and original_task_id in self.task_callbacks:
389 # Get the callback info for the original task
390 callback_info = self.task_callbacks.pop(original_task_id)
391 callback = callback_info.get("callback")
392 spinner = callback_info.get("spinner")
394 # Stop the spinner if it exists
395 if spinner:
396 spinner.stop()
398 # Call the callback with the success result
399 if callback:
400 callback(message["result"])
401 return
402 else:
403 # Find and remove any pending init_repo tasks from the callbacks
404 # to prevent timeout errors for the original task
405 for task_id, callback_info in list(self.task_callbacks.items()):
406 if callback_info.get("task_name") == "init_repo":
407 # Get the callback and remove the task
408 callback = callback_info.get("callback")
409 spinner = callback_info.get("spinner")
410 self.task_callbacks.pop(task_id)
412 # Stop the spinner if it exists
413 if spinner:
414 spinner.stop()
416 # Call the callback with the success result
417 if callback:
418 callback(message["result"])
419 return
421 # If no pending init_repo task was found, just call the handler directly
422 self._on_repo_initialized(message["result"])
423 return
425 # Handle general worker errors (no specific task ID)
426 if message_type == MessageType.TASK_ERROR and task_id is None:
427 error_msg = message.get("error", "Unknown error")
428 error_type = message.get("error_type", "Error")
430 # Log the error
431 logger.error(f"Worker error ({error_type}): {error_msg}")
433 # Display error in UI
434 self._log_error(f"{error_type}: {error_msg}")
436 # Show error dialog
437 messagebox.showerror("Worker Error", f"{error_type}: {error_msg}")
439 # Stop all active spinners
440 self._stop_all_spinners()
441 return
443 # Handle task-specific messages
444 if task_id in self.task_callbacks:
445 callback_info = self.task_callbacks.pop(task_id)
446 callback = callback_info["callback"]
447 spinner = callback_info.get("spinner")
449 # Stop the spinner if one was created
450 if spinner is not None:
451 spinner.stop()
453 if message_type == MessageType.TASK_RESULT:
454 callback(message["result"])
455 elif message_type == MessageType.TASK_ERROR:
456 error_msg = message["error"]
457 error_type = message["error_type"]
458 logger.error(f"Task error ({error_type}): {error_msg}")
460 # Handle pending confirmation errors differently
461 if error_type == "PendingConfirmation":
462 self._log_warning(error_msg)
463 # Don't show error dialog for pending confirmation
464 else:
465 # Display error in UI
466 self._log_error(f"{error_type}: {error_msg}")
468 # Show error dialog
469 messagebox.showerror("Error", f"{error_type}: {error_msg}")
471 # Call callback with error
472 callback(None, error=error_msg)
473 elif message_type == MessageType.TASK_ERROR:
474 # Handle error for unknown task ID
475 error_msg = message.get("error", "Unknown error")
476 error_type = message.get("error_type", "Error")
478 # Log the error
479 logger.error(f"Task error for unknown task ({error_type}): {error_msg}")
481 # Display error in UI
482 self._log_error(f"{error_type}: {error_msg}")
484 # Show error dialog
485 messagebox.showerror("Error", f"{error_type}: {error_msg}")
487 # Stop all active spinners as a precaution
488 self._stop_all_spinners()
490 def _handle_initial_repo_state(self, state):
491 """Handle the initial repository state message"""
492 branch = state.get("branch")
493 has_changes = state.get("has_changes", False)
494 has_submodules = bool(state.get("submodules", {}))
496 if branch:
497 if branch.startswith("HEAD:"):
498 commit = branch.split(":", 1)[1][:8] # Show first 8 chars of commit hash
499 self._log_output(f"Repository is in detached HEAD state at commit {commit}")
500 else:
501 self._log_output(f"Repository is on branch: {branch}")
503 if has_changes:
504 self._log_warning("Repository has uncommitted changes")
505 self._log_output("Waiting for user confirmation before proceeding...")
507 # Stop any existing spinners since we're waiting for user input
508 self._stop_all_spinners()
510 # Set flag to indicate we're waiting for confirmation
511 self.waiting_for_confirmation = True
513 # Build message with details about what will happen
514 message = (
515 "The repository has uncommitted changes. Version Finder will:\n\n"
516 "1. Stash your changes with a unique identifier\n"
517 "2. Perform the requested operations\n"
518 "3. Restore your original branch and stashed changes when closing\n"
519 )
521 if has_submodules:
522 message += "\nSubmodules with uncommitted changes will also be handled similarly."
524 message += "\n\nDo you want to proceed?"
526 # Ask user if they want to proceed
527 proceed = messagebox.askyesno(
528 "Uncommitted Changes",
529 message,
530 icon="warning"
531 )
533 # Reset flag
534 self.waiting_for_confirmation = False
536 if proceed:
537 # Send confirmation to worker
538 self._execute_task(
539 "confirm_init",
540 args={"repo_path": str(self.repo_path), "proceed": True},
541 callback=None # No callback needed as worker will send init_repo_confirmed
542 )
543 else:
544 self._log_output("Operation cancelled by user")
545 # Clear repository path
546 self.dir_entry.delete(0, "end")
547 self.repo_path = ""
549 # Save initial state for reference
550 self.initial_repo_state = state
552 def _log_warning(self, message: str):
553 """Log warning message to the output area"""
554 self.output_text.configure(state="normal")
555 self.output_text.insert("end", f"⚠️ Warning: {message}\n")
556 self.output_text.configure(state="disabled")
557 self.output_text.see("end")
558 logger.warning(message)
560 def _stop_all_spinners(self):
561 """Stop all active spinners"""
562 # Stop all spinners in task_callbacks
563 for task_id, callback_info in list(self.task_callbacks.items()):
564 spinner = callback_info.get("spinner")
565 if spinner is not None:
566 spinner.stop()
568 # Clear task callbacks since we've handled all pending tasks
569 self.task_callbacks.clear()
571 def _execute_task(
572 self,
573 task_name,
574 args=None,
575 callback=None,
576 show_spinner=True,
577 spinner_parent=None,
578 spinner_text="Processing...",
579 timeout=30):
580 """Execute a task in the worker process"""
581 if self.worker_process is None or not self.worker_process.is_alive():
582 self._start_worker_process()
584 # Don't execute new tasks if waiting for confirmation
585 if self.waiting_for_confirmation and task_name != "confirm_proceed":
586 logger.warning(f"Task {task_name} not executed - waiting for user confirmation")
587 if callback:
588 callback(None, error="Operation pending user confirmation")
589 return None
591 task_id = self.next_task_id
592 self.next_task_id += 1
594 # Create a spinner if requested
595 spinner = None
596 if show_spinner and spinner_parent is not None:
597 spinner = LoadingSpinner(spinner_parent, text=spinner_text)
598 spinner.start()
600 # Store callback with task ID
601 if callback is not None:
602 self.task_callbacks[task_id] = {
603 "callback": callback,
604 "spinner": spinner,
605 "start_time": time.time(),
606 "timeout": timeout,
607 "task_name": task_name # Store the task name for reference
608 }
610 # Send task to worker
611 self.request_queue.put({
612 "type": MessageType.TASK_REQUEST,
613 "task": task_name,
614 "args": args or {},
615 "task_id": task_id
616 })
618 return task_id
620 def _setup_window(self):
621 """Configure the main window settings"""
622 self.geometry("1200x800")
623 self.minsize(800, 600)
625 # Handle window close event
626 self.protocol("WM_DELETE_WINDOW", self._on_close)
628 def _on_close(self):
629 """Handle window close event"""
630 # Explicitly restore repository state before shutting down
631 if self.worker_process is not None and self.worker_process.is_alive():
632 try:
633 # Log that we're closing
634 logger.info("Application closing, restoring repository state...")
635 self._log_output("Closing application, restoring repository state...")
637 # First try to signal exit via event
638 logger.info("Setting exit event for worker process")
639 self.exit_event.set()
641 # Explicitly request state restoration
642 try:
643 # Send a task to explicitly restore state and delete the version_finder
644 self._execute_task(
645 "restore_state", callback=lambda result, error=None: logger.info(
646 f"State restoration result: {result}, error: {error}"), timeout=5)
647 # Give a moment for the task to complete
648 time.sleep(1)
649 except Exception as e:
650 logger.error(f"Error during explicit state restoration: {str(e)}")
652 # Wait for the worker to finish (with timeout)
653 self.worker_process.join(timeout=3)
655 # If still alive, try shutdown message
656 if self.worker_process.is_alive():
657 logger.info("Worker still alive, sending shutdown message")
658 self.request_queue.put({"type": MessageType.SHUTDOWN})
659 self.worker_process.join(timeout=2)
661 # If still alive, terminate
662 if self.worker_process.is_alive():
663 logger.warning("Worker process did not exit, terminating forcefully")
664 self.worker_process.terminate()
665 else:
666 logger.info("Worker process shut down gracefully")
668 except Exception as e:
669 logger.error(f"Error during shutdown: {str(e)}")
671 # Now destroy the window
672 self.destroy()
674 def _create_window_layout(self):
675 """Create the main layout with sidebar and content area"""
676 # Configure grid weights for the main window
677 self.grid_columnconfigure(0, weight=0) # Sidebar column (fixed width)
678 self.grid_columnconfigure(1, weight=1) # Content column (expandable)
679 self.grid_rowconfigure(0, weight=1)
681 # Create sidebar
682 self.sidebar_frame = ctk.CTkFrame(self, width=200)
683 self.sidebar_frame.grid(row=0, column=0, sticky="nsew")
684 self.sidebar_frame.grid_rowconfigure(0, weight=1)
685 self.sidebar_content_frame = self._create_sidebar(self.sidebar_frame)
686 self.sidebar_content_frame.grid(row=0, column=0, sticky="nsew", padx=5, pady=2)
688 # Create main area
689 self.main_frame = ctk.CTkFrame(self)
690 self.main_frame.grid(row=0, column=1, sticky="nsew")
691 self.main_frame.grid_columnconfigure(0, weight=1)
692 self.main_frame.grid_rowconfigure(0, weight=1)
693 self.main_content_frame = self._create_content_area(self.main_frame)
694 self.main_content_frame.grid(row=0, column=0, sticky="nsew", padx=5, pady=2)
696 def _create_sidebar(self, parent_frame):
697 """Create the sidebar with task selection buttons"""
699 sidebar_content_frame = ctk.CTkFrame(parent_frame)
700 # Configure sidebar grid
701 sidebar_content_frame.grid_columnconfigure(0, weight=1)
702 sidebar_content_frame.grid_rowconfigure(2, weight=1)
704 # App title
705 title = ctk.CTkLabel(
706 sidebar_content_frame,
707 text="Choose Task",
708 font=("Arial", 20, "bold")
709 )
710 title.grid(row=0, column=0, pady=[10, 30], padx=10)
712 sidebar_task_buttons_frame = ctk.CTkFrame(sidebar_content_frame, fg_color="transparent")
713 sidebar_task_buttons_frame.grid(row=1, column=0, sticky="nsew")
714 # Task selection buttons
715 tasks = [
716 ("Find Version", self._show_find_version),
717 ("Find Commits", self._show_find_commits),
718 ("Search Commits", self._show_search_commits)
719 ]
721 for idx, (text, command) in enumerate(tasks, start=1):
722 btn = ctk.CTkButton(
723 sidebar_task_buttons_frame,
724 text=text,
725 command=command,
726 width=180,
727 )
728 btn.grid(row=idx, column=0, pady=5, padx=10)
730 # Add configuration button at the bottom
731 config_btn = ctk.CTkButton(
732 sidebar_content_frame,
733 text="⚙️ Settings",
734 command=self._show_configuration,
735 width=180
736 )
737 config_btn.grid(row=2, column=0, pady=15, padx=10, sticky="s")
738 return sidebar_content_frame
740 def _create_header_frame(self, parent_frame):
741 """Create the header frame"""
742 header_frame = ctk.CTkFrame(parent_frame, fg_color="transparent")
743 # Header title
744 header = ctk.CTkLabel(
745 header_frame,
746 text="Version Finder",
747 font=ctk.CTkFont(size=36, weight="bold"),
748 text_color="#76B900"
749 )
750 header.grid(row=0, column=0, padx=20, pady=10)
751 return header_frame
753 def _create_content_area(self, parent_frame):
754 """
755 Create the main content area with constant widgets
756 # main_content_frame
757 ####################
758 # Row - 0: hear frame
759 # Row - 1: content frame
760 # content frame
761 ###############
762 # Row - 0: directory frame
763 # Row - 1: branch input frame
764 # Row - 2: submodule input frame
765 # Row - 3: Task input frame
766 # Row - 4: Operation buttons frame
767 # Row - 5: Output frame
768 """
769 main_content_frame = ctk.CTkFrame(parent_frame)
770 main_content_frame.grid_columnconfigure(0, weight=1)
771 main_content_frame.grid_rowconfigure(1, weight=1)
773 # Configure header frame grid
774 header_frame = self._create_header_frame(main_content_frame)
775 header_frame.grid(row=0, column=0, sticky="ew", pady=(0, 10))
776 header_frame.grid_columnconfigure(0, weight=1)
778 # Configure content frame grid
779 content_frame = ctk.CTkFrame(main_content_frame)
780 content_frame.grid(row=1, column=0, sticky="nsew", padx=20, pady=10)
781 content_frame.grid_columnconfigure(0, weight=1)
782 content_frame.grid_rowconfigure(5, weight=10)
784 # Directory selection
785 dir_frame = self._create_directory_section(content_frame)
786 dir_frame.grid(row=0, column=0, sticky="nsew", padx=15, pady=[10, 5])
788 # Branch selection
789 branch_frame = self._create_branch_selection(content_frame)
790 branch_frame.grid(row=1, column=0, sticky="nsew", padx=15, pady=5)
792 # Submodule selection
793 submodule_frame = self._create_submodule_selection(content_frame)
794 submodule_frame.grid(row=2, column=0, sticky="nsew", padx=15, pady=5)
796 # Task-specific content frame
797 self.task_frame = ctk.CTkFrame(content_frame)
798 self.task_frame.grid(row=3, column=0, sticky="nsew", padx=15, pady=5)
800 app_buttons_frame = self._create_app_buttons(content_frame)
801 app_buttons_frame.grid(row=4, column=0, sticky="nsew", padx=15, pady=15)
803 # Output area
804 output_frame = self._create_output_area(content_frame)
805 output_frame.grid(row=5, column=0, sticky="nsew", padx=15, pady=10)
807 return main_content_frame
809 def _create_directory_section(self, parent_frame):
810 """Create the directory selection section"""
811 dir_frame = ctk.CTkFrame(parent_frame)
812 dir_frame.grid(row=0, column=0, sticky="ew", pady=15)
813 dir_frame.grid_columnconfigure(1, weight=1)
815 ctk.CTkLabel(dir_frame, text="Repository Path:").grid(row=0, column=0, padx=5)
816 self.dir_entry = ctk.CTkEntry(dir_frame, width=400, placeholder_text="Enter repository path")
817 self.dir_entry.grid(row=0, column=1, padx=10, pady=10, sticky="ew")
819 browse_btn = ctk.CTkButton(
820 dir_frame,
821 text="Browse",
822 command=self._browse_directory
823 )
824 browse_btn.grid(row=0, column=2, padx=5)
825 return dir_frame
827 def _on_branch_select(self, branch):
828 """Handle branch selection"""
829 if not branch:
830 return
832 if branch != self.selected_branch:
833 self.selected_branch = branch
834 self._update_repository()
836 def _on_submodule_select(self, submodule: str):
837 """Handle submodule selection"""
838 self.selected_submodule = submodule
840 def _create_branch_selection(self, parent_frame):
841 """Create the branch selection section"""
842 branch_frame = ctk.CTkFrame(parent_frame)
843 branch_frame.grid_columnconfigure(1, weight=1)
845 ctk.CTkLabel(branch_frame, text="Branch:").grid(row=0, column=0, padx=5)
847 self.branch_entry = AutocompleteEntry(branch_frame, width=400, placeholder_text="Select a branch")
848 self.branch_entry.configure(state="disabled")
849 self.branch_entry.callback = self._on_branch_select
850 self.branch_entry.grid(row=0, column=1, padx=10, pady=10, sticky="ew")
852 return branch_frame
854 def _create_submodule_selection(self, parent_frame):
855 """Create the submodule selection section"""
856 submodule_frame = ctk.CTkFrame(parent_frame)
857 submodule_frame.grid_columnconfigure(1, weight=1)
859 ctk.CTkLabel(submodule_frame, text="Submodule:").grid(row=0, column=0, padx=5)
861 self.submodule_entry = AutocompleteEntry(
862 submodule_frame, width=400, placeholder_text="Select a submodule [Optional]")
863 self.submodule_entry.configure(state="disabled")
864 self.submodule_entry.callback = self._on_submodule_select
865 self.submodule_entry.grid(row=0, column=1, padx=10, pady=10, sticky="ew")
867 return submodule_frame
869 def _create_app_buttons(self, parent_frame):
870 buttons_frame = ctk.CTkFrame(parent_frame, fg_color="transparent")
872 # Create a gradient effect with multiple buttons
873 search_btn = ctk.CTkButton(
874 buttons_frame,
875 text="Search",
876 command=self._search,
877 corner_radius=10,
878 fg_color=("green", "darkgreen"),
879 hover_color=("darkgreen", "forestgreen")
880 )
881 search_btn.pack(side="left", padx=5, expand=True, fill="x")
883 clear_btn = ctk.CTkButton(
884 buttons_frame,
885 text="Clear",
886 command=self._clear_output,
887 corner_radius=10,
888 fg_color=("gray70", "gray30"),
889 hover_color=("gray60", "gray40")
890 )
891 clear_btn.pack(side="left", padx=5, expand=True, fill="x")
893 exit_btn = ctk.CTkButton(
894 buttons_frame,
895 text="Exit",
896 command=self._on_close,
897 corner_radius=10,
898 fg_color=("red", "darkred"),
899 hover_color=("darkred", "firebrick")
900 )
901 exit_btn.pack(side="right", padx=5, expand=True, fill="x")
902 return buttons_frame
904 def _create_output_area(self, parent_frame):
905 """Create the output/logging area"""
906 output_frame = ctk.CTkFrame(parent_frame, fg_color="transparent")
907 output_frame.grid_columnconfigure(0, weight=1)
908 output_frame.grid_rowconfigure(0, weight=1)
910 self.output_text = ctk.CTkTextbox(
911 output_frame,
912 wrap="word",
913 height=200,
914 font=("Arial", 12),
915 border_width=1,
916 corner_radius=10,
917 scrollbar_button_color=("gray80", "gray30")
918 )
919 self.output_text.grid(row=0, column=0, sticky="nsew", padx=10, pady=10)
920 return output_frame
922 def _clear_output(self):
923 self.output_text.delete("1.0", "end")
925 def _show_configuration(self):
926 """Show the configuration window"""
927 config_window = tk.Toplevel(self)
928 config_window.title("Settings")
929 config_window.geometry("400x400") # Increased height for additional buttons
931 center_window(config_window)
933 # Add your configuration options here
934 # For example:
935 config_frame = ctk.CTkFrame(config_window)
936 config_frame.pack(fill="both", expand=True, padx=10, pady=10)
938 # Theme selection
939 theme_label = ctk.CTkLabel(config_frame, text="Theme Settings", font=("Arial", 16, "bold"))
940 theme_label.pack(pady=(15, 10))
942 theme_var = tk.StringVar(value="Dark")
943 theme_menu = ctk.CTkOptionMenu(
944 config_frame,
945 values=["Light", "Dark", "System"],
946 variable=theme_var,
947 command=lambda x: ctk.set_appearance_mode(x)
948 )
949 theme_menu.pack(pady=15)
951 # Logging section
952 log_label = ctk.CTkLabel(config_frame, text="Logging", font=("Arial", 16, "bold"))
953 log_label.pack(pady=(15, 10))
955 # Open log file button
956 open_log_btn = ctk.CTkButton(
957 config_frame,
958 text="Open Log File",
959 command=self._open_log_file,
960 fg_color=("blue", "darkblue"),
961 hover_color=("darkblue", "navy")
962 )
963 open_log_btn.pack(pady=10)
965 # Apply button
966 apply_btn = ctk.CTkButton(
967 config_frame,
968 text="Apply Settings",
969 command=self._apply_settings,
970 fg_color=("green", "darkgreen"),
971 hover_color=("darkgreen", "forestgreen")
972 )
973 apply_btn.pack(pady=15)
974 self.config_window = config_window
976 def _open_log_file(self):
977 """Open the current log file with the default system application"""
978 from version_finder.logger import get_current_log_file_path
979 import subprocess
980 import os
981 import sys
983 log_path = get_current_log_file_path()
985 if log_path and os.path.exists(log_path):
986 self._log_output(f"Opening log file: {log_path}")
988 try:
989 if sys.platform.startswith('win'):
990 os.startfile(log_path)
991 elif sys.platform.startswith('darwin'): # macOS
992 subprocess.run(['open', log_path], check=True)
993 else: # Linux
994 subprocess.run(['xdg-open', log_path], check=True)
995 except Exception as e:
996 self._log_error(f"Failed to open log file: {str(e)}")
997 else:
998 self._log_error("Log file not found")
1000 def _apply_settings(self):
1001 """Apply configuration settings and return to previous view"""
1002 # You can add more configuration logic here
1003 self._log_output("Settings applied successfully!")
1004 # Return to the last active task view
1005 if hasattr(self, 'current_displayed_task'):
1006 if self.current_displayed_task == VersionFinderTasks.FIND_VERSION:
1007 self._show_find_version()
1008 elif self.current_displayed_task == VersionFinderTasks.COMMITS_BETWEEN_VERSIONS:
1009 self._show_find_commits()
1010 elif self.current_displayed_task == VersionFinderTasks.COMMITS_BY_TEXT:
1011 self._show_search_commits()
1012 self.config_window.destroy()
1014 def _show_find_version(self):
1015 """Show the find version task interface"""
1016 self._clear_task_frame()
1017 ctk.CTkLabel(self.task_frame, text="Commit SHA:").grid(row=0, column=0, padx=5)
1018 self.commit_entry = ctk.CTkEntry(self.task_frame, width=400, placeholder_text="Required")
1019 self.commit_entry.grid(row=0, column=1, padx=10, pady=10, sticky="ew")
1021 self.task_frame.grid_columnconfigure(1, weight=1)
1022 self.current_displayed_task = VersionFinderTasks.FIND_VERSION
1024 def _show_find_commits(self):
1025 """Show the find commits between versions task interface"""
1026 self._clear_task_frame()
1028 ctk.CTkLabel(self.task_frame, text="Start Version:").grid(row=0, column=0, padx=5)
1029 self.start_version_entry = ctk.CTkEntry(self.task_frame, width=400, placeholder_text="Required")
1030 self.start_version_entry.grid(row=0, column=1, padx=10, pady=10, sticky="ew")
1031 ctk.CTkLabel(self.task_frame, text="End Version:").grid(row=0, column=2, padx=5)
1032 self.end_version_entry = ctk.CTkEntry(self.task_frame, width=400, placeholder_text="Required")
1033 self.end_version_entry.grid(row=0, column=3, padx=10, pady=10, sticky="ew")
1035 self.current_displayed_task = VersionFinderTasks.COMMITS_BETWEEN_VERSIONS
1037 def _show_search_commits(self):
1038 """Show the search commits by text task interface"""
1039 self._clear_task_frame()
1041 ctk.CTkLabel(self.task_frame, text="Search Pattern:").grid(row=0, column=0, padx=5)
1042 self.search_text_pattern_entry = ctk.CTkEntry(self.task_frame, width=400, placeholder_text="Required")
1043 self.search_text_pattern_entry.grid(row=0, column=1, padx=10, pady=10, sticky="ew")
1045 self.task_frame.grid_columnconfigure(1, weight=1)
1046 self.current_displayed_task = VersionFinderTasks.COMMITS_BY_TEXT
1048 def _clear_task_frame(self):
1049 """Clear the task-specific frame"""
1050 for widget in self.task_frame.winfo_children():
1051 widget.destroy()
1052 # self.task_frame.grid_forget()
1054 def _browse_directory(self):
1055 """Open directory browser dialog"""
1056 directory = filedialog.askdirectory(initialdir=Path.cwd())
1057 if directory:
1058 # Stop any existing worker process
1059 self._stop_worker_process()
1061 # Update repository path
1062 self.repo_path = Path(directory).resolve()
1064 # Update directory entry
1065 self.dir_entry.delete(0, "end")
1066 self.dir_entry.insert(0, str(self.repo_path))
1068 # Clear branch and submodule selections
1069 self.selected_branch = ""
1070 self.selected_submodule = ""
1072 # Initialize with new repository
1073 self._initialize_version_finder()
1075 self._log_output(f"Selected repository: {self.repo_path}")
1076 else:
1077 self._log_output("No directory selected")
1079 def _initialize_version_finder(self):
1080 """Initialize the VersionFinder with the selected repository path"""
1081 try:
1082 # Initialize with user_confirmation=False first
1083 self._execute_task(
1084 "init_repo",
1085 args={"repo_path": str(self.repo_path), "user_confirmation": False},
1086 callback=self._handle_init_result,
1087 spinner_parent=self.main_frame,
1088 spinner_text="Initializing repository..."
1089 )
1090 except Exception as e:
1091 logger.error(f"Error initializing VersionFinder: {str(e)}")
1092 messagebox.showerror("Error", f"Failed to initialize repository: {str(e)}")
1094 def _handle_init_result(self, result, error=None):
1095 """Handle the result of repository initialization"""
1096 if error:
1097 # Check if error is due to uncommitted changes
1098 if "Repository has uncommitted changes" in str(error):
1099 message = (
1100 "The repository has uncommitted changes. Version Finder will:\n\n"
1101 "1. Stash your changes with a unique identifier\n"
1102 "2. Perform the requested operations\n"
1103 "3. Restore your original branch and stashed changes when closing\n\n"
1104 "Do you want to proceed?"
1105 )
1106 if messagebox.askyesno("Uncommitted Changes", message, icon="warning"):
1107 # Retry initialization with user_confirmation=True
1108 self._execute_task(
1109 "init_repo",
1110 args={"repo_path": str(self.repo_path), "user_confirmation": True},
1111 callback=self._on_repo_initialized,
1112 spinner_parent=self.main_frame,
1113 spinner_text="Initializing repository..."
1114 )
1115 else:
1116 self._log_output("Operation cancelled by user")
1117 # Clear repository path
1118 self.dir_entry.delete(0, "end")
1119 self.repo_path = ""
1120 return
1121 else:
1122 self._log_error(f"Error initializing repository: {error}")
1123 return
1125 self._on_repo_initialized(result)
1127 def _handle_branches_loaded(self, branches: Tuple[List[str], str], error=None):
1128 """Handle branches loaded from worker process"""
1129 branches, current_branch = branches
1130 if error:
1131 self._log_error(f"Error loading branches: {error}")
1132 return
1134 if not branches:
1135 self._log_warning("No branches found in repository")
1136 return
1138 # Update branch autocomplete entry
1139 self.branch_entry.configure(state="normal")
1140 self.branch_entry.suggestions = branches
1142 # Set the current branch as the first suggestion
1143 self.branch_entry.insert(0, current_branch)
1144 self._log_output(f"Loaded {len(branches)} branches")
1146 # Set the selected branch
1147 self.selected_branch = current_branch
1149 # Update repository with selected branch
1150 self._update_repository()
1152 def _update_repository(self):
1153 """Update the repository with the selected branch"""
1154 if not self.selected_branch:
1155 return
1157 self._execute_task(
1158 "update_repository",
1159 args={"branch": self.selected_branch},
1160 callback=self._handle_repository_updated,
1161 spinner_parent=self.main_content_frame,
1162 spinner_text=f"Updating to branch {self.selected_branch}..."
1163 )
1165 def _handle_repository_updated(self, result, error=None):
1166 """Handle repository update result"""
1167 if error:
1168 logger.error(f"Error updating repository: {error}")
1169 messagebox.showerror("Error", f"Failed to update repository: {error}")
1170 return
1172 # Get submodules
1173 self._execute_task(
1174 "get_submodules",
1175 callback=self._handle_submodules_loaded,
1176 spinner_parent=self.main_content_frame,
1177 spinner_text="Loading submodules..."
1178 )
1180 def _handle_submodules_loaded(self, submodules: List[str], error=None):
1181 """Handle submodules loaded from worker process"""
1182 if error:
1183 return
1184 self.submodule_entry.configure(state="normal")
1186 # Clear submodule entry
1187 self.submodule_entry.delete(0, "end")
1188 self.submodule_entry.suggestions = submodules
1190 # Update submodule entry
1191 if submodules:
1192 self.submodule_entry.insert(0, "Select a submodule [Optional]")
1193 self._log_output("Loaded submodules successfully.")
1194 else:
1195 self.submodule_entry.insert(0, f"No submodules found (on branch: {self.selected_branch})")
1196 self._log_output(
1197 f"There are no submodules in the repository (with selected branch: "
1198 f"{self.selected_branch}).")
1199 self.submodule_entry.configure(state="disabled")
1200 self.submodule_entry.configure(text_color="gray")
1202 # Enable UI elements now that repository is ready
1203 self._enable_ui_after_repo_load()
1205 def ensure_version_finder_initialized(func):
1206 def wrapper(self, *args, **kwargs):
1207 if self.version_finder is None:
1208 self._log_error("Repository not initialized")
1209 messagebox.showerror("Error", "Repository not initialized. Please select a repository first.")
1210 return None
1211 return func(self, *args, **kwargs)
1212 return wrapper
1214 @ensure_version_finder_initialized
1215 def _find_version(self):
1216 """Find version for the given commit"""
1217 commit_sha = self.commit_entry.get().strip()
1218 if not commit_sha:
1219 messagebox.showwarning("Input Error", "Please enter a commit SHA")
1220 return
1222 # Get current submodule selection
1223 submodule = self.selected_submodule if self.selected_submodule else None
1225 # Execute the task in the worker process
1226 self._execute_task(
1227 "find_version",
1228 args={
1229 "commit_sha": commit_sha,
1230 "submodule": submodule
1231 },
1232 callback=self._handle_find_version_result,
1233 spinner_parent=self.main_content_frame,
1234 spinner_text=f"Finding version for commit {commit_sha[:7]}..."
1235 )
1237 def _handle_find_version_result(self, result, error=None):
1238 """Handle the result of find_version task"""
1239 if error:
1240 self._log_error(f"Error finding version: {error}")
1241 return
1243 if not result:
1244 self._log_error("No version found for this commit")
1245 return
1247 # Display the result
1248 self._log_output(f"Version found: {result}")
1250 # Update the result label
1251 if hasattr(self, 'version_result_label'):
1252 self.version_result_label.configure(text=f"Version: {result}")
1254 def _find_all_commits_between_versions(self):
1255 """Find all commits between two versions"""
1256 from_version = self.start_version_entry.get().strip()
1257 to_version = self.end_version_entry.get().strip()
1259 if not from_version or not to_version:
1260 messagebox.showwarning("Input Error", "Please enter both from and to versions")
1261 return
1263 # Get current submodule selection
1264 submodule = self.selected_submodule if self.selected_submodule else None
1266 # Execute the task in the worker process
1267 self._execute_task(
1268 "find_all_commits_between_versions",
1269 args={
1270 "from_version": from_version,
1271 "to_version": to_version,
1272 "submodule": submodule
1273 },
1274 callback=self._handle_commits_between_versions_result,
1275 spinner_parent=self.commits_result_frame,
1276 spinner_text=f"Finding commits between {from_version} and {to_version}..."
1277 )
1279 def _handle_commits_between_versions_result(self, commits, error=None):
1280 """Handle the result of find_all_commits_between_versions task"""
1281 if error:
1282 self._log_error(f"Error finding commits: {error}")
1283 return
1285 if not commits:
1286 self._log_output("No commits found between these versions")
1287 return
1289 # Log the number of commits found
1290 self._log_output(f"Found {len(commits)} commits between versions")
1292 # Display commits in a new window
1293 CommitListWindow(self, "Commits Between Versions", commits)
1295 def _find_commit_by_text(self):
1296 """Find commits containing specific text"""
1297 search_text = self.search_text_pattern_entry.get().strip()
1299 if not search_text:
1300 messagebox.showwarning("Input Error", "Please enter search text")
1301 return
1303 # Get current submodule selection
1304 submodule = self.selected_submodule if self.selected_submodule else None
1306 # Execute the task in the worker process
1307 self._execute_task(
1308 "find_commit_by_text",
1309 args={
1310 "text": search_text,
1311 "submodule": submodule
1312 },
1313 callback=self._handle_find_commit_by_text_result,
1314 spinner_parent=self.main_content_frame,
1315 spinner_text=f"Searching for commits with text: {search_text}..."
1316 )
1318 def _handle_find_commit_by_text_result(self, commits, error=None):
1319 """Handle the result of find_commit_by_text task"""
1320 if error:
1321 self._log_error(f"Error searching commits: {error}")
1322 return
1324 if not commits:
1325 self._log_output("No commits found matching the search text")
1326 return
1328 # Log the number of commits found
1329 self._log_output(f"Found {len(commits)} commits matching the search")
1331 # Display commits in a new window
1332 CommitListWindow(self, "Search Results", commits)
1334 def _search(self):
1335 """Handle version search"""
1336 try:
1337 if not self._validate_inputs():
1338 return
1339 if (self.current_displayed_task == VersionFinderTasks.FIND_VERSION):
1340 self._find_version()
1341 elif (self.current_displayed_task == VersionFinderTasks.COMMITS_BETWEEN_VERSIONS):
1342 self._find_all_commits_between_versions()
1343 elif (self.current_displayed_task == VersionFinderTasks.COMMITS_BY_TEXT):
1344 self._find_commit_by_text()
1345 except Exception as e:
1346 self._log_error(str(e))
1348 def _validate_inputs(self) -> bool:
1349 """Validate required inputs"""
1350 if not self.dir_entry.get():
1351 messagebox.showerror("Error", "Please select a repository directory")
1352 return False
1353 if not self.branch_entry.get():
1354 messagebox.showerror("Error", "Please select a branch")
1355 return False
1357 # Check if repository is initialized
1358 if self.version_finder is None:
1359 self._log_warning("Repository not initialized, attempting to initialize now")
1360 self._initialize_version_finder()
1361 # Return False to prevent proceeding until initialization is complete
1362 return False
1364 # If we have a selected branch but repo is not task ready, update it first
1365 if self.selected_branch:
1366 self._execute_task(
1367 "update_repository",
1368 args={"branch": self.selected_branch},
1369 callback=lambda result, error=None: self._handle_update_before_search(result, error),
1370 spinner_parent=self.main_content_frame,
1371 spinner_text=f"Updating to branch {self.selected_branch}..."
1372 )
1373 return False # Return False to prevent proceeding until update is complete
1375 return True
1377 def _handle_update_before_search(self, result, error=None):
1378 """Handle repository update result and proceed with search if successful"""
1379 if error:
1380 logger.error(f"Error updating repository: {error}")
1381 messagebox.showerror("Error", f"Failed to update repository: {error}")
1382 return
1384 # Now proceed with the search
1385 if self.current_displayed_task == VersionFinderTasks.FIND_VERSION:
1386 self._find_version()
1387 elif self.current_displayed_task == VersionFinderTasks.COMMITS_BETWEEN_VERSIONS:
1388 self._find_all_commits_between_versions()
1389 elif self.current_displayed_task == VersionFinderTasks.COMMITS_BY_TEXT:
1390 self._find_commit_by_text()
1392 def _log_output(self, message: str):
1393 """Log output message to the output area"""
1394 self.output_text.configure(state="normal")
1395 self.output_text.insert("end", f"✅ {message}\n")
1396 self.output_text.configure(state="disabled")
1397 self.output_text.see("end")
1398 logger.debug(message)
1400 def _log_error(self, message: str):
1401 """Log error message to the output area"""
1402 self.output_text.configure(state="normal")
1403 self.output_text.insert("end", f"❌ Error: {message}\n")
1404 self.output_text.configure(state="disabled")
1405 self.output_text.see("end")
1406 logger.error(message)
1408 def _setup_icon(self):
1409 """Setup application icon"""
1410 try:
1411 with importlib.resources.path("version_finder_gui.assets", 'icon.png') as icon_path:
1412 if os.name == 'nt': # Windows
1413 # Convert PNG to ICO using PIL
1414 icon = Image.open(str(icon_path))
1415 icon_path_ico = str(icon_path).replace('.png', '.ico')
1416 icon.save(icon_path_ico, format='ICO')
1417 # Set both window and taskbar icons
1418 self.iconbitmap(icon_path_ico)
1419 self.wm_iconbitmap(icon_path_ico)
1420 # Clean up temporary .ico file
1421 os.remove(icon_path_ico)
1422 else: # Unix-like systems
1423 self.iconphoto(True, tk.PhotoImage(file=str(icon_path)))
1424 except Exception as e:
1425 logger.warning(f"Failed to set application icon: {e}")
1426 pass
1428 def center_window(window):
1429 """Center the window on the screen"""
1430 window.update()
1431 width = window.winfo_width()
1432 height = window.winfo_height()
1433 screen_width = window.winfo_screenwidth()
1434 screen_height = window.winfo_screenheight()
1436 x = (screen_width - width) // 2
1437 y = (screen_height - height) // 2
1439 window.geometry(f"{width}x{height}+{x}+{y}")
1441 def _enable_ui_after_repo_load(self):
1442 """Enable UI elements after repository is loaded"""
1443 # Enable search button and other UI elements
1444 for widget in self.task_frame.winfo_children():
1445 if isinstance(widget, ctk.CTkEntry) or isinstance(widget, ctk.CTkButton):
1446 widget.configure(state="normal")
1448 self._log_output("Repository ready for operations")
1450 def init_repo(self, repo_path):
1451 """Initialize the repository"""
1452 self.repo_path = repo_path
1453 self._log_output(f"Initializing repository: {repo_path}")
1455 # Start worker process if not already running
1456 if not self.worker_process or not self.worker_process.is_alive():
1457 self.request_queue = multiprocessing.Queue()
1458 self.response_queue = multiprocessing.Queue()
1459 self.exit_event = multiprocessing.Event()
1461 self.worker_process = multiprocessing.Process(
1462 target=version_finder_worker,
1463 args=(self.request_queue, self.response_queue, self.exit_event),
1464 daemon=False # Use non-daemon process for proper cleanup
1465 )
1466 self.worker_process.start()
1468 # Start response checker
1469 self.after(100, self._check_worker_responses)
1471 # Send init_repo task to worker
1472 self._execute_task(
1473 "init_repo",
1474 args={"repo_path": repo_path},
1475 callback=self._on_repo_initialized,
1476 show_spinner=True,
1477 spinner_parent=self.main_frame,
1478 spinner_text="Initializing repository..."
1479 )
1481 def _on_repo_initialized(self, result, error=None):
1482 """Handle repository initialization result"""
1483 if error:
1484 logger.error(f"Error initializing repository: {error}")
1485 messagebox.showerror("Error", f"Failed to initialize repository: {error}")
1486 return
1488 # Set version_finder to a non-None value to indicate repository is initialized
1489 # This is a placeholder since the actual VersionFinder object is in the worker process
1490 self.version_finder = True
1492 # Get branches
1493 self._execute_task(
1494 "get_branches",
1495 callback=self._handle_branches_loaded,
1496 spinner_parent=self.main_content_frame,
1497 spinner_text="Loading branches..."
1498 )
1501def gui_main(args: argparse.Namespace) -> int:
1502 if args.version:
1503 from version_finder_gui.__version__ import __version__
1504 print(f"version_finder gui-v{__version__}")
1505 logger.info(f"version_finder gui-v{__version__}")
1506 return 0
1508 configure_logging(verbose=args.verbose)
1510 app = VersionFinderGUI(args.path)
1511 app.mainloop()
1512 return 0
1515def main():
1516 args = parse_arguments()
1517 gui_main(args)
1520if __name__ == "__main__":
1521 main()