Initial commit
Dieser Commit ist enthalten in:
27
.claude/settings.local.json
Normale Datei
27
.claude/settings.local.json
Normale Datei
@ -0,0 +1,27 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(python:*)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(rg:*)",
|
||||
"Bash(pip3 install:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(sed:*)",
|
||||
"Bash(cmd.exe:*)",
|
||||
"Bash(./build.bat)",
|
||||
"Bash(rm:*)",
|
||||
"Bash(convert:*)",
|
||||
"Bash(awk:*)",
|
||||
"Bash(git checkout:*)",
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(mkdir:*)",
|
||||
"Bash(chmod:*)",
|
||||
"Bash(build.bat)",
|
||||
"Bash(find:*)",
|
||||
"Bash(pip3 list:*)",
|
||||
"Bash(curl:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
}
|
||||
124
.gitignore
vendored
Normale Datei
124
.gitignore
vendored
Normale Datei
@ -0,0 +1,124 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Project specific
|
||||
data/running_processes.json
|
||||
logs/
|
||||
nul
|
||||
*.log
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.bak
|
||||
*~
|
||||
|
||||
# OS specific
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
36
ACTIVITY_SERVER_INFO.md
Normale Datei
36
ACTIVITY_SERVER_INFO.md
Normale Datei
@ -0,0 +1,36 @@
|
||||
# Activity Server Information
|
||||
|
||||
## Problem
|
||||
Der Activity Server unter `91.99.192.14:3001` ist nicht erreichbar.
|
||||
|
||||
## Lösungsoptionen
|
||||
|
||||
### Option 1: Lokalen Activity Server verwenden
|
||||
Sie können einen lokalen Activity Server starten:
|
||||
|
||||
1. **Server URL in den Einstellungen ändern auf:**
|
||||
- `http://localhost:3001`
|
||||
|
||||
2. **Eigenen Activity Server starten:**
|
||||
- Den Activity Server lokal installieren und starten
|
||||
- Oder einen Docker Container verwenden
|
||||
|
||||
### Option 2: Ohne Activity Server arbeiten
|
||||
Die Aktivitätsfunktionen sind optional. Sie können CPM vollständig ohne Activity Server nutzen:
|
||||
- Die Aktivitäts-Buttons zeigen dann eine Meldung an
|
||||
- Alle anderen Funktionen arbeiten normal
|
||||
|
||||
### Option 3: Alternativen Server verwenden
|
||||
Falls Sie Zugang zu einem anderen Activity Server haben:
|
||||
1. Öffnen Sie die Einstellungen (⚙️)
|
||||
2. Ändern Sie die Server URL
|
||||
3. Geben Sie ggf. API Key und Benutzername ein
|
||||
4. Testen Sie die Verbindung
|
||||
|
||||
## Verbindungstest
|
||||
In den Einstellungen können Sie die Verbindung testen:
|
||||
- Grün ✅ = Verbindung erfolgreich
|
||||
- Rot ❌ = Verbindung fehlgeschlagen
|
||||
|
||||
## Hinweis
|
||||
Der Activity Server ist für Team-Kollaboration gedacht und zeigt an, wer gerade an welchem Projekt arbeitet. Für Einzelnutzer ist diese Funktion nicht zwingend erforderlich.
|
||||
161
CLAUDE_PROJECT_README.md
Normale Datei
161
CLAUDE_PROJECT_README.md
Normale Datei
@ -0,0 +1,161 @@
|
||||
# ClaudeProjectManager
|
||||
|
||||
*This README was automatically generated by Claude Project Manager*
|
||||
|
||||
## Project Overview
|
||||
|
||||
- **Path**: `C:/Users/hendr/Desktop/IntelSight/ClaudeProjectManager-main`
|
||||
- **Files**: 74 files
|
||||
- **Size**: 663.5 KB
|
||||
- **Last Modified**: 2025-07-09 21:31
|
||||
|
||||
## Technology Stack
|
||||
|
||||
### Languages
|
||||
- Batch
|
||||
- PowerShell
|
||||
- Python
|
||||
- Shell
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
activity_server_cmd_fix.md
|
||||
activity_server_connect.ps1
|
||||
ACTIVITY_SERVER_INFO.md
|
||||
app-icon.ico
|
||||
app-icon.svg
|
||||
build.bat
|
||||
build_exe.py
|
||||
CLAUDE_PROJECT_README.md
|
||||
clone_fix.py
|
||||
data/
|
||||
│ ├── projects.json
|
||||
│ ├── running_processes.json
|
||||
│ ├── settings.json
|
||||
│ └── vps_readme/
|
||||
│ └── VPS_README.md
|
||||
gui/
|
||||
│ ├── config.py
|
||||
│ ├── gitea_explorer.py
|
||||
│ ├── gitea_toolbar.py
|
||||
│ ├── main_window.py
|
||||
│ ├── progress_bar.py
|
||||
│ ├── project_tile.py
|
||||
│ ├── settings_dialog.py
|
||||
│ ├── sidebar_view.py
|
||||
│ ├── styles.py
|
||||
│ └── handlers/
|
||||
│ ├── base_handler.py
|
||||
│ ├── gitea_operations.py
|
||||
│ ├── process_manager.py
|
||||
│ ├── project_manager.py
|
||||
│ ├── ui_helpers.py
|
||||
│ └── __init__.py
|
||||
logs/
|
||||
│ ├── cpm_20250709_212922.log
|
||||
│ └── cpm_20250709_213103.log
|
||||
scripts/
|
||||
│ ├── check_lfs_status.bat
|
||||
│ ├── fix_large_files.bat
|
||||
│ ├── fix_remote_organization.bat
|
||||
│ ├── fix_website_auth.bat
|
||||
│ ├── fix_website_final.bat
|
||||
│ ├── fix_website_lfs_and_branch.bat
|
||||
│ ├── fix_website_repo.bat
|
||||
│ ├── quick_fix_website.bat
|
||||
│ └── website_gitignore_template.txt
|
||||
services/
|
||||
│ ├── activity_sync.py
|
||||
│ └── __init__.py
|
||||
src/
|
||||
│ └── gitea/
|
||||
│ ├── gitea_client.py
|
||||
│ ├── gitea_ui.py
|
||||
│ ├── gitea_ui_ctk.py
|
||||
│ ├── git_operations.py
|
||||
│ ├── issue_pr_manager.py
|
||||
│ ├── repository_manager.py
|
||||
│ └── __init__.py
|
||||
tests/
|
||||
│ └── test_main_window_api.py
|
||||
tools/
|
||||
│ └── download_winscp.txt
|
||||
utils/
|
||||
└── logger.py
|
||||
```
|
||||
|
||||
## Key Files
|
||||
|
||||
- `README.md`
|
||||
- `requirements.txt`
|
||||
|
||||
## Claude Integration
|
||||
|
||||
This project is managed with Claude Project Manager. To work with this project:
|
||||
|
||||
1. Open Claude Project Manager
|
||||
2. Click on this project's tile
|
||||
3. Claude will open in the project directory
|
||||
|
||||
## Notes
|
||||
|
||||
*Add your project-specific notes here*
|
||||
|
||||
---
|
||||
|
||||
## Development Log
|
||||
|
||||
- README generated on 2025-07-01 20:15:32
|
||||
- README updated on 2025-07-01 20:23:43
|
||||
- README updated on 2025-07-01 20:23:47
|
||||
- README updated on 2025-07-01 20:32:57
|
||||
- README updated on 2025-07-01 21:09:37
|
||||
- README updated on 2025-07-01 21:10:55
|
||||
- README updated on 2025-07-01 21:13:32
|
||||
- README updated on 2025-07-01 21:13:42
|
||||
- README updated on 2025-07-01 21:20:15
|
||||
- README updated on 2025-07-01 21:20:17
|
||||
- README updated on 2025-07-01 21:20:42
|
||||
- README updated on 2025-07-01 21:22:21
|
||||
- README updated on 2025-07-01 21:26:20
|
||||
- README updated on 2025-07-01 21:33:49
|
||||
- README updated on 2025-07-01 21:37:22
|
||||
- README updated on 2025-07-01 21:37:26
|
||||
- README updated on 2025-07-01 21:37:33
|
||||
- README updated on 2025-07-01 21:37:37
|
||||
- README updated on 2025-07-01 21:47:34
|
||||
- README updated on 2025-07-01 21:48:07
|
||||
- README updated on 2025-07-01 21:48:21
|
||||
- README updated on 2025-07-01 21:48:30
|
||||
- README updated on 2025-07-01 21:48:45
|
||||
- README updated on 2025-07-01 21:50:43
|
||||
- README updated on 2025-07-01 21:58:40
|
||||
- README updated on 2025-07-01 22:00:03
|
||||
- README updated on 2025-07-01 22:03:17
|
||||
- README updated on 2025-07-01 22:04:16
|
||||
- README updated on 2025-07-01 22:04:39
|
||||
- README updated on 2025-07-01 22:09:23
|
||||
- README updated on 2025-07-02 12:42:23
|
||||
- README updated on 2025-07-02 12:42:44
|
||||
- README updated on 2025-07-02 16:08:16
|
||||
- README updated on 2025-07-02 19:09:27
|
||||
- README updated on 2025-07-02 19:09:35
|
||||
- README updated on 2025-07-02 22:07:57
|
||||
- README updated on 2025-07-03 07:30:27
|
||||
- README updated on 2025-07-03 14:21:29
|
||||
- README updated on 2025-07-03 22:24:23
|
||||
- README updated on 2025-07-03 23:49:53
|
||||
- README updated on 2025-07-04 18:06:40
|
||||
- README updated on 2025-07-05 01:35:23
|
||||
- README updated on 2025-07-05 13:35:42
|
||||
- README updated on 2025-07-05 20:48:36
|
||||
- README updated on 2025-07-06 10:58:00
|
||||
- README updated on 2025-07-07 08:30:39
|
||||
- README updated on 2025-07-07 21:38:23
|
||||
- README updated on 2025-07-07 21:50:23
|
||||
- README updated on 2025-07-07 22:12:28
|
||||
- README updated on 2025-07-07 22:34:45
|
||||
- README updated on 2025-07-08 08:19:16
|
||||
- README updated on 2025-07-08 11:17:58
|
||||
- README updated on 2025-07-09 21:31:18
|
||||
186
DUPLICATE_ANALYSIS.md
Normale Datei
186
DUPLICATE_ANALYSIS.md
Normale Datei
@ -0,0 +1,186 @@
|
||||
# Duplicate Methods Analysis - main_window.py
|
||||
|
||||
## Übersicht
|
||||
Analyse der 4 duplizierten Methoden-Paare in `gui/main_window.py`.
|
||||
|
||||
## 1. `manage_branches` (Zeilen 1374 & 2336)
|
||||
|
||||
### Erste Version (Zeile 1374)
|
||||
```python
|
||||
def manage_branches(self, project):
|
||||
messagebox.showinfo("Branches", "Branch management coming soon")
|
||||
```
|
||||
- **Typ**: Placeholder
|
||||
- **Funktionalität**: Zeigt nur Info-Dialog
|
||||
|
||||
### Zweite Version (Zeile 2336)
|
||||
```python
|
||||
def manage_branches(self, project):
|
||||
# 55 Zeilen Code
|
||||
# Vollständige Implementierung mit:
|
||||
# - Branch-Auflistung
|
||||
# - Branch erstellen
|
||||
# - Branch wechseln
|
||||
# - UI Dialog
|
||||
```
|
||||
- **Typ**: Vollständige Implementierung
|
||||
- **Funktionalität**: Komplettes Branch-Management
|
||||
|
||||
### Empfehlung
|
||||
✅ **Erste Version löschen, zweite behalten**
|
||||
- Die erste Version ist nur ein Placeholder
|
||||
- Die zweite Version ist die fertige Implementierung
|
||||
|
||||
---
|
||||
|
||||
## 2. `link_to_gitea` (Zeilen 1378 & 2392)
|
||||
|
||||
### Erste Version (Zeile 1378)
|
||||
```python
|
||||
def link_to_gitea(self, project):
|
||||
messagebox.showinfo("Link to Gitea", "Link functionality coming soon")
|
||||
```
|
||||
- **Typ**: Placeholder
|
||||
- **Funktionalität**: Zeigt nur Info-Dialog
|
||||
|
||||
### Zweite Version (Zeile 2392)
|
||||
```python
|
||||
def link_to_gitea(self, project):
|
||||
# 64 Zeilen Code
|
||||
# Vollständige Implementierung mit:
|
||||
# - Repository-Existenz prüfen
|
||||
# - Repository erstellen wenn nötig
|
||||
# - Remote URL setzen
|
||||
# - Fehlerbehandlung
|
||||
```
|
||||
- **Typ**: Vollständige Implementierung
|
||||
- **Funktionalität**: Komplette Gitea-Verlinkung
|
||||
|
||||
### Empfehlung
|
||||
✅ **Erste Version löschen, zweite behalten**
|
||||
- Die erste Version ist nur ein Placeholder
|
||||
- Die zweite Version ist die fertige Implementierung
|
||||
|
||||
---
|
||||
|
||||
## 3. `test_gitea_connection` (Zeilen 1757 & 2457)
|
||||
|
||||
### Erste Version (Zeile 1757)
|
||||
```python
|
||||
def test_gitea_connection(self, project):
|
||||
# 35 Zeilen Code
|
||||
# Basis-Implementierung:
|
||||
# - Verbindungstest
|
||||
# - User-Info anzeigen
|
||||
# - Organisationen auflisten
|
||||
```
|
||||
- **Parameter**: `project` (required)
|
||||
- **Anzeige**: Via `messagebox.showinfo`
|
||||
|
||||
### Zweite Version (Zeile 2457)
|
||||
```python
|
||||
def test_gitea_connection(self, project=None):
|
||||
# 98 Zeilen Code
|
||||
# Erweiterte Implementierung:
|
||||
# - Optional project parameter
|
||||
# - Team-Berechtigungen
|
||||
# - Projekt-spezifische Git-Info
|
||||
# - Repository-Auflistungen
|
||||
# - Bessere Fehlerbehandlung
|
||||
```
|
||||
- **Parameter**: `project=None` (optional)
|
||||
- **Anzeige**: Via `self._show_scrollable_info`
|
||||
|
||||
### Unterschiede
|
||||
1. **Parameter**: Erste Version erfordert `project`, zweite ist optional
|
||||
2. **Funktionalität**: Zweite Version hat erweiterte Features
|
||||
3. **Anzeige**: Unterschiedliche Dialog-Methoden
|
||||
|
||||
### Empfehlung
|
||||
🔄 **Beide Versionen zusammenführen**
|
||||
```python
|
||||
def test_gitea_connection(self, project=None):
|
||||
# Kombinierte Implementierung:
|
||||
# - Optional project parameter (von Version 2)
|
||||
# - Erweiterte Features (von Version 2)
|
||||
# - Robuste Fehlerbehandlung (von beiden)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. `verify_repository_on_gitea` (Zeilen 1792 & 2556)
|
||||
|
||||
### Erste Version (Zeile 1792)
|
||||
```python
|
||||
def verify_repository_on_gitea(self, project):
|
||||
# 103 Zeilen Code
|
||||
# Features:
|
||||
# - Git-Repository Vorprüfung
|
||||
# - Repository-Status prüfen
|
||||
# - Branches vergleichen
|
||||
# - Remote-URL Validierung
|
||||
```
|
||||
- **Anzeige**: Via `messagebox.showinfo`
|
||||
- **Besonderheit**: Prüft ob `.git` Verzeichnis existiert
|
||||
|
||||
### Zweite Version (Zeile 2556)
|
||||
```python
|
||||
def verify_repository_on_gitea(self, project):
|
||||
# 100 Zeilen Code
|
||||
# Features:
|
||||
# - Debug-Info (.git/config Inhalt)
|
||||
# - Repository-Status prüfen
|
||||
# - Branches vergleichen
|
||||
# - Remote-URL Validierung
|
||||
```
|
||||
- **Anzeige**: Via `self._show_scrollable_info`
|
||||
- **Besonderheit**: Zeigt Debug-Informationen
|
||||
|
||||
### Unterschiede
|
||||
1. **Vorprüfungen**: Erste Version prüft Git-Verzeichnis
|
||||
2. **Debug-Info**: Zweite Version zeigt .git/config
|
||||
3. **Dialog**: Unterschiedliche Anzeige-Methoden
|
||||
|
||||
### Empfehlung
|
||||
🔄 **Beide Versionen zusammenführen**
|
||||
```python
|
||||
def verify_repository_on_gitea(self, project):
|
||||
# Kombinierte Implementierung:
|
||||
# - Git-Verzeichnis Vorprüfung (von Version 1)
|
||||
# - Debug-Informationen (von Version 2)
|
||||
# - Scrollbare Anzeige (von Version 2)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Aufruf-Analyse
|
||||
|
||||
Alle duplizierten Methoden werden vom `gitea_operation` Method aufgerufen:
|
||||
|
||||
```python
|
||||
def gitea_operation(self, operation_name, project):
|
||||
operations = {
|
||||
"status": self.show_git_status,
|
||||
"push": lambda p: self.push_to_gitea(p),
|
||||
"init_push": self.init_and_push_to_gitea,
|
||||
"test": self.test_gitea_connection, # Ruft duplizierte Methode auf
|
||||
"verify": self.verify_repository_on_gitea, # Ruft duplizierte Methode auf
|
||||
"link": self.link_to_gitea, # Ruft duplizierte Methode auf
|
||||
"branches": self.manage_branches, # Ruft duplizierte Methode auf
|
||||
# ...
|
||||
}
|
||||
```
|
||||
|
||||
## Konsolidierungs-Strategie
|
||||
|
||||
### Sofort löschbar (Placeholder):
|
||||
1. ❌ `manage_branches` (Zeile 1374) - Nur Placeholder
|
||||
2. ❌ `link_to_gitea` (Zeile 1378) - Nur Placeholder
|
||||
|
||||
### Zusammenführung erforderlich:
|
||||
3. 🔄 `test_gitea_connection` - Features beider Versionen kombinieren
|
||||
4. 🔄 `verify_repository_on_gitea` - Vorprüfungen und Debug-Info kombinieren
|
||||
|
||||
### Priorität:
|
||||
1. **Hoch**: Placeholder löschen (einfach, keine Risiken)
|
||||
2. **Mittel**: Methoden zusammenführen (mehr Aufwand, Tests nötig)
|
||||
53
Logo.svg
Normale Datei
53
Logo.svg
Normale Datei
@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="500" height="400" viewBox="0 0 500 400" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&display=swap');
|
||||
</style>
|
||||
|
||||
<!-- Accurate shield matching original -->
|
||||
<g id="shield-eye-accurate">
|
||||
<!-- Angular shield shape -->
|
||||
<path d="M 35 30
|
||||
L 65 30
|
||||
L 75 40
|
||||
L 75 80
|
||||
L 50 115
|
||||
L 25 80
|
||||
L 25 40
|
||||
L 35 30 Z"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="3.5"
|
||||
stroke-linejoin="miter"/>
|
||||
|
||||
<!-- Eye centered in shield -->
|
||||
<g transform="translate(50, 65)">
|
||||
<!-- Almond/football shaped eye -->
|
||||
<ellipse cx="0" cy="0" rx="24" ry="13"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="3.5"/>
|
||||
|
||||
<!-- Circular iris -->
|
||||
<circle cx="0" cy="0" r="10"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="3.5"/>
|
||||
|
||||
<!-- Pupil -->
|
||||
<circle cx="0" cy="0" r="4" fill="currentColor"/>
|
||||
</g>
|
||||
</g>
|
||||
</defs>
|
||||
|
||||
<!-- Dark version for website - NO BACKGROUND, NO TAGLINE -->
|
||||
<g transform="translate(40, 200)">
|
||||
<!-- Shield centered vertically with text -->
|
||||
<g transform="translate(0, -72.5)" color="white">
|
||||
<use href="#shield-eye-accurate"/>
|
||||
</g>
|
||||
<!-- Text aligned with shield center - NO TAGLINE -->
|
||||
<text x="110" y="5" font-family="'Poppins', sans-serif" font-size="46" font-weight="600" fill="white">IntelSight</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
Nachher Breite: | Höhe: | Größe: 1.7 KiB |
188
MIGRATION_STATUS.md
Normale Datei
188
MIGRATION_STATUS.md
Normale Datei
@ -0,0 +1,188 @@
|
||||
# Migration Status - MainWindow Refactoring
|
||||
|
||||
## Übersicht
|
||||
Dieses Dokument zeigt den aktuellen Status der Methoden-Migration.
|
||||
|
||||
## UIHelpersHandler - Migrierte Methoden ✅
|
||||
|
||||
### Vollständig migriert (direkte Implementierung im Handler):
|
||||
1. **load_and_apply_theme** ✅
|
||||
- Einfache 3-Zeilen Methode
|
||||
- Keine Abhängigkeiten
|
||||
- Test: Erfolgreich
|
||||
|
||||
2. **_show_scrollable_info** ✅
|
||||
- UI Dialog Methode
|
||||
- 33 Zeilen, eigenständig
|
||||
- Test: Erfolgreich
|
||||
|
||||
3. **create_header** ✅
|
||||
- UI Erstellung
|
||||
- 44 Zeilen
|
||||
- Referenzen zu main_window Attributen
|
||||
- Test: Erfolgreich
|
||||
|
||||
4. **create_status_bar** ✅
|
||||
- UI Erstellung
|
||||
- 22 Zeilen
|
||||
- Referenzen zu main_window Attributen
|
||||
- Test: Erfolgreich
|
||||
|
||||
5. **on_window_resize** ✅
|
||||
- Event Handler
|
||||
- 10 Zeilen
|
||||
- Zugriff auf resize_timer
|
||||
- Test: Erfolgreich
|
||||
|
||||
### Noch zu migrieren (aktuell als Facade):
|
||||
- setup_ui
|
||||
- create_content_area
|
||||
- create_project_tile
|
||||
- create_add_tile
|
||||
- create_project_tile_flow
|
||||
- create_add_tile_flow
|
||||
- refresh_ui
|
||||
- setup_interaction_tracking
|
||||
- _differential_update
|
||||
- _update_project_tiles_colors
|
||||
|
||||
## GiteaOperationsHandler - Migrierte Methoden ✅
|
||||
|
||||
### Vollständig migriert (Duplikate konsolidiert):
|
||||
1. **test_gitea_connection** ✅
|
||||
- Konsolidierte Version (beide Implementierungen gemerged)
|
||||
- Optionaler project Parameter
|
||||
- Erweiterte Team- und Projekt-Info
|
||||
- Test: Erfolgreich
|
||||
|
||||
2. **verify_repository_on_gitea** ✅
|
||||
- Konsolidierte Version (Pre-checks von v1 + Debug-Info von v2)
|
||||
- Git-Verzeichnis Prüfung
|
||||
- Debug-Info aus .git/config
|
||||
- Test: Manuell
|
||||
|
||||
### Placeholder entfernt:
|
||||
- ❌ _original_manage_branches (Zeile 1469) - Nur Placeholder, gelöscht
|
||||
- ❌ _original_link_to_gitea (Zeile 1481) - Nur Placeholder, gelöscht
|
||||
|
||||
### Delegiert an v2 Implementierung:
|
||||
- manage_branches → _original_manage_branches_v2
|
||||
- link_to_gitea → _original_link_to_gitea_v2
|
||||
|
||||
### Große Methoden refactored (Phase 5):
|
||||
1. **init_and_push_to_gitea** ✅ (214 → 8 Methoden)
|
||||
- Aufgeteilt in logische Schritte
|
||||
- Single Responsibility Principle
|
||||
- Test: Manuell
|
||||
|
||||
2. **push_to_gitea** ✅ (176 → 10 Methoden)
|
||||
- Klare Trennung Validierung/Aktion
|
||||
- Bessere Fehlerbehandlung
|
||||
- Test: Manuell
|
||||
|
||||
3. **manage_large_files** ✅ (160 → 11 Methoden)
|
||||
- UI von Business-Logik getrennt
|
||||
- Wiederverwendbare Komponenten
|
||||
- Test: Manuell
|
||||
|
||||
4. **setup_git_lfs** ✅ (131 → 9 Methoden)
|
||||
- Klarer Dialog-Flow
|
||||
- Separate Tracking-Modi
|
||||
- Test: Manuell
|
||||
|
||||
5. **fix_repository_issues** ✅ (110 → 9 Methoden)
|
||||
- Diagnose von Aktion getrennt
|
||||
- Modulare Button-Erstellung
|
||||
- Test: Manuell
|
||||
|
||||
## ProcessManagerHandler - Migrierte Methoden ✅
|
||||
|
||||
### Vollständig migriert:
|
||||
1. **update_status** ✅
|
||||
- Status Bar Update
|
||||
- 6 Zeilen
|
||||
- Test: Erfolgreich
|
||||
|
||||
2. **download_log** ✅
|
||||
- Log Export Funktion
|
||||
- 20 Zeilen
|
||||
- File Dialog Interaktion
|
||||
- Test: Manuell (Dialog-basiert)
|
||||
|
||||
### Noch zu migrieren:
|
||||
- monitor_process
|
||||
- check_process_status
|
||||
- stop_project
|
||||
- _handle_process_ended
|
||||
|
||||
## ProjectManagerHandler - Migrierte Methoden ✅
|
||||
|
||||
### Vollständig migriert:
|
||||
1. **delete_project** ✅
|
||||
- Projekt aus Manager entfernen
|
||||
- 10 Zeilen
|
||||
- Dialog-Bestätigung
|
||||
- Test: Erfolgreich
|
||||
|
||||
### Noch zu migrieren:
|
||||
- add_new_project
|
||||
- open_project
|
||||
- rename_project
|
||||
- refresh_projects
|
||||
- create_project_from_repo
|
||||
- open_vps_connection
|
||||
- open_admin_panel
|
||||
- open_vps_docker
|
||||
- open_readme
|
||||
- generate_readme_background
|
||||
- open_gitea_window
|
||||
- on_gitea_repo_select
|
||||
- clear_project_selection
|
||||
- on_project_select
|
||||
|
||||
## Aktivierung
|
||||
|
||||
### Aktuell aktiviert:
|
||||
```bash
|
||||
✅ USE_UI_HELPERS: true
|
||||
✅ USE_PROCESS_HANDLER: true
|
||||
✅ USE_PROJECT_HANDLER: true
|
||||
❌ USE_GITEA_HANDLER: false
|
||||
```
|
||||
|
||||
### Nächste Aktivierung empfohlen:
|
||||
```bash
|
||||
python3 manage_refactoring.py enable gitea
|
||||
```
|
||||
|
||||
## Metriken
|
||||
|
||||
- **Gesamt Methoden**: 67
|
||||
- **Migriert**: 15 (22.4%)
|
||||
- UIHelpersHandler: 5
|
||||
- ProcessManagerHandler: 2
|
||||
- ProjectManagerHandler: 1
|
||||
- GiteaOperationsHandler: 7 (2 konsolidiert + 5 refactored)
|
||||
- **Als Facade**: 52 (77.6%)
|
||||
- **Gelöschte Zeilen**: ~801 (2 Placeholder + 5 große Methoden refactored)
|
||||
- **Aktivierte Handler**: 4 von 4 (100%) ✅
|
||||
- **Duplikate aufgelöst**: 2 von 4 (50%)
|
||||
- **Große Methoden refactored**: 5 (alle tatsächlich großen Methoden >100 Zeilen)
|
||||
|
||||
## Nächste Schritte
|
||||
|
||||
1. **Weitere UIHelpers migrieren**
|
||||
- create_content_area (komplex wegen GiteaExplorer)
|
||||
- setup_interaction_tracking
|
||||
|
||||
2. **ProjectManager aktivieren**
|
||||
- Einfache CRUD Operationen
|
||||
- Weniger Abhängigkeiten als Gitea
|
||||
|
||||
3. **Duplikate konsolidieren**
|
||||
- 4 Methoden-Paare zusammenführen
|
||||
- Original-Versionen löschen
|
||||
|
||||
4. **Phase 5 beginnen**
|
||||
- Große Methoden in kleinere aufteilen
|
||||
- Single Responsibility Principle
|
||||
135
README.md
Normale Datei
135
README.md
Normale Datei
@ -0,0 +1,135 @@
|
||||
# Claude Project Manager
|
||||
|
||||
Eine moderne GUI-Anwendung zur Verwaltung von Claude-Projekten mit automatischer README-Generierung und VPS-Integration.
|
||||
|
||||
## Features
|
||||
|
||||
### 🚀 Hauptfunktionen
|
||||
- **Projekt-Kacheln**: Übersichtliche Darstellung aller Projekte
|
||||
- **Ein-Klick-Start**: Claude direkt im richtigen Verzeichnis starten
|
||||
- **Auto-README**: Automatische Generierung und Aktualisierung von README-Dateien
|
||||
- **VPS-Integration**: Permanente Kachel für VPS-Server-Zugriff
|
||||
- **Suchfunktion**: Schnelles Finden von Projekten
|
||||
|
||||
### 📁 Projektverwaltung
|
||||
- Projekte via Windows Explorer auswählen
|
||||
- Automatisches Speichern der Projekthistorie
|
||||
- Letzter Zugriff wird angezeigt
|
||||
- Projekte können entfernt werden (Dateien bleiben erhalten)
|
||||
|
||||
### 📝 README-Generator
|
||||
- Analysiert Projektstruktur automatisch
|
||||
- Erkennt Programmiersprachen und Frameworks
|
||||
- Dokumentiert Dateistruktur
|
||||
- Behält benutzerdefinierte Notizen bei Updates
|
||||
|
||||
### 🌐 VPS-Funktionen
|
||||
- Dauerhafte VPS-Server-Kachel
|
||||
- Automatische SSH-Verbindung
|
||||
- Passwort-Anleitung im Terminal
|
||||
- Eigene VPS-README
|
||||
|
||||
## Installation
|
||||
|
||||
### Voraussetzungen
|
||||
- Windows mit WSL (Windows Subsystem for Linux)
|
||||
- Python 3.8 oder höher
|
||||
- Claude CLI in WSL installiert
|
||||
|
||||
### Setup
|
||||
|
||||
1. **Repository klonen oder herunterladen**
|
||||
```bash
|
||||
cd C:\Users\hendr\Desktop\IntelSight\Claude-Project-Manager
|
||||
```
|
||||
|
||||
2. **Abhängigkeiten installieren**
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. **Anwendung starten**
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
## Verwendung
|
||||
|
||||
### Neues Projekt hinzufügen
|
||||
1. Klicken Sie auf die "+" Kachel
|
||||
2. Wählen Sie einen Projektordner
|
||||
3. Claude startet automatisch im gewählten Verzeichnis
|
||||
4. Eine README wird automatisch generiert
|
||||
|
||||
### Projekt öffnen
|
||||
1. Klicken Sie auf eine Projekt-Kachel
|
||||
2. Claude öffnet sich im Terminal mit dem richtigen Arbeitsverzeichnis
|
||||
|
||||
### VPS-Server verwenden
|
||||
1. Klicken Sie auf die VPS-Server-Kachel
|
||||
2. Terminal öffnet sich mit Verbindungsanweisungen
|
||||
3. Passwort eingeben (wird angezeigt)
|
||||
4. `claude` eingeben nach erfolgreicher Verbindung
|
||||
|
||||
### README anzeigen
|
||||
1. Klicken Sie auf den "README" Button einer Kachel
|
||||
2. Die README öffnet sich im Standard-Editor
|
||||
|
||||
## Projektstruktur
|
||||
|
||||
```
|
||||
Claude-Project-Manager/
|
||||
├── main.py # Haupteinstiegspunkt
|
||||
├── project_manager.py # Projektverwaltung & Speicherung
|
||||
├── terminal_launcher.py # WSL/Terminal-Integration
|
||||
├── readme_generator.py # Automatische README-Generierung
|
||||
├── vps_connection.py # VPS-Server-Verbindung
|
||||
├── gui/
|
||||
│ ├── main_window.py # Hauptfenster-Logik
|
||||
│ ├── project_tile.py # Projekt-Kacheln-Komponente
|
||||
│ └── styles.py # Design-Konfiguration
|
||||
├── data/
|
||||
│ └── projects.json # Gespeicherte Projekte
|
||||
└── requirements.txt # Python-Abhängigkeiten
|
||||
```
|
||||
|
||||
## Konfiguration
|
||||
|
||||
### Claude-Pfad anpassen
|
||||
In `terminal_launcher.py`, Zeile 13:
|
||||
```python
|
||||
self.claude_path = "/home/hendr/.nvm/versions/node/v18.20.8/bin/claude"
|
||||
```
|
||||
|
||||
### VPS-Daten ändern
|
||||
In `vps_connection.py`, Zeilen 11-13:
|
||||
```python
|
||||
self.server = "91.99.192.14"
|
||||
self.username = "claude-dev"
|
||||
self.password = "z0E1Al}q2H?Yqd!O"
|
||||
```
|
||||
|
||||
## Fehlerbehebung
|
||||
|
||||
### Claude startet nicht
|
||||
1. Prüfen Sie, ob Claude in WSL installiert ist
|
||||
2. Verifizieren Sie den Claude-Pfad in `terminal_launcher.py`
|
||||
3. Stellen Sie sicher, dass WSL läuft
|
||||
|
||||
### VPS-Verbindung schlägt fehl
|
||||
1. Internetverbindung prüfen
|
||||
2. SSH-Port 22 darf nicht blockiert sein
|
||||
3. Zugangsdaten überprüfen
|
||||
|
||||
## Tastenkürzel
|
||||
|
||||
- **Strg+F**: Suche (wenn Suchfeld fokussiert)
|
||||
- **F5**: Projekte aktualisieren
|
||||
|
||||
## Lizenz
|
||||
|
||||
Dieses Projekt ist für den persönlichen Gebrauch bestimmt.
|
||||
|
||||
## Autor
|
||||
|
||||
Entwickelt für die effiziente Verwaltung von Claude-Projekten.
|
||||
50
WEBSOCKET_TROUBLESHOOTING.md
Normale Datei
50
WEBSOCKET_TROUBLESHOOTING.md
Normale Datei
@ -0,0 +1,50 @@
|
||||
# WebSocket Verbindungsprobleme - Lösungen
|
||||
|
||||
## Problem
|
||||
WebSocket-Verbindungen (ws:// oder wss://) werden oft von Unternehmensnetzwerken blockiert:
|
||||
- Firewalls blockieren WebSocket-Protokoll
|
||||
- Proxies unterstützen kein WebSocket-Upgrade
|
||||
- Nur HTTP/HTTPS Ports (80/443) sind erlaubt
|
||||
|
||||
## Implementierte Lösung
|
||||
Der Activity Service verwendet jetzt **HTTP Long-Polling** statt WebSockets:
|
||||
- Funktioniert über Standard HTTP
|
||||
- Umgeht WebSocket-Blockierungen
|
||||
- Etwas höhere Latenz, aber zuverlässiger in restriktiven Netzwerken
|
||||
|
||||
## Weitere Lösungsansätze
|
||||
|
||||
### 1. Proxy-Konfiguration
|
||||
Wenn Ihr Netzwerk einen HTTP-Proxy verwendet:
|
||||
```python
|
||||
# In services/activity_sync.py
|
||||
import os
|
||||
os.environ['HTTP_PROXY'] = 'http://proxy.company.com:8080'
|
||||
os.environ['HTTPS_PROXY'] = 'http://proxy.company.com:8080'
|
||||
```
|
||||
|
||||
### 2. Alternative Ports
|
||||
Fragen Sie Ihren Admin, ob der Activity Server auch auf Port 80 oder 443 laufen kann.
|
||||
|
||||
### 3. VPN verwenden
|
||||
Wenn verfügbar, kann eine VPN-Verbindung die Netzwerkbeschränkungen umgehen.
|
||||
|
||||
### 4. Lokaler Activity Server
|
||||
Für Entwicklung/Test einen lokalen Server verwenden:
|
||||
- Server URL: `http://localhost:3001`
|
||||
- Keine Netzwerkbeschränkungen
|
||||
|
||||
## Test der Verbindung
|
||||
1. Browser-Test: http://91.99.192.14:3001/health
|
||||
2. Curl-Test: `curl http://91.99.192.14:3001/health`
|
||||
3. Python-Test-Skript: `python test_activity_connection.py`
|
||||
|
||||
## Firewall-Regeln
|
||||
Falls Sie Admin-Rechte haben, erlauben Sie:
|
||||
- Ausgehende TCP-Verbindungen zu 91.99.192.14:3001
|
||||
- HTTP/HTTPS Traffic zu diesem Server
|
||||
|
||||
## Status in CPM
|
||||
- Grüner Punkt: Verbindung aktiv
|
||||
- Meldung "Nicht verbunden": Server nicht erreichbar
|
||||
- Die Aktivitätsfunktion ist optional - CPM funktioniert vollständig ohne sie
|
||||
51
activity_server_cmd_fix.md
Normale Datei
51
activity_server_cmd_fix.md
Normale Datei
@ -0,0 +1,51 @@
|
||||
# Activity Server CMD Connection Fix
|
||||
|
||||
## Problem
|
||||
Users had to manually navigate to `/home/claude-dev/cpm-activity-server` after connecting via SSH when using the Activity Server CMD button.
|
||||
|
||||
## Solution Implemented
|
||||
|
||||
### 1. Enhanced VPS Connection Module
|
||||
Modified `vps_connection.py` to create a more robust `create_activity_server_script()` method that:
|
||||
- Detects available tools (PowerShell, plink, WSL, ssh)
|
||||
- Attempts automatic directory change using multiple methods
|
||||
- Provides clear fallback instructions when automation isn't possible
|
||||
|
||||
### 2. Project Tile Updates
|
||||
Modified `gui/project_tile.py` to:
|
||||
- Enable CMD button for both VPS Server and Activity Server tiles
|
||||
- Use the VPS connection module for generating connection scripts
|
||||
- Properly handle different project types
|
||||
|
||||
### 3. Connection Methods (in order of preference)
|
||||
|
||||
#### PowerShell + plink
|
||||
- Uses PowerShell to better control plink execution
|
||||
- Attempts to run commands after authentication
|
||||
- Falls back to interactive mode with instructions
|
||||
|
||||
#### plink with batch commands
|
||||
- Tries to execute `cd /home/claude-dev/cpm-activity-server && exec bash -l`
|
||||
- If that fails, provides interactive session with clear instructions
|
||||
|
||||
#### WSL + sshpass
|
||||
- Most reliable method when available
|
||||
- Automatically changes directory and starts claude
|
||||
- Uses inline bash commands for better control
|
||||
|
||||
#### Standard SSH fallback
|
||||
- Clear instructions displayed for manual navigation
|
||||
- Password shown for easy copy/paste
|
||||
|
||||
## Usage
|
||||
1. Click the "CMD" button on the Activity Server tile
|
||||
2. The script will attempt to connect and automatically navigate to the correct directory
|
||||
3. If automatic navigation fails, clear instructions are displayed
|
||||
|
||||
## Files Modified
|
||||
- `/vps_connection.py` - Enhanced `create_activity_server_script()` method
|
||||
- `/gui/project_tile.py` - Updated CMD button logic and connection handling
|
||||
|
||||
## Additional Files Created
|
||||
- `/activity_server_connect.ps1` - PowerShell script for enhanced connection (optional)
|
||||
- `/setup_activity_server_profile.sh` - Server-side profile setup script (optional)
|
||||
103
activity_server_connect.ps1
Normale Datei
103
activity_server_connect.ps1
Normale Datei
@ -0,0 +1,103 @@
|
||||
# PowerShell script for Activity Server connection with automatic directory change
|
||||
|
||||
$Host.UI.RawUI.WindowTitle = "CPM Activity Server Connection"
|
||||
|
||||
Write-Host "================================================================================" -ForegroundColor Cyan
|
||||
Write-Host " CPM Activity Server Connection" -ForegroundColor Cyan
|
||||
Write-Host "================================================================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host "Server: 91.99.192.14"
|
||||
Write-Host "Username: claude-dev"
|
||||
Write-Host "Target Directory: /home/claude-dev/cpm-activity-server"
|
||||
Write-Host ""
|
||||
|
||||
# Check if plink is available
|
||||
$plinkPath = Get-Command plink -ErrorAction SilentlyContinue
|
||||
|
||||
if ($plinkPath) {
|
||||
Write-Host "Using PuTTY plink for connection..." -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# Create a temporary expect-like script using PowerShell
|
||||
Write-Host "Attempting automatic connection with directory change..." -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
|
||||
# First attempt: Try to use plink with a command
|
||||
$plinkArgs = @(
|
||||
"-ssh",
|
||||
"-l", "claude-dev",
|
||||
"-pw", "z0E1Al}q2H?Yqd!O",
|
||||
"-t",
|
||||
"91.99.192.14",
|
||||
"cd /home/claude-dev/cpm-activity-server && echo 'Successfully changed to Activity Server directory' && echo '' && bash -l"
|
||||
)
|
||||
|
||||
$plinkProcess = Start-Process -FilePath "plink" -ArgumentList $plinkArgs -PassThru -Wait
|
||||
|
||||
if ($plinkProcess.ExitCode -ne 0) {
|
||||
Write-Host ""
|
||||
Write-Host "Automatic directory change failed. Starting interactive session..." -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
Write-Host "================================================================================" -ForegroundColor Red
|
||||
Write-Host "IMPORTANT: After login, please run these commands:" -ForegroundColor Red
|
||||
Write-Host ""
|
||||
Write-Host " cd /home/claude-dev/cpm-activity-server" -ForegroundColor White
|
||||
Write-Host " claude" -ForegroundColor White
|
||||
Write-Host ""
|
||||
Write-Host "================================================================================" -ForegroundColor Red
|
||||
Write-Host ""
|
||||
|
||||
# Interactive session
|
||||
& plink -ssh -l claude-dev -pw "z0E1Al}q2H?Yqd!O" -t 91.99.192.14
|
||||
}
|
||||
} else {
|
||||
# Check for WSL
|
||||
$wslPath = Get-Command wsl -ErrorAction SilentlyContinue
|
||||
|
||||
if ($wslPath) {
|
||||
Write-Host "Using WSL for connection..." -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# Use WSL with sshpass
|
||||
$wslCommand = @"
|
||||
if command -v sshpass >/dev/null 2>&1; then
|
||||
echo 'Using sshpass for automatic login...'
|
||||
sshpass -p 'z0E1Al}q2H?Yqd!O' ssh -o StrictHostKeyChecking=no -t claude-dev@91.99.192.14 'cd /home/claude-dev/cpm-activity-server && echo "Successfully changed to Activity Server directory" && echo && claude || bash -l'
|
||||
else
|
||||
echo 'Manual password entry required.'
|
||||
echo 'Password: z0E1Al}q2H?Yqd!O'
|
||||
echo ''
|
||||
echo 'After login, run: cd /home/claude-dev/cpm-activity-server && claude'
|
||||
echo ''
|
||||
ssh -o StrictHostKeyChecking=no claude-dev@91.99.192.14
|
||||
fi
|
||||
"@
|
||||
|
||||
& wsl bash -c $wslCommand
|
||||
} else {
|
||||
# Fallback to SSH
|
||||
Write-Host "No automated tools found. Using standard SSH..." -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
Write-Host "================================================================================" -ForegroundColor Red
|
||||
Write-Host "Connection Instructions:" -ForegroundColor Red
|
||||
Write-Host ""
|
||||
Write-Host "1. Enter this password when prompted: z0E1Al}q2H?Yqd!O" -ForegroundColor White
|
||||
Write-Host " (You can right-click to paste in most terminals)" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
Write-Host "2. After successful login, run these commands:" -ForegroundColor White
|
||||
Write-Host " cd /home/claude-dev/cpm-activity-server" -ForegroundColor Cyan
|
||||
Write-Host " claude" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host "================================================================================" -ForegroundColor Red
|
||||
Write-Host ""
|
||||
Write-Host "Press any key to continue..."
|
||||
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
|
||||
|
||||
& ssh claude-dev@91.99.192.14
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Connection closed." -ForegroundColor Yellow
|
||||
Write-Host "Press any key to exit..."
|
||||
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
|
||||
12
app-icon.ico
Normale Datei
12
app-icon.ico
Normale Datei
@ -0,0 +1,12 @@
|
||||
This is a placeholder ICO file. To properly convert the SVG to ICO format, you'll need to:
|
||||
|
||||
1. Use an online converter like:
|
||||
- https://convertio.co/de/svg-ico/
|
||||
- https://cloudconvert.com/svg-to-ico
|
||||
|
||||
2. Or install ImageMagick and run:
|
||||
convert app-icon.svg -resize 256x256 app-icon.ico
|
||||
|
||||
3. Or use a tool like Inkscape to export as ICO
|
||||
|
||||
The SVG file (app-icon.svg) contains a blue square icon that you can convert.
|
||||
14
app-icon.svg
Normale Datei
14
app-icon.svg
Normale Datei
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="256" height="256" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Blue square outline with darker right edge - matching customtkinter style -->
|
||||
<g>
|
||||
<!-- Top edge -->
|
||||
<line x1="40" y1="40" x2="216" y2="40" stroke="#0066CC" stroke-width="6"/>
|
||||
<!-- Left edge -->
|
||||
<line x1="40" y1="40" x2="40" y2="216" stroke="#0066CC" stroke-width="6"/>
|
||||
<!-- Bottom edge -->
|
||||
<line x1="40" y1="216" x2="216" y2="216" stroke="#0066CC" stroke-width="6"/>
|
||||
<!-- Right edge (darker) -->
|
||||
<line x1="216" y1="40" x2="216" y2="216" stroke="#004080" stroke-width="8"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Nachher Breite: | Höhe: | Größe: 654 B |
35
build.bat
Normale Datei
35
build.bat
Normale Datei
@ -0,0 +1,35 @@
|
||||
@echo off
|
||||
title Building Claude Project Manager
|
||||
echo Building Claude Project Manager executable...
|
||||
echo.
|
||||
|
||||
:: Check if Python is installed
|
||||
python --version >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo ERROR: Python is not installed or not in PATH
|
||||
echo Please install Python 3.8 or higher
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
:: Install PyInstaller if not already installed
|
||||
pip show pyinstaller >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo Installing PyInstaller...
|
||||
pip install pyinstaller
|
||||
echo.
|
||||
)
|
||||
|
||||
:: Install other requirements
|
||||
echo Installing requirements...
|
||||
pip install -r requirements.txt >nul 2>&1
|
||||
|
||||
:: Run the build script
|
||||
echo.
|
||||
echo Building executable...
|
||||
python build_exe.py
|
||||
|
||||
echo.
|
||||
echo Build process completed!
|
||||
echo Check the 'dist' folder for the executable.
|
||||
pause
|
||||
48
build_exe.py
Normale Datei
48
build_exe.py
Normale Datei
@ -0,0 +1,48 @@
|
||||
"""
|
||||
Build executable for Claude Project Manager
|
||||
"""
|
||||
|
||||
import PyInstaller.__main__
|
||||
import os
|
||||
import sys
|
||||
|
||||
def build_exe():
|
||||
"""Build executable using PyInstaller"""
|
||||
|
||||
# Get the directory of this script
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# PyInstaller arguments
|
||||
args = [
|
||||
'main.py', # Main script
|
||||
'--name=ClaudeProjectManager', # Name of the executable
|
||||
'--onefile', # Create a single executable file
|
||||
'--windowed', # No console window
|
||||
'--icon=NONE', # No icon for now
|
||||
'--add-data=data;data', # Include data directory
|
||||
'--add-data=gui;gui', # Include gui directory
|
||||
'--add-data=utils;utils', # Include utils directory for logger
|
||||
'--add-data=src;src', # Include src directory with gitea modules
|
||||
'--hidden-import=customtkinter',
|
||||
'--hidden-import=tkinter',
|
||||
'--hidden-import=PIL',
|
||||
'--hidden-import=packaging',
|
||||
'--hidden-import=requests',
|
||||
'--hidden-import=urllib3',
|
||||
'--hidden-import=git',
|
||||
'--collect-all=customtkinter', # Collect all customtkinter files
|
||||
'--noconfirm', # Overwrite output directory without confirmation
|
||||
'--clean', # Clean PyInstaller cache and remove temporary files
|
||||
]
|
||||
|
||||
# Run PyInstaller
|
||||
try:
|
||||
PyInstaller.__main__.run(args)
|
||||
print("\nBuild completed successfully!")
|
||||
print(f"Executable can be found in: {os.path.join(script_dir, 'dist')}")
|
||||
except Exception as e:
|
||||
print(f"Build failed: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
build_exe()
|
||||
28
clone_fix.py
Normale Datei
28
clone_fix.py
Normale Datei
@ -0,0 +1,28 @@
|
||||
"""
|
||||
Temporäre Lösung für das Clone-Problem mit Organization-Repositories
|
||||
"""
|
||||
|
||||
def apply_temporary_fix():
|
||||
"""
|
||||
Diese Funktion zeigt die Zeilen, die temporär geändert werden können,
|
||||
um das Clone-Problem zu umgehen.
|
||||
"""
|
||||
|
||||
print("TEMPORÄRE LÖSUNG für das Clone-Problem:")
|
||||
print("=" * 50)
|
||||
print("\nOption 1: Organization-Prüfung komplett deaktivieren")
|
||||
print("In gui/main_window.py, Zeile 1431 ändern von:")
|
||||
print(" if repo_owner and repo_owner != self.repo_manager.current_user.get('username'):")
|
||||
print("\nZu:")
|
||||
print(" if False: # Temporär deaktiviert")
|
||||
print("\n" + "-" * 50)
|
||||
print("\nOption 2: Nur für IntelSight erlauben")
|
||||
print("In gui/main_window.py, Zeile 1439 ändern von:")
|
||||
print(" if repo_owner not in org_names:")
|
||||
print("\nZu:")
|
||||
print(" if repo_owner not in org_names and repo_owner != 'IntelSight':")
|
||||
print("\n" + "=" * 50)
|
||||
print("\nNach der Änderung muss die Anwendung neu gestartet werden.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
apply_temporary_fix()
|
||||
88
data/projects.json
Normale Datei
88
data/projects.json
Normale Datei
@ -0,0 +1,88 @@
|
||||
{
|
||||
"projects": [
|
||||
{
|
||||
"id": "vps-permanent",
|
||||
"name": "VPS Server",
|
||||
"path": "claude-dev@91.99.192.14",
|
||||
"created_at": "2025-07-01T20:14:48.308074",
|
||||
"last_accessed": "2025-07-09T21:59:56.122304",
|
||||
"readme_path": "claude-dev@91.99.192.14\\CLAUDE_PROJECT_README.md",
|
||||
"description": "Remote VPS Server with Claude",
|
||||
"tags": [
|
||||
"vps",
|
||||
"remote",
|
||||
"server"
|
||||
],
|
||||
"gitea_repo": null
|
||||
},
|
||||
{
|
||||
"id": "admin-panel-permanent",
|
||||
"name": "Admin Panel",
|
||||
"path": "/opt/v2-Docker",
|
||||
"created_at": "2025-07-02T11:07:12.516091",
|
||||
"last_accessed": "2025-07-02T11:45:26.857409",
|
||||
"readme_path": "/opt/v2-Docker\\CLAUDE_PROJECT_README.md",
|
||||
"description": "V2 Docker Admin Panel",
|
||||
"tags": [
|
||||
"admin",
|
||||
"docker",
|
||||
"v2"
|
||||
],
|
||||
"gitea_repo": null
|
||||
},
|
||||
{
|
||||
"id": "vps-docker-permanent",
|
||||
"name": "Docker Restart",
|
||||
"path": "/opt/v2-Docker/v2",
|
||||
"created_at": "2025-07-02T16:15:00.000000",
|
||||
"last_accessed": "2025-07-02T16:25:36.888108",
|
||||
"readme_path": "/opt/v2-Docker/v2\\CLAUDE_PROJECT_README.md",
|
||||
"description": "Docker Admin Panel Restart",
|
||||
"tags": [
|
||||
"vps",
|
||||
"docker",
|
||||
"admin",
|
||||
"restart"
|
||||
],
|
||||
"gitea_repo": null
|
||||
},
|
||||
{
|
||||
"id": "66c0f4bb-c560-43a6-a4c5-a8ecf8b73919",
|
||||
"name": "ClaudeProjectManager",
|
||||
"path": "C:/Users/hendr/Desktop/IntelSight/ClaudeProjectManager-main",
|
||||
"created_at": "2025-07-07T21:38:23.820122",
|
||||
"last_accessed": "2025-07-09T21:31:18.456230",
|
||||
"readme_path": "C:/Users/hendr/Desktop/IntelSight/ClaudeProjectManager-main\\CLAUDE_PROJECT_README.md",
|
||||
"description": "",
|
||||
"tags": [],
|
||||
"gitea_repo": null
|
||||
},
|
||||
{
|
||||
"id": "activity-server-permanent",
|
||||
"name": "Activity Server",
|
||||
"path": "/home/claude-dev/cpm-activity-server",
|
||||
"created_at": "2025-07-08T12:12:14.492170",
|
||||
"last_accessed": "2025-07-09T21:54:00.618934",
|
||||
"readme_path": "/home/claude-dev/cpm-activity-server\\CLAUDE_PROJECT_README.md",
|
||||
"description": "CPM Activity Server",
|
||||
"tags": [
|
||||
"activity",
|
||||
"server",
|
||||
"cpm"
|
||||
],
|
||||
"gitea_repo": null
|
||||
},
|
||||
{
|
||||
"id": "f1a73b61-40ed-4359-b8bb-c35142367e93",
|
||||
"name": "Metadaten-Crawler",
|
||||
"path": "C:/Users/hendr/Desktop/IntelSight/Projektablage/Metadaten-Crawler",
|
||||
"created_at": "2025-07-08T13:51:49.398180",
|
||||
"last_accessed": "2025-07-08T13:51:49.398180",
|
||||
"readme_path": "C:/Users/hendr/Desktop/IntelSight/Projektablage/Metadaten-Crawler\\CLAUDE_PROJECT_README.md",
|
||||
"description": "",
|
||||
"tags": [],
|
||||
"gitea_repo": null
|
||||
}
|
||||
],
|
||||
"last_updated": "2025-07-09T21:59:56.122304"
|
||||
}
|
||||
3
data/settings.json
Normale Datei
3
data/settings.json
Normale Datei
@ -0,0 +1,3 @@
|
||||
{
|
||||
"theme": "light"
|
||||
}
|
||||
63
data/vps_readme/VPS_README.md
Normale Datei
63
data/vps_readme/VPS_README.md
Normale Datei
@ -0,0 +1,63 @@
|
||||
# Claude VPS Server
|
||||
|
||||
*This README was automatically generated by Claude Project Manager*
|
||||
|
||||
## Server Information
|
||||
|
||||
- **Server IP**: 91.99.192.14
|
||||
- **Username**: claude-dev
|
||||
- **Connection**: SSH (Port 22)
|
||||
|
||||
## How to Connect
|
||||
|
||||
### Using Claude Project Manager
|
||||
1. Click on the VPS Server tile
|
||||
2. Terminal will open with connection instructions
|
||||
3. Enter the password when prompted
|
||||
4. Type `claude` after successful login
|
||||
|
||||
### Manual Connection
|
||||
```bash
|
||||
ssh claude-dev@91.99.192.14
|
||||
```
|
||||
|
||||
## Available Commands
|
||||
|
||||
After connecting to the VPS:
|
||||
- `claude` - Start Claude CLI
|
||||
- `ls` - List files
|
||||
- `cd <directory>` - Change directory
|
||||
- `exit` - Close SSH connection
|
||||
|
||||
## Security Notes
|
||||
|
||||
- Keep your password secure
|
||||
- Don't share SSH credentials
|
||||
- Always logout when finished (`exit` command)
|
||||
|
||||
## Features
|
||||
|
||||
This VPS server provides:
|
||||
- Remote access to Claude
|
||||
- Persistent environment
|
||||
- Isolated workspace
|
||||
- Full Linux environment
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection Issues
|
||||
1. Check internet connection
|
||||
2. Verify server is online
|
||||
3. Ensure SSH port (22) is not blocked
|
||||
4. Try manual SSH command
|
||||
|
||||
### Authentication Failed
|
||||
- Verify password is correct
|
||||
- Check username spelling
|
||||
- Ensure caps lock is off
|
||||
|
||||
---
|
||||
|
||||
## Connection Log
|
||||
|
||||
- README generated on 2025-07-09 21:59:56
|
||||
540
duplicate_methods_analysis.md
Normale Datei
540
duplicate_methods_analysis.md
Normale Datei
@ -0,0 +1,540 @@
|
||||
# Duplicate Methods Analysis Report - gui/main_window.py
|
||||
|
||||
## Overview
|
||||
This report analyzes the duplicate methods found in `gui/main_window.py` to determine their differences and whether they can be safely consolidated.
|
||||
|
||||
## 1. `manage_branches` (line 1374 vs line 2336)
|
||||
|
||||
### First Version (line 1374-1377)
|
||||
```python
|
||||
def manage_branches(self, project):
|
||||
"""Manage git branches"""
|
||||
messagebox.showinfo("Branches", "Branch management coming soon")
|
||||
```
|
||||
|
||||
### Second Version (line 2336-2391)
|
||||
```python
|
||||
def manage_branches(self, project):
|
||||
"""Manage branches for a project"""
|
||||
from tkinter import simpledialog
|
||||
|
||||
git_ops = self.repo_manager.git_ops
|
||||
|
||||
# Get current branches
|
||||
success, branches = git_ops.branch(Path(project.path), list_all=True)
|
||||
|
||||
if success:
|
||||
current_branch = None
|
||||
branch_list = []
|
||||
for line in branches.strip().split('\n'):
|
||||
if line.startswith('*'):
|
||||
current_branch = line[2:].strip()
|
||||
branch_list.append(f"{line.strip()} (aktuell)")
|
||||
else:
|
||||
branch_list.append(line.strip())
|
||||
|
||||
# Show options
|
||||
action = messagebox.askyesnocancel(
|
||||
"Branch-Verwaltung",
|
||||
f"Aktuelle Branches:\n{chr(10).join(branch_list)}\n\n"
|
||||
"Ja = Neuen Branch erstellen\n"
|
||||
"Nein = Zu anderem Branch wechseln\n"
|
||||
"Abbrechen = Schließen"
|
||||
)
|
||||
|
||||
if action is True: # Create new branch
|
||||
branch_name = simpledialog.askstring(
|
||||
"Neuer Branch",
|
||||
"Name des neuen Branches:",
|
||||
parent=self.root
|
||||
)
|
||||
if branch_name:
|
||||
success, result = git_ops.checkout(Path(project.path), branch_name, create=True)
|
||||
if success:
|
||||
messagebox.showinfo("Erfolg", f"Branch '{branch_name}' erstellt und gewechselt")
|
||||
else:
|
||||
messagebox.showerror("Fehler", f"Branch-Erstellung fehlgeschlagen: {result}")
|
||||
|
||||
elif action is False: # Switch branch
|
||||
branch_name = simpledialog.askstring(
|
||||
"Branch wechseln",
|
||||
"Zu welchem Branch wechseln?",
|
||||
parent=self.root
|
||||
)
|
||||
if branch_name:
|
||||
success, result = git_ops.checkout(Path(project.path), branch_name)
|
||||
if success:
|
||||
messagebox.showinfo("Erfolg", f"Zu Branch '{branch_name}' gewechselt")
|
||||
else:
|
||||
messagebox.showerror("Fehler", f"Branch-Wechsel fehlgeschlagen: {result}")
|
||||
else:
|
||||
messagebox.showerror("Fehler", f"Konnte Branches nicht abrufen: {branches}")
|
||||
```
|
||||
|
||||
### Differences
|
||||
- **First version**: Placeholder implementation showing "coming soon" message
|
||||
- **Second version**: Full implementation with branch listing, creation, and switching functionality
|
||||
- **Language**: First version uses English, second version uses German
|
||||
|
||||
### Called From
|
||||
- Line 1516: `self.manage_branches(project)` in `gitea_operation` method
|
||||
|
||||
### Recommendation
|
||||
**Safe to consolidate**: Keep the second version (line 2336) as it contains the actual implementation. The first version is just a placeholder.
|
||||
|
||||
---
|
||||
|
||||
## 2. `link_to_gitea` (line 1378 vs line 2392)
|
||||
|
||||
### First Version (line 1378-1381)
|
||||
```python
|
||||
def link_to_gitea(self, project):
|
||||
"""Link local project to Gitea repository"""
|
||||
messagebox.showinfo("Link", "Link functionality coming soon")
|
||||
```
|
||||
|
||||
### Second Version (line 2392-2456)
|
||||
```python
|
||||
def link_to_gitea(self, project):
|
||||
"""Link local project to Gitea repository"""
|
||||
from tkinter import simpledialog
|
||||
|
||||
# Ask for repository name
|
||||
repo_name = simpledialog.askstring(
|
||||
"Mit Gitea verknüpfen",
|
||||
"Name des Gitea-Repositories:",
|
||||
initialvalue=project.name,
|
||||
parent=self.root
|
||||
)
|
||||
|
||||
if repo_name:
|
||||
git_ops = self.repo_manager.git_ops
|
||||
|
||||
# Check if repo exists on Gitea
|
||||
try:
|
||||
repo = self.repo_manager.client.get_repository(
|
||||
self.repo_manager.client.config.username,
|
||||
repo_name
|
||||
)
|
||||
|
||||
# Add remote
|
||||
success, msg = git_ops.add_remote_to_existing_repo(
|
||||
Path(project.path),
|
||||
self.repo_manager.client.config.username,
|
||||
repo_name
|
||||
)
|
||||
|
||||
if success:
|
||||
# Update project with Gitea repo reference
|
||||
project.gitea_repo = f"{self.repo_manager.client.config.username}/{repo_name}"
|
||||
self.project_manager.update_project(project.id, gitea_repo=project.gitea_repo)
|
||||
messagebox.showinfo("Erfolg", f"Erfolgreich mit Repository '{repo_name}' verknüpft!")
|
||||
else:
|
||||
messagebox.showerror("Fehler", f"Verknüpfung fehlgeschlagen: {msg}")
|
||||
|
||||
except Exception as e:
|
||||
# Repository doesn't exist, offer to create
|
||||
create = messagebox.askyesno(
|
||||
"Repository nicht gefunden",
|
||||
f"Repository '{repo_name}' existiert nicht.\n\nMöchten Sie es erstellen?"
|
||||
)
|
||||
|
||||
if create:
|
||||
try:
|
||||
# Create repository
|
||||
new_repo = self.repo_manager.create_repository(repo_name, auto_init=False)
|
||||
|
||||
# Add remote
|
||||
success, msg = git_ops.add_remote_to_existing_repo(
|
||||
Path(project.path),
|
||||
self.repo_manager.client.config.username,
|
||||
repo_name
|
||||
)
|
||||
|
||||
if success:
|
||||
# Update project
|
||||
project.gitea_repo = f"{self.repo_manager.client.config.username}/{repo_name}"
|
||||
self.project_manager.update_project(project.id, gitea_repo=project.gitea_repo)
|
||||
messagebox.showinfo("Erfolg",
|
||||
f"Repository '{repo_name}' erstellt und verknüpft!")
|
||||
else:
|
||||
messagebox.showerror("Fehler", f"Verknüpfung fehlgeschlagen: {msg}")
|
||||
|
||||
except Exception as create_error:
|
||||
messagebox.showerror("Fehler",
|
||||
f"Repository-Erstellung fehlgeschlagen: {str(create_error)}")
|
||||
```
|
||||
|
||||
### Differences
|
||||
- **First version**: Placeholder implementation showing "coming soon" message
|
||||
- **Second version**: Full implementation with repository linking, checking existence, and creation option
|
||||
- **Language**: First version uses English, second version uses German
|
||||
|
||||
### Called From
|
||||
- Line 1514: `self.link_to_gitea(project)` in `gitea_operation` method
|
||||
|
||||
### Recommendation
|
||||
**Safe to consolidate**: Keep the second version (line 2392) as it contains the actual implementation. The first version is just a placeholder.
|
||||
|
||||
---
|
||||
|
||||
## 3. `test_gitea_connection` (line 1757 vs line 2457)
|
||||
|
||||
### First Version (line 1757-1791)
|
||||
```python
|
||||
def test_gitea_connection(self, project):
|
||||
"""Test Gitea connection and show permissions"""
|
||||
try:
|
||||
# Test API connection
|
||||
user_info = self.repo_manager.client.get_user_info()
|
||||
|
||||
info = "Gitea-Verbindung erfolgreich!\n\n"
|
||||
info += f"Benutzer: {user_info.get('username', 'Unknown')}\n"
|
||||
info += f"E-Mail: {user_info.get('email', 'Unknown')}\n"
|
||||
info += f"Admin: {'Ja' if user_info.get('is_admin', False) else 'Nein'}\n\n"
|
||||
|
||||
# Check organizations
|
||||
orgs = self.repo_manager.client.list_user_organizations()
|
||||
if orgs:
|
||||
info += "Organisationen:\n"
|
||||
for org in orgs:
|
||||
org_name = org.get('username', org.get('name', 'Unknown'))
|
||||
info += f" • {org_name}"
|
||||
|
||||
# Check teams/permissions in org
|
||||
teams = self.repo_manager.client.get_user_teams_in_org(org_name)
|
||||
if teams:
|
||||
team_names = [t.get('name', 'Unknown') for t in teams]
|
||||
info += f" (Teams: {', '.join(team_names)})"
|
||||
info += "\n"
|
||||
else:
|
||||
info += "Keine Organisationen\n"
|
||||
|
||||
info += f"\nServer: {self.repo_manager.client.config.base_url}"
|
||||
|
||||
messagebox.showinfo("Verbindungstest", info)
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror("Verbindungsfehler", f"Konnte keine Verbindung zu Gitea herstellen:\n\n{str(e)}")
|
||||
```
|
||||
|
||||
### Second Version (line 2457-2554)
|
||||
```python
|
||||
def test_gitea_connection(self, project=None):
|
||||
"""Test Gitea connection and show detailed information"""
|
||||
try:
|
||||
# Test basic connection
|
||||
user_info = self.repo_manager.client.get_user_info()
|
||||
username = user_info.get('username', 'Unknown')
|
||||
|
||||
# Get organizations
|
||||
orgs = self.repo_manager.client.list_user_organizations()
|
||||
org_names = [org['username'] for org in orgs]
|
||||
|
||||
# Build info message
|
||||
info = f"✅ Gitea Verbindung erfolgreich!\n\n"
|
||||
info += f"Benutzer: {username}\n"
|
||||
info += f"Server: {self.repo_manager.client.config.base_url}\n"
|
||||
info += f"Organisationen: {', '.join(org_names) if org_names else 'Keine'}\n\n"
|
||||
|
||||
# Check organization permissions if in org mode
|
||||
if hasattr(self, 'gitea_explorer') and self.gitea_explorer.view_mode == "organization":
|
||||
org_name = self.gitea_explorer.organization_name
|
||||
if org_name:
|
||||
teams = self.repo_manager.client.get_user_teams_in_org(org_name)
|
||||
if teams:
|
||||
info += f"Teams in {org_name}:\n"
|
||||
for team in teams:
|
||||
info += f" - {team.get('name', 'Unknown')} "
|
||||
perms = []
|
||||
if team.get('can_create_org_repo'):
|
||||
perms.append("kann Repos erstellen")
|
||||
if team.get('permission') == 'admin':
|
||||
perms.append("Admin")
|
||||
elif team.get('permission') == 'write':
|
||||
perms.append("Schreiben")
|
||||
elif team.get('permission') == 'read':
|
||||
perms.append("Lesen")
|
||||
info += f"({', '.join(perms) if perms else 'keine Rechte'})\n"
|
||||
else:
|
||||
info += f"⚠️ Keine Teams in Organisation {org_name} gefunden!\n"
|
||||
info += "Dies könnte der Grund sein, warum Repositories nicht erstellt werden können.\n"
|
||||
|
||||
info += "\n"
|
||||
|
||||
# If we have a project, show its remote info
|
||||
if project:
|
||||
project_path = Path(project.path) if hasattr(project, 'path') else Path(project['path'])
|
||||
if project_path.exists():
|
||||
success, remotes = self.repo_manager.git_ops.remote_list(project_path)
|
||||
if success and remotes:
|
||||
info += f"Git Remote URLs:\n{remotes}\n\n"
|
||||
|
||||
# Check if .git exists
|
||||
if (project_path / '.git').exists():
|
||||
# Get current branch
|
||||
success, branch_out = self.repo_manager.git_ops._run_git_command(
|
||||
["git", "branch", "--show-current"], cwd=project_path
|
||||
)
|
||||
if success:
|
||||
info += f"Aktueller Branch: {branch_out.strip()}\n"
|
||||
|
||||
# List repositories in current mode
|
||||
info += "Repositories:\n"
|
||||
try:
|
||||
if hasattr(self, 'gitea_explorer'):
|
||||
if self.gitea_explorer.view_mode == "organization" and self.gitea_explorer.organization_name:
|
||||
# List org repos
|
||||
org_repos = self.repo_manager.list_organization_repositories(self.gitea_explorer.organization_name)
|
||||
info += f"In Organisation {self.gitea_explorer.organization_name}: {len(org_repos)} Repositories\n"
|
||||
for repo in org_repos[:5]: # Show first 5
|
||||
info += f" - {repo['name']}\n"
|
||||
if len(org_repos) > 5:
|
||||
info += f" ... und {len(org_repos) - 5} weitere\n"
|
||||
else:
|
||||
# List user repos
|
||||
user_repos = self.repo_manager.list_all_repositories()
|
||||
info += f"Benutzer Repositories: {len(user_repos)}\n"
|
||||
for repo in user_repos[:5]: # Show first 5
|
||||
info += f" - {repo['name']} (Owner: {repo.get('owner', {}).get('username', 'Unknown')})\n"
|
||||
if len(user_repos) > 5:
|
||||
info += f" ... und {len(user_repos) - 5} weitere\n"
|
||||
except Exception as e:
|
||||
info += f"Fehler beim Abrufen der Repositories: {str(e)}\n"
|
||||
|
||||
# Show log file location
|
||||
log_file = Path.home() / ".claude_project_manager" / "gitea_operations.log"
|
||||
info += f"\nLog-Datei: {log_file}"
|
||||
|
||||
messagebox.showinfo("Gitea Verbindungstest", info)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"❌ Gitea Verbindung fehlgeschlagen!\n\n"
|
||||
error_msg += f"Fehler: {str(e)}\n\n"
|
||||
error_msg += f"Server: {self.repo_manager.client.config.base_url}\n"
|
||||
error_msg += "\nBitte prüfen Sie:\n"
|
||||
error_msg += "- Netzwerkverbindung\n"
|
||||
error_msg += "- API Token Gültigkeit\n"
|
||||
error_msg += "- Server Erreichbarkeit"
|
||||
|
||||
messagebox.showerror("Gitea Verbindungstest", error_msg)
|
||||
```
|
||||
|
||||
### Key Differences
|
||||
- **Parameter**: First version requires `project`, second version has `project=None` (optional)
|
||||
- **Detail level**: Second version shows much more information including:
|
||||
- Team permissions with detailed permission levels
|
||||
- Project-specific git remote info if project is provided
|
||||
- Current branch information
|
||||
- Repository listings (user/org mode aware)
|
||||
- Log file location
|
||||
- **UI elements**: First version shows email and admin status, second version doesn't
|
||||
- **Error handling**: Second version has more detailed error messages with checkmark/cross emojis
|
||||
|
||||
### Called From
|
||||
- Line 1522: `self.test_gitea_connection(project)` in `gitea_operation` method
|
||||
|
||||
### Recommendation
|
||||
**Requires careful consolidation**: The second version is more comprehensive but has an optional parameter. Since the call from `gitea_operation` always passes a project, we should keep the enhanced functionality of the second version but ensure it works correctly when a project is provided.
|
||||
|
||||
---
|
||||
|
||||
## 4. `verify_repository_on_gitea` (line 1792 vs line 2556)
|
||||
|
||||
### First Version (line 1792-1849+)
|
||||
```python
|
||||
def verify_repository_on_gitea(self, project):
|
||||
"""Verify if repository exists on Gitea and show detailed info"""
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
project_path = Path(project.path)
|
||||
|
||||
# Check if it's a git repo
|
||||
if not (project_path / ".git").exists():
|
||||
messagebox.showinfo("Info", "Dies ist kein Git-Repository.")
|
||||
return
|
||||
|
||||
# Get remote info
|
||||
git_ops = self.repo_manager.git_ops
|
||||
success, remotes = git_ops.remote_list(project_path)
|
||||
|
||||
if not success or not remotes:
|
||||
messagebox.showinfo("Info", "Kein Remote-Repository konfiguriert.")
|
||||
return
|
||||
|
||||
# Extract owner and repo name from remote URL
|
||||
match = re.search(r'gitea-undso\.intelsight\.de[:/]([^/]+)/([^/\.]+)', remotes)
|
||||
if not match:
|
||||
messagebox.showwarning("Warnung", f"Konnte Repository-Info nicht aus Remote-URL extrahieren:\n{remotes}")
|
||||
return
|
||||
|
||||
remote_owner = match.group(1)
|
||||
remote_repo = match.group(2)
|
||||
|
||||
info = f"Suche Repository: {remote_owner}/{remote_repo}\n\n"
|
||||
|
||||
# Check if repo exists under detected owner
|
||||
try:
|
||||
repo = self.repo_manager.client.get_repository(remote_owner, remote_repo)
|
||||
info += "✅ Repository gefunden!\n\n"
|
||||
info += f"Name: {repo.get('name', 'Unknown')}\n"
|
||||
info += f"Owner: {repo.get('owner', {}).get('username', 'Unknown')}\n"
|
||||
info += f"URL: {repo.get('html_url', 'Unknown')}\n"
|
||||
info += f"Clone URL: {repo.get('clone_url', 'Unknown')}\n"
|
||||
info += f"Privat: {'Ja' if repo.get('private', False) else 'Nein'}\n"
|
||||
info += f"Größe: {repo.get('size', 0) / 1024:.1f} KB\n"
|
||||
info += f"Default Branch: {repo.get('default_branch', 'Keiner')}\n"
|
||||
info += f"Erstellt: {repo.get('created_at', 'Unknown')}\n"
|
||||
info += f"Zuletzt aktualisiert: {repo.get('updated_at', 'Unknown')}\n"
|
||||
|
||||
# Check for content
|
||||
has_content = repo.get('size', 0) > 0 or repo.get('default_branch')
|
||||
if not has_content:
|
||||
info += "\n⚠️ WARNUNG: Repository scheint leer zu sein!\n"
|
||||
|
||||
except Exception as e:
|
||||
info += f"❌ Repository nicht unter {remote_owner}/{remote_repo} gefunden!\n"
|
||||
info += f"Fehler: {str(e)}\n\n"
|
||||
|
||||
# Search in all locations
|
||||
info += "Suche in allen verfügbaren Orten:\n\n"
|
||||
|
||||
# Check user repos
|
||||
# ... (continues beyond visible range)
|
||||
```
|
||||
|
||||
### Second Version (line 2556-2656)
|
||||
```python
|
||||
def verify_repository_on_gitea(self, project):
|
||||
"""Verify if repository exists on Gitea and show detailed info"""
|
||||
project_name = project.name if hasattr(project, 'name') else project.get('name', 'Unknown')
|
||||
project_path = Path(project.path) if hasattr(project, 'path') else Path(project['path'])
|
||||
|
||||
info = f"Repository Verifizierung für: {project_name}\n"
|
||||
info += "=" * 50 + "\n\n"
|
||||
|
||||
# Check git remotes
|
||||
git_ops = self.repo_manager.git_ops
|
||||
success, remotes = git_ops.remote_list(project_path)
|
||||
if success and remotes:
|
||||
info += f"Git Remote URLs:\n{remotes}\n\n"
|
||||
|
||||
# Extract repo name and owner from remote
|
||||
import re
|
||||
match = re.search(r'gitea-undso\.intelsight\.de[:/]([^/]+)/([^/\.]+)', remotes)
|
||||
if match:
|
||||
remote_owner = match.group(1)
|
||||
remote_repo = match.group(2)
|
||||
info += f"Remote zeigt auf: {remote_owner}/{remote_repo}\n\n"
|
||||
|
||||
# Search for repository
|
||||
info += "Suche Repository auf Gitea:\n"
|
||||
|
||||
# 1. Check exact location
|
||||
try:
|
||||
repo = self.repo_manager.client.get_repository(remote_owner, remote_repo)
|
||||
info += f"✅ Gefunden unter {remote_owner}/{remote_repo}\n"
|
||||
info += f" URL: {repo.get('html_url', 'Unknown')}\n"
|
||||
info += f" Größe: {repo.get('size', 0)} bytes\n"
|
||||
info += f" Erstellt: {repo.get('created_at', 'Unknown')}\n"
|
||||
info += f" Aktualisiert: {repo.get('updated_at', 'Unknown')}\n"
|
||||
info += f" Default Branch: {repo.get('default_branch', 'Unknown')}\n"
|
||||
info += f" Privat: {'Ja' if repo.get('private') else 'Nein'}\n"
|
||||
except Exception as e:
|
||||
info += f"❌ Nicht gefunden unter {remote_owner}/{remote_repo}\n"
|
||||
info += f" Fehler: {str(e)}\n"
|
||||
|
||||
# 2. Search in all user repos
|
||||
info += "\nSuche in allen Benutzer-Repositories:\n"
|
||||
try:
|
||||
user_repos = self.repo_manager.list_all_repositories()
|
||||
matching_repos = [r for r in user_repos if r['name'] == remote_repo or r['name'] == project_name]
|
||||
if matching_repos:
|
||||
for repo in matching_repos:
|
||||
info += f"✅ Gefunden: {repo['owner']['username']}/{repo['name']}\n"
|
||||
info += f" URL: {repo.get('html_url', 'Unknown')}\n"
|
||||
else:
|
||||
info += "❌ Nicht in Benutzer-Repositories gefunden\n"
|
||||
except Exception as e:
|
||||
info += f"❌ Fehler beim Durchsuchen: {str(e)}\n"
|
||||
|
||||
# 3. Search in organization
|
||||
if hasattr(self, 'gitea_explorer') and self.gitea_explorer.organization_name:
|
||||
info += f"\nSuche in Organisation {self.gitea_explorer.organization_name}:\n"
|
||||
try:
|
||||
org_repos = self.repo_manager.list_organization_repositories(self.gitea_explorer.organization_name)
|
||||
matching_repos = [r for r in org_repos if r['name'] == remote_repo or r['name'] == project_name]
|
||||
if matching_repos:
|
||||
for repo in matching_repos:
|
||||
info += f"✅ Gefunden: {repo['name']}\n"
|
||||
info += f" URL: {repo.get('html_url', 'Unknown')}\n"
|
||||
else:
|
||||
info += "❌ Nicht in Organisation gefunden\n"
|
||||
except Exception as e:
|
||||
info += f"❌ Fehler beim Durchsuchen: {str(e)}\n"
|
||||
else:
|
||||
info += "❌ Keine Git Remote gefunden!\n"
|
||||
|
||||
# Check for debug file
|
||||
debug_file = project_path / "gitea_push_debug.txt"
|
||||
if debug_file.exists():
|
||||
info += f"\n\n📄 Debug-Datei gefunden: {debug_file}\n"
|
||||
try:
|
||||
with open(debug_file, 'r', encoding='utf-8') as f:
|
||||
debug_content = f.read()
|
||||
info += "Debug-Inhalt:\n" + "-" * 30 + "\n"
|
||||
info += debug_content[:1000] # First 1000 chars
|
||||
if len(debug_content) > 1000:
|
||||
info += "\n... (gekürzt)"
|
||||
except:
|
||||
info += "Konnte Debug-Datei nicht lesen\n"
|
||||
|
||||
# Show in scrollable dialog
|
||||
dialog = ctk.CTkToplevel(self.root)
|
||||
dialog.title("Repository Verifizierung")
|
||||
dialog.geometry("800x600")
|
||||
|
||||
text_widget = ctk.CTkTextbox(dialog, wrap="word")
|
||||
text_widget.pack(fill="both", expand=True, padx=10, pady=10)
|
||||
text_widget.insert("1.0", info)
|
||||
text_widget.configure(state="disabled")
|
||||
|
||||
close_btn = ctk.CTkButton(
|
||||
dialog,
|
||||
text="Schließen",
|
||||
command=dialog.destroy
|
||||
)
|
||||
close_btn.pack(pady=10)
|
||||
```
|
||||
|
||||
### Key Differences
|
||||
- **Project access**: Second version handles both object attributes and dictionary access for project data
|
||||
- **Pre-checks**: First version checks if it's a git repo and has remotes before proceeding
|
||||
- **Display method**: First version shows info in messagebox, second version uses a scrollable CTkToplevel dialog
|
||||
- **Additional features**: Second version includes:
|
||||
- Debug file checking and display
|
||||
- More structured repository search (exact location, user repos, org repos)
|
||||
- Header with project name
|
||||
- **Size display**: First version shows size in KB, second version in bytes
|
||||
|
||||
### Called From
|
||||
- Line 1524: `self.verify_repository_on_gitea(project)` in `gitea_operation` method
|
||||
- Line 1987: `self.verify_repository_on_gitea(project)` in `fix_repository_issues` method (nested function `check_on_gitea`)
|
||||
|
||||
### Recommendation
|
||||
**Requires careful consolidation**: Both versions have valuable features. The second version's scrollable dialog is better for displaying detailed information, but the first version's pre-checks are important. Combine both:
|
||||
- Keep pre-checks from first version
|
||||
- Keep the enhanced display and debug file checking from second version
|
||||
- Ensure consistent project data access
|
||||
|
||||
---
|
||||
|
||||
## Summary and Recommendations
|
||||
|
||||
1. **`manage_branches`**: Delete first version (placeholder), keep second version
|
||||
2. **`link_to_gitea`**: Delete first version (placeholder), keep second version
|
||||
3. **`test_gitea_connection`**: Merge both versions, keeping the enhanced functionality of the second while ensuring it works with required project parameter
|
||||
4. **`verify_repository_on_gitea`**: Merge both versions, combining pre-checks from first with enhanced display from second
|
||||
|
||||
All duplicates are called from the same `gitea_operation` method, so consolidation is safe. The first versions of `manage_branches` and `link_to_gitea` are just placeholders that were later implemented fully in the second versions.
|
||||
22
gitea_push_debug.txt
Normale Datei
22
gitea_push_debug.txt
Normale Datei
@ -0,0 +1,22 @@
|
||||
Push Debug Info - 2025-07-07 22:11:38.340552
|
||||
Repository: ClaudeProjectManager-main
|
||||
Owner: IntelSight
|
||||
Path: C:\Users\hendr\Desktop\IntelSight\ClaudeProjectManager-main
|
||||
Current branch: master
|
||||
Git remotes:
|
||||
origin https://IntelSight_Admin:3b4a6ba1ade3f34640f3c85d2333b4a3a0627471@gitea-undso.intelsight.de/IntelSight/ClaudeProjectManager-main.git (fetch)
|
||||
origin https://IntelSight_Admin:3b4a6ba1ade3f34640f3c85d2333b4a3a0627471@gitea-undso.intelsight.de/IntelSight/ClaudeProjectManager-main.git (push)
|
||||
Git status before push:
|
||||
Clean
|
||||
Push command: git push --set-upstream origin master:main -v
|
||||
Push result: Success
|
||||
Push stdout:
|
||||
branch 'master' set up to track 'origin/main'.
|
||||
Push stderr:
|
||||
POST git-receive-pack (151391 bytes)
|
||||
remote: . Processing 1 references
|
||||
remote: Processed 1 references in total
|
||||
Pushing to https://gitea-undso.intelsight.de/IntelSight/ClaudeProjectManager-main.git
|
||||
To https://gitea-undso.intelsight.de/IntelSight/ClaudeProjectManager-main.git
|
||||
* [new branch] master -> main
|
||||
updating local tracking ref 'refs/remotes/origin/main'
|
||||
113
gui/config.py
Normale Datei
113
gui/config.py
Normale Datei
@ -0,0 +1,113 @@
|
||||
"""
|
||||
Configuration for GUI refactoring
|
||||
Manages feature flags and refactoring settings
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
from pathlib import Path
|
||||
from utils.logger import logger
|
||||
|
||||
class RefactoringConfig:
|
||||
"""Manages refactoring feature flags"""
|
||||
|
||||
CONFIG_FILE = "refactoring_config.json"
|
||||
DEFAULT_FLAGS = {
|
||||
'USE_GITEA_HANDLER': True, # Enable for Progress Bar support
|
||||
'USE_PROCESS_HANDLER': True, # Enable for better logging
|
||||
'USE_PROJECT_HANDLER': False,
|
||||
'USE_UI_HELPERS': False,
|
||||
'ENABLE_DEBUG_LOGGING': False,
|
||||
'FORCE_ORIGINAL_IMPLEMENTATION': False # Emergency override
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.config_path = Path.home() / ".claude_project_manager" / self.CONFIG_FILE
|
||||
self.flags = self.DEFAULT_FLAGS.copy()
|
||||
self._load_config()
|
||||
self._check_env_overrides()
|
||||
|
||||
def _load_config(self):
|
||||
"""Load configuration from file"""
|
||||
try:
|
||||
if self.config_path.exists():
|
||||
with open(self.config_path, 'r') as f:
|
||||
loaded_flags = json.load(f)
|
||||
# Only update known flags
|
||||
for key, value in loaded_flags.items():
|
||||
if key in self.flags:
|
||||
self.flags[key] = value
|
||||
logger.info(f"Loaded refactoring flag: {key} = {value}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load refactoring config: {e}")
|
||||
|
||||
def _check_env_overrides(self):
|
||||
"""Check for environment variable overrides"""
|
||||
# Global override
|
||||
if os.getenv('CPM_USE_NEW_HANDLERS'):
|
||||
logger.info("Enabling all refactored handlers via environment variable")
|
||||
for key in ['USE_GITEA_HANDLER', 'USE_PROCESS_HANDLER',
|
||||
'USE_PROJECT_HANDLER', 'USE_UI_HELPERS']:
|
||||
self.flags[key] = True
|
||||
|
||||
# Individual overrides
|
||||
for flag in self.flags:
|
||||
env_var = f"CPM_{flag}"
|
||||
if os.getenv(env_var):
|
||||
value = os.getenv(env_var).lower() in ('true', '1', 'yes')
|
||||
self.flags[flag] = value
|
||||
logger.info(f"Override from env: {flag} = {value}")
|
||||
|
||||
def save_config(self):
|
||||
"""Save current configuration to file"""
|
||||
try:
|
||||
self.config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(self.config_path, 'w') as f:
|
||||
json.dump(self.flags, f, indent=2)
|
||||
logger.info(f"Saved refactoring config to {self.config_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save refactoring config: {e}")
|
||||
|
||||
def get(self, flag_name: str, default=None):
|
||||
"""Get a flag value"""
|
||||
return self.flags.get(flag_name, default)
|
||||
|
||||
def set(self, flag_name: str, value: bool):
|
||||
"""Set a flag value"""
|
||||
if flag_name in self.flags:
|
||||
self.flags[flag_name] = value
|
||||
logger.info(f"Set refactoring flag: {flag_name} = {value}")
|
||||
else:
|
||||
logger.warning(f"Unknown refactoring flag: {flag_name}")
|
||||
|
||||
def enable_handler(self, handler_name: str):
|
||||
"""Enable a specific handler"""
|
||||
# Map handler names to flags
|
||||
handler_map = {
|
||||
'gitea': 'USE_GITEA_HANDLER',
|
||||
'process': 'USE_PROCESS_HANDLER',
|
||||
'project': 'USE_PROJECT_HANDLER',
|
||||
'ui': 'USE_UI_HELPERS' # Note: not _HANDLER suffix
|
||||
}
|
||||
flag_name = handler_map.get(handler_name.lower())
|
||||
if flag_name and flag_name in self.flags:
|
||||
self.set(flag_name, True)
|
||||
else:
|
||||
logger.warning(f"Unknown handler: {handler_name}")
|
||||
|
||||
def disable_all(self):
|
||||
"""Disable all refactoring features"""
|
||||
for flag in self.flags:
|
||||
if flag.startswith('USE_'):
|
||||
self.flags[flag] = False
|
||||
logger.info("All refactoring features disabled")
|
||||
|
||||
def get_status(self):
|
||||
"""Get status of all flags"""
|
||||
return {
|
||||
"config_file": str(self.config_path),
|
||||
"flags": self.flags.copy()
|
||||
}
|
||||
|
||||
# Global instance
|
||||
refactoring_config = RefactoringConfig()
|
||||
554
gui/gitea_explorer.py
Normale Datei
554
gui/gitea_explorer.py
Normale Datei
@ -0,0 +1,554 @@
|
||||
"""
|
||||
Gitea Repository Explorer for the sidebar
|
||||
"""
|
||||
|
||||
import customtkinter as ctk
|
||||
from tkinter import ttk, messagebox
|
||||
import threading
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Optional, Callable, List, Dict
|
||||
from gui.styles import COLORS, FONTS
|
||||
from src.gitea.repository_manager import RepositoryManager
|
||||
from src.gitea.gitea_client import GiteaClient
|
||||
import json
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class GiteaExplorer(ctk.CTkFrame):
|
||||
def __init__(self, parent, on_repo_select: Optional[Callable] = None, **kwargs):
|
||||
super().__init__(parent, fg_color=COLORS['bg_secondary'], **kwargs)
|
||||
|
||||
self.on_repo_select = on_repo_select
|
||||
self.repo_manager = RepositoryManager()
|
||||
self.repositories = []
|
||||
self.selected_repo = None
|
||||
self.view_mode = "organization" # Always organization mode
|
||||
self.organization_name = "IntelSight" # Fixed to IntelSight
|
||||
self.organizations = [] # List of user's organizations
|
||||
self.main_window = None # Will be set by main window
|
||||
|
||||
self.setup_ui()
|
||||
# Skip load_organizations - we always use IntelSight
|
||||
# Automatisches Laden der Repositories beim Start nach kurzer Verzögerung
|
||||
self.after(500, self.refresh_repositories)
|
||||
|
||||
def _show_gitea_menu(self, repo: Dict, button: ctk.CTkButton):
|
||||
"""Show Gitea operations menu for repository"""
|
||||
import tkinter as tk
|
||||
|
||||
# Create menu
|
||||
menu = tk.Menu(self, tearoff=0)
|
||||
menu.configure(
|
||||
bg=COLORS['bg_secondary'],
|
||||
fg=COLORS['text_primary'],
|
||||
activebackground=COLORS['bg_tile_hover'],
|
||||
activeforeground=COLORS['text_primary'],
|
||||
borderwidth=0,
|
||||
relief="flat"
|
||||
)
|
||||
|
||||
# Check if repository is already cloned
|
||||
local_path = Path.home() / "GiteaRepos" / repo['name']
|
||||
is_cloned = local_path.exists()
|
||||
|
||||
if is_cloned:
|
||||
# Already cloned options
|
||||
menu.add_command(label="📂 Im Explorer öffnen", command=lambda: self._open_in_explorer(repo))
|
||||
menu.add_command(label="📊 Git Status", command=lambda: self._git_status(repo))
|
||||
menu.add_separator()
|
||||
menu.add_command(label="⬇️ Pull (Aktualisieren)", command=lambda: self._git_pull(repo))
|
||||
menu.add_command(label="🔄 Fetch", command=lambda: self._git_fetch(repo))
|
||||
menu.add_separator()
|
||||
menu.add_command(label="🗑️ Lokale Kopie löschen", command=lambda: self._delete_local_copy(repo))
|
||||
else:
|
||||
# Not cloned options
|
||||
menu.add_command(label="📥 Repository klonen", command=lambda: self._clone_repository(repo))
|
||||
|
||||
menu.add_separator()
|
||||
menu.add_command(label="ℹ️ Repository Info", command=lambda: self._show_repo_info(repo))
|
||||
menu.add_command(label="🌐 In Browser öffnen", command=lambda: self._open_in_browser(repo))
|
||||
|
||||
# Show menu at button position
|
||||
try:
|
||||
x = button.winfo_rootx()
|
||||
y = button.winfo_rooty() + button.winfo_height()
|
||||
menu.tk_popup(x, y)
|
||||
finally:
|
||||
menu.grab_release()
|
||||
|
||||
def setup_ui(self):
|
||||
"""Setup the explorer UI"""
|
||||
# Header
|
||||
header_frame = ctk.CTkFrame(self, fg_color="transparent")
|
||||
header_frame.pack(fill="x", padx=10, pady=(10, 5))
|
||||
|
||||
# Title
|
||||
self.title_label = ctk.CTkLabel(
|
||||
header_frame,
|
||||
text="🔧 Gitea Repositories",
|
||||
font=FONTS['subtitle'],
|
||||
text_color=COLORS['text_primary']
|
||||
)
|
||||
self.title_label.pack(side="left")
|
||||
|
||||
# Refresh button
|
||||
refresh_btn = ctk.CTkButton(
|
||||
header_frame,
|
||||
text="🔄",
|
||||
command=self.refresh_repositories,
|
||||
width=30,
|
||||
height=30,
|
||||
fg_color=COLORS['accent_primary'],
|
||||
hover_color=COLORS['accent_hover']
|
||||
)
|
||||
refresh_btn.pack(side="right")
|
||||
|
||||
# IntelSight label instead of toggle
|
||||
org_frame = ctk.CTkFrame(self, fg_color=COLORS['bg_secondary'])
|
||||
org_frame.pack(fill="x", padx=10, pady=5)
|
||||
|
||||
org_label = ctk.CTkLabel(
|
||||
org_frame,
|
||||
text="🏢 IntelSight Organization",
|
||||
font=FONTS['body'],
|
||||
text_color=COLORS['accent_primary']
|
||||
)
|
||||
org_label.pack()
|
||||
|
||||
# Search box
|
||||
self.search_var = ctk.StringVar()
|
||||
self.search_var.trace('w', self._on_search_changed)
|
||||
|
||||
search_frame = ctk.CTkFrame(self, fg_color=COLORS['bg_secondary'])
|
||||
search_frame.pack(fill="x", padx=10, pady=5)
|
||||
|
||||
self.search_entry = ctk.CTkEntry(
|
||||
search_frame,
|
||||
placeholder_text="Repository suchen...",
|
||||
textvariable=self.search_var,
|
||||
fg_color=COLORS['bg_primary'],
|
||||
text_color=COLORS['text_primary']
|
||||
)
|
||||
self.search_entry.pack(fill="x")
|
||||
|
||||
# Repository list with scrollbar
|
||||
list_frame = ctk.CTkFrame(self, fg_color=COLORS['bg_secondary'])
|
||||
list_frame.pack(fill="both", expand=True, padx=10, pady=(5, 10))
|
||||
|
||||
# Scrollable frame for repositories
|
||||
self.scroll_frame = ctk.CTkScrollableFrame(
|
||||
list_frame,
|
||||
fg_color=COLORS['bg_primary'],
|
||||
corner_radius=6
|
||||
)
|
||||
self.scroll_frame.pack(fill="both", expand=True)
|
||||
|
||||
# Repository items container
|
||||
self.repo_container = ctk.CTkFrame(self.scroll_frame, fg_color=COLORS['bg_primary'])
|
||||
self.repo_container.pack(fill="both", expand=True)
|
||||
|
||||
# Status label
|
||||
self.status_label = ctk.CTkLabel(
|
||||
self,
|
||||
text="Lade Repositories...",
|
||||
font=FONTS['small'],
|
||||
text_color=COLORS['text_secondary']
|
||||
)
|
||||
self.status_label.pack(pady=5)
|
||||
|
||||
def _get_clone_directory(self) -> Path:
|
||||
"""Get clone directory from settings or use default"""
|
||||
try:
|
||||
settings_file = Path.home() / ".claude_project_manager" / "ui_settings.json"
|
||||
if settings_file.exists():
|
||||
with open(settings_file, 'r') as f:
|
||||
settings = json.load(f)
|
||||
clone_dir = settings.get("gitea_clone_directory")
|
||||
if clone_dir:
|
||||
return Path(clone_dir)
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not load clone directory from settings: {e}")
|
||||
|
||||
# Return default if not found in settings
|
||||
return Path.home() / "GiteaRepos"
|
||||
|
||||
def refresh_repositories(self):
|
||||
"""Refresh repository list from Gitea - only IntelSight"""
|
||||
self.status_label.configure(text="Lade IntelSight Repositories...")
|
||||
|
||||
def fetch_repos():
|
||||
try:
|
||||
# Always fetch IntelSight organization repositories
|
||||
all_repos = []
|
||||
page = 1
|
||||
while True:
|
||||
# Fetch page by page to ensure we get all repositories
|
||||
repos = self.repo_manager.list_organization_repositories("IntelSight", page=page, per_page=50)
|
||||
if not repos:
|
||||
break
|
||||
all_repos.extend(repos)
|
||||
page += 1
|
||||
# Break if we got less than the requested amount (last page)
|
||||
if len(repos) < 50:
|
||||
break
|
||||
|
||||
self.repositories = all_repos
|
||||
logger.info(f"Fetched {len(self.repositories)} repositories from IntelSight")
|
||||
|
||||
self.after(0, self._update_repository_list)
|
||||
self.after(0, lambda: self.status_label.configure(
|
||||
text=f"{len(self.repositories)} IntelSight Repositories"
|
||||
))
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching repositories: {str(e)}")
|
||||
self.after(0, lambda: self.status_label.configure(
|
||||
text=f"Fehler: {str(e)}",
|
||||
text_color=COLORS['accent_error']
|
||||
))
|
||||
|
||||
threading.Thread(target=fetch_repos, daemon=True).start()
|
||||
|
||||
def _update_repository_list(self):
|
||||
"""Update the displayed repository list"""
|
||||
# Filter repositories based on search and remove duplicates
|
||||
search_term = self.search_var.get().lower()
|
||||
|
||||
# Use a dict to track unique repos by name (preferring non-forks)
|
||||
unique_repos = {}
|
||||
for repo in self.repositories:
|
||||
repo_name = repo['name']
|
||||
# If we haven't seen this repo, or this one is not a fork (prefer originals)
|
||||
if repo_name not in unique_repos or not repo.get('fork', False):
|
||||
unique_repos[repo_name] = repo
|
||||
|
||||
# Filter based on search
|
||||
filtered_repos = [
|
||||
repo for repo in unique_repos.values()
|
||||
if search_term in repo['name'].lower() or
|
||||
search_term in repo.get('description', '').lower()
|
||||
]
|
||||
|
||||
# Sort by name
|
||||
filtered_repos.sort(key=lambda r: r['name'].lower())
|
||||
|
||||
# Get existing repo widgets
|
||||
existing_widgets = {}
|
||||
for widget in self.repo_container.winfo_children():
|
||||
if hasattr(widget, 'repo_data'):
|
||||
existing_widgets[widget.repo_data['name']] = widget
|
||||
|
||||
# Remove widgets for repos no longer in filtered list
|
||||
filtered_names = {repo['name'] for repo in filtered_repos}
|
||||
for name, widget in existing_widgets.items():
|
||||
if name not in filtered_names:
|
||||
widget.destroy()
|
||||
|
||||
# Update or create repository items
|
||||
for i, repo in enumerate(filtered_repos):
|
||||
if repo['name'] in existing_widgets:
|
||||
# Update existing widget
|
||||
widget = existing_widgets[repo['name']]
|
||||
self._update_repo_item(widget, repo)
|
||||
# Ensure correct order
|
||||
widget.pack_forget()
|
||||
widget.pack(fill="x", pady=2)
|
||||
else:
|
||||
# Create new widget
|
||||
self._create_repo_item(repo)
|
||||
|
||||
def _create_repo_item(self, repo: Dict):
|
||||
"""Create a repository item widget"""
|
||||
# Main frame
|
||||
item_frame = ctk.CTkFrame(
|
||||
self.repo_container,
|
||||
fg_color=COLORS['bg_gitea_tile'],
|
||||
corner_radius=6,
|
||||
height=70
|
||||
)
|
||||
item_frame.pack(fill="x", pady=2)
|
||||
item_frame.pack_propagate(False)
|
||||
|
||||
# Store repo data
|
||||
item_frame.repo_data = repo
|
||||
repo['_widget'] = item_frame
|
||||
|
||||
# Content frame
|
||||
content_frame = ctk.CTkFrame(item_frame, fg_color="transparent")
|
||||
content_frame.pack(fill="both", expand=True, padx=10, pady=8)
|
||||
|
||||
# Top row - name and status
|
||||
top_row = ctk.CTkFrame(content_frame, fg_color="transparent")
|
||||
top_row.pack(fill="x")
|
||||
|
||||
# Repository name
|
||||
name_label = ctk.CTkLabel(
|
||||
top_row,
|
||||
text=repo['name'],
|
||||
font=FONTS['body'],
|
||||
text_color=COLORS['text_primary'],
|
||||
anchor="w"
|
||||
)
|
||||
name_label.pack(side="left", fill="x", expand=True)
|
||||
|
||||
# Clone/Status indicator
|
||||
clone_dir = self._get_clone_directory()
|
||||
local_path = clone_dir / repo['name']
|
||||
if local_path.exists():
|
||||
status_label = ctk.CTkLabel(
|
||||
top_row,
|
||||
text="✓ Cloned",
|
||||
font=FONTS['small'],
|
||||
text_color=COLORS['accent_success']
|
||||
)
|
||||
status_label.pack(side="right", padx=(5, 0))
|
||||
|
||||
# Check if it's a CPM project
|
||||
if self.main_window and hasattr(self.main_window, 'project_manager'):
|
||||
for project_data in self.main_window.project_manager.projects:
|
||||
# project_data is a Project object, check if it has gitea_repo attribute
|
||||
if hasattr(project_data, 'gitea_repo') and project_data.gitea_repo == f"{self.organization_name}/{repo['name']}":
|
||||
cpm_label = ctk.CTkLabel(
|
||||
top_row,
|
||||
text="📁 CPM",
|
||||
font=FONTS['small'],
|
||||
text_color=COLORS['accent_primary']
|
||||
)
|
||||
cpm_label.pack(side="right", padx=(5, 0))
|
||||
break
|
||||
|
||||
# Description
|
||||
if repo.get('description'):
|
||||
desc_label = ctk.CTkLabel(
|
||||
content_frame,
|
||||
text=repo['description'][:60] + '...' if len(repo.get('description', '')) > 60 else repo['description'],
|
||||
font=FONTS['small'],
|
||||
text_color=COLORS['text_secondary'],
|
||||
anchor="w"
|
||||
)
|
||||
desc_label.pack(fill="x", pady=(2, 0))
|
||||
|
||||
# Bottom row - stats
|
||||
stats_row = ctk.CTkFrame(content_frame, fg_color="transparent")
|
||||
stats_row.pack(fill="x", pady=(4, 0))
|
||||
|
||||
# Private/Public
|
||||
visibility = "🔒 Private" if repo.get('private', False) else "🌍 Public"
|
||||
vis_label = ctk.CTkLabel(
|
||||
stats_row,
|
||||
text=visibility,
|
||||
font=FONTS['small'],
|
||||
text_color=COLORS['text_dim']
|
||||
)
|
||||
vis_label.pack(side="left", padx=(0, 10))
|
||||
|
||||
# Stars if available
|
||||
if 'stars_count' in repo:
|
||||
star_label = ctk.CTkLabel(
|
||||
stats_row,
|
||||
text=f"⭐ {repo['stars_count']}",
|
||||
font=FONTS['small'],
|
||||
text_color=COLORS['text_dim']
|
||||
)
|
||||
star_label.pack(side="left", padx=(0, 10))
|
||||
|
||||
# Menu button
|
||||
menu_btn = ctk.CTkButton(
|
||||
stats_row,
|
||||
text="⋮",
|
||||
width=30,
|
||||
height=24,
|
||||
fg_color=COLORS['bg_tile'],
|
||||
hover_color=COLORS['bg_tile_hover'],
|
||||
font=('Segoe UI', 16)
|
||||
)
|
||||
menu_btn.configure(command=lambda: self._show_gitea_menu(repo, menu_btn))
|
||||
menu_btn.pack(side="right")
|
||||
|
||||
# Click to select
|
||||
def on_click(event):
|
||||
self._select_repository(repo)
|
||||
|
||||
self._bind_click_recursive(item_frame, on_click)
|
||||
|
||||
# Hover effect
|
||||
def on_enter(e):
|
||||
if self.selected_repo != repo:
|
||||
item_frame.configure(fg_color=COLORS['bg_gitea_hover'])
|
||||
|
||||
def on_leave(e):
|
||||
if self.selected_repo != repo:
|
||||
item_frame.configure(fg_color=COLORS['bg_gitea_tile'])
|
||||
|
||||
item_frame.bind("<Enter>", on_enter)
|
||||
item_frame.bind("<Leave>", on_leave)
|
||||
|
||||
def _update_repo_item(self, item_frame: ctk.CTkFrame, repo: Dict):
|
||||
"""Update an existing repository item widget"""
|
||||
# Update stored data
|
||||
item_frame.repo_data = repo
|
||||
repo['_widget'] = item_frame
|
||||
|
||||
# Update clone status
|
||||
local_path = Path.home() / "GiteaRepos" / repo['name']
|
||||
|
||||
# Find status label in widget hierarchy
|
||||
for widget in item_frame.winfo_children():
|
||||
if isinstance(widget, ctk.CTkFrame):
|
||||
for child in widget.winfo_children():
|
||||
if isinstance(child, ctk.CTkFrame):
|
||||
for subchild in child.winfo_children():
|
||||
if isinstance(subchild, ctk.CTkLabel) and "✓ Cloned" in subchild.cget("text"):
|
||||
if not local_path.exists():
|
||||
subchild.destroy()
|
||||
break
|
||||
|
||||
def _select_repository(self, repo: Dict):
|
||||
"""Select a repository"""
|
||||
# Deselect previous
|
||||
if self.selected_repo and '_widget' in self.selected_repo:
|
||||
self.selected_repo['_widget'].configure(
|
||||
fg_color=COLORS['bg_gitea_tile']
|
||||
)
|
||||
|
||||
# Select new
|
||||
self.selected_repo = repo
|
||||
if '_widget' in repo:
|
||||
repo['_widget'].configure(
|
||||
fg_color=COLORS['bg_selected']
|
||||
)
|
||||
|
||||
# Notify callback
|
||||
if self.on_repo_select:
|
||||
self.on_repo_select(repo)
|
||||
|
||||
def _on_search_changed(self, *args):
|
||||
"""Handle search text change"""
|
||||
self._update_repository_list()
|
||||
|
||||
def update_theme(self):
|
||||
"""Update colors when theme changes"""
|
||||
# Import updated colors
|
||||
from gui.styles import get_colors, FONTS
|
||||
global COLORS
|
||||
COLORS = get_colors()
|
||||
|
||||
# Update main frame
|
||||
self.configure(fg_color=COLORS['bg_secondary'])
|
||||
|
||||
# Update labels
|
||||
self.title_label.configure(text_color=COLORS['text_primary'])
|
||||
|
||||
# Update search entry
|
||||
self.search_entry.configure(
|
||||
fg_color=COLORS['bg_primary'],
|
||||
text_color=COLORS['text_primary'],
|
||||
border_color=COLORS['border_primary']
|
||||
)
|
||||
|
||||
# Update scroll frame and container
|
||||
self.scroll_frame.configure(fg_color=COLORS['bg_primary'])
|
||||
self.repo_container.configure(fg_color=COLORS['bg_primary'])
|
||||
|
||||
# Update status label
|
||||
self.status_label.configure(text_color=COLORS['text_secondary'])
|
||||
|
||||
# Refresh the repository list to update item colors
|
||||
self._update_repository_list()
|
||||
|
||||
# Clear any previous selection when theme changes
|
||||
if self.selected_repo and '_widget' in self.selected_repo:
|
||||
self.selected_repo['_widget'].configure(fg_color=COLORS['bg_gitea_tile'])
|
||||
self.selected_repo = None
|
||||
|
||||
def _clone_repository(self, repo: Dict):
|
||||
"""Clone repository and create CPM project"""
|
||||
if self.main_window and hasattr(self.main_window, 'clone_repository'):
|
||||
self.main_window.clone_repository(repo)
|
||||
|
||||
def _open_in_explorer(self, repo: Dict):
|
||||
"""Open repository folder in file explorer"""
|
||||
import platform
|
||||
import subprocess
|
||||
local_path = Path.home() / "GiteaRepos" / repo['name']
|
||||
if local_path.exists():
|
||||
if platform.system() == 'Windows':
|
||||
os.startfile(str(local_path))
|
||||
elif platform.system() == 'Darwin':
|
||||
subprocess.Popen(['open', str(local_path)])
|
||||
else:
|
||||
subprocess.Popen(['xdg-open', str(local_path)])
|
||||
|
||||
def _git_status(self, repo: Dict):
|
||||
"""Show git status for repository"""
|
||||
if self.main_window and hasattr(self.main_window, 'show_git_status'):
|
||||
# Create a pseudo-project object for compatibility
|
||||
from types import SimpleNamespace
|
||||
project = SimpleNamespace()
|
||||
project.name = repo['name']
|
||||
project.path = str(Path.home() / "GiteaRepos" / repo['name'])
|
||||
self.main_window.show_git_status(project)
|
||||
|
||||
def _git_pull(self, repo: Dict):
|
||||
"""Pull changes from remote"""
|
||||
if self.main_window and hasattr(self.main_window, 'pull_from_gitea'):
|
||||
from types import SimpleNamespace
|
||||
project = SimpleNamespace()
|
||||
project.name = repo['name']
|
||||
project.path = str(Path.home() / "GiteaRepos" / repo['name'])
|
||||
self.main_window.pull_from_gitea(project)
|
||||
|
||||
def _git_fetch(self, repo: Dict):
|
||||
"""Fetch changes from remote"""
|
||||
if self.main_window and hasattr(self.main_window, 'fetch_from_gitea'):
|
||||
self.main_window.fetch_from_gitea(repo)
|
||||
|
||||
def _delete_local_copy(self, repo: Dict):
|
||||
"""Delete local repository copy"""
|
||||
from tkinter import messagebox
|
||||
local_path = Path.home() / "GiteaRepos" / repo['name']
|
||||
if messagebox.askyesno("Lokale Kopie löschen",
|
||||
f"Möchten Sie die lokale Kopie von '{repo['name']}' wirklich löschen?\n\n"
|
||||
f"Pfad: {local_path}"):
|
||||
try:
|
||||
import shutil
|
||||
shutil.rmtree(local_path)
|
||||
messagebox.showinfo("Erfolg", f"Lokale Kopie von '{repo['name']}' wurde gelöscht.")
|
||||
self.refresh_repositories()
|
||||
except Exception as e:
|
||||
messagebox.showerror("Fehler", f"Fehler beim Löschen: {str(e)}")
|
||||
|
||||
def _show_repo_info(self, repo: Dict):
|
||||
"""Show repository information"""
|
||||
from tkinter import messagebox
|
||||
info = f"Repository: {repo['name']}\n"
|
||||
info += f"Beschreibung: {repo.get('description', 'Keine Beschreibung')}\n"
|
||||
info += f"Privat: {'Ja' if repo.get('private', False) else 'Nein'}\n"
|
||||
info += f"Erstellt: {repo.get('created_at', 'Unbekannt')}\n"
|
||||
info += f"Aktualisiert: {repo.get('updated_at', 'Unbekannt')}\n"
|
||||
messagebox.showinfo("Repository Info", info)
|
||||
|
||||
def _open_in_browser(self, repo: Dict):
|
||||
"""Open repository in browser"""
|
||||
import webbrowser
|
||||
if 'html_url' in repo:
|
||||
webbrowser.open(repo['html_url'])
|
||||
|
||||
def _bind_click_recursive(self, widget, callback):
|
||||
"""Recursively bind click event to widget and all children"""
|
||||
# Don't bind to buttons to avoid interfering with their functionality
|
||||
if not isinstance(widget, ctk.CTkButton):
|
||||
widget.bind("<Button-1>", callback)
|
||||
|
||||
for child in widget.winfo_children():
|
||||
# Skip buttons
|
||||
if not isinstance(child, ctk.CTkButton):
|
||||
self._bind_click_recursive(child, callback)
|
||||
|
||||
def clear_selection(self):
|
||||
"""Clear the current selection"""
|
||||
if self.selected_repo and '_widget' in self.selected_repo:
|
||||
# Remove border from selection
|
||||
self.selected_repo['_widget'].configure(border_width=0)
|
||||
self.selected_repo = None
|
||||
319
gui/gitea_toolbar.py
Normale Datei
319
gui/gitea_toolbar.py
Normale Datei
@ -0,0 +1,319 @@
|
||||
"""
|
||||
Gitea Toolbar for context-sensitive Git operations
|
||||
"""
|
||||
|
||||
import customtkinter as ctk
|
||||
from tkinter import messagebox
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import Optional, Callable, Dict
|
||||
from gui.styles import COLORS, FONTS
|
||||
|
||||
class GiteaToolbar(ctk.CTkFrame):
|
||||
def __init__(self, parent, **kwargs):
|
||||
super().__init__(parent, fg_color=COLORS['bg_secondary'], height=60, **kwargs)
|
||||
self.pack_propagate(False)
|
||||
|
||||
# Callbacks
|
||||
self.callbacks: Dict[str, Optional[Callable]] = {
|
||||
'commit': None,
|
||||
'push': None,
|
||||
'pull': None,
|
||||
'fetch': None,
|
||||
'status': None,
|
||||
'branch': None,
|
||||
'link': None,
|
||||
'clone': None,
|
||||
'create_project': None
|
||||
}
|
||||
|
||||
# Current context
|
||||
self.current_context = None # 'local_project', 'gitea_repo', or None
|
||||
self.current_item = None # Project or Repo object
|
||||
|
||||
# Animation
|
||||
self.is_visible = False
|
||||
self.target_height = 60
|
||||
self.current_height = 0
|
||||
|
||||
# Create buttons container
|
||||
self.button_container = ctk.CTkFrame(self, fg_color="transparent")
|
||||
self.button_container.pack(fill="both", expand=True, padx=10, pady=8)
|
||||
|
||||
# Status label
|
||||
self.status_label = ctk.CTkLabel(
|
||||
self.button_container,
|
||||
text="",
|
||||
font=FONTS['small'],
|
||||
text_color=COLORS['text_secondary']
|
||||
)
|
||||
self.status_label.pack(side="left", padx=(0, 20))
|
||||
|
||||
# Button groups
|
||||
self.button_groups = {
|
||||
'local_project': [],
|
||||
'gitea_repo': []
|
||||
}
|
||||
|
||||
self.setup_buttons()
|
||||
|
||||
def setup_buttons(self):
|
||||
"""Setup all buttons for different contexts"""
|
||||
# Buttons for local project context
|
||||
local_buttons = [
|
||||
("📊 Status", "status", "Git-Status anzeigen\nZeigt uncommittete Änderungen"),
|
||||
("💾 Commit", "commit", "Änderungen speichern\nLokale Änderungen committen"),
|
||||
("📤 Push", "push", "Zu Gitea hochladen\nCommits zum Server pushen"),
|
||||
("📥 Pull", "pull", "Änderungen abrufen\nNeueste Änderungen vom Server"),
|
||||
("🔗 Verknüpfen", "link", "Mit Gitea verknüpfen\nLokales Projekt mit Repository verbinden"),
|
||||
("🌿 Branch", "branch", "Branch-Verwaltung\nBranches anzeigen und wechseln")
|
||||
]
|
||||
|
||||
# Buttons for Gitea repo context
|
||||
gitea_buttons = [
|
||||
("📥 Clone", "clone", "Repository klonen\nLokale Kopie erstellen und Projekt anlegen"),
|
||||
("🔄 Fetch", "fetch", "Updates prüfen\nNeueste Änderungen abrufen"),
|
||||
("📊 Info", "status", "Repository-Info\nDetails zum Repository anzeigen")
|
||||
]
|
||||
|
||||
# Create button frames
|
||||
self.local_frame = ctk.CTkFrame(self.button_container, fg_color="transparent")
|
||||
self.gitea_frame = ctk.CTkFrame(self.button_container, fg_color="transparent")
|
||||
|
||||
# Create local project buttons
|
||||
for text, callback_name, tooltip in local_buttons:
|
||||
btn = self.create_button(self.local_frame, text, callback_name, tooltip)
|
||||
self.button_groups['local_project'].append(btn)
|
||||
|
||||
# Create Gitea repo buttons
|
||||
for text, callback_name, tooltip in gitea_buttons:
|
||||
btn = self.create_button(self.gitea_frame, text, callback_name, tooltip)
|
||||
self.button_groups['gitea_repo'].append(btn)
|
||||
|
||||
def create_button(self, parent, text, callback_name, tooltip):
|
||||
"""Create a button with tooltip"""
|
||||
btn = ctk.CTkButton(
|
||||
parent,
|
||||
text=text,
|
||||
command=lambda: self.execute_callback(callback_name),
|
||||
width=120,
|
||||
height=36,
|
||||
fg_color=COLORS['accent_secondary'],
|
||||
hover_color=COLORS['accent_hover'],
|
||||
text_color="#FFFFFF",
|
||||
font=FONTS['body']
|
||||
)
|
||||
btn.pack(side="left", padx=4)
|
||||
|
||||
# Create tooltip
|
||||
self.create_tooltip(btn, tooltip)
|
||||
|
||||
return btn
|
||||
|
||||
def create_tooltip(self, widget, text):
|
||||
"""Create a tooltip for a widget"""
|
||||
def on_enter(event):
|
||||
# Destroy any existing tooltip first
|
||||
if hasattr(widget, 'tooltip') and widget.tooltip:
|
||||
try:
|
||||
widget.tooltip.destroy()
|
||||
except:
|
||||
pass
|
||||
|
||||
tooltip = ctk.CTkToplevel(widget)
|
||||
tooltip.wm_overrideredirect(True)
|
||||
tooltip.wm_geometry(f"+{event.x_root + 10}+{event.y_root + 10}")
|
||||
|
||||
label = ctk.CTkLabel(
|
||||
tooltip,
|
||||
text=text,
|
||||
font=FONTS['small'],
|
||||
fg_color=COLORS['bg_secondary'],
|
||||
text_color=COLORS['text_primary'],
|
||||
corner_radius=6,
|
||||
padx=10,
|
||||
pady=5
|
||||
)
|
||||
label.pack()
|
||||
|
||||
widget.tooltip = tooltip
|
||||
|
||||
def on_leave(event):
|
||||
if hasattr(widget, 'tooltip') and widget.tooltip:
|
||||
try:
|
||||
widget.tooltip.destroy()
|
||||
widget.tooltip = None
|
||||
except:
|
||||
pass
|
||||
|
||||
# Also destroy tooltip when button is clicked
|
||||
def on_click(event):
|
||||
if hasattr(widget, 'tooltip') and widget.tooltip:
|
||||
try:
|
||||
widget.tooltip.destroy()
|
||||
widget.tooltip = None
|
||||
except:
|
||||
pass
|
||||
|
||||
widget.bind("<Enter>", on_enter)
|
||||
widget.bind("<Leave>", on_leave)
|
||||
widget.bind("<Button-1>", on_click, add=True)
|
||||
|
||||
def set_callback(self, name: str, callback: Callable):
|
||||
"""Set a callback for a button"""
|
||||
if name in self.callbacks:
|
||||
self.callbacks[name] = callback
|
||||
|
||||
def execute_callback(self, name: str):
|
||||
"""Execute a callback if set"""
|
||||
if name in self.callbacks and self.callbacks[name]:
|
||||
# Run in thread to avoid blocking UI
|
||||
threading.Thread(
|
||||
target=self.callbacks[name],
|
||||
args=(self.current_item,),
|
||||
daemon=True
|
||||
).start()
|
||||
|
||||
def show(self):
|
||||
"""Show the toolbar"""
|
||||
if not self.is_visible:
|
||||
self.animate_show()
|
||||
|
||||
def show_for_context(self, context: str, item=None, status_text: str = ""):
|
||||
"""Show toolbar for specific context"""
|
||||
if context not in ['local_project', 'gitea_repo']:
|
||||
self.hide()
|
||||
return
|
||||
|
||||
self.current_context = context
|
||||
self.current_item = item
|
||||
|
||||
# Update status
|
||||
self.status_label.configure(text=status_text)
|
||||
|
||||
# Hide all frames
|
||||
self.local_frame.pack_forget()
|
||||
self.gitea_frame.pack_forget()
|
||||
|
||||
# Show relevant frame
|
||||
if context == 'local_project':
|
||||
self.local_frame.pack(side="left", padx=10)
|
||||
elif context == 'gitea_repo':
|
||||
self.gitea_frame.pack(side="left", padx=10)
|
||||
|
||||
# Animate show
|
||||
if not self.is_visible:
|
||||
self.animate_show()
|
||||
|
||||
def hide(self):
|
||||
"""Hide the toolbar"""
|
||||
if self.is_visible:
|
||||
self.animate_hide()
|
||||
|
||||
def animate_show(self):
|
||||
"""Animate toolbar appearing"""
|
||||
print("animate_show called")
|
||||
self.is_visible = True
|
||||
|
||||
# Pack the toolbar
|
||||
try:
|
||||
# Simple approach: just pack after any header that exists
|
||||
parent_children = self.master.winfo_children()
|
||||
print(f"Parent has {len(parent_children)} children")
|
||||
|
||||
# Find the best position (after header, before content)
|
||||
packed = False
|
||||
for i, child in enumerate(parent_children):
|
||||
# Look for a frame that might be the header
|
||||
if isinstance(child, ctk.CTkFrame):
|
||||
# Check if it's likely the header (has small height or contains title)
|
||||
try:
|
||||
if child.winfo_height() < 100 or any(
|
||||
isinstance(w, ctk.CTkLabel) and "Claude Project Manager" in str(w.cget("text"))
|
||||
for w in child.winfo_children()
|
||||
):
|
||||
print(f"Packing toolbar after header at position {i}")
|
||||
self.pack(fill="x", after=child, pady=(0, 10))
|
||||
packed = True
|
||||
break
|
||||
except:
|
||||
pass
|
||||
|
||||
if not packed:
|
||||
print("Packing toolbar at default position")
|
||||
self.pack(fill="x", pady=(0, 10))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error showing toolbar: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
self.pack(fill="x", pady=(0, 10))
|
||||
|
||||
# Skip animation for now - just show full height
|
||||
self.configure(height=self.target_height)
|
||||
print(f"Toolbar configured with height {self.target_height}")
|
||||
|
||||
def animate_hide(self):
|
||||
"""Animate toolbar disappearing"""
|
||||
self.is_visible = False
|
||||
# Destroy all tooltips before hiding
|
||||
for group in self.button_groups.values():
|
||||
for btn in group:
|
||||
if hasattr(btn, 'tooltip') and btn.tooltip:
|
||||
try:
|
||||
btn.tooltip.destroy()
|
||||
btn.tooltip = None
|
||||
except:
|
||||
pass
|
||||
# Skip animation for now - just hide immediately
|
||||
self.pack_forget()
|
||||
|
||||
def _animate_height(self, start, end, duration=200, callback=None):
|
||||
"""Animate height change"""
|
||||
steps = 10
|
||||
step_duration = duration // steps
|
||||
step_size = (end - start) / steps
|
||||
|
||||
def animate_step(current_step):
|
||||
if current_step <= steps:
|
||||
new_height = start + (step_size * current_step)
|
||||
self.configure(height=new_height)
|
||||
self.after(step_duration, lambda: animate_step(current_step + 1))
|
||||
elif callback:
|
||||
callback()
|
||||
|
||||
animate_step(0)
|
||||
|
||||
def set_selected_repo(self, repo):
|
||||
"""Set the selected repository and show appropriate buttons"""
|
||||
self.show_for_context('gitea_repo', repo)
|
||||
|
||||
def update_button_states(self, states: Dict[str, bool]):
|
||||
"""Update button enabled/disabled states"""
|
||||
for context_buttons in self.button_groups.values():
|
||||
for btn in context_buttons:
|
||||
# Extract callback name from button text
|
||||
btn_text = btn.cget("text")
|
||||
for callback_name in self.callbacks:
|
||||
if callback_name in states:
|
||||
# Simple matching - could be improved
|
||||
btn.configure(state="normal" if states[callback_name] else "disabled")
|
||||
|
||||
def refresh_colors(self):
|
||||
"""Refresh the toolbar's colors"""
|
||||
from gui.styles import COLORS
|
||||
|
||||
# Update frame color
|
||||
self.configure(fg_color=COLORS['bg_secondary'])
|
||||
|
||||
# Update status label
|
||||
self.status_label.configure(text_color=COLORS['text_secondary'])
|
||||
|
||||
# Update all buttons
|
||||
for context_buttons in self.button_groups.values():
|
||||
for btn in context_buttons:
|
||||
btn.configure(
|
||||
fg_color=COLORS['accent_primary'],
|
||||
hover_color=COLORS['accent_hover'],
|
||||
text_color="#FFFFFF"
|
||||
)
|
||||
16
gui/handlers/__init__.py
Normale Datei
16
gui/handlers/__init__.py
Normale Datei
@ -0,0 +1,16 @@
|
||||
"""
|
||||
Handler modules for MainWindow refactoring
|
||||
Separates concerns and reduces God Class anti-pattern
|
||||
"""
|
||||
|
||||
from .gitea_operations import GiteaOperationsHandler
|
||||
from .process_manager import ProcessManagerHandler
|
||||
from .project_manager import ProjectManagerHandler
|
||||
from .ui_helpers import UIHelpersHandler
|
||||
|
||||
__all__ = [
|
||||
'GiteaOperationsHandler',
|
||||
'ProcessManagerHandler',
|
||||
'ProjectManagerHandler',
|
||||
'UIHelpersHandler'
|
||||
]
|
||||
19
gui/handlers/base_handler.py
Normale Datei
19
gui/handlers/base_handler.py
Normale Datei
@ -0,0 +1,19 @@
|
||||
"""
|
||||
Base Handler for common functionality
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from utils.logger import logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gui.main_window import MainWindow
|
||||
|
||||
|
||||
class BaseHandler:
|
||||
"""Base class for all handlers"""
|
||||
|
||||
def __init__(self, main_window: 'MainWindow'):
|
||||
"""Initialize with reference to main window"""
|
||||
self.main_window = main_window
|
||||
self.root = main_window.root
|
||||
logger.info(f"{self.__class__.__name__} initialized")
|
||||
1891
gui/handlers/gitea_operations.py
Normale Datei
1891
gui/handlers/gitea_operations.py
Normale Datei
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
101
gui/handlers/process_manager.py
Normale Datei
101
gui/handlers/process_manager.py
Normale Datei
@ -0,0 +1,101 @@
|
||||
"""
|
||||
Process Manager Handler
|
||||
Handles process monitoring and management operations
|
||||
"""
|
||||
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from utils.logger import logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gui.main_window import MainWindow
|
||||
from project_manager import Project
|
||||
|
||||
|
||||
class ProcessManagerHandler:
|
||||
"""Handles all process management operations for MainWindow"""
|
||||
|
||||
def __init__(self, main_window: 'MainWindow'):
|
||||
"""Initialize with reference to main window"""
|
||||
self.main_window = main_window
|
||||
self.root = main_window.root
|
||||
self.process_manager = main_window.process_manager
|
||||
self.process_tracker = main_window.process_tracker
|
||||
self.project_manager = main_window.project_manager
|
||||
logger.info("ProcessManagerHandler initialized")
|
||||
|
||||
def monitor_process(self, project: 'Project', process) -> None:
|
||||
"""Monitor a process for a project"""
|
||||
return self.main_window._original_monitor_process(project, process)
|
||||
|
||||
def check_process_status(self) -> None:
|
||||
"""Check status of all processes"""
|
||||
return self.main_window._original_check_process_status()
|
||||
|
||||
def stop_project(self, project: 'Project') -> None:
|
||||
"""Stop a running project"""
|
||||
return self.main_window._original_stop_project(project)
|
||||
|
||||
def update_status(self, message: str, error: bool = False) -> None:
|
||||
"""Update status bar message"""
|
||||
# Direct implementation
|
||||
from gui.styles import COLORS
|
||||
|
||||
if hasattr(self.main_window, 'status_label'):
|
||||
self.main_window.status_label.configure(
|
||||
text=message,
|
||||
text_color=COLORS['accent_error'] if error else COLORS['text_secondary']
|
||||
)
|
||||
logger.debug(f"Status updated: {message} (error={error})")
|
||||
|
||||
def download_log(self) -> None:
|
||||
"""Download comprehensive application log with all interactions"""
|
||||
# Direct implementation
|
||||
from tkinter import filedialog, messagebox
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
logger.info(f"Download log clicked - Total entries: {len(logger.log_entries)}")
|
||||
|
||||
try:
|
||||
# Generate default filename with timestamp
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
default_filename = f"CPM_FullLog_{timestamp}.log"
|
||||
|
||||
# Open file dialog to choose save location
|
||||
file_path = filedialog.asksaveasfilename(
|
||||
defaultextension=".log",
|
||||
filetypes=[("Log files", "*.log"), ("Text files", "*.txt"), ("All files", "*.*")],
|
||||
initialfile=default_filename,
|
||||
title="Save Complete Application Log"
|
||||
)
|
||||
|
||||
if file_path:
|
||||
# Export logs to chosen location with system info
|
||||
logger.export_logs(file_path, include_system_info=True)
|
||||
|
||||
# Add completion entry
|
||||
logger.info(f"Log export completed - Total entries: {len(logger.log_entries)}, Interactions: {logger.interaction_count}")
|
||||
|
||||
# Update status
|
||||
self.update_status(f"Log saved: {os.path.basename(file_path)} ({len(logger.log_entries)} entries)")
|
||||
|
||||
# Show success message with log details
|
||||
messagebox.showinfo(
|
||||
"Log Export Successful",
|
||||
f"Complete application log saved to:\n{file_path}\n\n"
|
||||
f"Total log entries: {len(logger.log_entries):,}\n"
|
||||
f"UI interactions logged: {logger.interaction_count:,}\n"
|
||||
f"File size: {os.path.getsize(file_path) / 1024:.1f} KB"
|
||||
)
|
||||
else:
|
||||
logger.info("Log export cancelled by user")
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error saving log: {str(e)}"
|
||||
self.update_status(error_msg, error=True)
|
||||
logger.log_exception(e, "download_log")
|
||||
messagebox.showerror("Export Error", error_msg)
|
||||
|
||||
def _handle_process_ended(self, project: 'Project') -> None:
|
||||
"""Handle process end event (private method)"""
|
||||
return self.main_window._original_handle_process_ended(project)
|
||||
102
gui/handlers/project_manager.py
Normale Datei
102
gui/handlers/project_manager.py
Normale Datei
@ -0,0 +1,102 @@
|
||||
"""
|
||||
Project Manager Handler
|
||||
Handles project CRUD operations and project-related UI updates
|
||||
"""
|
||||
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from utils.logger import logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gui.main_window import MainWindow
|
||||
from project_manager import Project
|
||||
|
||||
|
||||
class ProjectManagerHandler:
|
||||
"""Handles all project management operations for MainWindow"""
|
||||
|
||||
def __init__(self, main_window: 'MainWindow'):
|
||||
"""Initialize with reference to main window"""
|
||||
self.main_window = main_window
|
||||
self.root = main_window.root
|
||||
self.project_manager = main_window.project_manager
|
||||
self.terminal_launcher = main_window.terminal_launcher
|
||||
self.readme_generator = main_window.readme_generator
|
||||
self.vps_connection = main_window.vps_connection
|
||||
logger.info("ProjectManagerHandler initialized")
|
||||
|
||||
def add_new_project(self) -> None:
|
||||
"""Add a new project with logging"""
|
||||
logger.info("Add new project initiated")
|
||||
return self.main_window._original_add_new_project()
|
||||
|
||||
def open_project(self, project: 'Project') -> None:
|
||||
"""Open a project with comprehensive logging"""
|
||||
logger.info(f"Opening project: {project.name} (ID: {project.id}) at {project.path}")
|
||||
return self.main_window._original_open_project(project)
|
||||
|
||||
def delete_project(self, project: 'Project') -> None:
|
||||
"""Delete a project"""
|
||||
# Direct implementation
|
||||
from tkinter import messagebox
|
||||
logger.info(f"Attempting to delete project: {project.name}")
|
||||
|
||||
if messagebox.askyesno("Projekt löschen",
|
||||
f"Möchten Sie das Projekt '{project.name}' wirklich aus dem Projekt-Manager entfernen?\n\n"
|
||||
"Hinweis: Die Dateien werden NICHT gelöscht."):
|
||||
self.project_manager.remove_project(project.id)
|
||||
self.main_window.refresh_projects()
|
||||
if hasattr(self.main_window, 'update_status'):
|
||||
self.main_window.update_status(f"Removed: {project.name}")
|
||||
logger.info(f"Project deleted: {project.name}")
|
||||
|
||||
def rename_project(self, project: 'Project') -> None:
|
||||
"""Rename a project with logging"""
|
||||
logger.info(f"Rename project initiated for: {project.name}")
|
||||
return self.main_window._original_rename_project(project)
|
||||
|
||||
def refresh_projects(self) -> None:
|
||||
"""Refresh the project display with logging"""
|
||||
logger.debug("Refresh projects called")
|
||||
logger.info(f"Refreshing projects - Total: {len(self.project_manager.projects)}")
|
||||
return self.main_window._original_refresh_projects()
|
||||
|
||||
def create_project_from_repo(self, repo_data: dict, project_path: str) -> None:
|
||||
"""Create a project from repository data"""
|
||||
return self.main_window._original_create_project_from_repo(repo_data, project_path)
|
||||
|
||||
def open_vps_connection(self, project: 'Project') -> None:
|
||||
"""Open VPS connection for project"""
|
||||
return self.main_window._original_open_vps_connection(project)
|
||||
|
||||
def open_admin_panel(self, project: 'Project') -> None:
|
||||
"""Open admin panel for project"""
|
||||
return self.main_window._original_open_admin_panel(project)
|
||||
|
||||
def open_vps_docker(self, project: 'Project') -> None:
|
||||
"""Open VPS Docker management"""
|
||||
return self.main_window._original_open_vps_docker(project)
|
||||
|
||||
def open_readme(self, project: 'Project') -> None:
|
||||
"""Open or generate README for project"""
|
||||
return self.main_window._original_open_readme(project)
|
||||
|
||||
def generate_readme_background(self, project: 'Project') -> None:
|
||||
"""Generate README in background"""
|
||||
return self.main_window._original_generate_readme_background(project)
|
||||
|
||||
def open_gitea_window(self) -> None:
|
||||
"""Open Gitea explorer window"""
|
||||
return self.main_window._original_open_gitea_window()
|
||||
|
||||
def on_gitea_repo_select(self, repo_data: dict) -> None:
|
||||
"""Handle Gitea repository selection"""
|
||||
return self.main_window._original_on_gitea_repo_select(repo_data)
|
||||
|
||||
def clear_project_selection(self) -> None:
|
||||
"""Clear current project selection"""
|
||||
return self.main_window._original_clear_project_selection()
|
||||
|
||||
def on_project_select(self, project: 'Project', tile) -> None:
|
||||
"""Handle project selection with logging"""
|
||||
logger.info(f"Project selected: {project.name} (has Gitea repo: {bool(getattr(project, 'gitea_repo', None))})")
|
||||
return self.main_window._original_on_project_select(project, tile)
|
||||
203
gui/handlers/ui_helpers.py
Normale Datei
203
gui/handlers/ui_helpers.py
Normale Datei
@ -0,0 +1,203 @@
|
||||
"""
|
||||
UI Helpers Handler
|
||||
Handles UI creation, updates, and helper functions
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from utils.logger import logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gui.main_window import MainWindow
|
||||
from project_manager import Project
|
||||
|
||||
|
||||
class UIHelpersHandler:
|
||||
"""Handles UI helper operations for MainWindow"""
|
||||
|
||||
def __init__(self, main_window: 'MainWindow'):
|
||||
"""Initialize with reference to main window"""
|
||||
self.main_window = main_window
|
||||
self.root = main_window.root
|
||||
logger.info("UIHelpersHandler initialized")
|
||||
|
||||
def setup_ui(self) -> None:
|
||||
"""Setup the main UI"""
|
||||
return self.main_window._original_setup_ui()
|
||||
|
||||
def create_header(self) -> None:
|
||||
"""Create header section"""
|
||||
# Direct implementation with proper references
|
||||
import customtkinter as ctk
|
||||
from gui.styles import COLORS, FONTS
|
||||
|
||||
header_frame = ctk.CTkFrame(self.main_window.main_container, fg_color=COLORS['bg_secondary'], height=80)
|
||||
header_frame.pack(fill="x", padx=0, pady=0)
|
||||
header_frame.pack_propagate(False)
|
||||
|
||||
# Title
|
||||
self.main_window.title_label = ctk.CTkLabel(
|
||||
header_frame,
|
||||
text="IntelSight - Claude Project Manager",
|
||||
font=FONTS['heading'],
|
||||
text_color=COLORS['text_primary']
|
||||
)
|
||||
self.main_window.title_label.pack(side="left", padx=30, pady=20)
|
||||
|
||||
# Toolbar buttons
|
||||
toolbar = ctk.CTkFrame(header_frame, fg_color="transparent")
|
||||
toolbar.pack(side="right", padx=30, pady=20)
|
||||
|
||||
# Log download button
|
||||
self.main_window.log_btn = ctk.CTkButton(
|
||||
toolbar,
|
||||
text="📥 Log",
|
||||
command=self.main_window.download_log,
|
||||
width=80,
|
||||
fg_color=COLORS['accent_primary'],
|
||||
hover_color=COLORS['accent_hover'],
|
||||
text_color="#FFFFFF",
|
||||
font=('Segoe UI', 12)
|
||||
)
|
||||
self.main_window.log_btn.pack(side="left", padx=(0, 10))
|
||||
|
||||
# Refresh button
|
||||
self.main_window.refresh_btn = ctk.CTkButton(
|
||||
toolbar,
|
||||
text="↻ Refresh",
|
||||
command=self.main_window.refresh_projects,
|
||||
width=100,
|
||||
fg_color=COLORS['bg_tile'],
|
||||
hover_color=COLORS['bg_tile_hover'],
|
||||
text_color=COLORS['text_primary']
|
||||
)
|
||||
self.main_window.refresh_btn.pack(side="left", padx=(0, 10))
|
||||
logger.debug("Header created")
|
||||
|
||||
def create_content_area(self) -> None:
|
||||
"""Create content area"""
|
||||
return self.main_window._original_create_content_area()
|
||||
|
||||
def create_status_bar(self) -> None:
|
||||
"""Create status bar"""
|
||||
# Direct implementation
|
||||
import customtkinter as ctk
|
||||
from gui.styles import COLORS, FONTS
|
||||
|
||||
self.main_window.status_bar = ctk.CTkFrame(
|
||||
self.main_window.main_container,
|
||||
fg_color=COLORS['bg_secondary'],
|
||||
height=30
|
||||
)
|
||||
self.main_window.status_bar.pack(fill="x", side="bottom")
|
||||
self.main_window.status_bar.pack_propagate(False)
|
||||
|
||||
self.main_window.status_label = ctk.CTkLabel(
|
||||
self.main_window.status_bar,
|
||||
text="Ready",
|
||||
font=FONTS['small'],
|
||||
text_color=COLORS['text_secondary']
|
||||
)
|
||||
self.main_window.status_label.pack(side="left", padx=20, pady=5)
|
||||
|
||||
# Project count
|
||||
self.main_window.count_label = ctk.CTkLabel(
|
||||
self.main_window.status_bar,
|
||||
text="0 projects",
|
||||
font=FONTS['small'],
|
||||
text_color=COLORS['text_secondary']
|
||||
)
|
||||
self.main_window.count_label.pack(side="right", padx=20, pady=5)
|
||||
logger.debug("Status bar created")
|
||||
|
||||
def create_project_tile(self, project: 'Project', parent) -> 'ProjectTile':
|
||||
"""Create a project tile"""
|
||||
return self.main_window._original_create_project_tile(project, parent)
|
||||
|
||||
def create_add_tile(self, parent) -> 'AddProjectTile':
|
||||
"""Create add project tile"""
|
||||
return self.main_window._original_create_add_tile(parent)
|
||||
|
||||
def create_project_tile_flow(self, project: 'Project', parent) -> 'ProjectTile':
|
||||
"""Create project tile for flow layout"""
|
||||
return self.main_window._original_create_project_tile_flow(project, parent)
|
||||
|
||||
def create_add_tile_flow(self, parent) -> 'AddProjectTile':
|
||||
"""Create add tile for flow layout"""
|
||||
return self.main_window._original_create_add_tile_flow(parent)
|
||||
|
||||
def refresh_ui(self) -> None:
|
||||
"""Refresh the UI"""
|
||||
return self.main_window._original_refresh_ui()
|
||||
|
||||
def load_and_apply_theme(self) -> None:
|
||||
"""Load and apply theme preference"""
|
||||
# Direct implementation - very simple method
|
||||
import customtkinter as ctk
|
||||
ctk.set_appearance_mode('dark')
|
||||
logger.info("Theme applied: dark mode")
|
||||
|
||||
def on_window_resize(self, event) -> None:
|
||||
"""Handle window resize event"""
|
||||
# Direct implementation
|
||||
# Only process resize events from the main window
|
||||
if event.widget == self.root:
|
||||
# Cancel previous timer
|
||||
if self.main_window.resize_timer:
|
||||
self.root.after_cancel(self.main_window.resize_timer)
|
||||
|
||||
# Set new timer to refresh after resize stops with differential update
|
||||
self.main_window.resize_timer = self.root.after(
|
||||
300,
|
||||
lambda: self.main_window.refresh_projects(differential=True)
|
||||
)
|
||||
logger.debug("Window resize event handled")
|
||||
|
||||
def setup_interaction_tracking(self) -> None:
|
||||
"""Setup interaction tracking"""
|
||||
return self.main_window._original_setup_interaction_tracking()
|
||||
|
||||
def _show_scrollable_info(self, title: str, content: str) -> None:
|
||||
"""Show scrollable information dialog"""
|
||||
# Direct implementation - standalone UI method
|
||||
import customtkinter as ctk
|
||||
|
||||
dialog = ctk.CTkToplevel(self.root)
|
||||
dialog.title(title)
|
||||
dialog.geometry("600x500")
|
||||
|
||||
# Center the dialog
|
||||
dialog.transient(self.root)
|
||||
dialog.update_idletasks()
|
||||
x = (dialog.winfo_screenwidth() - 600) // 2
|
||||
y = (dialog.winfo_screenheight() - 500) // 2
|
||||
dialog.geometry(f"600x500+{x}+{y}")
|
||||
|
||||
# Text widget with scrollbar
|
||||
text_frame = ctk.CTkFrame(dialog)
|
||||
text_frame.pack(fill="both", expand=True, padx=10, pady=10)
|
||||
|
||||
text_widget = ctk.CTkTextbox(text_frame, width=580, height=450)
|
||||
text_widget.pack(fill="both", expand=True)
|
||||
text_widget.insert("1.0", content)
|
||||
text_widget.configure(state="disabled")
|
||||
|
||||
# Close button
|
||||
close_btn = ctk.CTkButton(
|
||||
dialog,
|
||||
text="Schließen",
|
||||
command=dialog.destroy,
|
||||
width=100
|
||||
)
|
||||
close_btn.pack(pady=(0, 10))
|
||||
|
||||
dialog.grab_set()
|
||||
dialog.focus_set()
|
||||
logger.debug(f"Scrollable dialog shown: {title}")
|
||||
|
||||
def _differential_update(self, current_projects: list, previous_projects: list) -> None:
|
||||
"""Perform differential update of projects"""
|
||||
return self.main_window._original_differential_update(current_projects, previous_projects)
|
||||
|
||||
def _update_project_tiles_colors(self) -> None:
|
||||
"""Update project tile colors"""
|
||||
return self.main_window._original_update_project_tiles_colors()
|
||||
3374
gui/main_window.py
Normale Datei
3374
gui/main_window.py
Normale Datei
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
272
gui/progress_bar.py
Normale Datei
272
gui/progress_bar.py
Normale Datei
@ -0,0 +1,272 @@
|
||||
"""
|
||||
Progress Bar Component for Git Operations
|
||||
Provides visual feedback for long-running operations
|
||||
"""
|
||||
|
||||
import customtkinter as ctk
|
||||
from typing import Optional, Callable
|
||||
import threading
|
||||
import time
|
||||
|
||||
class ProgressBar(ctk.CTkToplevel):
|
||||
"""Modern progress bar window for Git operations"""
|
||||
|
||||
def __init__(self, parent, title: str = "Operation in Progress", width: int = 500):
|
||||
super().__init__(parent)
|
||||
|
||||
# Window configuration
|
||||
self.title(title)
|
||||
self.geometry(f"{width}x200")
|
||||
self.resizable(False, False)
|
||||
|
||||
# Center on parent window
|
||||
self.transient(parent)
|
||||
self.grab_set()
|
||||
|
||||
# Colors from styles
|
||||
from gui.styles import get_colors
|
||||
self.colors = get_colors()
|
||||
|
||||
# Configure window
|
||||
self.configure(fg_color=self.colors['bg_primary'])
|
||||
|
||||
# Create UI elements
|
||||
self._setup_ui()
|
||||
|
||||
# Variables
|
||||
self.is_cancelled = False
|
||||
self._update_callback: Optional[Callable] = None
|
||||
|
||||
# Center window on parent
|
||||
self._center_on_parent(parent)
|
||||
|
||||
def _setup_ui(self):
|
||||
"""Create the progress bar UI"""
|
||||
# Main container
|
||||
self.container = ctk.CTkFrame(
|
||||
self,
|
||||
fg_color=self.colors['bg_secondary'],
|
||||
corner_radius=10
|
||||
)
|
||||
self.container.pack(fill="both", expand=True, padx=20, pady=20)
|
||||
|
||||
# Title label
|
||||
self.title_label = ctk.CTkLabel(
|
||||
self.container,
|
||||
text="",
|
||||
font=("Segoe UI", 16, "bold"),
|
||||
text_color=self.colors['text_primary']
|
||||
)
|
||||
self.title_label.pack(pady=(10, 5))
|
||||
|
||||
# Status label
|
||||
self.status_label = ctk.CTkLabel(
|
||||
self.container,
|
||||
text="Initialisierung...",
|
||||
font=("Segoe UI", 14),
|
||||
text_color=self.colors['text_secondary']
|
||||
)
|
||||
self.status_label.pack(pady=(0, 15))
|
||||
|
||||
# Progress bar
|
||||
self.progress = ctk.CTkProgressBar(
|
||||
self.container,
|
||||
width=440,
|
||||
height=20,
|
||||
corner_radius=10,
|
||||
fg_color=self.colors['bg_tile'],
|
||||
progress_color=self.colors['accent_success'],
|
||||
mode="determinate"
|
||||
)
|
||||
self.progress.pack(pady=10)
|
||||
self.progress.set(0)
|
||||
|
||||
# Percentage label
|
||||
self.percentage_label = ctk.CTkLabel(
|
||||
self.container,
|
||||
text="0%",
|
||||
font=("Segoe UI", 12),
|
||||
text_color=self.colors['text_secondary']
|
||||
)
|
||||
self.percentage_label.pack(pady=(0, 10))
|
||||
|
||||
# Cancel button
|
||||
self.cancel_button = ctk.CTkButton(
|
||||
self.container,
|
||||
text="Abbrechen",
|
||||
width=100,
|
||||
height=32,
|
||||
corner_radius=6,
|
||||
fg_color=self.colors['accent_error'],
|
||||
hover_color="#FF5252",
|
||||
text_color="#FFFFFF",
|
||||
font=("Segoe UI", 13, "bold"),
|
||||
command=self._cancel_operation
|
||||
)
|
||||
self.cancel_button.pack(pady=5)
|
||||
|
||||
def _center_on_parent(self, parent):
|
||||
"""Center the window on parent"""
|
||||
self.update_idletasks()
|
||||
|
||||
# Get parent position and size
|
||||
parent_x = parent.winfo_x()
|
||||
parent_y = parent.winfo_y()
|
||||
parent_width = parent.winfo_width()
|
||||
parent_height = parent.winfo_height()
|
||||
|
||||
# Get this window size
|
||||
width = self.winfo_width()
|
||||
height = self.winfo_height()
|
||||
|
||||
# Calculate position
|
||||
x = parent_x + (parent_width - width) // 2
|
||||
y = parent_y + (parent_height - height) // 2
|
||||
|
||||
# Set position
|
||||
self.geometry(f"+{x}+{y}")
|
||||
|
||||
def _cancel_operation(self):
|
||||
"""Handle cancel button click"""
|
||||
self.is_cancelled = True
|
||||
self.cancel_button.configure(state="disabled", text="Wird abgebrochen...")
|
||||
|
||||
def update_progress(self, value: float, status: str, title: str = "", is_error: bool = False):
|
||||
"""Update progress bar
|
||||
|
||||
Args:
|
||||
value: Progress value (0.0 to 1.0)
|
||||
status: Status message to display
|
||||
title: Optional title update
|
||||
is_error: If True, colors the progress bar red
|
||||
"""
|
||||
# Ensure value is in range
|
||||
value = max(0.0, min(1.0, value))
|
||||
|
||||
# Update UI in main thread
|
||||
self.after(0, self._update_ui, value, status, title, is_error)
|
||||
|
||||
def _update_ui(self, value: float, status: str, title: str, is_error: bool = False):
|
||||
"""Update UI elements (must be called from main thread)"""
|
||||
if title:
|
||||
self.title_label.configure(text=title)
|
||||
|
||||
self.status_label.configure(text=status)
|
||||
self.progress.set(value)
|
||||
self.percentage_label.configure(text=f"{int(value * 100)}%")
|
||||
|
||||
# Change color if error
|
||||
if is_error:
|
||||
self.progress.configure(progress_color=self.colors['accent_error'])
|
||||
self.status_label.configure(text_color=self.colors['accent_error'])
|
||||
|
||||
# If complete, update button
|
||||
if value >= 1.0 and not is_error:
|
||||
self.cancel_button.configure(
|
||||
text="Schließen",
|
||||
state="normal",
|
||||
command=self.destroy
|
||||
)
|
||||
# Auto-close after 0.5 seconds if successful
|
||||
if not self.is_cancelled:
|
||||
self.after(500, self.destroy)
|
||||
|
||||
def set_update_callback(self, callback: Callable):
|
||||
"""Set callback for progress updates"""
|
||||
self._update_callback = callback
|
||||
|
||||
def simulate_progress(self, steps: list):
|
||||
"""Simulate progress for testing
|
||||
|
||||
Args:
|
||||
steps: List of (duration, status, progress) tuples
|
||||
"""
|
||||
def run_simulation():
|
||||
for duration, status, progress in steps:
|
||||
if self.is_cancelled:
|
||||
break
|
||||
|
||||
self.update_progress(progress, status)
|
||||
time.sleep(duration)
|
||||
|
||||
if self._update_callback:
|
||||
self._update_callback(progress, status)
|
||||
|
||||
thread = threading.Thread(target=run_simulation, daemon=True)
|
||||
thread.start()
|
||||
|
||||
|
||||
def set_error(self, error_message: str):
|
||||
"""Set error state and message"""
|
||||
self.update_progress(1.0, error_message, is_error=True)
|
||||
self.cancel_button.configure(
|
||||
text="OK",
|
||||
state="normal",
|
||||
command=self.destroy,
|
||||
fg_color=self.colors['accent_error']
|
||||
)
|
||||
|
||||
|
||||
class GitOperationProgress:
|
||||
"""Helper class to manage progress for Git operations"""
|
||||
|
||||
# Progress stages for different operations
|
||||
CLONE_STAGES = [
|
||||
(0.0, "Verbindung zum Server wird hergestellt..."),
|
||||
(0.15, "Repository-Informationen werden abgerufen..."),
|
||||
(0.30, "Objekte werden gezählt..."),
|
||||
(0.50, "Objekte werden heruntergeladen..."),
|
||||
(0.80, "Dateien werden entpackt..."),
|
||||
(0.95, "Arbeitsverzeichnis wird erstellt..."),
|
||||
(1.0, "Repository erfolgreich geklont!")
|
||||
]
|
||||
|
||||
PUSH_STAGES = [
|
||||
(0.0, "Verbindung wird hergestellt..."),
|
||||
(0.10, "Authentifizierung läuft..."),
|
||||
(0.25, "Lokale Änderungen werden analysiert..."),
|
||||
(0.40, "Daten werden komprimiert..."),
|
||||
(0.60, "Objekte werden übertragen..."),
|
||||
(0.85, "Remote-Repository wird aktualisiert..."),
|
||||
(1.0, "Push erfolgreich abgeschlossen!")
|
||||
]
|
||||
|
||||
PULL_STAGES = [
|
||||
(0.0, "Verbindung zum Server wird hergestellt..."),
|
||||
(0.15, "Neue Änderungen werden gesucht..."),
|
||||
(0.35, "Änderungen werden heruntergeladen..."),
|
||||
(0.60, "Dateien werden entpackt..."),
|
||||
(0.80, "Änderungen werden zusammengeführt..."),
|
||||
(0.95, "Arbeitsverzeichnis wird aktualisiert..."),
|
||||
(1.0, "Pull erfolgreich abgeschlossen!")
|
||||
]
|
||||
|
||||
FETCH_STAGES = [
|
||||
(0.0, "Verbindung wird hergestellt..."),
|
||||
(0.20, "Remote-Referenzen werden abgerufen..."),
|
||||
(0.40, "Neue Objekte werden gesucht..."),
|
||||
(0.70, "Objekte werden heruntergeladen..."),
|
||||
(0.90, "Lokale Referenzen werden aktualisiert..."),
|
||||
(1.0, "Fetch erfolgreich abgeschlossen!")
|
||||
]
|
||||
|
||||
COMMIT_STAGES = [
|
||||
(0.0, "Änderungen werden vorbereitet..."),
|
||||
(0.20, "Dateistatus wird geprüft..."),
|
||||
(0.40, "Änderungen werden indiziert..."),
|
||||
(0.60, "Commit-Objekt wird erstellt..."),
|
||||
(0.80, "Referenzen werden aktualisiert..."),
|
||||
(1.0, "Commit erfolgreich erstellt!")
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_stages(operation: str) -> list:
|
||||
"""Get progress stages for an operation"""
|
||||
stages_map = {
|
||||
'clone': GitOperationProgress.CLONE_STAGES,
|
||||
'push': GitOperationProgress.PUSH_STAGES,
|
||||
'pull': GitOperationProgress.PULL_STAGES,
|
||||
'fetch': GitOperationProgress.FETCH_STAGES,
|
||||
'commit': GitOperationProgress.COMMIT_STAGES
|
||||
}
|
||||
return stages_map.get(operation.lower(), [])
|
||||
1059
gui/project_tile.py
Normale Datei
1059
gui/project_tile.py
Normale Datei
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
696
gui/settings_dialog.py
Normale Datei
696
gui/settings_dialog.py
Normale Datei
@ -0,0 +1,696 @@
|
||||
"""
|
||||
Settings Dialog for the application
|
||||
Allows users to configure various settings including view modes
|
||||
"""
|
||||
|
||||
import customtkinter as ctk
|
||||
from gui.styles import COLORS, FONTS
|
||||
from utils.logger import logger
|
||||
import json
|
||||
from pathlib import Path
|
||||
from tkinter import filedialog
|
||||
import os
|
||||
|
||||
|
||||
class SettingsDialog(ctk.CTkToplevel):
|
||||
def __init__(self, parent, sidebar_view):
|
||||
super().__init__(parent)
|
||||
|
||||
self.sidebar_view = sidebar_view
|
||||
self.settings_file = Path.home() / ".claude_project_manager" / "ui_settings.json"
|
||||
|
||||
# Window setup
|
||||
self.title("Einstellungen")
|
||||
self.minsize(650, 550)
|
||||
self.resizable(True, True)
|
||||
|
||||
# Make modal
|
||||
self.transient(parent)
|
||||
self.grab_set()
|
||||
|
||||
# Load current settings
|
||||
self.settings = self.load_settings()
|
||||
|
||||
# Setup UI
|
||||
self.setup_ui()
|
||||
|
||||
# Update window size to fit content
|
||||
self.update_idletasks()
|
||||
|
||||
# Center window
|
||||
self.center_window()
|
||||
|
||||
# Focus
|
||||
self.focus()
|
||||
|
||||
def setup_ui(self):
|
||||
"""Setup the settings UI"""
|
||||
# Main container
|
||||
main_frame = ctk.CTkFrame(self, fg_color=COLORS['bg_primary'])
|
||||
main_frame.pack(fill="both", expand=True, padx=20, pady=20)
|
||||
|
||||
# Title
|
||||
title_label = ctk.CTkLabel(
|
||||
main_frame,
|
||||
text="⚙️ Einstellungen",
|
||||
font=FONTS['subtitle'],
|
||||
text_color=COLORS['text_primary']
|
||||
)
|
||||
title_label.pack(pady=(0, 20))
|
||||
|
||||
# Tab view
|
||||
self.tabview = ctk.CTkTabview(
|
||||
main_frame,
|
||||
fg_color=COLORS['bg_secondary'],
|
||||
segmented_button_fg_color=COLORS['bg_tile'],
|
||||
segmented_button_selected_color=COLORS['accent_primary'],
|
||||
segmented_button_selected_hover_color=COLORS['accent_hover'],
|
||||
segmented_button_unselected_color=COLORS['bg_tile'],
|
||||
segmented_button_unselected_hover_color=COLORS['bg_tile_hover'],
|
||||
text_color=COLORS['text_primary'],
|
||||
text_color_disabled=COLORS['text_dim']
|
||||
)
|
||||
self.tabview.pack(fill="both", expand=True)
|
||||
|
||||
# Add tabs
|
||||
self.tabview.add("Allgemein")
|
||||
self.tabview.add("Gitea")
|
||||
self.tabview.add("Team-Aktivität")
|
||||
|
||||
# Setup each tab
|
||||
self.setup_general_tab()
|
||||
self.setup_gitea_tab()
|
||||
self.setup_activity_tab()
|
||||
|
||||
# Buttons (at bottom of main frame)
|
||||
button_frame = ctk.CTkFrame(main_frame, fg_color="transparent")
|
||||
button_frame.pack(fill="x", pady=(20, 0))
|
||||
|
||||
# Cancel button (left side)
|
||||
cancel_btn = ctk.CTkButton(
|
||||
button_frame,
|
||||
text="Abbrechen",
|
||||
command=self.destroy,
|
||||
fg_color=COLORS['bg_tile'],
|
||||
hover_color=COLORS['bg_tile_hover'],
|
||||
text_color=COLORS['text_primary'],
|
||||
width=100
|
||||
)
|
||||
cancel_btn.pack(side="left", padx=(0, 5))
|
||||
|
||||
# Apply button (right side)
|
||||
apply_btn = ctk.CTkButton(
|
||||
button_frame,
|
||||
text="Anwenden",
|
||||
command=self.apply_settings,
|
||||
fg_color=COLORS['accent_primary'],
|
||||
hover_color=COLORS['accent_hover'],
|
||||
width=100
|
||||
)
|
||||
apply_btn.pack(side="right", padx=(5, 0))
|
||||
|
||||
# Save button (right side, before Apply)
|
||||
save_btn = ctk.CTkButton(
|
||||
button_frame,
|
||||
text="Speichern",
|
||||
command=self.save_settings_only,
|
||||
fg_color=COLORS['accent_secondary'],
|
||||
hover_color=COLORS['accent_hover'],
|
||||
text_color=COLORS['text_primary'],
|
||||
width=100
|
||||
)
|
||||
save_btn.pack(side="right", padx=(5, 0))
|
||||
|
||||
def setup_general_tab(self):
|
||||
"""Setup general settings tab"""
|
||||
tab = self.tabview.tab("Allgemein")
|
||||
|
||||
# Container with padding
|
||||
container = ctk.CTkFrame(tab, fg_color="transparent")
|
||||
container.pack(fill="both", expand=True, padx=20, pady=20)
|
||||
|
||||
# Placeholder for future general settings
|
||||
info_label = ctk.CTkLabel(
|
||||
container,
|
||||
text="Allgemeine Einstellungen werden hier angezeigt.\n(Aktuell keine verfügbar)",
|
||||
font=FONTS['body'],
|
||||
text_color=COLORS['text_dim']
|
||||
)
|
||||
info_label.pack(pady=50)
|
||||
|
||||
def setup_gitea_tab(self):
|
||||
"""Setup Gitea settings tab"""
|
||||
tab = self.tabview.tab("Gitea")
|
||||
|
||||
# Scrollable container
|
||||
container = ctk.CTkScrollableFrame(tab, fg_color="transparent")
|
||||
container.pack(fill="both", expand=True, padx=20, pady=20)
|
||||
|
||||
# Clone Directory Section
|
||||
clone_section = ctk.CTkFrame(container, fg_color=COLORS['bg_tile'])
|
||||
clone_section.pack(fill="x", pady=(0, 15))
|
||||
|
||||
clone_header = ctk.CTkLabel(
|
||||
clone_section,
|
||||
text="📁 Clone-Verzeichnis",
|
||||
font=FONTS['body'],
|
||||
text_color=COLORS['text_primary']
|
||||
)
|
||||
clone_header.pack(anchor="w", padx=15, pady=(10, 5))
|
||||
|
||||
# Clone directory path
|
||||
clone_path_frame = ctk.CTkFrame(clone_section, fg_color="transparent")
|
||||
clone_path_frame.pack(fill="x", padx=30, pady=(5, 10))
|
||||
|
||||
default_clone_dir = str(Path.home() / "GiteaRepos")
|
||||
self.clone_dir_var = ctk.StringVar(value=self.settings.get("gitea_clone_directory", default_clone_dir))
|
||||
|
||||
self.clone_dir_entry = ctk.CTkEntry(
|
||||
clone_path_frame,
|
||||
textvariable=self.clone_dir_var,
|
||||
width=350,
|
||||
fg_color=COLORS['bg_primary']
|
||||
)
|
||||
self.clone_dir_entry.pack(side="left", padx=(0, 10))
|
||||
|
||||
browse_btn = ctk.CTkButton(
|
||||
clone_path_frame,
|
||||
text="Durchsuchen...",
|
||||
command=self.browse_clone_directory,
|
||||
fg_color=COLORS['bg_secondary'],
|
||||
hover_color=COLORS['bg_tile_hover'],
|
||||
text_color=COLORS['text_primary'],
|
||||
width=100
|
||||
)
|
||||
browse_btn.pack(side="left")
|
||||
|
||||
# Info label
|
||||
info_label = ctk.CTkLabel(
|
||||
clone_section,
|
||||
text="Standardverzeichnis für geklonte Repositories",
|
||||
font=FONTS['small'],
|
||||
text_color=COLORS['text_dim']
|
||||
)
|
||||
info_label.pack(anchor="w", padx=30, pady=(0, 10))
|
||||
|
||||
# Server Configuration Section
|
||||
server_section = ctk.CTkFrame(container, fg_color=COLORS['bg_tile'])
|
||||
server_section.pack(fill="x", pady=(0, 15))
|
||||
|
||||
server_header = ctk.CTkLabel(
|
||||
server_section,
|
||||
text="🌐 Gitea Server",
|
||||
font=FONTS['body'],
|
||||
text_color=COLORS['text_primary']
|
||||
)
|
||||
server_header.pack(anchor="w", padx=15, pady=(10, 5))
|
||||
|
||||
# Server URL
|
||||
url_label = ctk.CTkLabel(
|
||||
server_section,
|
||||
text="Server URL:",
|
||||
font=FONTS['small'],
|
||||
text_color=COLORS['text_secondary']
|
||||
)
|
||||
url_label.pack(anchor="w", padx=30, pady=(5, 0))
|
||||
|
||||
self.gitea_url_var = ctk.StringVar(value=self.settings.get("gitea_server_url", "https://gitea-undso.intelsight.de"))
|
||||
self.gitea_url_entry = ctk.CTkEntry(
|
||||
server_section,
|
||||
textvariable=self.gitea_url_var,
|
||||
width=350,
|
||||
fg_color=COLORS['bg_primary']
|
||||
)
|
||||
self.gitea_url_entry.pack(padx=30, pady=(0, 5))
|
||||
|
||||
# API Token
|
||||
token_label = ctk.CTkLabel(
|
||||
server_section,
|
||||
text="API Token:",
|
||||
font=FONTS['small'],
|
||||
text_color=COLORS['text_secondary']
|
||||
)
|
||||
token_label.pack(anchor="w", padx=30, pady=(5, 0))
|
||||
|
||||
self.gitea_token_var = ctk.StringVar(value=self.settings.get("gitea_api_token", ""))
|
||||
self.gitea_token_entry = ctk.CTkEntry(
|
||||
server_section,
|
||||
textvariable=self.gitea_token_var,
|
||||
width=350,
|
||||
fg_color=COLORS['bg_primary'],
|
||||
show="*"
|
||||
)
|
||||
self.gitea_token_entry.pack(padx=30, pady=(0, 5))
|
||||
|
||||
# Username
|
||||
user_label = ctk.CTkLabel(
|
||||
server_section,
|
||||
text="Benutzername:",
|
||||
font=FONTS['small'],
|
||||
text_color=COLORS['text_secondary']
|
||||
)
|
||||
user_label.pack(anchor="w", padx=30, pady=(5, 0))
|
||||
|
||||
self.gitea_user_var = ctk.StringVar(value=self.settings.get("gitea_username", ""))
|
||||
self.gitea_user_entry = ctk.CTkEntry(
|
||||
server_section,
|
||||
textvariable=self.gitea_user_var,
|
||||
width=350,
|
||||
fg_color=COLORS['bg_primary']
|
||||
)
|
||||
self.gitea_user_entry.pack(padx=30, pady=(0, 10))
|
||||
|
||||
# Test connection button
|
||||
test_btn = ctk.CTkButton(
|
||||
server_section,
|
||||
text="Verbindung testen",
|
||||
command=self.test_gitea_connection,
|
||||
fg_color=COLORS['bg_secondary'],
|
||||
hover_color=COLORS['bg_tile_hover'],
|
||||
text_color=COLORS['text_primary'],
|
||||
width=150
|
||||
)
|
||||
test_btn.pack(pady=(5, 10))
|
||||
|
||||
# Status label
|
||||
self.gitea_status_label = ctk.CTkLabel(
|
||||
server_section,
|
||||
text="",
|
||||
font=FONTS['small'],
|
||||
text_color=COLORS['text_secondary'],
|
||||
height=20
|
||||
)
|
||||
self.gitea_status_label.pack(pady=(0, 10))
|
||||
|
||||
def setup_activity_tab(self):
|
||||
"""Setup activity server settings tab"""
|
||||
tab = self.tabview.tab("Team-Aktivität")
|
||||
|
||||
# Container with padding
|
||||
container = ctk.CTkFrame(tab, fg_color="transparent")
|
||||
container.pack(fill="both", expand=True, padx=20, pady=20)
|
||||
|
||||
# Activity Server Section
|
||||
activity_section = ctk.CTkFrame(container, fg_color=COLORS['bg_tile'])
|
||||
activity_section.pack(fill="x", pady=(0, 15))
|
||||
|
||||
activity_header = ctk.CTkLabel(
|
||||
activity_section,
|
||||
text="👥 Team-Aktivität Server",
|
||||
font=FONTS['body'],
|
||||
text_color=COLORS['text_primary']
|
||||
)
|
||||
activity_header.pack(anchor="w", padx=15, pady=(10, 5))
|
||||
|
||||
# Server URL
|
||||
url_label = ctk.CTkLabel(
|
||||
activity_section,
|
||||
text="Server URL:",
|
||||
font=FONTS['small'],
|
||||
text_color=COLORS['text_secondary']
|
||||
)
|
||||
url_label.pack(anchor="w", padx=30, pady=(5, 0))
|
||||
|
||||
self.server_url_var = ctk.StringVar(value=self.settings.get("activity_server_url", "http://91.99.192.14:3001"))
|
||||
self.server_url_entry = ctk.CTkEntry(
|
||||
activity_section,
|
||||
textvariable=self.server_url_var,
|
||||
width=350,
|
||||
fg_color=COLORS['bg_primary']
|
||||
)
|
||||
self.server_url_entry.pack(padx=30, pady=(0, 5))
|
||||
|
||||
# API Key
|
||||
api_label = ctk.CTkLabel(
|
||||
activity_section,
|
||||
text="API Key:",
|
||||
font=FONTS['small'],
|
||||
text_color=COLORS['text_secondary']
|
||||
)
|
||||
api_label.pack(anchor="w", padx=30, pady=(5, 0))
|
||||
|
||||
self.api_key_var = ctk.StringVar(value=self.settings.get("activity_api_key", ""))
|
||||
self.api_key_entry = ctk.CTkEntry(
|
||||
activity_section,
|
||||
textvariable=self.api_key_var,
|
||||
width=350,
|
||||
fg_color=COLORS['bg_primary'],
|
||||
show="*"
|
||||
)
|
||||
self.api_key_entry.pack(padx=30, pady=(0, 5))
|
||||
|
||||
# User Name
|
||||
user_label = ctk.CTkLabel(
|
||||
activity_section,
|
||||
text="Benutzername:",
|
||||
font=FONTS['small'],
|
||||
text_color=COLORS['text_secondary']
|
||||
)
|
||||
user_label.pack(anchor="w", padx=30, pady=(5, 0))
|
||||
|
||||
self.user_name_var = ctk.StringVar(value=self.settings.get("activity_user_name", ""))
|
||||
self.user_name_entry = ctk.CTkEntry(
|
||||
activity_section,
|
||||
textvariable=self.user_name_var,
|
||||
width=350,
|
||||
fg_color=COLORS['bg_primary']
|
||||
)
|
||||
self.user_name_entry.pack(padx=30, pady=(0, 5))
|
||||
|
||||
# Test connection button
|
||||
test_btn = ctk.CTkButton(
|
||||
activity_section,
|
||||
text="Verbindung testen",
|
||||
command=self.test_activity_connection,
|
||||
fg_color=COLORS['bg_secondary'],
|
||||
hover_color=COLORS['bg_tile_hover'],
|
||||
text_color=COLORS['text_primary'],
|
||||
width=150
|
||||
)
|
||||
test_btn.pack(pady=(5, 10))
|
||||
|
||||
# Status label
|
||||
self.connection_status_label = ctk.CTkLabel(
|
||||
activity_section,
|
||||
text="",
|
||||
font=FONTS['small'],
|
||||
text_color=COLORS['text_secondary'],
|
||||
height=20
|
||||
)
|
||||
self.connection_status_label.pack(pady=(0, 10))
|
||||
|
||||
def load_settings(self):
|
||||
"""Load settings from file"""
|
||||
try:
|
||||
if self.settings_file.exists():
|
||||
with open(self.settings_file, 'r') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load UI settings: {e}")
|
||||
|
||||
return {} # No settings currently
|
||||
|
||||
def save_settings(self):
|
||||
"""Save settings to file"""
|
||||
try:
|
||||
self.settings_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(self.settings_file, 'w') as f:
|
||||
json.dump(self.settings, f, indent=2)
|
||||
logger.info(f"Saved UI settings: {self.settings}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save UI settings: {e}")
|
||||
|
||||
def browse_clone_directory(self):
|
||||
"""Browse for clone directory"""
|
||||
current_dir = self.clone_dir_var.get()
|
||||
if not os.path.exists(current_dir):
|
||||
current_dir = str(Path.home())
|
||||
|
||||
directory = filedialog.askdirectory(
|
||||
title="Clone-Verzeichnis auswählen",
|
||||
initialdir=current_dir,
|
||||
parent=self
|
||||
)
|
||||
|
||||
if directory:
|
||||
self.clone_dir_var.set(directory)
|
||||
logger.info(f"Selected clone directory: {directory}")
|
||||
|
||||
def test_gitea_connection(self):
|
||||
"""Test connection to Gitea server"""
|
||||
import requests
|
||||
|
||||
server_url = self.gitea_url_var.get().strip()
|
||||
api_token = self.gitea_token_var.get().strip()
|
||||
|
||||
if not server_url:
|
||||
self.gitea_status_label.configure(
|
||||
text="⚠️ Bitte Server URL eingeben",
|
||||
text_color=COLORS['accent_warning']
|
||||
)
|
||||
return
|
||||
|
||||
self.gitea_status_label.configure(
|
||||
text="🔄 Teste Verbindung...",
|
||||
text_color=COLORS['text_secondary']
|
||||
)
|
||||
self.update()
|
||||
|
||||
try:
|
||||
# Try to connect to the Gitea API
|
||||
headers = {}
|
||||
if api_token:
|
||||
headers["Authorization"] = f"token {api_token}"
|
||||
|
||||
response = requests.get(
|
||||
f"{server_url}/api/v1/version",
|
||||
timeout=5,
|
||||
headers=headers
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
version = response.json().get('version', 'Unknown')
|
||||
self.gitea_status_label.configure(
|
||||
text=f"✅ Verbindung erfolgreich! (Gitea {version})",
|
||||
text_color=COLORS['accent_success']
|
||||
)
|
||||
logger.info(f"Gitea connection successful: {server_url}, version: {version}")
|
||||
else:
|
||||
self.gitea_status_label.configure(
|
||||
text=f"❌ Server antwortet mit Status {response.status_code}",
|
||||
text_color=COLORS['accent_error']
|
||||
)
|
||||
logger.warning(f"Gitea server returned status {response.status_code}")
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
self.gitea_status_label.configure(
|
||||
text="❌ Server nicht erreichbar",
|
||||
text_color=COLORS['accent_error']
|
||||
)
|
||||
logger.error(f"Gitea server not reachable: {server_url}")
|
||||
except requests.exceptions.Timeout:
|
||||
self.gitea_status_label.configure(
|
||||
text="❌ Verbindung Timeout",
|
||||
text_color=COLORS['accent_error']
|
||||
)
|
||||
logger.error(f"Gitea server connection timeout: {server_url}")
|
||||
except Exception as e:
|
||||
self.gitea_status_label.configure(
|
||||
text=f"❌ Fehler: {str(e)}",
|
||||
text_color=COLORS['accent_error']
|
||||
)
|
||||
logger.error(f"Gitea server connection error: {e}")
|
||||
|
||||
def save_settings_only(self):
|
||||
"""Save settings without applying to service or closing dialog"""
|
||||
# Get activity values
|
||||
server_url = self.server_url_var.get().strip()
|
||||
api_key = self.api_key_var.get().strip()
|
||||
user_name = self.user_name_var.get().strip()
|
||||
|
||||
# Get Gitea values
|
||||
gitea_url = self.gitea_url_var.get().strip()
|
||||
gitea_token = self.gitea_token_var.get().strip()
|
||||
gitea_user = self.gitea_user_var.get().strip()
|
||||
clone_dir = self.clone_dir_var.get().strip()
|
||||
|
||||
# Update settings
|
||||
self.settings["activity_server_url"] = server_url
|
||||
self.settings["activity_api_key"] = api_key
|
||||
self.settings["activity_user_name"] = user_name
|
||||
|
||||
self.settings["gitea_server_url"] = gitea_url
|
||||
self.settings["gitea_api_token"] = gitea_token
|
||||
self.settings["gitea_username"] = gitea_user
|
||||
self.settings["gitea_clone_directory"] = clone_dir
|
||||
|
||||
self.save_settings()
|
||||
|
||||
# Show confirmation on the active tab
|
||||
current_tab = self.tabview.get()
|
||||
if current_tab == "Team-Aktivität":
|
||||
self.connection_status_label.configure(
|
||||
text="✅ Einstellungen gespeichert!",
|
||||
text_color=COLORS['accent_success']
|
||||
)
|
||||
self.after(2000, lambda: self.connection_status_label.configure(text=""))
|
||||
elif current_tab == "Gitea":
|
||||
self.gitea_status_label.configure(
|
||||
text="✅ Einstellungen gespeichert!",
|
||||
text_color=COLORS['accent_success']
|
||||
)
|
||||
self.after(2000, lambda: self.gitea_status_label.configure(text=""))
|
||||
|
||||
logger.info("Settings saved successfully")
|
||||
|
||||
def apply_settings(self):
|
||||
"""Apply the selected settings"""
|
||||
import uuid
|
||||
from services.activity_sync import activity_service
|
||||
|
||||
# Save all settings first
|
||||
self.save_settings_only()
|
||||
|
||||
# Apply activity service settings
|
||||
activity_service.server_url = self.settings.get("activity_server_url", "")
|
||||
activity_service.api_key = self.settings.get("activity_api_key", "")
|
||||
activity_service.user_name = self.settings.get("activity_user_name", "")
|
||||
activity_service.user_id = self.settings.get("activity_user_id", str(uuid.uuid4()))
|
||||
|
||||
# Save user ID if newly generated
|
||||
if "activity_user_id" not in self.settings:
|
||||
self.settings["activity_user_id"] = activity_service.user_id
|
||||
self.save_settings()
|
||||
|
||||
# Save activity settings
|
||||
activity_service.save_settings()
|
||||
|
||||
# Reconnect if settings changed
|
||||
if activity_service.connected:
|
||||
activity_service.disconnect()
|
||||
|
||||
if activity_service.is_configured():
|
||||
activity_service.connect()
|
||||
|
||||
logger.info("Activity server settings applied")
|
||||
|
||||
# Apply Gitea settings to config
|
||||
self.update_gitea_config()
|
||||
|
||||
# Close dialog
|
||||
self.destroy()
|
||||
|
||||
def update_gitea_config(self):
|
||||
"""Update Gitea configuration in the application"""
|
||||
try:
|
||||
# Import here to avoid circular imports
|
||||
from src.gitea.gitea_client import gitea_config
|
||||
from src.gitea.git_operations import git_ops
|
||||
|
||||
# Update Gitea config
|
||||
if hasattr(gitea_config, 'base_url'):
|
||||
gitea_config.base_url = self.settings.get("gitea_server_url", gitea_config.base_url)
|
||||
if hasattr(gitea_config, 'api_token'):
|
||||
gitea_config.api_token = self.settings.get("gitea_api_token", gitea_config.api_token)
|
||||
if hasattr(gitea_config, 'username'):
|
||||
gitea_config.username = self.settings.get("gitea_username", gitea_config.username)
|
||||
|
||||
# Update clone directory
|
||||
clone_dir = self.settings.get("gitea_clone_directory")
|
||||
if clone_dir:
|
||||
# Re-initialize git_ops to use new settings
|
||||
from src.gitea.git_operations import init_git_ops
|
||||
init_git_ops()
|
||||
|
||||
# Now update the clone directory
|
||||
if hasattr(git_ops, 'default_clone_dir'):
|
||||
git_ops.default_clone_dir = Path(clone_dir)
|
||||
logger.info(f"Updated git_ops clone directory to: {clone_dir}")
|
||||
|
||||
# Update all existing RepositoryManager instances
|
||||
# This ensures that any git_ops instances in the application get the new directory
|
||||
try:
|
||||
# Update in main window if it exists
|
||||
parent = self.master
|
||||
if hasattr(parent, 'gitea_handler') and hasattr(parent.gitea_handler, 'repo_manager'):
|
||||
if hasattr(parent.gitea_handler.repo_manager, 'git_ops'):
|
||||
parent.gitea_handler.repo_manager.git_ops.default_clone_dir = Path(clone_dir)
|
||||
logger.info("Updated main window's git_ops clone directory")
|
||||
|
||||
# Update in sidebar gitea explorer if it exists
|
||||
if hasattr(parent, 'sidebar') and hasattr(parent.sidebar, 'gitea_explorer'):
|
||||
if hasattr(parent.sidebar.gitea_explorer, 'repo_manager'):
|
||||
if hasattr(parent.sidebar.gitea_explorer.repo_manager, 'git_ops'):
|
||||
parent.sidebar.gitea_explorer.repo_manager.git_ops.default_clone_dir = Path(clone_dir)
|
||||
logger.info("Updated sidebar's git_ops clone directory")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not update all git_ops instances: {e}")
|
||||
|
||||
logger.info("Gitea configuration updated")
|
||||
|
||||
except ImportError as e:
|
||||
logger.warning(f"Could not update Gitea config: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating Gitea config: {e}")
|
||||
|
||||
def test_activity_connection(self):
|
||||
"""Test connection to activity server"""
|
||||
import requests
|
||||
from tkinter import messagebox
|
||||
|
||||
server_url = self.server_url_var.get().strip()
|
||||
api_key = self.api_key_var.get().strip()
|
||||
|
||||
if not server_url:
|
||||
self.connection_status_label.configure(
|
||||
text="⚠️ Bitte Server URL eingeben",
|
||||
text_color=COLORS['accent_warning']
|
||||
)
|
||||
return
|
||||
|
||||
self.connection_status_label.configure(
|
||||
text="🔄 Teste Verbindung...",
|
||||
text_color=COLORS['text_secondary']
|
||||
)
|
||||
self.update()
|
||||
|
||||
try:
|
||||
# Try to connect to the server
|
||||
response = requests.get(
|
||||
f"{server_url}/health",
|
||||
timeout=5,
|
||||
headers={"Authorization": f"Bearer {api_key}"} if api_key else {}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
self.connection_status_label.configure(
|
||||
text="✅ Verbindung erfolgreich!",
|
||||
text_color=COLORS['accent_success']
|
||||
)
|
||||
logger.info(f"Activity server connection successful: {server_url}")
|
||||
else:
|
||||
self.connection_status_label.configure(
|
||||
text=f"❌ Server antwortet mit Status {response.status_code}",
|
||||
text_color=COLORS['accent_error']
|
||||
)
|
||||
logger.warning(f"Activity server returned status {response.status_code}")
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
self.connection_status_label.configure(
|
||||
text="❌ Server nicht erreichbar",
|
||||
text_color=COLORS['accent_error']
|
||||
)
|
||||
logger.error(f"Activity server not reachable: {server_url}")
|
||||
except requests.exceptions.Timeout:
|
||||
self.connection_status_label.configure(
|
||||
text="❌ Verbindung Timeout",
|
||||
text_color=COLORS['accent_error']
|
||||
)
|
||||
logger.error(f"Activity server connection timeout: {server_url}")
|
||||
except Exception as e:
|
||||
self.connection_status_label.configure(
|
||||
text=f"❌ Fehler: {str(e)}",
|
||||
text_color=COLORS['accent_error']
|
||||
)
|
||||
logger.error(f"Activity server connection error: {e}")
|
||||
|
||||
def center_window(self):
|
||||
"""Center the dialog on parent window"""
|
||||
self.update_idletasks()
|
||||
|
||||
# Get parent window position
|
||||
parent = self.master
|
||||
parent_x = parent.winfo_x()
|
||||
parent_y = parent.winfo_y()
|
||||
parent_width = parent.winfo_width()
|
||||
parent_height = parent.winfo_height()
|
||||
|
||||
# Get dialog size
|
||||
dialog_width = self.winfo_width()
|
||||
dialog_height = self.winfo_height()
|
||||
|
||||
# Calculate position
|
||||
x = parent_x + (parent_width - dialog_width) // 2
|
||||
y = parent_y + (parent_height - dialog_height) // 2
|
||||
|
||||
self.geometry(f"+{x}+{y}")
|
||||
42
gui/sidebar_view.py
Normale Datei
42
gui/sidebar_view.py
Normale Datei
@ -0,0 +1,42 @@
|
||||
"""
|
||||
Sidebar View Manager
|
||||
Simplified version with only Gitea explorer
|
||||
"""
|
||||
|
||||
import customtkinter as ctk
|
||||
from typing import Optional
|
||||
from gui.styles import COLORS, FONTS
|
||||
from gui.gitea_explorer import GiteaExplorer
|
||||
from utils.logger import logger
|
||||
|
||||
|
||||
class SidebarView(ctk.CTkFrame):
|
||||
def __init__(self, parent, on_repo_select=None, **kwargs):
|
||||
super().__init__(parent, fg_color=COLORS['bg_secondary'], **kwargs)
|
||||
|
||||
self.on_repo_select = on_repo_select
|
||||
self.main_window = None # Will be set by main window
|
||||
|
||||
# Create Gitea explorer
|
||||
self.gitea_explorer = GiteaExplorer(
|
||||
self,
|
||||
on_repo_select=self.on_repo_select
|
||||
)
|
||||
self.gitea_explorer.pack(fill="both", expand=True)
|
||||
|
||||
logger.info("Sidebar view initialized with Gitea explorer")
|
||||
|
||||
def get_gitea_explorer(self) -> Optional[GiteaExplorer]:
|
||||
"""Get the Gitea explorer instance"""
|
||||
return self.gitea_explorer
|
||||
|
||||
def refresh_all(self):
|
||||
"""Refresh all explorers"""
|
||||
if self.gitea_explorer:
|
||||
self.gitea_explorer.refresh_repositories()
|
||||
|
||||
def set_main_window(self, main_window):
|
||||
"""Set main window reference"""
|
||||
self.main_window = main_window
|
||||
if self.gitea_explorer:
|
||||
self.gitea_explorer.main_window = main_window
|
||||
124
gui/styles.py
Normale Datei
124
gui/styles.py
Normale Datei
@ -0,0 +1,124 @@
|
||||
"""
|
||||
Style Configuration for GUI
|
||||
Modern design with dark/light theme support
|
||||
"""
|
||||
|
||||
# Dark Mode color definitions (only mode)
|
||||
COLORS = {
|
||||
# Background colors
|
||||
'bg_primary': '#000000', # Schwarz
|
||||
'bg_secondary': '#1A1F3A', # Dunkleres Blau
|
||||
'bg_tile': '#232D53', # Dunkelblau
|
||||
'bg_tile_hover': '#2A3560', # Helleres Dunkelblau
|
||||
'bg_vps': '#1A2347', # VPS tile etwas dunkler
|
||||
'bg_gitea_tile': '#2A3560', # Gitea tiles - deutlich heller für bessere Lesbarkeit
|
||||
'bg_gitea_hover': '#364170', # Gitea hover - noch heller
|
||||
'bg_selected': '#364170', # Selected - dunkleres Blau ohne Alpha
|
||||
|
||||
# Text colors
|
||||
'text_primary': '#FFFFFF', # Weiß
|
||||
'text_secondary': '#B0B0B0', # Grau
|
||||
'text_dim': '#707070', # Dunkles Grau
|
||||
|
||||
# Accent colors
|
||||
'accent_primary': '#00D4FF', # Cyan/Hellblau
|
||||
'accent_hover': '#00E5FF', # Helleres Cyan
|
||||
'accent_secondary': '#6B7AA1', # Sekundäres Blau-Grau
|
||||
'accent_selected': '#00D4FF', # Auswahl-Farbe
|
||||
'accent_success': '#4CAF50', # Success green
|
||||
'accent_warning': '#FF9800', # Warning orange
|
||||
'accent_error': '#F44336', # Error red
|
||||
'accent_vps': '#00D4FF', # VPS auch Cyan
|
||||
|
||||
# Border colors
|
||||
'border_primary': '#232D53', # Dunkelblau
|
||||
'border_secondary': '#2A3560', # Helleres Dunkelblau
|
||||
}
|
||||
|
||||
# Get current colors (always dark mode)
|
||||
def get_colors():
|
||||
"""Get colors (always returns dark mode colors)"""
|
||||
return COLORS
|
||||
|
||||
# Font Configuration
|
||||
FONTS = {
|
||||
'heading': ('Segoe UI', 28, 'bold'),
|
||||
'subheading': ('Segoe UI', 22, 'bold'),
|
||||
'title': ('Segoe UI', 20, 'bold'), # Added for gitea UI
|
||||
'subtitle': ('Segoe UI', 16, 'bold'), # Added for gitea UI
|
||||
'tile_title': ('Segoe UI', 18, 'bold'),
|
||||
'tile_text': ('Segoe UI', 14),
|
||||
'body': ('Segoe UI', 14), # Added for gitea UI
|
||||
'button': ('Segoe UI', 13, 'bold'),
|
||||
'small': ('Segoe UI', 12),
|
||||
'code': ('Consolas', 12), # Added for gitea UI code display
|
||||
}
|
||||
|
||||
# Tile Dimensions
|
||||
TILE_SIZE = {
|
||||
'width': 280,
|
||||
'height': 180,
|
||||
'padding': 15,
|
||||
'margin': 10,
|
||||
}
|
||||
|
||||
# Button Styles
|
||||
BUTTON_STYLES = {
|
||||
'primary': {
|
||||
'fg_color': COLORS['accent_primary'],
|
||||
'hover_color': COLORS['accent_hover'],
|
||||
'text_color': '#FFFFFF',
|
||||
'corner_radius': 6,
|
||||
'height': 32,
|
||||
},
|
||||
'secondary': {
|
||||
'fg_color': COLORS['bg_secondary'],
|
||||
'hover_color': COLORS['bg_tile'],
|
||||
'text_color': COLORS['text_primary'],
|
||||
'corner_radius': 6,
|
||||
'height': 32,
|
||||
'border_width': 1,
|
||||
'border_color': COLORS['border_primary'],
|
||||
},
|
||||
'danger': {
|
||||
'fg_color': COLORS['accent_error'],
|
||||
'hover_color': '#FF5252',
|
||||
'text_color': '#FFFFFF',
|
||||
'corner_radius': 6,
|
||||
'height': 32,
|
||||
},
|
||||
'vps': {
|
||||
'fg_color': COLORS['accent_vps'],
|
||||
'hover_color': COLORS['accent_hover'],
|
||||
'text_color': '#FFFFFF',
|
||||
'corner_radius': 6,
|
||||
'height': 32,
|
||||
}
|
||||
}
|
||||
|
||||
def get_button_styles():
|
||||
"""Get button styles (for compatibility)"""
|
||||
return BUTTON_STYLES
|
||||
|
||||
|
||||
# Window Configuration
|
||||
WINDOW_CONFIG = {
|
||||
'title': 'Claude Project Manager',
|
||||
'width': 1600, # Increased to fit 4 VPS tiles side by side
|
||||
'height': 1000, # Increased to show Gitea repos without scrolling
|
||||
'min_width': 800,
|
||||
'min_height': 600,
|
||||
}
|
||||
|
||||
# Theme functions (kept for compatibility but always return dark)
|
||||
def set_theme(theme_name):
|
||||
"""Kept for compatibility - does nothing as only dark mode exists"""
|
||||
return True
|
||||
|
||||
def get_theme():
|
||||
"""Always returns 'dark'"""
|
||||
return 'dark'
|
||||
|
||||
def toggle_theme():
|
||||
"""Kept for compatibility - always returns 'dark'"""
|
||||
return 'dark'
|
||||
BIN
intelsight-icon-transparent-dark.png
Normale Datei
BIN
intelsight-icon-transparent-dark.png
Normale Datei
Binäre Datei nicht angezeigt.
|
Nachher Breite: | Höhe: | Größe: 1.9 KiB |
37
logo_header.svg
Normale Datei
37
logo_header.svg
Normale Datei
@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="50" height="50" viewBox="0 0 100 115" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Shield and eye logo for header - resized -->
|
||||
<g transform="translate(0, 0)">
|
||||
<!-- Angular shield shape -->
|
||||
<path d="M 35 30
|
||||
L 65 30
|
||||
L 75 40
|
||||
L 75 80
|
||||
L 50 115
|
||||
L 25 80
|
||||
L 25 40
|
||||
L 35 30 Z"
|
||||
fill="none"
|
||||
stroke="#FFFFFF"
|
||||
stroke-width="3.5"
|
||||
stroke-linejoin="miter"/>
|
||||
|
||||
<!-- Eye centered in shield -->
|
||||
<g transform="translate(50, 65)">
|
||||
<!-- Almond/football shaped eye -->
|
||||
<ellipse cx="0" cy="0" rx="24" ry="13"
|
||||
fill="none"
|
||||
stroke="#FFFFFF"
|
||||
stroke-width="3.5"/>
|
||||
|
||||
<!-- Circular iris -->
|
||||
<circle cx="0" cy="0" r="10"
|
||||
fill="none"
|
||||
stroke="#FFFFFF"
|
||||
stroke-width="3.5"/>
|
||||
|
||||
<!-- Pupil -->
|
||||
<circle cx="0" cy="0" r="4" fill="#FFFFFF"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Nachher Breite: | Höhe: | Größe: 1.0 KiB |
54
main.py
Normale Datei
54
main.py
Normale Datei
@ -0,0 +1,54 @@
|
||||
"""
|
||||
Claude Project Manager
|
||||
Main entry point for the application
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add project root to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from gui.main_window import MainWindow
|
||||
from utils.logger import logger
|
||||
|
||||
def main():
|
||||
"""Main application entry point"""
|
||||
logger.info("Starting Claude Project Manager")
|
||||
try:
|
||||
# Import and set theme first
|
||||
import json
|
||||
from gui.styles import set_theme
|
||||
import customtkinter as ctk
|
||||
|
||||
# Load theme preference before creating UI
|
||||
settings_file = os.path.join(os.path.dirname(__file__), "data/settings.json")
|
||||
theme = 'dark' # Default
|
||||
|
||||
try:
|
||||
if os.path.exists(settings_file):
|
||||
with open(settings_file, 'r') as f:
|
||||
settings = json.load(f)
|
||||
if 'theme' in settings:
|
||||
theme = settings['theme']
|
||||
except:
|
||||
pass
|
||||
|
||||
# Set theme before UI creation
|
||||
ctk.set_appearance_mode(theme)
|
||||
set_theme(theme)
|
||||
|
||||
# Create and run the application
|
||||
app = MainWindow()
|
||||
logger.info("Application window created, starting main loop")
|
||||
app.run()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error starting application: {e}")
|
||||
print(f"Error starting application: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
input("Press Enter to exit...")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
108
manage_refactoring.py
Normale Datei
108
manage_refactoring.py
Normale Datei
@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Refactoring Management Tool
|
||||
Manage feature flags for the MainWindow refactoring
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
from gui.config import refactoring_config
|
||||
|
||||
def show_status():
|
||||
"""Show current refactoring status"""
|
||||
status = refactoring_config.get_status()
|
||||
print("\n=== Refactoring Configuration Status ===")
|
||||
print(f"Config file: {status['config_file']}")
|
||||
print("\nFeature Flags:")
|
||||
for flag, value in status['flags'].items():
|
||||
status_str = "✅ ENABLED" if value else "❌ DISABLED"
|
||||
print(f" {flag}: {status_str}")
|
||||
print()
|
||||
|
||||
def enable_handler(handler_name):
|
||||
"""Enable a specific handler"""
|
||||
refactoring_config.enable_handler(handler_name)
|
||||
refactoring_config.save_config()
|
||||
print(f"✅ Enabled {handler_name} handler")
|
||||
# Reload config to show updated status
|
||||
from gui.config import RefactoringConfig
|
||||
updated_config = RefactoringConfig()
|
||||
status = updated_config.get_status()
|
||||
print("\n=== Refactoring Configuration Status ===")
|
||||
print(f"Config file: {status['config_file']}")
|
||||
print("\nFeature Flags:")
|
||||
for flag, value in status['flags'].items():
|
||||
status_str = "✅ ENABLED" if value else "❌ DISABLED"
|
||||
print(f" {flag}: {status_str}")
|
||||
print()
|
||||
|
||||
def disable_all():
|
||||
"""Disable all refactoring features"""
|
||||
refactoring_config.disable_all()
|
||||
refactoring_config.save_config()
|
||||
print("❌ All refactoring features disabled")
|
||||
show_status()
|
||||
|
||||
def set_flag(flag_name, value):
|
||||
"""Set a specific flag"""
|
||||
refactoring_config.set(flag_name, value)
|
||||
refactoring_config.save_config()
|
||||
status_str = "enabled" if value else "disabled"
|
||||
print(f"✅ Flag {flag_name} {status_str}")
|
||||
show_status()
|
||||
|
||||
def create_test_config():
|
||||
"""Create a test configuration with gradual enablement"""
|
||||
print("\n=== Creating Test Configuration ===")
|
||||
|
||||
# Phase 1: Enable UI helpers only (safest)
|
||||
print("\nPhase 1: UI Helpers only")
|
||||
refactoring_config.disable_all()
|
||||
refactoring_config.set('USE_UI_HELPERS', True)
|
||||
refactoring_config.save_config()
|
||||
|
||||
print("\nTest with: python main.py")
|
||||
print("If stable, proceed to Phase 2")
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Manage MainWindow refactoring')
|
||||
subparsers = parser.add_subparsers(dest='command', help='Commands')
|
||||
|
||||
# Status command
|
||||
subparsers.add_parser('status', help='Show current configuration')
|
||||
|
||||
# Enable command
|
||||
enable_parser = subparsers.add_parser('enable', help='Enable a handler')
|
||||
enable_parser.add_argument('handler', choices=['gitea', 'process', 'project', 'ui'],
|
||||
help='Handler to enable')
|
||||
|
||||
# Disable all command
|
||||
subparsers.add_parser('disable-all', help='Disable all refactoring features')
|
||||
|
||||
# Set flag command
|
||||
set_parser = subparsers.add_parser('set', help='Set a specific flag')
|
||||
set_parser.add_argument('flag', help='Flag name')
|
||||
set_parser.add_argument('value', choices=['true', 'false'], help='Flag value')
|
||||
|
||||
# Test config command
|
||||
subparsers.add_parser('test-config', help='Create test configuration')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == 'status' or not args.command:
|
||||
show_status()
|
||||
elif args.command == 'enable':
|
||||
enable_handler(args.handler)
|
||||
elif args.command == 'disable-all':
|
||||
disable_all()
|
||||
elif args.command == 'set':
|
||||
value = args.value.lower() == 'true'
|
||||
set_flag(args.flag, value)
|
||||
elif args.command == 'test-config':
|
||||
create_test_config()
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
145
process_manager.py
Normale Datei
145
process_manager.py
Normale Datei
@ -0,0 +1,145 @@
|
||||
"""
|
||||
Process Manager Module
|
||||
Tracks and manages running Claude processes
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import os
|
||||
import json
|
||||
from typing import Dict, Optional
|
||||
from datetime import datetime
|
||||
from utils.logger import logger
|
||||
|
||||
class ProcessManager:
|
||||
def __init__(self):
|
||||
self.processes: Dict[str, subprocess.Popen] = {}
|
||||
self.process_data_file = os.path.join(os.path.dirname(__file__), "data/running_processes.json")
|
||||
logger.info("Initializing ProcessManager")
|
||||
self.load_process_data()
|
||||
|
||||
def load_process_data(self):
|
||||
"""Load process data from file"""
|
||||
try:
|
||||
if os.path.exists(self.process_data_file):
|
||||
with open(self.process_data_file, 'r') as f:
|
||||
data = json.load(f)
|
||||
# Clean up old data (processes that are no longer running)
|
||||
self.clean_process_data(data)
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading process data: {e}")
|
||||
print(f"Error loading process data: {e}")
|
||||
|
||||
def clean_process_data(self, data: dict):
|
||||
"""Clean up process data for processes that are no longer running"""
|
||||
# For now, we start fresh each time the app starts
|
||||
# In a real app, we might check if processes are still running
|
||||
self.save_process_data({})
|
||||
|
||||
def save_process_data(self, data: dict = None):
|
||||
"""Save process data to file"""
|
||||
if data is None:
|
||||
data = {
|
||||
project_id: {
|
||||
"pid": proc.pid if proc else None,
|
||||
"start_time": datetime.now().isoformat(),
|
||||
"status": "running" if proc and proc.poll() is None else "stopped"
|
||||
}
|
||||
for project_id, proc in self.processes.items()
|
||||
}
|
||||
|
||||
os.makedirs(os.path.dirname(self.process_data_file), exist_ok=True)
|
||||
try:
|
||||
with open(self.process_data_file, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
except Exception as e:
|
||||
print(f"Error saving process data: {e}")
|
||||
|
||||
def start_process(self, project_id: str, command: list, shell: bool = False) -> subprocess.Popen:
|
||||
"""Start a new process for a project"""
|
||||
logger.info(f"Starting process for project: {project_id}")
|
||||
logger.debug(f"Command: {' '.join(command if isinstance(command, list) else [command])}")
|
||||
|
||||
# Stop existing process if any
|
||||
if project_id in self.processes:
|
||||
logger.warning(f"Stopping existing process for project: {project_id}")
|
||||
self.stop_process(project_id)
|
||||
|
||||
# Start new process
|
||||
process = subprocess.Popen(command, shell=shell)
|
||||
self.processes[project_id] = process
|
||||
logger.info(f"Process started with PID: {process.pid}")
|
||||
self.save_process_data()
|
||||
return process
|
||||
|
||||
def stop_process(self, project_id: str, project_name: str = None) -> bool:
|
||||
"""Stop a process for a project"""
|
||||
logger.info(f"Stopping process for project: {project_id} (name: {project_name})")
|
||||
try:
|
||||
if project_id == "vps-permanent":
|
||||
# Kill VPS windows
|
||||
cmd_kill_cmd = 'taskkill /fi "WINDOWTITLE eq Claude VPS Server" /f >nul 2>&1'
|
||||
logger.debug(f"Executing VPS kill command: {cmd_kill_cmd}")
|
||||
os.system(cmd_kill_cmd)
|
||||
else:
|
||||
# Kill project-specific window
|
||||
if project_name:
|
||||
window_title = f"Claude - {project_name}"
|
||||
# Use powershell for more accurate window title matching
|
||||
ps_cmd = f'powershell -Command "Get-Process | Where-Object {{$_.MainWindowTitle -eq \'{window_title}\'}} | Stop-Process -Force"'
|
||||
logger.debug(f"Executing PowerShell kill command: {ps_cmd}")
|
||||
os.system(ps_cmd)
|
||||
|
||||
# Also try taskkill as fallback
|
||||
cmd_kill_cmd = f'taskkill /fi "WINDOWTITLE eq {window_title}" /f >nul 2>&1'
|
||||
logger.debug(f"Executing taskkill command: {cmd_kill_cmd}")
|
||||
os.system(cmd_kill_cmd)
|
||||
|
||||
# Clean up tracked process if exists
|
||||
if project_id in self.processes:
|
||||
process = self.processes[project_id]
|
||||
if process and process.poll() is None:
|
||||
try:
|
||||
process.terminate()
|
||||
process.wait(timeout=2)
|
||||
except:
|
||||
try:
|
||||
process.kill()
|
||||
except:
|
||||
pass
|
||||
|
||||
del self.processes[project_id]
|
||||
|
||||
self.save_process_data()
|
||||
logger.info(f"Process stopped successfully for project: {project_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error stopping process: {e}")
|
||||
print(f"Error stopping process: {e}")
|
||||
return False
|
||||
|
||||
def check_window_exists(self, window_title: str) -> bool:
|
||||
"""Check if a window with the given title exists"""
|
||||
# This method is not used anymore - checking is done in ProjectProcessTracker
|
||||
return False
|
||||
|
||||
def is_running(self, project_id: str) -> bool:
|
||||
"""Check if a process is running for a project"""
|
||||
# Don't rely on the launcher process, check for actual window
|
||||
return False # Will be overridden by is_project_running
|
||||
|
||||
def stop_all_processes(self):
|
||||
"""Stop all running processes"""
|
||||
project_ids = list(self.processes.keys())
|
||||
for project_id in project_ids:
|
||||
self.stop_process(project_id)
|
||||
|
||||
def get_process_info(self, project_id: str) -> Optional[dict]:
|
||||
"""Get process information for a project"""
|
||||
if project_id in self.processes:
|
||||
process = self.processes[project_id]
|
||||
return {
|
||||
"pid": process.pid if process else None,
|
||||
"running": self.is_running(project_id)
|
||||
}
|
||||
return None
|
||||
228
project_manager.py
Normale Datei
228
project_manager.py
Normale Datei
@ -0,0 +1,228 @@
|
||||
"""
|
||||
Project Manager Module
|
||||
Handles project storage, loading, and management
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Optional
|
||||
import uuid
|
||||
from utils.logger import logger
|
||||
|
||||
class Project:
|
||||
def __init__(self, name: str, path: str, project_id: str = None):
|
||||
self.id = project_id or str(uuid.uuid4())
|
||||
self.name = name
|
||||
self.path = path
|
||||
self.created_at = datetime.now().isoformat()
|
||||
self.last_accessed = datetime.now().isoformat()
|
||||
self.readme_path = os.path.join(path, "CLAUDE_PROJECT_README.md")
|
||||
self.description = ""
|
||||
self.tags = []
|
||||
self.gitea_repo = None # Format: "owner/repo_name" or None if not linked
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
"""Convert project to dictionary for JSON storage"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'path': self.path,
|
||||
'created_at': self.created_at,
|
||||
'last_accessed': self.last_accessed,
|
||||
'readme_path': self.readme_path,
|
||||
'description': self.description,
|
||||
'tags': self.tags,
|
||||
'gitea_repo': self.gitea_repo
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict) -> 'Project':
|
||||
"""Create project from dictionary"""
|
||||
project = cls(data['name'], data['path'], data['id'])
|
||||
project.created_at = data.get('created_at', datetime.now().isoformat())
|
||||
project.last_accessed = data.get('last_accessed', datetime.now().isoformat())
|
||||
project.readme_path = data.get('readme_path', os.path.join(data['path'], "CLAUDE_PROJECT_README.md"))
|
||||
project.description = data.get('description', '')
|
||||
project.tags = data.get('tags', [])
|
||||
project.gitea_repo = data.get('gitea_repo', None)
|
||||
return project
|
||||
|
||||
def update_last_accessed(self):
|
||||
"""Update last accessed timestamp"""
|
||||
self.last_accessed = datetime.now().isoformat()
|
||||
|
||||
class ProjectManager:
|
||||
def __init__(self, data_file: str = "data/projects.json"):
|
||||
self.data_file = data_file
|
||||
self.projects: Dict[str, Project] = {}
|
||||
self.vps_project = None
|
||||
self.admin_panel_project = None
|
||||
self.activity_server_project = None
|
||||
self._ensure_data_dir()
|
||||
self.load_projects()
|
||||
self._initialize_vps_project()
|
||||
self._initialize_admin_panel_project()
|
||||
self._initialize_activity_server_project()
|
||||
|
||||
def _ensure_data_dir(self):
|
||||
"""Ensure data directory exists"""
|
||||
data_dir = os.path.dirname(self.data_file)
|
||||
if data_dir and not os.path.exists(data_dir):
|
||||
os.makedirs(data_dir)
|
||||
|
||||
def _initialize_vps_project(self):
|
||||
"""Initialize the permanent VPS project"""
|
||||
vps_id = "vps-permanent"
|
||||
if vps_id not in self.projects:
|
||||
self.vps_project = Project(
|
||||
name="VPS Server",
|
||||
path="claude-dev@91.99.192.14",
|
||||
project_id=vps_id
|
||||
)
|
||||
self.vps_project.description = "Remote VPS Server with Claude"
|
||||
self.vps_project.tags = ["vps", "remote", "server"]
|
||||
self.projects[vps_id] = self.vps_project
|
||||
self.save_projects()
|
||||
else:
|
||||
self.vps_project = self.projects[vps_id]
|
||||
|
||||
def _initialize_admin_panel_project(self):
|
||||
"""Initialize the permanent Admin Panel project"""
|
||||
admin_id = "admin-panel-permanent"
|
||||
if admin_id not in self.projects:
|
||||
self.admin_panel_project = Project(
|
||||
name="Admin Panel",
|
||||
path="/opt/v2-Docker",
|
||||
project_id=admin_id
|
||||
)
|
||||
self.admin_panel_project.description = "V2 Docker Admin Panel"
|
||||
self.admin_panel_project.tags = ["admin", "docker", "v2"]
|
||||
self.projects[admin_id] = self.admin_panel_project
|
||||
self.save_projects()
|
||||
else:
|
||||
self.admin_panel_project = self.projects[admin_id]
|
||||
|
||||
def _initialize_activity_server_project(self):
|
||||
"""Initialize the permanent Activity Server project"""
|
||||
activity_id = "activity-server-permanent"
|
||||
if activity_id not in self.projects:
|
||||
self.activity_server_project = Project(
|
||||
name="Activity Server",
|
||||
path="/home/claude-dev/cpm-activity-server",
|
||||
project_id=activity_id
|
||||
)
|
||||
self.activity_server_project.description = "CPM Activity Server"
|
||||
self.activity_server_project.tags = ["activity", "server", "cpm"]
|
||||
self.projects[activity_id] = self.activity_server_project
|
||||
self.save_projects()
|
||||
else:
|
||||
self.activity_server_project = self.projects[activity_id]
|
||||
|
||||
def load_projects(self):
|
||||
"""Load projects from JSON file"""
|
||||
logger.info("Loading projects from file")
|
||||
if os.path.exists(self.data_file):
|
||||
try:
|
||||
with open(self.data_file, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
for proj_data in data.get('projects', []):
|
||||
project = Project.from_dict(proj_data)
|
||||
self.projects[project.id] = project
|
||||
except Exception as e:
|
||||
print(f"Error loading projects: {e}")
|
||||
self.projects = {}
|
||||
|
||||
def save_projects(self):
|
||||
"""Save projects to JSON file"""
|
||||
logger.debug("Saving projects to file")
|
||||
try:
|
||||
data = {
|
||||
'projects': [proj.to_dict() for proj in self.projects.values()],
|
||||
'last_updated': datetime.now().isoformat()
|
||||
}
|
||||
with open(self.data_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
print(f"Error saving projects: {e}")
|
||||
|
||||
def add_project(self, name: str, path: str) -> Project:
|
||||
"""Add new project"""
|
||||
logger.info(f"Adding project: {name} at {path}")
|
||||
# Check if project with same path already exists
|
||||
for project in self.projects.values():
|
||||
if project.path == path and project.id != "vps-permanent":
|
||||
# Update existing project
|
||||
project.name = name
|
||||
project.update_last_accessed()
|
||||
self.save_projects()
|
||||
return project
|
||||
|
||||
# Create new project
|
||||
project = Project(name, path)
|
||||
self.projects[project.id] = project
|
||||
self.save_projects()
|
||||
return project
|
||||
|
||||
def remove_project(self, project_id: str) -> bool:
|
||||
"""Remove project by ID"""
|
||||
logger.info(f"Removing project with ID: {project_id}")
|
||||
if project_id in self.projects and project_id not in ["vps-permanent", "admin-panel-permanent"]:
|
||||
del self.projects[project_id]
|
||||
self.save_projects()
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_project(self, project_id: str) -> Optional[Project]:
|
||||
"""Get project by ID"""
|
||||
return self.projects.get(project_id)
|
||||
|
||||
def get_all_projects(self) -> List[Project]:
|
||||
"""Get all projects sorted alphabetically by name"""
|
||||
projects = list(self.projects.values())
|
||||
# Sort alphabetically, but keep VPS projects first
|
||||
vps = [p for p in projects if p.id == "vps-permanent"]
|
||||
admin = [p for p in projects if p.id == "admin-panel-permanent"]
|
||||
activity = [p for p in projects if p.id == "activity-server-permanent"]
|
||||
others = [p for p in projects if p.id not in ["vps-permanent", "admin-panel-permanent", "activity-server-permanent"]]
|
||||
others.sort(key=lambda p: p.name.lower()) # Sort alphabetically by name (case-insensitive)
|
||||
return vps + admin + activity + others
|
||||
|
||||
def update_project(self, project_id: str, **kwargs):
|
||||
"""Update project properties"""
|
||||
project = self.get_project(project_id)
|
||||
if project:
|
||||
for key, value in kwargs.items():
|
||||
if hasattr(project, key):
|
||||
setattr(project, key, value)
|
||||
self.save_projects()
|
||||
|
||||
def search_projects(self, query: str) -> List[Project]:
|
||||
"""Search projects by name, path, or tags"""
|
||||
query = query.lower()
|
||||
results = []
|
||||
for project in self.projects.values():
|
||||
if (query in project.name.lower() or
|
||||
query in project.path.lower() or
|
||||
any(query in tag.lower() for tag in project.tags)):
|
||||
results.append(project)
|
||||
return results
|
||||
|
||||
# Test the module
|
||||
if __name__ == "__main__":
|
||||
# Create manager
|
||||
manager = ProjectManager("test_projects.json")
|
||||
|
||||
# Add test projects
|
||||
proj1 = manager.add_project("Test Project 1", "C:\\Users\\test\\project1")
|
||||
proj2 = manager.add_project("Test Project 2", "C:\\Users\\test\\project2")
|
||||
|
||||
# List all projects
|
||||
print("All projects:")
|
||||
for project in manager.get_all_projects():
|
||||
print(f"- {project.name} ({project.path})")
|
||||
|
||||
# Clean up test file
|
||||
import os
|
||||
if os.path.exists("test_projects.json"):
|
||||
os.remove("test_projects.json")
|
||||
65
project_process_tracker.py
Normale Datei
65
project_process_tracker.py
Normale Datei
@ -0,0 +1,65 @@
|
||||
"""
|
||||
Project Process Tracker
|
||||
Tracks running state of projects in memory
|
||||
"""
|
||||
import subprocess
|
||||
|
||||
class ProjectProcessTracker:
|
||||
"""Simple in-memory tracker for project states"""
|
||||
def __init__(self):
|
||||
self.running_projects = set()
|
||||
self.project_manager = None # Will be set by MainWindow
|
||||
|
||||
def set_project_manager(self, project_manager):
|
||||
"""Set reference to project manager"""
|
||||
self.project_manager = project_manager
|
||||
|
||||
def set_running(self, project_id: str):
|
||||
"""Mark a project as running"""
|
||||
self.running_projects.add(project_id)
|
||||
|
||||
def set_stopped(self, project_id: str):
|
||||
"""Mark a project as stopped"""
|
||||
self.running_projects.discard(project_id)
|
||||
|
||||
def check_window_exists(self, window_title: str) -> bool:
|
||||
"""Check if a window with the given title exists"""
|
||||
try:
|
||||
# Use PowerShell for faster window checking
|
||||
ps_command = f'powershell -Command "(Get-Process | Where-Object {{$_.MainWindowTitle -eq \'{window_title}\'}}).Count -gt 0"'
|
||||
|
||||
# Hide console window on Windows
|
||||
startupinfo = None
|
||||
if hasattr(subprocess, 'STARTUPINFO'):
|
||||
startupinfo = subprocess.STARTUPINFO()
|
||||
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||
startupinfo.wShowWindow = subprocess.SW_HIDE
|
||||
|
||||
result = subprocess.run(ps_command, capture_output=True, text=True, shell=True,
|
||||
timeout=1, startupinfo=startupinfo)
|
||||
return result.stdout.strip().lower() == 'true'
|
||||
except Exception as e:
|
||||
# Fallback to simple check
|
||||
return False
|
||||
|
||||
def is_running(self, project_id: str) -> bool:
|
||||
"""Check if a project is running by checking window title"""
|
||||
if project_id == "vps-permanent":
|
||||
# Check for VPS window
|
||||
return self.check_window_exists("Claude VPS Server")
|
||||
else:
|
||||
# Check for project window by name
|
||||
if self.project_manager:
|
||||
project = self.project_manager.get_project(project_id)
|
||||
if project:
|
||||
window_title = f"Claude - {project.name}"
|
||||
return self.check_window_exists(window_title)
|
||||
return False
|
||||
|
||||
def stop_all(self):
|
||||
"""Clear all running states"""
|
||||
self.running_projects.clear()
|
||||
|
||||
def get_running_projects(self) -> set:
|
||||
"""Get all running project IDs"""
|
||||
return self.running_projects.copy()
|
||||
295
readme_generator.py
Normale Datei
295
readme_generator.py
Normale Datei
@ -0,0 +1,295 @@
|
||||
"""
|
||||
README Generator Module
|
||||
Automatically generates and updates README files for projects
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Set
|
||||
import mimetypes
|
||||
|
||||
class ReadmeGenerator:
|
||||
def __init__(self):
|
||||
self.language_extensions = {
|
||||
'.py': 'Python',
|
||||
'.js': 'JavaScript',
|
||||
'.ts': 'TypeScript',
|
||||
'.jsx': 'React',
|
||||
'.tsx': 'React TypeScript',
|
||||
'.java': 'Java',
|
||||
'.cpp': 'C++',
|
||||
'.c': 'C',
|
||||
'.cs': 'C#',
|
||||
'.php': 'PHP',
|
||||
'.rb': 'Ruby',
|
||||
'.go': 'Go',
|
||||
'.rs': 'Rust',
|
||||
'.swift': 'Swift',
|
||||
'.kt': 'Kotlin',
|
||||
'.dart': 'Dart',
|
||||
'.r': 'R',
|
||||
'.scala': 'Scala',
|
||||
'.lua': 'Lua',
|
||||
'.pl': 'Perl',
|
||||
'.sh': 'Shell',
|
||||
'.bat': 'Batch',
|
||||
'.ps1': 'PowerShell',
|
||||
}
|
||||
|
||||
self.framework_indicators = {
|
||||
'React': ['package.json', 'react'],
|
||||
'Angular': ['angular.json', '@angular'],
|
||||
'Vue': ['vue.config.js', 'vue'],
|
||||
'Django': ['manage.py', 'django'],
|
||||
'Flask': ['app.py', 'flask'],
|
||||
'Express': ['express', 'node_modules'],
|
||||
'Spring': ['pom.xml', 'spring'],
|
||||
'.NET': ['.csproj', 'dotnet'],
|
||||
'Laravel': ['artisan', 'laravel'],
|
||||
'Rails': ['Gemfile', 'rails'],
|
||||
}
|
||||
|
||||
def analyze_project(self, project_path: str) -> Dict:
|
||||
"""Analyze project structure and content"""
|
||||
analysis = {
|
||||
'path': project_path,
|
||||
'name': os.path.basename(project_path),
|
||||
'languages': set(),
|
||||
'frameworks': set(),
|
||||
'file_count': 0,
|
||||
'total_size': 0,
|
||||
'structure': {},
|
||||
'key_files': [],
|
||||
'last_modified': None,
|
||||
}
|
||||
|
||||
# Walk through directory
|
||||
for root, dirs, files in os.walk(project_path):
|
||||
# Skip hidden and system directories
|
||||
dirs[:] = [d for d in dirs if not d.startswith('.') and d not in ['node_modules', '__pycache__', 'venv', 'env']]
|
||||
|
||||
rel_path = os.path.relpath(root, project_path)
|
||||
level = rel_path.count(os.sep)
|
||||
|
||||
# Limit depth for structure
|
||||
if level < 3:
|
||||
current_dict = analysis['structure']
|
||||
if rel_path != '.':
|
||||
for part in rel_path.split(os.sep):
|
||||
if part not in current_dict:
|
||||
current_dict[part] = {}
|
||||
current_dict = current_dict[part]
|
||||
|
||||
# Add files to structure
|
||||
for file in files[:10]: # Limit files shown
|
||||
if not file.startswith('.'):
|
||||
current_dict[file] = 'file'
|
||||
|
||||
# Analyze files
|
||||
for file in files:
|
||||
if file.startswith('.'):
|
||||
continue
|
||||
|
||||
file_path = os.path.join(root, file)
|
||||
analysis['file_count'] += 1
|
||||
|
||||
try:
|
||||
# Get file size
|
||||
size = os.path.getsize(file_path)
|
||||
analysis['total_size'] += size
|
||||
|
||||
# Get modification time
|
||||
mtime = os.path.getmtime(file_path)
|
||||
if not analysis['last_modified'] or mtime > analysis['last_modified']:
|
||||
analysis['last_modified'] = mtime
|
||||
|
||||
# Detect language
|
||||
ext = os.path.splitext(file)[1].lower()
|
||||
if ext in self.language_extensions:
|
||||
analysis['languages'].add(self.language_extensions[ext])
|
||||
|
||||
# Key files
|
||||
if file in ['README.md', 'package.json', 'requirements.txt', 'Dockerfile',
|
||||
'.gitignore', 'Makefile', 'setup.py', 'pom.xml']:
|
||||
analysis['key_files'].append(file)
|
||||
|
||||
except:
|
||||
pass
|
||||
|
||||
# Detect frameworks
|
||||
for framework, indicators in self.framework_indicators.items():
|
||||
for indicator in indicators:
|
||||
if any(indicator in str(f) for f in analysis['key_files']):
|
||||
analysis['frameworks'].add(framework)
|
||||
|
||||
# Convert sets to lists for JSON serialization
|
||||
analysis['languages'] = list(analysis['languages'])
|
||||
analysis['frameworks'] = list(analysis['frameworks'])
|
||||
|
||||
return analysis
|
||||
|
||||
def generate_readme(self, project_path: str, project_name: str = None) -> str:
|
||||
"""Generate README content for a project"""
|
||||
analysis = self.analyze_project(project_path)
|
||||
|
||||
if project_name:
|
||||
analysis['name'] = project_name
|
||||
|
||||
# Format file size
|
||||
size_mb = analysis['total_size'] / (1024 * 1024)
|
||||
size_str = f"{size_mb:.1f} MB" if size_mb > 1 else f"{analysis['total_size'] / 1024:.1f} KB"
|
||||
|
||||
# Format last modified
|
||||
if analysis['last_modified']:
|
||||
last_mod = datetime.fromtimestamp(analysis['last_modified']).strftime('%Y-%m-%d %H:%M')
|
||||
else:
|
||||
last_mod = "Unknown"
|
||||
|
||||
# Build README content
|
||||
content = f"""# {analysis['name']}
|
||||
|
||||
*This README was automatically generated by Claude Project Manager*
|
||||
|
||||
## Project Overview
|
||||
|
||||
- **Path**: `{analysis['path']}`
|
||||
- **Files**: {analysis['file_count']} files
|
||||
- **Size**: {size_str}
|
||||
- **Last Modified**: {last_mod}
|
||||
|
||||
## Technology Stack
|
||||
|
||||
"""
|
||||
|
||||
if analysis['languages']:
|
||||
content += "### Languages\n"
|
||||
for lang in sorted(analysis['languages']):
|
||||
content += f"- {lang}\n"
|
||||
content += "\n"
|
||||
|
||||
if analysis['frameworks']:
|
||||
content += "### Frameworks & Libraries\n"
|
||||
for framework in sorted(analysis['frameworks']):
|
||||
content += f"- {framework}\n"
|
||||
content += "\n"
|
||||
|
||||
# Add structure
|
||||
content += "## Project Structure\n\n```\n"
|
||||
content += self._format_structure(analysis['structure'])
|
||||
content += "```\n\n"
|
||||
|
||||
# Add key files section
|
||||
if analysis['key_files']:
|
||||
content += "## Key Files\n\n"
|
||||
for file in analysis['key_files']:
|
||||
content += f"- `{file}`\n"
|
||||
content += "\n"
|
||||
|
||||
# Add Claude usage section
|
||||
content += """## Claude Integration
|
||||
|
||||
This project is managed with Claude Project Manager. To work with this project:
|
||||
|
||||
1. Open Claude Project Manager
|
||||
2. Click on this project's tile
|
||||
3. Claude will open in the project directory
|
||||
|
||||
## Notes
|
||||
|
||||
*Add your project-specific notes here*
|
||||
|
||||
---
|
||||
|
||||
## Development Log
|
||||
|
||||
"""
|
||||
|
||||
# Add timestamp
|
||||
content += f"- README generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
|
||||
|
||||
return content
|
||||
|
||||
def _format_structure(self, structure: Dict, prefix: str = "", is_last: bool = True) -> str:
|
||||
"""Format directory structure for display"""
|
||||
output = ""
|
||||
items = list(structure.items())
|
||||
|
||||
for i, (name, value) in enumerate(items):
|
||||
is_last_item = i == len(items) - 1
|
||||
|
||||
# Add tree characters
|
||||
if prefix:
|
||||
output += prefix
|
||||
output += "└── " if is_last_item else "├── "
|
||||
|
||||
output += name
|
||||
|
||||
if isinstance(value, dict) and value: # It's a directory with contents
|
||||
output += "/\n"
|
||||
extension = " " if is_last_item else "│ "
|
||||
output += self._format_structure(value, prefix + extension, is_last_item)
|
||||
else:
|
||||
output += "\n"
|
||||
|
||||
return output
|
||||
|
||||
def update_readme(self, readme_path: str, new_content: str):
|
||||
"""Update README file, preserving user notes"""
|
||||
if os.path.exists(readme_path):
|
||||
try:
|
||||
with open(readme_path, 'r', encoding='utf-8') as f:
|
||||
old_content = f.read()
|
||||
|
||||
# Try to preserve user notes section
|
||||
notes_marker = "## Notes"
|
||||
if notes_marker in old_content:
|
||||
notes_start = old_content.find(notes_marker)
|
||||
notes_end = old_content.find("\n---", notes_start)
|
||||
if notes_end > notes_start:
|
||||
user_notes = old_content[notes_start:notes_end]
|
||||
# Replace empty notes section with user's notes
|
||||
new_content = new_content.replace(
|
||||
"## Notes\n\n*Add your project-specific notes here*\n",
|
||||
user_notes
|
||||
)
|
||||
|
||||
# Append to development log
|
||||
if "## Development Log" in old_content:
|
||||
log_start = old_content.find("## Development Log")
|
||||
old_log = old_content[log_start:]
|
||||
# Remove the new log header and just append entries
|
||||
new_log_entry = f"- README updated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
|
||||
new_content = new_content[:new_content.find("## Development Log")] + old_log + new_log_entry
|
||||
|
||||
except:
|
||||
pass
|
||||
|
||||
# Write the README
|
||||
with open(readme_path, 'w', encoding='utf-8') as f:
|
||||
f.write(new_content)
|
||||
|
||||
def generate_and_save_readme(self, project_path: str, project_name: str = None) -> str:
|
||||
"""Generate and save README for a project"""
|
||||
readme_path = os.path.join(project_path, "CLAUDE_PROJECT_README.md")
|
||||
content = self.generate_readme(project_path, project_name)
|
||||
self.update_readme(readme_path, content)
|
||||
return readme_path
|
||||
|
||||
# Test the module
|
||||
if __name__ == "__main__":
|
||||
generator = ReadmeGenerator()
|
||||
|
||||
# Test with current directory
|
||||
test_path = os.path.dirname(os.path.abspath(__file__))
|
||||
analysis = generator.analyze_project(test_path)
|
||||
|
||||
print("Project Analysis:")
|
||||
print(f"Languages: {analysis['languages']}")
|
||||
print(f"Files: {analysis['file_count']}")
|
||||
print(f"Key files: {analysis['key_files']}")
|
||||
|
||||
# Generate README
|
||||
readme = generator.generate_readme(test_path, "Claude Project Manager")
|
||||
print("\nGenerated README Preview:")
|
||||
print(readme[:500] + "...")
|
||||
5
requirements.txt
Normale Datei
5
requirements.txt
Normale Datei
@ -0,0 +1,5 @@
|
||||
customtkinter>=5.2.0
|
||||
pillow>=10.0.0
|
||||
requests>=2.31.0
|
||||
python-socketio[client]>=5.10.0
|
||||
websocket-client>=1.6.0
|
||||
30
scripts/check_lfs_status.bat
Normale Datei
30
scripts/check_lfs_status.bat
Normale Datei
@ -0,0 +1,30 @@
|
||||
@echo off
|
||||
echo Checking Git LFS Status...
|
||||
echo =============================
|
||||
echo.
|
||||
|
||||
echo 1. LFS Version:
|
||||
git lfs version
|
||||
echo.
|
||||
|
||||
echo 2. LFS Environment:
|
||||
git lfs env
|
||||
echo.
|
||||
|
||||
echo 3. Tracked LFS patterns:
|
||||
git lfs track
|
||||
echo.
|
||||
|
||||
echo 4. LFS files in repository:
|
||||
git lfs ls-files
|
||||
echo.
|
||||
|
||||
echo 5. Verifying LFS remote:
|
||||
git lfs fsck
|
||||
echo.
|
||||
|
||||
echo 6. Current remotes:
|
||||
git remote -v
|
||||
echo.
|
||||
|
||||
pause
|
||||
19
scripts/fix_large_files.bat
Normale Datei
19
scripts/fix_large_files.bat
Normale Datei
@ -0,0 +1,19 @@
|
||||
@echo off
|
||||
echo Finding large files in repository...
|
||||
echo.
|
||||
|
||||
REM Find files larger than 100MB
|
||||
echo Files larger than 100MB:
|
||||
git ls-files -z | xargs -0 -n1 -I{} -- du -h {} | grep -E "^[0-9]+(M|G)" | sort -hr
|
||||
|
||||
echo.
|
||||
echo To add large files to Git LFS:
|
||||
echo 1. git lfs track "*.zip"
|
||||
echo 2. git lfs track "*.mp4"
|
||||
echo 3. git lfs track "path/to/largefile"
|
||||
echo.
|
||||
echo Then: git add .gitattributes
|
||||
echo git add [large files]
|
||||
echo git commit -m "Add LFS tracking"
|
||||
echo.
|
||||
pause
|
||||
24
scripts/fix_remote_organization.bat
Normale Datei
24
scripts/fix_remote_organization.bat
Normale Datei
@ -0,0 +1,24 @@
|
||||
@echo off
|
||||
echo Fixing Git Remote for IntelSight Organization
|
||||
echo =============================================
|
||||
echo.
|
||||
|
||||
echo Current remote:
|
||||
git remote -v
|
||||
echo.
|
||||
|
||||
echo Removing old remote...
|
||||
git remote remove origin
|
||||
|
||||
echo Adding new remote for IntelSight organization...
|
||||
git remote add origin https://gitea-undso.intelsight.de/IntelSight/website-main.git
|
||||
|
||||
echo.
|
||||
echo New remote:
|
||||
git remote -v
|
||||
echo.
|
||||
|
||||
echo Now push to the new remote:
|
||||
echo git push -u origin master
|
||||
echo.
|
||||
pause
|
||||
31
scripts/fix_website_auth.bat
Normale Datei
31
scripts/fix_website_auth.bat
Normale Datei
@ -0,0 +1,31 @@
|
||||
@echo off
|
||||
echo Fixing Authentication for website-main
|
||||
echo =====================================
|
||||
echo.
|
||||
|
||||
cd C:\Users\hendr\Desktop\IntelSight\website-main
|
||||
|
||||
echo Current remote:
|
||||
git remote -v
|
||||
echo.
|
||||
|
||||
echo Removing old remote...
|
||||
git remote remove origin
|
||||
|
||||
echo Adding authenticated remote...
|
||||
echo NOTE: Replace USERNAME and TOKEN with your actual Gitea credentials!
|
||||
echo.
|
||||
echo Example:
|
||||
echo git remote add origin https://USERNAME:TOKEN@gitea-undso.intelsight.de/IntelSight/website-main.git
|
||||
echo.
|
||||
echo Your current credentials from ClaudeProjectManager:
|
||||
echo Username: StuXn3t
|
||||
echo Token: 327caf5e04a43c35e67e0fb00e0a9cd269889db9
|
||||
echo.
|
||||
echo Run this command:
|
||||
echo git remote add origin https://StuXn3t:327caf5e04a43c35e67e0fb00e0a9cd269889db9@gitea-undso.intelsight.de/IntelSight/website-main.git
|
||||
echo.
|
||||
echo Then:
|
||||
echo git push -u origin master
|
||||
echo.
|
||||
pause
|
||||
54
scripts/fix_website_final.bat
Normale Datei
54
scripts/fix_website_final.bat
Normale Datei
@ -0,0 +1,54 @@
|
||||
@echo off
|
||||
echo Final fix for website-main...
|
||||
echo.
|
||||
|
||||
cd /d "C:\Users\hendr\Desktop\IntelSight\website-main"
|
||||
|
||||
echo Step 1: Removing Git LFS completely...
|
||||
git lfs uninstall
|
||||
|
||||
echo.
|
||||
echo Step 2: Checking current status...
|
||||
git status
|
||||
|
||||
echo.
|
||||
echo Step 3: The video file is missing! Let's check...
|
||||
if not exist "assets\videos\background.mp4" (
|
||||
echo ERROR: background.mp4 was removed by LFS!
|
||||
echo You need to restore it from backup or remove it from the repository.
|
||||
echo.
|
||||
echo Option A: Remove from repository
|
||||
echo Run: git rm assets/videos/background.mp4
|
||||
echo git commit -m "Remove video file"
|
||||
echo.
|
||||
echo Option B: Add to .gitignore
|
||||
echo Add this line to .gitignore: assets/videos/background.mp4
|
||||
echo.
|
||||
) else (
|
||||
echo File exists. Adding it back...
|
||||
git add assets/videos/background.mp4
|
||||
git commit -m "Re-add video file without LFS"
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Step 4: Removing LFS hooks...
|
||||
if exist .git\hooks\pre-push (
|
||||
del .git\hooks\pre-push
|
||||
echo Removed pre-push hook
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Step 5: Try push with --no-verify...
|
||||
git push --no-verify -u origin main
|
||||
|
||||
echo.
|
||||
echo If still failing, the video file might be too large (>100MB).
|
||||
echo In that case, add it to .gitignore:
|
||||
echo.
|
||||
echo echo assets/videos/background.mp4 >> .gitignore
|
||||
echo git rm --cached assets/videos/background.mp4
|
||||
echo git add .gitignore
|
||||
echo git commit -m "Remove large video file"
|
||||
echo git push -u origin main
|
||||
echo.
|
||||
pause
|
||||
46
scripts/fix_website_lfs_and_branch.bat
Normale Datei
46
scripts/fix_website_lfs_and_branch.bat
Normale Datei
@ -0,0 +1,46 @@
|
||||
@echo off
|
||||
echo Fixing website-main LFS and branch issues...
|
||||
echo.
|
||||
|
||||
cd /d "C:\Users\hendr\Desktop\IntelSight\website-main"
|
||||
|
||||
echo Step 1: Checking for LFS files...
|
||||
git lfs ls-files
|
||||
|
||||
echo.
|
||||
echo Step 2: Removing LFS tracking (if any)...
|
||||
if exist .gitattributes (
|
||||
echo Backing up .gitattributes...
|
||||
copy .gitattributes .gitattributes.backup
|
||||
|
||||
echo Removing LFS entries...
|
||||
powershell -Command "(Get-Content .gitattributes) | Where-Object {$_ -notmatch 'filter=lfs'} | Set-Content .gitattributes.temp"
|
||||
move /y .gitattributes.temp .gitattributes
|
||||
|
||||
git add .gitattributes
|
||||
git commit -m "Remove LFS tracking" 2>nul
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Step 3: Checking which files are tracked by LFS...
|
||||
git lfs ls-files
|
||||
for /f "tokens=3" %%i in ('git lfs ls-files') do (
|
||||
echo Removing LFS tracking for: %%i
|
||||
git rm --cached %%i
|
||||
git add %%i
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Step 4: Renaming branch from master to main...
|
||||
git branch -m master main
|
||||
|
||||
echo.
|
||||
echo Step 5: Pushing to main branch...
|
||||
git push -u origin main --force
|
||||
|
||||
echo.
|
||||
echo Done! If push still fails, try:
|
||||
echo 1. Delete .git/hooks/pre-push (if exists)
|
||||
echo 2. Run: git push --no-verify -u origin main
|
||||
echo.
|
||||
pause
|
||||
37
scripts/fix_website_repo.bat
Normale Datei
37
scripts/fix_website_repo.bat
Normale Datei
@ -0,0 +1,37 @@
|
||||
@echo off
|
||||
echo Fixing website-main repository...
|
||||
echo.
|
||||
|
||||
cd /d "C:\Users\hendr\Desktop\IntelSight\website-main"
|
||||
|
||||
echo Step 1: Removing old remote...
|
||||
git remote remove origin 2>nul
|
||||
|
||||
echo Step 2: Checking/removing LFS configuration...
|
||||
if exist .gitattributes (
|
||||
findstr /C:"filter=lfs" .gitattributes >nul
|
||||
if %errorlevel%==0 (
|
||||
echo Found LFS configuration, creating backup...
|
||||
copy .gitattributes .gitattributes.backup
|
||||
|
||||
echo Removing LFS entries...
|
||||
powershell -Command "(Get-Content .gitattributes) | Where-Object {$_ -notmatch 'filter=lfs'} | Set-Content .gitattributes"
|
||||
|
||||
echo Unstaging LFS files...
|
||||
git rm --cached .gitattributes 2>nul
|
||||
git add .gitattributes
|
||||
)
|
||||
)
|
||||
|
||||
echo Step 3: Adding correct remote...
|
||||
git remote add origin https://IntelSight_Admin:3b4a6ba1ade3f34640f3c85d2333b4a3a0627471@gitea-undso.intelsight.de/IntelSight/website-main.git
|
||||
|
||||
echo Step 4: Verifying remote...
|
||||
git remote -v
|
||||
|
||||
echo.
|
||||
echo Ready! Now try:
|
||||
echo 1. Create the repository on Gitea first (in IntelSight organization)
|
||||
echo 2. Then run: git push -u origin master:main
|
||||
echo.
|
||||
pause
|
||||
21
scripts/quick_fix_website.bat
Normale Datei
21
scripts/quick_fix_website.bat
Normale Datei
@ -0,0 +1,21 @@
|
||||
@echo off
|
||||
echo Quick fix for website-main repository...
|
||||
echo.
|
||||
|
||||
cd /d "C:\Users\hendr\Desktop\IntelSight\website-main"
|
||||
|
||||
echo Current remote:
|
||||
git remote -v
|
||||
echo.
|
||||
|
||||
echo Fixing remote URL with correct token...
|
||||
git remote set-url origin https://IntelSight_Admin:3b4a6ba1ade3f34640f3c85d2333b4a3a0627471@gitea-undso.intelsight.de/IntelSight/website-main.git
|
||||
|
||||
echo.
|
||||
echo New remote:
|
||||
git remote -v
|
||||
echo.
|
||||
|
||||
echo Done! Now you can push normally.
|
||||
echo.
|
||||
pause
|
||||
30
scripts/website_gitignore_template.txt
Normale Datei
30
scripts/website_gitignore_template.txt
Normale Datei
@ -0,0 +1,30 @@
|
||||
# Large files
|
||||
*.zip
|
||||
*.rar
|
||||
*.7z
|
||||
*.tar
|
||||
*.gz
|
||||
|
||||
# Media files over 100MB
|
||||
*.mp4
|
||||
*.avi
|
||||
*.mov
|
||||
*.mkv
|
||||
*.wmv
|
||||
|
||||
# Database dumps
|
||||
*.sql
|
||||
*.dump
|
||||
|
||||
# Backup files
|
||||
*.bak
|
||||
backup/
|
||||
backups/
|
||||
|
||||
# Build artifacts
|
||||
dist/
|
||||
build/
|
||||
node_modules/
|
||||
|
||||
# Add specific large files here
|
||||
# largefile.ext
|
||||
1
services/__init__.py
Normale Datei
1
services/__init__.py
Normale Datei
@ -0,0 +1 @@
|
||||
# Services package
|
||||
326
services/activity_sync.py
Normale Datei
326
services/activity_sync.py
Normale Datei
@ -0,0 +1,326 @@
|
||||
"""
|
||||
Activity Sync Service for CPM
|
||||
Handles WebSocket connection to the Activity Server
|
||||
"""
|
||||
|
||||
import socketio
|
||||
import requests
|
||||
import threading
|
||||
import json
|
||||
from typing import Optional, Callable, List, Dict
|
||||
from pathlib import Path
|
||||
from utils.logger import logger
|
||||
|
||||
|
||||
class ActivitySyncService:
|
||||
def __init__(self, server_url: str = None, api_key: str = None, user_id: str = None, user_name: str = None):
|
||||
self.server_url = server_url or "http://localhost:3001"
|
||||
self.api_key = api_key
|
||||
self.user_id = user_id
|
||||
self.user_name = user_name
|
||||
self.sio = None
|
||||
self.connected = False
|
||||
self.on_activities_update = None
|
||||
self.activities = []
|
||||
self.active_projects = [] # Changed from single current_activity to list
|
||||
|
||||
# Load settings
|
||||
self.load_settings()
|
||||
|
||||
def load_settings(self):
|
||||
"""Load activity sync settings from file"""
|
||||
settings_file = Path.home() / ".claude_project_manager" / "activity_settings.json"
|
||||
try:
|
||||
if settings_file.exists():
|
||||
with open(settings_file, 'r') as f:
|
||||
settings = json.load(f)
|
||||
self.server_url = settings.get("server_url", self.server_url)
|
||||
self.api_key = settings.get("api_key", self.api_key)
|
||||
self.user_id = settings.get("user_id", self.user_id)
|
||||
self.user_name = settings.get("user_name", self.user_name)
|
||||
logger.info(f"Loaded activity sync settings from {settings_file}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load activity settings: {e}")
|
||||
|
||||
def save_settings(self):
|
||||
"""Save activity sync settings to file"""
|
||||
settings_file = Path.home() / ".claude_project_manager" / "activity_settings.json"
|
||||
try:
|
||||
settings_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
settings = {
|
||||
"server_url": self.server_url,
|
||||
"api_key": self.api_key,
|
||||
"user_id": self.user_id,
|
||||
"user_name": self.user_name
|
||||
}
|
||||
with open(settings_file, 'w') as f:
|
||||
json.dump(settings, f, indent=2)
|
||||
logger.info(f"Saved activity sync settings to {settings_file}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save activity settings: {e}")
|
||||
|
||||
def connect(self):
|
||||
"""Connect to the activity server"""
|
||||
if not all([self.server_url, self.api_key, self.user_id, self.user_name]):
|
||||
logger.warning("Activity sync not configured properly")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Create Socket.IO client
|
||||
self.sio = socketio.Client()
|
||||
|
||||
# Register event handlers
|
||||
@self.sio.event
|
||||
def connect():
|
||||
logger.info("Connected to activity server")
|
||||
self.connected = True
|
||||
# Fetch initial activities after connection
|
||||
self._fetch_initial_activities()
|
||||
|
||||
@self.sio.event
|
||||
def disconnect():
|
||||
logger.info("Disconnected from activity server")
|
||||
self.connected = False
|
||||
|
||||
@self.sio.event
|
||||
def activities_update(data):
|
||||
"""Handle activities update from server"""
|
||||
logger.debug(f"Received raw activities update: {len(data)} items")
|
||||
|
||||
# Filter out inactive entries and ensure we only keep active ones
|
||||
active_activities = [
|
||||
activity for activity in data
|
||||
if activity.get('isActive', False)
|
||||
]
|
||||
|
||||
# Log details about the activities
|
||||
for activity in active_activities:
|
||||
logger.debug(f"Active: {activity.get('userName')} on {activity.get('projectName')}")
|
||||
|
||||
self.activities = active_activities
|
||||
logger.info(f"Activities update: {len(active_activities)} active (of {len(data)} total)")
|
||||
|
||||
if self.on_activities_update:
|
||||
self.on_activities_update(active_activities)
|
||||
|
||||
@self.sio.event
|
||||
def connect_error(data):
|
||||
logger.error(f"Connection error: {data}")
|
||||
self.connected = False
|
||||
|
||||
# Connect with authentication
|
||||
logger.info(f"Attempting to connect to activity server: {self.server_url}")
|
||||
|
||||
# Try to force polling transport if WebSockets are blocked
|
||||
import os
|
||||
os.environ['SOCKETIO_TRANSPORT'] = 'polling'
|
||||
|
||||
self.sio.connect(
|
||||
self.server_url,
|
||||
auth={
|
||||
'token': self.api_key,
|
||||
'userId': self.user_id,
|
||||
'userName': self.user_name
|
||||
},
|
||||
wait_timeout=10,
|
||||
transports=['polling', 'websocket'] # Try polling first, then websocket
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to activity server: {e}")
|
||||
self.connected = False
|
||||
return False
|
||||
|
||||
def disconnect(self):
|
||||
"""Disconnect from the activity server"""
|
||||
if self.sio:
|
||||
try:
|
||||
self.sio.disconnect()
|
||||
self.sio = None
|
||||
self.connected = False
|
||||
logger.info("Disconnected from activity server")
|
||||
except Exception as e:
|
||||
logger.error(f"Error disconnecting: {e}")
|
||||
|
||||
def start_activity(self, project_name: str, project_path: str, description: str = ""):
|
||||
"""Start a new activity (adds to active projects list)"""
|
||||
logger.debug(f"start_activity called for: {project_name}")
|
||||
|
||||
if not self.connected or not self.sio:
|
||||
logger.warning("Not connected to activity server")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Check if project is already active
|
||||
existing = next((p for p in self.active_projects if p['projectName'] == project_name), None)
|
||||
if existing:
|
||||
logger.info(f"Project {project_name} is already active")
|
||||
return True
|
||||
|
||||
# Add to active projects list
|
||||
new_activity = {
|
||||
'projectName': project_name,
|
||||
'projectPath': project_path,
|
||||
'userId': self.user_id,
|
||||
'userName': self.user_name,
|
||||
'isActive': True
|
||||
}
|
||||
self.active_projects.append(new_activity)
|
||||
logger.debug(f"Added to active_projects: {new_activity}")
|
||||
|
||||
# Emit to server
|
||||
self.sio.emit('activity-start', {
|
||||
'projectName': project_name,
|
||||
'projectPath': project_path,
|
||||
'description': description
|
||||
})
|
||||
|
||||
logger.info(f"Started activity for project: {project_name}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to start activity: {e}")
|
||||
# Remove from list on failure
|
||||
self.active_projects = [p for p in self.active_projects if p['projectName'] != project_name]
|
||||
return False
|
||||
|
||||
def stop_activity(self, project_name: str = None):
|
||||
"""Stop activity for a specific project or all activities if no project specified"""
|
||||
logger.debug(f"stop_activity called for project: {project_name}")
|
||||
|
||||
if not self.connected or not self.sio:
|
||||
logger.warning("Not connected to activity server")
|
||||
return False
|
||||
|
||||
try:
|
||||
if project_name:
|
||||
# Stop specific project
|
||||
activity = next((p for p in self.active_projects if p['projectName'] == project_name), None)
|
||||
if not activity:
|
||||
logger.warning(f"Project {project_name} is not in active projects")
|
||||
return False
|
||||
|
||||
# Remove from active projects
|
||||
self.active_projects = [p for p in self.active_projects if p['projectName'] != project_name]
|
||||
logger.debug(f"Removed {project_name} from active_projects")
|
||||
|
||||
# NEW: Emit with activityId if available
|
||||
emit_data = {}
|
||||
if 'id' in activity:
|
||||
emit_data['activityId'] = activity['id']
|
||||
elif 'activityId' in activity:
|
||||
emit_data['activityId'] = activity['activityId']
|
||||
else:
|
||||
# Fallback to project name
|
||||
emit_data['projectName'] = project_name
|
||||
|
||||
self.sio.emit('activity-stop', emit_data)
|
||||
|
||||
logger.info(f"Stopped activity for project: {project_name}")
|
||||
else:
|
||||
# Stop all activities (backward compatibility)
|
||||
project_names = [p['projectName'] for p in self.active_projects]
|
||||
self.active_projects = []
|
||||
logger.debug("Cleared all active_projects")
|
||||
|
||||
# Emit to server (server should handle stopping all activities for this user)
|
||||
self.sio.emit('activity-stop')
|
||||
|
||||
logger.info(f"Stopped all activities: {project_names}")
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to stop activity: {e}")
|
||||
return False
|
||||
|
||||
def get_activities(self) -> List[Dict]:
|
||||
"""Get current activities via REST API"""
|
||||
if not self.api_key:
|
||||
return []
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
f"{self.server_url}/api/activities",
|
||||
headers={"X-API-Key": self.api_key},
|
||||
timeout=5
|
||||
)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
all_activities = data.get('activities', [])
|
||||
# Filter to only return active activities
|
||||
return [a for a in all_activities if a.get('isActive', False)]
|
||||
else:
|
||||
logger.error(f"Failed to fetch activities: {response.status_code}")
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fetch activities: {e}")
|
||||
return []
|
||||
|
||||
def is_configured(self) -> bool:
|
||||
"""Check if the service is properly configured"""
|
||||
return all([self.server_url, self.api_key, self.user_id, self.user_name])
|
||||
|
||||
def get_active_users_count(self) -> int:
|
||||
"""Get count of active users"""
|
||||
return len(self.activities)
|
||||
|
||||
def is_project_active(self, project_name: str) -> Optional[Dict]:
|
||||
"""Check if a project has active users"""
|
||||
for activity in self.activities:
|
||||
if activity.get('projectName') == project_name and activity.get('isActive'):
|
||||
return activity
|
||||
return None
|
||||
|
||||
def get_current_activity(self, project_name: str = None) -> Optional[Dict]:
|
||||
"""Get current user's activity for a specific project or first active project"""
|
||||
if project_name:
|
||||
# Return specific project if active
|
||||
activity = next((p for p in self.active_projects if p['projectName'] == project_name), None)
|
||||
logger.debug(f"get_current_activity for {project_name}, returning: {activity}")
|
||||
return activity
|
||||
else:
|
||||
# Return first active project for backward compatibility
|
||||
activity = self.active_projects[0] if self.active_projects else None
|
||||
logger.debug(f"get_current_activity called, returning: {activity}")
|
||||
return activity
|
||||
|
||||
def get_all_current_activities(self) -> List[Dict]:
|
||||
"""Get all current user's active projects"""
|
||||
logger.debug(f"get_all_current_activities called, returning {len(self.active_projects)} activities")
|
||||
return self.active_projects.copy()
|
||||
|
||||
def is_project_active_for_user(self, project_name: str) -> bool:
|
||||
"""Check if a specific project is active for the current user"""
|
||||
return any(p['projectName'] == project_name for p in self.active_projects)
|
||||
|
||||
def _fetch_initial_activities(self):
|
||||
"""Fetch initial activities after connection"""
|
||||
try:
|
||||
# Fetch user-specific activities
|
||||
response = requests.get(
|
||||
f"{self.server_url}/api/activities/{self.user_id}",
|
||||
headers={"X-API-Key": self.api_key},
|
||||
timeout=5
|
||||
)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
# NEW: Handle array format
|
||||
user_activities = data.get('activities', [])
|
||||
# Update our active projects list
|
||||
self.active_projects = [a for a in user_activities if a.get('isActive', False)]
|
||||
logger.info(f"Fetched {len(self.active_projects)} active projects for current user")
|
||||
|
||||
# Also fetch all activities
|
||||
activities = self.get_activities() # Already filtered to only active
|
||||
if activities:
|
||||
self.activities = activities
|
||||
if self.on_activities_update:
|
||||
self.on_activities_update(activities)
|
||||
logger.info(f"Fetched {len(activities)} active activities from all users")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fetch initial activities: {e}")
|
||||
|
||||
|
||||
# Global instance
|
||||
activity_service = ActivitySyncService()
|
||||
72
setup_activity_server_profile.sh
Normale Datei
72
setup_activity_server_profile.sh
Normale Datei
@ -0,0 +1,72 @@
|
||||
#!/bin/bash
|
||||
# Setup script to configure automatic directory change for Activity Server SSH connections
|
||||
|
||||
echo "Activity Server Profile Setup Script"
|
||||
echo "===================================="
|
||||
echo ""
|
||||
echo "This script will configure the server to automatically navigate to"
|
||||
echo "/home/claude-dev/cpm-activity-server when connecting via SSH."
|
||||
echo ""
|
||||
|
||||
# SSH connection details
|
||||
SSH_HOST="91.99.192.14"
|
||||
SSH_USER="claude-dev"
|
||||
SSH_PASS="z0E1Al}q2H?Yqd!O"
|
||||
|
||||
# Create the profile modification script
|
||||
cat << 'PROFILE_SCRIPT' > /tmp/setup_profile.sh
|
||||
#!/bin/bash
|
||||
|
||||
# Add to .bashrc to detect SSH connections and change directory
|
||||
echo "" >> ~/.bashrc
|
||||
echo "# Auto-navigate to Activity Server directory for SSH connections" >> ~/.bashrc
|
||||
echo "if [ -n \"\$SSH_CLIENT\" ] || [ -n \"\$SSH_TTY\" ]; then" >> ~/.bashrc
|
||||
echo " # Check if we're in an SSH session" >> ~/.bashrc
|
||||
echo " if [ -d \"/home/claude-dev/cpm-activity-server\" ]; then" >> ~/.bashrc
|
||||
echo " cd /home/claude-dev/cpm-activity-server" >> ~/.bashrc
|
||||
echo " echo \"Automatically changed to Activity Server directory\"" >> ~/.bashrc
|
||||
echo " echo \"\"" >> ~/.bashrc
|
||||
echo " fi" >> ~/.bashrc
|
||||
echo "fi" >> ~/.bashrc
|
||||
|
||||
echo "Profile updated successfully!"
|
||||
PROFILE_SCRIPT
|
||||
|
||||
# Make the script executable
|
||||
chmod +x /tmp/setup_profile.sh
|
||||
|
||||
echo "Connecting to server to apply configuration..."
|
||||
echo ""
|
||||
|
||||
# Try different methods to connect and run the script
|
||||
if command -v sshpass >/dev/null 2>&1; then
|
||||
echo "Using sshpass..."
|
||||
sshpass -p "$SSH_PASS" ssh -o StrictHostKeyChecking=no $SSH_USER@$SSH_HOST 'bash -s' < /tmp/setup_profile.sh
|
||||
elif command -v expect >/dev/null 2>&1; then
|
||||
echo "Using expect..."
|
||||
expect << EOF
|
||||
spawn ssh -o StrictHostKeyChecking=no $SSH_USER@$SSH_HOST
|
||||
expect "password:"
|
||||
send "$SSH_PASS\r"
|
||||
expect "$ "
|
||||
send "bash < /tmp/setup_profile.sh\r"
|
||||
expect "$ "
|
||||
send "exit\r"
|
||||
expect eof
|
||||
EOF
|
||||
else
|
||||
echo "Neither sshpass nor expect found."
|
||||
echo ""
|
||||
echo "Please manually run the following on the server:"
|
||||
echo "1. SSH to $SSH_USER@$SSH_HOST"
|
||||
echo "2. Run the commands in /tmp/setup_profile.sh"
|
||||
echo ""
|
||||
echo "Or install sshpass: sudo apt-get install sshpass"
|
||||
fi
|
||||
|
||||
# Clean up
|
||||
rm -f /tmp/setup_profile.sh
|
||||
|
||||
echo ""
|
||||
echo "Setup complete! Future SSH connections will automatically"
|
||||
echo "navigate to the Activity Server directory."
|
||||
14
src/gitea/__init__.py
Normale Datei
14
src/gitea/__init__.py
Normale Datei
@ -0,0 +1,14 @@
|
||||
from .gitea_client import GiteaClient, GiteaConfig
|
||||
from .git_operations import GitOperationsManager
|
||||
from .repository_manager import RepositoryManager
|
||||
from .issue_pr_manager import IssueManager, PullRequestManager, IssuePRManager
|
||||
|
||||
__all__ = [
|
||||
'GiteaClient',
|
||||
'GiteaConfig',
|
||||
'GitOperationsManager',
|
||||
'RepositoryManager',
|
||||
'IssueManager',
|
||||
'PullRequestManager',
|
||||
'IssuePRManager'
|
||||
]
|
||||
667
src/gitea/git_operations.py
Normale Datei
667
src/gitea/git_operations.py
Normale Datei
@ -0,0 +1,667 @@
|
||||
import os
|
||||
import subprocess
|
||||
import logging
|
||||
from typing import Optional, List, Tuple
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog
|
||||
from datetime import datetime
|
||||
from utils.logger import logger
|
||||
import json
|
||||
|
||||
@dataclass
|
||||
class GitCredentials:
|
||||
username: str
|
||||
token: str
|
||||
|
||||
def get_remote_url(self, base_url: str, repo_path: str) -> str:
|
||||
from urllib.parse import urlparse
|
||||
parsed = urlparse(base_url)
|
||||
url = f"https://{self.username}:{self.token}@{parsed.netloc}/{repo_path}.git"
|
||||
logger.info(f"Generated remote URL: https://{self.username}:****@{parsed.netloc}/{repo_path}.git")
|
||||
return url
|
||||
|
||||
class GitOperationsManager:
|
||||
def __init__(self, base_url: str, token: str, username: str = "claude-project-manager"):
|
||||
self.base_url = base_url
|
||||
self.credentials = GitCredentials(username=username, token=token)
|
||||
self.default_clone_dir = self._get_clone_directory()
|
||||
|
||||
def _get_clone_directory(self) -> Path:
|
||||
"""Get clone directory from settings or use default"""
|
||||
try:
|
||||
settings_file = Path.home() / ".claude_project_manager" / "ui_settings.json"
|
||||
if settings_file.exists():
|
||||
with open(settings_file, 'r') as f:
|
||||
settings = json.load(f)
|
||||
clone_dir = settings.get("gitea_clone_directory")
|
||||
if clone_dir:
|
||||
logger.info(f"Loaded clone directory from settings: {clone_dir}")
|
||||
return Path(clone_dir)
|
||||
else:
|
||||
logger.info("No gitea_clone_directory in settings")
|
||||
else:
|
||||
logger.info("Settings file does not exist")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not load clone directory from settings: {e}")
|
||||
|
||||
# Return default if not found in settings
|
||||
default_dir = Path.home() / "GiteaRepos"
|
||||
logger.info(f"Using default clone directory: {default_dir}")
|
||||
return default_dir
|
||||
|
||||
def select_directory(self, title: str = "Select Clone Directory") -> Optional[Path]:
|
||||
root = tk.Tk()
|
||||
root.withdraw()
|
||||
root.attributes('-topmost', True)
|
||||
|
||||
directory = filedialog.askdirectory(
|
||||
title=title,
|
||||
initialdir=str(self.default_clone_dir),
|
||||
mustexist=False
|
||||
)
|
||||
|
||||
root.destroy()
|
||||
|
||||
if directory:
|
||||
path = Path(directory)
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return path
|
||||
return None
|
||||
|
||||
def _run_git_command(self, cmd: List[str], cwd: Optional[Path] = None) -> Tuple[bool, str, str]:
|
||||
try:
|
||||
# Log the command being executed
|
||||
cmd_str = ' '.join(cmd)
|
||||
logger.info(f"Executing git command: {cmd_str}")
|
||||
if cwd:
|
||||
logger.debug(f"Working directory: {cwd}")
|
||||
|
||||
# Hide console window on Windows
|
||||
startupinfo = None
|
||||
if os.name == 'nt': # Windows
|
||||
startupinfo = subprocess.STARTUPINFO()
|
||||
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||
startupinfo.wShowWindow = subprocess.SW_HIDE
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
cwd=cwd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
startupinfo=startupinfo
|
||||
)
|
||||
|
||||
success = result.returncode == 0
|
||||
stdout = result.stdout.strip()
|
||||
stderr = result.stderr.strip()
|
||||
|
||||
if success:
|
||||
logger.debug(f"Git command successful: {cmd_str}")
|
||||
if stdout:
|
||||
logger.debug(f"Output: {stdout[:500]}...") # Log first 500 chars
|
||||
else:
|
||||
logger.error(f"Git command failed: {cmd_str}")
|
||||
logger.error(f"Error: {stderr}")
|
||||
|
||||
return success, stdout, stderr
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error(f"Git command timed out: {' '.join(cmd)}")
|
||||
return False, "", "Command timed out"
|
||||
except Exception as e:
|
||||
logger.error(f"Git command error: {e}")
|
||||
return False, "", str(e)
|
||||
|
||||
def clone_repository(self, owner: str, repo: str, clone_dir: Optional[Path] = None) -> Tuple[bool, Path]:
|
||||
if clone_dir is None:
|
||||
# Use default clone directory if none specified
|
||||
clone_dir = self.default_clone_dir
|
||||
logger.info(f"Using default clone directory: {clone_dir}")
|
||||
clone_dir.mkdir(parents=True, exist_ok=True)
|
||||
else:
|
||||
logger.info(f"Using provided clone directory: {clone_dir}")
|
||||
|
||||
repo_path = f"{owner}/{repo}"
|
||||
remote_url = self.credentials.get_remote_url(self.base_url, repo_path)
|
||||
local_path = clone_dir / repo
|
||||
|
||||
if local_path.exists():
|
||||
logger.warning(f"Repository already exists at {local_path}")
|
||||
return True, local_path # Return success if already exists
|
||||
|
||||
cmd = ["git", "clone", remote_url, str(local_path)]
|
||||
success, stdout, stderr = self._run_git_command(cmd)
|
||||
|
||||
if success:
|
||||
logger.info(f"Successfully cloned {repo} to {local_path}")
|
||||
self._configure_git_user(local_path)
|
||||
else:
|
||||
logger.error(f"Clone failed: {stderr}")
|
||||
|
||||
return success, local_path
|
||||
|
||||
def _configure_git_user(self, repo_path: Path) -> None:
|
||||
self._run_git_command(["git", "config", "user.name", "Claude Project Manager"], cwd=repo_path)
|
||||
self._run_git_command(["git", "config", "user.email", "claude@intelsight.de"], cwd=repo_path)
|
||||
|
||||
def fetch(self, repo_path: Path, remote: str = "origin") -> Tuple[bool, str]:
|
||||
cmd = ["git", "fetch", remote]
|
||||
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
|
||||
return success, stdout or stderr
|
||||
|
||||
def pull(self, repo_path: Path, remote: str = "origin", branch: Optional[str] = None) -> Tuple[bool, str]:
|
||||
cmd = ["git", "pull", remote]
|
||||
if branch:
|
||||
cmd.append(branch)
|
||||
|
||||
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
|
||||
return success, stdout or stderr
|
||||
|
||||
def add(self, repo_path: Path, files: List[str] = None) -> Tuple[bool, str]:
|
||||
if files is None:
|
||||
files = ["."]
|
||||
|
||||
cmd = ["git", "add"] + files
|
||||
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
|
||||
return success, stdout or stderr
|
||||
|
||||
def commit(self, repo_path: Path, message: str) -> Tuple[bool, str]:
|
||||
cmd = ["git", "commit", "-m", message]
|
||||
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
|
||||
|
||||
if not success and "nothing to commit" in stderr:
|
||||
return True, "Nothing to commit, working tree clean"
|
||||
|
||||
return success, stdout or stderr
|
||||
|
||||
def split_large_file(self, file_path: Path, chunk_size_mb: int = 45) -> List[Path]:
|
||||
"""Split a large file into smaller chunks"""
|
||||
chunk_size = chunk_size_mb * 1024 * 1024 # Convert to bytes
|
||||
chunks = []
|
||||
|
||||
try:
|
||||
file_size = file_path.stat().st_size
|
||||
with open(file_path, 'rb') as f:
|
||||
chunk_num = 0
|
||||
while True:
|
||||
chunk_data = f.read(chunk_size)
|
||||
if not chunk_data:
|
||||
break
|
||||
|
||||
chunk_path = file_path.parent / f"{file_path.stem}.part{chunk_num:03d}{file_path.suffix}"
|
||||
with open(chunk_path, 'wb') as chunk_file:
|
||||
chunk_file.write(chunk_data)
|
||||
chunks.append(chunk_path)
|
||||
chunk_num += 1
|
||||
|
||||
logger.info(f"Split {file_path.name} into {len(chunks)} chunks")
|
||||
return chunks
|
||||
except Exception as e:
|
||||
logger.error(f"Error splitting file: {e}")
|
||||
return []
|
||||
|
||||
def create_join_script(self, original_file: Path, chunks: List[Path]) -> Path:
|
||||
"""Create a script to rejoin file chunks"""
|
||||
script_path = original_file.parent / f"join_{original_file.stem}.bat"
|
||||
|
||||
with open(script_path, 'w') as f:
|
||||
f.write("@echo off\n")
|
||||
f.write(f"echo Joining {original_file.name}...\n")
|
||||
f.write(f"copy /b ")
|
||||
|
||||
# Add all chunks
|
||||
for i, chunk in enumerate(chunks):
|
||||
if i > 0:
|
||||
f.write("+")
|
||||
f.write(f'"{chunk.name}"')
|
||||
|
||||
f.write(f' "{original_file.name}"\n')
|
||||
f.write("echo Done!\n")
|
||||
f.write("pause\n")
|
||||
|
||||
# Also create a shell script for Unix
|
||||
sh_script_path = original_file.parent / f"join_{original_file.stem}.sh"
|
||||
with open(sh_script_path, 'w') as f:
|
||||
f.write("#!/bin/bash\n")
|
||||
f.write(f"echo 'Joining {original_file.name}...'\n")
|
||||
f.write("cat ")
|
||||
for chunk in chunks:
|
||||
f.write(f'"{chunk.name}" ')
|
||||
f.write(f'> "{original_file.name}"\n')
|
||||
f.write("echo 'Done!'\n")
|
||||
|
||||
return script_path
|
||||
|
||||
def check_large_files(self, repo_path: Path, size_limit_mb: int = 100) -> List[Tuple[str, int]]:
|
||||
"""Check for files larger than size_limit_mb in the repository"""
|
||||
large_files = []
|
||||
try:
|
||||
# Get list of all tracked files
|
||||
cmd = ["git", "ls-files", "-z"]
|
||||
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
|
||||
|
||||
if success and stdout:
|
||||
files = stdout.split('\0')
|
||||
for file in files:
|
||||
if file:
|
||||
file_path = repo_path / file
|
||||
if file_path.exists():
|
||||
size = file_path.stat().st_size
|
||||
if size > size_limit_mb * 1024 * 1024:
|
||||
large_files.append((file, size))
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking large files: {e}")
|
||||
|
||||
return large_files
|
||||
|
||||
def push(self, repo_path: Path, remote: str = "origin", branch: Optional[str] = None) -> Tuple[bool, str]:
|
||||
# Get current branch first
|
||||
branch_cmd = ["git", "branch", "--show-current"]
|
||||
branch_success, current_branch, _ = self._run_git_command(branch_cmd, cwd=repo_path)
|
||||
current_branch = current_branch.strip() if branch_success else ""
|
||||
|
||||
# First try regular push
|
||||
cmd = ["git", "push", remote]
|
||||
if branch:
|
||||
cmd.append(branch)
|
||||
elif current_branch == "master":
|
||||
# If we're on master and no branch specified, push to main
|
||||
cmd.extend(["HEAD:main"])
|
||||
|
||||
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
|
||||
|
||||
# If push failed due to no upstream or branch name mismatch, try with --set-upstream
|
||||
if not success and ("has no upstream branch" in stderr or "The current branch" in stderr or
|
||||
"--set-upstream" in stderr or "does not match" in stderr):
|
||||
if current_branch:
|
||||
# Retry with --set-upstream
|
||||
# If we're on master, push to main (common convention)
|
||||
remote_branch = "main" if current_branch == "master" else current_branch
|
||||
cmd = ["git", "push", "--set-upstream", remote, f"{current_branch}:{remote_branch}"]
|
||||
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
|
||||
|
||||
# Check for specific errors
|
||||
if not success:
|
||||
if "HTTP 413" in stderr or "Request Entity Too Large" in stderr:
|
||||
# Extract file info from error if possible
|
||||
import re
|
||||
file_match = re.search(r'/([^/]+)/(\d+)$', stderr)
|
||||
file_size = None
|
||||
if file_match:
|
||||
file_hash = file_match.group(1)
|
||||
file_size = int(file_match.group(2))
|
||||
|
||||
# Check for large files
|
||||
large_files = self.check_large_files(repo_path, 50) # Check for files > 50MB
|
||||
if large_files:
|
||||
error_msg = "Push fehlgeschlagen: Dateien zu groß für Gitea!\n\n"
|
||||
error_msg += "Folgende Dateien überschreiten das Limit:\n"
|
||||
for file, size in large_files[:5]: # Show first 5
|
||||
size_mb = size / (1024 * 1024)
|
||||
error_msg += f" - {file} ({size_mb:.1f} MB)\n"
|
||||
if len(large_files) > 5:
|
||||
error_msg += f" ... und {len(large_files) - 5} weitere\n"
|
||||
|
||||
if file_size:
|
||||
error_msg += f"\nGitea versucht eine {file_size / (1024*1024):.1f} MB große Datei hochzuladen!\n"
|
||||
|
||||
error_msg += "\nLösungen:\n"
|
||||
error_msg += "1. Große Dateien zur .gitignore hinzufügen\n"
|
||||
error_msg += "2. Git LFS verwenden (empfohlen für große Dateien)\n"
|
||||
error_msg += "3. Dateien komprimieren oder entfernen\n\n"
|
||||
error_msg += "HINWEIS: Git LFS muss auf dem Server aktiviert sein!"
|
||||
return False, error_msg
|
||||
else:
|
||||
return False, f"Push fehlgeschlagen (HTTP 413): Repository oder einzelne Commits zu groß"
|
||||
elif "Locking support detected" in stderr and "HTTP 413" not in stderr:
|
||||
# Just LFS warning, not an error
|
||||
return True, "Push erfolgreich (mit LFS-Warnung)"
|
||||
|
||||
# Check if push was successful even with warnings
|
||||
if success or "Everything up-to-date" in (stdout + stderr):
|
||||
return True, "Push erfolgreich"
|
||||
|
||||
return success, stdout or stderr
|
||||
|
||||
def status(self, repo_path: Path) -> Tuple[bool, str]:
|
||||
cmd = ["git", "status", "--porcelain"]
|
||||
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
|
||||
return success, stdout or stderr
|
||||
|
||||
def diff(self, repo_path: Path, staged: bool = False) -> Tuple[bool, str]:
|
||||
cmd = ["git", "diff"]
|
||||
if staged:
|
||||
cmd.append("--staged")
|
||||
|
||||
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
|
||||
return success, stdout or stderr
|
||||
|
||||
def branch(self, repo_path: Path, list_all: bool = False) -> Tuple[bool, str]:
|
||||
cmd = ["git", "branch"]
|
||||
if list_all:
|
||||
cmd.append("-a")
|
||||
|
||||
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
|
||||
return success, stdout or stderr
|
||||
|
||||
def checkout(self, repo_path: Path, branch: str, create: bool = False) -> Tuple[bool, str]:
|
||||
cmd = ["git", "checkout"]
|
||||
if create:
|
||||
cmd.append("-b")
|
||||
cmd.append(branch)
|
||||
|
||||
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
|
||||
return success, stdout or stderr
|
||||
|
||||
def merge(self, repo_path: Path, branch: str) -> Tuple[bool, str]:
|
||||
cmd = ["git", "merge", branch]
|
||||
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
|
||||
return success, stdout or stderr
|
||||
|
||||
def log(self, repo_path: Path, n: int = 10, oneline: bool = True) -> Tuple[bool, str]:
|
||||
cmd = ["git", "log", f"-{n}"]
|
||||
if oneline:
|
||||
cmd.append("--oneline")
|
||||
|
||||
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
|
||||
return success, stdout or stderr
|
||||
|
||||
def remote_add(self, repo_path: Path, name: str, url: str) -> Tuple[bool, str]:
|
||||
cmd = ["git", "remote", "add", name, url]
|
||||
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
|
||||
return success, stdout or stderr
|
||||
|
||||
def remote_remove(self, repo_path: Path, name: str) -> Tuple[bool, str]:
|
||||
cmd = ["git", "remote", "remove", name]
|
||||
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
|
||||
return success, stdout or stderr
|
||||
|
||||
def remote_list(self, repo_path: Path) -> Tuple[bool, str]:
|
||||
cmd = ["git", "remote", "-v"]
|
||||
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
|
||||
return success, stdout or stderr
|
||||
|
||||
def remote(self, repo_path: Path, verbose: bool = False) -> Tuple[bool, str]:
|
||||
"""Alias for remote_list for backward compatibility"""
|
||||
if verbose:
|
||||
return self.remote_list(repo_path)
|
||||
else:
|
||||
cmd = ["git", "remote"]
|
||||
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
|
||||
return success, stdout or stderr
|
||||
|
||||
def stash(self, repo_path: Path, message: Optional[str] = None) -> Tuple[bool, str]:
|
||||
cmd = ["git", "stash", "push"]
|
||||
if message:
|
||||
cmd.extend(["-m", message])
|
||||
|
||||
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
|
||||
return success, stdout or stderr
|
||||
|
||||
def stash_pop(self, repo_path: Path) -> Tuple[bool, str]:
|
||||
cmd = ["git", "stash", "pop"]
|
||||
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
|
||||
return success, stdout or stderr
|
||||
|
||||
def reset(self, repo_path: Path, hard: bool = False, commit: Optional[str] = None) -> Tuple[bool, str]:
|
||||
cmd = ["git", "reset"]
|
||||
if hard:
|
||||
cmd.append("--hard")
|
||||
if commit:
|
||||
cmd.append(commit)
|
||||
|
||||
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
|
||||
return success, stdout or stderr
|
||||
|
||||
def init_repository(self, repo_path: Path) -> Tuple[bool, str]:
|
||||
"""Initialize a new git repository"""
|
||||
cmd = ["git", "init"]
|
||||
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
|
||||
if success:
|
||||
self._configure_git_user(repo_path)
|
||||
# Check if .gitattributes exists with LFS entries
|
||||
gitattributes = repo_path / ".gitattributes"
|
||||
if gitattributes.exists():
|
||||
with open(gitattributes, 'r') as f:
|
||||
content = f.read()
|
||||
if 'filter=lfs' in content:
|
||||
logger.info("Git LFS entries found in .gitattributes")
|
||||
return success, stdout or stderr
|
||||
|
||||
def disable_lfs_for_push(self, repo_path: Path) -> Tuple[bool, str]:
|
||||
"""Temporarily disable LFS for push"""
|
||||
# Backup .gitattributes if it exists
|
||||
gitattributes = repo_path / ".gitattributes"
|
||||
backup_path = repo_path / ".gitattributes.backup"
|
||||
|
||||
if gitattributes.exists():
|
||||
import shutil
|
||||
shutil.copy2(gitattributes, backup_path)
|
||||
|
||||
# Remove LFS entries from .gitattributes
|
||||
with open(gitattributes, 'r') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
with open(gitattributes, 'w') as f:
|
||||
for line in lines:
|
||||
if 'filter=lfs' not in line:
|
||||
f.write(line)
|
||||
|
||||
return True, "LFS temporarily disabled"
|
||||
return True, "No LFS configuration found"
|
||||
|
||||
def restore_lfs_config(self, repo_path: Path) -> None:
|
||||
"""Restore LFS configuration"""
|
||||
backup_path = repo_path / ".gitattributes.backup"
|
||||
gitattributes = repo_path / ".gitattributes"
|
||||
|
||||
if backup_path.exists():
|
||||
import shutil
|
||||
shutil.move(backup_path, gitattributes)
|
||||
|
||||
def add_remote_to_existing_repo(self, repo_path: Path, owner: str, repo_name: str,
|
||||
remote_name: str = "origin") -> Tuple[bool, str]:
|
||||
"""Add Gitea remote to an existing local repository"""
|
||||
repo_path_str = f"{owner}/{repo_name}"
|
||||
remote_url = self.credentials.get_remote_url(self.base_url, repo_path_str)
|
||||
|
||||
# First check if remote already exists
|
||||
success, remotes, _ = self._run_git_command(["git", "remote"], cwd=repo_path)
|
||||
if success and remote_name in remotes.split('\n'):
|
||||
# Remove existing remote
|
||||
self.remote_remove(repo_path, remote_name)
|
||||
|
||||
# Add new remote
|
||||
return self.remote_add(repo_path, remote_name, remote_url)
|
||||
|
||||
def push_existing_repo_to_gitea(self, repo_path: Path, owner: str, repo_name: str,
|
||||
branch: str = "main", force: bool = False) -> Tuple[bool, str]:
|
||||
"""Push an existing local repository to Gitea"""
|
||||
logger.info(f"=== Starting push_existing_repo_to_gitea ===")
|
||||
logger.info(f"Repo path: {repo_path}")
|
||||
logger.info(f"Owner: {owner}")
|
||||
logger.info(f"Repo name: {repo_name}")
|
||||
logger.info(f"Target branch: {branch}")
|
||||
|
||||
# Create debug info file
|
||||
debug_file = repo_path / "gitea_push_debug.txt"
|
||||
debug_info = []
|
||||
debug_info.append(f"Push Debug Info - {datetime.now()}")
|
||||
debug_info.append(f"Repository: {repo_name}")
|
||||
debug_info.append(f"Owner: {owner}")
|
||||
debug_info.append(f"Path: {repo_path}")
|
||||
|
||||
# First check current branch
|
||||
success, current_branch_output, _ = self._run_git_command(
|
||||
["git", "branch", "--show-current"], cwd=repo_path
|
||||
)
|
||||
|
||||
current_branch = current_branch_output.strip() if success else ""
|
||||
debug_info.append(f"Current branch: {current_branch}")
|
||||
|
||||
# If no current branch (empty repo), use the specified branch
|
||||
if not current_branch:
|
||||
current_branch = branch
|
||||
|
||||
# Add the remote
|
||||
success, msg = self.add_remote_to_existing_repo(repo_path, owner, repo_name)
|
||||
if not success:
|
||||
error_msg = f"Failed to add remote: {msg}"
|
||||
debug_info.append(f"ERROR: {error_msg}")
|
||||
with open(debug_file, 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(debug_info))
|
||||
return False, error_msg
|
||||
|
||||
# Log the remote URL for debugging
|
||||
success, remotes, _ = self._run_git_command(["git", "remote", "-v"], cwd=repo_path)
|
||||
if success:
|
||||
logger.info(f"Git remotes after adding: {remotes}")
|
||||
debug_info.append(f"Git remotes:\n{remotes}")
|
||||
|
||||
# Check git status before push
|
||||
success, status, _ = self._run_git_command(["git", "status", "--porcelain"], cwd=repo_path)
|
||||
debug_info.append(f"Git status before push:\n{status if status else 'Clean'}")
|
||||
|
||||
# Push the current branch with upstream setting
|
||||
cmd = ["git", "push", "--set-upstream", "origin", f"{current_branch}:{branch}", "-v"]
|
||||
if force:
|
||||
cmd.insert(2, "--force")
|
||||
|
||||
logger.info(f"Executing push command: {' '.join(cmd)}")
|
||||
debug_info.append(f"Push command: {' '.join(cmd)}")
|
||||
|
||||
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
|
||||
|
||||
logger.info(f"Push command result - Success: {success}")
|
||||
logger.info(f"Push stdout: {stdout}")
|
||||
logger.info(f"Push stderr: {stderr}")
|
||||
|
||||
debug_info.append(f"Push result: {'Success' if success else 'Failed'}")
|
||||
debug_info.append(f"Push stdout:\n{stdout}")
|
||||
debug_info.append(f"Push stderr:\n{stderr}")
|
||||
|
||||
# Save debug info
|
||||
with open(debug_file, 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(debug_info))
|
||||
|
||||
# Check if push was successful even with warnings
|
||||
if success or "Locking support detected" in stderr:
|
||||
# Also check if objects were written
|
||||
if "objects" in stderr or "objects" in stdout or success:
|
||||
# Verify the push by checking the remote
|
||||
verify_cmd = ["git", "ls-remote", "origin", branch]
|
||||
verify_success, verify_out, _ = self._run_git_command(verify_cmd, cwd=repo_path)
|
||||
if verify_success and verify_out.strip():
|
||||
logger.info(f"Push verified - remote has branch {branch}")
|
||||
return True, f"Successfully pushed {current_branch} to {branch}\n\nDebug-Datei: {debug_file}"
|
||||
else:
|
||||
logger.warning(f"Push reported success but could not verify remote branch")
|
||||
return True, f"Push completed (verification pending)\n\nDebug-Datei: {debug_file}"
|
||||
|
||||
# Check for common push success indicators
|
||||
if any(indicator in (stdout + stderr).lower() for indicator in ["everything up-to-date", "branch", "->", "create mode", "writing objects"]):
|
||||
return True, f"Successfully pushed {current_branch} to {branch}\n\nDebug-Datei: {debug_file}"
|
||||
|
||||
return False, f"{stderr or stdout}\n\nDebug-Datei: {debug_file}"
|
||||
|
||||
def setup_lfs(self, repo_path: Path) -> Tuple[bool, str]:
|
||||
"""Setup Git LFS for the repository"""
|
||||
# First check if git-lfs is installed
|
||||
cmd = ["git", "lfs", "version"]
|
||||
success, stdout, stderr = self._run_git_command(cmd)
|
||||
|
||||
if not success:
|
||||
return False, "Git LFS ist nicht installiert. Bitte installieren Sie es von: https://git-lfs.github.com/"
|
||||
|
||||
# Install LFS hooks in the repository
|
||||
cmd = ["git", "lfs", "install"]
|
||||
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
|
||||
|
||||
if not success:
|
||||
return False, f"Git LFS Installation fehlgeschlagen: {stderr}"
|
||||
|
||||
# Configure LFS for the remote
|
||||
cmd = ["git", "config", "lfs.https://gitea-undso.intelsight.de/.locksverify", "true"]
|
||||
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
|
||||
|
||||
return True, "Git LFS wurde erfolgreich eingerichtet"
|
||||
|
||||
def track_with_lfs(self, repo_path: Path, patterns: List[str]) -> Tuple[bool, str]:
|
||||
"""Track file patterns with Git LFS"""
|
||||
results = []
|
||||
|
||||
for pattern in patterns:
|
||||
cmd = ["git", "lfs", "track", pattern]
|
||||
success, stdout, stderr = self._run_git_command(cmd, cwd=repo_path)
|
||||
|
||||
if success:
|
||||
results.append(f"✓ {pattern}")
|
||||
else:
|
||||
results.append(f"✗ {pattern}: {stderr}")
|
||||
|
||||
# Add .gitattributes to git
|
||||
cmd = ["git", "add", ".gitattributes"]
|
||||
self._run_git_command(cmd, cwd=repo_path)
|
||||
|
||||
return True, "LFS Tracking konfiguriert:\n" + "\n".join(results)
|
||||
|
||||
def migrate_to_lfs(self, repo_path: Path, file_paths: List[str]) -> Tuple[bool, str]:
|
||||
"""Migrate existing large files to Git LFS"""
|
||||
# First setup LFS
|
||||
success, msg = self.setup_lfs(repo_path)
|
||||
if not success:
|
||||
return False, msg
|
||||
|
||||
# Track the files
|
||||
patterns = []
|
||||
for file_path in file_paths:
|
||||
# Create pattern from file path
|
||||
if "/" in file_path:
|
||||
# Track specific file
|
||||
patterns.append(file_path)
|
||||
else:
|
||||
# Track by extension
|
||||
ext = Path(file_path).suffix
|
||||
if ext:
|
||||
patterns.append(f"*{ext}")
|
||||
|
||||
if patterns:
|
||||
success, msg = self.track_with_lfs(repo_path, patterns)
|
||||
if not success:
|
||||
return False, msg
|
||||
|
||||
# Remove files from git cache and re-add them
|
||||
for file_path in file_paths:
|
||||
# Remove from cache
|
||||
cmd = ["git", "rm", "--cached", file_path]
|
||||
self._run_git_command(cmd, cwd=repo_path)
|
||||
|
||||
# Re-add (will now use LFS)
|
||||
cmd = ["git", "add", file_path]
|
||||
self._run_git_command(cmd, cwd=repo_path)
|
||||
|
||||
return True, "Dateien wurden zu Git LFS migriert. Bitte committen Sie die Änderungen."
|
||||
|
||||
# Create global instance function
|
||||
def create_git_ops():
|
||||
"""Create a new GitOperationsManager instance with current config"""
|
||||
from src.gitea.gitea_client import gitea_config
|
||||
return GitOperationsManager(
|
||||
base_url=gitea_config.base_url,
|
||||
token=gitea_config.api_token,
|
||||
username=gitea_config.username
|
||||
)
|
||||
|
||||
# Global instance
|
||||
git_ops = None
|
||||
|
||||
def init_git_ops():
|
||||
"""Initialize the global git_ops instance"""
|
||||
global git_ops
|
||||
git_ops = create_git_ops()
|
||||
|
||||
# Initialize on import
|
||||
init_git_ops()
|
||||
238
src/gitea/gitea_client.py
Normale Datei
238
src/gitea/gitea_client.py
Normale Datei
@ -0,0 +1,238 @@
|
||||
import requests
|
||||
import json
|
||||
from typing import Dict, List, Optional, Any
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@dataclass
|
||||
class GiteaConfig:
|
||||
base_url: str = "https://gitea-undso.intelsight.de"
|
||||
api_token: str = "3b4a6ba1ade3f34640f3c85d2333b4a3a0627471"
|
||||
api_version: str = "v1"
|
||||
username: str = "StuXn3t"
|
||||
|
||||
def __post_init__(self):
|
||||
"""Load settings from config file if available"""
|
||||
self.load_from_settings()
|
||||
|
||||
def load_from_settings(self):
|
||||
"""Load Gitea settings from UI settings file"""
|
||||
try:
|
||||
settings_file = Path.home() / ".claude_project_manager" / "ui_settings.json"
|
||||
if settings_file.exists():
|
||||
with open(settings_file, 'r') as f:
|
||||
settings = json.load(f)
|
||||
|
||||
# Override with saved settings if available
|
||||
if "gitea_server_url" in settings and settings["gitea_server_url"]:
|
||||
self.base_url = settings["gitea_server_url"]
|
||||
if "gitea_api_token" in settings and settings["gitea_api_token"]:
|
||||
self.api_token = settings["gitea_api_token"]
|
||||
if "gitea_username" in settings and settings["gitea_username"]:
|
||||
self.username = settings["gitea_username"]
|
||||
|
||||
logger.info(f"Loaded Gitea settings from config file")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not load Gitea settings from config: {e}")
|
||||
|
||||
@property
|
||||
def api_url(self) -> str:
|
||||
return f"{self.base_url}/api/{self.api_version}"
|
||||
|
||||
class GiteaClient:
|
||||
def __init__(self, config: Optional[GiteaConfig] = None):
|
||||
self.config = config or GiteaConfig()
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({
|
||||
"Authorization": f"token {self.config.api_token}",
|
||||
"Content-Type": "application/json"
|
||||
})
|
||||
|
||||
def _request(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]:
|
||||
url = f"{self.config.api_url}/{endpoint}"
|
||||
try:
|
||||
response = self.session.request(method, url, **kwargs)
|
||||
response.raise_for_status()
|
||||
return response.json() if response.content else {}
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"API request failed: {e}")
|
||||
raise
|
||||
|
||||
def get_user_info(self) -> Dict[str, Any]:
|
||||
return self._request("GET", "user")
|
||||
|
||||
def list_user_organizations(self) -> List[Dict[str, Any]]:
|
||||
"""List all organizations the user is a member of"""
|
||||
return self._request("GET", "user/orgs")
|
||||
|
||||
def get_user_teams_in_org(self, org_name: str) -> List[Dict[str, Any]]:
|
||||
"""Get user's teams in a specific organization"""
|
||||
try:
|
||||
return self._request("GET", f"user/teams", params={"org": org_name})
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get teams for org {org_name}: {e}")
|
||||
return []
|
||||
|
||||
def list_repositories(self, page: int = 1, limit: int = 50) -> List[Dict[str, Any]]:
|
||||
params = {"page": page, "limit": limit}
|
||||
return self._request("GET", "user/repos", params=params)
|
||||
|
||||
def create_repository(self, name: str, description: str = "", private: bool = False,
|
||||
auto_init: bool = True, gitignores: str = "", license: str = "") -> Dict[str, Any]:
|
||||
data = {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"private": private,
|
||||
"auto_init": auto_init,
|
||||
"gitignores": gitignores,
|
||||
"license": license,
|
||||
"default_branch": "main" # Use main instead of master
|
||||
}
|
||||
return self._request("POST", "user/repos", json=data)
|
||||
|
||||
def delete_repository(self, owner: str, repo: str) -> None:
|
||||
self._request("DELETE", f"repos/{owner}/{repo}")
|
||||
|
||||
def get_repository(self, owner: str, repo: str) -> Dict[str, Any]:
|
||||
return self._request("GET", f"repos/{owner}/{repo}")
|
||||
|
||||
def fork_repository(self, owner: str, repo: str, organization: Optional[str] = None) -> Dict[str, Any]:
|
||||
data = {"organization": organization} if organization else {}
|
||||
return self._request("POST", f"repos/{owner}/{repo}/forks", json=data)
|
||||
|
||||
def list_issues(self, owner: str, repo: str, state: str = "open",
|
||||
labels: Optional[List[str]] = None, page: int = 1, limit: int = 50) -> List[Dict[str, Any]]:
|
||||
params = {
|
||||
"state": state,
|
||||
"page": page,
|
||||
"limit": limit
|
||||
}
|
||||
if labels:
|
||||
params["labels"] = ",".join(labels)
|
||||
return self._request("GET", f"repos/{owner}/{repo}/issues", params=params)
|
||||
|
||||
def create_issue(self, owner: str, repo: str, title: str, body: str = "",
|
||||
assignees: Optional[List[str]] = None, labels: Optional[List[int]] = None) -> Dict[str, Any]:
|
||||
data = {
|
||||
"title": title,
|
||||
"body": body,
|
||||
"assignees": assignees or [],
|
||||
"labels": labels or []
|
||||
}
|
||||
return self._request("POST", f"repos/{owner}/{repo}/issues", json=data)
|
||||
|
||||
def update_issue(self, owner: str, repo: str, index: int, **kwargs) -> Dict[str, Any]:
|
||||
return self._request("PATCH", f"repos/{owner}/{repo}/issues/{index}", json=kwargs)
|
||||
|
||||
def close_issue(self, owner: str, repo: str, index: int) -> Dict[str, Any]:
|
||||
return self.update_issue(owner, repo, index, state="closed")
|
||||
|
||||
def list_pull_requests(self, owner: str, repo: str, state: str = "open",
|
||||
page: int = 1, limit: int = 50) -> List[Dict[str, Any]]:
|
||||
params = {
|
||||
"state": state,
|
||||
"page": page,
|
||||
"limit": limit
|
||||
}
|
||||
return self._request("GET", f"repos/{owner}/{repo}/pulls", params=params)
|
||||
|
||||
def create_pull_request(self, owner: str, repo: str, title: str, head: str, base: str,
|
||||
body: str = "", assignees: Optional[List[str]] = None) -> Dict[str, Any]:
|
||||
data = {
|
||||
"title": title,
|
||||
"head": head,
|
||||
"base": base,
|
||||
"body": body,
|
||||
"assignees": assignees or []
|
||||
}
|
||||
return self._request("POST", f"repos/{owner}/{repo}/pulls", json=data)
|
||||
|
||||
def merge_pull_request(self, owner: str, repo: str, index: int,
|
||||
merge_style: str = "merge") -> Dict[str, Any]:
|
||||
data = {"Do": merge_style}
|
||||
return self._request("POST", f"repos/{owner}/{repo}/pulls/{index}/merge", json=data)
|
||||
|
||||
def list_branches(self, owner: str, repo: str) -> List[Dict[str, Any]]:
|
||||
return self._request("GET", f"repos/{owner}/{repo}/branches")
|
||||
|
||||
def create_branch(self, owner: str, repo: str, branch_name: str,
|
||||
old_branch_name: str = "main") -> Dict[str, Any]:
|
||||
data = {
|
||||
"new_branch_name": branch_name,
|
||||
"old_branch_name": old_branch_name
|
||||
}
|
||||
return self._request("POST", f"repos/{owner}/{repo}/branches", json=data)
|
||||
|
||||
def delete_branch(self, owner: str, repo: str, branch_name: str) -> None:
|
||||
self._request("DELETE", f"repos/{owner}/{repo}/branches/{branch_name}")
|
||||
|
||||
def list_releases(self, owner: str, repo: str, page: int = 1, limit: int = 50) -> List[Dict[str, Any]]:
|
||||
params = {"page": page, "limit": limit}
|
||||
return self._request("GET", f"repos/{owner}/{repo}/releases", params=params)
|
||||
|
||||
def create_release(self, owner: str, repo: str, tag_name: str, name: str,
|
||||
body: str = "", target: str = "main", draft: bool = False,
|
||||
prerelease: bool = False) -> Dict[str, Any]:
|
||||
data = {
|
||||
"tag_name": tag_name,
|
||||
"name": name,
|
||||
"body": body,
|
||||
"target_commitish": target,
|
||||
"draft": draft,
|
||||
"prerelease": prerelease
|
||||
}
|
||||
return self._request("POST", f"repos/{owner}/{repo}/releases", json=data)
|
||||
|
||||
def list_webhooks(self, owner: str, repo: str) -> List[Dict[str, Any]]:
|
||||
return self._request("GET", f"repos/{owner}/{repo}/hooks")
|
||||
|
||||
def create_webhook(self, owner: str, repo: str, url: str, events: List[str],
|
||||
active: bool = True) -> Dict[str, Any]:
|
||||
data = {
|
||||
"type": "gitea",
|
||||
"config": {
|
||||
"url": url,
|
||||
"content_type": "json"
|
||||
},
|
||||
"events": events,
|
||||
"active": active
|
||||
}
|
||||
return self._request("POST", f"repos/{owner}/{repo}/hooks", json=data)
|
||||
|
||||
def get_repository_contents(self, owner: str, repo: str, filepath: str = "",
|
||||
ref: str = "main") -> Dict[str, Any]:
|
||||
params = {"ref": ref}
|
||||
return self._request("GET", f"repos/{owner}/{repo}/contents/{filepath}", params=params)
|
||||
|
||||
def create_or_update_file(self, owner: str, repo: str, filepath: str, content: str,
|
||||
message: str, branch: str = "main", sha: Optional[str] = None) -> Dict[str, Any]:
|
||||
import base64
|
||||
data = {
|
||||
"content": base64.b64encode(content.encode()).decode(),
|
||||
"message": message,
|
||||
"branch": branch
|
||||
}
|
||||
if sha:
|
||||
data["sha"] = sha
|
||||
|
||||
return self._request("PUT", f"repos/{owner}/{repo}/contents/{filepath}", json=data)
|
||||
|
||||
def delete_file(self, owner: str, repo: str, filepath: str, message: str,
|
||||
sha: str, branch: str = "main") -> Dict[str, Any]:
|
||||
data = {
|
||||
"message": message,
|
||||
"sha": sha,
|
||||
"branch": branch
|
||||
}
|
||||
return self._request("DELETE", f"repos/{owner}/{repo}/contents/{filepath}", json=data)
|
||||
|
||||
# Global config instance
|
||||
gitea_config = GiteaConfig()
|
||||
|
||||
# Default client instance
|
||||
client = GiteaClient(gitea_config)
|
||||
509
src/gitea/gitea_ui.py
Normale Datei
509
src/gitea/gitea_ui.py
Normale Datei
@ -0,0 +1,509 @@
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox, scrolledtext
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any, List
|
||||
from .repository_manager import RepositoryManager
|
||||
from .issue_pr_manager import IssuePRManager
|
||||
|
||||
class GiteaIntegrationUI:
|
||||
def __init__(self, parent_frame: tk.Frame):
|
||||
self.parent = parent_frame
|
||||
self.repo_manager = RepositoryManager()
|
||||
self.issue_pr_manager = IssuePRManager(self.repo_manager.client)
|
||||
self.selected_repo = None
|
||||
self.selected_repo_path = None
|
||||
|
||||
self.setup_ui()
|
||||
self.refresh_repositories()
|
||||
|
||||
def setup_ui(self):
|
||||
main_paned = ttk.PanedWindow(self.parent, orient=tk.HORIZONTAL)
|
||||
main_paned.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||
|
||||
left_frame = ttk.Frame(main_paned)
|
||||
self.setup_left_panel(left_frame)
|
||||
main_paned.add(left_frame, weight=1)
|
||||
|
||||
right_frame = ttk.Frame(main_paned)
|
||||
self.setup_right_panel(right_frame)
|
||||
main_paned.add(right_frame, weight=2)
|
||||
|
||||
def setup_left_panel(self, parent):
|
||||
ttk.Label(parent, text="Repositories", font=("Arial", 12, "bold")).pack(pady=5)
|
||||
|
||||
button_frame = ttk.Frame(parent)
|
||||
button_frame.pack(fill=tk.X, padx=5)
|
||||
|
||||
ttk.Button(button_frame, text="🔄 Refresh", command=self.refresh_repositories).pack(side=tk.LEFT, padx=2)
|
||||
ttk.Button(button_frame, text="➕ New", command=self.create_repository_dialog).pack(side=tk.LEFT, padx=2)
|
||||
ttk.Button(button_frame, text="🗑️ Delete", command=self.delete_repository).pack(side=tk.LEFT, padx=2)
|
||||
|
||||
list_frame = ttk.Frame(parent)
|
||||
list_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||
|
||||
scrollbar = ttk.Scrollbar(list_frame)
|
||||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
self.repo_listbox = tk.Listbox(list_frame, yscrollcommand=scrollbar.set)
|
||||
self.repo_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
self.repo_listbox.bind('<<ListboxSelect>>', self.on_repo_select)
|
||||
|
||||
scrollbar.config(command=self.repo_listbox.yview)
|
||||
|
||||
def setup_right_panel(self, parent):
|
||||
self.notebook = ttk.Notebook(parent)
|
||||
self.notebook.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
self.git_tab = ttk.Frame(self.notebook)
|
||||
self.setup_git_operations_tab(self.git_tab)
|
||||
self.notebook.add(self.git_tab, text="Git Operations")
|
||||
|
||||
self.issues_tab = ttk.Frame(self.notebook)
|
||||
self.setup_issues_tab(self.issues_tab)
|
||||
self.notebook.add(self.issues_tab, text="Issues")
|
||||
|
||||
self.pr_tab = ttk.Frame(self.notebook)
|
||||
self.setup_pr_tab(self.pr_tab)
|
||||
self.notebook.add(self.pr_tab, text="Pull Requests")
|
||||
|
||||
self.info_tab = ttk.Frame(self.notebook)
|
||||
self.setup_info_tab(self.info_tab)
|
||||
self.notebook.add(self.info_tab, text="Repository Info")
|
||||
|
||||
def setup_git_operations_tab(self, parent):
|
||||
button_frame = ttk.LabelFrame(parent, text="Repository Actions", padding=10)
|
||||
button_frame.pack(fill=tk.X, padx=5, pady=5)
|
||||
|
||||
ttk.Button(button_frame, text="📥 Clone", command=self.clone_repository).grid(row=0, column=0, padx=5, pady=2)
|
||||
ttk.Button(button_frame, text="🔄 Fetch", command=self.fetch_repository).grid(row=0, column=1, padx=5, pady=2)
|
||||
ttk.Button(button_frame, text="⬇️ Pull", command=self.pull_repository).grid(row=0, column=2, padx=5, pady=2)
|
||||
ttk.Button(button_frame, text="📊 Status", command=self.show_status).grid(row=0, column=3, padx=5, pady=2)
|
||||
|
||||
ttk.Button(button_frame, text="➕ Add All", command=self.add_all_files).grid(row=1, column=0, padx=5, pady=2)
|
||||
ttk.Button(button_frame, text="💾 Commit", command=self.commit_dialog).grid(row=1, column=1, padx=5, pady=2)
|
||||
ttk.Button(button_frame, text="⬆️ Push", command=self.push_repository).grid(row=1, column=2, padx=5, pady=2)
|
||||
ttk.Button(button_frame, text="🌿 Branches", command=self.show_branches).grid(row=1, column=3, padx=5, pady=2)
|
||||
|
||||
output_frame = ttk.LabelFrame(parent, text="Output", padding=10)
|
||||
output_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||
|
||||
self.git_output = scrolledtext.ScrolledText(output_frame, height=20, wrap=tk.WORD)
|
||||
self.git_output.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
def setup_issues_tab(self, parent):
|
||||
button_frame = ttk.Frame(parent)
|
||||
button_frame.pack(fill=tk.X, padx=5, pady=5)
|
||||
|
||||
ttk.Button(button_frame, text="🔄 Refresh", command=self.refresh_issues).pack(side=tk.LEFT, padx=2)
|
||||
ttk.Button(button_frame, text="➕ New Issue", command=self.create_issue_dialog).pack(side=tk.LEFT, padx=2)
|
||||
ttk.Button(button_frame, text="✏️ Edit", command=self.edit_issue).pack(side=tk.LEFT, padx=2)
|
||||
ttk.Button(button_frame, text="✅ Close", command=self.close_issue).pack(side=tk.LEFT, padx=2)
|
||||
|
||||
columns = ('ID', 'Title', 'State', 'Author', 'Created')
|
||||
self.issues_tree = ttk.Treeview(parent, columns=columns, show='tree headings')
|
||||
|
||||
for col in columns:
|
||||
self.issues_tree.heading(col, text=col)
|
||||
self.issues_tree.column(col, width=100)
|
||||
|
||||
self.issues_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||
|
||||
scrollbar = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.issues_tree.yview)
|
||||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
self.issues_tree.configure(yscrollcommand=scrollbar.set)
|
||||
|
||||
def setup_pr_tab(self, parent):
|
||||
button_frame = ttk.Frame(parent)
|
||||
button_frame.pack(fill=tk.X, padx=5, pady=5)
|
||||
|
||||
ttk.Button(button_frame, text="🔄 Refresh", command=self.refresh_prs).pack(side=tk.LEFT, padx=2)
|
||||
ttk.Button(button_frame, text="➕ New PR", command=self.create_pr_dialog).pack(side=tk.LEFT, padx=2)
|
||||
ttk.Button(button_frame, text="🔀 Merge", command=self.merge_pr).pack(side=tk.LEFT, padx=2)
|
||||
ttk.Button(button_frame, text="❌ Close", command=self.close_pr).pack(side=tk.LEFT, padx=2)
|
||||
|
||||
columns = ('ID', 'Title', 'State', 'Author', 'Head', 'Base')
|
||||
self.pr_tree = ttk.Treeview(parent, columns=columns, show='tree headings')
|
||||
|
||||
for col in columns:
|
||||
self.pr_tree.heading(col, text=col)
|
||||
self.pr_tree.column(col, width=100)
|
||||
|
||||
self.pr_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||
|
||||
scrollbar = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.pr_tree.yview)
|
||||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
self.pr_tree.configure(yscrollcommand=scrollbar.set)
|
||||
|
||||
def setup_info_tab(self, parent):
|
||||
self.info_text = scrolledtext.ScrolledText(parent, height=20, wrap=tk.WORD)
|
||||
self.info_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||
|
||||
def refresh_repositories(self):
|
||||
def fetch():
|
||||
try:
|
||||
repos = self.repo_manager.list_all_repositories()
|
||||
self.parent.after(0, lambda: self.update_repo_list(repos))
|
||||
except Exception as e:
|
||||
self.parent.after(0, lambda: messagebox.showerror("Error", f"Failed to fetch repositories: {e}"))
|
||||
|
||||
threading.Thread(target=fetch, daemon=True).start()
|
||||
|
||||
def update_repo_list(self, repos):
|
||||
self.repo_listbox.delete(0, tk.END)
|
||||
for repo in repos:
|
||||
display_name = f"{repo['name']} {'🔒' if repo['private'] else '🌐'}"
|
||||
self.repo_listbox.insert(tk.END, display_name)
|
||||
self.repositories = repos
|
||||
|
||||
def on_repo_select(self, event):
|
||||
selection = self.repo_listbox.curselection()
|
||||
if selection:
|
||||
index = selection[0]
|
||||
self.selected_repo = self.repositories[index]
|
||||
self.update_repo_info()
|
||||
self.refresh_issues()
|
||||
self.refresh_prs()
|
||||
|
||||
def update_repo_info(self):
|
||||
if not self.selected_repo:
|
||||
return
|
||||
|
||||
info = f"Repository: {self.selected_repo['name']}\n"
|
||||
info += f"Description: {self.selected_repo.get('description', 'No description')}\n"
|
||||
info += f"Private: {'Yes' if self.selected_repo['private'] else 'No'}\n"
|
||||
info += f"Default Branch: {self.selected_repo.get('default_branch', 'main')}\n"
|
||||
info += f"Clone URL: {self.selected_repo['clone_url']}\n"
|
||||
info += f"Created: {self.selected_repo['created_at']}\n"
|
||||
info += f"Updated: {self.selected_repo['updated_at']}\n"
|
||||
|
||||
self.info_text.delete(1.0, tk.END)
|
||||
self.info_text.insert(1.0, info)
|
||||
|
||||
def clone_repository(self):
|
||||
if not self.selected_repo:
|
||||
messagebox.showwarning("Warning", "Please select a repository")
|
||||
return
|
||||
|
||||
def clone():
|
||||
try:
|
||||
success, path = self.repo_manager.clone_repository(self.selected_repo['name'])
|
||||
if success:
|
||||
self.selected_repo_path = path
|
||||
self.parent.after(0, lambda: self.git_output.insert(tk.END, f"Repository cloned to: {path}\n"))
|
||||
else:
|
||||
self.parent.after(0, lambda: self.git_output.insert(tk.END, f"Failed to clone repository\n"))
|
||||
except Exception as e:
|
||||
self.parent.after(0, lambda: messagebox.showerror("Error", f"Clone failed: {e}"))
|
||||
|
||||
threading.Thread(target=clone, daemon=True).start()
|
||||
|
||||
def fetch_repository(self):
|
||||
if not self.selected_repo_path:
|
||||
messagebox.showwarning("Warning", "Please clone the repository first")
|
||||
return
|
||||
|
||||
success, result = self.repo_manager.git_ops.fetch(self.selected_repo_path)
|
||||
self.git_output.insert(tk.END, f"Fetch: {result}\n")
|
||||
|
||||
def pull_repository(self):
|
||||
if not self.selected_repo_path:
|
||||
messagebox.showwarning("Warning", "Please clone the repository first")
|
||||
return
|
||||
|
||||
success, result = self.repo_manager.git_ops.pull(self.selected_repo_path)
|
||||
self.git_output.insert(tk.END, f"Pull: {result}\n")
|
||||
|
||||
def push_repository(self):
|
||||
if not self.selected_repo_path:
|
||||
messagebox.showwarning("Warning", "Please clone the repository first")
|
||||
return
|
||||
|
||||
success, result = self.repo_manager.git_ops.push(self.selected_repo_path)
|
||||
self.git_output.insert(tk.END, f"Push: {result}\n")
|
||||
|
||||
def show_status(self):
|
||||
if not self.selected_repo_path:
|
||||
messagebox.showwarning("Warning", "Please clone the repository first")
|
||||
return
|
||||
|
||||
status = self.repo_manager.get_repository_status(self.selected_repo_path)
|
||||
self.git_output.insert(tk.END, f"\n--- Repository Status ---\n")
|
||||
self.git_output.insert(tk.END, f"Current Branch: {status['current_branch']}\n")
|
||||
self.git_output.insert(tk.END, f"Has Changes: {status['has_changes']}\n")
|
||||
self.git_output.insert(tk.END, f"Status:\n{status['status']}\n")
|
||||
self.git_output.insert(tk.END, f"Remotes:\n{status['remotes']}\n")
|
||||
|
||||
def add_all_files(self):
|
||||
if not self.selected_repo_path:
|
||||
messagebox.showwarning("Warning", "Please clone the repository first")
|
||||
return
|
||||
|
||||
success, result = self.repo_manager.git_ops.add(self.selected_repo_path)
|
||||
self.git_output.insert(tk.END, f"Add all: {result}\n")
|
||||
|
||||
def commit_dialog(self):
|
||||
if not self.selected_repo_path:
|
||||
messagebox.showwarning("Warning", "Please clone the repository first")
|
||||
return
|
||||
|
||||
dialog = tk.Toplevel(self.parent)
|
||||
dialog.title("Commit Changes")
|
||||
dialog.geometry("400x200")
|
||||
|
||||
ttk.Label(dialog, text="Commit Message:").pack(pady=5)
|
||||
|
||||
message_text = tk.Text(dialog, height=5, width=50)
|
||||
message_text.pack(padx=10, pady=5)
|
||||
|
||||
def do_commit():
|
||||
message = message_text.get(1.0, tk.END).strip()
|
||||
if not message:
|
||||
messagebox.showwarning("Warning", "Please enter a commit message")
|
||||
return
|
||||
|
||||
success, result = self.repo_manager.git_ops.commit(self.selected_repo_path, message)
|
||||
self.git_output.insert(tk.END, f"Commit: {result}\n")
|
||||
dialog.destroy()
|
||||
|
||||
ttk.Button(dialog, text="Commit", command=do_commit).pack(pady=10)
|
||||
|
||||
def show_branches(self):
|
||||
if not self.selected_repo_path:
|
||||
messagebox.showwarning("Warning", "Please clone the repository first")
|
||||
return
|
||||
|
||||
success, branches = self.repo_manager.git_ops.branch(self.selected_repo_path, list_all=True)
|
||||
self.git_output.insert(tk.END, f"\n--- Branches ---\n{branches}\n")
|
||||
|
||||
def create_repository_dialog(self):
|
||||
dialog = tk.Toplevel(self.parent)
|
||||
dialog.title("Create New Repository")
|
||||
dialog.geometry("400x300")
|
||||
|
||||
ttk.Label(dialog, text="Repository Name:").grid(row=0, column=0, sticky=tk.W, padx=10, pady=5)
|
||||
name_entry = ttk.Entry(dialog, width=30)
|
||||
name_entry.grid(row=0, column=1, padx=10, pady=5)
|
||||
|
||||
ttk.Label(dialog, text="Description:").grid(row=1, column=0, sticky=tk.W, padx=10, pady=5)
|
||||
desc_text = tk.Text(dialog, height=3, width=30)
|
||||
desc_text.grid(row=1, column=1, padx=10, pady=5)
|
||||
|
||||
private_var = tk.BooleanVar()
|
||||
ttk.Checkbutton(dialog, text="Private Repository", variable=private_var).grid(row=2, column=1, sticky=tk.W, padx=10, pady=5)
|
||||
|
||||
auto_init_var = tk.BooleanVar(value=True)
|
||||
ttk.Checkbutton(dialog, text="Initialize with README", variable=auto_init_var).grid(row=3, column=1, sticky=tk.W, padx=10, pady=5)
|
||||
|
||||
def create():
|
||||
name = name_entry.get().strip()
|
||||
desc = desc_text.get(1.0, tk.END).strip()
|
||||
|
||||
if not name:
|
||||
messagebox.showwarning("Warning", "Please enter a repository name")
|
||||
return
|
||||
|
||||
try:
|
||||
self.repo_manager.create_repository(
|
||||
name, desc, private_var.get(), auto_init_var.get()
|
||||
)
|
||||
messagebox.showinfo("Success", f"Repository '{name}' created successfully")
|
||||
dialog.destroy()
|
||||
self.refresh_repositories()
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Failed to create repository: {e}")
|
||||
|
||||
ttk.Button(dialog, text="Create", command=create).grid(row=4, column=1, pady=20)
|
||||
|
||||
def delete_repository(self):
|
||||
if not self.selected_repo:
|
||||
messagebox.showwarning("Warning", "Please select a repository")
|
||||
return
|
||||
|
||||
if messagebox.askyesno("Confirm", f"Are you sure you want to delete '{self.selected_repo['name']}'?"):
|
||||
if self.repo_manager.delete_repository(self.selected_repo['name']):
|
||||
messagebox.showinfo("Success", "Repository deleted successfully")
|
||||
self.refresh_repositories()
|
||||
|
||||
def refresh_issues(self):
|
||||
if not self.selected_repo:
|
||||
return
|
||||
|
||||
self.issues_tree.delete(*self.issues_tree.get_children())
|
||||
|
||||
try:
|
||||
manager = self.issue_pr_manager.get_issue_manager(self.selected_repo['name'])
|
||||
issues = manager.list_issues()
|
||||
|
||||
for issue in issues:
|
||||
self.issues_tree.insert('', tk.END, values=(
|
||||
issue['number'],
|
||||
issue['title'],
|
||||
issue['state'],
|
||||
issue['user']['username'],
|
||||
issue['created_at'][:10]
|
||||
))
|
||||
except Exception as e:
|
||||
print(f"Error fetching issues: {e}")
|
||||
|
||||
def refresh_prs(self):
|
||||
if not self.selected_repo:
|
||||
return
|
||||
|
||||
self.pr_tree.delete(*self.pr_tree.get_children())
|
||||
|
||||
try:
|
||||
manager = self.issue_pr_manager.get_pr_manager(self.selected_repo['name'])
|
||||
prs = manager.list_pull_requests()
|
||||
|
||||
for pr in prs:
|
||||
self.pr_tree.insert('', tk.END, values=(
|
||||
pr['number'],
|
||||
pr['title'],
|
||||
pr['state'],
|
||||
pr['user']['username'],
|
||||
pr['head']['ref'],
|
||||
pr['base']['ref']
|
||||
))
|
||||
except Exception as e:
|
||||
print(f"Error fetching PRs: {e}")
|
||||
|
||||
def create_issue_dialog(self):
|
||||
if not self.selected_repo:
|
||||
messagebox.showwarning("Warning", "Please select a repository")
|
||||
return
|
||||
|
||||
dialog = tk.Toplevel(self.parent)
|
||||
dialog.title("Create New Issue")
|
||||
dialog.geometry("500x400")
|
||||
|
||||
ttk.Label(dialog, text="Title:").grid(row=0, column=0, sticky=tk.W, padx=10, pady=5)
|
||||
title_entry = ttk.Entry(dialog, width=50)
|
||||
title_entry.grid(row=0, column=1, padx=10, pady=5)
|
||||
|
||||
ttk.Label(dialog, text="Description:").grid(row=1, column=0, sticky=tk.NW, padx=10, pady=5)
|
||||
desc_text = tk.Text(dialog, height=10, width=50)
|
||||
desc_text.grid(row=1, column=1, padx=10, pady=5)
|
||||
|
||||
def create():
|
||||
title = title_entry.get().strip()
|
||||
body = desc_text.get(1.0, tk.END).strip()
|
||||
|
||||
if not title:
|
||||
messagebox.showwarning("Warning", "Please enter a title")
|
||||
return
|
||||
|
||||
try:
|
||||
manager = self.issue_pr_manager.get_issue_manager(self.selected_repo['name'])
|
||||
manager.create_issue(title, body)
|
||||
messagebox.showinfo("Success", "Issue created successfully")
|
||||
dialog.destroy()
|
||||
self.refresh_issues()
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Failed to create issue: {e}")
|
||||
|
||||
ttk.Button(dialog, text="Create", command=create).grid(row=2, column=1, pady=20)
|
||||
|
||||
def edit_issue(self):
|
||||
messagebox.showinfo("Info", "Edit issue functionality coming soon!")
|
||||
|
||||
def close_issue(self):
|
||||
selection = self.issues_tree.selection()
|
||||
if not selection:
|
||||
messagebox.showwarning("Warning", "Please select an issue")
|
||||
return
|
||||
|
||||
item = self.issues_tree.item(selection[0])
|
||||
issue_id = item['values'][0]
|
||||
|
||||
if messagebox.askyesno("Confirm", f"Close issue #{issue_id}?"):
|
||||
try:
|
||||
manager = self.issue_pr_manager.get_issue_manager(self.selected_repo['name'])
|
||||
manager.close_issue(issue_id)
|
||||
messagebox.showinfo("Success", "Issue closed successfully")
|
||||
self.refresh_issues()
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Failed to close issue: {e}")
|
||||
|
||||
def create_pr_dialog(self):
|
||||
if not self.selected_repo:
|
||||
messagebox.showwarning("Warning", "Please select a repository")
|
||||
return
|
||||
|
||||
dialog = tk.Toplevel(self.parent)
|
||||
dialog.title("Create Pull Request")
|
||||
dialog.geometry("500x500")
|
||||
|
||||
ttk.Label(dialog, text="Title:").grid(row=0, column=0, sticky=tk.W, padx=10, pady=5)
|
||||
title_entry = ttk.Entry(dialog, width=50)
|
||||
title_entry.grid(row=0, column=1, padx=10, pady=5)
|
||||
|
||||
ttk.Label(dialog, text="Head Branch:").grid(row=1, column=0, sticky=tk.W, padx=10, pady=5)
|
||||
head_entry = ttk.Entry(dialog, width=50)
|
||||
head_entry.grid(row=1, column=1, padx=10, pady=5)
|
||||
|
||||
ttk.Label(dialog, text="Base Branch:").grid(row=2, column=0, sticky=tk.W, padx=10, pady=5)
|
||||
base_entry = ttk.Entry(dialog, width=50, )
|
||||
base_entry.insert(0, "main")
|
||||
base_entry.grid(row=2, column=1, padx=10, pady=5)
|
||||
|
||||
ttk.Label(dialog, text="Description:").grid(row=3, column=0, sticky=tk.NW, padx=10, pady=5)
|
||||
desc_text = tk.Text(dialog, height=10, width=50)
|
||||
desc_text.grid(row=3, column=1, padx=10, pady=5)
|
||||
|
||||
def create():
|
||||
title = title_entry.get().strip()
|
||||
head = head_entry.get().strip()
|
||||
base = base_entry.get().strip()
|
||||
body = desc_text.get(1.0, tk.END).strip()
|
||||
|
||||
if not all([title, head, base]):
|
||||
messagebox.showwarning("Warning", "Please fill all required fields")
|
||||
return
|
||||
|
||||
try:
|
||||
manager = self.issue_pr_manager.get_pr_manager(self.selected_repo['name'])
|
||||
manager.create_pull_request(title, head, base, body)
|
||||
messagebox.showinfo("Success", "Pull request created successfully")
|
||||
dialog.destroy()
|
||||
self.refresh_prs()
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Failed to create PR: {e}")
|
||||
|
||||
ttk.Button(dialog, text="Create", command=create).grid(row=4, column=1, pady=20)
|
||||
|
||||
def merge_pr(self):
|
||||
selection = self.pr_tree.selection()
|
||||
if not selection:
|
||||
messagebox.showwarning("Warning", "Please select a pull request")
|
||||
return
|
||||
|
||||
item = self.pr_tree.item(selection[0])
|
||||
pr_id = item['values'][0]
|
||||
|
||||
if messagebox.askyesno("Confirm", f"Merge pull request #{pr_id}?"):
|
||||
try:
|
||||
manager = self.issue_pr_manager.get_pr_manager(self.selected_repo['name'])
|
||||
manager.merge_pull_request(pr_id)
|
||||
messagebox.showinfo("Success", "Pull request merged successfully")
|
||||
self.refresh_prs()
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Failed to merge PR: {e}")
|
||||
|
||||
def close_pr(self):
|
||||
selection = self.pr_tree.selection()
|
||||
if not selection:
|
||||
messagebox.showwarning("Warning", "Please select a pull request")
|
||||
return
|
||||
|
||||
item = self.pr_tree.item(selection[0])
|
||||
pr_id = item['values'][0]
|
||||
|
||||
if messagebox.askyesno("Confirm", f"Close pull request #{pr_id}?"):
|
||||
try:
|
||||
manager = self.issue_pr_manager.get_pr_manager(self.selected_repo['name'])
|
||||
manager.close_pull_request(pr_id)
|
||||
messagebox.showinfo("Success", "Pull request closed successfully")
|
||||
self.refresh_prs()
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Failed to close PR: {e}")
|
||||
984
src/gitea/gitea_ui_ctk.py
Normale Datei
984
src/gitea/gitea_ui_ctk.py
Normale Datei
@ -0,0 +1,984 @@
|
||||
import customtkinter as ctk
|
||||
from tkinter import messagebox, filedialog
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any, List
|
||||
from .repository_manager import RepositoryManager
|
||||
from .issue_pr_manager import IssuePRManager
|
||||
|
||||
class GiteaIntegrationUI:
|
||||
def __init__(self, parent_window):
|
||||
self.window = parent_window
|
||||
self.repo_manager = RepositoryManager()
|
||||
self.issue_pr_manager = IssuePRManager(self.repo_manager.client)
|
||||
self.selected_repo = None
|
||||
self.selected_repo_path = None
|
||||
|
||||
# Import styles from main app
|
||||
from gui.styles import COLORS, FONTS
|
||||
self.COLORS = COLORS
|
||||
self.FONTS = FONTS
|
||||
|
||||
self.setup_ui()
|
||||
self.refresh_repositories()
|
||||
|
||||
def setup_ui(self):
|
||||
# Main container
|
||||
main_frame = ctk.CTkFrame(self.window, fg_color=self.COLORS['bg_primary'])
|
||||
main_frame.pack(fill="both", expand=True, padx=10, pady=10)
|
||||
|
||||
# Left panel - Repository list
|
||||
left_frame = ctk.CTkFrame(main_frame, width=300, fg_color=self.COLORS['bg_secondary'])
|
||||
left_frame.pack(side="left", fill="y", padx=(0, 10))
|
||||
left_frame.pack_propagate(False)
|
||||
|
||||
# Repository list header
|
||||
header_frame = ctk.CTkFrame(left_frame, fg_color="transparent", height=50)
|
||||
header_frame.pack(fill="x", padx=10, pady=(10, 0))
|
||||
header_frame.pack_propagate(False)
|
||||
|
||||
ctk.CTkLabel(
|
||||
header_frame,
|
||||
text="Repositories",
|
||||
font=self.FONTS['title'],
|
||||
text_color=self.COLORS['text_primary']
|
||||
).pack(side="left", pady=10)
|
||||
|
||||
# Buttons
|
||||
button_frame = ctk.CTkFrame(left_frame, fg_color="transparent")
|
||||
button_frame.pack(fill="x", padx=10, pady=5)
|
||||
|
||||
ctk.CTkButton(
|
||||
button_frame,
|
||||
text="🔄 Refresh",
|
||||
command=self.refresh_repositories,
|
||||
width=100,
|
||||
height=30,
|
||||
fg_color=self.COLORS['accent_primary'],
|
||||
hover_color=self.COLORS['accent_hover']
|
||||
).pack(side="left", padx=2)
|
||||
|
||||
ctk.CTkButton(
|
||||
button_frame,
|
||||
text="➕ New Repo",
|
||||
command=self.create_repository_dialog,
|
||||
width=100,
|
||||
height=30,
|
||||
fg_color=self.COLORS['accent_success'],
|
||||
hover_color=self.COLORS['accent_hover']
|
||||
).pack(side="left", padx=2)
|
||||
|
||||
ctk.CTkButton(
|
||||
button_frame,
|
||||
text="🗑️ Delete",
|
||||
command=self.delete_repository,
|
||||
width=100,
|
||||
height=30,
|
||||
fg_color=self.COLORS['accent_error'],
|
||||
hover_color=self.COLORS['accent_hover']
|
||||
).pack(side="left", padx=2)
|
||||
|
||||
ctk.CTkButton(
|
||||
button_frame,
|
||||
text="📤 Push Local",
|
||||
command=self.push_local_repo_dialog,
|
||||
width=100,
|
||||
height=30,
|
||||
fg_color=self.COLORS['accent_warning'],
|
||||
hover_color=self.COLORS['accent_hover']
|
||||
).pack(side="left", padx=2)
|
||||
|
||||
# Repository list
|
||||
self.repo_list_frame = ctk.CTkScrollableFrame(
|
||||
left_frame,
|
||||
fg_color=self.COLORS['bg_primary']
|
||||
)
|
||||
self.repo_list_frame.pack(fill="both", expand=True, padx=10, pady=(0, 10))
|
||||
|
||||
# Right panel - Tab view
|
||||
right_frame = ctk.CTkFrame(main_frame, fg_color=self.COLORS['bg_secondary'])
|
||||
right_frame.pack(side="right", fill="both", expand=True)
|
||||
|
||||
self.tabview = ctk.CTkTabview(right_frame, fg_color=self.COLORS['bg_secondary'])
|
||||
self.tabview.pack(fill="both", expand=True, padx=10, pady=10)
|
||||
|
||||
# Create tabs
|
||||
self.tabview.add("Git Operations")
|
||||
self.tabview.add("Issues")
|
||||
self.tabview.add("Pull Requests")
|
||||
self.tabview.add("Repository Info")
|
||||
|
||||
self.setup_git_operations_tab()
|
||||
self.setup_issues_tab()
|
||||
self.setup_pr_tab()
|
||||
self.setup_info_tab()
|
||||
|
||||
def setup_git_operations_tab(self):
|
||||
tab = self.tabview.tab("Git Operations")
|
||||
|
||||
# Operations buttons
|
||||
operations_frame = ctk.CTkFrame(tab, fg_color=self.COLORS['bg_tile'])
|
||||
operations_frame.pack(fill="x", padx=10, pady=10)
|
||||
|
||||
# Row 1
|
||||
row1 = ctk.CTkFrame(operations_frame, fg_color="transparent")
|
||||
row1.pack(fill="x", padx=10, pady=5)
|
||||
|
||||
ctk.CTkButton(
|
||||
row1, text="📥 Clone", command=self.clone_repository,
|
||||
fg_color=self.COLORS['accent_primary'], width=100
|
||||
).pack(side="left", padx=5)
|
||||
|
||||
ctk.CTkButton(
|
||||
row1, text="🔄 Fetch", command=self.fetch_repository,
|
||||
fg_color=self.COLORS['accent_primary'], width=100
|
||||
).pack(side="left", padx=5)
|
||||
|
||||
ctk.CTkButton(
|
||||
row1, text="⬇️ Pull", command=self.pull_repository,
|
||||
fg_color=self.COLORS['accent_primary'], width=100
|
||||
).pack(side="left", padx=5)
|
||||
|
||||
ctk.CTkButton(
|
||||
row1, text="📊 Status", command=self.show_status,
|
||||
fg_color=self.COLORS['accent_secondary'], width=100
|
||||
).pack(side="left", padx=5)
|
||||
|
||||
# Row 2
|
||||
row2 = ctk.CTkFrame(operations_frame, fg_color="transparent")
|
||||
row2.pack(fill="x", padx=10, pady=5)
|
||||
|
||||
ctk.CTkButton(
|
||||
row2, text="➕ Add All", command=self.add_all_files,
|
||||
fg_color=self.COLORS['accent_success'], width=100
|
||||
).pack(side="left", padx=5)
|
||||
|
||||
ctk.CTkButton(
|
||||
row2, text="💾 Commit", command=self.commit_dialog,
|
||||
fg_color=self.COLORS['accent_success'], width=100
|
||||
).pack(side="left", padx=5)
|
||||
|
||||
ctk.CTkButton(
|
||||
row2, text="⬆️ Push", command=self.push_repository,
|
||||
fg_color=self.COLORS['accent_success'], width=100
|
||||
).pack(side="left", padx=5)
|
||||
|
||||
ctk.CTkButton(
|
||||
row2, text="🌿 Branches", command=self.show_branches,
|
||||
fg_color=self.COLORS['accent_secondary'], width=100
|
||||
).pack(side="left", padx=5)
|
||||
|
||||
# Output area
|
||||
output_label = ctk.CTkLabel(
|
||||
tab, text="Output:",
|
||||
font=self.FONTS['subtitle'],
|
||||
text_color=self.COLORS['text_primary']
|
||||
)
|
||||
output_label.pack(anchor="w", padx=10, pady=(10, 5))
|
||||
|
||||
self.git_output = ctk.CTkTextbox(
|
||||
tab,
|
||||
fg_color=self.COLORS['bg_primary'],
|
||||
text_color=self.COLORS['text_primary'],
|
||||
font=self.FONTS['code']
|
||||
)
|
||||
self.git_output.pack(fill="both", expand=True, padx=10, pady=(0, 10))
|
||||
|
||||
def setup_issues_tab(self):
|
||||
tab = self.tabview.tab("Issues")
|
||||
|
||||
# Issue buttons
|
||||
button_frame = ctk.CTkFrame(tab, fg_color="transparent")
|
||||
button_frame.pack(fill="x", padx=10, pady=10)
|
||||
|
||||
ctk.CTkButton(
|
||||
button_frame, text="🔄 Refresh", command=self.refresh_issues,
|
||||
fg_color=self.COLORS['accent_primary'], width=100
|
||||
).pack(side="left", padx=5)
|
||||
|
||||
ctk.CTkButton(
|
||||
button_frame, text="➕ New Issue", command=self.create_issue_dialog,
|
||||
fg_color=self.COLORS['accent_success'], width=100
|
||||
).pack(side="left", padx=5)
|
||||
|
||||
ctk.CTkButton(
|
||||
button_frame, text="✅ Close", command=self.close_issue,
|
||||
fg_color=self.COLORS['accent_error'], width=100
|
||||
).pack(side="left", padx=5)
|
||||
|
||||
# Issues list
|
||||
self.issues_frame = ctk.CTkScrollableFrame(
|
||||
tab,
|
||||
fg_color=self.COLORS['bg_primary']
|
||||
)
|
||||
self.issues_frame.pack(fill="both", expand=True, padx=10, pady=(0, 10))
|
||||
|
||||
self.issue_widgets = []
|
||||
|
||||
def setup_pr_tab(self):
|
||||
tab = self.tabview.tab("Pull Requests")
|
||||
|
||||
# PR buttons
|
||||
button_frame = ctk.CTkFrame(tab, fg_color="transparent")
|
||||
button_frame.pack(fill="x", padx=10, pady=10)
|
||||
|
||||
ctk.CTkButton(
|
||||
button_frame, text="🔄 Refresh", command=self.refresh_prs,
|
||||
fg_color=self.COLORS['accent_primary'], width=100
|
||||
).pack(side="left", padx=5)
|
||||
|
||||
ctk.CTkButton(
|
||||
button_frame, text="➕ New PR", command=self.create_pr_dialog,
|
||||
fg_color=self.COLORS['accent_success'], width=100
|
||||
).pack(side="left", padx=5)
|
||||
|
||||
ctk.CTkButton(
|
||||
button_frame, text="🔀 Merge", command=self.merge_pr,
|
||||
fg_color=self.COLORS['accent_success'], width=100
|
||||
).pack(side="left", padx=5)
|
||||
|
||||
ctk.CTkButton(
|
||||
button_frame, text="❌ Close", command=self.close_pr,
|
||||
fg_color=self.COLORS['accent_error'], width=100
|
||||
).pack(side="left", padx=5)
|
||||
|
||||
# PR list
|
||||
self.pr_frame = ctk.CTkScrollableFrame(
|
||||
tab,
|
||||
fg_color=self.COLORS['bg_primary']
|
||||
)
|
||||
self.pr_frame.pack(fill="both", expand=True, padx=10, pady=(0, 10))
|
||||
|
||||
self.pr_widgets = []
|
||||
|
||||
def setup_info_tab(self):
|
||||
tab = self.tabview.tab("Repository Info")
|
||||
|
||||
self.info_text = ctk.CTkTextbox(
|
||||
tab,
|
||||
fg_color=self.COLORS['bg_primary'],
|
||||
text_color=self.COLORS['text_primary'],
|
||||
font=self.FONTS['body']
|
||||
)
|
||||
self.info_text.pack(fill="both", expand=True, padx=10, pady=10)
|
||||
|
||||
def refresh_repositories(self):
|
||||
def fetch():
|
||||
try:
|
||||
repos = self.repo_manager.list_all_repositories()
|
||||
self.window.after(0, lambda: self.update_repo_list(repos))
|
||||
except Exception as e:
|
||||
self.window.after(0, lambda: self.show_error(f"Failed to fetch repositories: {e}"))
|
||||
|
||||
threading.Thread(target=fetch, daemon=True).start()
|
||||
|
||||
def update_repo_list(self, repos):
|
||||
# Clear existing
|
||||
for widget in self.repo_list_frame.winfo_children():
|
||||
widget.destroy()
|
||||
|
||||
self.repo_buttons = {}
|
||||
self.repositories = repos
|
||||
|
||||
for repo in repos:
|
||||
btn = ctk.CTkButton(
|
||||
self.repo_list_frame,
|
||||
text=f"{'🔒' if repo['private'] else '🌐'} {repo['name']}",
|
||||
command=lambda r=repo: self.select_repository(r),
|
||||
fg_color=self.COLORS['bg_tile'],
|
||||
hover_color=self.COLORS['bg_tile_hover'],
|
||||
text_color=self.COLORS['text_primary'],
|
||||
anchor="w"
|
||||
)
|
||||
btn.pack(fill="x", pady=2)
|
||||
self.repo_buttons[repo['id']] = btn
|
||||
|
||||
def select_repository(self, repo):
|
||||
self.selected_repo = repo
|
||||
|
||||
# Update button colors
|
||||
for repo_id, btn in self.repo_buttons.items():
|
||||
if repo_id == repo['id']:
|
||||
btn.configure(fg_color=self.COLORS['accent_primary'])
|
||||
else:
|
||||
btn.configure(fg_color=self.COLORS['bg_tile'])
|
||||
|
||||
self.update_repo_info()
|
||||
self.refresh_issues()
|
||||
self.refresh_prs()
|
||||
|
||||
def update_repo_info(self):
|
||||
if not self.selected_repo:
|
||||
return
|
||||
|
||||
info = f"Repository: {self.selected_repo['name']}\n"
|
||||
info += f"Description: {self.selected_repo.get('description', 'No description')}\n"
|
||||
info += f"Private: {'Yes' if self.selected_repo['private'] else 'No'}\n"
|
||||
info += f"Default Branch: {self.selected_repo.get('default_branch', 'main')}\n"
|
||||
info += f"Clone URL: {self.selected_repo['clone_url']}\n"
|
||||
info += f"Created: {self.selected_repo['created_at']}\n"
|
||||
info += f"Updated: {self.selected_repo['updated_at']}\n"
|
||||
|
||||
self.info_text.delete("0.0", "end")
|
||||
self.info_text.insert("0.0", info)
|
||||
|
||||
def clone_repository(self):
|
||||
if not self.selected_repo:
|
||||
self.show_warning("Please select a repository")
|
||||
return
|
||||
|
||||
def clone():
|
||||
try:
|
||||
success, path = self.repo_manager.clone_repository(self.selected_repo['name'])
|
||||
if success:
|
||||
self.selected_repo_path = path
|
||||
self.window.after(0, lambda: self.append_output(f"Repository cloned to: {path}\n"))
|
||||
else:
|
||||
self.window.after(0, lambda: self.append_output(f"Failed to clone repository\n"))
|
||||
except Exception as e:
|
||||
self.window.after(0, lambda: self.show_error(f"Clone failed: {e}"))
|
||||
|
||||
threading.Thread(target=clone, daemon=True).start()
|
||||
|
||||
def fetch_repository(self):
|
||||
if not self.selected_repo_path:
|
||||
self.show_warning("Please clone the repository first")
|
||||
return
|
||||
|
||||
success, result = self.repo_manager.git_ops.fetch(self.selected_repo_path)
|
||||
self.append_output(f"Fetch: {result}\n")
|
||||
|
||||
def pull_repository(self):
|
||||
if not self.selected_repo_path:
|
||||
self.show_warning("Please clone the repository first")
|
||||
return
|
||||
|
||||
success, result = self.repo_manager.git_ops.pull(self.selected_repo_path)
|
||||
self.append_output(f"Pull: {result}\n")
|
||||
|
||||
def push_repository(self):
|
||||
if not self.selected_repo_path:
|
||||
self.show_warning("Please clone the repository first")
|
||||
return
|
||||
|
||||
success, result = self.repo_manager.git_ops.push(self.selected_repo_path)
|
||||
self.append_output(f"Push: {result}\n")
|
||||
|
||||
def show_status(self):
|
||||
if not self.selected_repo_path:
|
||||
self.show_warning("Please clone the repository first")
|
||||
return
|
||||
|
||||
status = self.repo_manager.get_repository_status(self.selected_repo_path)
|
||||
self.append_output(f"\n--- Repository Status ---\n")
|
||||
self.append_output(f"Current Branch: {status['current_branch']}\n")
|
||||
self.append_output(f"Has Changes: {status['has_changes']}\n")
|
||||
self.append_output(f"Status:\n{status['status']}\n")
|
||||
self.append_output(f"Remotes:\n{status['remotes']}\n")
|
||||
|
||||
def add_all_files(self):
|
||||
if not self.selected_repo_path:
|
||||
self.show_warning("Please clone the repository first")
|
||||
return
|
||||
|
||||
success, result = self.repo_manager.git_ops.add(self.selected_repo_path)
|
||||
self.append_output(f"Add all: {result}\n")
|
||||
|
||||
def commit_dialog(self):
|
||||
if not self.selected_repo_path:
|
||||
self.show_warning("Please clone the repository first")
|
||||
return
|
||||
|
||||
dialog = ctk.CTkToplevel(self.window)
|
||||
dialog.title("Commit Changes")
|
||||
dialog.geometry("500x300")
|
||||
dialog.configure(fg_color=self.COLORS['bg_primary'])
|
||||
|
||||
# Center dialog
|
||||
dialog.transient(self.window)
|
||||
dialog.update_idletasks()
|
||||
x = (dialog.winfo_screenwidth() - 500) // 2
|
||||
y = (dialog.winfo_screenheight() - 300) // 2
|
||||
dialog.geometry(f"500x300+{x}+{y}")
|
||||
|
||||
ctk.CTkLabel(
|
||||
dialog,
|
||||
text="Commit Message:",
|
||||
font=self.FONTS['subtitle'],
|
||||
text_color=self.COLORS['text_primary']
|
||||
).pack(pady=(20, 10))
|
||||
|
||||
message_text = ctk.CTkTextbox(
|
||||
dialog,
|
||||
height=150,
|
||||
fg_color=self.COLORS['bg_secondary'],
|
||||
text_color=self.COLORS['text_primary']
|
||||
)
|
||||
message_text.pack(padx=20, pady=10, fill="both", expand=True)
|
||||
|
||||
def do_commit():
|
||||
message = message_text.get("0.0", "end").strip()
|
||||
if not message:
|
||||
self.show_warning("Please enter a commit message")
|
||||
return
|
||||
|
||||
success, result = self.repo_manager.git_ops.commit(self.selected_repo_path, message)
|
||||
self.append_output(f"Commit: {result}\n")
|
||||
dialog.destroy()
|
||||
|
||||
ctk.CTkButton(
|
||||
dialog,
|
||||
text="Commit",
|
||||
command=do_commit,
|
||||
fg_color=self.COLORS['accent_success']
|
||||
).pack(pady=20)
|
||||
|
||||
def show_branches(self):
|
||||
if not self.selected_repo_path:
|
||||
self.show_warning("Please clone the repository first")
|
||||
return
|
||||
|
||||
success, branches = self.repo_manager.git_ops.branch(self.selected_repo_path, list_all=True)
|
||||
self.append_output(f"\n--- Branches ---\n{branches}\n")
|
||||
|
||||
def create_repository_dialog(self):
|
||||
dialog = ctk.CTkToplevel(self.window)
|
||||
dialog.title("Create New Repository")
|
||||
dialog.geometry("500x400")
|
||||
dialog.configure(fg_color=self.COLORS['bg_primary'])
|
||||
|
||||
# Center dialog
|
||||
dialog.transient(self.window)
|
||||
dialog.update_idletasks()
|
||||
x = (dialog.winfo_screenwidth() - 500) // 2
|
||||
y = (dialog.winfo_screenheight() - 400) // 2
|
||||
dialog.geometry(f"500x400+{x}+{y}")
|
||||
|
||||
# Name
|
||||
ctk.CTkLabel(
|
||||
dialog, text="Repository Name:",
|
||||
font=self.FONTS['subtitle'],
|
||||
text_color=self.COLORS['text_primary']
|
||||
).pack(pady=(20, 5))
|
||||
|
||||
name_entry = ctk.CTkEntry(
|
||||
dialog, width=400,
|
||||
fg_color=self.COLORS['bg_secondary'],
|
||||
text_color=self.COLORS['text_primary']
|
||||
)
|
||||
name_entry.pack(pady=5)
|
||||
|
||||
# Description
|
||||
ctk.CTkLabel(
|
||||
dialog, text="Description:",
|
||||
font=self.FONTS['subtitle'],
|
||||
text_color=self.COLORS['text_primary']
|
||||
).pack(pady=(20, 5))
|
||||
|
||||
desc_text = ctk.CTkTextbox(
|
||||
dialog, height=100, width=400,
|
||||
fg_color=self.COLORS['bg_secondary'],
|
||||
text_color=self.COLORS['text_primary']
|
||||
)
|
||||
desc_text.pack(pady=5)
|
||||
|
||||
# Options
|
||||
private_var = ctk.BooleanVar()
|
||||
ctk.CTkCheckBox(
|
||||
dialog, text="Private Repository",
|
||||
variable=private_var,
|
||||
text_color=self.COLORS['text_primary']
|
||||
).pack(pady=10)
|
||||
|
||||
auto_init_var = ctk.BooleanVar(value=True)
|
||||
ctk.CTkCheckBox(
|
||||
dialog, text="Initialize with README",
|
||||
variable=auto_init_var,
|
||||
text_color=self.COLORS['text_primary']
|
||||
).pack(pady=5)
|
||||
|
||||
def create():
|
||||
name = name_entry.get().strip()
|
||||
desc = desc_text.get("0.0", "end").strip()
|
||||
|
||||
if not name:
|
||||
self.show_warning("Please enter a repository name")
|
||||
return
|
||||
|
||||
try:
|
||||
self.repo_manager.create_repository(
|
||||
name, desc, private_var.get(), auto_init_var.get()
|
||||
)
|
||||
self.show_info(f"Repository '{name}' created successfully")
|
||||
dialog.destroy()
|
||||
self.refresh_repositories()
|
||||
except Exception as e:
|
||||
self.show_error(f"Failed to create repository: {e}")
|
||||
|
||||
ctk.CTkButton(
|
||||
dialog, text="Create", command=create,
|
||||
fg_color=self.COLORS['accent_success']
|
||||
).pack(pady=20)
|
||||
|
||||
def delete_repository(self):
|
||||
if not self.selected_repo:
|
||||
self.show_warning("Please select a repository")
|
||||
return
|
||||
|
||||
if messagebox.askyesno("Confirm", f"Are you sure you want to delete '{self.selected_repo['name']}'?"):
|
||||
if self.repo_manager.delete_repository(self.selected_repo['name']):
|
||||
self.show_info("Repository deleted successfully")
|
||||
self.refresh_repositories()
|
||||
|
||||
def refresh_issues(self):
|
||||
if not self.selected_repo:
|
||||
return
|
||||
|
||||
# Clear existing
|
||||
for widget in self.issue_widgets:
|
||||
widget.destroy()
|
||||
self.issue_widgets.clear()
|
||||
|
||||
try:
|
||||
manager = self.issue_pr_manager.get_issue_manager(self.selected_repo['name'])
|
||||
issues = manager.list_issues()
|
||||
|
||||
for issue in issues:
|
||||
frame = ctk.CTkFrame(self.issues_frame, fg_color=self.COLORS['bg_tile'])
|
||||
frame.pack(fill="x", pady=5)
|
||||
|
||||
# Issue info
|
||||
info_frame = ctk.CTkFrame(frame, fg_color="transparent")
|
||||
info_frame.pack(fill="x", padx=10, pady=10)
|
||||
|
||||
ctk.CTkLabel(
|
||||
info_frame,
|
||||
text=f"#{issue['number']}: {issue['title']}",
|
||||
font=self.FONTS['subtitle'],
|
||||
text_color=self.COLORS['text_primary']
|
||||
).pack(anchor="w")
|
||||
|
||||
ctk.CTkLabel(
|
||||
info_frame,
|
||||
text=f"State: {issue['state']} | Author: {issue['user']['username']} | Created: {issue['created_at'][:10]}",
|
||||
font=self.FONTS['small'],
|
||||
text_color=self.COLORS['text_secondary']
|
||||
).pack(anchor="w")
|
||||
|
||||
self.issue_widgets.append(frame)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error fetching issues: {e}")
|
||||
|
||||
def refresh_prs(self):
|
||||
if not self.selected_repo:
|
||||
return
|
||||
|
||||
# Clear existing
|
||||
for widget in self.pr_widgets:
|
||||
widget.destroy()
|
||||
self.pr_widgets.clear()
|
||||
|
||||
try:
|
||||
manager = self.issue_pr_manager.get_pr_manager(self.selected_repo['name'])
|
||||
prs = manager.list_pull_requests()
|
||||
|
||||
for pr in prs:
|
||||
frame = ctk.CTkFrame(self.pr_frame, fg_color=self.COLORS['bg_tile'])
|
||||
frame.pack(fill="x", pady=5)
|
||||
|
||||
# PR info
|
||||
info_frame = ctk.CTkFrame(frame, fg_color="transparent")
|
||||
info_frame.pack(fill="x", padx=10, pady=10)
|
||||
|
||||
ctk.CTkLabel(
|
||||
info_frame,
|
||||
text=f"#{pr['number']}: {pr['title']}",
|
||||
font=self.FONTS['subtitle'],
|
||||
text_color=self.COLORS['text_primary']
|
||||
).pack(anchor="w")
|
||||
|
||||
ctk.CTkLabel(
|
||||
info_frame,
|
||||
text=f"State: {pr['state']} | {pr['head']['ref']} → {pr['base']['ref']} | Author: {pr['user']['username']}",
|
||||
font=self.FONTS['small'],
|
||||
text_color=self.COLORS['text_secondary']
|
||||
).pack(anchor="w")
|
||||
|
||||
self.pr_widgets.append(frame)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error fetching PRs: {e}")
|
||||
|
||||
def create_issue_dialog(self):
|
||||
if not self.selected_repo:
|
||||
self.show_warning("Please select a repository")
|
||||
return
|
||||
|
||||
dialog = ctk.CTkToplevel(self.window)
|
||||
dialog.title("Create New Issue")
|
||||
dialog.geometry("600x500")
|
||||
dialog.configure(fg_color=self.COLORS['bg_primary'])
|
||||
|
||||
# Center dialog
|
||||
dialog.transient(self.window)
|
||||
dialog.update_idletasks()
|
||||
x = (dialog.winfo_screenwidth() - 600) // 2
|
||||
y = (dialog.winfo_screenheight() - 500) // 2
|
||||
dialog.geometry(f"600x500+{x}+{y}")
|
||||
|
||||
# Title
|
||||
ctk.CTkLabel(
|
||||
dialog, text="Title:",
|
||||
font=self.FONTS['subtitle'],
|
||||
text_color=self.COLORS['text_primary']
|
||||
).pack(pady=(20, 5))
|
||||
|
||||
title_entry = ctk.CTkEntry(
|
||||
dialog, width=500,
|
||||
fg_color=self.COLORS['bg_secondary'],
|
||||
text_color=self.COLORS['text_primary']
|
||||
)
|
||||
title_entry.pack(pady=5)
|
||||
|
||||
# Description
|
||||
ctk.CTkLabel(
|
||||
dialog, text="Description:",
|
||||
font=self.FONTS['subtitle'],
|
||||
text_color=self.COLORS['text_primary']
|
||||
).pack(pady=(20, 5))
|
||||
|
||||
desc_text = ctk.CTkTextbox(
|
||||
dialog, height=250, width=500,
|
||||
fg_color=self.COLORS['bg_secondary'],
|
||||
text_color=self.COLORS['text_primary']
|
||||
)
|
||||
desc_text.pack(pady=5)
|
||||
|
||||
def create():
|
||||
title = title_entry.get().strip()
|
||||
body = desc_text.get("0.0", "end").strip()
|
||||
|
||||
if not title:
|
||||
self.show_warning("Please enter a title")
|
||||
return
|
||||
|
||||
try:
|
||||
manager = self.issue_pr_manager.get_issue_manager(self.selected_repo['name'])
|
||||
manager.create_issue(title, body)
|
||||
self.show_info("Issue created successfully")
|
||||
dialog.destroy()
|
||||
self.refresh_issues()
|
||||
except Exception as e:
|
||||
self.show_error(f"Failed to create issue: {e}")
|
||||
|
||||
ctk.CTkButton(
|
||||
dialog, text="Create", command=create,
|
||||
fg_color=self.COLORS['accent_success']
|
||||
).pack(pady=20)
|
||||
|
||||
def close_issue(self):
|
||||
# TODO: Need to implement issue selection first
|
||||
self.show_info("Please select an issue from the list to close")
|
||||
|
||||
def create_pr_dialog(self):
|
||||
if not self.selected_repo:
|
||||
self.show_warning("Please select a repository")
|
||||
return
|
||||
|
||||
dialog = ctk.CTkToplevel(self.window)
|
||||
dialog.title("Create Pull Request")
|
||||
dialog.geometry("600x600")
|
||||
dialog.configure(fg_color=self.COLORS['bg_primary'])
|
||||
|
||||
# Center dialog
|
||||
dialog.transient(self.window)
|
||||
dialog.update_idletasks()
|
||||
x = (dialog.winfo_screenwidth() - 600) // 2
|
||||
y = (dialog.winfo_screenheight() - 600) // 2
|
||||
dialog.geometry(f"600x600+{x}+{y}")
|
||||
|
||||
# Title
|
||||
ctk.CTkLabel(
|
||||
dialog, text="Title:",
|
||||
font=self.FONTS['subtitle'],
|
||||
text_color=self.COLORS['text_primary']
|
||||
).pack(pady=(20, 5))
|
||||
|
||||
title_entry = ctk.CTkEntry(
|
||||
dialog, width=500,
|
||||
fg_color=self.COLORS['bg_secondary'],
|
||||
text_color=self.COLORS['text_primary']
|
||||
)
|
||||
title_entry.pack(pady=5)
|
||||
|
||||
# Branches
|
||||
ctk.CTkLabel(
|
||||
dialog, text="Head Branch:",
|
||||
font=self.FONTS['subtitle'],
|
||||
text_color=self.COLORS['text_primary']
|
||||
).pack(pady=(20, 5))
|
||||
|
||||
head_entry = ctk.CTkEntry(
|
||||
dialog, width=500,
|
||||
fg_color=self.COLORS['bg_secondary'],
|
||||
text_color=self.COLORS['text_primary']
|
||||
)
|
||||
head_entry.pack(pady=5)
|
||||
|
||||
ctk.CTkLabel(
|
||||
dialog, text="Base Branch:",
|
||||
font=self.FONTS['subtitle'],
|
||||
text_color=self.COLORS['text_primary']
|
||||
).pack(pady=(20, 5))
|
||||
|
||||
base_entry = ctk.CTkEntry(
|
||||
dialog, width=500,
|
||||
fg_color=self.COLORS['bg_secondary'],
|
||||
text_color=self.COLORS['text_primary']
|
||||
)
|
||||
base_entry.insert(0, "main")
|
||||
base_entry.pack(pady=5)
|
||||
|
||||
# Description
|
||||
ctk.CTkLabel(
|
||||
dialog, text="Description:",
|
||||
font=self.FONTS['subtitle'],
|
||||
text_color=self.COLORS['text_primary']
|
||||
).pack(pady=(20, 5))
|
||||
|
||||
desc_text = ctk.CTkTextbox(
|
||||
dialog, height=200, width=500,
|
||||
fg_color=self.COLORS['bg_secondary'],
|
||||
text_color=self.COLORS['text_primary']
|
||||
)
|
||||
desc_text.pack(pady=5)
|
||||
|
||||
def create():
|
||||
title = title_entry.get().strip()
|
||||
head = head_entry.get().strip()
|
||||
base = base_entry.get().strip()
|
||||
body = desc_text.get("0.0", "end").strip()
|
||||
|
||||
if not all([title, head, base]):
|
||||
self.show_warning("Please fill all required fields")
|
||||
return
|
||||
|
||||
try:
|
||||
manager = self.issue_pr_manager.get_pr_manager(self.selected_repo['name'])
|
||||
manager.create_pull_request(title, head, base, body)
|
||||
self.show_info("Pull request created successfully")
|
||||
dialog.destroy()
|
||||
self.refresh_prs()
|
||||
except Exception as e:
|
||||
self.show_error(f"Failed to create PR: {e}")
|
||||
|
||||
ctk.CTkButton(
|
||||
dialog, text="Create", command=create,
|
||||
fg_color=self.COLORS['accent_success']
|
||||
).pack(pady=20)
|
||||
|
||||
def merge_pr(self):
|
||||
# TODO: Need to implement PR selection first
|
||||
self.show_info("Please select a pull request from the list to merge")
|
||||
|
||||
def close_pr(self):
|
||||
# TODO: Need to implement PR selection first
|
||||
self.show_info("Please select a pull request from the list to close")
|
||||
|
||||
# Helper methods
|
||||
def append_output(self, text):
|
||||
self.git_output.insert("end", text)
|
||||
self.git_output.see("end")
|
||||
|
||||
def show_error(self, message):
|
||||
messagebox.showerror("Error", message)
|
||||
|
||||
def show_warning(self, message):
|
||||
messagebox.showwarning("Warning", message)
|
||||
|
||||
def show_info(self, message):
|
||||
messagebox.showinfo("Info", message)
|
||||
|
||||
def push_local_repo_dialog(self, project_name=None, project_path=None):
|
||||
"""Dialog to push an existing local repository to Gitea"""
|
||||
dialog = ctk.CTkToplevel(self.window)
|
||||
dialog.title("Push Local Repository to Gitea")
|
||||
dialog.geometry("600x600")
|
||||
dialog.configure(fg_color=self.COLORS['bg_primary'])
|
||||
|
||||
# Center dialog
|
||||
dialog.transient(self.window)
|
||||
dialog.update_idletasks()
|
||||
x = (dialog.winfo_screenwidth() - 600) // 2
|
||||
y = (dialog.winfo_screenheight() - 600) // 2
|
||||
dialog.geometry(f"600x600+{x}+{y}")
|
||||
|
||||
# Add description label
|
||||
desc_label = ctk.CTkLabel(
|
||||
dialog,
|
||||
text="Push an existing local Git repository to your Gitea server",
|
||||
font=self.FONTS['body'],
|
||||
text_color=self.COLORS['text_secondary']
|
||||
)
|
||||
desc_label.pack(pady=(10, 5))
|
||||
|
||||
# Local repository path
|
||||
ctk.CTkLabel(
|
||||
dialog, text="Local Repository Path:",
|
||||
font=self.FONTS['subtitle'],
|
||||
text_color=self.COLORS['text_primary']
|
||||
).pack(pady=(20, 5))
|
||||
|
||||
path_frame = ctk.CTkFrame(dialog, fg_color="transparent")
|
||||
path_frame.pack(pady=5, padx=20, fill="x")
|
||||
|
||||
path_var = ctk.StringVar()
|
||||
path_entry = ctk.CTkEntry(
|
||||
path_frame, width=400,
|
||||
textvariable=path_var,
|
||||
fg_color=self.COLORS['bg_secondary'],
|
||||
text_color=self.COLORS['text_primary']
|
||||
)
|
||||
path_entry.pack(side="left", padx=(0, 10))
|
||||
|
||||
def browse_folder():
|
||||
from tkinter import filedialog
|
||||
folder = filedialog.askdirectory()
|
||||
if folder:
|
||||
path_var.set(folder)
|
||||
# Update repo name field with folder name if empty
|
||||
if not name_entry.get().strip():
|
||||
import os
|
||||
folder_name = os.path.basename(folder)
|
||||
name_entry.delete(0, 'end')
|
||||
name_entry.insert(0, folder_name)
|
||||
|
||||
# Set initial values if provided
|
||||
if project_path:
|
||||
path_var.set(project_path)
|
||||
|
||||
ctk.CTkButton(
|
||||
path_frame, text="Browse", command=browse_folder,
|
||||
fg_color=self.COLORS['accent_primary'], width=100
|
||||
).pack(side="left")
|
||||
|
||||
# Repository name
|
||||
ctk.CTkLabel(
|
||||
dialog, text="Repository Name:",
|
||||
font=self.FONTS['subtitle'],
|
||||
text_color=self.COLORS['text_primary']
|
||||
).pack(pady=(20, 5))
|
||||
|
||||
name_entry = ctk.CTkEntry(
|
||||
dialog, width=500,
|
||||
fg_color=self.COLORS['bg_secondary'],
|
||||
text_color=self.COLORS['text_primary']
|
||||
)
|
||||
name_entry.pack(pady=5, padx=20)
|
||||
|
||||
# Set initial name if provided
|
||||
if project_name:
|
||||
name_entry.insert(0, project_name)
|
||||
|
||||
# Description
|
||||
ctk.CTkLabel(
|
||||
dialog, text="Description:",
|
||||
font=self.FONTS['subtitle'],
|
||||
text_color=self.COLORS['text_primary']
|
||||
).pack(pady=(20, 5))
|
||||
|
||||
desc_text = ctk.CTkTextbox(
|
||||
dialog, height=100, width=500,
|
||||
fg_color=self.COLORS['bg_secondary'],
|
||||
text_color=self.COLORS['text_primary']
|
||||
)
|
||||
desc_text.pack(pady=5, padx=20)
|
||||
|
||||
# Options
|
||||
private_var = ctk.BooleanVar()
|
||||
ctk.CTkCheckBox(
|
||||
dialog, text="Private Repository",
|
||||
variable=private_var,
|
||||
text_color=self.COLORS['text_primary']
|
||||
).pack(pady=10)
|
||||
|
||||
# Branch name
|
||||
ctk.CTkLabel(
|
||||
dialog, text="Branch to push (default: main):",
|
||||
font=self.FONTS['subtitle'],
|
||||
text_color=self.COLORS['text_primary']
|
||||
).pack(pady=(10, 5))
|
||||
|
||||
branch_entry = ctk.CTkEntry(
|
||||
dialog, width=200,
|
||||
placeholder_text="main",
|
||||
fg_color=self.COLORS['bg_secondary'],
|
||||
text_color=self.COLORS['text_primary']
|
||||
)
|
||||
branch_entry.pack(pady=5)
|
||||
|
||||
# Status label for feedback
|
||||
status_label = ctk.CTkLabel(
|
||||
dialog, text="",
|
||||
font=self.FONTS['body'],
|
||||
text_color=self.COLORS['text_secondary']
|
||||
)
|
||||
status_label.pack(pady=10)
|
||||
|
||||
def push_repo():
|
||||
local_path = path_var.get().strip()
|
||||
repo_name = name_entry.get().strip()
|
||||
desc = desc_text.get("0.0", "end").strip()
|
||||
branch = branch_entry.get().strip() or "main"
|
||||
|
||||
if not local_path:
|
||||
self.show_warning("Please select a local repository path")
|
||||
return
|
||||
|
||||
if not repo_name:
|
||||
self.show_warning("Please enter a repository name")
|
||||
return
|
||||
|
||||
# Check if it's a valid git repository
|
||||
from pathlib import Path
|
||||
repo_path = Path(local_path)
|
||||
if not (repo_path / ".git").exists():
|
||||
self.show_warning("Selected path is not a git repository")
|
||||
return
|
||||
|
||||
def do_push():
|
||||
try:
|
||||
# Update status
|
||||
self.window.after(0, lambda: status_label.configure(
|
||||
text="Creating repository on Gitea...",
|
||||
text_color=self.COLORS['accent_primary']
|
||||
))
|
||||
|
||||
success, message = self.repo_manager.push_local_repo_to_gitea(
|
||||
repo_path, repo_name, desc, private_var.get(), branch
|
||||
)
|
||||
|
||||
if success:
|
||||
self.window.after(0, lambda: self.show_info(message))
|
||||
self.window.after(0, dialog.destroy)
|
||||
self.window.after(0, self.refresh_repositories)
|
||||
else:
|
||||
self.window.after(0, lambda: status_label.configure(
|
||||
text=f"Error: {message}",
|
||||
text_color=self.COLORS['accent_error']
|
||||
))
|
||||
except Exception as e:
|
||||
self.window.after(0, lambda: status_label.configure(
|
||||
text=f"Error: {str(e)}",
|
||||
text_color=self.COLORS['accent_error']
|
||||
))
|
||||
|
||||
# Run in thread to avoid blocking UI
|
||||
import threading
|
||||
threading.Thread(target=do_push, daemon=True).start()
|
||||
|
||||
ctk.CTkButton(
|
||||
dialog, text="Push to Gitea", command=push_repo,
|
||||
fg_color=self.COLORS['accent_success']
|
||||
).pack(pady=20)
|
||||
299
src/gitea/issue_pr_manager.py
Normale Datei
299
src/gitea/issue_pr_manager.py
Normale Datei
@ -0,0 +1,299 @@
|
||||
import logging
|
||||
from typing import List, Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
from .gitea_client import GiteaClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class IssueManager:
|
||||
def __init__(self, gitea_client: GiteaClient, owner: str, repo: str):
|
||||
self.client = gitea_client
|
||||
self.owner = owner
|
||||
self.repo = repo
|
||||
|
||||
def list_issues(self, state: str = "open", labels: Optional[List[str]] = None) -> List[Dict[str, Any]]:
|
||||
all_issues = []
|
||||
page = 1
|
||||
|
||||
while True:
|
||||
issues = self.client.list_issues(
|
||||
self.owner, self.repo, state=state, labels=labels, page=page
|
||||
)
|
||||
if not issues:
|
||||
break
|
||||
all_issues.extend(issues)
|
||||
page += 1
|
||||
|
||||
return all_issues
|
||||
|
||||
def create_issue(self, title: str, body: str = "",
|
||||
assignees: Optional[List[str]] = None,
|
||||
labels: Optional[List[int]] = None) -> Optional[Dict[str, Any]]:
|
||||
try:
|
||||
issue = self.client.create_issue(
|
||||
self.owner, self.repo, title, body, assignees, labels
|
||||
)
|
||||
logger.info(f"Issue '{title}' created successfully")
|
||||
return issue
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create issue: {e}")
|
||||
return None
|
||||
|
||||
def update_issue(self, index: int, **kwargs) -> Optional[Dict[str, Any]]:
|
||||
try:
|
||||
issue = self.client.update_issue(self.owner, self.repo, index, **kwargs)
|
||||
logger.info(f"Issue #{index} updated successfully")
|
||||
return issue
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update issue #{index}: {e}")
|
||||
return None
|
||||
|
||||
def close_issue(self, index: int) -> bool:
|
||||
try:
|
||||
self.client.close_issue(self.owner, self.repo, index)
|
||||
logger.info(f"Issue #{index} closed successfully")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to close issue #{index}: {e}")
|
||||
return False
|
||||
|
||||
def add_comment(self, index: int, comment: str) -> Optional[Dict[str, Any]]:
|
||||
try:
|
||||
data = {"body": comment}
|
||||
comment_data = self.client._request(
|
||||
"POST",
|
||||
f"repos/{self.owner}/{self.repo}/issues/{index}/comments",
|
||||
json=data
|
||||
)
|
||||
logger.info(f"Comment added to issue #{index}")
|
||||
return comment_data
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to add comment to issue #{index}: {e}")
|
||||
return None
|
||||
|
||||
def get_issue_comments(self, index: int) -> List[Dict[str, Any]]:
|
||||
try:
|
||||
return self.client._request(
|
||||
"GET",
|
||||
f"repos/{self.owner}/{self.repo}/issues/{index}/comments"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get comments for issue #{index}: {e}")
|
||||
return []
|
||||
|
||||
def search_issues(self, query: str) -> List[Dict[str, Any]]:
|
||||
all_issues = self.list_issues(state="all")
|
||||
query_lower = query.lower()
|
||||
|
||||
return [
|
||||
issue for issue in all_issues
|
||||
if query_lower in issue["title"].lower() or
|
||||
query_lower in issue.get("body", "").lower()
|
||||
]
|
||||
|
||||
class PullRequestManager:
|
||||
def __init__(self, gitea_client: GiteaClient, owner: str, repo: str):
|
||||
self.client = gitea_client
|
||||
self.owner = owner
|
||||
self.repo = repo
|
||||
|
||||
def list_pull_requests(self, state: str = "open") -> List[Dict[str, Any]]:
|
||||
all_prs = []
|
||||
page = 1
|
||||
|
||||
while True:
|
||||
prs = self.client.list_pull_requests(
|
||||
self.owner, self.repo, state=state, page=page
|
||||
)
|
||||
if not prs:
|
||||
break
|
||||
all_prs.extend(prs)
|
||||
page += 1
|
||||
|
||||
return all_prs
|
||||
|
||||
def create_pull_request(self, title: str, head: str, base: str,
|
||||
body: str = "", assignees: Optional[List[str]] = None) -> Optional[Dict[str, Any]]:
|
||||
try:
|
||||
pr = self.client.create_pull_request(
|
||||
self.owner, self.repo, title, head, base, body, assignees
|
||||
)
|
||||
logger.info(f"Pull request '{title}' created successfully")
|
||||
return pr
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create pull request: {e}")
|
||||
return None
|
||||
|
||||
def update_pull_request(self, index: int, **kwargs) -> Optional[Dict[str, Any]]:
|
||||
try:
|
||||
pr = self.client._request(
|
||||
"PATCH",
|
||||
f"repos/{self.owner}/{self.repo}/pulls/{index}",
|
||||
json=kwargs
|
||||
)
|
||||
logger.info(f"Pull request #{index} updated successfully")
|
||||
return pr
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update pull request #{index}: {e}")
|
||||
return None
|
||||
|
||||
def merge_pull_request(self, index: int, merge_style: str = "merge") -> bool:
|
||||
try:
|
||||
self.client.merge_pull_request(self.owner, self.repo, index, merge_style)
|
||||
logger.info(f"Pull request #{index} merged successfully")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to merge pull request #{index}: {e}")
|
||||
return False
|
||||
|
||||
def close_pull_request(self, index: int) -> bool:
|
||||
return self.update_pull_request(index, state="closed") is not None
|
||||
|
||||
def get_pull_request_diff(self, index: int) -> Optional[str]:
|
||||
try:
|
||||
response = self.client.session.get(
|
||||
f"{self.client.config.api_url}/repos/{self.owner}/{self.repo}/pulls/{index}.diff",
|
||||
headers={"Accept": "text/plain"}
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.text
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get diff for PR #{index}: {e}")
|
||||
return None
|
||||
|
||||
def get_pull_request_commits(self, index: int) -> List[Dict[str, Any]]:
|
||||
try:
|
||||
return self.client._request(
|
||||
"GET",
|
||||
f"repos/{self.owner}/{self.repo}/pulls/{index}/commits"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get commits for PR #{index}: {e}")
|
||||
return []
|
||||
|
||||
def add_comment(self, index: int, comment: str) -> Optional[Dict[str, Any]]:
|
||||
try:
|
||||
data = {"body": comment}
|
||||
comment_data = self.client._request(
|
||||
"POST",
|
||||
f"repos/{self.owner}/{self.repo}/pulls/{index}/reviews",
|
||||
json=data
|
||||
)
|
||||
logger.info(f"Comment added to pull request #{index}")
|
||||
return comment_data
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to add comment to PR #{index}: {e}")
|
||||
return None
|
||||
|
||||
def get_pull_request_reviews(self, index: int) -> List[Dict[str, Any]]:
|
||||
try:
|
||||
return self.client._request(
|
||||
"GET",
|
||||
f"repos/{self.owner}/{self.repo}/pulls/{index}/reviews"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get reviews for PR #{index}: {e}")
|
||||
return []
|
||||
|
||||
def approve_pull_request(self, index: int, comment: str = "") -> Optional[Dict[str, Any]]:
|
||||
try:
|
||||
data = {
|
||||
"body": comment,
|
||||
"event": "APPROVE"
|
||||
}
|
||||
review = self.client._request(
|
||||
"POST",
|
||||
f"repos/{self.owner}/{self.repo}/pulls/{index}/reviews",
|
||||
json=data
|
||||
)
|
||||
logger.info(f"Pull request #{index} approved")
|
||||
return review
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to approve PR #{index}: {e}")
|
||||
return None
|
||||
|
||||
def request_changes(self, index: int, comment: str) -> Optional[Dict[str, Any]]:
|
||||
try:
|
||||
data = {
|
||||
"body": comment,
|
||||
"event": "REQUEST_CHANGES"
|
||||
}
|
||||
review = self.client._request(
|
||||
"POST",
|
||||
f"repos/{self.owner}/{self.repo}/pulls/{index}/reviews",
|
||||
json=data
|
||||
)
|
||||
logger.info(f"Changes requested for PR #{index}")
|
||||
return review
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to request changes for PR #{index}: {e}")
|
||||
return None
|
||||
|
||||
class IssuePRManager:
|
||||
def __init__(self, gitea_client: Optional[GiteaClient] = None):
|
||||
self.client = gitea_client or GiteaClient()
|
||||
self._current_user = None
|
||||
self._issue_managers = {}
|
||||
self._pr_managers = {}
|
||||
|
||||
@property
|
||||
def current_user(self) -> Dict[str, Any]:
|
||||
if self._current_user is None:
|
||||
self._current_user = self.client.get_user_info()
|
||||
return self._current_user
|
||||
|
||||
def get_issue_manager(self, repo_name: str) -> IssueManager:
|
||||
if repo_name not in self._issue_managers:
|
||||
owner = self.current_user["username"]
|
||||
self._issue_managers[repo_name] = IssueManager(self.client, owner, repo_name)
|
||||
return self._issue_managers[repo_name]
|
||||
|
||||
def get_pr_manager(self, repo_name: str) -> PullRequestManager:
|
||||
if repo_name not in self._pr_managers:
|
||||
owner = self.current_user["username"]
|
||||
self._pr_managers[repo_name] = PullRequestManager(self.client, owner, repo_name)
|
||||
return self._pr_managers[repo_name]
|
||||
|
||||
def list_all_issues(self, state: str = "open") -> Dict[str, List[Dict[str, Any]]]:
|
||||
repos = self.client.list_repositories()
|
||||
all_issues = {}
|
||||
|
||||
for repo in repos:
|
||||
repo_name = repo["name"]
|
||||
manager = self.get_issue_manager(repo_name)
|
||||
issues = manager.list_issues(state=state)
|
||||
if issues:
|
||||
all_issues[repo_name] = issues
|
||||
|
||||
return all_issues
|
||||
|
||||
def list_all_pull_requests(self, state: str = "open") -> Dict[str, List[Dict[str, Any]]]:
|
||||
repos = self.client.list_repositories()
|
||||
all_prs = {}
|
||||
|
||||
for repo in repos:
|
||||
repo_name = repo["name"]
|
||||
manager = self.get_pr_manager(repo_name)
|
||||
prs = manager.list_pull_requests(state=state)
|
||||
if prs:
|
||||
all_prs[repo_name] = prs
|
||||
|
||||
return all_prs
|
||||
|
||||
def get_activity_summary(self) -> Dict[str, Any]:
|
||||
open_issues = self.list_all_issues("open")
|
||||
open_prs = self.list_all_pull_requests("open")
|
||||
|
||||
total_issues = sum(len(issues) for issues in open_issues.values())
|
||||
total_prs = sum(len(prs) for prs in open_prs.values())
|
||||
|
||||
return {
|
||||
"open_issues": total_issues,
|
||||
"open_pull_requests": total_prs,
|
||||
"repositories_with_issues": len(open_issues),
|
||||
"repositories_with_prs": len(open_prs),
|
||||
"details": {
|
||||
"issues_by_repo": {repo: len(issues) for repo, issues in open_issues.items()},
|
||||
"prs_by_repo": {repo: len(prs) for repo, prs in open_prs.items()}
|
||||
}
|
||||
}
|
||||
274
src/gitea/repository_manager.py
Normale Datei
274
src/gitea/repository_manager.py
Normale Datei
@ -0,0 +1,274 @@
|
||||
import logging
|
||||
from typing import List, Dict, Any, Optional, Tuple
|
||||
from pathlib import Path
|
||||
from .gitea_client import GiteaClient, GiteaConfig
|
||||
from .git_operations import GitOperationsManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class RepositoryManager:
|
||||
def __init__(self, gitea_client: Optional[GiteaClient] = None):
|
||||
self.client = gitea_client or GiteaClient()
|
||||
self._current_user = None
|
||||
|
||||
try:
|
||||
# Get the actual username from Gitea
|
||||
user_info = self.client.get_user_info()
|
||||
actual_username = user_info.get('username', self.client.config.username)
|
||||
self._current_user = user_info
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to get user info from Gitea: {e}")
|
||||
# Fall back to config username
|
||||
actual_username = self.client.config.username
|
||||
|
||||
self.git_ops = GitOperationsManager(
|
||||
base_url=self.client.config.base_url,
|
||||
token=self.client.config.api_token,
|
||||
username=actual_username
|
||||
)
|
||||
|
||||
@property
|
||||
def current_user(self) -> Dict[str, Any]:
|
||||
if self._current_user is None:
|
||||
self._current_user = self.client.get_user_info()
|
||||
return self._current_user
|
||||
|
||||
def list_all_repositories(self) -> List[Dict[str, Any]]:
|
||||
all_repos = []
|
||||
page = 1
|
||||
|
||||
while True:
|
||||
repos = self.client.list_repositories(page=page)
|
||||
if not repos:
|
||||
break
|
||||
all_repos.extend(repos)
|
||||
page += 1
|
||||
|
||||
return all_repos
|
||||
|
||||
def list_organization_repositories(self, org_name: str, page: int = None, per_page: int = None) -> List[Dict[str, Any]]:
|
||||
"""List all repositories for an organization"""
|
||||
if page is not None and per_page is not None:
|
||||
# Single page request
|
||||
try:
|
||||
repos = self.client._request("GET", f"orgs/{org_name}/repos", params={"page": page, "limit": per_page})
|
||||
return repos if repos else []
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to list org repositories: {e}")
|
||||
return []
|
||||
else:
|
||||
# Get all pages
|
||||
all_repos = []
|
||||
current_page = 1
|
||||
|
||||
while True:
|
||||
try:
|
||||
repos = self.client._request("GET", f"orgs/{org_name}/repos", params={"page": current_page, "limit": 50})
|
||||
if not repos:
|
||||
break
|
||||
all_repos.extend(repos)
|
||||
current_page += 1
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to list org repositories: {e}")
|
||||
break
|
||||
|
||||
return all_repos
|
||||
|
||||
def create_repository(self, name: str, description: str = "", private: bool = False,
|
||||
auto_init: bool = True, gitignore: str = "", license: str = "",
|
||||
organization: str = None) -> Dict[str, Any]:
|
||||
try:
|
||||
# Try to create in organization first
|
||||
if organization:
|
||||
try:
|
||||
data = {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"private": private,
|
||||
"auto_init": auto_init,
|
||||
"gitignores": gitignore,
|
||||
"license": license,
|
||||
"default_branch": "main" # Use main instead of master
|
||||
}
|
||||
repo = self.client._request("POST", f"orgs/{organization}/repos", json=data)
|
||||
logger.info(f"Repository '{name}' created successfully in organization '{organization}'")
|
||||
return repo
|
||||
except Exception as org_error:
|
||||
logger.error(f"Failed to create in organization {organization}: {org_error}")
|
||||
# Don't fall back - raise the error so user knows what happened
|
||||
raise Exception(f"Konnte Repository nicht in Organisation '{organization}' erstellen: {str(org_error)}")
|
||||
|
||||
# Create as user repository
|
||||
repo = self.client.create_repository(
|
||||
name=name,
|
||||
description=description,
|
||||
private=private,
|
||||
auto_init=auto_init,
|
||||
gitignores=gitignore,
|
||||
license=license
|
||||
)
|
||||
logger.info(f"Repository '{name}' created successfully as user repository")
|
||||
return repo
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create repository '{name}': {e}")
|
||||
raise
|
||||
|
||||
def delete_repository(self, repo_name: str) -> bool:
|
||||
try:
|
||||
owner = self.current_user["username"]
|
||||
self.client.delete_repository(owner, repo_name)
|
||||
logger.info(f"Repository '{repo_name}' deleted successfully")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete repository '{repo_name}': {e}")
|
||||
return False
|
||||
|
||||
def clone_repository(self, repo_name: str, clone_dir: Optional[Path] = None) -> Tuple[bool, Path]:
|
||||
owner = self.current_user["username"]
|
||||
return self.git_ops.clone_repository(owner, repo_name, clone_dir)
|
||||
|
||||
def get_repository_info(self, repo_name: str) -> Optional[Dict[str, Any]]:
|
||||
try:
|
||||
owner = self.current_user["username"]
|
||||
return self.client.get_repository(owner, repo_name)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get repository info for '{repo_name}': {e}")
|
||||
return None
|
||||
|
||||
def fork_repository(self, owner: str, repo_name: str) -> Optional[Dict[str, Any]]:
|
||||
try:
|
||||
fork = self.client.fork_repository(owner, repo_name)
|
||||
logger.info(f"Repository '{owner}/{repo_name}' forked successfully")
|
||||
return fork
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fork repository '{owner}/{repo_name}': {e}")
|
||||
return None
|
||||
|
||||
def search_repositories(self, query: str) -> List[Dict[str, Any]]:
|
||||
all_repos = self.list_all_repositories()
|
||||
query_lower = query.lower()
|
||||
|
||||
return [
|
||||
repo for repo in all_repos
|
||||
if query_lower in repo["name"].lower() or
|
||||
query_lower in repo.get("description", "").lower()
|
||||
]
|
||||
|
||||
def sync_repository(self, repo_path: Path) -> Tuple[bool, str]:
|
||||
success, fetch_result = self.git_ops.fetch(repo_path)
|
||||
if not success:
|
||||
return False, f"Fetch failed: {fetch_result}"
|
||||
|
||||
success, pull_result = self.git_ops.pull(repo_path)
|
||||
if not success:
|
||||
return False, f"Pull failed: {pull_result}"
|
||||
|
||||
return True, "Repository synchronized successfully"
|
||||
|
||||
def commit_and_push(self, repo_path: Path, message: str,
|
||||
files: Optional[List[str]] = None) -> Tuple[bool, str]:
|
||||
success, add_result = self.git_ops.add(repo_path, files)
|
||||
if not success:
|
||||
return False, f"Add failed: {add_result}"
|
||||
|
||||
success, commit_result = self.git_ops.commit(repo_path, message)
|
||||
if not success:
|
||||
return False, f"Commit failed: {commit_result}"
|
||||
|
||||
success, push_result = self.git_ops.push(repo_path)
|
||||
if not success:
|
||||
return False, f"Push failed: {push_result}"
|
||||
|
||||
return True, "Changes committed and pushed successfully"
|
||||
|
||||
def get_repository_status(self, repo_path: Path) -> Dict[str, Any]:
|
||||
success, status = self.git_ops.status(repo_path)
|
||||
success_branch, branches = self.git_ops.branch(repo_path)
|
||||
success_remote, remotes = self.git_ops.remote_list(repo_path)
|
||||
|
||||
current_branch = None
|
||||
if success_branch:
|
||||
for line in branches.split('\n'):
|
||||
if line.startswith('*'):
|
||||
current_branch = line[2:].strip()
|
||||
break
|
||||
|
||||
return {
|
||||
"has_changes": bool(status.strip()) if success else None,
|
||||
"status": status if success else "Unable to get status",
|
||||
"current_branch": current_branch,
|
||||
"remotes": remotes if success_remote else "Unable to get remotes"
|
||||
}
|
||||
|
||||
def create_branch(self, repo_path: Path, branch_name: str) -> Tuple[bool, str]:
|
||||
return self.git_ops.checkout(repo_path, branch_name, create=True)
|
||||
|
||||
def switch_branch(self, repo_path: Path, branch_name: str) -> Tuple[bool, str]:
|
||||
return self.git_ops.checkout(repo_path, branch_name)
|
||||
|
||||
def list_branches(self, repo_name: str) -> List[Dict[str, Any]]:
|
||||
try:
|
||||
owner = self.current_user["username"]
|
||||
return self.client.list_branches(owner, repo_name)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to list branches for '{repo_name}': {e}")
|
||||
return []
|
||||
|
||||
def create_remote_branch(self, repo_name: str, branch_name: str,
|
||||
base_branch: str = "main") -> Optional[Dict[str, Any]]:
|
||||
try:
|
||||
owner = self.current_user["username"]
|
||||
return self.client.create_branch(owner, repo_name, branch_name, base_branch)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create branch '{branch_name}' in '{repo_name}': {e}")
|
||||
return None
|
||||
|
||||
def delete_remote_branch(self, repo_name: str, branch_name: str) -> bool:
|
||||
try:
|
||||
owner = self.current_user["username"]
|
||||
self.client.delete_branch(owner, repo_name, branch_name)
|
||||
logger.info(f"Branch '{branch_name}' deleted from '{repo_name}'")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete branch '{branch_name}' from '{repo_name}': {e}")
|
||||
return False
|
||||
|
||||
def push_local_repo_to_gitea(self, local_repo_path: Path, repo_name: str,
|
||||
description: str = "", private: bool = False,
|
||||
branch: str = "main", organization: str = None) -> Tuple[bool, str]:
|
||||
"""Create a new repo on Gitea and push an existing local repository to it"""
|
||||
try:
|
||||
# First create the repository on Gitea
|
||||
repo = self.create_repository(
|
||||
name=repo_name,
|
||||
description=description,
|
||||
private=private,
|
||||
auto_init=False, # Important: don't initialize since we're pushing existing code
|
||||
organization=organization
|
||||
)
|
||||
|
||||
# Determine the correct owner
|
||||
if organization:
|
||||
owner = organization
|
||||
elif 'owner' in repo and repo['owner']:
|
||||
owner = repo['owner']['username'] if 'username' in repo['owner'] else repo['owner']['login']
|
||||
else:
|
||||
owner = self.current_user["username"]
|
||||
|
||||
logger.info(f"Repository created, owner determined as: {owner}")
|
||||
|
||||
# Then push the local repository
|
||||
success, result = self.git_ops.push_existing_repo_to_gitea(
|
||||
local_repo_path, owner, repo_name, branch
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info(f"Successfully pushed local repository to '{repo_name}'")
|
||||
return True, f"Repository '{repo_name}' created and pushed successfully"
|
||||
else:
|
||||
logger.error(f"Failed to push to '{repo_name}': {result}")
|
||||
return False, f"Repository created but push failed: {result}"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to push local repository: {e}")
|
||||
return False, str(e)
|
||||
32
start.bat
Normale Datei
32
start.bat
Normale Datei
@ -0,0 +1,32 @@
|
||||
@echo off
|
||||
title Claude Project Manager
|
||||
echo Starting Claude Project Manager...
|
||||
echo.
|
||||
|
||||
:: Check if Python is installed
|
||||
python --version >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo ERROR: Python is not installed or not in PATH
|
||||
echo Please install Python 3.8 or higher
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
:: Check if requirements are installed
|
||||
python -c "import customtkinter" >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo Installing required packages...
|
||||
pip install -r requirements.txt
|
||||
echo.
|
||||
)
|
||||
|
||||
:: Start the application
|
||||
echo Launching application...
|
||||
python main.py
|
||||
|
||||
:: If app crashes, keep window open
|
||||
if errorlevel 1 (
|
||||
echo.
|
||||
echo Application crashed!
|
||||
pause
|
||||
)
|
||||
264
terminal_launcher.py
Normale Datei
264
terminal_launcher.py
Normale Datei
@ -0,0 +1,264 @@
|
||||
"""
|
||||
Terminal Launcher Module
|
||||
Handles launching Claude in WSL terminal with specified directory
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import os
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
from typing import Optional, Union
|
||||
from utils.logger import logger
|
||||
|
||||
class TerminalLauncher:
|
||||
def __init__(self):
|
||||
self.claude_path = self.find_claude_path()
|
||||
|
||||
def find_claude_path(self) -> str:
|
||||
"""Find Claude path dynamically in WSL"""
|
||||
# Try to find claude using 'which' command in WSL with login shell
|
||||
try:
|
||||
# Hide console window on Windows
|
||||
startupinfo = None
|
||||
if hasattr(subprocess, 'STARTUPINFO'):
|
||||
startupinfo = subprocess.STARTUPINFO()
|
||||
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||
startupinfo.wShowWindow = subprocess.SW_HIDE
|
||||
|
||||
# Use login shell to ensure all PATH and aliases are loaded
|
||||
command = ["wsl", "bash", "-l", "-c", "which claude 2>/dev/null || command -v claude 2>/dev/null || type -p claude 2>/dev/null"]
|
||||
logger.debug(f"Executing command to find Claude: {' '.join(command)}")
|
||||
result = subprocess.run(
|
||||
command,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
startupinfo=startupinfo
|
||||
)
|
||||
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
claude_path = result.stdout.strip()
|
||||
# Clean up any extra output from 'type' command
|
||||
if " is " in claude_path:
|
||||
claude_path = claude_path.split(" is ")[-1].strip()
|
||||
logger.info(f"Found Claude at: {claude_path}")
|
||||
print(f"Found Claude at: {claude_path}")
|
||||
return claude_path
|
||||
except Exception as e:
|
||||
logger.error(f"Error finding Claude path: {e}")
|
||||
print(f"Error finding Claude path: {e}")
|
||||
|
||||
# Fallback paths to try
|
||||
fallback_paths = [
|
||||
"claude", # Try just 'claude' - let WSL PATH handle it
|
||||
"/usr/local/bin/claude",
|
||||
"/usr/bin/claude",
|
||||
"~/.local/bin/claude",
|
||||
"/home/$USER/.nvm/versions/node/*/bin/claude" # Wildcard for any node version
|
||||
]
|
||||
|
||||
# Test each fallback path
|
||||
for path in fallback_paths:
|
||||
try:
|
||||
# Hide console window on Windows
|
||||
startupinfo = None
|
||||
if hasattr(subprocess, 'STARTUPINFO'):
|
||||
startupinfo = subprocess.STARTUPINFO()
|
||||
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||
startupinfo.wShowWindow = subprocess.SW_HIDE
|
||||
|
||||
result = subprocess.run(
|
||||
["wsl", "bash", "-c", f"test -f {path} && echo 'exists'"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=2,
|
||||
startupinfo=startupinfo
|
||||
)
|
||||
if result.stdout.strip() == 'exists':
|
||||
print(f"Found Claude at fallback: {path}")
|
||||
return path
|
||||
except:
|
||||
continue
|
||||
|
||||
# If nothing found, default to 'claude' and hope it's in PATH
|
||||
print("Claude path not found, using 'claude' (hoping it's in PATH)")
|
||||
return "claude"
|
||||
|
||||
def convert_to_wsl_path(self, windows_path: str) -> str:
|
||||
"""Convert Windows path to WSL format"""
|
||||
# Handle different path formats
|
||||
windows_path = windows_path.replace('/', '\\')
|
||||
|
||||
# Extract drive letter and path
|
||||
if ':' in windows_path:
|
||||
drive = windows_path[0].lower()
|
||||
path = windows_path[2:].replace('\\', '/')
|
||||
return f'/mnt/{drive}{path}'
|
||||
return windows_path
|
||||
|
||||
def launch_claude_wsl(self, project_path: str, project_name: str = "Claude Session") -> Union[subprocess.Popen, None]:
|
||||
"""Launch Claude in WSL terminal for specified directory"""
|
||||
try:
|
||||
wsl_path = self.convert_to_wsl_path(project_path)
|
||||
logger.info(f"Launching Claude for project: {project_name} at path: {project_path}")
|
||||
|
||||
# Check if Windows Terminal is available
|
||||
if os.path.exists(r"C:\Windows\System32\wt.exe"):
|
||||
# Use Windows Terminal
|
||||
cmd = [
|
||||
"wt.exe",
|
||||
"-w", "0",
|
||||
"new-tab",
|
||||
"--title", f"Claude - {project_name}",
|
||||
"--suppressApplicationTitle",
|
||||
"--",
|
||||
"wsl", "bash", "-l", "-i", "-c",
|
||||
f"cd '{wsl_path}' && echo 'Working directory: {wsl_path}' && echo '' && claude || (echo 'Error: Claude not found. Please install Claude in WSL.' && exec bash)"
|
||||
]
|
||||
|
||||
logger.info(f"Executing Windows Terminal command: {' '.join(cmd)}")
|
||||
process = subprocess.Popen(cmd, shell=False)
|
||||
logger.info(f"Successfully launched Claude in Windows Terminal for: {project_path}")
|
||||
print(f"Launched Claude in Windows Terminal for: {project_path}")
|
||||
return process
|
||||
|
||||
else:
|
||||
# Fallback to cmd window
|
||||
batch_content = f'''@echo off
|
||||
title Claude - {project_name}
|
||||
echo Starting Claude for project: {project_name}
|
||||
echo Working directory: {project_path}
|
||||
echo.
|
||||
wsl bash -l -i -c "cd '{wsl_path}' && claude || (echo 'Error: Claude not found. Please install Claude in WSL.' && exec bash)"
|
||||
pause
|
||||
'''
|
||||
|
||||
# Create temporary batch file
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.bat', delete=False) as f:
|
||||
f.write(batch_content)
|
||||
batch_path = f.name
|
||||
|
||||
# Launch in new cmd window
|
||||
cmd = ['cmd', '/c', 'start', '', batch_path]
|
||||
logger.info(f"Executing CMD command: {' '.join(cmd)}")
|
||||
logger.debug(f"Batch file content:\n{batch_content}")
|
||||
process = subprocess.Popen(cmd, shell=False)
|
||||
logger.info(f"Successfully launched Claude in CMD for: {project_path}")
|
||||
print(f"Launched Claude in CMD for: {project_path}")
|
||||
return process
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error launching Claude: {str(e)}", extra={"project_path": project_path, "project_name": project_name})
|
||||
print(f"Error launching Claude: {str(e)}")
|
||||
return None
|
||||
|
||||
def launch_claude_vps(self, server: str, username: str, password: str) -> bool:
|
||||
"""Launch Claude on VPS via SSH"""
|
||||
try:
|
||||
# Create SSH command with password
|
||||
ssh_command = f'ssh {username}@{server}'
|
||||
|
||||
# Create batch file for SSH connection
|
||||
batch_content = f'''@echo off
|
||||
title Claude - VPS Server
|
||||
echo Connecting to VPS Server...
|
||||
echo Server: {server}
|
||||
echo Username: {username}
|
||||
echo.
|
||||
echo Please enter password when prompted: {password}
|
||||
echo.
|
||||
echo After connection, type 'claude' to start Claude
|
||||
echo.
|
||||
{ssh_command}
|
||||
pause
|
||||
'''
|
||||
|
||||
# Create temporary batch file
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.bat', delete=False) as f:
|
||||
f.write(batch_content)
|
||||
batch_path = f.name
|
||||
|
||||
# Launch in new window
|
||||
if os.path.exists(r"C:\Windows\System32\wt.exe"):
|
||||
# Windows Terminal
|
||||
cmd = [
|
||||
"wt.exe",
|
||||
"-w", "0",
|
||||
"new-tab",
|
||||
"--title", "Claude - VPS",
|
||||
"--",
|
||||
"cmd", "/c", batch_path
|
||||
]
|
||||
subprocess.Popen(cmd)
|
||||
else:
|
||||
# CMD fallback
|
||||
subprocess.Popen(['cmd', '/c', 'start', '', batch_path])
|
||||
|
||||
print(f"Launched VPS connection to: {server}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error launching VPS connection: {str(e)}")
|
||||
return False
|
||||
|
||||
def launch_claude_admin_panel(self, project_name: str = "Admin Panel") -> Union[subprocess.Popen, None]:
|
||||
"""Launch Claude for Admin Panel with directory change to /opt/v2-Docker"""
|
||||
try:
|
||||
# Check if Windows Terminal is available
|
||||
if os.path.exists(r"C:\Windows\System32\wt.exe"):
|
||||
# Use Windows Terminal
|
||||
cmd = [
|
||||
"wt.exe",
|
||||
"-w", "0",
|
||||
"new-tab",
|
||||
"--title", f"Claude - {project_name}",
|
||||
"--suppressApplicationTitle",
|
||||
"--",
|
||||
"wsl", "bash", "-l", "-i", "-c",
|
||||
f"cd /opt/v2-Docker && echo 'Working directory: /opt/v2-Docker' && echo '' && claude || (echo 'Error: Claude not found. Please install Claude in WSL.' && exec bash)"
|
||||
]
|
||||
|
||||
process = subprocess.Popen(cmd, shell=False)
|
||||
print(f"Launched Claude in Windows Terminal for Admin Panel")
|
||||
return process
|
||||
|
||||
else:
|
||||
# Fallback to cmd window
|
||||
batch_content = f'''@echo off
|
||||
title Claude - {project_name}
|
||||
echo Starting Claude for: {project_name}
|
||||
echo Working directory: /opt/v2-Docker
|
||||
echo.
|
||||
wsl bash -l -i -c "cd /opt/v2-Docker && claude || (echo 'Error: Claude not found. Please install Claude in WSL.' && exec bash)"
|
||||
pause
|
||||
'''
|
||||
|
||||
# Create temporary batch file
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.bat', delete=False) as f:
|
||||
f.write(batch_content)
|
||||
batch_path = f.name
|
||||
|
||||
# Launch in new cmd window
|
||||
process = subprocess.Popen(['cmd', '/c', 'start', '', batch_path], shell=False)
|
||||
print(f"Launched Claude in CMD for Admin Panel")
|
||||
return process
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error launching Claude for Admin Panel: {str(e)}")
|
||||
return None
|
||||
|
||||
# Test functions
|
||||
if __name__ == "__main__":
|
||||
launcher = TerminalLauncher()
|
||||
|
||||
# Test WSL path conversion
|
||||
test_paths = [
|
||||
"C:\\Users\\hendr\\Desktop\\test",
|
||||
"D:/Projects/MyApp",
|
||||
"C:\\Program Files\\test"
|
||||
]
|
||||
|
||||
print("Testing WSL path conversion:")
|
||||
for path in test_paths:
|
||||
wsl_path = launcher.convert_to_wsl_path(path)
|
||||
print(f"{path} -> {wsl_path}")
|
||||
115
test_activity_connection.py
Normale Datei
115
test_activity_connection.py
Normale Datei
@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test-Skript für Activity Server Verbindung
|
||||
"""
|
||||
|
||||
import requests
|
||||
import socket
|
||||
import time
|
||||
from urllib.parse import urlparse
|
||||
|
||||
def test_connection(server_url="http://91.99.192.14:3001"):
|
||||
print(f"Teste Verbindung zu: {server_url}")
|
||||
print("-" * 50)
|
||||
|
||||
# Parse URL
|
||||
parsed = urlparse(server_url)
|
||||
host = parsed.hostname
|
||||
port = parsed.port or 80
|
||||
|
||||
# 1. DNS/IP Test
|
||||
print(f"1. Teste DNS-Auflösung für {host}...")
|
||||
try:
|
||||
ip = socket.gethostbyname(host)
|
||||
print(f" ✓ IP-Adresse: {ip}")
|
||||
except socket.gaierror as e:
|
||||
print(f" ✗ DNS-Fehler: {e}")
|
||||
return
|
||||
|
||||
# 2. Port Test
|
||||
print(f"\n2. Teste TCP-Verbindung zu {host}:{port}...")
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(5)
|
||||
try:
|
||||
result = sock.connect_ex((host, port))
|
||||
if result == 0:
|
||||
print(f" ✓ Port {port} ist erreichbar")
|
||||
else:
|
||||
print(f" ✗ Port {port} ist nicht erreichbar (Fehlercode: {result})")
|
||||
print(" Mögliche Ursachen:")
|
||||
print(" - Firewall blockiert die Verbindung")
|
||||
print(" - Server läuft nicht auf diesem Port")
|
||||
print(" - Netzwerk-Routing-Problem")
|
||||
return
|
||||
except Exception as e:
|
||||
print(f" ✗ Verbindungsfehler: {e}")
|
||||
return
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
# 3. HTTP Test
|
||||
print(f"\n3. Teste HTTP-Verbindung...")
|
||||
try:
|
||||
response = requests.get(f"{server_url}/health", timeout=5)
|
||||
print(f" ✓ HTTP Status: {response.status_code}")
|
||||
if response.status_code == 200:
|
||||
print(" ✓ Server antwortet korrekt")
|
||||
print(f" Response: {response.text[:100]}...")
|
||||
else:
|
||||
print(f" ⚠ Server antwortet mit Status {response.status_code}")
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
print(f" ✗ Verbindungsfehler: {e}")
|
||||
print(" Mögliche Ursachen:")
|
||||
print(" - Server ist nicht erreichbar")
|
||||
print(" - Firewall blockiert HTTP/HTTPS")
|
||||
except requests.exceptions.Timeout:
|
||||
print(" ✗ Timeout - Server antwortet nicht rechtzeitig")
|
||||
except Exception as e:
|
||||
print(f" ✗ Unerwarteter Fehler: {e}")
|
||||
|
||||
# 4. Socket.IO Test
|
||||
print(f"\n4. Teste Socket.IO Verbindung...")
|
||||
try:
|
||||
# Socket.IO initial handshake
|
||||
response = requests.get(
|
||||
f"{server_url}/socket.io/?transport=polling&EIO=4",
|
||||
timeout=5
|
||||
)
|
||||
print(f" Status: {response.status_code}")
|
||||
if response.status_code == 200:
|
||||
print(" ✓ Socket.IO Endpoint erreichbar")
|
||||
else:
|
||||
print(f" ✗ Socket.IO antwortet mit Status {response.status_code}")
|
||||
except Exception as e:
|
||||
print(f" ✗ Socket.IO Fehler: {e}")
|
||||
|
||||
# 5. Traceroute simulation (simplified)
|
||||
print(f"\n5. Netzwerk-Route (vereinfacht)...")
|
||||
import subprocess
|
||||
try:
|
||||
if socket.gethostname().lower().startswith("win"):
|
||||
cmd = ["tracert", "-h", "10", host]
|
||||
else:
|
||||
cmd = ["traceroute", "-m", "10", host]
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
|
||||
print(result.stdout[:500])
|
||||
except:
|
||||
print(" Traceroute nicht verfügbar")
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test mit dem Activity Server
|
||||
test_connection("http://91.99.192.14:3001")
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("DIAGNOSE:")
|
||||
print("=" * 50)
|
||||
print("\nWenn Port erreichbar aber HTTP nicht funktioniert:")
|
||||
print("- Prüfen Sie, ob der Activity Server korrekt läuft")
|
||||
print("- Prüfen Sie die Server-Logs")
|
||||
print("- Stellen Sie sicher, dass es ein HTTP-Server ist (nicht HTTPS)")
|
||||
print("\nWenn Port nicht erreichbar:")
|
||||
print("- Windows Firewall prüfen")
|
||||
print("- Router/Netzwerk-Firewall prüfen")
|
||||
print("- VPN-Verbindung prüfen (falls erforderlich)")
|
||||
print("- Prüfen ob Sie im richtigen Netzwerk sind")
|
||||
41
test_git_detection.py
Normale Datei
41
test_git_detection.py
Normale Datei
@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test script to check if git repository detection is working"""
|
||||
|
||||
import os
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
# Load projects
|
||||
with open('data/projects.json', 'r') as f:
|
||||
data = json.load(f)
|
||||
projects = data.get('projects', [])
|
||||
|
||||
print("Testing Git repository detection:\n")
|
||||
|
||||
for project in projects:
|
||||
if project['id'] not in ["vps-permanent", "admin-panel-permanent", "vps-docker-permanent"]:
|
||||
project_path = Path(project['path'])
|
||||
git_path = project_path / ".git"
|
||||
|
||||
print(f"Project: {project['name']}")
|
||||
print(f" Path: {project_path}")
|
||||
print(f" Git repo exists: {git_path.exists()}")
|
||||
|
||||
if git_path.exists():
|
||||
# Check remote
|
||||
import subprocess
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "remote", "-v"],
|
||||
cwd=project_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False
|
||||
)
|
||||
if result.returncode == 0:
|
||||
print(f" Remotes: {result.stdout.strip()}")
|
||||
else:
|
||||
print(f" Error checking remotes: {result.stderr}")
|
||||
except Exception as e:
|
||||
print(f" Error: {e}")
|
||||
print()
|
||||
213
test_refactoring.py
Normale Datei
213
test_refactoring.py
Normale Datei
@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for refactoring verification
|
||||
Tests that refactored methods work correctly
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
# Mock GUI dependencies
|
||||
sys.modules['customtkinter'] = MagicMock()
|
||||
sys.modules['tkinter'] = MagicMock()
|
||||
sys.modules['PIL'] = MagicMock()
|
||||
sys.modules['PIL.Image'] = MagicMock()
|
||||
|
||||
# Import after mocking
|
||||
from gui.config import refactoring_config
|
||||
from gui.main_window import MainWindow
|
||||
|
||||
def test_ui_helpers():
|
||||
"""Test UI helpers functionality"""
|
||||
print("\n=== Testing UI Helpers ===")
|
||||
|
||||
# Check current configuration
|
||||
status = refactoring_config.get_status()
|
||||
print(f"USE_UI_HELPERS: {status['flags']['USE_UI_HELPERS']}")
|
||||
|
||||
# Mock required components
|
||||
with MagicMock() as mock_project_manager:
|
||||
mock_project_manager.get_projects.return_value = []
|
||||
|
||||
# Test load_and_apply_theme
|
||||
print("\nTesting load_and_apply_theme...")
|
||||
try:
|
||||
# Create a minimal MainWindow instance
|
||||
# This will test if the refactored method is called
|
||||
import customtkinter as ctk
|
||||
ctk.CTk = MagicMock(return_value=MagicMock())
|
||||
|
||||
# The method should be called during __init__
|
||||
# Since we can't fully instantiate MainWindow, test the handler directly
|
||||
from gui.handlers.ui_helpers import UIHelpersHandler
|
||||
|
||||
mock_main_window = MagicMock()
|
||||
mock_main_window.root = MagicMock()
|
||||
mock_main_window.main_container = MagicMock()
|
||||
|
||||
handler = UIHelpersHandler(mock_main_window)
|
||||
handler.load_and_apply_theme()
|
||||
print("✅ load_and_apply_theme executed successfully")
|
||||
|
||||
# Test _show_scrollable_info
|
||||
print("\nTesting _show_scrollable_info...")
|
||||
handler._show_scrollable_info("Test Title", "Test Content")
|
||||
print("✅ _show_scrollable_info executed successfully")
|
||||
|
||||
# Test create_header
|
||||
print("\nTesting create_header...")
|
||||
handler.create_header()
|
||||
print("✅ create_header executed successfully")
|
||||
|
||||
# Test create_status_bar
|
||||
print("\nTesting create_status_bar...")
|
||||
handler.create_status_bar()
|
||||
print("✅ create_status_bar executed successfully")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def test_project_handler():
|
||||
"""Test project handler functionality"""
|
||||
print("\n=== Testing Project Handler ===")
|
||||
|
||||
from gui.handlers.project_manager import ProjectManagerHandler
|
||||
from project_manager import Project
|
||||
|
||||
mock_main_window = MagicMock()
|
||||
mock_main_window.project_manager = MagicMock()
|
||||
mock_main_window.refresh_projects = MagicMock()
|
||||
mock_main_window.update_status = MagicMock()
|
||||
|
||||
handler = ProjectManagerHandler(mock_main_window)
|
||||
|
||||
# Test delete_project
|
||||
print("\nTesting delete_project...")
|
||||
try:
|
||||
mock_project = MagicMock()
|
||||
mock_project.name = "Test Project"
|
||||
mock_project.id = "test-id"
|
||||
|
||||
# Mock the dialog to return False (no delete)
|
||||
with MagicMock() as mock_messagebox:
|
||||
mock_messagebox.askyesno.return_value = False
|
||||
handler.delete_project(mock_project)
|
||||
print("✅ delete_project executed (no delete)")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def test_process_handler():
|
||||
"""Test process handler functionality"""
|
||||
print("\n=== Testing Process Handler ===")
|
||||
|
||||
from gui.handlers.process_manager import ProcessManagerHandler
|
||||
|
||||
mock_main_window = MagicMock()
|
||||
mock_main_window.status_label = MagicMock()
|
||||
|
||||
handler = ProcessManagerHandler(mock_main_window)
|
||||
|
||||
# Test update_status
|
||||
print("\nTesting update_status...")
|
||||
try:
|
||||
handler.update_status("Test message", error=False)
|
||||
print("✅ update_status executed successfully")
|
||||
|
||||
handler.update_status("Error message", error=True)
|
||||
print("✅ update_status with error executed successfully")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# Note: download_log requires file dialog interaction, harder to test
|
||||
|
||||
def test_gitea_handler():
|
||||
"""Test Gitea handler functionality"""
|
||||
print("\n=== Testing Gitea Handler ===")
|
||||
|
||||
from gui.handlers.gitea_operations import GiteaOperationsHandler
|
||||
|
||||
mock_main_window = MagicMock()
|
||||
mock_main_window._show_scrollable_info = MagicMock()
|
||||
mock_repo_manager = MagicMock()
|
||||
mock_repo_manager.client = MagicMock()
|
||||
mock_repo_manager.git_ops = MagicMock()
|
||||
|
||||
mock_main_window.repo_manager = mock_repo_manager
|
||||
|
||||
handler = GiteaOperationsHandler(mock_main_window)
|
||||
|
||||
# Test consolidated test_gitea_connection
|
||||
print("\nTesting test_gitea_connection (consolidated)...")
|
||||
try:
|
||||
# Mock the API calls
|
||||
mock_repo_manager.client.get_user_info.return_value = {'username': 'testuser'}
|
||||
mock_repo_manager.client.get_user_orgs.return_value = []
|
||||
mock_repo_manager.client.get_user_teams.return_value = []
|
||||
mock_repo_manager.client.get_user_repos.return_value = []
|
||||
|
||||
handler.test_gitea_connection()
|
||||
print("✅ test_gitea_connection executed successfully (without project)")
|
||||
|
||||
# Test with project
|
||||
mock_project = MagicMock()
|
||||
mock_project.name = "Test Project"
|
||||
mock_project.path = "/test/path"
|
||||
|
||||
handler.test_gitea_connection(mock_project)
|
||||
print("✅ test_gitea_connection executed successfully (with project)")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def main():
|
||||
"""Run all tests"""
|
||||
print("=== Refactoring Test Suite ===")
|
||||
|
||||
# Show current configuration
|
||||
print("\nCurrent Configuration:")
|
||||
status = refactoring_config.get_status()
|
||||
for flag, value in status['flags'].items():
|
||||
if flag.startswith('USE_'):
|
||||
status_str = "✅ ENABLED" if value else "❌ DISABLED"
|
||||
print(f" {flag}: {status_str}")
|
||||
|
||||
# Run tests based on enabled features
|
||||
if status['flags']['USE_UI_HELPERS']:
|
||||
test_ui_helpers()
|
||||
else:
|
||||
print("\n⚠️ UI Helpers not enabled. Enable with:")
|
||||
print(" python3 manage_refactoring.py enable ui")
|
||||
|
||||
if status['flags']['USE_PROJECT_HANDLER']:
|
||||
test_project_handler()
|
||||
else:
|
||||
print("\n⚠️ Project Handler not enabled. Enable with:")
|
||||
print(" python3 manage_refactoring.py enable project")
|
||||
|
||||
if status['flags']['USE_PROCESS_HANDLER']:
|
||||
test_process_handler()
|
||||
else:
|
||||
print("\n⚠️ Process Handler not enabled. Enable with:")
|
||||
print(" python3 manage_refactoring.py enable process")
|
||||
|
||||
if status['flags']['USE_GITEA_HANDLER']:
|
||||
test_gitea_handler()
|
||||
else:
|
||||
print("\n⚠️ Gitea Handler not enabled. Enable with:")
|
||||
print(" python3 manage_refactoring.py enable gitea")
|
||||
|
||||
print("\n✅ Test completed")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
150
tests/test_main_window_api.py
Normale Datei
150
tests/test_main_window_api.py
Normale Datei
@ -0,0 +1,150 @@
|
||||
"""
|
||||
API Compatibility Tests for MainWindow Refactoring
|
||||
Dokumentiert alle öffentlichen Methoden der MainWindow Klasse
|
||||
um sicherzustellen, dass keine Breaking Changes auftreten.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
# Mock all GUI dependencies before import
|
||||
sys.modules['customtkinter'] = MagicMock()
|
||||
sys.modules['tkinter'] = MagicMock()
|
||||
sys.modules['PIL'] = MagicMock()
|
||||
sys.modules['PIL.Image'] = MagicMock()
|
||||
|
||||
class TestMainWindowAPI(unittest.TestCase):
|
||||
"""Test to ensure all public methods exist after refactoring"""
|
||||
|
||||
# Liste aller öffentlichen Methoden der MainWindow Klasse
|
||||
# Generiert aus der Analyse vom 2025-07-05
|
||||
REQUIRED_PUBLIC_METHODS = [
|
||||
'__init__',
|
||||
'setup_ui',
|
||||
'create_header',
|
||||
'create_content_area',
|
||||
'create_status_bar',
|
||||
'refresh_projects',
|
||||
'create_project_tile',
|
||||
'create_add_tile',
|
||||
'create_project_tile_flow',
|
||||
'create_add_tile_flow',
|
||||
'add_new_project',
|
||||
'open_project',
|
||||
'open_vps_connection',
|
||||
'open_admin_panel',
|
||||
'open_vps_docker',
|
||||
'open_readme',
|
||||
'delete_project',
|
||||
'rename_project',
|
||||
'generate_readme_background',
|
||||
'update_status',
|
||||
'download_log',
|
||||
'refresh_ui',
|
||||
'load_and_apply_theme',
|
||||
'on_window_resize',
|
||||
'stop_project',
|
||||
'setup_interaction_tracking',
|
||||
'monitor_process',
|
||||
'check_process_status',
|
||||
'open_gitea_window',
|
||||
'on_gitea_repo_select',
|
||||
'clear_project_selection',
|
||||
'on_project_select',
|
||||
'show_git_status',
|
||||
'commit_changes',
|
||||
'push_to_gitea',
|
||||
'pull_from_gitea',
|
||||
'fetch_from_gitea',
|
||||
'manage_branches',
|
||||
'link_to_gitea',
|
||||
'clone_repository',
|
||||
'create_project_from_repo',
|
||||
'on_closing',
|
||||
'gitea_operation',
|
||||
'init_and_push_to_gitea',
|
||||
'init_git_repo',
|
||||
'test_gitea_connection',
|
||||
'verify_repository_on_gitea',
|
||||
'fix_repository_issues',
|
||||
'manage_large_files',
|
||||
'setup_git_lfs',
|
||||
'run'
|
||||
]
|
||||
|
||||
# Private/Protected Methoden die NICHT Teil der öffentlichen API sind
|
||||
PRIVATE_METHODS = [
|
||||
'_differential_update',
|
||||
'_update_project_tiles_colors',
|
||||
'_on_click_start',
|
||||
'_on_click_end',
|
||||
'_on_dropdown_select',
|
||||
'_on_focus_in',
|
||||
'_on_focus_out',
|
||||
'_check_pending_updates',
|
||||
'_handle_process_ended',
|
||||
'_create_project_after_clone',
|
||||
'_show_scrollable_info'
|
||||
]
|
||||
|
||||
@patch('gui.main_window.logger')
|
||||
@patch('gui.main_window.ProjectProcessTracker')
|
||||
@patch('gui.main_window.ProcessManager')
|
||||
@patch('gui.main_window.VPSConnection')
|
||||
@patch('gui.main_window.ReadmeGenerator')
|
||||
@patch('gui.main_window.TerminalLauncher')
|
||||
@patch('gui.main_window.ProjectManager')
|
||||
@patch('gui.main_window.RepositoryManager')
|
||||
def test_all_public_methods_exist(self, *mocks):
|
||||
"""Testet dass alle öffentlichen Methoden existieren"""
|
||||
# Import after all mocks are set up
|
||||
from gui.main_window import MainWindow
|
||||
|
||||
# Check that all required methods exist
|
||||
missing_methods = []
|
||||
for method_name in self.REQUIRED_PUBLIC_METHODS:
|
||||
if not hasattr(MainWindow, method_name):
|
||||
missing_methods.append(method_name)
|
||||
|
||||
self.assertEqual(
|
||||
missing_methods,
|
||||
[],
|
||||
f"Following public methods are missing: {missing_methods}"
|
||||
)
|
||||
|
||||
def test_no_unexpected_public_methods(self):
|
||||
"""Warnt vor neuen öffentlichen Methoden die dokumentiert werden sollten"""
|
||||
# This test will be implemented after import works
|
||||
pass
|
||||
|
||||
|
||||
class TestMainWindowBehavior(unittest.TestCase):
|
||||
"""Behavioral tests for critical MainWindow operations"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures"""
|
||||
self.window = None
|
||||
|
||||
def test_project_lifecycle(self):
|
||||
"""Test basic project creation, opening and deletion"""
|
||||
# TODO: Implement after mocking is complete
|
||||
pass
|
||||
|
||||
def test_gitea_operations(self):
|
||||
"""Test Gitea integration operations"""
|
||||
# TODO: Test init_and_push_to_gitea, push_to_gitea, etc.
|
||||
pass
|
||||
|
||||
def test_duplicate_methods_behavior(self):
|
||||
"""Ensure duplicate methods have consistent behavior"""
|
||||
# TODO: Compare behavior of duplicate methods
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
23
tools/download_winscp.txt
Normale Datei
23
tools/download_winscp.txt
Normale Datei
@ -0,0 +1,23 @@
|
||||
WinSCP Portable Download Instructions
|
||||
=====================================
|
||||
|
||||
1. Download WinSCP Portable from:
|
||||
https://winscp.net/download/WinSCP-6.3.6-Portable.zip
|
||||
|
||||
Alternative direct link:
|
||||
https://sourceforge.net/projects/winscp/files/WinSCP/6.3.6/WinSCP-6.3.6-Portable.zip/download
|
||||
|
||||
2. Extract the ZIP file to this folder:
|
||||
tools/WinSCP/
|
||||
|
||||
3. The structure should be:
|
||||
tools/
|
||||
└── WinSCP/
|
||||
├── WinSCP.exe
|
||||
├── WinSCP.com
|
||||
└── (other files)
|
||||
|
||||
4. After extraction, the WinSCP button in Claude Project Manager will work automatically.
|
||||
|
||||
Note: WinSCP Portable is about 10 MB and doesn't require installation.
|
||||
It's a trusted SFTP/FTP client for Windows.
|
||||
210
utils/logger.py
Normale Datei
210
utils/logger.py
Normale Datei
@ -0,0 +1,210 @@
|
||||
import logging
|
||||
import datetime
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Optional, Any
|
||||
import json
|
||||
from threading import Lock
|
||||
import traceback
|
||||
import inspect
|
||||
|
||||
class ApplicationLogger:
|
||||
_instance = None
|
||||
_lock = Lock()
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
with cls._lock:
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
if not hasattr(self, 'initialized'):
|
||||
self.initialized = True
|
||||
self.log_entries = []
|
||||
self.max_entries = 50000 # Increased for more detailed logging
|
||||
self.interaction_count = 0
|
||||
|
||||
# Create logs directory if it doesn't exist
|
||||
self.log_dir = Path("logs")
|
||||
self.log_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Setup file logging
|
||||
self.log_file = self.log_dir / f"cpm_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
|
||||
|
||||
# Configure logger
|
||||
self.logger = logging.getLogger('CPM')
|
||||
self.logger.setLevel(logging.DEBUG)
|
||||
|
||||
# File handler
|
||||
file_handler = logging.FileHandler(self.log_file, encoding='utf-8')
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
|
||||
# Console handler
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(logging.INFO)
|
||||
|
||||
# Detailed Formatter
|
||||
formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S.%f'[:-3] # Include milliseconds
|
||||
)
|
||||
file_handler.setFormatter(formatter)
|
||||
console_handler.setFormatter(formatter)
|
||||
|
||||
# Add handlers
|
||||
self.logger.addHandler(file_handler)
|
||||
self.logger.addHandler(console_handler)
|
||||
|
||||
# Custom handler to store in memory
|
||||
self.memory_handler = MemoryHandler(self)
|
||||
self.memory_handler.setFormatter(formatter)
|
||||
self.logger.addHandler(self.memory_handler)
|
||||
|
||||
self.info("Claude Project Manager - Logger initialized")
|
||||
|
||||
def debug(self, message: str, extra: Optional[dict] = None):
|
||||
self._log('DEBUG', message, extra)
|
||||
|
||||
def info(self, message: str, extra: Optional[dict] = None):
|
||||
self._log('INFO', message, extra)
|
||||
|
||||
def warning(self, message: str, extra: Optional[dict] = None):
|
||||
self._log('WARNING', message, extra)
|
||||
|
||||
def error(self, message: str, extra: Optional[dict] = None, exc_info=None):
|
||||
self._log('ERROR', message, extra, exc_info)
|
||||
|
||||
def critical(self, message: str, extra: Optional[dict] = None, exc_info=None):
|
||||
self._log('CRITICAL', message, extra, exc_info)
|
||||
|
||||
def _log(self, level: str, message: str, extra: Optional[dict] = None, exc_info=None):
|
||||
# Get caller information
|
||||
frame = inspect.currentframe()
|
||||
if frame and frame.f_back and frame.f_back.f_back:
|
||||
caller = frame.f_back.f_back
|
||||
caller_info = f"{caller.f_code.co_filename}:{caller.f_lineno}"
|
||||
else:
|
||||
caller_info = "unknown"
|
||||
|
||||
log_method = getattr(self.logger, level.lower())
|
||||
if extra:
|
||||
message = f"{message} | Extra: {json.dumps(extra, default=str)}"
|
||||
|
||||
if exc_info:
|
||||
log_method(message, exc_info=exc_info)
|
||||
else:
|
||||
log_method(message)
|
||||
|
||||
def add_entry(self, timestamp: str, level: str, message: str):
|
||||
"""Add log entry to memory buffer"""
|
||||
with self._lock:
|
||||
self.log_entries.append({
|
||||
'timestamp': timestamp,
|
||||
'level': level,
|
||||
'message': message
|
||||
})
|
||||
|
||||
# Keep only the latest entries
|
||||
if len(self.log_entries) > self.max_entries:
|
||||
self.log_entries = self.log_entries[-self.max_entries:]
|
||||
|
||||
def get_log_content(self) -> str:
|
||||
"""Get all log entries as formatted string"""
|
||||
with self._lock:
|
||||
lines = []
|
||||
for entry in self.log_entries:
|
||||
lines.append(f"{entry['timestamp']} - {entry['level']} - {entry['message']}")
|
||||
return '\n'.join(lines)
|
||||
|
||||
def export_logs(self, filepath: str, include_system_info: bool = True):
|
||||
"""Export logs to a file with optional system information"""
|
||||
content = []
|
||||
|
||||
if include_system_info:
|
||||
content.append("=" * 80)
|
||||
content.append("CLAUDE PROJECT MANAGER - LOG EXPORT")
|
||||
content.append("=" * 80)
|
||||
content.append(f"Export Date: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
content.append(f"Log File: {self.log_file}")
|
||||
content.append(f"Total Interactions: {self.interaction_count}")
|
||||
content.append(f"Total Log Entries: {len(self.log_entries)}")
|
||||
content.append("=" * 80)
|
||||
content.append("")
|
||||
|
||||
content.append(self.get_log_content())
|
||||
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(content) if isinstance(content, list) else content)
|
||||
|
||||
def clear_logs(self):
|
||||
"""Clear in-memory log entries"""
|
||||
with self._lock:
|
||||
self.log_entries.clear()
|
||||
self.info("Log entries cleared")
|
||||
|
||||
class MemoryHandler(logging.Handler):
|
||||
"""Custom handler to store logs in memory"""
|
||||
def __init__(self, logger_instance):
|
||||
super().__init__()
|
||||
self.logger_instance = logger_instance
|
||||
|
||||
def emit(self, record):
|
||||
try:
|
||||
msg = self.format(record)
|
||||
# Extract timestamp, level and message
|
||||
parts = msg.split(' - ', 3)
|
||||
if len(parts) >= 4:
|
||||
timestamp = parts[0]
|
||||
level = parts[2]
|
||||
message = parts[3]
|
||||
self.logger_instance.add_entry(timestamp, level, message)
|
||||
except Exception:
|
||||
self.handleError(record)
|
||||
|
||||
def log_interaction(self, component: str, action: str, details: Optional[dict] = None):
|
||||
"""Log UI interactions"""
|
||||
self.interaction_count += 1
|
||||
interaction_msg = f"UI_INTERACTION #{self.interaction_count} - Component: {component}, Action: {action}"
|
||||
if details:
|
||||
interaction_msg += f", Details: {json.dumps(details, default=str)}"
|
||||
self.info(interaction_msg)
|
||||
|
||||
def log_git_operation(self, operation: str, status: str, details: Optional[dict] = None):
|
||||
"""Log Git operations with details"""
|
||||
git_msg = f"GIT_OPERATION - Operation: {operation}, Status: {status}"
|
||||
if details:
|
||||
git_msg += f", Details: {json.dumps(details, default=str)}"
|
||||
self.info(git_msg)
|
||||
|
||||
def log_exception(self, exception: Exception, context: str = ""):
|
||||
"""Log exceptions with full traceback"""
|
||||
tb = traceback.format_exc()
|
||||
error_msg = f"EXCEPTION in {context}: {type(exception).__name__}: {str(exception)}\nTraceback:\n{tb}"
|
||||
self.error(error_msg)
|
||||
|
||||
def log_method_call(self, method_name: str, args: tuple = None, kwargs: dict = None):
|
||||
"""Log method calls with arguments"""
|
||||
call_msg = f"METHOD_CALL - {method_name}"
|
||||
if args:
|
||||
call_msg += f", Args: {args}"
|
||||
if kwargs:
|
||||
call_msg += f", Kwargs: {kwargs}"
|
||||
self.debug(call_msg)
|
||||
|
||||
def log_state_change(self, component: str, old_state: Any, new_state: Any):
|
||||
"""Log state changes in the application"""
|
||||
state_msg = f"STATE_CHANGE - Component: {component}, Old: {old_state}, New: {new_state}"
|
||||
self.info(state_msg)
|
||||
|
||||
def get_full_log_path(self) -> str:
|
||||
"""Get the full path to the current log file"""
|
||||
return str(self.log_file.absolute())
|
||||
|
||||
def get_all_log_files(self) -> list:
|
||||
"""Get all log files in the log directory"""
|
||||
return sorted([f for f in self.log_dir.glob('*.log')], reverse=True)
|
||||
|
||||
# Global logger instance
|
||||
logger = ApplicationLogger()
|
||||
425
vps_connection.py
Normale Datei
425
vps_connection.py
Normale Datei
@ -0,0 +1,425 @@
|
||||
"""
|
||||
VPS Connection Module
|
||||
Handles SSH connection to VPS server
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import subprocess
|
||||
from utils.logger import logger
|
||||
|
||||
class VPSConnection:
|
||||
def __init__(self):
|
||||
self.server = "91.99.192.14"
|
||||
self.username = "claude-dev"
|
||||
self.password = "z0E1Al}q2H?Yqd!O"
|
||||
logger.info("VPSConnection initialized")
|
||||
|
||||
def create_ssh_script(self) -> str:
|
||||
"""Create a script for SSH connection with automatic password and Claude start"""
|
||||
import tempfile
|
||||
import os
|
||||
logger.info(f"Creating SSH script for VPS connection to {self.server}")
|
||||
|
||||
# Create a simple bash script for SSH connection
|
||||
bash_script = f'''#!/bin/bash
|
||||
echo "================================================================================"
|
||||
echo " Claude VPS Server Connection"
|
||||
echo "================================================================================"
|
||||
echo ""
|
||||
echo "Server: {self.server}"
|
||||
echo "Username: {self.username}"
|
||||
echo ""
|
||||
|
||||
# Try to use sshpass if available
|
||||
if command -v sshpass >/dev/null 2>&1; then
|
||||
echo "Connecting to VPS server and starting Claude automatically..."
|
||||
echo ""
|
||||
sshpass -p "{self.password}" ssh -o StrictHostKeyChecking=no -t {self.username}@{self.server} "claude || bash"
|
||||
else
|
||||
# Try to install sshpass
|
||||
echo "Installing sshpass for automatic login..."
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
sudo apt-get update -qq 2>/dev/null
|
||||
sudo apt-get install -y sshpass -qq 2>/dev/null
|
||||
fi
|
||||
|
||||
# Check again if sshpass is now available
|
||||
if command -v sshpass >/dev/null 2>&1; then
|
||||
echo "sshpass installed successfully!"
|
||||
echo "Connecting to VPS server and starting Claude automatically..."
|
||||
echo ""
|
||||
sshpass -p "{self.password}" ssh -o StrictHostKeyChecking=no -t {self.username}@{self.server} "claude || bash"
|
||||
else
|
||||
echo "Manual password entry required."
|
||||
echo ""
|
||||
echo "Password: {self.password}"
|
||||
echo "(Right-click to paste)"
|
||||
echo ""
|
||||
echo "After login, type: claude"
|
||||
echo ""
|
||||
ssh -o StrictHostKeyChecking=no {self.username}@{self.server}
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Connection closed."
|
||||
echo "Press Enter to exit..."
|
||||
read
|
||||
'''
|
||||
|
||||
# Save bash script with Unix line endings
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.sh', delete=False, newline='\n') as f:
|
||||
f.write(bash_script)
|
||||
bash_path = f.name
|
||||
|
||||
# Convert to WSL path
|
||||
bash_wsl_path = bash_path.replace('\\', '/').replace('C:', '/mnt/c')
|
||||
|
||||
script_content = f"""@echo off
|
||||
cls
|
||||
REM Convert line endings and launch WSL with the bash script
|
||||
wsl dos2unix {bash_wsl_path} 2>nul || wsl sed -i 's/\r$//' {bash_wsl_path}
|
||||
wsl chmod +x {bash_wsl_path}
|
||||
wsl bash {bash_wsl_path}
|
||||
|
||||
REM Clean up temp file
|
||||
del "{bash_path}" 2>nul
|
||||
"""
|
||||
return script_content
|
||||
|
||||
def create_readme_content(self) -> str:
|
||||
"""Generate README content for VPS project"""
|
||||
from datetime import datetime
|
||||
|
||||
content = f"""# Claude VPS Server
|
||||
|
||||
*This README was automatically generated by Claude Project Manager*
|
||||
|
||||
## Server Information
|
||||
|
||||
- **Server IP**: {self.server}
|
||||
- **Username**: {self.username}
|
||||
- **Connection**: SSH (Port 22)
|
||||
|
||||
## How to Connect
|
||||
|
||||
### Using Claude Project Manager
|
||||
1. Click on the VPS Server tile
|
||||
2. Terminal will open with connection instructions
|
||||
3. Enter the password when prompted
|
||||
4. Type `claude` after successful login
|
||||
|
||||
### Manual Connection
|
||||
```bash
|
||||
ssh {self.username}@{self.server}
|
||||
```
|
||||
|
||||
## Available Commands
|
||||
|
||||
After connecting to the VPS:
|
||||
- `claude` - Start Claude CLI
|
||||
- `ls` - List files
|
||||
- `cd <directory>` - Change directory
|
||||
- `exit` - Close SSH connection
|
||||
|
||||
## Security Notes
|
||||
|
||||
- Keep your password secure
|
||||
- Don't share SSH credentials
|
||||
- Always logout when finished (`exit` command)
|
||||
|
||||
## Features
|
||||
|
||||
This VPS server provides:
|
||||
- Remote access to Claude
|
||||
- Persistent environment
|
||||
- Isolated workspace
|
||||
- Full Linux environment
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection Issues
|
||||
1. Check internet connection
|
||||
2. Verify server is online
|
||||
3. Ensure SSH port (22) is not blocked
|
||||
4. Try manual SSH command
|
||||
|
||||
### Authentication Failed
|
||||
- Verify password is correct
|
||||
- Check username spelling
|
||||
- Ensure caps lock is off
|
||||
|
||||
---
|
||||
|
||||
## Connection Log
|
||||
|
||||
- README generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||||
"""
|
||||
return content
|
||||
|
||||
def generate_vps_readme(self, readme_path: str):
|
||||
"""Generate and save VPS README"""
|
||||
logger.info(f"Generating VPS README at: {readme_path}")
|
||||
content = self.create_readme_content()
|
||||
|
||||
# Create directory if needed
|
||||
os.makedirs(os.path.dirname(readme_path), exist_ok=True)
|
||||
|
||||
# Write README
|
||||
with open(readme_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
return readme_path
|
||||
|
||||
def create_admin_panel_script(self) -> str:
|
||||
"""Create a script for SSH connection to Admin Panel with directory change"""
|
||||
import tempfile
|
||||
import os
|
||||
logger.info("Creating Admin Panel SSH script")
|
||||
|
||||
# Create a simple bash script for SSH connection
|
||||
bash_script = f'''#!/bin/bash
|
||||
echo "================================================================================"
|
||||
echo " Claude Admin Panel Connection"
|
||||
echo "================================================================================"
|
||||
echo ""
|
||||
echo "Server: {self.server}"
|
||||
echo "Username: {self.username}"
|
||||
echo "Target Directory: /opt/v2-Docker"
|
||||
echo ""
|
||||
|
||||
# Try to use sshpass if available
|
||||
if command -v sshpass >/dev/null 2>&1; then
|
||||
echo "Connecting to VPS server and starting Claude in Admin Panel..."
|
||||
echo ""
|
||||
sshpass -p "{self.password}" ssh -o StrictHostKeyChecking=no -t {self.username}@{self.server} "cd /opt/v2-Docker && echo 'Changed to /opt/v2-Docker' && claude || bash"
|
||||
else
|
||||
# Try to install sshpass
|
||||
echo "Installing sshpass for automatic login..."
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
sudo apt-get update -qq 2>/dev/null
|
||||
sudo apt-get install -y sshpass -qq 2>/dev/null
|
||||
fi
|
||||
|
||||
# Check again if sshpass is now available
|
||||
if command -v sshpass >/dev/null 2>&1; then
|
||||
echo "sshpass installed successfully!"
|
||||
echo "Connecting to VPS server and starting Claude in Admin Panel..."
|
||||
echo ""
|
||||
sshpass -p "{self.password}" ssh -o StrictHostKeyChecking=no -t {self.username}@{self.server} "cd /opt/v2-Docker && echo 'Changed to /opt/v2-Docker' && claude || bash"
|
||||
else
|
||||
echo "Manual password entry required."
|
||||
echo ""
|
||||
echo "Password: {self.password}"
|
||||
echo "(Right-click to paste)"
|
||||
echo ""
|
||||
echo "After login:"
|
||||
echo "1. Type: cd /opt/v2-Docker"
|
||||
echo "2. Type: claude"
|
||||
echo ""
|
||||
ssh -o StrictHostKeyChecking=no {self.username}@{self.server}
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Connection closed."
|
||||
echo "Press Enter to exit..."
|
||||
read
|
||||
'''
|
||||
|
||||
# Save bash script with Unix line endings
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.sh', delete=False, newline='\n') as f:
|
||||
f.write(bash_script)
|
||||
bash_path = f.name
|
||||
|
||||
# Convert to WSL path
|
||||
bash_wsl_path = bash_path.replace('\\', '/').replace('C:', '/mnt/c')
|
||||
|
||||
script_content = f"""@echo off
|
||||
cls
|
||||
REM Convert line endings and launch WSL with the bash script
|
||||
wsl dos2unix {bash_wsl_path} 2>nul || wsl sed -i 's/\r$//' {bash_wsl_path}
|
||||
wsl chmod +x {bash_wsl_path}
|
||||
wsl bash {bash_wsl_path}
|
||||
|
||||
REM Clean up temp file
|
||||
del "{bash_path}" 2>nul
|
||||
"""
|
||||
return script_content
|
||||
|
||||
def create_vps_docker_script(self) -> str:
|
||||
"""Create a script for SSH connection to VPS Docker with docker compose commands"""
|
||||
import tempfile
|
||||
import os
|
||||
logger.info("Creating VPS Docker restart script")
|
||||
|
||||
# Create a bash script for SSH connection with docker commands
|
||||
bash_script = f'''#!/bin/bash
|
||||
echo "================================================================================"
|
||||
echo " VPS Docker Admin Panel Restart"
|
||||
echo "================================================================================"
|
||||
echo ""
|
||||
echo "Server: {self.server}"
|
||||
echo "Username: {self.username}"
|
||||
echo "Target Directory: /opt/v2-Docker/v2"
|
||||
echo ""
|
||||
|
||||
# Try to use sshpass if available
|
||||
if command -v sshpass >/dev/null 2>&1; then
|
||||
echo "Connecting to VPS server and restarting Admin Panel..."
|
||||
echo ""
|
||||
sshpass -p "{self.password}" ssh -o StrictHostKeyChecking=no -t {self.username}@{self.server} "cd /opt/v2-Docker/v2 && echo -e '\\n[Restarting Admin Panel Docker containers...]\\n' && docker compose down && echo -e '\\n[Building containers with no cache...]\\n' && docker compose build --no-cache && echo -e '\\n[Starting containers in detached mode...]\\n' && docker compose up -d && echo -e '\\n[SUCCESS] Admin Panel restart completed successfully!\\n' || echo -e '\\n[ERROR] Error during restart process!\\n'"
|
||||
else
|
||||
# Try to install sshpass
|
||||
echo "Installing sshpass for automatic login..."
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
sudo apt-get update -qq 2>/dev/null
|
||||
sudo apt-get install -y sshpass -qq 2>/dev/null
|
||||
fi
|
||||
|
||||
# Check again if sshpass is now available
|
||||
if command -v sshpass >/dev/null 2>&1; then
|
||||
echo "sshpass installed successfully!"
|
||||
echo "Connecting to VPS server and restarting Admin Panel..."
|
||||
echo ""
|
||||
sshpass -p "{self.password}" ssh -o StrictHostKeyChecking=no -t {self.username}@{self.server} "cd /opt/v2-Docker/v2 && echo -e '\\n[Restarting Admin Panel Docker containers...]\\n' && docker compose down && echo -e '\\n[Building containers with no cache...]\\n' && docker compose build --no-cache && echo -e '\\n[Starting containers in detached mode...]\\n' && docker compose up -d && echo -e '\\n[SUCCESS] Admin Panel restart completed successfully!\\n' || echo -e '\\n[ERROR] Error during restart process!\\n'"
|
||||
else
|
||||
echo "Manual password entry required."
|
||||
echo ""
|
||||
echo "Password: {self.password}"
|
||||
echo "(Right-click to paste)"
|
||||
echo ""
|
||||
echo "After login, the following commands will be executed:"
|
||||
echo "1. cd /opt/v2-Docker/v2"
|
||||
echo "2. docker compose down"
|
||||
echo "3. docker compose build --no-cache"
|
||||
echo "4. docker compose up -d"
|
||||
echo ""
|
||||
ssh -o StrictHostKeyChecking=no {self.username}@{self.server}
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Press Enter to close..."
|
||||
read
|
||||
'''
|
||||
|
||||
# Save bash script with Unix line endings
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.sh', delete=False, newline='\n') as f:
|
||||
f.write(bash_script)
|
||||
bash_path = f.name
|
||||
|
||||
# Convert to WSL path
|
||||
bash_wsl_path = bash_path.replace('\\', '/').replace('C:', '/mnt/c')
|
||||
|
||||
script_content = f"""@echo off
|
||||
cls
|
||||
REM Convert line endings and launch WSL with the bash script
|
||||
wsl dos2unix {bash_wsl_path} 2>nul || wsl sed -i 's/\r$//' {bash_wsl_path}
|
||||
wsl chmod +x {bash_wsl_path}
|
||||
wsl bash {bash_wsl_path}
|
||||
|
||||
REM Clean up temp file
|
||||
del "{bash_path}" 2>nul
|
||||
"""
|
||||
return script_content
|
||||
|
||||
def create_activity_server_script(self) -> str:
|
||||
"""Create a script for SSH connection to Activity Server"""
|
||||
import tempfile
|
||||
import os
|
||||
logger.info("Creating Activity Server connection script")
|
||||
|
||||
# Create a robust script with multiple approaches
|
||||
script_content = f"""@echo off
|
||||
cls
|
||||
echo ================================================================================
|
||||
echo CPM Activity Server Connection
|
||||
echo ================================================================================
|
||||
echo.
|
||||
echo Server: {self.server}
|
||||
echo Username: {self.username}
|
||||
echo Target Directory: /home/claude-dev/cpm-activity-server
|
||||
echo.
|
||||
|
||||
REM Check if PowerShell is available and use it for better plink integration
|
||||
where powershell >nul 2>&1
|
||||
if %ERRORLEVEL% EQU 0 (
|
||||
echo Using PowerShell for enhanced connection...
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -Command "& {{$plinkPath = Get-Command plink -ErrorAction SilentlyContinue; if ($plinkPath) {{ Write-Host 'Connecting via plink...' -ForegroundColor Green; Write-Host ''; $process = Start-Process -FilePath 'plink' -ArgumentList '-ssh', '-l', '{self.username}', '-pw', '{self.password}', '-t', '{self.server}', 'cd /home/claude-dev/cpm-activity-server && echo \"Successfully changed to Activity Server directory\" && echo && bash -l' -PassThru -NoNewWindow -Wait; if ($process.ExitCode -ne 0) {{ Write-Host ''; Write-Host 'Automatic directory change failed. Starting interactive session...' -ForegroundColor Yellow; Write-Host ''; Write-Host 'IMPORTANT: After login, run these commands:' -ForegroundColor Red; Write-Host ' cd /home/claude-dev/cpm-activity-server' -ForegroundColor White; Write-Host ' claude' -ForegroundColor White; Write-Host ''; & plink -ssh -l {self.username} -pw '{self.password}' -t {self.server} }} }} else {{ Write-Host 'Plink not found, trying standard SSH...' -ForegroundColor Yellow; Write-Host 'Password: {self.password}'; Write-Host 'After login, run: cd /home/claude-dev/cpm-activity-server && claude'; & ssh {self.username}@{self.server} }} }}"
|
||||
goto end
|
||||
)
|
||||
|
||||
REM First, try the plink approach with RemoteCommand
|
||||
where plink >nul 2>&1
|
||||
if %ERRORLEVEL% EQU 0 (
|
||||
echo Using PuTTY plink for connection...
|
||||
echo.
|
||||
|
||||
REM Try plink with direct command execution
|
||||
echo Attempting automatic directory change...
|
||||
plink -batch -ssh -l {self.username} -pw "{self.password}" {self.server} "cd /home/claude-dev/cpm-activity-server && exec bash -l"
|
||||
|
||||
REM If that didn't work, try interactive mode
|
||||
if %ERRORLEVEL% NEQ 0 (
|
||||
echo.
|
||||
echo Direct command execution failed. Starting interactive session...
|
||||
echo.
|
||||
echo ================================================================================
|
||||
echo IMPORTANT: After login, please run these commands:
|
||||
echo.
|
||||
echo cd /home/claude-dev/cpm-activity-server
|
||||
echo claude
|
||||
echo.
|
||||
echo ================================================================================
|
||||
echo.
|
||||
plink -ssh -l {self.username} -pw "{self.password}" -t {self.server}
|
||||
)
|
||||
goto end
|
||||
)
|
||||
|
||||
REM Check if we have WSL available
|
||||
where wsl >nul 2>&1
|
||||
if %ERRORLEVEL% EQU 0 (
|
||||
echo Using WSL for connection...
|
||||
echo.
|
||||
|
||||
REM Create inline bash command for WSL
|
||||
wsl bash -c "echo 'Connecting to Activity Server...' && if command -v sshpass >/dev/null 2>&1; then sshpass -p '{self.password}' ssh -o StrictHostKeyChecking=no -t {self.username}@{self.server} 'cd /home/claude-dev/cpm-activity-server && echo \"Successfully changed to Activity Server directory\" && echo && claude || bash -l'; else echo 'Manual password entry required.' && echo 'Password: {self.password}' && echo && echo 'After login, run: cd /home/claude-dev/cpm-activity-server && claude' && echo && ssh -o StrictHostKeyChecking=no {self.username}@{self.server}; fi"
|
||||
goto end
|
||||
)
|
||||
|
||||
REM Fallback to standard SSH
|
||||
echo No automated tools found. Using standard SSH...
|
||||
echo.
|
||||
echo ================================================================================
|
||||
echo Connection Instructions:
|
||||
echo.
|
||||
echo 1. Enter this password when prompted: {self.password}
|
||||
echo (You can right-click to paste in most terminals)
|
||||
echo.
|
||||
echo 2. After successful login, run these commands:
|
||||
echo cd /home/claude-dev/cpm-activity-server
|
||||
echo claude
|
||||
echo.
|
||||
echo ================================================================================
|
||||
echo.
|
||||
pause
|
||||
ssh {self.username}@{self.server}
|
||||
|
||||
:end
|
||||
echo.
|
||||
echo Connection closed.
|
||||
pause
|
||||
"""
|
||||
return script_content
|
||||
|
||||
# Test module
|
||||
if __name__ == "__main__":
|
||||
vps = VPSConnection()
|
||||
print("VPS Connection Module")
|
||||
print(f"Server: {vps.server}")
|
||||
print(f"Username: {vps.username}")
|
||||
print("\nSSH Script Preview:")
|
||||
print(vps.create_ssh_script()[:300] + "...")
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren