diff --git a/app.py b/app.py index 201301a..70adc07 100644 --- a/app.py +++ b/app.py @@ -92,6 +92,182 @@ def get_kicad_lib_path(): return None return os.path.join(um_kicad, 'lib') +def check_library_git_status(): + """Check if the library repository has uncommitted changes or unpushed commits.""" + um_kicad = os.environ.get('UM_KICAD') + print(f"[DEBUG] check_library_git_status called, UM_KICAD={um_kicad}") + if not um_kicad or not os.path.exists(um_kicad): + print(f"[DEBUG] Path check failed: exists={os.path.exists(um_kicad) if um_kicad else 'N/A'}") + return {'error': 'Library repository path not found'} + + # The git repository is at ${UM_KICAD}/lib + lib_repo_path = os.path.join(um_kicad, 'lib') + print(f"[DEBUG] Checking git repo at: {lib_repo_path}") + if not os.path.exists(lib_repo_path): + print(f"[DEBUG] Library path does not exist: {lib_repo_path}") + return {'error': 'Library path not found'} + + try: + # Check if it's a git repository + result = subprocess.run( + ['git', 'rev-parse', '--git-dir'], + cwd=lib_repo_path, + capture_output=True, + text=True, + timeout=5 + ) + + if result.returncode != 0: + print(f"[DEBUG] Not a git repository: {result.stderr}") + return {'error': 'Not a git repository'} + + print(f"[DEBUG] Git repo detected at {lib_repo_path}") + + # Check for uncommitted changes (staged or unstaged) + status_result = subprocess.run( + ['git', 'status', '--porcelain'], + cwd=lib_repo_path, + capture_output=True, + text=True, + timeout=5 + ) + + has_changes = bool(status_result.stdout.strip()) + print(f"[DEBUG] Git status output: {repr(status_result.stdout)}") + print(f"[DEBUG] has_changes={has_changes}") + + # Check for unpushed commits + unpushed_result = subprocess.run( + ['git', 'log', '@{u}..', '--oneline'], + cwd=lib_repo_path, + capture_output=True, + text=True, + timeout=5 + ) + + has_unpushed = bool(unpushed_result.stdout.strip()) + print(f"[DEBUG] Unpushed commits output: {repr(unpushed_result.stdout)}") + print(f"[DEBUG] has_unpushed={has_unpushed}") + + # Get current branch + branch_result = subprocess.run( + ['git', 'rev-parse', '--abbrev-ref', 'HEAD'], + cwd=lib_repo_path, + capture_output=True, + text=True, + timeout=5 + ) + + current_branch = branch_result.stdout.strip() if branch_result.returncode == 0 else 'unknown' + + result_data = { + 'has_changes': has_changes, + 'has_unpushed': has_unpushed, + 'current_branch': current_branch, + 'repo_path': lib_repo_path + } + print(f"[DEBUG] Returning git status: {result_data}") + return result_data + + except subprocess.TimeoutExpired: + return {'error': 'Git command timeout'} + except FileNotFoundError: + return {'error': 'Git not found'} + except Exception as e: + return {'error': str(e)} + +def get_library_changed_parts(): + """Get list of changed parts (symbols/footprints) in the library repository.""" + um_kicad = os.environ.get('UM_KICAD') + if not um_kicad or not os.path.exists(um_kicad): + return {'error': 'Library repository path not found'} + + lib_repo_path = os.path.join(um_kicad, 'lib') + if not os.path.exists(lib_repo_path): + return {'error': 'Library path not found'} + + try: + # Get list of changed files + status_result = subprocess.run( + ['git', 'status', '--porcelain'], + cwd=lib_repo_path, + capture_output=True, + text=True, + timeout=5 + ) + + if status_result.returncode != 0: + return {'error': 'Failed to get git status'} + + changed_symbols = [] + changed_footprints = [] + other_changes = [] + + for line in status_result.stdout.strip().split('\n'): + if not line.strip(): + continue + + # Parse git status line: "XY filename" + status = line[:2] + filepath = line[3:].strip() + + # Remove quotes if present + if filepath.startswith('"') and filepath.endswith('"'): + filepath = filepath[1:-1] + + # Determine change type + change_type = 'modified' + if 'A' in status: + change_type = 'added' + elif 'D' in status: + change_type = 'deleted' + elif 'M' in status: + change_type = 'modified' + elif '?' in status: + change_type = 'untracked' + + # Check if it's a symbol file + if filepath.startswith('symbols/') and filepath.endswith('.kicad_sym'): + # Extract library name and get symbols within it + lib_name = os.path.basename(filepath)[:-10] # Remove .kicad_sym + changed_symbols.append({ + 'library': lib_name, + 'file': filepath, + 'change_type': change_type + }) + # Check if it's a footprint file + elif filepath.startswith('footprints/') and '.pretty/' in filepath and filepath.endswith('.kicad_mod'): + # Extract library name and footprint name + parts = filepath.split('/') + lib_name = parts[1] if len(parts) > 1 else 'unknown' + fp_name = os.path.basename(filepath)[:-10] # Remove .kicad_mod + changed_footprints.append({ + 'library': lib_name, + 'footprint': fp_name, + 'file': filepath, + 'change_type': change_type + }) + else: + # Other changes (themes, etc.) + other_changes.append({ + 'file': filepath, + 'change_type': change_type + }) + + return { + 'symbols': changed_symbols, + 'footprints': changed_footprints, + 'other': other_changes, + 'total_changes': len(changed_symbols) + len(changed_footprints) + len(other_changes) + } + + except subprocess.TimeoutExpired: + return {'error': 'Git command timeout'} + except FileNotFoundError: + return {'error': 'Git not found'} + except Exception as e: + return {'error': str(e)} + def get_symbol_libraries(): """Get list of all symbol library files.""" lib_path = get_kicad_lib_path() @@ -2161,6 +2337,301 @@ def handle_get_footprints_in_library(data): import traceback emit('library_error', {'error': f'{str(e)}\n\nTraceback:\n{traceback.format_exc()}'}) +@socketio.on('check_library_git_status') +def handle_check_library_git_status(): + """Check git status of the library repository.""" + try: + status = check_library_git_status() + emit('library_git_status', status) + except Exception as e: + import traceback + emit('library_error', {'error': f'{str(e)}\n\nTraceback:\n{traceback.format_exc()}'}) + +@socketio.on('get_library_changes') +def handle_get_library_changes(): + """Get detailed list of changed parts in the library repository.""" + try: + changes = get_library_changed_parts() + emit('library_changes_result', changes) + except Exception as e: + import traceback + emit('library_error', {'error': f'{str(e)}\n\nTraceback:\n{traceback.format_exc()}'}) + +@socketio.on('commit_library_changes') +def handle_commit_library_changes(data): + """Commit changes to the library repository.""" + try: + commit_message = data.get('message', '').strip() + if not commit_message: + emit('library_commit_error', {'error': 'Commit message is required'}) + return + + um_kicad = os.environ.get('UM_KICAD') + if not um_kicad: + emit('library_commit_error', {'error': 'UM_KICAD not set'}) + return + + lib_repo_path = os.path.join(um_kicad, 'lib') + if not os.path.exists(lib_repo_path): + emit('library_commit_error', {'error': 'Library path not found'}) + return + + emit('library_commit_status', {'status': 'Staging all changes...'}) + + # Stage all changes + add_result = subprocess.run( + ['git', 'add', '-A'], + cwd=lib_repo_path, + capture_output=True, + text=True, + timeout=10 + ) + + if add_result.returncode != 0: + emit('library_commit_error', {'error': f'Failed to stage changes: {add_result.stderr}'}) + return + + emit('library_commit_status', {'status': 'Committing changes...'}) + + # Commit changes + commit_result = subprocess.run( + ['git', 'commit', '-m', commit_message], + cwd=lib_repo_path, + capture_output=True, + text=True, + timeout=10 + ) + + if commit_result.returncode != 0: + # Check if it's just "nothing to commit" + if 'nothing to commit' in commit_result.stdout.lower(): + emit('library_commit_error', {'error': 'No changes to commit'}) + else: + emit('library_commit_error', {'error': f'Commit failed: {commit_result.stderr}'}) + return + + emit('library_commit_complete', {'message': 'Changes committed successfully!'}) + + # Refresh git status + status = check_library_git_status() + emit('library_git_status', status) + + except subprocess.TimeoutExpired: + emit('library_commit_error', {'error': 'Git command timeout'}) + except Exception as e: + import traceback + emit('library_commit_error', {'error': f'{str(e)}\n\nTraceback:\n{traceback.format_exc()}'}) + +@socketio.on('get_git_remote_info') +def handle_get_git_remote_info(): + """Get information about the git remote configuration.""" + try: + um_kicad = os.environ.get('UM_KICAD') + if not um_kicad: + emit('git_remote_info', {'error': 'UM_KICAD not set'}) + return + + lib_repo_path = os.path.join(um_kicad, 'lib') + if not os.path.exists(lib_repo_path): + emit('git_remote_info', {'error': 'Library path not found'}) + return + + # Get remote URL + remote_result = subprocess.run( + ['git', 'config', '--get', 'remote.origin.url'], + cwd=lib_repo_path, + capture_output=True, + text=True, + timeout=5 + ) + + if remote_result.returncode != 0: + emit('git_remote_info', {'error': 'No remote configured'}) + return + + remote_url = remote_result.stdout.strip() + uses_ssh = remote_url.startswith('git@') or remote_url.startswith('ssh://') + + # Get credential helper config + cred_result = subprocess.run( + ['git', 'config', '--get', 'credential.helper'], + cwd=lib_repo_path, + capture_output=True, + text=True, + timeout=5 + ) + + credential_helper = cred_result.stdout.strip() if cred_result.returncode == 0 else 'none' + + emit('git_remote_info', { + 'remote_url': remote_url, + 'uses_ssh': uses_ssh, + 'credential_helper': credential_helper, + 'repo_path': lib_repo_path + }) + + except Exception as e: + import traceback + emit('git_remote_info', {'error': f'{str(e)}\n\nTraceback:\n{traceback.format_exc()}'}) + +@socketio.on('setup_git_credentials') +def handle_setup_git_credentials(): + """Open a terminal window at the library repo for manual git credential setup.""" + try: + um_kicad = os.environ.get('UM_KICAD') + if not um_kicad: + emit('git_setup_error', {'error': 'UM_KICAD not set'}) + return + + lib_repo_path = os.path.join(um_kicad, 'lib') + if not os.path.exists(lib_repo_path): + emit('git_setup_error', {'error': 'Library path not found'}) + return + + # Open terminal at the repo location + # Windows: open cmd + if os.name == 'nt': + subprocess.Popen(['cmd', '/K', f'cd /d {lib_repo_path}']) + else: + # Linux/Mac: try common terminals + try: + subprocess.Popen(['gnome-terminal', '--working-directory', lib_repo_path]) + except: + try: + subprocess.Popen(['xterm', '-e', f'cd {lib_repo_path} && bash']) + except: + subprocess.Popen(['open', '-a', 'Terminal', lib_repo_path]) + + emit('git_setup_complete', {'message': f'Terminal opened at: {lib_repo_path}\n\nYou can now run git commands to set up authentication.'}) + + except Exception as e: + import traceback + emit('git_setup_error', {'error': f'{str(e)}\n\nTraceback:\n{traceback.format_exc()}'}) + +@socketio.on('commit_push_library_changes') +def handle_commit_push_library_changes(data): + """Commit and push changes to the library repository.""" + try: + commit_message = data.get('message', '').strip() + if not commit_message: + emit('library_commit_error', {'error': 'Commit message is required'}) + return + + um_kicad = os.environ.get('UM_KICAD') + if not um_kicad: + emit('library_commit_error', {'error': 'UM_KICAD not set'}) + return + + lib_repo_path = os.path.join(um_kicad, 'lib') + if not os.path.exists(lib_repo_path): + emit('library_commit_error', {'error': 'Library path not found'}) + return + + emit('library_commit_status', {'status': 'Staging all changes...'}) + + # Stage all changes + add_result = subprocess.run( + ['git', 'add', '-A'], + cwd=lib_repo_path, + capture_output=True, + text=True, + timeout=10 + ) + + if add_result.returncode != 0: + emit('library_commit_error', {'error': f'Failed to stage changes: {add_result.stderr}'}) + return + + emit('library_commit_status', {'status': 'Committing changes...'}) + + # Commit changes + commit_result = subprocess.run( + ['git', 'commit', '-m', commit_message], + cwd=lib_repo_path, + capture_output=True, + text=True, + timeout=10 + ) + + if commit_result.returncode != 0: + if 'nothing to commit' in commit_result.stdout.lower(): + # No commit, but maybe there are unpushed commits - try to push anyway + emit('library_commit_status', {'status': 'No new changes to commit, checking for unpushed commits...'}) + else: + emit('library_commit_error', {'error': f'Commit failed: {commit_result.stderr}'}) + return + + emit('library_commit_status', {'status': 'Pushing to remote...'}) + + # Get remote URL to check auth type + remote_result = subprocess.run( + ['git', 'config', '--get', 'remote.origin.url'], + cwd=lib_repo_path, + capture_output=True, + text=True, + timeout=5 + ) + + remote_url = remote_result.stdout.strip() if remote_result.returncode == 0 else '' + uses_ssh = remote_url.startswith('git@') or remote_url.startswith('ssh://') + + # Push to remote + push_result = subprocess.run( + ['git', 'push'], + cwd=lib_repo_path, + capture_output=True, + text=True, + timeout=30 + ) + + if push_result.returncode != 0: + error_msg = push_result.stderr + + # Provide helpful error messages based on the type of failure + if 'Authentication failed' in error_msg or 'Failed to authenticate' in error_msg: + if uses_ssh: + helpful_msg = ( + f'Push failed: Authentication error.\n\n' + f'Your repository uses SSH authentication ({remote_url}).\n\n' + f'To fix this:\n' + f'1. Ensure your SSH key is added to your Git hosting service (GitHub/GitLab/etc.)\n' + f'2. Test SSH access: ssh -T git@github.com (or your Git host)\n' + f'3. Make sure ssh-agent is running and has your key loaded\n\n' + f'Original error: {error_msg}' + ) + else: + helpful_msg = ( + f'Push failed: Authentication error.\n\n' + f'Your repository uses HTTPS authentication ({remote_url}).\n\n' + f'To fix this:\n' + f'1. Configure Git Credential Manager:\n' + f' git config --global credential.helper manager-core\n' + f'2. Or use a personal access token instead of password\n' + f'3. You can also switch to SSH authentication\n\n' + f'To configure credentials now, open a terminal in:\n' + f'{lib_repo_path}\n' + f'And run: git push\n\n' + f'Original error: {error_msg}' + ) + emit('library_commit_error', {'error': helpful_msg}) + elif 'rejected' in error_msg.lower(): + emit('library_commit_error', {'error': f'Push rejected. The remote has changes you don\'t have locally.\n\nPlease pull changes first:\n{error_msg}'}) + else: + emit('library_commit_error', {'error': f'Push failed: {error_msg}'}) + return + + emit('library_commit_complete', {'message': 'Changes committed and pushed successfully!'}) + + # Refresh git status + status = check_library_git_status() + emit('library_git_status', status) + + except subprocess.TimeoutExpired: + emit('library_commit_error', {'error': 'Git command timeout'}) + except Exception as e: + import traceback + emit('library_commit_error', {'error': f'{str(e)}\n\nTraceback:\n{traceback.format_exc()}'}) + @socketio.on('render_footprint') def handle_render_footprint(data): try: diff --git a/templates/index.html b/templates/index.html index 8fba5f2..0f626fb 100644 --- a/templates/index.html +++ b/templates/index.html @@ -402,6 +402,19 @@