diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1c11102 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +PORT=4000 +NODE_ENV=production +PORTAINER_URL=https://your-portainer.example.com +PORTAINER_API_KEY=your_api_key_here diff --git a/backend/index.js b/backend/index.js index dcc837e..398d5a3 100644 --- a/backend/index.js +++ b/backend/index.js @@ -15,92 +15,99 @@ const __dirname = path.dirname(__filename); const app = express(); app.use(express.json()); -// Statische Frontend-Dateien ausliefern app.use(express.static(path.join(__dirname, 'public'))); -// SPA-Fallback für React-Router app.get('*', (req, res, next) => { if (req.path.startsWith('/api')) return next(); res.sendFile(path.join(__dirname, 'public', 'index.html')); }); -// Backend-Port fix const PORT = 4001; - -// Endpoint-ID aus der env const ENDPOINT_ID = Number(process.env.PORTAINER_ENDPOINT_ID); -// HTTPS Agent für Self-Signed-Zertifikate const agent = new https.Agent({ rejectUnauthorized: false }); - -// Axios-Instance für Portainer const axiosInstance = axios.create({ httpsAgent: agent, headers: { "X-API-Key": process.env.PORTAINER_API_KEY }, baseURL: process.env.PORTAINER_URL, }); -// In-Memory Redeploy-Status const redeployingStacks = {}; -// HTTP Server + Socket.IO const server = http.createServer(app); const io = new Server(server, { cors: { origin: "*" } }); io.on("connection", (socket) => { - console.log("Client verbunden:", socket.id); + console.log(`🔌 [Socket] Client verbunden: ${socket.id}`); }); const broadcastRedeployStatus = (stackId, status) => { redeployingStacks[stackId] = status; io.emit("redeployStatus", { stackId, status }); + console.log(`🔄 [RedeployStatus] Stack ${stackId} ist jetzt ${status ? "im Redeploy" : "fertig"}`); }; // --- API Endpoints --- + app.get('/api/stacks', async (req, res) => { + console.log("ℹ️ [API] GET /api/stacks: Abruf gestartet"); try { const stacksRes = await axiosInstance.get('/api/stacks'); - - // Filter nach Endpoint-ID const filteredStacks = stacksRes.data.filter(stack => stack.EndpointId === ENDPOINT_ID); + const uniqueStacksMap = {}; + filteredStacks.forEach(stack => { + if (!uniqueStacksMap[stack.Name]) uniqueStacksMap[stack.Name] = stack; + }); + const uniqueStacks = Object.values(uniqueStacksMap); + const stacksWithStatus = await Promise.all( - filteredStacks.map(async (stack) => { + uniqueStacks.map(async (stack) => { try { const statusRes = await axiosInstance.get( `/api/stacks/${stack.Id}/images_status?refresh=true` ); const statusEmoji = statusRes.data.Status === 'outdated' ? '⚠️' : '✅'; - return { ...stack, updateStatus: statusEmoji, redeploying: redeployingStacks[stack.Id] || false }; - } catch { + return { + ...stack, + updateStatus: statusEmoji, + redeploying: redeployingStacks[stack.Id] || false + }; + } catch (err) { + console.error(`❌ Fehler beim Abrufen des Status für Stack ${stack.Id}:`, err.message); return { ...stack, updateStatus: '❌', redeploying: redeployingStacks[stack.Id] || false }; } }) ); stacksWithStatus.sort((a, b) => a.Name.localeCompare(b.Name)); + console.log(`✅ GET /api/stacks: Abruf erfolgreich, ${stacksWithStatus.length} Stacks geladen`); res.json(stacksWithStatus); } catch (err) { + console.error(`❌ Fehler beim Abrufen der Stacks:`, err.message); res.status(500).json({ error: err.message }); } }); app.put('/api/stacks/:id/redeploy', async (req, res) => { const { id } = req.params; + console.log(`🔄 PUT /api/stacks/${id}/redeploy: Redeploy gestartet`); + try { broadcastRedeployStatus(id, true); const stackRes = await axiosInstance.get(`/api/stacks/${id}`); const stack = stackRes.data; - // Prüfen, ob Stack zum konfigurierten Endpoint gehört if (stack.EndpointId !== ENDPOINT_ID) { throw new Error(`Stack gehört nicht zum Endpoint ${ENDPOINT_ID}`); } if (stack.Type === 1) { + console.log(`🔄 [Redeploy] Git Stack "${stack.Name}" (${id}) wird redeployed`); await axiosInstance.put(`/api/stacks/${id}/git/redeploy?endpointId=${stack.EndpointId}`); } else if (stack.Type === 2) { + console.log(`🔄 [Redeploy] Compose Stack "${stack.Name}" (${id}) wird redeployed`); const fileRes = await axiosInstance.get(`/api/stacks/${id}/file`); const stackFileContent = fileRes.data?.StackFileContent; if (!stackFileContent) throw new Error("Stack file konnte nicht geladen werden"); @@ -110,10 +117,13 @@ app.put('/api/stacks/:id/redeploy', async (req, res) => { const imageName = services[serviceName].image; if (!imageName) continue; try { + console.log(`🖼️ Pulling image "${imageName}" für Service "${serviceName}"`); await axiosInstance.post( `/api/endpoints/${stack.EndpointId}/docker/images/create?fromImage=${encodeURIComponent(imageName)}` ); - } catch {} + } catch (err) { + console.error(`❌ Fehler beim Pulling von Image "${imageName}":`, err.message); + } } await axiosInstance.put(`/api/stacks/${id}`, @@ -123,14 +133,15 @@ app.put('/api/stacks/:id/redeploy', async (req, res) => { } broadcastRedeployStatus(id, false); + console.log(`✅ PUT /api/stacks/${id}/redeploy: Redeploy erfolgreich abgeschlossen`); res.json({ success: true, message: 'Stack redeployed' }); } catch (err) { broadcastRedeployStatus(id, false); + console.error(`❌ Fehler beim Redeploy von Stack ${id}:`, err.message); res.status(500).json({ error: err.message }); } }); -// Server starten server.listen(PORT, '0.0.0.0', () => { - console.log(`Backend läuft auf Port ${PORT}`); + console.log(`🚀 Backend läuft auf Port ${PORT}`); }); diff --git a/backend/index.js.bak b/backend/index.js.bak new file mode 100644 index 0000000..ee67420 --- /dev/null +++ b/backend/index.js.bak @@ -0,0 +1,154 @@ +import express from 'express'; +import dotenv from 'dotenv'; +import https from 'https'; +import axios from 'axios'; +import http from 'http'; +import { Server } from 'socket.io'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +dotenv.config(); + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const app = express(); +app.use(express.json()); + +// Statische Dateien +app.use(express.static(path.join(__dirname, 'public'))); + +// SPA-Fallback +app.get('*', (req, res, next) => { + if (req.path.startsWith('/api')) return next(); + res.sendFile(path.join(__dirname, 'public', 'index.html')); +}); + +const PORT = 4001; +const ENDPOINT_ID = Number(process.env.PORTAINER_ENDPOINT_ID); + +// HTTPS Agent +const agent = new https.Agent({ rejectUnauthorized: false }); + +// Axios für Portainer +const axiosInstance = axios.create({ + httpsAgent: agent, + headers: { "X-API-Key": process.env.PORTAINER_API_KEY }, + baseURL: process.env.PORTAINER_URL, +}); + +const redeployingStacks = {}; + +// HTTP Server + Socket.IO +const server = http.createServer(app); +const io = new Server(server, { cors: { origin: "*" } }); + +io.on("connection", (socket) => { + console.log(`🔌 [Socket] Client verbunden: ${socket.id}`); +}); + +const broadcastRedeployStatus = (stackId, status) => { + redeployingStacks[stackId] = status; + io.emit("redeployStatus", { stackId, status }); + console.log(`🔄 [RedeployStatus] Stack ${stackId} ist jetzt ${status ? "im Redeploy" : "fertig"}`); +}; + +// --- API Endpoints --- + +// Stacks abrufen +app.get('/api/stacks', async (req, res) => { + console.log("ℹ️ [API] GET /api/stacks: Abruf gestartet"); + try { + const stacksRes = await axiosInstance.get('/api/stacks'); + const filteredStacks = stacksRes.data.filter(stack => stack.EndpointId === ENDPOINT_ID); + + const uniqueStacksMap = {}; + filteredStacks.forEach(stack => { + if (!uniqueStacksMap[stack.Name]) uniqueStacksMap[stack.Name] = stack; + }); + const uniqueStacks = Object.values(uniqueStacksMap); + + const stacksWithStatus = await Promise.all( + uniqueStacks.map(async (stack) => { + try { + const statusRes = await axiosInstance.get( + `/api/stacks/${stack.Id}/images_status?refresh=true` + ); + const statusEmoji = statusRes.data.Status === 'outdated' ? '⚠️' : '✅'; + return { + ...stack, + updateStatus: statusEmoji, + redeploying: redeployingStacks[stack.Id] || false + }; + } catch (err) { + console.error(`❌ [API] Fehler beim Abrufen Status Stack ${stack.Id}:`, err.message); + return { ...stack, updateStatus: '❌', redeploying: redeployingStacks[stack.Id] || false }; + } + }) + ); + + stacksWithStatus.sort((a, b) => a.Name.localeCompare(b.Name)); + console.log(`✅ [API] GET /api/stacks: Abruf erfolgreich, ${stacksWithStatus.length} Stacks geladen`); + res.json(stacksWithStatus); + } catch (err) { + console.error(`❌ [API] Fehler beim Abrufen der Stacks:`, err.message); + res.status(500).json({ error: err.message }); + } +}); + +// Stack redeployen +app.put('/api/stacks/:id/redeploy', async (req, res) => { + const { id } = req.params; + console.log(`🔄 [API] PUT /api/stacks/${id}/redeploy: Redeploy gestartet`); + + try { + broadcastRedeployStatus(id, true); + + const stackRes = await axiosInstance.get(`/api/stacks/${id}`); + const stack = stackRes.data; + + if (stack.EndpointId !== ENDPOINT_ID) throw new Error(`Stack gehört nicht zum Endpoint ${ENDPOINT_ID}`); + + if (stack.Type === 1) { + console.log(`🔄 [Redeploy] Git Stack "${stack.Name}" (${id}) wird redeployed`); + await axiosInstance.put(`/api/stacks/${id}/git/redeploy?endpointId=${stack.EndpointId}`); + } else if (stack.Type === 2) { + console.log(`🔄 [Redeploy] Compose Stack "${stack.Name}" (${id}) wird redeployed`); + const fileRes = await axiosInstance.get(`/api/stacks/${id}/file`); + const stackFileContent = fileRes.data?.StackFileContent; + if (!stackFileContent) throw new Error("Stack file konnte nicht geladen werden"); + + const services = fileRes.data?.Config?.services || {}; + for (const serviceName in services) { + const imageName = services[serviceName].image; + if (!imageName) continue; + try { + console.log(`🖼️ [Redeploy] Pulling image "${imageName}" für Service "${serviceName}"`); + await axiosInstance.post( + `/api/endpoints/${stack.EndpointId}/docker/images/create?fromImage=${encodeURIComponent(imageName)}` + ); + } catch (err) { + console.error(`❌ [Redeploy] Fehler beim Pulling von Image "${imageName}":`, err.message); + } + } + + await axiosInstance.put(`/api/stacks/${id}`, + { StackFileContent: stackFileContent, Prune: false, PullImage: true }, + { params: { endpointId: stack.EndpointId } } + ); + } + + broadcastRedeployStatus(id, false); + console.log(`✅ [API] PUT /api/stacks/${id}/redeploy: Redeploy erfolgreich abgeschlossen`); + res.json({ success: true, message: 'Stack redeployed' }); + } catch (err) { + broadcastRedeployStatus(id, false); + console.error(`❌ [API] Fehler beim Redeploy von Stack ${id}:`, err.message); + res.status(500).json({ error: err.message }); + } +}); + +// Server starten +server.listen(PORT, '0.0.0.0', () => { + console.log(`🚀 [Server] Backend läuft auf Port ${PORT}`); +}); diff --git a/frontend/src/Stacks.jsx b/frontend/src/Stacks.jsx index 9560ebd..cc6c353 100644 --- a/frontend/src/Stacks.jsx +++ b/frontend/src/Stacks.jsx @@ -6,37 +6,40 @@ export default function Stacks() { const [stacks, setStacks] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); - const [redeploying, setRedeploying] = useState({}); - // WebSocket initialisieren useEffect(() => { - const socket = io("/"); - console.log("Socket connected"); + const socket = io("/", { transports: ["websocket"] }); + console.log("🔌 Socket connected"); socket.on("redeployStatus", async ({ stackId, status }) => { - setRedeploying(prev => ({ ...prev, [stackId]: status })); + console.log(`🔄 Stack ${stackId} Redeploy Status: ${status ? "running" : "finished"}`); - // Wenn Redeploy beendet, Stack-Status neu laden if (!status) { + // Status nach Redeploy neu vom Server holen try { const res = await axios.get("/api/stacks"); - const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); - setStacks(sortedStacks); + setStacks(res.data.sort((a, b) => a.Name.localeCompare(b.Name))); } catch (err) { - console.error("Fehler beim Aktualisieren des Status nach Redeploy:", err); + console.error("Fehler beim Aktualisieren nach Redeploy:", err); } + } else { + // UI direkt auf redeploying setzen + setStacks(prev => + prev.map(stack => + stack.Id === stackId ? { ...stack, redeploying: true } : stack + ) + ); } }); return () => socket.disconnect(); }, []); - // Stacks initial laden const fetchStacks = async () => { + setLoading(true); try { const res = await axios.get("/api/stacks"); - const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); - setStacks(sortedStacks); + setStacks(res.data.map(stack => ({ ...stack, redeploying: stack.redeploying || false }))); } catch (err) { console.error("❌ Fehler beim Abrufen der Stacks:", err); setError("Fehler beim Laden der Stacks"); @@ -49,15 +52,19 @@ export default function Stacks() { fetchStacks(); }, []); - // Redeploy eines Stacks const handleRedeploy = async (stackId) => { - setRedeploying(prev => ({ ...prev, [stackId]: true })); + setStacks(prev => + prev.map(stack => stack.Id === stackId ? { ...stack, redeploying: true } : stack) + ); try { await axios.put(`/api/stacks/${stackId}/redeploy`); + // Socket.IO Event aktualisiert Status automatisch } catch (err) { console.error("❌ Fehler beim Redeploy:", err); - setRedeploying(prev => ({ ...prev, [stackId]: false })); + setStacks(prev => + prev.map(stack => stack.Id === stackId ? { ...stack, redeploying: false } : stack) + ); } }; @@ -67,8 +74,7 @@ export default function Stacks() { return (
{stacks.map(stack => { - const isRedeploying = redeploying[stack.Id] || false; - const isUpToDate = stack.updateStatus === "✅"; + const isRedeploying = stack.redeploying; return (
- {/* Status Indicator */}
-
+ />

{stack.Name}

ID: {stack.Id}

- {!isUpToDate && ( - - )} +
); })} diff --git a/frontend/src/Stacks.jsx.bak b/frontend/src/Stacks.jsx.bak new file mode 100644 index 0000000..e890c62 --- /dev/null +++ b/frontend/src/Stacks.jsx.bak @@ -0,0 +1,112 @@ +import React, { useEffect, useState } from "react"; +import axios from "axios"; +import { io } from "socket.io-client"; + +export default function Stacks() { + const [stacks, setStacks] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + + // Socket.IO initialisieren (Proxy leitet /socket.io an Backend) + useEffect(() => { + const socket = io("/", { transports: ["websocket"] }); + console.log("🔌 Socket connected"); + + socket.on("redeployStatus", ({ stackId, status }) => { + console.log(`🔄 Stack ${stackId} Redeploy Status: ${status ? "running" : "finished"}`); + setStacks(prevStacks => + prevStacks.map(stack => + stack.Id === stackId + ? { ...stack, redeploying: status, updateStatus: status ? stack.updateStatus : "✅" } + : stack + ) + ); + }); + + return () => socket.disconnect(); + }, []); + + // Stacks initial laden + const fetchStacks = async () => { + setLoading(true); + try { + const res = await axios.get("/api/stacks"); + setStacks( + res.data + .sort((a, b) => a.Name.localeCompare(b.Name)) + .map(stack => ({ ...stack, redeploying: stack.redeploying || false })) + ); + } catch (err) { + console.error("❌ Fehler beim Abrufen der Stacks:", err); + setError("Fehler beim Laden der Stacks"); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchStacks(); + }, []); + + // Redeploy starten + const handleRedeploy = async (stackId) => { + // Sofort UI auf redeploying setzen + setStacks(prev => + prev.map(stack => stack.Id === stackId ? { ...stack, redeploying: true } : stack) + ); + + try { + await axios.put(`/api/stacks/${stackId}/redeploy`); + // Backend sendet Socket-Event → UI aktualisiert automatisch + } catch (err) { + console.error("❌ Fehler beim Redeploy:", err); + setStacks(prev => + prev.map(stack => stack.Id === stackId ? { ...stack, redeploying: false } : stack) + ); + } + }; + + if (loading) return

Lade Stacks...

; + if (error) return

{error}

; + + return ( +
+ {stacks.map(stack => { + const isRedeploying = stack.redeploying; + + return ( +
+
+ {/* Status Indicator */} +
+
+

{stack.Name}

+

ID: {stack.Id}

+
+
+ + {/* Redeploy Button */} + +
+ ); + })} + {stacks.length === 0 &&

Keine Stacks gefunden.

} +
+ ); +} diff --git a/frontend/src/Stacks.jsx.bak_working b/frontend/src/Stacks.jsx.bak_working deleted file mode 100644 index ddd32be..0000000 --- a/frontend/src/Stacks.jsx.bak_working +++ /dev/null @@ -1,112 +0,0 @@ -import React, { useEffect, useState } from "react"; -import axios from "axios"; -import { io } from "socket.io-client"; - -export default function Stacks() { - const [stacks, setStacks] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(""); - const [redeploying, setRedeploying] = useState({}); // { stackId: true/false } - - // WebSocket initialisieren - useEffect(() => { - const socket = io("/"); - console.log("Socket connected"); - - socket.on("redeployStatus", async ({ stackId, status }) => { - setRedeploying(prev => ({ ...prev, [stackId]: status })); - - // Wenn Redeploy beendet, Stack-Status neu laden - if (!status) { - try { - const res = await axios.get("/api/stacks"); - const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); - setStacks(sortedStacks); - } catch (err) { - console.error("Fehler beim Aktualisieren des Status nach Redeploy:", err); - } - } - }); - - return () => socket.disconnect(); - }, []); - - // Stacks initial laden - const fetchStacks = async () => { - try { - const res = await axios.get("/api/stacks"); - const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); - setStacks(sortedStacks); - } catch (err) { - console.error("❌ Fehler beim Abrufen der Stacks:", err); - setError("Fehler beim Laden der Stacks"); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - fetchStacks(); - }, []); - - // Redeploy eines Stacks - const handleRedeploy = async (stackId) => { - // Button sofort auf Redeploying setzen - setRedeploying(prev => ({ ...prev, [stackId]: true })); - - try { - await axios.put(`/api/stacks/${stackId}/redeploy`); - } catch (err) { - console.error("❌ Fehler beim Redeploy:", err); - } - - // Stack-Status immer neu laden - try { - const res = await axios.get("/api/stacks"); - const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); - setStacks(sortedStacks); - } catch (err) { - console.error("Fehler beim Aktualisieren nach Redeploy:", err); - } finally { - // Erst nach Datenaktualisierung Button zurücksetzen - setRedeploying(prev => ({ ...prev, [stackId]: false })); - } - }; - - if (loading) return

Lade Stacks...

; - if (error) return

{error}

; - - return ( - - ); -} diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 131eeb1..d33ca44 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -1,18 +1,21 @@ -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], server: { - host: true, - port: 5173, + host: true, // Hört auf allen Netzwerk-Interfaces, kein localhost notwendig + port: 5173, // optional, Standardport für Vite proxy: { - "/api": { - target: "http://127.0.0.1:4001", // dein Backend - changeOrigin: true, + '/api': { + target: 'http://127.0.0.1:4001', // Backend lokal auf Port 4001 + changeOrigin: true }, - }, - allowedHosts: "all", - }, + '/socket.io': { + target: 'http://127.0.0.1:4001', // WebSocket-Verbindungen über Proxy weiterleiten + ws: true, + changeOrigin: true + } + } + } }); - diff --git a/scripts/docker-release.sh b/scripts/docker-release.sh new file mode 100755 index 0000000..d584e68 --- /dev/null +++ b/scripts/docker-release.sh @@ -0,0 +1,36 @@ +#!/bin/bash +set -e + +# --- Konfiguration --- +GHCR_USERNAME="mboehmlaender" +REPO_NAME="stackpulse" + +# --- Branch prüfen --- +BRANCH=$(git rev-parse --abbrev-ref HEAD) +if [[ "$BRANCH" != "master" ]]; then + echo "Fehler: Du musst auf 'master' sein, um ein Release zu machen." + exit 1 +fi + +# --- Versionsnummer abfragen --- +while true; do + read -p "Bitte Versionsnummer für das Docker-Image eingeben (z.B. v0.1): " VERSION_TAG + if [[ -n "$VERSION_TAG" ]]; then break; else echo "Versionsnummer darf nicht leer sein."; fi +done + +# --- Docker: Login --- +if [ -z "$CR_PAT" ]; then + echo "CR_PAT (GitHub Token) nicht gesetzt! Bitte export CR_PAT=" + exit 1 +fi +echo $CR_PAT | docker login ghcr.io -u $GHCR_USERNAME --password-stdin + +# --- Docker: Build & Tag --- +docker build -t ghcr.io/$GHCR_USERNAME/$REPO_NAME:$VERSION_TAG . +docker tag ghcr.io/$GHCR_USERNAME/$REPO_NAME:$VERSION_TAG ghcr.io/$GHCR_USERNAME/$REPO_NAME:latest + +# --- Docker: Push --- +docker push ghcr.io/$GHCR_USERNAME/$REPO_NAME:$VERSION_TAG +docker push ghcr.io/$GHCR_USERNAME/$REPO_NAME:latest + +echo "Docker-Release $VERSION_TAG erfolgreich gebaut und zu GHCR gepusht!" diff --git a/scripts/switch-branch.sh b/scripts/switch-branch.sh index c025b64..0249a19 100755 --- a/scripts/switch-branch.sh +++ b/scripts/switch-branch.sh @@ -1,5 +1,19 @@ #!/bin/bash +# =============================== +# Dateien sichern, die nicht gepusht werden +# =============================== +UNVERSIONED_FILES=("scripts/docker-release.sh") +for f in "${UNVERSIONED_FILES[@]}"; do + if [[ -f $f ]]; then + mkdir -p /tmp/git_safe_backup + cp "$f" "/tmp/git_safe_backup/$(basename "$f")" + fi +done + +# =============================== +# Alle Branches sammeln +# =============================== # Alle lokalen Branches holen, führende Sternchen und Leerzeichen entfernen LOCAL_BRANCHES=$(git branch | sed 's/* //' | sed 's/^[[:space:]]*//') @@ -58,4 +72,16 @@ fi git reset --hard origin/$SELECTED_BRANCH git clean -fd +# =============================== +# Gesicherte unversionierte Dateien zurückkopieren +# =============================== +for f in "${UNVERSIONED_FILES[@]}"; do + if [[ -f "/tmp/git_safe_backup/$(basename "$f")" ]]; then + mkdir -p "$(dirname "$f")" + mv "/tmp/git_safe_backup/$(basename "$f")" "$f" + fi +done +rm -rf /tmp/git_safe_backup + echo "Branch '$SELECTED_BRANCH' ist nun aktiv. Arbeitsverzeichnis entspricht exakt dem Remote-Stand." +echo "Unversionierte Dateien wurden wiederhergestellt."