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

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 

18 

19logger = get_logger() 

20# Define message types for inter-process communication 

21 

22class MessageType(Enum): 

23 TASK_REQUEST = auto() 

24 TASK_RESULT = auto() 

25 TASK_ERROR = auto() 

26 SHUTDOWN = auto() 

27 

28 

29class VersionFinderTasks(Enum): 

30 FIND_VERSION = auto() 

31 COMMITS_BETWEEN_VERSIONS = auto() 

32 COMMITS_BY_TEXT = auto() 

33 

34# Worker process function 

35 

36 

37def version_finder_worker(request_queue, response_queue, exit_event=None): 

38 """Worker process for version finder operations""" 

39 logger.info("Worker process started") 

40 

41 version_finder = None 

42 waiting_for_confirmation = False 

43 pending_init_task_id = None 

44 

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 

51 

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 

57 

58 # Handle shutdown message 

59 if message["type"] == MessageType.SHUTDOWN: 

60 logger.info("Shutdown message received") 

61 break 

62 

63 # Extract task info 

64 task = message.get("task") 

65 task_id = message.get("task_id") 

66 args = message.get("args", {}) 

67 

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 

75 

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 

87 

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}") 

95 

96 try: 

97 # Initialize version finder with specified force parameter 

98 version_finder = VersionFinder(repo_path, force=user_confirmation) 

99 

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 

174 

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 

180 

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}") 

200 

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}") 

214 

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 }) 

223 

224 logger.info("Worker process terminated") 

225 

226 

227ctk.set_default_color_theme("green") 

228 

229 

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 = '' 

238 

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 

246 

247 # Initialize UI 

248 self._setup_window() 

249 self._create_window_layout() 

250 self._setup_icon() 

251 self._show_find_version() 

252 

253 # Center window on screen 

254 center_window(self) 

255 

256 # Focus on window 

257 self.focus_force() 

258 

259 # Start checking for worker responses 

260 self.after(100, self._check_worker_responses) 

261 

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()) 

266 

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() 

273 

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 

278 

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}") 

289 

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() 

297 

298 # Give the process a moment to shut down gracefully 

299 self.worker_process.join(timeout=2) 

300 

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) 

306 

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)}") 

315 

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 

327 

328 # Check for timed-out tasks 

329 self._check_task_timeouts() 

330 

331 except Exception as e: 

332 logger.error(f"Error checking worker responses: {str(e)}") 

333 

334 # Schedule the next check 

335 self.after(100, self._check_worker_responses) 

336 

337 def _check_task_timeouts(self): 

338 """Check for tasks that have timed out""" 

339 current_time = time.time() 

340 timed_out_tasks = [] 

341 

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) 

346 

347 if current_time - start_time > timeout: 

348 timed_out_tasks.append(task_id) 

349 

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") 

355 

356 # Stop the spinner 

357 if spinner is not None: 

358 spinner.stop() 

359 

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}") 

363 

364 # Display error in UI 

365 self._log_error(f"Timeout: {error_msg}") 

366 

367 # Call callback with error 

368 if callback is not None: 

369 callback(None, error=error_msg) 

370 

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") 

375 

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 

380 

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") 

384 

385 # Check if we have the original task ID 

386 original_task_id = message.get("result", {}).get("original_task_id") 

387 

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") 

393 

394 # Stop the spinner if it exists 

395 if spinner: 

396 spinner.stop() 

397 

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) 

411 

412 # Stop the spinner if it exists 

413 if spinner: 

414 spinner.stop() 

415 

416 # Call the callback with the success result 

417 if callback: 

418 callback(message["result"]) 

419 return 

420 

421 # If no pending init_repo task was found, just call the handler directly 

422 self._on_repo_initialized(message["result"]) 

423 return 

424 

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") 

429 

430 # Log the error 

431 logger.error(f"Worker error ({error_type}): {error_msg}") 

432 

433 # Display error in UI 

434 self._log_error(f"{error_type}: {error_msg}") 

435 

436 # Show error dialog 

437 messagebox.showerror("Worker Error", f"{error_type}: {error_msg}") 

438 

439 # Stop all active spinners 

440 self._stop_all_spinners() 

441 return 

442 

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") 

448 

449 # Stop the spinner if one was created 

450 if spinner is not None: 

451 spinner.stop() 

452 

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}") 

459 

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}") 

467 

468 # Show error dialog 

469 messagebox.showerror("Error", f"{error_type}: {error_msg}") 

470 

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") 

477 

478 # Log the error 

479 logger.error(f"Task error for unknown task ({error_type}): {error_msg}") 

480 

481 # Display error in UI 

482 self._log_error(f"{error_type}: {error_msg}") 

483 

484 # Show error dialog 

485 messagebox.showerror("Error", f"{error_type}: {error_msg}") 

486 

487 # Stop all active spinners as a precaution 

488 self._stop_all_spinners() 

489 

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", {})) 

495 

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}") 

502 

503 if has_changes: 

504 self._log_warning("Repository has uncommitted changes") 

505 self._log_output("Waiting for user confirmation before proceeding...") 

506 

507 # Stop any existing spinners since we're waiting for user input 

508 self._stop_all_spinners() 

509 

510 # Set flag to indicate we're waiting for confirmation 

511 self.waiting_for_confirmation = True 

512 

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 ) 

520 

521 if has_submodules: 

522 message += "\nSubmodules with uncommitted changes will also be handled similarly." 

523 

524 message += "\n\nDo you want to proceed?" 

525 

526 # Ask user if they want to proceed 

527 proceed = messagebox.askyesno( 

528 "Uncommitted Changes", 

529 message, 

530 icon="warning" 

531 ) 

532 

533 # Reset flag 

534 self.waiting_for_confirmation = False 

535 

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 = "" 

548 

549 # Save initial state for reference 

550 self.initial_repo_state = state 

551 

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) 

559 

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() 

567 

568 # Clear task callbacks since we've handled all pending tasks 

569 self.task_callbacks.clear() 

570 

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() 

583 

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 

590 

591 task_id = self.next_task_id 

592 self.next_task_id += 1 

593 

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() 

599 

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 } 

609 

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 }) 

617 

618 return task_id 

619 

620 def _setup_window(self): 

621 """Configure the main window settings""" 

622 self.geometry("1200x800") 

623 self.minsize(800, 600) 

624 

625 # Handle window close event 

626 self.protocol("WM_DELETE_WINDOW", self._on_close) 

627 

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...") 

636 

637 # First try to signal exit via event 

638 logger.info("Setting exit event for worker process") 

639 self.exit_event.set() 

640 

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)}") 

651 

652 # Wait for the worker to finish (with timeout) 

653 self.worker_process.join(timeout=3) 

654 

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) 

660 

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") 

667 

668 except Exception as e: 

669 logger.error(f"Error during shutdown: {str(e)}") 

670 

671 # Now destroy the window 

672 self.destroy() 

673 

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) 

680 

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) 

687 

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) 

695 

696 def _create_sidebar(self, parent_frame): 

697 """Create the sidebar with task selection buttons""" 

698 

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) 

703 

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) 

711 

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 ] 

720 

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) 

729 

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 

739 

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 

752 

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) 

772 

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) 

777 

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) 

783 

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]) 

787 

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) 

791 

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) 

795 

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) 

799 

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) 

802 

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) 

806 

807 return main_content_frame 

808 

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) 

814 

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") 

818 

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 

826 

827 def _on_branch_select(self, branch): 

828 """Handle branch selection""" 

829 if not branch: 

830 return 

831 

832 if branch != self.selected_branch: 

833 self.selected_branch = branch 

834 self._update_repository() 

835 

836 def _on_submodule_select(self, submodule: str): 

837 """Handle submodule selection""" 

838 self.selected_submodule = submodule 

839 

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) 

844 

845 ctk.CTkLabel(branch_frame, text="Branch:").grid(row=0, column=0, padx=5) 

846 

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") 

851 

852 return branch_frame 

853 

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) 

858 

859 ctk.CTkLabel(submodule_frame, text="Submodule:").grid(row=0, column=0, padx=5) 

860 

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") 

866 

867 return submodule_frame 

868 

869 def _create_app_buttons(self, parent_frame): 

870 buttons_frame = ctk.CTkFrame(parent_frame, fg_color="transparent") 

871 

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") 

882 

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") 

892 

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 

903 

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) 

909 

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 

921 

922 def _clear_output(self): 

923 self.output_text.delete("1.0", "end") 

924 

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 

930 

931 center_window(config_window) 

932 

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) 

937 

938 # Theme selection 

939 theme_label = ctk.CTkLabel(config_frame, text="Theme Settings", font=("Arial", 16, "bold")) 

940 theme_label.pack(pady=(15, 10)) 

941 

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) 

950 

951 # Logging section 

952 log_label = ctk.CTkLabel(config_frame, text="Logging", font=("Arial", 16, "bold")) 

953 log_label.pack(pady=(15, 10)) 

954 

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) 

964 

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 

975 

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 

982 

983 log_path = get_current_log_file_path() 

984 

985 if log_path and os.path.exists(log_path): 

986 self._log_output(f"Opening log file: {log_path}") 

987 

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") 

999 

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() 

1013 

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") 

1020 

1021 self.task_frame.grid_columnconfigure(1, weight=1) 

1022 self.current_displayed_task = VersionFinderTasks.FIND_VERSION 

1023 

1024 def _show_find_commits(self): 

1025 """Show the find commits between versions task interface""" 

1026 self._clear_task_frame() 

1027 

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") 

1034 

1035 self.current_displayed_task = VersionFinderTasks.COMMITS_BETWEEN_VERSIONS 

1036 

1037 def _show_search_commits(self): 

1038 """Show the search commits by text task interface""" 

1039 self._clear_task_frame() 

1040 

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") 

1044 

1045 self.task_frame.grid_columnconfigure(1, weight=1) 

1046 self.current_displayed_task = VersionFinderTasks.COMMITS_BY_TEXT 

1047 

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() 

1053 

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() 

1060 

1061 # Update repository path 

1062 self.repo_path = Path(directory).resolve() 

1063 

1064 # Update directory entry 

1065 self.dir_entry.delete(0, "end") 

1066 self.dir_entry.insert(0, str(self.repo_path)) 

1067 

1068 # Clear branch and submodule selections 

1069 self.selected_branch = "" 

1070 self.selected_submodule = "" 

1071 

1072 # Initialize with new repository 

1073 self._initialize_version_finder() 

1074 

1075 self._log_output(f"Selected repository: {self.repo_path}") 

1076 else: 

1077 self._log_output("No directory selected") 

1078 

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)}") 

1093 

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 

1124 

1125 self._on_repo_initialized(result) 

1126 

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 

1133 

1134 if not branches: 

1135 self._log_warning("No branches found in repository") 

1136 return 

1137 

1138 # Update branch autocomplete entry 

1139 self.branch_entry.configure(state="normal") 

1140 self.branch_entry.suggestions = branches 

1141 

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") 

1145 

1146 # Set the selected branch 

1147 self.selected_branch = current_branch 

1148 

1149 # Update repository with selected branch 

1150 self._update_repository() 

1151 

1152 def _update_repository(self): 

1153 """Update the repository with the selected branch""" 

1154 if not self.selected_branch: 

1155 return 

1156 

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 ) 

1164 

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 

1171 

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 ) 

1179 

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") 

1185 

1186 # Clear submodule entry 

1187 self.submodule_entry.delete(0, "end") 

1188 self.submodule_entry.suggestions = submodules 

1189 

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") 

1201 

1202 # Enable UI elements now that repository is ready 

1203 self._enable_ui_after_repo_load() 

1204 

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 

1213 

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 

1221 

1222 # Get current submodule selection 

1223 submodule = self.selected_submodule if self.selected_submodule else None 

1224 

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 ) 

1236 

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 

1242 

1243 if not result: 

1244 self._log_error("No version found for this commit") 

1245 return 

1246 

1247 # Display the result 

1248 self._log_output(f"Version found: {result}") 

1249 

1250 # Update the result label 

1251 if hasattr(self, 'version_result_label'): 

1252 self.version_result_label.configure(text=f"Version: {result}") 

1253 

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() 

1258 

1259 if not from_version or not to_version: 

1260 messagebox.showwarning("Input Error", "Please enter both from and to versions") 

1261 return 

1262 

1263 # Get current submodule selection 

1264 submodule = self.selected_submodule if self.selected_submodule else None 

1265 

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 ) 

1278 

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 

1284 

1285 if not commits: 

1286 self._log_output("No commits found between these versions") 

1287 return 

1288 

1289 # Log the number of commits found 

1290 self._log_output(f"Found {len(commits)} commits between versions") 

1291 

1292 # Display commits in a new window 

1293 CommitListWindow(self, "Commits Between Versions", commits) 

1294 

1295 def _find_commit_by_text(self): 

1296 """Find commits containing specific text""" 

1297 search_text = self.search_text_pattern_entry.get().strip() 

1298 

1299 if not search_text: 

1300 messagebox.showwarning("Input Error", "Please enter search text") 

1301 return 

1302 

1303 # Get current submodule selection 

1304 submodule = self.selected_submodule if self.selected_submodule else None 

1305 

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 ) 

1317 

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 

1323 

1324 if not commits: 

1325 self._log_output("No commits found matching the search text") 

1326 return 

1327 

1328 # Log the number of commits found 

1329 self._log_output(f"Found {len(commits)} commits matching the search") 

1330 

1331 # Display commits in a new window 

1332 CommitListWindow(self, "Search Results", commits) 

1333 

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)) 

1347 

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 

1356 

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 

1363 

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 

1374 

1375 return True 

1376 

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 

1383 

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() 

1391 

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) 

1399 

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) 

1407 

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 

1427 

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() 

1435 

1436 x = (screen_width - width) // 2 

1437 y = (screen_height - height) // 2 

1438 

1439 window.geometry(f"{width}x{height}+{x}+{y}") 

1440 

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") 

1447 

1448 self._log_output("Repository ready for operations") 

1449 

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}") 

1454 

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() 

1460 

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() 

1467 

1468 # Start response checker 

1469 self.after(100, self._check_worker_responses) 

1470 

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 ) 

1480 

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 

1487 

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 

1491 

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 ) 

1499 

1500 

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 

1507 

1508 configure_logging(verbose=args.verbose) 

1509 

1510 app = VersionFinderGUI(args.path) 

1511 app.mainloop() 

1512 return 0 

1513 

1514 

1515def main(): 

1516 args = parse_arguments() 

1517 gui_main(args) 

1518 

1519 

1520if __name__ == "__main__": 

1521 main()