const util = require('util'); const exec = util.promisify(require('child_process').exec); const path = require('path'); const { promises: fs } = require("fs"); const axios = require('axios'); const ROOT_PATH = '/app'; const MAX_BUFFER = 1024 * 1024 * 50; const GITEA_DOMAIN = 'gitea.flatlogic.app'; const USERNAME = 'admin'; const API_TOKEN = 'f22e83489657f49c320d081fc934e5c9daacfa08'; class VCS { // Main method – controller of the repository initialization process static async initRepo(projectId = 'test') { try { // 1. Ensure the remote repository exists (create if needed) const remoteUrl = await this.setupRemote(projectId); // 2. Set up the local repository (initialization, fetching and reset) await this.setupLocalRepo(remoteUrl); console.log(`[DEBUG] Repository "${projectId}" is ready (remote code applied).`); return { message: `Repository ${projectId} is ready (remote code applied).` }; } catch (error) { throw new Error(`Error during repo initialization: ${error.message}`); } } // Checks for the existence of the remote repo and creates it if it doesn't exist static async setupRemote(projectId) { console.log(`[DEBUG] Checking remote repository "${projectId}"...`); let repoData = await this.checkRepoExists(projectId); if (!repoData) { console.log(`[DEBUG] Remote repository "${projectId}" does not exist. Creating...`); repoData = await this.createRemoteRepo(projectId); console.log(`[DEBUG] Remote repository created: ${JSON.stringify(repoData)}`); } else { console.log(`[DEBUG] Remote repository "${projectId}" already exists.`); } // Return the URL with token authentication return `https://${USERNAME}:${API_TOKEN}@${GITEA_DOMAIN}/${USERNAME}/${projectId}.git`; } // Sets up the local repository: either fetches/reset if .git exists, // initializes git in a non-empty directory, or clones the repository if empty. static async setupLocalRepo(remoteUrl) { const gitDir = path.join(ROOT_PATH, '.git'); const localRepoExists = await this.exists(gitDir); if (localRepoExists) { await this.fetchAndResetRepo(); } else { const files = await fs.readdir(ROOT_PATH); if (files.length > 0) { await this.initializeGitRepo(remoteUrl); } else { console.log('[DEBUG] Local directory is empty. Cloning remote repository...'); await exec(`git clone ${remoteUrl} .`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); } } } // Check if a file/directory exists static async exists(pathToCheck) { try { await fs.access(pathToCheck); return true; } catch { return false; } } // If the local repository exists, fetches remote data and resets the repository state static async fetchAndResetRepo() { console.log('[DEBUG] Local repository exists. Fetching remote...'); await exec(`git fetch origin`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); const branchReset = await this.tryResetToBranch('fl-dev'); if (!branchReset) { // If 'fl-dev' branch is not found, try 'master' const masterReset = await this.tryResetToBranch('master'); if (masterReset) { // Create 'fl-dev' branch and push it to remote console.log('[DEBUG] Creating and switching to branch "fl-dev"...'); await exec(`git branch fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); await exec(`git checkout fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); console.log('[DEBUG] Pushing fl-dev branch to remote...'); await exec(`git push -u origin fl-dev --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); } else { // If neither remote master nor fl-dev exist – make an initial commit console.log('[DEBUG] Neither "origin/fl-dev" nor "origin/master" exist. Creating initial commit...'); await this.commitInitialChanges(); } } } // Tries to check out and reset to the specified branch static async tryResetToBranch(branchName) { try { console.log(`[DEBUG] Checking for remote branch "origin/${branchName}"...`); await exec(`git rev-parse --verify origin/${branchName}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); console.log(`[DEBUG] Remote branch "origin/${branchName}" found. Resetting local repository to "origin/${branchName}"...`); await exec(`git reset --hard origin/${branchName}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); await exec(`git checkout ${branchName === 'fl-dev' ? 'fl-dev' : branchName}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); return true; } catch (e) { console.log(`[DEBUG] Remote branch "origin/${branchName}" does NOT exist.`); return false; } } // If remote branch doesn't exist, make the initial commit and set up branches static async commitInitialChanges() { console.log('[DEBUG] Adding all files for initial commit...'); await exec(`git add .`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); const { stdout: status } = await exec(`git status --porcelain`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); if (status.trim()) { await exec(`git commit -m "Initial version"`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); await exec(`git push -u origin master --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); console.log('[DEBUG] Creating and switching to branch "fl-dev"...'); await exec(`git branch fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); await exec(`git checkout fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); console.log('[DEBUG] Making fl-dev branch identical to master...'); await exec(`git reset --hard origin/master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); console.log('[DEBUG] Pushing fl-dev branch to remote...'); await exec(`git push -u origin fl-dev --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); } else { console.log('[DEBUG] No local changes to commit.'); } } // If the local directory is not empty but .git doesn't exist, initialize git, // add .gitignore, configure the user, and add the remote origin. static async initializeGitRepo(remoteUrl) { console.log('[DEBUG] Local directory is not empty. Initializing git...'); const gitignorePath = path.join(ROOT_PATH, '.gitignore'); const ignoreContent = `node_modules/\n*/node_modules/\n*/build/\n`; await fs.writeFile(gitignorePath, ignoreContent, 'utf8'); await exec(`git init`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); console.log('[DEBUG] Configuring git user...'); await exec(`git config user.email "support@flatlogic.com"`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); await exec(`git config user.name "Flatlogic Bot"`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); console.log(`[DEBUG] Adding remote ${remoteUrl}...`); await exec(`git remote add origin ${remoteUrl}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); console.log('[DEBUG] Fetching remote...'); await exec(`git fetch origin`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); try { console.log('[DEBUG] Checking for remote branch "origin/fl-dev"...'); await exec(`git rev-parse --verify origin/fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); console.log('[DEBUG] Remote branch "origin/fl-dev" exists. Resetting local repository to origin/fl-dev...'); await exec(`git reset --hard origin/fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); console.log('[DEBUG] Switching to branch "fl-dev"...'); await exec(`git checkout -B fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); } catch (e) { console.log('[DEBUG] Remote branch "origin/fl-dev" does NOT exist. Creating initial commit...'); await this.commitInitialChanges(); } } // Method to check if the repository exists on remote server static async checkRepoExists(repoName) { const url = `https://${GITEA_DOMAIN}/api/v1/repos/${USERNAME}/${repoName}`; try { const response = await axios.get(url, { headers: { Authorization: `token ${API_TOKEN}` } }); return response.data; } catch (err) { if (err.response && err.response.status === 404) { return null; } throw new Error('Error checking repository existence: ' + err.message); } } // Method to create a remote repository via API static async createRemoteRepo(repoName) { const createUrl = `https://${GITEA_DOMAIN}/api/v1/user/repos`; console.log("[DEBUG] createUrl", createUrl); try { const response = await axios.post(createUrl, { name: repoName, description: `Repository for project ${repoName}`, private: false }, { headers: { Authorization: `token ${API_TOKEN}` } }); return response.data; } catch (err) { throw new Error('Error creating repository via API: ' + err.message); } } static async commitChanges(message = "", files = '.') { // Ensure that we are on branch 'fl-dev' before making any commits await this._ensureDevBranch(); try { await exec(`git add ${files}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); const { stdout: status } = await exec('git status --porcelain', { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); if (!status.trim()) { return { message: "No changes to commit" }; } const now = new Date(); const commitMessage = message || `Auto commit: ${now.toISOString()}`; console.log('commitMessage:', commitMessage); await exec(`git commit -m "${commitMessage}"`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); await this._pushChanges(); console.log('Pushed'); return { message: "Changes committed" }; } catch (error) { throw new Error(`Error during commit: ${error.message}`); } } static async getLog() { try { const { stdout } = await exec('git log fl-dev --oneline', { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); const lines = stdout.split(/\r?\n/).filter(line => line.trim() !== ''); const result = {}; lines.forEach((line) => { const firstSpaceIndex = line.indexOf(' '); if (firstSpaceIndex > 0) { const hash = line.substring(0, firstSpaceIndex); const message = line.substring(firstSpaceIndex + 1).trim(); result[hash] = message; } }); return result; } catch (error) { throw new Error(`Error during get log: ${error.message}`); } } static async checkout(ref) { try { await exec(`git checkout ${ref}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); return { message: `Checked out to ${ref}` }; } catch (error) { throw new Error(`Error during checkout: ${error.message}`); } } static async revert(commitHash) { try { await exec(`git reset --hard`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); // Rollback to the specified commit hash await exec( `git revert --no-edit ${commitHash}..HEAD --no-commit`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER } ); // Commit the changes await exec( `git commit -m "Revert to version ${commitHash}"`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER } ); await this._pushChanges(); return { message: `Reverted to commit ${commitHash}` }; } catch (error) { console.error("Error during range revert:", error.message); if (error.stdout) { console.error("Revert stdout:", error.stdout); } if (error.stderr) { console.error("Revert stderr:", error.stderr); } throw new Error(`Error during range revert: ${error.message}`); } } static async mergeDevIntoMaster() { try { // Switch to branch 'master' console.log('Switching to branch "master"...'); await exec(`git checkout master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); // Merge branch 'fl-dev' into 'master' with a forced merge. // Parameter -X theirs is used to resolve conflicts by keeping the changes from the branch being merged in case of conflicts. console.log('Merging branch "fl-dev" into "master" (force merge with -X theirs)...'); await exec( `git merge fl-dev --no-ff -X theirs -m "Forced merge: merge fl-dev into master"`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER } ); // Push the merged 'master' branch to remote console.log('Pushing merged master branch to remote...'); const { stdout, stderr } = await exec(`git push origin master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); if (stdout) { console.log("Git push stdout:", stdout); } if (stderr) { console.error("Git push stderr:", stderr); } return { message: "Branch fl-dev merged into master and pushed to remote" }; } catch (error) { console.error("Error during mergeDevIntoMaster:", error.message); if (error.stdout) { console.error("Merge stdout:", error.stdout); } if (error.stderr) { console.error("Merge stderr:", error.stderr); } throw error; } } static async resetDevBranch() { try { console.log('[DEBUG] Switching to branch "master"...'); await exec(`git checkout master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); console.log('[DEBUG] Resetting branch "fl-dev" to be identical to "master"...'); // Command checkout -B fl-dev master creates branch 'fl-dev' from 'master' and switches to it await exec(`git checkout -B fl-dev master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); console.log('[DEBUG] Pushing updated branch "fl-dev" to remote (force push)...'); await exec(`git push -u origin fl-dev --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); return { message: 'fl-dev branch successfully reset to master.' }; } catch (error) { console.error("Error during resetting fl-dev branch:", error.message); if (error.stdout) { console.error("Reset stdout:", error.stdout); } if (error.stderr) { console.error("Reset stderr:", error.stderr); } throw new Error(`Error during resetting fl-dev branch: ${error.message}`); } } static async _pushChanges() { try { const { stdout, stderr } = await exec(`git push origin fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); if (stdout) { console.log("Git push stdout:", stdout); } if (stderr) { console.error("Git push stderr:", stderr); } return { message: "Changes pushed to remote repository (fl-dev branch)" }; } catch (error) { console.error("Git push error:", error.message); if (error.stdout) { console.error("Git push stdout:", error.stdout); } if (error.stderr) { console.error("Git push stderr:", error.stderr); } } } static async _ensureDevBranch() { try { // Check if branch 'fl-dev' exists const { stdout: branchList } = await exec(`git branch --list fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER, }); if (!branchList || branchList.trim() === '') { console.log("Branch 'fl-dev' not found. Creating branch 'fl-dev'."); await exec(`git checkout -b fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); } else { // Determine current branch const { stdout: currentBranchStdout } = await exec(`git rev-parse --abbrev-ref HEAD`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER, }); const currentBranch = currentBranchStdout.trim(); if (currentBranch !== 'fl-dev') { console.log(`Switching from branch '${currentBranch}' to 'fl-dev'.`); await exec(`git checkout fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); } else { console.log("Already on branch 'fl-dev'."); } } } catch (error) { console.error("Error ensuring branch 'fl-dev':", error.message); throw error; } } } module.exports = VCS;