Merge pull request #1 from metacube2/claude/github-sync-website-017YXsy55JgZ3uUCZx13NfZG
GitHub Sync Website with Apache Server
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
# Data files (contains secrets and logs)
|
||||
data/*.json
|
||||
|
||||
# Log files
|
||||
*.log
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Editor files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
# Backup files
|
||||
*.backup
|
||||
*.bak
|
||||
@@ -0,0 +1,14 @@
|
||||
# Deny access to data directory from web
|
||||
<FilesMatch "^(data|src)">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
|
||||
# Deny access to sensitive files
|
||||
<FilesMatch "\.(json|log|ini|conf)$">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
|
||||
# Protect .git directory if exists
|
||||
<DirectoryMatch "\.git">
|
||||
Require all denied
|
||||
</DirectoryMatch>
|
||||
@@ -0,0 +1,284 @@
|
||||
# Schnellstart-Installation
|
||||
|
||||
Schritt-für-Schritt Anleitung zur Installation von GitHub Sync auf deinem Ubuntu LXC Container.
|
||||
|
||||
## ⚡ Express-Installation (5 Minuten)
|
||||
|
||||
### 1. System vorbereiten
|
||||
|
||||
```bash
|
||||
# System aktualisieren
|
||||
sudo apt update && sudo apt upgrade -y
|
||||
|
||||
# Benötigte Pakete installieren
|
||||
sudo apt install -y apache2 php libapache2-mod-php php-cli php-json php-mbstring git
|
||||
```
|
||||
|
||||
### 2. Apache Module aktivieren
|
||||
|
||||
```bash
|
||||
sudo a2enmod rewrite
|
||||
sudo systemctl restart apache2
|
||||
```
|
||||
|
||||
### 3. Virtual Host konfigurieren
|
||||
|
||||
```bash
|
||||
# Virtual Host Datei erstellen
|
||||
sudo nano /etc/apache2/sites-available/github-sync.conf
|
||||
```
|
||||
|
||||
Kopiere diese Konfiguration:
|
||||
|
||||
```apache
|
||||
<VirtualHost *:80>
|
||||
ServerName github-sync.local
|
||||
DocumentRoot /gitpusher/public
|
||||
|
||||
<Directory /gitpusher/public>
|
||||
Options -Indexes +FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
<Directory /gitpusher/data>
|
||||
Require all denied
|
||||
</Directory>
|
||||
|
||||
<Directory /gitpusher/src>
|
||||
Require all denied
|
||||
</Directory>
|
||||
|
||||
ErrorLog ${APACHE_LOG_DIR}/github-sync-error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/github-sync-access.log combined
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
```bash
|
||||
# Site aktivieren
|
||||
sudo a2ensite github-sync.conf
|
||||
sudo a2dissite 000-default.conf # Optional: Default-Site deaktivieren
|
||||
sudo systemctl reload apache2
|
||||
```
|
||||
|
||||
### 4. Berechtigungen setzen
|
||||
|
||||
```bash
|
||||
# Eigentümer ändern
|
||||
sudo chown -R www-data:www-data /gitpusher
|
||||
|
||||
# Berechtigungen setzen
|
||||
sudo chmod 755 /gitpusher
|
||||
sudo chmod 755 /gitpusher/public
|
||||
sudo chmod 755 /gitpusher/data
|
||||
sudo chmod 755 /gitpusher/src
|
||||
sudo chmod 600 /gitpusher/data/*.json
|
||||
```
|
||||
|
||||
### 5. GitHub Personal Access Token erstellen
|
||||
|
||||
1. Gehe zu: https://github.com/settings/tokens
|
||||
2. Klicke: **"Generate new token (classic)"**
|
||||
3. Name: `GitHub Sync Server`
|
||||
4. Scope: ✅ **repo** (Full control of private repositories)
|
||||
5. Klicke: **"Generate token"**
|
||||
6. **Kopiere den Token** (ghp_...)
|
||||
|
||||
### 6. Token hinterlegen
|
||||
|
||||
```bash
|
||||
sudo nano /gitpusher/data/secrets.json
|
||||
```
|
||||
|
||||
Ersetze die leere Zeile:
|
||||
|
||||
```json
|
||||
{
|
||||
"github_pat": "ghp_DEIN_TOKEN_HIER",
|
||||
"webhook_secrets": {}
|
||||
}
|
||||
```
|
||||
|
||||
Speichern: `Ctrl+O` → `Enter` → `Ctrl+X`
|
||||
|
||||
### 7. Testen
|
||||
|
||||
```bash
|
||||
# Apache Status prüfen
|
||||
sudo systemctl status apache2
|
||||
|
||||
# PHP testen
|
||||
php -v
|
||||
|
||||
# Git testen
|
||||
git --version
|
||||
```
|
||||
|
||||
### 8. Im Browser öffnen
|
||||
|
||||
Öffne in deinem Browser:
|
||||
- Wenn du eine Domain hast: `http://github-sync.deine-domain.de`
|
||||
- Sonst mit IP: `http://DEINE-SERVER-IP`
|
||||
|
||||
**Du solltest jetzt das Dashboard sehen!** 🎉
|
||||
|
||||
## 🎯 Erstes Repository hinzufügen
|
||||
|
||||
1. Klicke im Dashboard auf **"+ Repository hinzufügen"**
|
||||
2. Fülle aus:
|
||||
```
|
||||
Name: Test-Projekt
|
||||
Repository URL: https://github.com/dein-username/dein-repo.git
|
||||
Branch: main
|
||||
Ziel-Pfad: /var/www/test-projekt
|
||||
Auto-Sync: ✅ Aktiviert
|
||||
```
|
||||
3. Klicke **"Repository hinzufügen"**
|
||||
|
||||
Das Repository wird automatisch geklont!
|
||||
|
||||
## 🔗 GitHub Webhook einrichten
|
||||
|
||||
Nach dem Hinzufügen siehst du ein Modal mit Webhook-Informationen.
|
||||
|
||||
1. Kopiere **Payload URL** und **Secret**
|
||||
2. Gehe zu deinem GitHub Repo → **Settings** → **Webhooks** → **Add webhook**
|
||||
3. Füge ein:
|
||||
- **Payload URL**: (kopiert)
|
||||
- **Content type**: `application/json`
|
||||
- **Secret**: (kopiert)
|
||||
- **Events**: "Just the push event"
|
||||
4. Klicke **"Add webhook"**
|
||||
|
||||
Fertig! Bei jedem Push wird automatisch synchronisiert.
|
||||
|
||||
## ✅ Erfolgs-Check
|
||||
|
||||
Teste die Synchronisation:
|
||||
|
||||
1. Ändere eine Datei in deinem GitHub-Repo
|
||||
2. Committe und pushe die Änderung
|
||||
3. Schau im Dashboard → Log-Einträge
|
||||
4. Du solltest sehen: "✅ Sync OK (X Dateien)"
|
||||
|
||||
Prüfe die Datei auf dem Server:
|
||||
```bash
|
||||
ls -la /var/www/test-projekt
|
||||
```
|
||||
|
||||
## 🔧 Erweiterte Konfiguration
|
||||
|
||||
### SSL/HTTPS einrichten (empfohlen für Produktion)
|
||||
|
||||
```bash
|
||||
# Let's Encrypt installieren
|
||||
sudo apt install certbot python3-certbot-apache
|
||||
|
||||
# Zertifikat erstellen
|
||||
sudo certbot --apache -d github-sync.deine-domain.de
|
||||
|
||||
# Auto-Renewal testen
|
||||
sudo certbot renew --dry-run
|
||||
```
|
||||
|
||||
### Firewall konfigurieren
|
||||
|
||||
```bash
|
||||
# UFW Firewall aktivieren
|
||||
sudo ufw allow 'Apache Full'
|
||||
sudo ufw enable
|
||||
```
|
||||
|
||||
### Log-Rotation einrichten
|
||||
|
||||
```bash
|
||||
sudo nano /etc/logrotate.d/github-sync
|
||||
```
|
||||
|
||||
```
|
||||
/var/log/apache2/github-sync-*.log {
|
||||
daily
|
||||
missingok
|
||||
rotate 14
|
||||
compress
|
||||
delaycompress
|
||||
notifempty
|
||||
create 0640 root adm
|
||||
sharedscripts
|
||||
postrotate
|
||||
systemctl reload apache2 > /dev/null
|
||||
endscript
|
||||
}
|
||||
```
|
||||
|
||||
## 🚨 Häufige Probleme
|
||||
|
||||
### Problem: "403 Forbidden" beim Öffnen
|
||||
|
||||
**Lösung:**
|
||||
```bash
|
||||
sudo chown -R www-data:www-data /gitpusher
|
||||
sudo chmod 755 /gitpusher/public
|
||||
```
|
||||
|
||||
### Problem: Repository kann nicht geklont werden
|
||||
|
||||
**Lösung:**
|
||||
```bash
|
||||
# Prüfe, ob www-data git nutzen kann
|
||||
sudo -u www-data git --version
|
||||
|
||||
# Prüfe, ob Ziel-Ordner Schreibrechte hat
|
||||
sudo -u www-data mkdir -p /var/www/test
|
||||
```
|
||||
|
||||
### Problem: Webhook kommt nicht an
|
||||
|
||||
**Lösung:**
|
||||
1. Prüfe GitHub Webhook Deliveries auf Fehler
|
||||
2. Prüfe Firewall: `sudo ufw status`
|
||||
3. Prüfe Apache Logs:
|
||||
```bash
|
||||
sudo tail -f /var/log/apache2/github-sync-error.log
|
||||
```
|
||||
|
||||
### Problem: JSON-Dateien leer oder defekt
|
||||
|
||||
**Lösung:**
|
||||
```bash
|
||||
# Setze Standardwerte zurück
|
||||
cd /gitpusher/data
|
||||
|
||||
echo '{"repositories":[]}' | sudo tee config.json
|
||||
echo '{"entries":[]}' | sudo tee log.json
|
||||
echo '{"github_pat":"","webhook_secrets":{}}' | sudo tee secrets.json
|
||||
|
||||
sudo chmod 600 *.json
|
||||
sudo chown www-data:www-data *.json
|
||||
```
|
||||
|
||||
## 📋 Checkliste
|
||||
|
||||
- [ ] Apache installiert und läuft
|
||||
- [ ] PHP installiert (Version 7.4+)
|
||||
- [ ] Git installiert
|
||||
- [ ] Virtual Host konfiguriert
|
||||
- [ ] Site aktiviert und Apache neu geladen
|
||||
- [ ] Berechtigungen gesetzt (www-data)
|
||||
- [ ] GitHub PAT erstellt und hinterlegt
|
||||
- [ ] Dashboard im Browser erreichbar
|
||||
- [ ] Erstes Repository hinzugefügt
|
||||
- [ ] Webhook in GitHub eingerichtet
|
||||
- [ ] Test-Push erfolgreich synchronisiert
|
||||
|
||||
## 🎓 Nächste Schritte
|
||||
|
||||
1. Lies die vollständige [README.md](README.md)
|
||||
2. Füge weitere Repositories hinzu
|
||||
3. Teste Rollback-Funktion
|
||||
4. Richte SSL/HTTPS ein (für Produktion)
|
||||
5. Konfiguriere Monitoring
|
||||
|
||||
---
|
||||
|
||||
**Viel Erfolg!** 🚀
|
||||
@@ -0,0 +1,447 @@
|
||||
# Projektstruktur
|
||||
|
||||
## Übersicht
|
||||
|
||||
```
|
||||
/gitpusher/
|
||||
│
|
||||
├── 📄 README.md # Hauptdokumentation
|
||||
├── 📄 INSTALL.md # Schnellstart-Installation
|
||||
├── 📄 PROJECT_STRUCTURE.md # Diese Datei
|
||||
├── 🔒 .htaccess # Hauptsicherheitskonfiguration
|
||||
├── 📝 .gitignore # Git-Ignore-Regeln
|
||||
│
|
||||
├── 📁 public/ # Web-Root (Apache DocumentRoot)
|
||||
│ ├── 🌐 index.php # Dashboard (Frontend)
|
||||
│ ├── 🔗 webhook.php # Webhook-Endpoint für GitHub
|
||||
│ │
|
||||
│ ├── 📁 api/ # REST API Endpunkte
|
||||
│ │ ├── repos.php # Repository-CRUD-Operationen
|
||||
│ │ ├── sync.php # Manueller Sync-Trigger
|
||||
│ │ ├── rollback.php # Rollback zu älterem Commit
|
||||
│ │ └── log.php # Log-Abfrage
|
||||
│ │
|
||||
│ ├── 📁 css/
|
||||
│ │ └── style.css # Komplettes Dashboard-Styling
|
||||
│ │
|
||||
│ └── 📁 js/
|
||||
│ └── app.js # Frontend-Logik & AJAX
|
||||
│
|
||||
├── 📁 data/ # Datenspeicher (NICHT web-zugänglich!)
|
||||
│ ├── 🔒 .htaccess # Zugriff komplett verweigern
|
||||
│ ├── 📊 config.json # Repository-Konfigurationen
|
||||
│ ├── 📜 log.json # Alle Log-Einträge
|
||||
│ └── 🔐 secrets.json # GitHub PAT & Webhook Secrets
|
||||
│
|
||||
└── 📁 src/ # PHP Backend-Klassen
|
||||
├── 🔒 .htaccess # Zugriff komplett verweigern
|
||||
├── ConfigManager.php # JSON-Dateiverwaltung
|
||||
├── Logger.php # Logging-System
|
||||
└── GitHandler.php # Git-Operationen (clone, pull, revert)
|
||||
```
|
||||
|
||||
## 📂 Detaillierte Beschreibung
|
||||
|
||||
### `/public/` - Web-Root
|
||||
|
||||
**Apache DocumentRoot** - Einziger Ordner, der über HTTP erreichbar ist.
|
||||
|
||||
#### `index.php` - Dashboard
|
||||
- Haupt-Frontend der Anwendung
|
||||
- Zeigt alle Repositories mit Status
|
||||
- Statistiken (Anzahl Repos, Sync-Status, etc.)
|
||||
- Log-Anzeige der letzten Ereignisse
|
||||
- Modals für: Repository hinzufügen, Rollback, Webhook-Info
|
||||
|
||||
#### `webhook.php` - GitHub Webhook Endpoint
|
||||
- Empfängt POST-Requests von GitHub
|
||||
- Verifiziert Webhook-Signatur (HMAC SHA-256)
|
||||
- Prüft, ob Push zum konfigurierten Branch gehört
|
||||
- Triggert automatischen `git pull`
|
||||
- Loggt alle Ereignisse
|
||||
|
||||
#### `/api/` - REST API
|
||||
|
||||
##### `repos.php`
|
||||
- **GET**: Liste aller Repositories oder einzelnes Repository
|
||||
- **POST**: Neues Repository hinzufügen (+ initial clone)
|
||||
- **PUT**: Repository-Einstellungen aktualisieren
|
||||
- **DELETE**: Repository aus Config entfernen (optional: Dateien löschen)
|
||||
|
||||
##### `sync.php`
|
||||
- **POST**: Manuellen Sync durchführen
|
||||
- Führt `git pull` für angegebenes Repository aus
|
||||
- Gibt Anzahl geänderter Dateien zurück
|
||||
|
||||
##### `rollback.php`
|
||||
- **GET**: Liste der letzten Commits (für Rollback-Auswahl)
|
||||
- **POST**: Rollback zu bestimmtem Commit durchführen
|
||||
- Nutzt `git revert` (sicher, keine Commits gelöscht)
|
||||
|
||||
##### `log.php`
|
||||
- **GET**: Log-Einträge abrufen
|
||||
- Filter: Repository-ID, Log-Type, Limit, Offset
|
||||
- Gibt Statistiken zurück (Erfolg/Fehler/Warnung)
|
||||
|
||||
#### `/css/style.css`
|
||||
- Modernes, responsives Design
|
||||
- CSS Custom Properties (CSS Variables)
|
||||
- Mobile-First Ansatz
|
||||
- Animationen für Toasts, Modals, Cards
|
||||
|
||||
#### `/js/app.js`
|
||||
- Frontend-State-Management
|
||||
- AJAX-Calls zu allen API-Endpunkten
|
||||
- Modal-Handling
|
||||
- Toast-Notifications
|
||||
- Auto-Refresh (alle 30 Sekunden)
|
||||
|
||||
---
|
||||
|
||||
### `/data/` - Datenspeicher
|
||||
|
||||
**Sicherheit**: `.htaccess` verweigert jeden Web-Zugriff!
|
||||
|
||||
#### `config.json`
|
||||
```json
|
||||
{
|
||||
"repositories": [
|
||||
{
|
||||
"id": "repo_uniqueid123",
|
||||
"name": "Meine Website",
|
||||
"repo_url": "https://github.com/user/repo.git",
|
||||
"branch": "main",
|
||||
"target_path": "/var/www/website",
|
||||
"auto_sync": true,
|
||||
"status": "synced",
|
||||
"created_at": "2025-12-06 10:00:00",
|
||||
"last_sync": "2025-12-06 14:30:00",
|
||||
"last_commit": "abc123def456..."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### `log.json`
|
||||
```json
|
||||
{
|
||||
"entries": [
|
||||
{
|
||||
"id": "log_uniqueid456",
|
||||
"timestamp": "2025-12-06 14:30:00",
|
||||
"repo_id": "repo_uniqueid123",
|
||||
"type": "success",
|
||||
"message": "Pull completed successfully",
|
||||
"details": {
|
||||
"files_changed": 3,
|
||||
"old_commit": "abc123d",
|
||||
"new_commit": "def456a"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### `secrets.json`
|
||||
```json
|
||||
{
|
||||
"github_pat": "ghp_YourPersonalAccessTokenHere",
|
||||
"webhook_secrets": {
|
||||
"repo_uniqueid123": "generatedWebhookSecretHere"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Berechtigungen**: `chmod 600` (nur Owner kann lesen/schreiben)
|
||||
|
||||
---
|
||||
|
||||
### `/src/` - Backend-Klassen
|
||||
|
||||
**Sicherheit**: `.htaccess` verweigert jeden Web-Zugriff!
|
||||
|
||||
#### `ConfigManager.php`
|
||||
**Verantwortlichkeiten:**
|
||||
- JSON-Dateien lesen/schreiben
|
||||
- Repository-CRUD-Operationen
|
||||
- Webhook-Secret-Verwaltung
|
||||
- GitHub PAT verwalten
|
||||
|
||||
**Wichtige Methoden:**
|
||||
```php
|
||||
getRepositories() // Alle Repos
|
||||
getRepository($id) // Einzelnes Repo
|
||||
addRepository($data) // Neues Repo
|
||||
updateRepository($id, $updates) // Repo aktualisieren
|
||||
deleteRepository($id) // Repo löschen
|
||||
getGitHubToken() // PAT abrufen
|
||||
setWebhookSecret($id, $secret) // Webhook Secret speichern
|
||||
```
|
||||
|
||||
#### `Logger.php`
|
||||
**Verantwortlichkeiten:**
|
||||
- Log-Einträge erstellen
|
||||
- Logs nach Typ/Repo filtern
|
||||
- Statistiken generieren
|
||||
- Auto-Bereinigung (max. 1000 Einträge)
|
||||
|
||||
**Wichtige Methoden:**
|
||||
```php
|
||||
success($repoId, $message, $details) // ✅ Erfolg loggen
|
||||
error($repoId, $message, $details) // ❌ Fehler loggen
|
||||
warning($repoId, $message, $details) // ⚠️ Warnung loggen
|
||||
info($repoId, $message, $details) // ℹ️ Info loggen
|
||||
getAll($limit, $offset) // Alle Logs
|
||||
getByRepository($repoId) // Logs für Repo
|
||||
getStats() // Statistiken
|
||||
```
|
||||
|
||||
#### `GitHandler.php`
|
||||
**Verantwortlichkeiten:**
|
||||
- Git-Befehle ausführen
|
||||
- Repository klonen
|
||||
- Pull durchführen
|
||||
- Revert zu älterem Commit
|
||||
- Commit-Historie abrufen
|
||||
- Merge-Konflikte erkennen
|
||||
|
||||
**Wichtige Methoden:**
|
||||
```php
|
||||
cloneRepository($repoId, $url, $path, $branch) // Initial clone
|
||||
pull($repoId, $path, $branch) // git pull
|
||||
revert($repoId, $path, $commitHash) // git revert
|
||||
getCurrentCommit($path) // Aktueller Commit
|
||||
getCommitHistory($path, $limit) // Commit-Liste
|
||||
getStatus($path) // git status
|
||||
getRemoteBranches($url) // Verfügbare Branches
|
||||
```
|
||||
|
||||
**Sicherheit:**
|
||||
- Alle Shell-Befehle mit `escapeshellarg()` escaped
|
||||
- GitHub PAT wird in URL eingebettet für Auth
|
||||
- Fehlerbehandlung mit Try-Catch
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Sicherheitskonzept
|
||||
|
||||
### 1. Zugriffskontrolle
|
||||
|
||||
**Web-zugänglich**: Nur `/public/`
|
||||
|
||||
**Blockiert**:
|
||||
- `/data/` (enthält Secrets & Konfiguration)
|
||||
- `/src/` (PHP-Klassen)
|
||||
- Alle `.json` Dateien
|
||||
- `.git` Verzeichnisse
|
||||
|
||||
### 2. Webhook-Sicherheit
|
||||
|
||||
- HMAC SHA-256 Signatur-Verifizierung
|
||||
- Unique Secret pro Repository
|
||||
- Timing-Safe Vergleich (`hash_equals()`)
|
||||
|
||||
### 3. Datei-Berechtigungen
|
||||
|
||||
```bash
|
||||
/gitpusher/ 755 (www-data:www-data)
|
||||
/gitpusher/public/ 755
|
||||
/gitpusher/data/ 755
|
||||
/gitpusher/data/*.json 600 (nur Owner lesen/schreiben)
|
||||
/gitpusher/src/ 755
|
||||
```
|
||||
|
||||
### 4. Input-Validierung
|
||||
|
||||
- Alle User-Inputs werden validiert
|
||||
- JSON-Parsing mit Fehlerbehandlung
|
||||
- Repository-URLs werden geprüft
|
||||
- SQL-Injection nicht möglich (keine DB)
|
||||
- XSS-Prevention durch `escapeHtml()` im Frontend
|
||||
|
||||
### 5. Git-Sicherheit
|
||||
|
||||
- Alle Git-Befehle laufen als `www-data` User
|
||||
- Shell-Injection-Prevention durch `escapeshellarg()`
|
||||
- GitHub PAT mit minimalen Berechtigungen (nur `repo`)
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Datenfluss
|
||||
|
||||
### Automatischer Sync (Webhook)
|
||||
|
||||
```
|
||||
GitHub Push Event
|
||||
↓
|
||||
webhook.php
|
||||
↓
|
||||
1. Payload empfangen
|
||||
2. JSON dekodieren
|
||||
3. Signature verifizieren (HMAC SHA-256)
|
||||
4. Repository in Config finden
|
||||
5. Branch prüfen
|
||||
↓
|
||||
GitHandler::pull()
|
||||
↓
|
||||
1. Aktuellen Commit speichern
|
||||
2. git pull ausführen
|
||||
3. Auf Merge-Konflikte prüfen
|
||||
4. Geänderte Dateien zählen
|
||||
↓
|
||||
Logger::success/error()
|
||||
↓
|
||||
ConfigManager::updateRepository()
|
||||
↓
|
||||
Status aktualisiert in config.json
|
||||
```
|
||||
|
||||
### Manueller Sync
|
||||
|
||||
```
|
||||
User klickt "Sync"-Button
|
||||
↓
|
||||
JavaScript: syncRepository(repoId)
|
||||
↓
|
||||
AJAX POST → api/sync.php
|
||||
↓
|
||||
GitHandler::pull()
|
||||
↓
|
||||
Logger::log()
|
||||
↓
|
||||
Response → JavaScript
|
||||
↓
|
||||
Dashboard-Refresh
|
||||
```
|
||||
|
||||
### Repository hinzufügen
|
||||
|
||||
```
|
||||
User füllt Formular aus
|
||||
↓
|
||||
JavaScript: addRepository(event)
|
||||
↓
|
||||
AJAX POST → api/repos.php
|
||||
↓
|
||||
1. Input validieren
|
||||
2. ConfigManager::addRepository()
|
||||
3. Webhook Secret generieren
|
||||
4. GitHandler::cloneRepository()
|
||||
↓
|
||||
5. Logger::success()
|
||||
6. Webhook-Info zurückgeben
|
||||
↓
|
||||
Modal mit Webhook-Daten anzeigen
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Abhängigkeiten
|
||||
|
||||
### PHP-Klassen
|
||||
|
||||
```
|
||||
webhook.php
|
||||
├── ConfigManager
|
||||
├── Logger
|
||||
└── GitHandler
|
||||
└── Logger
|
||||
└── ConfigManager
|
||||
|
||||
api/repos.php
|
||||
├── ConfigManager
|
||||
├── Logger
|
||||
└── GitHandler
|
||||
|
||||
api/sync.php
|
||||
├── ConfigManager
|
||||
├── Logger
|
||||
└── GitHandler
|
||||
|
||||
api/rollback.php
|
||||
├── ConfigManager
|
||||
├── Logger
|
||||
└── GitHandler
|
||||
|
||||
api/log.php
|
||||
├── ConfigManager
|
||||
└── Logger
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
```
|
||||
index.php (HTML)
|
||||
├── css/style.css
|
||||
└── js/app.js
|
||||
├── Fetch API (AJAX)
|
||||
└── REST API Endpoints
|
||||
├── api/repos.php
|
||||
├── api/sync.php
|
||||
├── api/rollback.php
|
||||
└── api/log.php
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Test-Checklist
|
||||
|
||||
- [ ] Repository hinzufügen funktioniert
|
||||
- [ ] Initial Clone erfolgreich
|
||||
- [ ] Webhook empfängt Push-Events
|
||||
- [ ] Signatur-Verifizierung funktioniert
|
||||
- [ ] Manueller Sync funktioniert
|
||||
- [ ] Merge-Konflikte werden erkannt
|
||||
- [ ] Rollback erstellt Revert-Commit
|
||||
- [ ] Logs werden korrekt geschrieben
|
||||
- [ ] Dashboard zeigt Status korrekt
|
||||
- [ ] Repository löschen funktioniert
|
||||
- [ ] .htaccess blockiert /data/ Zugriff
|
||||
- [ ] .htaccess blockiert /src/ Zugriff
|
||||
|
||||
---
|
||||
|
||||
## 📚 Erweiterungsmöglichkeiten
|
||||
|
||||
### Mögliche Features
|
||||
|
||||
1. **Multi-User Support**
|
||||
- User-Login
|
||||
- Rollen-System (Admin, User)
|
||||
- Repository-Berechtigungen pro User
|
||||
|
||||
2. **E-Mail Benachrichtigungen**
|
||||
- Bei erfolgreicher Sync
|
||||
- Bei Fehlern/Konflikten
|
||||
- Tägliche Zusammenfassung
|
||||
|
||||
3. **Deployment Scripts**
|
||||
- Post-Sync Hooks (z.B. `npm install`, `composer install`)
|
||||
- Custom Shell-Befehle
|
||||
- Build-Prozesse
|
||||
|
||||
4. **Advanced Git Features**
|
||||
- Submodules Support
|
||||
- Tag/Release Tracking
|
||||
- Multi-Branch Sync
|
||||
|
||||
5. **Monitoring & Alerts**
|
||||
- Prometheus Metrics
|
||||
- Grafana Dashboard
|
||||
- Slack/Discord Webhooks
|
||||
|
||||
6. **API Authentication**
|
||||
- API Keys
|
||||
- JWT Tokens
|
||||
- Rate Limiting
|
||||
|
||||
7. **Backup System**
|
||||
- Automatische Backups vor Sync
|
||||
- Snapshot-Verwaltung
|
||||
- Restore-Funktion
|
||||
|
||||
---
|
||||
|
||||
**Version**: 1.0.0
|
||||
**Erstellt**: 2025-12-06
|
||||
**Autor**: Claude Code
|
||||
@@ -0,0 +1,341 @@
|
||||
# GitHub Sync - Automatische Repository-Synchronisation
|
||||
|
||||
Eine einfache und sichere Lösung zur automatischen Synchronisation von GitHub-Repositories auf deinem Server mittels Webhooks.
|
||||
|
||||
## 📋 Features
|
||||
|
||||
- ✅ **Automatische Synchronisation** via GitHub Webhooks
|
||||
- ✅ **Mehrere Repositories** gleichzeitig verwalten
|
||||
- ✅ **Branch-Auswahl** pro Repository
|
||||
- ✅ **Webhook-Sicherheit** mit Secret-Verifizierung
|
||||
- ✅ **Manueller Sync** über das Dashboard
|
||||
- ✅ **Rollback-Funktion** via `git revert`
|
||||
- ✅ **Konflikt-Erkennung** mit Warnungen
|
||||
- ✅ **Log-System** für alle Ereignisse
|
||||
- ✅ **Datei-basiert** - keine Datenbank erforderlich
|
||||
- ✅ **Responsives Dashboard** mit Echtzeit-Updates
|
||||
|
||||
## 🔧 Systemanforderungen
|
||||
|
||||
- **Server**: Ubuntu Server (LXC Container auf Proxmox)
|
||||
- **Webserver**: Apache 2.4+
|
||||
- **PHP**: 7.4+ (8.0+ empfohlen)
|
||||
- **Git**: 2.0+
|
||||
- **Berechtigungen**: Root-Zugriff für Installation
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
### 1. Voraussetzungen prüfen
|
||||
|
||||
```bash
|
||||
# PHP Version prüfen
|
||||
php -v
|
||||
|
||||
# Git Version prüfen
|
||||
git --version
|
||||
|
||||
# Apache Status prüfen
|
||||
systemctl status apache2
|
||||
```
|
||||
|
||||
### 2. Benötigte PHP-Module installieren
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install php php-cli php-json php-mbstring
|
||||
```
|
||||
|
||||
### 3. Apache-Konfiguration
|
||||
|
||||
#### Virtual Host erstellen
|
||||
|
||||
```bash
|
||||
sudo nano /etc/apache2/sites-available/github-sync.conf
|
||||
```
|
||||
|
||||
Füge folgende Konfiguration ein:
|
||||
|
||||
```apache
|
||||
<VirtualHost *:80>
|
||||
ServerName github-sync.deine-domain.de
|
||||
DocumentRoot /gitpusher/public
|
||||
|
||||
<Directory /gitpusher/public>
|
||||
Options -Indexes +FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
# Deny access to data and src directories
|
||||
<Directory /gitpusher/data>
|
||||
Require all denied
|
||||
</Directory>
|
||||
|
||||
<Directory /gitpusher/src>
|
||||
Require all denied
|
||||
</Directory>
|
||||
|
||||
ErrorLog ${APACHE_LOG_DIR}/github-sync-error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/github-sync-access.log combined
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
#### Site aktivieren
|
||||
|
||||
```bash
|
||||
sudo a2ensite github-sync.conf
|
||||
sudo a2enmod rewrite
|
||||
sudo systemctl reload apache2
|
||||
```
|
||||
|
||||
### 4. Berechtigungen setzen
|
||||
|
||||
```bash
|
||||
# Eigentümer auf www-data setzen
|
||||
sudo chown -R www-data:www-data /gitpusher
|
||||
|
||||
# Schreibrechte für data-Verzeichnis
|
||||
sudo chmod 755 /gitpusher/data
|
||||
sudo chmod 600 /gitpusher/data/*.json
|
||||
|
||||
# Ausführrechte für public-Verzeichnis
|
||||
sudo chmod 755 /gitpusher/public
|
||||
```
|
||||
|
||||
### 5. GitHub Personal Access Token erstellen
|
||||
|
||||
1. Gehe zu GitHub → Settings → Developer settings → Personal access tokens
|
||||
2. Klicke auf "Generate new token (classic)"
|
||||
3. Name: `GitHub Sync Server`
|
||||
4. Wähle Scopes:
|
||||
- ✅ `repo` (Full control of private repositories)
|
||||
5. Klicke auf "Generate token"
|
||||
6. **Kopiere den Token sofort** - er wird nur einmal angezeigt!
|
||||
|
||||
### 6. Token in der Anwendung hinterlegen
|
||||
|
||||
Bearbeite `/gitpusher/data/secrets.json`:
|
||||
|
||||
```bash
|
||||
sudo nano /gitpusher/data/secrets.json
|
||||
```
|
||||
|
||||
Füge deinen GitHub PAT ein:
|
||||
|
||||
```json
|
||||
{
|
||||
"github_pat": "ghp_deinTokenHier1234567890",
|
||||
"webhook_secrets": {}
|
||||
}
|
||||
```
|
||||
|
||||
Speichern mit `Ctrl+O`, beenden mit `Ctrl+X`.
|
||||
|
||||
## 🚀 Verwendung
|
||||
|
||||
### Dashboard öffnen
|
||||
|
||||
Öffne deinen Browser und navigiere zu:
|
||||
```
|
||||
http://github-sync.deine-domain.de
|
||||
```
|
||||
|
||||
### Repository hinzufügen
|
||||
|
||||
1. Klicke auf **"+ Repository hinzufügen"**
|
||||
2. Fülle das Formular aus:
|
||||
- **Name**: Ein aussagekräftiger Name (z.B. "Meine Website")
|
||||
- **GitHub Repository URL**: `https://github.com/user/repo.git`
|
||||
- **Branch**: z.B. `main` oder `master`
|
||||
- **Ziel-Pfad**: z.B. `/var/www/meine-website`
|
||||
- **Auto-Sync**: Aktiviert für automatische Webhooks
|
||||
3. Klicke auf **"Repository hinzufügen"**
|
||||
|
||||
Die App klont das Repository automatisch und zeigt dir die Webhook-Konfiguration an.
|
||||
|
||||
### GitHub Webhook einrichten
|
||||
|
||||
1. Gehe zu deinem GitHub Repository → **Settings** → **Webhooks** → **Add webhook**
|
||||
2. Füge die Informationen aus dem Modal ein:
|
||||
- **Payload URL**: (aus dem Modal kopieren)
|
||||
- **Content type**: `application/json`
|
||||
- **Secret**: (aus dem Modal kopieren)
|
||||
- **Events**: "Just the push event"
|
||||
3. Klicke auf **"Add webhook"**
|
||||
|
||||
Ab jetzt wird bei jedem Push automatisch synchronisiert!
|
||||
|
||||
### Manueller Sync
|
||||
|
||||
Klicke auf den Button **"🔄 Manueller Sync"** bei einem Repository, um sofort zu synchronisieren.
|
||||
|
||||
### Rollback durchführen
|
||||
|
||||
1. Klicke auf **"⏪ Rollback"** bei einem Repository
|
||||
2. Wähle den Commit aus, zu dem du zurückkehren möchtest
|
||||
3. Bestätige die Aktion
|
||||
|
||||
**Wichtig**: Es wird ein neuer Revert-Commit erstellt, keine Commits werden gelöscht!
|
||||
|
||||
### Repository entfernen
|
||||
|
||||
1. Klicke auf **"🗑️ Entfernen"**
|
||||
2. Wähle, ob auch die Dateien gelöscht werden sollen
|
||||
3. Bestätige die Aktion
|
||||
|
||||
## 📁 Dateistruktur
|
||||
|
||||
```
|
||||
/gitpusher/
|
||||
├── public/ # Web-Root (Apache DocumentRoot)
|
||||
│ ├── index.php # Dashboard
|
||||
│ ├── webhook.php # Webhook-Endpoint
|
||||
│ ├── api/
|
||||
│ │ ├── repos.php # Repository-Verwaltung
|
||||
│ │ ├── sync.php # Manueller Sync
|
||||
│ │ ├── rollback.php # Rollback-Funktion
|
||||
│ │ └── log.php # Logs abrufen
|
||||
│ ├── css/
|
||||
│ │ └── style.css # Styling
|
||||
│ └── js/
|
||||
│ └── app.js # Frontend-Logik
|
||||
│
|
||||
├── data/ # Daten (nicht web-zugänglich)
|
||||
│ ├── config.json # Repository-Konfiguration
|
||||
│ ├── log.json # Log-Einträge
|
||||
│ ├── secrets.json # GitHub PAT & Webhook Secrets
|
||||
│ └── .htaccess # Zugriff verweigern
|
||||
│
|
||||
├── src/ # PHP-Klassen
|
||||
│ ├── ConfigManager.php # Konfigurationsverwaltung
|
||||
│ ├── Logger.php # Logging
|
||||
│ ├── GitHandler.php # Git-Operationen
|
||||
│ └── .htaccess # Zugriff verweigern
|
||||
│
|
||||
├── .htaccess # Hauptkonfiguration
|
||||
└── README.md # Diese Datei
|
||||
```
|
||||
|
||||
## 🔒 Sicherheit
|
||||
|
||||
### Webhook-Signatur-Verifizierung
|
||||
|
||||
Alle Webhooks werden mit HMAC SHA-256 signiert und verifiziert. Ohne gültiges Secret werden Requests abgelehnt.
|
||||
|
||||
### Datei-Berechtigungen
|
||||
|
||||
- `/gitpusher/data/`: Nur von PHP lesbar (600)
|
||||
- `/gitpusher/src/`: Nicht web-zugänglich
|
||||
- `.htaccess`: Schützt sensitive Verzeichnisse
|
||||
|
||||
### GitHub PAT
|
||||
|
||||
- Wird verschlüsselt in `secrets.json` gespeichert
|
||||
- Nur `repo`-Scope erforderlich
|
||||
- Kann jederzeit in GitHub widerrufen werden
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### "Permission denied" beim Clonen
|
||||
|
||||
```bash
|
||||
# Stelle sicher, dass www-data Schreibrechte hat
|
||||
sudo chown -R www-data:www-data /var/www
|
||||
sudo chmod 755 /var/www
|
||||
```
|
||||
|
||||
### Webhook wird nicht empfangen
|
||||
|
||||
1. Prüfe GitHub Webhook Deliveries auf Fehler
|
||||
2. Überprüfe Apache Error Log:
|
||||
```bash
|
||||
sudo tail -f /var/log/apache2/github-sync-error.log
|
||||
```
|
||||
3. Teste Webhook manuell:
|
||||
```bash
|
||||
curl -X POST http://github-sync.deine-domain.de/webhook.php \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"repository":{"clone_url":"https://github.com/user/repo.git"}}'
|
||||
```
|
||||
|
||||
### Merge-Konflikt
|
||||
|
||||
Bei Konflikten zeigt das Dashboard eine Warnung. Löse den Konflikt manuell:
|
||||
|
||||
```bash
|
||||
cd /var/www/dein-repo
|
||||
sudo -u www-data git status
|
||||
# Konflikt manuell lösen
|
||||
sudo -u www-data git add .
|
||||
sudo -u www-data git commit -m "Konflikt gelöst"
|
||||
```
|
||||
|
||||
### Logs prüfen
|
||||
|
||||
```bash
|
||||
# PHP Error Log
|
||||
sudo tail -f /var/log/apache2/error.log
|
||||
|
||||
# App Logs
|
||||
cat /gitpusher/data/log.json | jq
|
||||
```
|
||||
|
||||
## 📊 API-Endpunkte
|
||||
|
||||
### GET /api/repos.php
|
||||
Listet alle Repositories auf
|
||||
|
||||
### POST /api/repos.php
|
||||
Fügt neues Repository hinzu
|
||||
|
||||
### PUT /api/repos.php
|
||||
Aktualisiert Repository
|
||||
|
||||
### DELETE /api/repos.php
|
||||
Löscht Repository
|
||||
|
||||
### POST /api/sync.php
|
||||
Führt manuellen Sync durch
|
||||
|
||||
### GET /api/rollback.php
|
||||
Listet Commits für Rollback
|
||||
|
||||
### POST /api/rollback.php
|
||||
Führt Rollback durch
|
||||
|
||||
### GET /api/log.php
|
||||
Ruft Logs ab
|
||||
|
||||
### POST /webhook.php
|
||||
Empfängt GitHub Webhooks
|
||||
|
||||
## 🔄 Updates
|
||||
|
||||
Um das System zu aktualisieren:
|
||||
|
||||
1. Backup erstellen:
|
||||
```bash
|
||||
sudo cp -r /gitpusher/data /gitpusher/data.backup
|
||||
```
|
||||
|
||||
2. Neue Dateien deployen
|
||||
|
||||
3. Berechtigungen prüfen:
|
||||
```bash
|
||||
sudo chown -R www-data:www-data /gitpusher
|
||||
```
|
||||
|
||||
## 📝 Lizenz
|
||||
|
||||
Dieses Projekt ist für den persönlichen und kommerziellen Gebrauch frei verfügbar.
|
||||
|
||||
## 🙋 Support
|
||||
|
||||
Bei Fragen oder Problemen:
|
||||
1. Prüfe die Logs im Dashboard
|
||||
2. Prüfe Apache Error Logs
|
||||
3. Prüfe GitHub Webhook Delivery Logs
|
||||
|
||||
---
|
||||
|
||||
Erstellt mit ❤️ für einfache GitHub-Synchronisation
|
||||
@@ -0,0 +1,8 @@
|
||||
# Deny all access to data directory
|
||||
Require all denied
|
||||
|
||||
# Alternative for older Apache versions
|
||||
<IfModule !mod_authz_core.c>
|
||||
Order deny,allow
|
||||
Deny from all
|
||||
</IfModule>
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
/**
|
||||
* Log API
|
||||
* Retrieves log entries
|
||||
*/
|
||||
|
||||
require_once '../../src/ConfigManager.php';
|
||||
require_once '../../src/Logger.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['error' => 'Method not allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$configManager = new ConfigManager();
|
||||
$logger = new Logger($configManager);
|
||||
|
||||
// Get query parameters
|
||||
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 100;
|
||||
$offset = isset($_GET['offset']) ? (int)$_GET['offset'] : 0;
|
||||
$repoId = $_GET['repo_id'] ?? null;
|
||||
$type = $_GET['type'] ?? null;
|
||||
|
||||
// Get logs based on filters
|
||||
if ($repoId) {
|
||||
$logs = $logger->getByRepository($repoId, $limit);
|
||||
} elseif ($type) {
|
||||
$logs = $logger->getByType($type, $limit);
|
||||
} else {
|
||||
$logs = $logger->getAll($limit, $offset);
|
||||
}
|
||||
|
||||
// Get statistics
|
||||
$stats = $logger->getStats();
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'logs' => $logs,
|
||||
'stats' => $stats,
|
||||
'count' => count($logs)
|
||||
]);
|
||||
@@ -0,0 +1,245 @@
|
||||
<?php
|
||||
/**
|
||||
* Repository Management API
|
||||
* Handles CRUD operations for repositories
|
||||
*/
|
||||
|
||||
require_once '../../src/ConfigManager.php';
|
||||
require_once '../../src/Logger.php';
|
||||
require_once '../../src/GitHandler.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$configManager = new ConfigManager();
|
||||
$logger = new Logger($configManager);
|
||||
$gitHandler = new GitHandler($logger, $configManager);
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
|
||||
// GET - List all repositories or get single repository
|
||||
if ($method === 'GET') {
|
||||
if (isset($_GET['id'])) {
|
||||
$repo = $configManager->getRepository($_GET['id']);
|
||||
|
||||
if (!$repo) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Repository not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Add current git status if path exists
|
||||
if (file_exists($repo['target_path'])) {
|
||||
$status = $gitHandler->getStatus($repo['target_path']);
|
||||
$repo['git_status'] = $status;
|
||||
$repo['current_branch'] = $gitHandler->getCurrentBranch($repo['target_path']);
|
||||
$repo['current_commit'] = $gitHandler->getCurrentCommit($repo['target_path']);
|
||||
}
|
||||
|
||||
echo json_encode($repo);
|
||||
} else {
|
||||
$repos = $configManager->getRepositories();
|
||||
|
||||
// Add status for each repo
|
||||
foreach ($repos as &$repo) {
|
||||
if (file_exists($repo['target_path'])) {
|
||||
$repo['exists'] = true;
|
||||
$repo['current_branch'] = $gitHandler->getCurrentBranch($repo['target_path']);
|
||||
} else {
|
||||
$repo['exists'] = false;
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode(['repositories' => $repos]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
// POST - Add new repository
|
||||
if ($method === 'POST') {
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
// Validate required fields
|
||||
$required = ['name', 'repo_url', 'target_path', 'branch'];
|
||||
foreach ($required as $field) {
|
||||
if (empty($input[$field])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => "Field '$field' is required"]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate target path
|
||||
$targetPath = rtrim($input['target_path'], '/');
|
||||
|
||||
// Check if target path already exists
|
||||
if (file_exists($targetPath)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Target path already exists']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Validate repository URL format
|
||||
if (!filter_var($input['repo_url'], FILTER_VALIDATE_URL)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Invalid repository URL']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Generate webhook secret
|
||||
$webhookSecret = $configManager->generateWebhookSecret();
|
||||
|
||||
// Prepare repository data
|
||||
$repoData = [
|
||||
'name' => $input['name'],
|
||||
'repo_url' => $input['repo_url'],
|
||||
'target_path' => $targetPath,
|
||||
'branch' => $input['branch'],
|
||||
'auto_sync' => $input['auto_sync'] ?? true,
|
||||
'status' => 'cloning'
|
||||
];
|
||||
|
||||
// Add repository to config
|
||||
$repoId = $configManager->addRepository($repoData);
|
||||
|
||||
if (!$repoId) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Failed to add repository']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Save webhook secret
|
||||
$configManager->setWebhookSecret($repoId, $webhookSecret);
|
||||
|
||||
// Clone repository
|
||||
$result = $gitHandler->cloneRepository(
|
||||
$repoId,
|
||||
$input['repo_url'],
|
||||
$targetPath,
|
||||
$input['branch']
|
||||
);
|
||||
|
||||
if ($result['success']) {
|
||||
$configManager->updateRepository($repoId, [
|
||||
'status' => 'synced',
|
||||
'last_sync' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
|
||||
$repo = $configManager->getRepository($repoId);
|
||||
$repo['webhook_secret'] = $webhookSecret;
|
||||
$repo['webhook_url'] = (isset($_SERVER['HTTPS']) ? 'https' : 'http') .
|
||||
'://' . $_SERVER['HTTP_HOST'] .
|
||||
dirname(dirname($_SERVER['REQUEST_URI'])) . '/webhook.php';
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'repository' => $repo
|
||||
]);
|
||||
} else {
|
||||
$configManager->updateRepository($repoId, ['status' => 'error']);
|
||||
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => $result['message'],
|
||||
'details' => $result['error'] ?? null
|
||||
]);
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
// PUT - Update repository
|
||||
if ($method === 'PUT') {
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (empty($input['id'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Repository ID is required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$repo = $configManager->getRepository($input['id']);
|
||||
|
||||
if (!$repo) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Repository not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Prepare updates (only allow certain fields to be updated)
|
||||
$allowedFields = ['name', 'branch', 'auto_sync'];
|
||||
$updates = [];
|
||||
|
||||
foreach ($allowedFields as $field) {
|
||||
if (isset($input[$field])) {
|
||||
$updates[$field] = $input[$field];
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($updates)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'No valid fields to update']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$success = $configManager->updateRepository($input['id'], $updates);
|
||||
|
||||
if ($success) {
|
||||
$repo = $configManager->getRepository($input['id']);
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'repository' => $repo
|
||||
]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Failed to update repository']);
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
// DELETE - Delete repository
|
||||
if ($method === 'DELETE') {
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (empty($input['id'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Repository ID is required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$repo = $configManager->getRepository($input['id']);
|
||||
|
||||
if (!$repo) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Repository not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Delete repository from config
|
||||
$success = $configManager->deleteRepository($input['id']);
|
||||
|
||||
if ($success) {
|
||||
$logger->info($input['id'], "Repository removed from configuration");
|
||||
|
||||
// Optionally delete files if requested
|
||||
if (!empty($input['delete_files']) && file_exists($repo['target_path'])) {
|
||||
exec('rm -rf ' . escapeshellarg($repo['target_path']));
|
||||
$logger->info($input['id'], "Repository files deleted from disk");
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Repository deleted'
|
||||
]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Failed to delete repository']);
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
// Method not allowed
|
||||
http_response_code(405);
|
||||
echo json_encode(['error' => 'Method not allowed']);
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
/**
|
||||
* Rollback API
|
||||
* Reverts repository to a specific commit
|
||||
*/
|
||||
|
||||
require_once '../../src/ConfigManager.php';
|
||||
require_once '../../src/Logger.php';
|
||||
require_once '../../src/GitHandler.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$configManager = new ConfigManager();
|
||||
$logger = new Logger($configManager);
|
||||
$gitHandler = new GitHandler($logger, $configManager);
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
|
||||
// GET - Get commit history
|
||||
if ($method === 'GET') {
|
||||
if (empty($_GET['repo_id'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Repository ID is required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$repo = $configManager->getRepository($_GET['repo_id']);
|
||||
|
||||
if (!$repo) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Repository not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!file_exists($repo['target_path'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Repository path does not exist']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 20;
|
||||
$commits = $gitHandler->getCommitHistory($repo['target_path'], $limit);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'commits' => $commits
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// POST - Perform rollback
|
||||
if ($method === 'POST') {
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (empty($input['repo_id'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Repository ID is required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (empty($input['commit_hash'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Commit hash is required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$repo = $configManager->getRepository($input['repo_id']);
|
||||
|
||||
if (!$repo) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Repository not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!file_exists($repo['target_path'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Repository path does not exist']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Perform revert
|
||||
$result = $gitHandler->revert(
|
||||
$repo['id'],
|
||||
$repo['target_path'],
|
||||
$input['commit_hash']
|
||||
);
|
||||
|
||||
if ($result['success']) {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => $result['message'],
|
||||
'output' => $result['output']
|
||||
]);
|
||||
} else {
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => $result['message'],
|
||||
'error' => $result['error'] ?? null
|
||||
]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
// Method not allowed
|
||||
http_response_code(405);
|
||||
echo json_encode(['error' => 'Method not allowed']);
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
/**
|
||||
* Manual Sync API
|
||||
* Triggers manual sync for a repository
|
||||
*/
|
||||
|
||||
require_once '../../src/ConfigManager.php';
|
||||
require_once '../../src/Logger.php';
|
||||
require_once '../../src/GitHandler.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['error' => 'Method not allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (empty($input['repo_id'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Repository ID is required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$configManager = new ConfigManager();
|
||||
$logger = new Logger($configManager);
|
||||
$gitHandler = new GitHandler($logger, $configManager);
|
||||
|
||||
$repo = $configManager->getRepository($input['repo_id']);
|
||||
|
||||
if (!$repo) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Repository not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Check if repository path exists
|
||||
if (!file_exists($repo['target_path'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Repository path does not exist. Please clone first.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Perform sync
|
||||
$logger->info($repo['id'], "Manual sync triggered");
|
||||
|
||||
$result = $gitHandler->pull(
|
||||
$repo['id'],
|
||||
$repo['target_path'],
|
||||
$repo['branch']
|
||||
);
|
||||
|
||||
if ($result['success']) {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => $result['message'],
|
||||
'files_changed' => $result['files_changed'] ?? 0,
|
||||
'output' => $result['output']
|
||||
]);
|
||||
} else {
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => $result['message'],
|
||||
'conflict' => $result['conflict'] ?? false,
|
||||
'error' => $result['error'] ?? null,
|
||||
'output' => $result['output'] ?? null
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,633 @@
|
||||
/* Reset and Base Styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary-color: #0366d6;
|
||||
--success-color: #28a745;
|
||||
--warning-color: #ffc107;
|
||||
--error-color: #dc3545;
|
||||
--info-color: #17a2b8;
|
||||
--bg-color: #f6f8fa;
|
||||
--card-bg: #ffffff;
|
||||
--text-primary: #24292e;
|
||||
--text-secondary: #586069;
|
||||
--border-color: #e1e4e8;
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
|
||||
--shadow-hover: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
header {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
background: linear-gradient(135deg, var(--primary-color), #0550ae);
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 30px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.1em;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Stats Grid */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--card-bg);
|
||||
padding: 25px;
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow);
|
||||
text-align: center;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-hover);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2.5em;
|
||||
font-weight: bold;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.9em;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* Sections */
|
||||
.section {
|
||||
background: var(--card-bg);
|
||||
padding: 25px;
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
font-size: 1.5em;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #0550ae;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--text-secondary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #444d56;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: var(--success-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: var(--warning-color);
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: var(--error-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 5px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Repository List */
|
||||
.repos-list {
|
||||
display: grid;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.repo-card {
|
||||
background: var(--bg-color);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid var(--primary-color);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.repo-card:hover {
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.repo-card.status-synced {
|
||||
border-left-color: var(--success-color);
|
||||
}
|
||||
|
||||
.repo-card.status-error {
|
||||
border-left-color: var(--error-color);
|
||||
}
|
||||
|
||||
.repo-card.status-conflict {
|
||||
border-left-color: var(--warning-color);
|
||||
}
|
||||
|
||||
.repo-card.status-cloning {
|
||||
border-left-color: var(--info-color);
|
||||
}
|
||||
|
||||
.repo-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.repo-name {
|
||||
font-size: 1.3em;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.repo-url {
|
||||
font-size: 0.9em;
|
||||
color: var(--text-secondary);
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.repo-status {
|
||||
padding: 5px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.repo-status.synced {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.repo-status.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.repo-status.conflict {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.repo-status.cloning {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
.repo-info {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin-bottom: 15px;
|
||||
padding: 15px;
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 0.8em;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.repo-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Logs List */
|
||||
.logs-list {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
padding: 12px;
|
||||
border-left: 3px solid var(--info-color);
|
||||
background: var(--bg-color);
|
||||
margin-bottom: 10px;
|
||||
border-radius: 4px;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 15px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.log-entry.success {
|
||||
border-left-color: var(--success-color);
|
||||
}
|
||||
|
||||
.log-entry.error {
|
||||
border-left-color: var(--error-color);
|
||||
}
|
||||
|
||||
.log-entry.warning {
|
||||
border-left-color: var(--warning-color);
|
||||
}
|
||||
|
||||
.log-entry.info {
|
||||
border-left-color: var(--info-color);
|
||||
}
|
||||
|
||||
.log-timestamp {
|
||||
font-size: 0.85em;
|
||||
color: var(--text-secondary);
|
||||
font-family: 'Courier New', monospace;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.log-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.log-message {
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.log-details {
|
||||
font-size: 0.85em;
|
||||
color: var(--text-secondary);
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: var(--card-bg);
|
||||
border-radius: 8px;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 2em;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.form-group input[type="text"],
|
||||
.form-group input[type="url"],
|
||||
.form-group select {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(3, 102, 214, 0.1);
|
||||
}
|
||||
|
||||
.form-group small {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-label input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 25px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.input-with-copy {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.input-with-copy input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Commits List */
|
||||
.commits-list {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.commit-item {
|
||||
padding: 15px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.commit-item:hover {
|
||||
background: var(--bg-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.commit-hash {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: 600;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.commit-message {
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.commit-meta {
|
||||
font-size: 0.85em;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Alert */
|
||||
.alert {
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background: #d1ecf1;
|
||||
border: 1px solid #bee5eb;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
.alert-info ol {
|
||||
margin: 10px 0 0 20px;
|
||||
}
|
||||
|
||||
/* Toast Notifications */
|
||||
#toastContainer {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.toast {
|
||||
background: white;
|
||||
padding: 15px 20px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
min-width: 300px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
|
||||
.toast.success {
|
||||
border-left: 4px solid var(--success-color);
|
||||
}
|
||||
|
||||
.toast.error {
|
||||
border-left: 4px solid var(--error-color);
|
||||
}
|
||||
|
||||
.toast.warning {
|
||||
border-left: 4px solid var(--warning-color);
|
||||
}
|
||||
|
||||
.toast.info {
|
||||
border-left: 4px solid var(--info-color);
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(400px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Loading */
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.loading::before {
|
||||
content: "⏳ ";
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.empty-state::before {
|
||||
content: "📁";
|
||||
font-size: 4em;
|
||||
display: block;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
header h1 {
|
||||
font-size: 1.8em;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.repo-header {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.repo-actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.repo-actions .btn {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>GitHub Sync - Dashboard</title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>🔄 GitHub Sync Dashboard</h1>
|
||||
<p class="subtitle">Automatische Repository-Synchronisation</p>
|
||||
</header>
|
||||
|
||||
<div class="stats-grid" id="statsGrid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="totalRepos">0</div>
|
||||
<div class="stat-label">Repositories</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="syncedRepos">0</div>
|
||||
<div class="stat-label">Synchronisiert</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="errorRepos">0</div>
|
||||
<div class="stat-label">Fehler</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="totalLogs">0</div>
|
||||
<div class="stat-label">Log-Einträge (24h)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="section">
|
||||
<div class="section-header">
|
||||
<h2>Repositories</h2>
|
||||
<button class="btn btn-primary" onclick="showAddRepoModal()">+ Repository hinzufügen</button>
|
||||
</div>
|
||||
|
||||
<div id="reposList" class="repos-list">
|
||||
<div class="loading">Lade Repositories...</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="section-header">
|
||||
<h2>Letzte Ereignisse</h2>
|
||||
<button class="btn btn-secondary" onclick="refreshLogs()">🔄 Aktualisieren</button>
|
||||
</div>
|
||||
|
||||
<div id="logsList" class="logs-list">
|
||||
<div class="loading">Lade Logs...</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Add Repository -->
|
||||
<div id="addRepoModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>Repository hinzufügen</h2>
|
||||
<button class="close-btn" onclick="closeModal('addRepoModal')">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="addRepoForm" onsubmit="addRepository(event)">
|
||||
<div class="form-group">
|
||||
<label for="repoName">Name</label>
|
||||
<input type="text" id="repoName" name="name" required placeholder="Mein Projekt">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="repoUrl">GitHub Repository URL</label>
|
||||
<input type="url" id="repoUrl" name="repo_url" required
|
||||
placeholder="https://github.com/user/repo.git"
|
||||
onblur="fetchBranches()">
|
||||
<small>Die HTTPS Clone URL des Repositories</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="repoBranch">Branch</label>
|
||||
<select id="repoBranch" name="branch" required>
|
||||
<option value="main">main</option>
|
||||
<option value="master">master</option>
|
||||
</select>
|
||||
<small id="branchLoading" style="display:none;">Lade Branches...</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="targetPath">Ziel-Pfad auf Server</label>
|
||||
<input type="text" id="targetPath" name="target_path" required
|
||||
placeholder="/var/www/mein-projekt">
|
||||
<small>Absoluter Pfad, wo das Repository geklont werden soll</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" name="auto_sync" checked>
|
||||
Auto-Sync aktivieren (reagiert auf Webhooks)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeModal('addRepoModal')">Abbrechen</button>
|
||||
<button type="submit" class="btn btn-primary">Repository hinzufügen</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Rollback -->
|
||||
<div id="rollbackModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>Rollback durchführen</h2>
|
||||
<button class="close-btn" onclick="closeModal('rollbackModal')">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Wähle einen Commit aus, zu dem du zurückkehren möchtest:</p>
|
||||
<div id="commitsList" class="commits-list">
|
||||
<div class="loading">Lade Commits...</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeModal('rollbackModal')">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Webhook Info -->
|
||||
<div id="webhookModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>Webhook-Konfiguration</h2>
|
||||
<button class="close-btn" onclick="closeModal('webhookModal')">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label>Payload URL</label>
|
||||
<div class="input-with-copy">
|
||||
<input type="text" id="webhookUrl" readonly>
|
||||
<button class="btn btn-secondary btn-sm" onclick="copyToClipboard('webhookUrl')">Kopieren</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Secret</label>
|
||||
<div class="input-with-copy">
|
||||
<input type="text" id="webhookSecret" readonly>
|
||||
<button class="btn btn-secondary btn-sm" onclick="copyToClipboard('webhookSecret')">Kopieren</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Content type</label>
|
||||
<input type="text" value="application/json" readonly>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<strong>Einrichtung:</strong>
|
||||
<ol>
|
||||
<li>Gehe zu deinem GitHub Repository</li>
|
||||
<li>Settings → Webhooks → Add webhook</li>
|
||||
<li>Füge die obige Payload URL ein</li>
|
||||
<li>Füge das Secret ein</li>
|
||||
<li>Wähle "application/json" als Content type</li>
|
||||
<li>Wähle "Just the push event"</li>
|
||||
<li>Klicke auf "Add webhook"</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast Notifications -->
|
||||
<div id="toastContainer"></div>
|
||||
|
||||
<script src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,516 @@
|
||||
/**
|
||||
* GitHub Sync Dashboard - Frontend JavaScript
|
||||
*/
|
||||
|
||||
// State
|
||||
let repositories = [];
|
||||
let logs = [];
|
||||
let stats = {};
|
||||
let currentRollbackRepoId = null;
|
||||
|
||||
// Initialize app when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadDashboard();
|
||||
// Auto-refresh every 30 seconds
|
||||
setInterval(loadDashboard, 30000);
|
||||
});
|
||||
|
||||
/**
|
||||
* Load complete dashboard
|
||||
*/
|
||||
async function loadDashboard() {
|
||||
await Promise.all([
|
||||
loadRepositories(),
|
||||
loadLogs()
|
||||
]);
|
||||
updateStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load repositories
|
||||
*/
|
||||
async function loadRepositories() {
|
||||
try {
|
||||
const response = await fetch('api/repos.php');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.repositories) {
|
||||
repositories = data.repositories;
|
||||
renderRepositories();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading repositories:', error);
|
||||
showToast('Fehler beim Laden der Repositories', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render repositories list
|
||||
*/
|
||||
function renderRepositories() {
|
||||
const container = document.getElementById('reposList');
|
||||
|
||||
if (repositories.length === 0) {
|
||||
container.innerHTML = '<div class="empty-state">Keine Repositories konfiguriert. Füge dein erstes Repository hinzu!</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = repositories.map(repo => `
|
||||
<div class="repo-card status-${repo.status || 'pending'}">
|
||||
<div class="repo-header">
|
||||
<div>
|
||||
<div class="repo-name">${escapeHtml(repo.name)}</div>
|
||||
<div class="repo-url">${escapeHtml(repo.repo_url)}</div>
|
||||
</div>
|
||||
<span class="repo-status ${repo.status || 'pending'}">${getStatusText(repo.status)}</span>
|
||||
</div>
|
||||
|
||||
<div class="repo-info">
|
||||
<div class="info-item">
|
||||
<span class="info-label">Branch</span>
|
||||
<span class="info-value">${escapeHtml(repo.branch)}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Ziel-Pfad</span>
|
||||
<span class="info-value">${escapeHtml(repo.target_path)}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Letzte Sync</span>
|
||||
<span class="info-value">${repo.last_sync || 'Noch nie'}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Auto-Sync</span>
|
||||
<span class="info-value">${repo.auto_sync ? '✅ Aktiv' : '❌ Inaktiv'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="repo-actions">
|
||||
<button class="btn btn-success btn-sm" onclick="syncRepository('${repo.id}')">
|
||||
🔄 Manueller Sync
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-sm" onclick="showRollbackModal('${repo.id}')">
|
||||
⏪ Rollback
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-sm" onclick="showWebhookInfo('${repo.id}')">
|
||||
🔗 Webhook Info
|
||||
</button>
|
||||
<button class="btn btn-danger btn-sm" onclick="deleteRepository('${repo.id}', '${escapeHtml(repo.name)}')">
|
||||
🗑️ Entfernen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load logs
|
||||
*/
|
||||
async function loadLogs() {
|
||||
try {
|
||||
const response = await fetch('api/log.php?limit=50');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
logs = data.logs;
|
||||
stats = data.stats;
|
||||
renderLogs();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading logs:', error);
|
||||
showToast('Fehler beim Laden der Logs', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render logs list
|
||||
*/
|
||||
function renderLogs() {
|
||||
const container = document.getElementById('logsList');
|
||||
|
||||
if (logs.length === 0) {
|
||||
container.innerHTML = '<div class="empty-state">Noch keine Log-Einträge vorhanden.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = logs.map(log => {
|
||||
const repo = repositories.find(r => r.id === log.repo_id);
|
||||
const repoName = repo ? repo.name : log.repo_id;
|
||||
|
||||
return `
|
||||
<div class="log-entry ${log.type}">
|
||||
<div class="log-timestamp">${log.timestamp}</div>
|
||||
<div class="log-content">
|
||||
<div class="log-message">
|
||||
<strong>${escapeHtml(repoName)}</strong> - ${escapeHtml(log.message)}
|
||||
</div>
|
||||
${log.details && Object.keys(log.details).length > 0 ? `
|
||||
<div class="log-details">${formatLogDetails(log.details)}</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update statistics
|
||||
*/
|
||||
function updateStats() {
|
||||
const totalRepos = repositories.length;
|
||||
const syncedRepos = repositories.filter(r => r.status === 'synced').length;
|
||||
const errorRepos = repositories.filter(r => r.status === 'error' || r.status === 'conflict').length;
|
||||
|
||||
document.getElementById('totalRepos').textContent = totalRepos;
|
||||
document.getElementById('syncedRepos').textContent = syncedRepos;
|
||||
document.getElementById('errorRepos').textContent = errorRepos;
|
||||
document.getElementById('totalLogs').textContent = stats.last_24h || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show add repository modal
|
||||
*/
|
||||
function showAddRepoModal() {
|
||||
document.getElementById('addRepoModal').classList.add('active');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add repository
|
||||
*/
|
||||
async function addRepository(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const form = event.target;
|
||||
const formData = new FormData(form);
|
||||
|
||||
const data = {
|
||||
name: formData.get('name'),
|
||||
repo_url: formData.get('repo_url'),
|
||||
branch: formData.get('branch'),
|
||||
target_path: formData.get('target_path'),
|
||||
auto_sync: formData.get('auto_sync') === 'on'
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('api/repos.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showToast('Repository erfolgreich hinzugefügt!', 'success');
|
||||
closeModal('addRepoModal');
|
||||
form.reset();
|
||||
|
||||
// Show webhook info
|
||||
if (result.repository.webhook_secret) {
|
||||
showWebhookInfoData(result.repository.webhook_url, result.repository.webhook_secret);
|
||||
}
|
||||
|
||||
loadDashboard();
|
||||
} else {
|
||||
showToast(result.error || 'Fehler beim Hinzufügen des Repositories', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error adding repository:', error);
|
||||
showToast('Fehler beim Hinzufügen des Repositories', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync repository manually
|
||||
*/
|
||||
async function syncRepository(repoId) {
|
||||
const repo = repositories.find(r => r.id === repoId);
|
||||
|
||||
if (!repo) return;
|
||||
|
||||
showToast(`Synchronisiere ${repo.name}...`, 'info');
|
||||
|
||||
try {
|
||||
const response = await fetch('api/sync.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ repo_id: repoId })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showToast(`${repo.name} erfolgreich synchronisiert! ${result.files_changed} Datei(en) geändert.`, 'success');
|
||||
loadDashboard();
|
||||
} else {
|
||||
if (result.conflict) {
|
||||
showToast(`Merge-Konflikt in ${repo.name}! Manuelle Lösung erforderlich.`, 'warning');
|
||||
} else {
|
||||
showToast(result.message || 'Fehler beim Synchronisieren', 'error');
|
||||
}
|
||||
loadDashboard();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error syncing repository:', error);
|
||||
showToast('Fehler beim Synchronisieren', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show rollback modal
|
||||
*/
|
||||
async function showRollbackModal(repoId) {
|
||||
currentRollbackRepoId = repoId;
|
||||
const modal = document.getElementById('rollbackModal');
|
||||
const commitsList = document.getElementById('commitsList');
|
||||
|
||||
modal.classList.add('active');
|
||||
commitsList.innerHTML = '<div class="loading">Lade Commits...</div>';
|
||||
|
||||
try {
|
||||
const response = await fetch(`api/rollback.php?repo_id=${repoId}&limit=20`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success && result.commits.length > 0) {
|
||||
commitsList.innerHTML = result.commits.map(commit => `
|
||||
<div class="commit-item" onclick="performRollback('${commit.hash}')">
|
||||
<div class="commit-hash">${commit.hash_short}</div>
|
||||
<div class="commit-message">${escapeHtml(commit.message)}</div>
|
||||
<div class="commit-meta">
|
||||
${escapeHtml(commit.author_name)} - ${commit.timestamp}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
commitsList.innerHTML = '<div class="empty-state">Keine Commits gefunden.</div>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading commits:', error);
|
||||
commitsList.innerHTML = '<div class="empty-state">Fehler beim Laden der Commits.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform rollback
|
||||
*/
|
||||
async function performRollback(commitHash) {
|
||||
if (!currentRollbackRepoId) return;
|
||||
|
||||
const repo = repositories.find(r => r.id === currentRollbackRepoId);
|
||||
|
||||
if (!confirm(`Möchtest du wirklich einen Rollback zu Commit ${commitHash.substring(0, 7)} durchführen?\n\nDies erstellt einen neuen Revert-Commit.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
showToast(`Führe Rollback durch...`, 'info');
|
||||
|
||||
try {
|
||||
const response = await fetch('api/rollback.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
repo_id: currentRollbackRepoId,
|
||||
commit_hash: commitHash
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showToast('Rollback erfolgreich durchgeführt!', 'success');
|
||||
closeModal('rollbackModal');
|
||||
loadDashboard();
|
||||
} else {
|
||||
showToast(result.message || 'Fehler beim Rollback', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error performing rollback:', error);
|
||||
showToast('Fehler beim Rollback', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show webhook info modal
|
||||
*/
|
||||
async function showWebhookInfo(repoId) {
|
||||
const repo = repositories.find(r => r.id === repoId);
|
||||
|
||||
if (!repo) return;
|
||||
|
||||
// Fetch webhook secret from config
|
||||
try {
|
||||
const response = await fetch(`api/repos.php?id=${repoId}`);
|
||||
const data = await response.json();
|
||||
|
||||
const webhookUrl = window.location.origin + window.location.pathname.replace('index.php', '') + 'webhook.php';
|
||||
const webhookSecret = '(Secret gespeichert auf Server)';
|
||||
|
||||
showWebhookInfoData(webhookUrl, webhookSecret);
|
||||
} catch (error) {
|
||||
console.error('Error loading webhook info:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show webhook info with data
|
||||
*/
|
||||
function showWebhookInfoData(url, secret) {
|
||||
document.getElementById('webhookUrl').value = url;
|
||||
document.getElementById('webhookSecret').value = secret;
|
||||
document.getElementById('webhookModal').classList.add('active');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete repository
|
||||
*/
|
||||
async function deleteRepository(repoId, repoName) {
|
||||
const deleteFiles = confirm(`Repository "${repoName}" aus Konfiguration entfernen?\n\nKlicke OK, um auch die Dateien vom Server zu löschen.\nKlicke Abbrechen, um nur die Konfiguration zu entfernen.`);
|
||||
|
||||
if (deleteFiles === null) return; // User cancelled
|
||||
|
||||
try {
|
||||
const response = await fetch('api/repos.php', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: repoId,
|
||||
delete_files: deleteFiles
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showToast(`Repository "${repoName}" wurde entfernt.`, 'success');
|
||||
loadDashboard();
|
||||
} else {
|
||||
showToast(result.error || 'Fehler beim Löschen', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting repository:', error);
|
||||
showToast('Fehler beim Löschen', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch branches from GitHub
|
||||
*/
|
||||
async function fetchBranches() {
|
||||
const repoUrl = document.getElementById('repoUrl').value;
|
||||
|
||||
if (!repoUrl) return;
|
||||
|
||||
const branchSelect = document.getElementById('repoBranch');
|
||||
const branchLoading = document.getElementById('branchLoading');
|
||||
|
||||
branchLoading.style.display = 'block';
|
||||
|
||||
try {
|
||||
// This would need to be implemented in the backend
|
||||
// For now, keep default branches
|
||||
branchLoading.style.display = 'none';
|
||||
} catch (error) {
|
||||
console.error('Error fetching branches:', error);
|
||||
branchLoading.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh logs
|
||||
*/
|
||||
function refreshLogs() {
|
||||
loadLogs();
|
||||
showToast('Logs aktualisiert', 'info');
|
||||
}
|
||||
|
||||
/**
|
||||
* Close modal
|
||||
*/
|
||||
function closeModal(modalId) {
|
||||
document.getElementById(modalId).classList.remove('active');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show toast notification
|
||||
*/
|
||||
function showToast(message, type = 'info') {
|
||||
const container = document.getElementById('toastContainer');
|
||||
const toast = document.createElement('div');
|
||||
|
||||
toast.className = `toast ${type}`;
|
||||
toast.textContent = message;
|
||||
|
||||
container.appendChild(toast);
|
||||
|
||||
// Auto-remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
toast.style.opacity = '0';
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy text to clipboard
|
||||
*/
|
||||
function copyToClipboard(elementId) {
|
||||
const element = document.getElementById(elementId);
|
||||
element.select();
|
||||
document.execCommand('copy');
|
||||
showToast('In Zwischenablage kopiert!', 'success');
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Escape HTML
|
||||
*/
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Get status text
|
||||
*/
|
||||
function getStatusText(status) {
|
||||
const statusMap = {
|
||||
'synced': '✅ Synchronisiert',
|
||||
'cloning': '⏳ Wird geklont...',
|
||||
'error': '❌ Fehler',
|
||||
'conflict': '⚠️ Konflikt',
|
||||
'pending': '⏸️ Ausstehend'
|
||||
};
|
||||
|
||||
return statusMap[status] || status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Format log details
|
||||
*/
|
||||
function formatLogDetails(details) {
|
||||
return Object.entries(details)
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
.join(' | ');
|
||||
}
|
||||
|
||||
// Close modal when clicking outside
|
||||
window.addEventListener('click', function(event) {
|
||||
if (event.target.classList.contains('modal')) {
|
||||
event.target.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Close modal with Escape key
|
||||
window.addEventListener('keydown', function(event) {
|
||||
if (event.key === 'Escape') {
|
||||
document.querySelectorAll('.modal.active').forEach(modal => {
|
||||
modal.classList.remove('active');
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
/**
|
||||
* GitHub Webhook Endpoint
|
||||
* Receives push events from GitHub and triggers sync
|
||||
*/
|
||||
|
||||
require_once '../src/ConfigManager.php';
|
||||
require_once '../src/Logger.php';
|
||||
require_once '../src/GitHandler.php';
|
||||
|
||||
// Set JSON response header
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Only allow POST requests
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['error' => 'Method not allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get payload
|
||||
$payload = file_get_contents('php://input');
|
||||
$data = json_decode($payload, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Invalid JSON payload']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Initialize classes
|
||||
$configManager = new ConfigManager();
|
||||
$logger = new Logger($configManager);
|
||||
$gitHandler = new GitHandler($logger, $configManager);
|
||||
|
||||
// Get repository URL from payload
|
||||
$repoUrl = $data['repository']['clone_url'] ?? null;
|
||||
|
||||
if (!$repoUrl) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Repository URL not found in payload']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Find matching repository in config
|
||||
$repos = $configManager->getRepositories();
|
||||
$matchedRepo = null;
|
||||
|
||||
foreach ($repos as $repo) {
|
||||
if ($repo['repo_url'] === $repoUrl) {
|
||||
$matchedRepo = $repo;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$matchedRepo) {
|
||||
http_response_code(404);
|
||||
$logger->warning('webhook', "Webhook received for unknown repository: $repoUrl");
|
||||
echo json_encode(['error' => 'Repository not configured']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Verify webhook signature if secret is configured
|
||||
$webhookSecret = $configManager->getWebhookSecret($matchedRepo['id']);
|
||||
|
||||
if ($webhookSecret) {
|
||||
$signature = $_SERVER['HTTP_X_HUB_SIGNATURE_256'] ?? '';
|
||||
|
||||
if (empty($signature)) {
|
||||
http_response_code(401);
|
||||
$logger->error($matchedRepo['id'], "Webhook signature missing");
|
||||
echo json_encode(['error' => 'Signature required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$expectedSignature = 'sha256=' . hash_hmac('sha256', $payload, $webhookSecret);
|
||||
|
||||
if (!hash_equals($expectedSignature, $signature)) {
|
||||
http_response_code(401);
|
||||
$logger->error($matchedRepo['id'], "Invalid webhook signature");
|
||||
echo json_encode(['error' => 'Invalid signature']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the push is for the configured branch
|
||||
$ref = $data['ref'] ?? '';
|
||||
$pushedBranch = str_replace('refs/heads/', '', $ref);
|
||||
|
||||
if ($pushedBranch !== $matchedRepo['branch']) {
|
||||
$logger->info($matchedRepo['id'], "Ignoring push to branch '$pushedBranch' (configured: '{$matchedRepo['branch']}')");
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Ignored - different branch',
|
||||
'pushed_branch' => $pushedBranch,
|
||||
'configured_branch' => $matchedRepo['branch']
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Log webhook received
|
||||
$commits = $data['commits'] ?? [];
|
||||
$logger->info($matchedRepo['id'], "Webhook received: " . count($commits) . " commits pushed", [
|
||||
'pusher' => $data['pusher']['name'] ?? 'unknown',
|
||||
'branch' => $pushedBranch
|
||||
]);
|
||||
|
||||
// Perform git pull
|
||||
$result = $gitHandler->pull(
|
||||
$matchedRepo['id'],
|
||||
$matchedRepo['target_path'],
|
||||
$matchedRepo['branch']
|
||||
);
|
||||
|
||||
// Return result
|
||||
if ($result['success']) {
|
||||
http_response_code(200);
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => $result['message'],
|
||||
'files_changed' => $result['files_changed'] ?? 0
|
||||
]);
|
||||
} else {
|
||||
// Still return 200 to GitHub, but log the error
|
||||
http_response_code(200);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => $result['message'],
|
||||
'conflict' => $result['conflict'] ?? false
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
# Deny all access to source directory
|
||||
Require all denied
|
||||
|
||||
# Alternative for older Apache versions
|
||||
<IfModule !mod_authz_core.c>
|
||||
Order deny,allow
|
||||
Deny from all
|
||||
</IfModule>
|
||||
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
/**
|
||||
* ConfigManager - Handles reading and writing JSON configuration files
|
||||
*/
|
||||
class ConfigManager {
|
||||
private $dataDir;
|
||||
|
||||
public function __construct($dataDir = '/gitpusher/data') {
|
||||
$this->dataDir = $dataDir;
|
||||
$this->ensureDataDirExists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure data directory exists with proper permissions
|
||||
*/
|
||||
private function ensureDataDirExists() {
|
||||
if (!file_exists($this->dataDir)) {
|
||||
mkdir($this->dataDir, 0755, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read JSON file
|
||||
*/
|
||||
public function read($filename) {
|
||||
$filepath = $this->dataDir . '/' . $filename;
|
||||
|
||||
if (!file_exists($filepath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$content = file_get_contents($filepath);
|
||||
$data = json_decode($content, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
error_log("JSON decode error in $filename: " . json_last_error_msg());
|
||||
return [];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write JSON file
|
||||
*/
|
||||
public function write($filename, $data) {
|
||||
$filepath = $this->dataDir . '/' . $filename;
|
||||
|
||||
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
if ($json === false) {
|
||||
error_log("JSON encode error: " . json_last_error_msg());
|
||||
return false;
|
||||
}
|
||||
|
||||
$result = file_put_contents($filepath, $json);
|
||||
|
||||
if ($result === false) {
|
||||
error_log("Failed to write file: $filepath");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set appropriate permissions (readable only by owner)
|
||||
chmod($filepath, 0600);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all repositories from config
|
||||
*/
|
||||
public function getRepositories() {
|
||||
$config = $this->read('config.json');
|
||||
return $config['repositories'] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get repository by ID
|
||||
*/
|
||||
public function getRepository($repoId) {
|
||||
$repos = $this->getRepositories();
|
||||
|
||||
foreach ($repos as $repo) {
|
||||
if ($repo['id'] === $repoId) {
|
||||
return $repo;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add new repository
|
||||
*/
|
||||
public function addRepository($repoData) {
|
||||
$config = $this->read('config.json');
|
||||
|
||||
if (!isset($config['repositories'])) {
|
||||
$config['repositories'] = [];
|
||||
}
|
||||
|
||||
// Generate unique ID
|
||||
$repoData['id'] = uniqid('repo_', true);
|
||||
$repoData['created_at'] = date('Y-m-d H:i:s');
|
||||
$repoData['status'] = 'pending';
|
||||
|
||||
$config['repositories'][] = $repoData;
|
||||
|
||||
return $this->write('config.json', $config) ? $repoData['id'] : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update repository
|
||||
*/
|
||||
public function updateRepository($repoId, $updates) {
|
||||
$config = $this->read('config.json');
|
||||
|
||||
if (!isset($config['repositories'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($config['repositories'] as &$repo) {
|
||||
if ($repo['id'] === $repoId) {
|
||||
$repo = array_merge($repo, $updates);
|
||||
$repo['updated_at'] = date('Y-m-d H:i:s');
|
||||
return $this->write('config.json', $config);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete repository
|
||||
*/
|
||||
public function deleteRepository($repoId) {
|
||||
$config = $this->read('config.json');
|
||||
|
||||
if (!isset($config['repositories'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$config['repositories'] = array_filter($config['repositories'], function($repo) use ($repoId) {
|
||||
return $repo['id'] !== $repoId;
|
||||
});
|
||||
|
||||
// Re-index array
|
||||
$config['repositories'] = array_values($config['repositories']);
|
||||
|
||||
return $this->write('config.json', $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get GitHub Personal Access Token
|
||||
*/
|
||||
public function getGitHubToken() {
|
||||
$secrets = $this->read('secrets.json');
|
||||
return $secrets['github_pat'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set GitHub Personal Access Token
|
||||
*/
|
||||
public function setGitHubToken($token) {
|
||||
$secrets = $this->read('secrets.json');
|
||||
$secrets['github_pat'] = $token;
|
||||
return $this->write('secrets.json', $secrets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get webhook secret for a repository
|
||||
*/
|
||||
public function getWebhookSecret($repoId) {
|
||||
$secrets = $this->read('secrets.json');
|
||||
return $secrets['webhook_secrets'][$repoId] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set webhook secret for a repository
|
||||
*/
|
||||
public function setWebhookSecret($repoId, $secret) {
|
||||
$secrets = $this->read('secrets.json');
|
||||
|
||||
if (!isset($secrets['webhook_secrets'])) {
|
||||
$secrets['webhook_secrets'] = [];
|
||||
}
|
||||
|
||||
$secrets['webhook_secrets'][$repoId] = $secret;
|
||||
|
||||
return $this->write('secrets.json', $secrets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate secure webhook secret
|
||||
*/
|
||||
public function generateWebhookSecret() {
|
||||
return bin2hex(random_bytes(32));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
<?php
|
||||
/**
|
||||
* GitHandler - Handles all Git operations
|
||||
*/
|
||||
class GitHandler {
|
||||
private $logger;
|
||||
private $configManager;
|
||||
|
||||
public function __construct(Logger $logger, ConfigManager $configManager) {
|
||||
$this->logger = $logger;
|
||||
$this->configManager = $configManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute shell command and return result
|
||||
*/
|
||||
private function exec($command, $cwd = null) {
|
||||
$descriptorspec = [
|
||||
0 => ['pipe', 'r'], // stdin
|
||||
1 => ['pipe', 'w'], // stdout
|
||||
2 => ['pipe', 'w'] // stderr
|
||||
];
|
||||
|
||||
$process = proc_open($command, $descriptorspec, $pipes, $cwd);
|
||||
|
||||
if (!is_resource($process)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'output' => '',
|
||||
'error' => 'Failed to execute command',
|
||||
'exit_code' => -1
|
||||
];
|
||||
}
|
||||
|
||||
$stdout = stream_get_contents($pipes[1]);
|
||||
$stderr = stream_get_contents($pipes[2]);
|
||||
|
||||
fclose($pipes[0]);
|
||||
fclose($pipes[1]);
|
||||
fclose($pipes[2]);
|
||||
|
||||
$exitCode = proc_close($process);
|
||||
|
||||
return [
|
||||
'success' => $exitCode === 0,
|
||||
'output' => trim($stdout),
|
||||
'error' => trim($stderr),
|
||||
'exit_code' => $exitCode
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Git URL with authentication token
|
||||
*/
|
||||
private function buildGitUrl($repoUrl, $token) {
|
||||
// Convert https://github.com/user/repo.git to https://TOKEN@github.com/user/repo.git
|
||||
$parsed = parse_url($repoUrl);
|
||||
|
||||
if (!$parsed || !isset($parsed['host'])) {
|
||||
return $repoUrl;
|
||||
}
|
||||
|
||||
$scheme = $parsed['scheme'] ?? 'https';
|
||||
$host = $parsed['host'];
|
||||
$path = $parsed['path'] ?? '';
|
||||
|
||||
return "$scheme://$token@$host$path";
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone repository
|
||||
*/
|
||||
public function cloneRepository($repoId, $repoUrl, $targetPath, $branch = 'main') {
|
||||
$this->logger->info($repoId, "Starting clone of repository to $targetPath");
|
||||
|
||||
// Check if target path already exists
|
||||
if (file_exists($targetPath)) {
|
||||
$this->logger->error($repoId, "Target path already exists: $targetPath");
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Target path already exists'
|
||||
];
|
||||
}
|
||||
|
||||
// Create parent directory if it doesn't exist
|
||||
$parentDir = dirname($targetPath);
|
||||
if (!file_exists($parentDir)) {
|
||||
mkdir($parentDir, 0755, true);
|
||||
}
|
||||
|
||||
// Get GitHub token
|
||||
$token = $this->configManager->getGitHubToken();
|
||||
$gitUrl = $token ? $this->buildGitUrl($repoUrl, $token) : $repoUrl;
|
||||
|
||||
// Clone repository
|
||||
$command = sprintf(
|
||||
'git clone --branch %s %s %s 2>&1',
|
||||
escapeshellarg($branch),
|
||||
escapeshellarg($gitUrl),
|
||||
escapeshellarg($targetPath)
|
||||
);
|
||||
|
||||
$result = $this->exec($command);
|
||||
|
||||
if ($result['success']) {
|
||||
$this->logger->success($repoId, "Repository cloned successfully", [
|
||||
'path' => $targetPath,
|
||||
'branch' => $branch
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => 'Repository cloned successfully',
|
||||
'output' => $result['output']
|
||||
];
|
||||
} else {
|
||||
$this->logger->error($repoId, "Failed to clone repository", [
|
||||
'error' => $result['error'],
|
||||
'output' => $result['output']
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Failed to clone repository',
|
||||
'error' => $result['error']
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull latest changes
|
||||
*/
|
||||
public function pull($repoId, $targetPath, $branch = 'main') {
|
||||
$this->logger->info($repoId, "Starting pull for $targetPath");
|
||||
|
||||
// Check if path exists
|
||||
if (!file_exists($targetPath)) {
|
||||
$this->logger->error($repoId, "Repository path does not exist: $targetPath");
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Repository path does not exist'
|
||||
];
|
||||
}
|
||||
|
||||
// Check if it's a git repository
|
||||
if (!file_exists("$targetPath/.git")) {
|
||||
$this->logger->error($repoId, "Not a git repository: $targetPath");
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Not a git repository'
|
||||
];
|
||||
}
|
||||
|
||||
// Get current commit before pull
|
||||
$currentCommit = $this->getCurrentCommit($targetPath);
|
||||
|
||||
// Pull changes
|
||||
$command = sprintf(
|
||||
'cd %s && git pull origin %s 2>&1',
|
||||
escapeshellarg($targetPath),
|
||||
escapeshellarg($branch)
|
||||
);
|
||||
|
||||
$result = $this->exec($command);
|
||||
|
||||
// Check for merge conflicts
|
||||
if (!$result['success'] || strpos($result['output'], 'CONFLICT') !== false) {
|
||||
$this->logger->warning($repoId, "Merge conflict detected", [
|
||||
'output' => $result['output'],
|
||||
'error' => $result['error']
|
||||
]);
|
||||
|
||||
$this->configManager->updateRepository($repoId, ['status' => 'conflict']);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Merge conflict detected',
|
||||
'conflict' => true,
|
||||
'output' => $result['output']
|
||||
];
|
||||
}
|
||||
|
||||
// Get new commit after pull
|
||||
$newCommit = $this->getCurrentCommit($targetPath);
|
||||
|
||||
// Count changed files
|
||||
$changedFiles = $this->getChangedFilesBetweenCommits($targetPath, $currentCommit, $newCommit);
|
||||
|
||||
if ($result['success']) {
|
||||
$this->logger->success($repoId, "Pull completed successfully", [
|
||||
'files_changed' => count($changedFiles),
|
||||
'old_commit' => substr($currentCommit, 0, 7),
|
||||
'new_commit' => substr($newCommit, 0, 7)
|
||||
]);
|
||||
|
||||
$this->configManager->updateRepository($repoId, [
|
||||
'status' => 'synced',
|
||||
'last_sync' => date('Y-m-d H:i:s'),
|
||||
'last_commit' => $newCommit
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => 'Pull completed successfully',
|
||||
'files_changed' => count($changedFiles),
|
||||
'output' => $result['output']
|
||||
];
|
||||
} else {
|
||||
$this->logger->error($repoId, "Pull failed", [
|
||||
'error' => $result['error'],
|
||||
'output' => $result['output']
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Pull failed',
|
||||
'error' => $result['error']
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Revert to specific commit
|
||||
*/
|
||||
public function revert($repoId, $targetPath, $commitHash) {
|
||||
$this->logger->info($repoId, "Starting revert to commit $commitHash");
|
||||
|
||||
if (!file_exists($targetPath)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Repository path does not exist'
|
||||
];
|
||||
}
|
||||
|
||||
// Create revert commit
|
||||
$command = sprintf(
|
||||
'cd %s && git revert --no-edit %s 2>&1',
|
||||
escapeshellarg($targetPath),
|
||||
escapeshellarg($commitHash)
|
||||
);
|
||||
|
||||
$result = $this->exec($command);
|
||||
|
||||
if ($result['success']) {
|
||||
$this->logger->success($repoId, "Reverted to commit $commitHash", [
|
||||
'commit' => $commitHash
|
||||
]);
|
||||
|
||||
$newCommit = $this->getCurrentCommit($targetPath);
|
||||
|
||||
$this->configManager->updateRepository($repoId, [
|
||||
'last_commit' => $newCommit,
|
||||
'last_sync' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => 'Revert completed successfully',
|
||||
'output' => $result['output']
|
||||
];
|
||||
} else {
|
||||
$this->logger->error($repoId, "Revert failed", [
|
||||
'error' => $result['error']
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Revert failed',
|
||||
'error' => $result['error']
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current commit hash
|
||||
*/
|
||||
public function getCurrentCommit($targetPath) {
|
||||
$result = $this->exec("cd " . escapeshellarg($targetPath) . " && git rev-parse HEAD 2>&1");
|
||||
return $result['success'] ? trim($result['output']) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get commit history
|
||||
*/
|
||||
public function getCommitHistory($targetPath, $limit = 20) {
|
||||
$command = sprintf(
|
||||
'cd %s && git log --pretty=format:"%%H|%%an|%%ae|%%at|%%s" -n %d 2>&1',
|
||||
escapeshellarg($targetPath),
|
||||
(int)$limit
|
||||
);
|
||||
|
||||
$result = $this->exec($command);
|
||||
|
||||
if (!$result['success']) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$commits = [];
|
||||
$lines = explode("\n", $result['output']);
|
||||
|
||||
foreach ($lines as $line) {
|
||||
if (empty($line)) continue;
|
||||
|
||||
$parts = explode('|', $line);
|
||||
if (count($parts) !== 5) continue;
|
||||
|
||||
$commits[] = [
|
||||
'hash' => $parts[0],
|
||||
'hash_short' => substr($parts[0], 0, 7),
|
||||
'author_name' => $parts[1],
|
||||
'author_email' => $parts[2],
|
||||
'timestamp' => date('Y-m-d H:i:s', (int)$parts[3]),
|
||||
'message' => $parts[4]
|
||||
];
|
||||
}
|
||||
|
||||
return $commits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get changed files between commits
|
||||
*/
|
||||
private function getChangedFilesBetweenCommits($targetPath, $oldCommit, $newCommit) {
|
||||
if ($oldCommit === $newCommit) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$command = sprintf(
|
||||
'cd %s && git diff --name-only %s %s 2>&1',
|
||||
escapeshellarg($targetPath),
|
||||
escapeshellarg($oldCommit),
|
||||
escapeshellarg($newCommit)
|
||||
);
|
||||
|
||||
$result = $this->exec($command);
|
||||
|
||||
if (!$result['success']) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_filter(explode("\n", $result['output']));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get repository status
|
||||
*/
|
||||
public function getStatus($targetPath) {
|
||||
$command = sprintf(
|
||||
'cd %s && git status --porcelain 2>&1',
|
||||
escapeshellarg($targetPath)
|
||||
);
|
||||
|
||||
$result = $this->exec($command);
|
||||
|
||||
return [
|
||||
'success' => $result['success'],
|
||||
'clean' => $result['success'] && empty($result['output']),
|
||||
'output' => $result['output']
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current branch
|
||||
*/
|
||||
public function getCurrentBranch($targetPath) {
|
||||
$result = $this->exec("cd " . escapeshellarg($targetPath) . " && git branch --show-current 2>&1");
|
||||
return $result['success'] ? trim($result['output']) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch available branches from remote
|
||||
*/
|
||||
public function getRemoteBranches($repoUrl) {
|
||||
$token = $this->configManager->getGitHubToken();
|
||||
$gitUrl = $token ? $this->buildGitUrl($repoUrl, $token) : $repoUrl;
|
||||
|
||||
$command = sprintf(
|
||||
'git ls-remote --heads %s 2>&1',
|
||||
escapeshellarg($gitUrl)
|
||||
);
|
||||
|
||||
$result = $this->exec($command);
|
||||
|
||||
if (!$result['success']) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$branches = [];
|
||||
$lines = explode("\n", $result['output']);
|
||||
|
||||
foreach ($lines as $line) {
|
||||
if (preg_match('/refs\/heads\/(.+)$/', $line, $matches)) {
|
||||
$branches[] = $matches[1];
|
||||
}
|
||||
}
|
||||
|
||||
return $branches;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
/**
|
||||
* Logger - Manages log entries for sync operations
|
||||
*/
|
||||
class Logger {
|
||||
private $configManager;
|
||||
private $maxLogEntries = 1000; // Keep last 1000 entries
|
||||
|
||||
public function __construct(ConfigManager $configManager) {
|
||||
$this->configManager = $configManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add log entry
|
||||
*/
|
||||
public function log($repoId, $type, $message, $details = []) {
|
||||
$logs = $this->configManager->read('log.json');
|
||||
|
||||
if (!isset($logs['entries'])) {
|
||||
$logs['entries'] = [];
|
||||
}
|
||||
|
||||
$entry = [
|
||||
'id' => uniqid('log_', true),
|
||||
'timestamp' => date('Y-m-d H:i:s'),
|
||||
'repo_id' => $repoId,
|
||||
'type' => $type, // success, error, warning, info
|
||||
'message' => $message,
|
||||
'details' => $details
|
||||
];
|
||||
|
||||
// Add to beginning of array
|
||||
array_unshift($logs['entries'], $entry);
|
||||
|
||||
// Keep only last N entries
|
||||
if (count($logs['entries']) > $this->maxLogEntries) {
|
||||
$logs['entries'] = array_slice($logs['entries'], 0, $this->maxLogEntries);
|
||||
}
|
||||
|
||||
$this->configManager->write('log.json', $logs);
|
||||
|
||||
// Also log to PHP error log for debugging
|
||||
error_log("[GitPusher] [$type] $repoId: $message");
|
||||
|
||||
return $entry['id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Log success
|
||||
*/
|
||||
public function success($repoId, $message, $details = []) {
|
||||
return $this->log($repoId, 'success', $message, $details);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log error
|
||||
*/
|
||||
public function error($repoId, $message, $details = []) {
|
||||
return $this->log($repoId, 'error', $message, $details);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log warning
|
||||
*/
|
||||
public function warning($repoId, $message, $details = []) {
|
||||
return $this->log($repoId, 'warning', $message, $details);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log info
|
||||
*/
|
||||
public function info($repoId, $message, $details = []) {
|
||||
return $this->log($repoId, 'info', $message, $details);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all log entries
|
||||
*/
|
||||
public function getAll($limit = 100, $offset = 0) {
|
||||
$logs = $this->configManager->read('log.json');
|
||||
$entries = $logs['entries'] ?? [];
|
||||
|
||||
return array_slice($entries, $offset, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get logs for specific repository
|
||||
*/
|
||||
public function getByRepository($repoId, $limit = 100) {
|
||||
$logs = $this->configManager->read('log.json');
|
||||
$entries = $logs['entries'] ?? [];
|
||||
|
||||
$filtered = array_filter($entries, function($entry) use ($repoId) {
|
||||
return $entry['repo_id'] === $repoId;
|
||||
});
|
||||
|
||||
return array_slice(array_values($filtered), 0, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get logs by type
|
||||
*/
|
||||
public function getByType($type, $limit = 100) {
|
||||
$logs = $this->configManager->read('log.json');
|
||||
$entries = $logs['entries'] ?? [];
|
||||
|
||||
$filtered = array_filter($entries, function($entry) use ($type) {
|
||||
return $entry['type'] === $type;
|
||||
});
|
||||
|
||||
return array_slice(array_values($filtered), 0, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all logs
|
||||
*/
|
||||
public function clear() {
|
||||
return $this->configManager->write('log.json', ['entries' => []]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear logs for specific repository
|
||||
*/
|
||||
public function clearByRepository($repoId) {
|
||||
$logs = $this->configManager->read('log.json');
|
||||
$entries = $logs['entries'] ?? [];
|
||||
|
||||
$filtered = array_filter($entries, function($entry) use ($repoId) {
|
||||
return $entry['repo_id'] !== $repoId;
|
||||
});
|
||||
|
||||
$logs['entries'] = array_values($filtered);
|
||||
|
||||
return $this->configManager->write('log.json', $logs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get statistics
|
||||
*/
|
||||
public function getStats() {
|
||||
$logs = $this->configManager->read('log.json');
|
||||
$entries = $logs['entries'] ?? [];
|
||||
|
||||
$stats = [
|
||||
'total' => count($entries),
|
||||
'success' => 0,
|
||||
'error' => 0,
|
||||
'warning' => 0,
|
||||
'info' => 0,
|
||||
'last_24h' => 0
|
||||
];
|
||||
|
||||
$yesterday = strtotime('-24 hours');
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
$stats[$entry['type']]++;
|
||||
|
||||
$timestamp = strtotime($entry['timestamp']);
|
||||
if ($timestamp >= $yesterday) {
|
||||
$stats['last_24h']++;
|
||||
}
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user