2025-03-12 14:32:47 +00:00

392 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;