View file File name : app.js Content :require("dotenv").config(); const express = require("express"); const { exec } = require("child_process"); const util = require("util"); const execAsync = util.promisify(exec); const path = require("path"); const cors = require("cors"); const bodyParser = require("body-parser"); const fs = require("fs"); const { v4: uuidv4 } = require("uuid"); const { createClient } = require("@supabase/supabase-js"); // إعداد Supabase const supabase = createClient( 'https://qlvqxmfpjfkruxxirrau.supabase.co', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InFsdnF4bWZwamZrcnV4eGlycmF1Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDM5NzY2NjksImV4cCI6MjA1OTU1MjY2OX0.-EzJ3r8jHLBnIxFm4dVdvy01qGluBfhKGw3tSDqhysU' ); const app = express(); const PORT = process.env.PORT || 30000; const tempDirsQueue = []; let cleanupInProgress = false; app.use(cors({ origin: "*", // أو "http://localhost:8080" فقط إذا أردت التقييد methods: ["GET", "POST", "OPTIONS"], allowedHeaders: ["Content-Type", "Authorization"] })); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); app.use(express.static(path.join(__dirname, "public"))); const tempBasePath = path.join(__dirname, "temp"); if (!fs.existsSync(tempBasePath)) { fs.mkdirSync(tempBasePath, { recursive: true }); } app.post("/api/update-commit", async (req, res) => { const { repoUrl, token, commitHash, newMessage, branch } = req.body; if (!repoUrl || !token || !commitHash || !newMessage || !branch) { return res.status(400).json({ success: false, message: "جميع الحقول مطلوبة: repoUrl, token, commitHash, newMessage, branch", }); } async function saveCommitMessageHistory({ hash, parentHash, message, }) { const shortHash = hash.slice(0, 7); const { error } = await supabase.from("commit_message").insert({ hash, short_hash: shortHash, parent_commit_hash: parentHash || null, message, }); if (error) { console.error("❌ فشل حفظ سجل الرسالة في Supabase:", error.message); } else { console.log("✅ تم حفظ سجل رسالة الكوميت في Supabase."); } } const uniqueDirName = `repo-${uuidv4()}`; const tempDir = path.join(tempBasePath, uniqueDirName); tempDirsQueue.push(tempDir); const fullUrl = repoUrl.replace("https://github.com", `https://${token}@github.com`); try { console.log(`📦 بدء العملية: استنساخ المستودع...`); await execAsync(`git clone --branch "${branch}" "${fullUrl}" "${tempDir}"`); console.log(`✅ تم استنساخ المستودع.`); await execAsync(`git config user.email "bot@example.com"`, { cwd: tempDir }); await execAsync(`git config user.name "Commit Bot"`, { cwd: tempDir }); await execAsync(`git cat-file -e ${commitHash}`, { cwd: tempDir }); console.log(`✅ تم التحقق من وجود الـ commit.`); let isInitialCommit = false; try { await execAsync(`git rev-parse "${commitHash}^"`, { cwd: tempDir }); } catch { isInitialCommit = true; } let parentCommitHash = null; const tempBranch = `temp-branch-${uuidv4().substring(0, 8)}`; if (isInitialCommit) { await execAsync(`git checkout --orphan ${tempBranch}`, { cwd: tempDir }); await execAsync(`git reset --hard`, { cwd: tempDir }); console.log(`🌿 تم إنشاء فرع يتيم وتنظيف الملفات: ${tempBranch}`); } else { const { stdout: parentOutput } = await execAsync(`git rev-parse "${commitHash}^"`, { cwd: tempDir }); parentCommitHash = parentOutput.trim(); await execAsync(`git checkout -b ${tempBranch} ${parentCommitHash}`, { cwd: tempDir }); } await execAsync(`git cherry-pick ${commitHash}`, { cwd: tempDir }); console.log(`🍒 تم cherry-pick للـ commit.`); const { stdout: originalMessageOutput } = await execAsync(`git log -n 1 --pretty=format:%B HEAD`, { cwd: tempDir }); const originalMessage = originalMessageOutput.trim(); const finalMessage = `${newMessage}\n\n${originalMessage}`; const messageFilePath = path.join(tempDir, "commit_message.txt"); await fs.promises.writeFile(messageFilePath, finalMessage, "utf8"); await execAsync(`git commit --amend -F "${messageFilePath}"`, { cwd: tempDir }); console.log(`✏️ تم تعديل الرسالة مع الحفاظ على الكومنتات.`); const { stdout: newCommitOutput } = await execAsync(`git rev-parse HEAD`, { cwd: tempDir }); const newCommitHash = newCommitOutput.trim(); // ⭐ حفظ التعليقات من الكوميت القديم ثم نسخها على الجديد const [owner, repo] = repoUrl .replace("https://github.com/", "") .replace(/\.git$/, "") .split("/"); const rebaseCommand = `git rebase --onto ${tempBranch} ${commitHash} ${branch}`; await execAsync(rebaseCommand, { cwd: tempDir }); // console.log({ // hash: newCommitHash, // oldHash: commitHash, // parent_commit_hash: parentCommitHash, // message: newMessage, // }); const { error: dbError } = await supabase.from("commit_message").insert([ { hash: newCommitHash, parent_commit_hash: parentCommitHash, message: newMessage, }, ]); if (dbError) { console.warn("⚠️ فشل في حفظ الرسالة في Supabase:", dbError.message); } else { console.log("💾 تم حفظ الرسالة في Supabase."); } console.log(`🚀 جاري دفع التغييرات...`); await execAsync(`git push origin ${branch} --force`, { cwd: tempDir }); console.log(`✅ تم دفع التغييرات بنجاح.`); const commentsRes = await fetch( `https://api.github.com/repos/${owner}/${repo}/commits/${commitHash}/comments`, { headers: { Authorization: `Bearer ${token}`, "User-Agent": "commit-updater", }, } ); if (!commentsRes.ok) { console.warn("⚠️ فشل في جلب التعليقات:", await commentsRes.text()); } else { const comments = await commentsRes.json(); for (const comment of comments) { const copiedBody = `${comment.body}`; const postCommentRes = await fetch( `https://api.github.com/repos/${owner}/${repo}/commits/${newCommitHash}/comments`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "User-Agent": "commit-updater", "Content-Type": "application/json", }, body: JSON.stringify({ body: copiedBody }), } ); if (!postCommentRes.ok) { console.warn("⚠️ فشل في إضافة تعليق جديد:", await postCommentRes.text()); } } console.log(`💬 تم نسخ ${comments.length} تعليق إلى الكوميت الجديد.`); } await saveCommitMessageHistory({ hash: newCommitHash, parentHash: parentCommitHash, message: newMessage, }); await fs.promises.unlink(messageFilePath).catch(() => { console.warn("⚠️ لم يتمكن من حذف ملف الرسالة المؤقت."); }); sendResponse(res, true, "تم تحديث رسالة الـ commit بنجاح", null, null, ""); } catch (error) { console.error(`❌ حدث خطأ:`, error); sendResponse( res, false, "حدث خطأ أثناء تعديل رسالة الـ commit", error.message, error.stderr ); } }); // update commit message for gitlab app.post("/api/update-commit-gitlab", async (req, res) => { const { repoUrl, token, commitHash, newMessage, branch } = req.body; if (!repoUrl || !token || !commitHash || !newMessage || !branch) { return res.status(400).json({ success: false, message: "جميع الحقول مطلوبة: repoUrl, token, commitHash, newMessage, branch", }); } async function saveCommitMessageHistory({ hash, parentHash, message }) { const shortHash = hash.slice(0, 7); const { error } = await supabase.from("commit_message").insert({ hash, short_hash: shortHash, parent_commit_hash: parentHash || null, message, }); if (error) { console.error("❌ فشل حفظ سجل الرسالة في Supabase:", error.message); } else { console.log("✅ تم حفظ سجل رسالة الكوميت في Supabase."); } } const uniqueDirName = `repo-${uuidv4()}`; const tempDir = path.join(tempBasePath, uniqueDirName); tempDirsQueue.push(tempDir); const fullUrl = repoUrl.replace("https://gitlab.com", `https://oauth2:${token}@gitlab.com`); try { console.log(`📦 بدء العملية: استنساخ المستودع...`); await execAsync(`git clone --branch "${branch}" "${fullUrl}" "${tempDir}"`); console.log(`✅ تم استنساخ المستودع.`); await execAsync(`git config user.email "bot@example.com"`, { cwd: tempDir }); await execAsync(`git config user.name "Commit Bot"`, { cwd: tempDir }); await execAsync(`git cat-file -e ${commitHash}`, { cwd: tempDir }); let isInitialCommit = false; try { await execAsync(`git rev-parse "${commitHash}^"`, { cwd: tempDir }); } catch { isInitialCommit = true; } let parentCommitHash = null; const tempBranch = `temp-branch-${uuidv4().substring(0, 8)}`; if (isInitialCommit) { await execAsync(`git checkout --orphan ${tempBranch}`, { cwd: tempDir }); await execAsync(`git reset --hard`, { cwd: tempDir }); } else { const { stdout: parentOutput } = await execAsync(`git rev-parse "${commitHash}^"`, { cwd: tempDir }); parentCommitHash = parentOutput.trim(); await execAsync(`git checkout -b ${tempBranch} ${parentCommitHash}`, { cwd: tempDir }); } await execAsync(`git cherry-pick ${commitHash}`, { cwd: tempDir }); const { stdout: originalMessageOutput } = await execAsync(`git log -n 1 --pretty=format:%B HEAD`, { cwd: tempDir }); const originalMessage = originalMessageOutput.trim(); const finalMessage = `${newMessage}\n\n${originalMessage}`; const messageFilePath = path.join(tempDir, "commit_message.txt"); await fs.promises.writeFile(messageFilePath, finalMessage, "utf8"); await execAsync(`git commit --amend -F "${messageFilePath}"`, { cwd: tempDir }); const { stdout: newCommitOutput } = await execAsync(`git rev-parse HEAD`, { cwd: tempDir }); const newCommitHash = newCommitOutput.trim(); const projectPath = repoUrl .replace("https://gitlab.com/", "") .replace(/\.git$/, "") .trim(); const encodedProjectPath = encodeURIComponent(projectPath); // 🟡 استدعاء API للحصول على التعليقات على الكوميت من GitLab const commentsRes = await fetch( `https://gitlab.com/api/v4/projects/${encodedProjectPath}/repository/commits/${commitHash}/comments`, { headers: { "PRIVATE-TOKEN": token, }, } ); if (!commentsRes.ok) { console.warn("⚠️ فشل في جلب تعليقات GitLab:", await commentsRes.text()); } else { const comments = await commentsRes.json(); for (const comment of comments) { const copiedBody = comment.note; const postCommentRes = await fetch( `https://gitlab.com/api/v4/projects/${encodedProjectPath}/repository/commits/${newCommitHash}/comments`, { method: "POST", headers: { "PRIVATE-TOKEN": token, "Content-Type": "application/json", }, body: JSON.stringify({ note: copiedBody }), } ); if (!postCommentRes.ok) { console.warn("⚠️ فشل في إضافة تعليق جديد:", await postCommentRes.text()); } } console.log(`💬 تم نسخ ${comments.length} تعليق إلى الكوميت الجديد.`); } const rebaseCommand = `git rebase --onto ${tempBranch} ${commitHash} ${branch}`; await execAsync(rebaseCommand, { cwd: tempDir }); const { error: dbError } = await supabase.from("commit_message").insert([ { hash: newCommitHash, parent_commit_hash: parentCommitHash, message: newMessage, }, ]); if (dbError) { console.warn("⚠️ فشل في حفظ الرسالة في Supabase:", dbError.message); } await saveCommitMessageHistory({ hash: newCommitHash, parentHash: parentCommitHash, message: newMessage, }); await fs.promises.unlink(messageFilePath).catch(() => { console.warn("⚠️ لم يتمكن من حذف ملف الرسالة المؤقت."); }); console.log(`🚀 جاري دفع التغييرات...`); await execAsync(`git push origin ${branch} --force`, { cwd: tempDir }); console.log(`✅ تم دفع التغييرات بنجاح.`); sendResponse(res, true, "تم تحديث رسالة الـ commit بنجاح", null, null, ""); } catch (error) { console.error(`❌ حدث خطأ:`, error); const errorMessage = error.message || ""; const isGitLabProtectedBranchError = errorMessage.includes("GitLab: You are not allowed to force push") || error.stderr?.includes("GitLab: You are not allowed to force push"); if (isGitLabProtectedBranchError) { return sendResponse(res, false, "You can't edit the commit message because the branch is protected in GitLab. Unprotect the branch or allow force push from your GitLab settings.", error.message, error.stderr); } sendResponse( res, false, "حدث خطأ أثناء تعديل رسالة الـ commit", error.message, error.stderr ); } }); app.get("/api/commit-message/:commitHash", async (req, res) => { const { commitHash } = req.params; const { repoUrl, token } = req.query; if (!repoUrl || !token || !commitHash) { return res.status(400).json({ success: false, message: "يرجى توفير repoUrl و token و commitHash" }); } function extractGitHubRepoPath(repoUrl) { const path = new URL(repoUrl).pathname.replace(/^\/|\.git$/g, ''); return path.split('/'); } const [owner, repo] = extractGitHubRepoPath(repoUrl); const apiUrl = `https://api.github.com/repos/${owner}/${repo}/commits/${commitHash}`; try { const response = await fetch(apiUrl, { headers: { Authorization: `token ${token}`, Accept: 'application/vnd.github.v3+json', } }); if (!response.ok) { const error = await response.text(); return res.status(response.status).json({ success: false, message: "فشل في جلب الكوميت من GitHub", error }); } const data = await response.json(); res.json({ success: true, commitHash: data.sha, message: data.commit.message, author: data.commit.author?.name, date: data.commit.author?.date, }); } catch (err) { res.status(500).json({ success: false, message: "حدث خطأ أثناء التواصل مع GitHub", error: err.message, }); } }); // get history of commit messages in gitlab app.get("/api/gitlab/commit-message/:commitHash", async (req, res) => { const { commitHash } = req.params; const { repoUrl, token } = req.query; if (!repoUrl || !token || !commitHash) { return res.status(400).json({ success: false, message: "يرجى توفير repoUrl و token و commitHash" }); } function extractGitLabRepoPath(repoUrl) { // استخراج المسار من URL مثل https://gitlab.com/username/project.git -> username/project return new URL(repoUrl).pathname.replace(/^\/|\.git$/g, ''); } const projectPath = extractGitLabRepoPath(repoUrl); const encodedProjectPath = encodeURIComponent(projectPath); const apiUrl = `https://gitlab.com/api/v4/projects/${encodedProjectPath}/repository/commits/${commitHash}`; try { const response = await fetch(apiUrl, { headers: { 'PRIVATE-TOKEN': token, } }); if (!response.ok) { const error = await response.text(); return res.status(response.status).json({ success: false, message: "فشل في جلب الكوميت من GitLab", error }); } const data = await response.json(); res.json({ success: true, commitHash: data.id, message: data.message, author: data.author_name, date: data.created_at, }); } catch (err) { res.status(500).json({ success: false, message: "حدث خطأ أثناء التواصل مع GitLab", error: err.message, }); } }); // دالة إرسال الرد function sendResponse(res, success, message, error, stderr, output) { scheduleCleanup(); if (success) { return res.json({ success: true, message, output }); } else { return res.status(500).json({ success: false, message, error, stderr }); } } // تنظيف مؤقت function scheduleCleanup() { if (cleanupInProgress || tempDirsQueue.length === 0) return; cleanupInProgress = true; setTimeout(() => { const currentDir = tempDirsQueue.shift(); if (currentDir && fs.existsSync(currentDir)) { console.log(`تنظيف المجلد المؤقت: ${currentDir}`); try { if (process.platform === "win32") { exec(`taskkill /F /IM git.exe /T`, () => { setTimeout(() => { try { fs.rmSync(currentDir, { recursive: true, force: true }); } catch (e) { console.warn(`تعذر حذف المجلد: ${e.message}`); } finally { cleanupInProgress = false; scheduleCleanup(); } }, 500); }); } else { fs.rmSync(currentDir, { recursive: true, force: true }); cleanupInProgress = false; scheduleCleanup(); } } catch (e) { console.error(`فشل التنظيف: ${e.message}`); cleanupInProgress = false; scheduleCleanup(); } } else { cleanupInProgress = false; scheduleCleanup(); } }, 2000); } // تنظيف دوري setInterval(() => { try { if (fs.existsSync(tempBasePath)) { fs.readdirSync(tempBasePath).forEach(dir => { const dirPath = path.join(tempBasePath, dir); const stats = fs.statSync(dirPath); if (Date.now() - stats.mtimeMs > 3600000) { try { fs.rmSync(dirPath, { recursive: true, force: true }); console.log(`تم حذف مجلد قديم: ${dirPath}`); } catch (e) { console.warn(`تعذر حذف مجلد قديم: ${e.message}`); } } }); } } catch (e) { console.error(`خطأ في التنظيف الدوري: ${e.message}`); } }, 1800000); // الواجهة الرئيسية app.get("/", (req, res) => { res.sendFile(path.join(__dirname, "public", "index.html")); }); process.on("exit", () => { console.log("تنظيف المجلدات المؤقتة قبل الخروج..."); if (fs.existsSync(tempBasePath)) { try { fs.rmSync(tempBasePath, { recursive: true, force: true }); } catch (e) { console.error(`تعذر تنظيف المجلدات عند الخروج: ${e.message}`); } } }); ["SIGINT", "SIGTERM", "SIGQUIT"].forEach(signal => { process.on(signal, () => { console.log(`تم استلام إشارة ${signal}، جاري إغلاق التطبيق...`); process.exit(0); }); }); app.listen(PORT,"0.0.0.0", () => { console.log(`الخادم يعمل على المنفذ ${PORT}`); });