Add library git change detection and management system
Features: - Yellow banner alert on Library tab when changes detected - Custom diff viewer showing changed symbols/footprints by name - Categorizes changes: symbols, footprints, and other files - Color-coded status indicators (added/modified/deleted/untracked) - Commit and push functionality from UI - Smart authentication error handling with helpful messages - Git configuration info display (remote URL, auth type, credential helper) - Terminal opener for manual git credential setup - Auto-refresh after commits to update status Backend: - check_library_git_status(): Checks for uncommitted/unpushed changes - get_library_changed_parts(): Parses git status and categorizes by part type - Socket handlers for git status, changes, commit, and push operations - Authentication type detection (SSH vs HTTPS) - Context-specific error messages for auth failures Frontend: - Git status banner with view changes button - Modal with tabular display of changed parts - Commit message input with commit/push buttons - Real-time status updates during git operations - Git config info section with terminal helper button 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
471
app.py
471
app.py
@@ -92,6 +92,182 @@ def get_kicad_lib_path():
|
|||||||
return None
|
return None
|
||||||
return os.path.join(um_kicad, 'lib')
|
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():
|
def get_symbol_libraries():
|
||||||
"""Get list of all symbol library files."""
|
"""Get list of all symbol library files."""
|
||||||
lib_path = get_kicad_lib_path()
|
lib_path = get_kicad_lib_path()
|
||||||
@@ -2161,6 +2337,301 @@ def handle_get_footprints_in_library(data):
|
|||||||
import traceback
|
import traceback
|
||||||
emit('library_error', {'error': f'{str(e)}\n\nTraceback:\n{traceback.format_exc()}'})
|
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')
|
@socketio.on('render_footprint')
|
||||||
def handle_render_footprint(data):
|
def handle_render_footprint(data):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -402,6 +402,19 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<h2>Parts Library Browser</h2>
|
<h2>Parts Library Browser</h2>
|
||||||
|
|
||||||
|
<!-- Git Status Banner -->
|
||||||
|
<div id="libraryGitBanner" style="display: none; margin: 10px 0; padding: 15px; background-color: #fff3cd; border-left: 4px solid #ffc107; border-radius: 5px;">
|
||||||
|
<div style="display: flex; align-items: center; justify-content: space-between;">
|
||||||
|
<div>
|
||||||
|
<strong style="color: #856404;">⚠ Library Repository has changes</strong>
|
||||||
|
<p id="gitBannerMessage" style="margin: 5px 0 0 0; color: #856404; font-size: 14px;"></p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button id="viewGitDiffBtn" class="btn" style="background-color: #ffc107; color: #000; padding: 8px 16px; font-size: 14px;">View Changes</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Browser View -->
|
<!-- Browser View -->
|
||||||
<div id="libraryBrowserView">
|
<div id="libraryBrowserView">
|
||||||
<div style="margin: 20px 0;">
|
<div style="margin: 20px 0;">
|
||||||
@@ -569,6 +582,41 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Library Changes Modal -->
|
||||||
|
<div id="libraryChangesModal" class="modal">
|
||||||
|
<div class="modal-content" style="width: 70%; max-width: 900px;">
|
||||||
|
<span class="close" onclick="closeLibraryChangesModal()">×</span>
|
||||||
|
<h2>Library Changes</h2>
|
||||||
|
|
||||||
|
<!-- Git Config Info -->
|
||||||
|
<div id="gitConfigInfo" style="display: none; margin-bottom: 15px; padding: 10px; background: #f8f9fa; border-radius: 5px; font-size: 13px;">
|
||||||
|
<strong>Repository:</strong> <span id="gitRemoteUrl" style="font-family: monospace;"></span><br>
|
||||||
|
<strong>Auth Type:</strong> <span id="gitAuthType"></span><br>
|
||||||
|
<strong>Credential Helper:</strong> <span id="gitCredHelper"></span>
|
||||||
|
<button id="openTerminalBtn" class="btn" style="margin-top: 5px; padding: 5px 10px; font-size: 12px; background-color: #6c757d;">Open Terminal for Setup</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>The following parts have been modified in the library repository:</p>
|
||||||
|
|
||||||
|
<div id="libraryChangesContent" style="max-height: 500px; overflow-y: auto;">
|
||||||
|
<div id="libraryChangesLoading" style="text-align: center; padding: 20px;">
|
||||||
|
<p>Loading changes...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 20px; padding-top: 20px; border-top: 2px solid #dee2e6;">
|
||||||
|
<div style="display: flex; gap: 10px; align-items: center;">
|
||||||
|
<input type="text" id="commitMessage" placeholder="Enter commit message..."
|
||||||
|
style="flex: 1; padding: 10px; border: 1px solid #dee2e6; border-radius: 5px; font-size: 14px;">
|
||||||
|
<button id="commitChangesBtn" class="btn" style="background-color: #28a745;">Commit Changes</button>
|
||||||
|
<button id="commitPushBtn" class="btn" style="background-color: #007bff;">Commit & Push</button>
|
||||||
|
<button class="btn secondary" onclick="closeLibraryChangesModal()">Close</button>
|
||||||
|
</div>
|
||||||
|
<div id="gitActionMessage" class="message" style="margin-top: 10px;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
||||||
@@ -978,6 +1026,8 @@
|
|||||||
} else if (tabName === 'library') {
|
} else if (tabName === 'library') {
|
||||||
document.getElementById('libraryTab').classList.add('active');
|
document.getElementById('libraryTab').classList.add('active');
|
||||||
document.querySelectorAll('.tab')[2].classList.add('active');
|
document.querySelectorAll('.tab')[2].classList.add('active');
|
||||||
|
// Check git status when library tab is shown
|
||||||
|
checkLibraryGitStatus();
|
||||||
} else if (tabName === 'variants') {
|
} else if (tabName === 'variants') {
|
||||||
document.getElementById('variantsTab').classList.add('active');
|
document.getElementById('variantsTab').classList.add('active');
|
||||||
document.querySelectorAll('.tab')[3].classList.add('active');
|
document.querySelectorAll('.tab')[3].classList.add('active');
|
||||||
@@ -985,6 +1035,269 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Library Git Status
|
||||||
|
// ========================================
|
||||||
|
let gitStatusData = null;
|
||||||
|
|
||||||
|
function checkLibraryGitStatus() {
|
||||||
|
socket.emit('check_library_git_status');
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.on('library_git_status', (data) => {
|
||||||
|
console.log('[DEBUG] Received library_git_status:', data);
|
||||||
|
gitStatusData = data;
|
||||||
|
const banner = document.getElementById('libraryGitBanner');
|
||||||
|
const messageEl = document.getElementById('gitBannerMessage');
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
// Don't show banner if there's an error (e.g., not a git repo)
|
||||||
|
console.log('[DEBUG] Git status error:', data.error);
|
||||||
|
banner.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[DEBUG] has_changes:', data.has_changes, 'has_unpushed:', data.has_unpushed);
|
||||||
|
|
||||||
|
if (data.has_changes || data.has_unpushed) {
|
||||||
|
let message = '';
|
||||||
|
if (data.has_changes && data.has_unpushed) {
|
||||||
|
message = 'The library repository has uncommitted changes and unpushed commits.';
|
||||||
|
} else if (data.has_changes) {
|
||||||
|
message = 'The library repository has uncommitted changes.';
|
||||||
|
} else if (data.has_unpushed) {
|
||||||
|
message = 'The library repository has unpushed commits.';
|
||||||
|
}
|
||||||
|
message += ` Branch: ${data.current_branch}`;
|
||||||
|
|
||||||
|
messageEl.textContent = message;
|
||||||
|
banner.style.display = 'block';
|
||||||
|
console.log('[DEBUG] Banner shown with message:', message);
|
||||||
|
} else {
|
||||||
|
banner.style.display = 'none';
|
||||||
|
console.log('[DEBUG] No changes detected, banner hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('viewGitDiffBtn').addEventListener('click', () => {
|
||||||
|
showLibraryChangesModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
function showLibraryChangesModal() {
|
||||||
|
const modal = document.getElementById('libraryChangesModal');
|
||||||
|
const content = document.getElementById('libraryChangesContent');
|
||||||
|
const loading = document.getElementById('libraryChangesLoading');
|
||||||
|
|
||||||
|
modal.style.display = 'block';
|
||||||
|
content.innerHTML = '';
|
||||||
|
loading.style.display = 'block';
|
||||||
|
content.appendChild(loading);
|
||||||
|
|
||||||
|
// Request git remote info and changes from server
|
||||||
|
socket.emit('get_git_remote_info');
|
||||||
|
socket.emit('get_library_changes');
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.on('git_remote_info', (data) => {
|
||||||
|
const gitConfigInfo = document.getElementById('gitConfigInfo');
|
||||||
|
const gitRemoteUrl = document.getElementById('gitRemoteUrl');
|
||||||
|
const gitAuthType = document.getElementById('gitAuthType');
|
||||||
|
const gitCredHelper = document.getElementById('gitCredHelper');
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
gitConfigInfo.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gitRemoteUrl.textContent = data.remote_url;
|
||||||
|
gitAuthType.textContent = data.uses_ssh ? 'SSH' : 'HTTPS';
|
||||||
|
gitCredHelper.textContent = data.credential_helper || 'Not configured';
|
||||||
|
gitConfigInfo.style.display = 'block';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('openTerminalBtn').addEventListener('click', () => {
|
||||||
|
socket.emit('setup_git_credentials');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('git_setup_complete', (data) => {
|
||||||
|
showGitActionMessage(data.message, 'success');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('git_setup_error', (data) => {
|
||||||
|
showGitActionMessage('Error: ' + data.error, 'error');
|
||||||
|
});
|
||||||
|
|
||||||
|
window.closeLibraryChangesModal = function() {
|
||||||
|
document.getElementById('libraryChangesModal').style.display = 'none';
|
||||||
|
document.getElementById('commitMessage').value = '';
|
||||||
|
document.getElementById('gitActionMessage').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.on('library_changes_result', (data) => {
|
||||||
|
const content = document.getElementById('libraryChangesContent');
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
content.innerHTML = `<p style="color: #dc3545;">Error: ${data.error}</p>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.total_changes === 0) {
|
||||||
|
content.innerHTML = '<p style="color: #6c757d;">No changes detected in the library.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
// Changed Symbols
|
||||||
|
if (data.symbols && data.symbols.length > 0) {
|
||||||
|
html += '<div style="margin-bottom: 20px;">';
|
||||||
|
html += '<h3 style="color: #007bff; border-bottom: 2px solid #007bff; padding-bottom: 5px;">Symbols</h3>';
|
||||||
|
html += '<table style="width: 100%; border-collapse: collapse; font-size: 14px;">';
|
||||||
|
html += '<thead><tr style="background: #f8f9fa;">';
|
||||||
|
html += '<th style="padding: 8px; text-align: left; border: 1px solid #dee2e6;">Library</th>';
|
||||||
|
html += '<th style="padding: 8px; text-align: left; border: 1px solid #dee2e6;">Status</th>';
|
||||||
|
html += '<th style="padding: 8px; text-align: left; border: 1px solid #dee2e6;">File</th>';
|
||||||
|
html += '</tr></thead><tbody>';
|
||||||
|
|
||||||
|
for (const sym of data.symbols) {
|
||||||
|
const statusColor = getChangeTypeColor(sym.change_type);
|
||||||
|
html += '<tr>';
|
||||||
|
html += `<td style="padding: 8px; border: 1px solid #dee2e6;"><strong>${sym.library}</strong></td>`;
|
||||||
|
html += `<td style="padding: 8px; border: 1px solid #dee2e6;"><span style="color: ${statusColor}; font-weight: bold;">${sym.change_type.toUpperCase()}</span></td>`;
|
||||||
|
html += `<td style="padding: 8px; border: 1px solid #dee2e6; font-family: monospace; font-size: 12px;">${sym.file}</td>`;
|
||||||
|
html += '</tr>';
|
||||||
|
}
|
||||||
|
html += '</tbody></table></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Changed Footprints
|
||||||
|
if (data.footprints && data.footprints.length > 0) {
|
||||||
|
html += '<div style="margin-bottom: 20px;">';
|
||||||
|
html += '<h3 style="color: #28a745; border-bottom: 2px solid #28a745; padding-bottom: 5px;">Footprints</h3>';
|
||||||
|
html += '<table style="width: 100%; border-collapse: collapse; font-size: 14px;">';
|
||||||
|
html += '<thead><tr style="background: #f8f9fa;">';
|
||||||
|
html += '<th style="padding: 8px; text-align: left; border: 1px solid #dee2e6;">Library</th>';
|
||||||
|
html += '<th style="padding: 8px; text-align: left; border: 1px solid #dee2e6;">Footprint</th>';
|
||||||
|
html += '<th style="padding: 8px; text-align: left; border: 1px solid #dee2e6;">Status</th>';
|
||||||
|
html += '<th style="padding: 8px; text-align: left; border: 1px solid #dee2e6;">File</th>';
|
||||||
|
html += '</tr></thead><tbody>';
|
||||||
|
|
||||||
|
for (const fp of data.footprints) {
|
||||||
|
const statusColor = getChangeTypeColor(fp.change_type);
|
||||||
|
html += '<tr>';
|
||||||
|
html += `<td style="padding: 8px; border: 1px solid #dee2e6;"><strong>${fp.library}</strong></td>`;
|
||||||
|
html += `<td style="padding: 8px; border: 1px solid #dee2e6;">${fp.footprint}</td>`;
|
||||||
|
html += `<td style="padding: 8px; border: 1px solid #dee2e6;"><span style="color: ${statusColor}; font-weight: bold;">${fp.change_type.toUpperCase()}</span></td>`;
|
||||||
|
html += `<td style="padding: 8px; border: 1px solid #dee2e6; font-family: monospace; font-size: 12px;">${fp.file}</td>`;
|
||||||
|
html += '</tr>';
|
||||||
|
}
|
||||||
|
html += '</tbody></table></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other Changes
|
||||||
|
if (data.other && data.other.length > 0) {
|
||||||
|
html += '<div style="margin-bottom: 20px;">';
|
||||||
|
html += '<h3 style="color: #6c757d; border-bottom: 2px solid #6c757d; padding-bottom: 5px;">Other Files</h3>';
|
||||||
|
html += '<table style="width: 100%; border-collapse: collapse; font-size: 14px;">';
|
||||||
|
html += '<thead><tr style="background: #f8f9fa;">';
|
||||||
|
html += '<th style="padding: 8px; text-align: left; border: 1px solid #dee2e6;">File</th>';
|
||||||
|
html += '<th style="padding: 8px; text-align: left; border: 1px solid #dee2e6;">Status</th>';
|
||||||
|
html += '</tr></thead><tbody>';
|
||||||
|
|
||||||
|
for (const other of data.other) {
|
||||||
|
const statusColor = getChangeTypeColor(other.change_type);
|
||||||
|
html += '<tr>';
|
||||||
|
html += `<td style="padding: 8px; border: 1px solid #dee2e6; font-family: monospace; font-size: 12px;">${other.file}</td>`;
|
||||||
|
html += `<td style="padding: 8px; border: 1px solid #dee2e6;"><span style="color: ${statusColor}; font-weight: bold;">${other.change_type.toUpperCase()}</span></td>`;
|
||||||
|
html += '</tr>';
|
||||||
|
}
|
||||||
|
html += '</tbody></table></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
content.innerHTML = html;
|
||||||
|
});
|
||||||
|
|
||||||
|
function getChangeTypeColor(changeType) {
|
||||||
|
switch(changeType) {
|
||||||
|
case 'added': return '#28a745';
|
||||||
|
case 'deleted': return '#dc3545';
|
||||||
|
case 'modified': return '#ffc107';
|
||||||
|
case 'untracked': return '#17a2b8';
|
||||||
|
default: return '#6c757d';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit and Push Handlers
|
||||||
|
const gitActionMessage = document.getElementById('gitActionMessage');
|
||||||
|
const commitChangesBtn = document.getElementById('commitChangesBtn');
|
||||||
|
const commitPushBtn = document.getElementById('commitPushBtn');
|
||||||
|
const commitMessageInput = document.getElementById('commitMessage');
|
||||||
|
|
||||||
|
function showGitActionMessage(text, type) {
|
||||||
|
gitActionMessage.textContent = text;
|
||||||
|
gitActionMessage.className = 'message ' + type;
|
||||||
|
gitActionMessage.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
commitChangesBtn.addEventListener('click', () => {
|
||||||
|
const message = commitMessageInput.value.trim();
|
||||||
|
if (!message) {
|
||||||
|
showGitActionMessage('Please enter a commit message', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
commitChangesBtn.disabled = true;
|
||||||
|
commitPushBtn.disabled = true;
|
||||||
|
showGitActionMessage('Committing changes...', 'info');
|
||||||
|
|
||||||
|
socket.emit('commit_library_changes', { message: message });
|
||||||
|
});
|
||||||
|
|
||||||
|
commitPushBtn.addEventListener('click', () => {
|
||||||
|
const message = commitMessageInput.value.trim();
|
||||||
|
if (!message) {
|
||||||
|
showGitActionMessage('Please enter a commit message', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
commitChangesBtn.disabled = true;
|
||||||
|
commitPushBtn.disabled = true;
|
||||||
|
showGitActionMessage('Committing and pushing changes...', 'info');
|
||||||
|
|
||||||
|
socket.emit('commit_push_library_changes', { message: message });
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('library_commit_status', (data) => {
|
||||||
|
showGitActionMessage(data.status, 'info');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('library_commit_complete', (data) => {
|
||||||
|
showGitActionMessage(data.message, 'success');
|
||||||
|
commitChangesBtn.disabled = false;
|
||||||
|
commitPushBtn.disabled = false;
|
||||||
|
commitMessageInput.value = '';
|
||||||
|
|
||||||
|
// Refresh the changes list
|
||||||
|
setTimeout(() => {
|
||||||
|
socket.emit('get_library_changes');
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('library_commit_error', (data) => {
|
||||||
|
// Format error message with line breaks preserved
|
||||||
|
const errorMsg = data.error.replace(/\n/g, '<br>');
|
||||||
|
gitActionMessage.innerHTML = '<strong>Error:</strong><br>' + errorMsg;
|
||||||
|
gitActionMessage.className = 'message error';
|
||||||
|
gitActionMessage.style.display = 'block';
|
||||||
|
commitChangesBtn.disabled = false;
|
||||||
|
commitPushBtn.disabled = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check git status on initial load if in library-only mode
|
||||||
|
if (libraryOnlyMode) {
|
||||||
|
checkLibraryGitStatus();
|
||||||
|
}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// Library Browser
|
// Library Browser
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|||||||
Reference in New Issue
Block a user