550 Zeilen
15 KiB
JavaScript
550 Zeilen
15 KiB
JavaScript
/**
|
|
* 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
|
|
};
|