Dieser Commit ist enthalten in:
Claude Project Manager
2025-12-28 21:36:45 +00:00
Commit ab1e5be9a9
146 geänderte Dateien mit 65525 neuen und 0 gelöschten Zeilen

549
backend/services/gitService.js Normale Datei
Datei anzeigen

@ -0,0 +1,549 @@
/**
* TASKMATE - Git Service
* =======================
* Lokale Git-Operationen über child_process
*/
const { execSync, exec } = require('child_process');
const path = require('path');
const fs = require('fs');
const logger = require('../utils/logger');
/**
* Konvertiert einen Windows-Pfad zu einem Docker-Container-Pfad
* z.B. "D:\Projekte\MyApp" -> "/mnt/d/Projekte/MyApp"
*/
function windowsToContainerPath(windowsPath) {
if (!windowsPath) return null;
// Bereits ein Container-Pfad?
if (windowsPath.startsWith('/mnt/')) {
return windowsPath;
}
// Windows-Pfad konvertieren (z.B. "C:\foo" oder "C:/foo")
const normalized = windowsPath.replace(/\\/g, '/');
const match = normalized.match(/^([a-zA-Z]):[\/](.*)$/);
if (match) {
const drive = match[1].toLowerCase();
const restPath = match[2];
return `/mnt/${drive}/${restPath}`;
}
return windowsPath;
}
/**
* Konvertiert einen Docker-Container-Pfad zu einem Windows-Pfad
* z.B. "/mnt/d/Projekte/MyApp" -> "D:\Projekte\MyApp"
*/
function containerToWindowsPath(containerPath) {
if (!containerPath) return null;
const match = containerPath.match(/^\/mnt\/([a-z])\/(.*)$/);
if (match) {
const drive = match[1].toUpperCase();
const restPath = match[2].replace(/\//g, '\\');
return `${drive}:\\${restPath}`;
}
return containerPath;
}
/**
* Prüft, ob ein Pfad existiert und erreichbar ist
*/
function isPathAccessible(localPath) {
const containerPath = windowsToContainerPath(localPath);
try {
// Prüfe ob das übergeordnete Verzeichnis existiert
const parentDir = path.dirname(containerPath);
return fs.existsSync(parentDir);
} catch (error) {
logger.error('Pfadzugriff fehlgeschlagen:', error);
return false;
}
}
/**
* Prüft, ob ein Verzeichnis ein Git-Repository ist
*/
function isGitRepository(localPath) {
const containerPath = windowsToContainerPath(localPath);
try {
const gitDir = path.join(containerPath, '.git');
return fs.existsSync(gitDir);
} catch (error) {
return false;
}
}
/**
* Führt einen Git-Befehl aus
*/
function execGitCommand(command, cwd, options = {}) {
const containerPath = windowsToContainerPath(cwd);
const timeout = options.timeout || 60000; // 60 Sekunden Standard-Timeout
try {
const result = execSync(command, {
cwd: containerPath,
encoding: 'utf8',
timeout,
maxBuffer: 10 * 1024 * 1024, // 10 MB
env: {
...process.env,
GIT_TERMINAL_PROMPT: '0', // Keine interaktiven Prompts
GIT_SSH_COMMAND: 'ssh -o StrictHostKeyChecking=no'
}
});
return { success: true, output: result.trim() };
} catch (error) {
logger.error(`Git-Befehl fehlgeschlagen: ${command}`, error.message);
return {
success: false,
error: error.message,
stderr: error.stderr?.toString() || ''
};
}
}
/**
* Repository klonen
*/
async function cloneRepository(repoUrl, localPath, options = {}) {
const containerPath = windowsToContainerPath(localPath);
const branch = options.branch || 'main';
// Prüfe, ob das Zielverzeichnis bereits existiert
if (fs.existsSync(containerPath)) {
if (isGitRepository(localPath)) {
return { success: false, error: 'Verzeichnis enthält bereits ein Git-Repository' };
}
// Verzeichnis existiert, aber ist kein Git-Repo - prüfe ob leer
const files = fs.readdirSync(containerPath);
if (files.length > 0) {
return { success: false, error: 'Verzeichnis ist nicht leer' };
}
} else {
// Erstelle Verzeichnis
fs.mkdirSync(containerPath, { recursive: true });
}
// Gitea-Token für Authentifizierung hinzufügen
let authUrl = repoUrl;
const giteaToken = process.env.GITEA_TOKEN;
if (giteaToken && repoUrl.includes('gitea')) {
// Füge Token zur URL hinzu: https://token@gitea.example.com/...
authUrl = repoUrl.replace('https://', `https://${giteaToken}@`);
}
const command = `git clone --branch ${branch} "${authUrl}" "${containerPath}"`;
try {
execSync(command, {
encoding: 'utf8',
timeout: 300000, // 5 Minuten für Clone
maxBuffer: 50 * 1024 * 1024, // 50 MB
env: {
...process.env,
GIT_TERMINAL_PROMPT: '0'
}
});
logger.info(`Repository geklont: ${repoUrl} -> ${localPath}`);
return { success: true, message: 'Repository erfolgreich geklont' };
} catch (error) {
logger.error('Clone fehlgeschlagen:', error.message);
// Bereinige bei Fehler
try {
if (fs.existsSync(containerPath)) {
fs.rmSync(containerPath, { recursive: true, force: true });
}
} catch (e) {
// Ignorieren
}
return { success: false, error: error.message };
}
}
/**
* Änderungen ziehen (Pull)
*/
function pullChanges(localPath, options = {}) {
if (!isGitRepository(localPath)) {
return { success: false, error: 'Kein Git-Repository' };
}
const branch = options.branch || '';
const command = branch ? `git pull origin ${branch}` : 'git pull';
return execGitCommand(command, localPath);
}
/**
* Änderungen hochladen (Push)
*/
function pushChanges(localPath, options = {}) {
if (!isGitRepository(localPath)) {
return { success: false, error: 'Kein Git-Repository' };
}
const branch = options.branch || '';
const command = branch ? `git push origin ${branch}` : 'git push';
return execGitCommand(command, localPath, { timeout: 120000 });
}
/**
* Git-Status abrufen
*/
function getStatus(localPath) {
if (!isGitRepository(localPath)) {
return { success: false, error: 'Kein Git-Repository' };
}
const result = execGitCommand('git status --porcelain', localPath);
if (!result.success) {
return result;
}
const lines = result.output.split('\n').filter(l => l.trim());
const changes = lines.map(line => {
const status = line.substring(0, 2);
const file = line.substring(3);
return { status, file };
});
// Zusätzliche Infos
const branchResult = execGitCommand('git branch --show-current', localPath);
const aheadBehindResult = execGitCommand('git rev-list --left-right --count HEAD...@{upstream} 2>/dev/null || echo "0 0"', localPath);
let ahead = 0;
let behind = 0;
if (aheadBehindResult.success) {
const parts = aheadBehindResult.output.split(/\s+/);
ahead = parseInt(parts[0]) || 0;
behind = parseInt(parts[1]) || 0;
}
return {
success: true,
branch: branchResult.success ? branchResult.output : 'unknown',
changes,
hasChanges: changes.length > 0,
ahead,
behind,
isClean: changes.length === 0 && ahead === 0
};
}
/**
* Alle Änderungen stagen
*/
function stageAll(localPath) {
if (!isGitRepository(localPath)) {
return { success: false, error: 'Kein Git-Repository' };
}
return execGitCommand('git add -A', localPath);
}
/**
* Commit erstellen
*/
function commit(localPath, message) {
if (!isGitRepository(localPath)) {
return { success: false, error: 'Kein Git-Repository' };
}
if (!message || message.trim() === '') {
return { success: false, error: 'Commit-Nachricht erforderlich' };
}
// Escape für Shell
const escapedMessage = message.replace(/"/g, '\\"');
return execGitCommand(`git commit -m "${escapedMessage}"`, localPath);
}
/**
* Commit-Historie abrufen
*/
function getCommitHistory(localPath, limit = 20) {
if (!isGitRepository(localPath)) {
return { success: false, error: 'Kein Git-Repository' };
}
const format = '%H|%h|%an|%ae|%at|%s';
const result = execGitCommand(`git log -${limit} --format="${format}"`, localPath);
if (!result.success) {
return result;
}
const commits = result.output.split('\n').filter(l => l.trim()).map(line => {
const [hash, shortHash, author, email, timestamp, subject] = line.split('|');
return {
hash,
shortHash,
author,
email,
date: new Date(parseInt(timestamp) * 1000).toISOString(),
message: subject
};
});
return { success: true, commits };
}
/**
* Branches auflisten
*/
function getBranches(localPath) {
if (!isGitRepository(localPath)) {
return { success: false, error: 'Kein Git-Repository' };
}
const result = execGitCommand('git branch -a', localPath);
if (!result.success) {
return result;
}
const branches = result.output.split('\n').filter(l => l.trim()).map(line => {
const isCurrent = line.startsWith('*');
const name = line.replace(/^\*?\s+/, '').trim();
const isRemote = name.startsWith('remotes/');
return { name, isCurrent, isRemote };
});
return { success: true, branches };
}
/**
* Branch wechseln
*/
function checkoutBranch(localPath, branch) {
if (!isGitRepository(localPath)) {
return { success: false, error: 'Kein Git-Repository' };
}
return execGitCommand(`git checkout ${branch}`, localPath);
}
/**
* Fetch von Remote
*/
function fetchRemote(localPath) {
if (!isGitRepository(localPath)) {
return { success: false, error: 'Kein Git-Repository' };
}
return execGitCommand('git fetch --all', localPath, { timeout: 120000 });
}
/**
* Remote-URL abrufen
*/
function getRemoteUrl(localPath) {
if (!isGitRepository(localPath)) {
return { success: false, error: 'Kein Git-Repository' };
}
const result = execGitCommand('git remote get-url origin', localPath);
if (result.success) {
// Entferne Token aus URL für Anzeige
let url = result.output;
url = url.replace(/https:\/\/[^@]+@/, 'https://');
return { success: true, url };
}
return result;
}
/**
* Git Repository initialisieren
*/
function initRepository(localPath, options = {}) {
const containerPath = windowsToContainerPath(localPath);
const branch = options.branch || 'main';
// Prüfe ob bereits ein Git-Repo existiert
if (isGitRepository(localPath)) {
return { success: true, message: 'Git-Repository existiert bereits' };
}
// Prüfe ob Verzeichnis existiert
if (!fs.existsSync(containerPath)) {
return { success: false, error: 'Verzeichnis existiert nicht' };
}
// Git init ausführen
const initResult = execGitCommand(`git init -b ${branch}`, localPath);
if (!initResult.success) {
return initResult;
}
logger.info(`Git-Repository initialisiert: ${localPath}`);
return { success: true, message: 'Git-Repository initialisiert' };
}
/**
* Remote hinzufügen oder aktualisieren
*/
function setRemote(localPath, remoteUrl, remoteName = 'origin') {
if (!isGitRepository(localPath)) {
return { success: false, error: 'Kein Git-Repository' };
}
// Gitea-Token für Authentifizierung hinzufügen
let authUrl = remoteUrl;
const giteaToken = process.env.GITEA_TOKEN;
if (giteaToken && (remoteUrl.includes('gitea') || remoteUrl.includes('aegis-sight'))) {
authUrl = remoteUrl.replace('https://', `https://${giteaToken}@`);
}
// Prüfe ob Remote bereits existiert
const checkResult = execGitCommand(`git remote get-url ${remoteName}`, localPath);
if (checkResult.success) {
// Remote existiert - aktualisieren
const result = execGitCommand(`git remote set-url ${remoteName} "${authUrl}"`, localPath);
if (result.success) {
logger.info(`Remote '${remoteName}' aktualisiert: ${remoteUrl}`);
return { success: true, message: 'Remote aktualisiert' };
}
return result;
} else {
// Remote existiert nicht - hinzufügen
const result = execGitCommand(`git remote add ${remoteName} "${authUrl}"`, localPath);
if (result.success) {
logger.info(`Remote '${remoteName}' hinzugefügt: ${remoteUrl}`);
return { success: true, message: 'Remote hinzugefügt' };
}
return result;
}
}
/**
* Prüft ob ein Remote existiert
*/
function hasRemote(localPath, remoteName = 'origin') {
if (!isGitRepository(localPath)) {
return false;
}
const result = execGitCommand(`git remote get-url ${remoteName}`, localPath);
return result.success;
}
/**
* Initialen Push mit Upstream-Tracking
*/
function pushWithUpstream(localPath, branch = null, remoteName = 'origin') {
if (!isGitRepository(localPath)) {
return { success: false, error: 'Kein Git-Repository' };
}
// Aktuellen Branch ermitteln falls nicht angegeben
if (!branch) {
const branchResult = execGitCommand('git branch --show-current', localPath);
branch = branchResult.success && branchResult.output ? branchResult.output : 'main';
}
// Prüfe ob Commits existieren
const logResult = execGitCommand('git rev-parse HEAD', localPath);
if (!logResult.success) {
// Keine Commits - erstelle einen initialen Commit
const statusResult = execGitCommand('git status --porcelain', localPath);
if (statusResult.success && statusResult.output.trim()) {
// Es gibt Dateien - stage und commit
const stageResult = stageAll(localPath);
if (!stageResult.success) {
return { success: false, error: 'Staging fehlgeschlagen: ' + stageResult.error };
}
const commitResult = commit(localPath, 'Initial commit');
if (!commitResult.success) {
return { success: false, error: 'Initial commit fehlgeschlagen: ' + commitResult.error };
}
logger.info('Initialer Commit erstellt vor Push');
} else {
return { success: false, error: 'Keine Commits und keine Dateien zum Committen vorhanden' };
}
}
// Push mit -u für Upstream-Tracking
return execGitCommand(`git push -u ${remoteName} ${branch}`, localPath, { timeout: 120000 });
}
/**
* Repository für Gitea vorbereiten (init, remote, initial commit)
*/
function prepareForGitea(localPath, remoteUrl, options = {}) {
const branch = options.branch || 'main';
const containerPath = windowsToContainerPath(localPath);
// 1. Prüfe ob Git-Repo existiert, wenn nicht initialisieren
if (!isGitRepository(localPath)) {
const initResult = initRepository(localPath, { branch });
if (!initResult.success) {
return initResult;
}
}
// 2. Remote hinzufügen/aktualisieren
const remoteResult = setRemote(localPath, remoteUrl);
if (!remoteResult.success) {
return remoteResult;
}
// 3. Prüfe ob Commits existieren
const logResult = execGitCommand('git rev-parse HEAD', localPath);
if (!logResult.success) {
// Keine Commits - erstelle initialen Commit wenn Dateien vorhanden
const statusResult = execGitCommand('git status --porcelain', localPath);
if (statusResult.success && statusResult.output.trim()) {
// Es gibt Dateien - stage und commit
const stageResult = stageAll(localPath);
if (!stageResult.success) {
return stageResult;
}
const commitResult = commit(localPath, 'Initial commit');
if (!commitResult.success) {
return commitResult;
}
}
}
logger.info(`Repository für Gitea vorbereitet: ${localPath} -> ${remoteUrl}`);
return { success: true, message: 'Repository für Gitea vorbereitet' };
}
module.exports = {
windowsToContainerPath,
containerToWindowsPath,
isPathAccessible,
isGitRepository,
cloneRepository,
pullChanges,
pushChanges,
getStatus,
stageAll,
commit,
getCommitHistory,
getBranches,
checkoutBranch,
fetchRemote,
getRemoteUrl,
initRepository,
setRemote,
hasRemote,
pushWithUpstream,
prepareForGitea
};