27639/src/services/vcs.js
2025-03-17 20:18:23 +05:00

1003 lines
48 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 config = require('../config.js');
const ROOT_PATH = './';
const MAX_BUFFER = 1024 * 1024 * 50;
const GITEA_DOMAIN = config.gitea_domain;
const USERNAME = config.gitea_username;
const API_TOKEN = config.gitea_api_token;
const GITHUB_REPO_URL = config.github_repo_url;
const GITHUB_TOKEN = config.github_token;
class VCS {
static isInitRepoRunning = false;
// Main method controller of the repository initialization process
static async initRepo(projectId = 'test') {
if (VCS.isInitRepoRunning) {
console.warn('[WARNING] initRepo is already running. Skipping.');
return;
}
VCS.isInitRepoRunning = true;
try {
console.log(`[DEBUG] Starting repository initialization for project "${projectId}"...`);
if (GITHUB_REPO_URL) {
console.log(`[DEBUG] GitHub repository URL provided: ${GITHUB_REPO_URL}`);
console.log(`[DEBUG] Setting up local GitHub repository...`);
await this.setupLocalGitHubRepo();
console.log(`[DEBUG] GitHub repository setup completed.`);
} else {
console.log(`[DEBUG] No GitHub repository URL provided. Skipping GitHub setup.`);
}
console.log(`[DEBUG] Setting up Gitea remote repository for project "${projectId}"...`);
const giteaRemoteUrl = await this.setupGiteaRemote(projectId);
console.log(`[DEBUG] Gitea remote URL: ${giteaRemoteUrl.replace(/\/\/.*?@/, '//***@')}`); // Скрываем токен в логах
if (!GITHUB_REPO_URL) {
console.log(`[DEBUG] Setting up local repository with Gitea remote...`);
await this.setupLocalRepo(giteaRemoteUrl);
console.log(`[DEBUG] Local repository setup with Gitea remote completed.`);
} else {
console.log(`[DEBUG] Adding Gitea as additional remote to existing GitHub repository...`);
await this._addGiteaRemote(giteaRemoteUrl);
console.log(`[DEBUG] Gitea remote added to GitHub repository.`);
}
console.log(`[DEBUG] Repository initialization for project "${projectId}" completed successfully.`);
console.log(`[DEBUG] Repository configuration: GitHub: ${GITHUB_REPO_URL ? 'Yes' : 'No'}, Gitea: Yes`);
return { message: `Repository ${projectId} is ready.` };
} catch (error) {
console.error(`[ERROR] Repository initialization for project "${projectId}" failed: ${error.message}`);
throw new Error(`Error during repo initialization: ${error.message}`);
} finally {
VCS.isInitRepoRunning = false;
console.log(`[DEBUG] Repository initialization process for "${projectId}" finished.`);
}
}
// Checks for the existence of the remote repo and creates it if it doesn't exist
static async setupGiteaRemote(projectId) {
console.log(`[DEBUG] Checking Gitea remote repository "${projectId}"...`);
let repoData = await this.checkRepoExists(projectId);
if (!repoData) {
console.log(`[DEBUG] Gitea remote repository "${projectId}" does not exist. Creating...`);
repoData = await this.createRemoteRepo(projectId);
console.log(`[DEBUG] Gitea remote repository created: ${JSON.stringify(repoData)}`);
} else {
console.log(`[DEBUG] Gitea 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 });
}
}
}
static async setupLocalGitHubRepo() {
try {
if (!GITHUB_REPO_URL) {
console.log('[DEBUG] GITHUB_REPO_URL is not set. Skipping GitHub repo setup.');
return;
}
const gitDir = path.join(ROOT_PATH, '.git');
const repoExists = await this.exists(gitDir);
if (repoExists) {
console.log('[DEBUG] Git repository already initialized. Fetching and resetting...');
await this._addGithubRemote();
console.log('[DEBUG] Fetching GitHub remote...');
await exec(`git fetch github`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
try {
console.log('[DEBUG] Checking for remote branch "github/ai-dev"...');
await exec(`git rev-parse --verify github/ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log('[DEBUG] Remote branch "github/ai-dev" exists. Resetting local repository to github/ai-dev...');
await exec(`git reset --hard github/ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log('[DEBUG] Switching to branch "ai-dev"...');
await exec(`git checkout -B ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
} catch (e) {
console.log('[DEBUG] Remote branch "github/ai-dev" does NOT exist. Creating initial commit...');
await this.commitInitialChanges();
}
return;
}
console.log('[DEBUG] Initializing git in existing directory...');
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 });
await this._addGithubRemote();
console.log('[DEBUG] Fetching GitHub remote...');
await exec(`git fetch github`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
try {
console.log('[DEBUG] Checking for remote branch "github/ai-dev"...');
await exec(`git rev-parse --verify github/ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log('[DEBUG] Remote branch "github/ai-dev" exists. Resetting local repository to github/ai-dev...');
await exec(`git reset --hard github/ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log('[DEBUG] Switching to branch "ai-dev"...');
await exec(`git checkout -B ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
} catch (e) {
console.log('[DEBUG] Remote branch "github/ai-dev" does NOT exist. Creating initial commit...');
await this.commitInitialChanges();
}
} catch (error) {
console.error(`[ERROR] Failed to setup local GitHub repo: ${error.message}`);
throw error;
}
}
// 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 remotes...');
if (GITHUB_REPO_URL) {
await exec(`git fetch github`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
const branchReset = await this.tryResetToBranch('ai-dev', 'github');
if (branchReset) {
return;
}
}
await exec(`git fetch gitea`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
const branchReset = await this.tryResetToBranch('ai-dev', 'gitea');
if (!branchReset) {
const masterReset = await this.tryResetToBranch('master', 'gitea');
if (masterReset) {
console.log('[DEBUG] Creating and switching to branch "ai-dev"...');
await exec(`git branch ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
await exec(`git checkout ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log('[DEBUG] Pushing ai-dev branch to remotes...');
await exec(`git push -u gitea ai-dev --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
if (GITHUB_REPO_URL) {
await exec(`git push -u github ai-dev --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
}
} else {
console.log('[DEBUG] Neither "gitea/ai-dev" nor "gitea/master" exist. Creating initial commit...');
await this.commitInitialChanges();
}
}
}
// Tries to check out and reset to the specified branch
static async tryResetToBranch(branchName, remote) {
try {
console.log(`[DEBUG] Checking for remote branch "${remote}/${branchName}"...`);
await exec(`git rev-parse --verify ${remote}/${branchName}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log(`[DEBUG] Remote branch "${remote}/${branchName}" found. Resetting local repository to "${remote}/${branchName}"...`);
await exec(`git reset --hard ${remote}/${branchName}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
await exec(`git checkout ${branchName === 'ai-dev' ? 'ai-dev' : branchName}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
return true;
} catch (e) {
console.log(`[DEBUG] Remote branch "${remote}/${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 });
if (GITHUB_REPO_URL) {
await exec(`git push -u github master --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
}
await exec(`git push -u gitea master --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log('[DEBUG] Creating and switching to branch "ai-dev"...');
await exec(`git branch ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
await exec(`git checkout ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log('[DEBUG] Making ai-dev branch identical to master...');
if (GITHUB_REPO_URL) {
await exec(`git reset --hard github/master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
} else {
await exec(`git reset --hard gitea/master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
}
console.log('[DEBUG] Pushing ai-dev branch to remotes...');
if (GITHUB_REPO_URL) {
await exec(`git push -u github ai-dev --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
}
await exec(`git push -u gitea ai-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(giteaRemoteUrl) {
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 Gitea remote ${giteaRemoteUrl}...`);
await exec(`git remote add gitea ${giteaRemoteUrl}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
if (GITHUB_REPO_URL) {
await this._addGithubRemote();
}
console.log('[DEBUG] Fetching Gitea remote...');
await exec(`git fetch gitea`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
try {
console.log('[DEBUG] Checking for remote branch "gitea/ai-dev"...');
await exec(`git rev-parse --verify gitea/ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log('[DEBUG] Remote branch "gitea/ai-dev" exists. Resetting local repository to gitea/ai-dev...');
await exec(`git reset --hard gitea/ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log('[DEBUG] Switching to branch "ai-dev"...');
await exec(`git checkout -B ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
} catch (e) {
console.log('[DEBUG] Remote branch "gitea/ai-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 = '.') {
try {
console.log(`[DEBUG] Starting commit process...`);
await this._ensureDevBranch();
console.log(`[DEBUG] Ensuring .gitignore is properly configured...`);
await this._ensureGitignore();
console.log(`[DEBUG] Adding files to git index: ${files}`);
if (files === '.') {
await exec(`git add . ':!node_modules/' ':!*/node_modules/' ':!**/node_modules/'`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
} else {
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 });
console.log(`[DEBUG] Git status before commit: ${status}`);
if (!status.trim()) {
console.log(`[DEBUG] No changes to commit`);
return { message: "No changes to commit" };
}
const now = new Date();
const commitMessage = message || `Auto commit: ${now.toISOString()}`;
console.log(`[DEBUG] Committing changes with message: "${commitMessage}"`);
const { stdout: commitOutput, stderr: commitError } = await exec(`git commit -m "${commitMessage}"`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log(`[DEBUG] Commit output: ${commitOutput}`);
if (commitError) {
console.log(`[DEBUG] Commit stderr: ${commitError}`);
}
const { stdout: currentBranch } = await exec(`git rev-parse --abbrev-ref HEAD`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
const branchName = currentBranch.trim();
console.log(`[DEBUG] Current branch: ${branchName}`);
console.log(`[DEBUG] Pushing changes to Gitea...`);
try {
const { stdout: giteaPushOutput, stderr: giteaPushError } = await exec(`git push gitea ${branchName}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log(`[DEBUG] Gitea push output: ${giteaPushOutput}`);
if (giteaPushError) {
console.log(`[DEBUG] Gitea push stderr: ${giteaPushError}`);
}
} catch (giteaError) {
console.error(`[ERROR] Failed to push to Gitea: ${giteaError.message}`);
if (giteaError.stderr && giteaError.stderr.includes('rejected')) {
console.log(`[DEBUG] Push rejected, trying with --force...`);
try {
const { stdout, stderr } = await exec(`git push gitea ${branchName} --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log(`[DEBUG] Force push to Gitea output: ${stdout}`);
if (stderr) {
console.log(`[DEBUG] Force push to Gitea stderr: ${stderr}`);
}
} catch (forceError) {
console.error(`[ERROR] Force push to Gitea failed: ${forceError.message}`);
}
}
}
if (GITHUB_REPO_URL) {
console.log(`[DEBUG] Pushing changes to GitHub...`);
try {
const { stdout: githubPushOutput, stderr: githubPushError } = await exec(`git push github ${branchName}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log(`[DEBUG] GitHub push output: ${githubPushOutput}`);
if (githubPushError) {
console.log(`[DEBUG] GitHub push stderr: ${githubPushError}`);
}
} catch (githubError) {
console.error(`[ERROR] Failed to push to GitHub: ${githubError.message}`);
if (githubError.stderr && githubError.stderr.includes('rejected')) {
console.log(`[DEBUG] Push rejected, trying with --force...`);
try {
const { stdout, stderr } = await exec(`git push github ${branchName} --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log(`[DEBUG] Force push to GitHub output: ${stdout}`);
if (stderr) {
console.log(`[DEBUG] Force push to GitHub stderr: ${stderr}`);
}
} catch (forceError) {
console.error(`[ERROR] Force push to GitHub failed: ${forceError.message}`);
}
}
}
}
console.log(`[DEBUG] Commit process completed`);
return { message: "Changes committed" };
} catch (error) {
console.error(`[ERROR] Error during commit process: ${error.message}`);
throw new Error(`Error during commit: ${error.message}`);
}
}
static async getLog() {
try {
const remote = GITHUB_REPO_URL ? 'github' : 'gitea';
const { stdout: remotes } = await exec(`git remote -v`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log(`[DEBUG] Remotes: ${remotes}`);
const { stdout: branches } = await exec(`git branch -a`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log(`[DEBUG] Branches: ${branches}`);
const { stdout } = await exec(`git log ${remote}/ai-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) {
console.error(`[ERROR] Error during get log: ${error.message}`);
throw error;
}
}
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 {
const { stdout: currentBranch } = await exec(`git rev-parse --abbrev-ref HEAD`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
const branchName = currentBranch.trim();
await exec(`git reset --hard`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
await exec(
`git revert --no-edit ${commitHash}..HEAD --no-commit`,
{ cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }
);
await exec(
`git commit -m "Revert to version ${commitHash}"`,
{ cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }
);
await exec(`git push gitea ${branchName}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
if (GITHUB_REPO_URL) {
await exec(`git push github ${branchName}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
}
return { message: `Reverted to commit ${commitHash}` };
} catch (error) {
console.error("Error during 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 revert: ${error.message}`);
}
}
static async mergeDevIntoMaster() {
try {
// First, make sure we have the latest changes from both branches
console.log('[DEBUG] Fetching latest changes from remote repositories...');
await exec(`git fetch gitea`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
if (GITHUB_REPO_URL) {
await exec(`git fetch github`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
}
// Switch to branch 'master'
console.log('[DEBUG] Switching to branch "master"...');
await exec(`git checkout master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
// Pull latest changes from master
console.log('[DEBUG] Pulling latest changes from master branch...');
try {
await exec(`git pull gitea master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log('[DEBUG] Successfully pulled from Gitea master');
} catch (pullError) {
console.warn(`[WARN] Failed to pull from Gitea master: ${pullError.message}`);
// Try to continue anyway
}
// Switch to ai-dev and make sure it's up to date
console.log('[DEBUG] Switching to branch "ai-dev"...');
await exec(`git checkout ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
// Pull latest changes from ai-dev
console.log('[DEBUG] Pulling latest changes from ai-dev branch...');
try {
await exec(`git pull gitea ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log('[DEBUG] Successfully pulled from Gitea ai-dev');
} catch (pullError) {
console.warn(`[WARN] Failed to pull from Gitea ai-dev: ${pullError.message}`);
// Try to continue anyway
}
// Switch back to master for the merge
console.log('[DEBUG] Switching back to branch "master"...');
await exec(`git checkout master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
// Merge branch 'ai-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('[DEBUG] Merging branch "ai-dev" into "master" (force merge with -X theirs)...');
try {
const { stdout: mergeOutput, stderr: mergeError } = await exec(
`git merge ai-dev --no-ff -X theirs -m "Forced merge: merge ai-dev into master"`,
{ cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }
);
console.log(`[DEBUG] Merge output: ${mergeOutput}`);
if (mergeError) {
console.log(`[DEBUG] Merge stderr: ${mergeError}`);
}
} catch (mergeError) {
console.error(`[ERROR] Merge failed: ${mergeError.message}`);
if (mergeError.stdout) {
console.error(`[ERROR] Merge stdout: ${mergeError.stdout}`);
}
if (mergeError.stderr) {
console.error(`[ERROR] Merge stderr: ${mergeError.stderr}`);
}
// Abort the merge if it failed
console.log('[DEBUG] Aborting failed merge...');
await exec(`git merge --abort`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
throw new Error(`Failed to merge ai-dev into master: ${mergeError.message}`);
}
// Push the merged 'master' branch to both remotes
console.log('[DEBUG] Pushing merged master branch to Gitea remote...');
try {
const { stdout: giteaPushOutput, stderr: giteaPushError } = await exec(`git push gitea master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log(`[DEBUG] Gitea push output: ${giteaPushOutput}`);
if (giteaPushError) {
console.log(`[DEBUG] Gitea push stderr: ${giteaPushError}`);
}
} catch (pushError) {
console.error(`[ERROR] Failed to push to Gitea: ${pushError.message}`);
// If push is rejected, try with --force
if (pushError.stderr && pushError.stderr.includes('rejected')) {
console.log('[DEBUG] Push rejected, trying with --force...');
try {
const { stdout, stderr } = await exec(`git push gitea master --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log(`[DEBUG] Force push to Gitea output: ${stdout}`);
if (stderr) {
console.log(`[DEBUG] Force push to Gitea stderr: ${stderr}`);
}
} catch (forceError) {
console.error(`[ERROR] Force push to Gitea also failed: ${forceError.message}`);
throw forceError;
}
} else {
throw pushError;
}
}
if (GITHUB_REPO_URL) {
console.log('[DEBUG] Pushing merged master branch to GitHub remote...');
try {
const { stdout: githubPushOutput, stderr: githubPushError } = await exec(`git push github master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log(`[DEBUG] GitHub push output: ${githubPushOutput}`);
if (githubPushError) {
console.log(`[DEBUG] GitHub push stderr: ${githubPushError}`);
}
} catch (pushError) {
console.error(`[ERROR] Failed to push to GitHub: ${pushError.message}`);
// If push is rejected, try with --force
if (pushError.stderr && pushError.stderr.includes('rejected')) {
console.log('[DEBUG] Push rejected, trying with --force...');
try {
const { stdout, stderr } = await exec(`git push github master --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log(`[DEBUG] Force push to GitHub output: ${stdout}`);
if (stderr) {
console.log(`[DEBUG] Force push to GitHub stderr: ${stderr}`);
}
} catch (forceError) {
console.error(`[ERROR] Force push to GitHub also failed: ${forceError.message}`);
throw forceError;
}
} else {
throw pushError;
}
}
}
return { message: "Branch ai-dev merged into master and pushed to all remotes" };
} catch (error) {
console.error(`[ERROR] Error during mergeDevIntoMaster: ${error.message}`);
throw new Error(`Error during merge of ai-dev into master: ${error.message}`);
}
}
static async _mergeDevIntoMasterGitHub() {
try {
// Switch to branch 'master'
console.log('Switching to branch "master" (GitHub)...');
await exec(`git checkout master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
// Merge branch 'ai-dev' into 'master' with a forced merge.
console.log('Merging branch "ai-dev" into "master" (GitHub, force merge with -X theirs)...');
await exec(
`git merge ai-dev --no-ff -X theirs -m "Forced merge: merge ai-dev into master"`,
{ cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }
);
// Push the merged 'master' branch to remote (GitHub)
console.log('Pushing merged master branch to remote (GitHub)...');
const { stdout, stderr } = await exec(`git push -f github master`, {
cwd: ROOT_PATH,
maxBuffer: MAX_BUFFER
});
if (stdout) {
console.log("Git push GitHub stdout:", stdout);
}
if (stderr) {
console.error("Git push GitHub stderr:", stderr);
}
return { message: "Branch ai-dev merged into master and pushed to GitHub remote" };
} catch (error) {
console.error("Error during mergeDevIntoMasterGitHub:", error.message);
if (error.stdout) {
console.error("Merge GitHub stdout:", error.stdout);
}
if (error.stderr) {
console.error("Merge GitHub stderr:", error.stderr);
}
throw error;
}
}
static async resetDevBranch() {
try {
console.log(`[DEBUG] Starting reset of ai-dev branch to match master...`);
// Check current branch state
const { stdout: initialBranches } = await exec(`git branch -a`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log(`[DEBUG] Initial branches: ${initialBranches}`);
// Check if master branch exists
const { stdout: masterExists } = await exec(`git branch --list master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
if (!masterExists.trim()) {
console.log(`[DEBUG] Master branch does not exist. Creating it...`);
await exec(`git checkout -b master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
}
// Switch to master branch
console.log(`[DEBUG] Switching to branch "master"...`);
await exec(`git checkout master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
// Verify we are on master branch
const { stdout: currentBranch } = await exec(`git rev-parse --abbrev-ref HEAD`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log(`[DEBUG] Current branch after checkout: ${currentBranch.trim()}`);
// Get master branch commit hash
const { stdout: masterCommit } = await exec(`git rev-parse master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log(`[DEBUG] Master branch commit hash: ${masterCommit.trim()}`);
// Delete local ai-dev branch if it exists
try {
await exec(`git branch -D ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log(`[DEBUG] Local branch ai-dev deleted successfully`);
} catch (error) {
console.log(`[DEBUG] Local branch ai-dev does not exist or could not be deleted: ${error.message}`);
}
// Create new ai-dev branch from master
await exec(`git checkout -b ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log(`[DEBUG] Created new ai-dev branch from master`);
// Verify we are on ai-dev branch
const { stdout: newCurrentBranch } = await exec(`git rev-parse --abbrev-ref HEAD`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log(`[DEBUG] Current branch after creating ai-dev: ${newCurrentBranch.trim()}`);
// Delete remote ai-dev branches if they exist
console.log(`[DEBUG] Deleting remote ai-dev branches if they exist...`);
try {
await exec(`git push gitea --delete ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log(`[DEBUG] Remote branch ai-dev on Gitea deleted successfully`);
} catch (error) {
console.log(`[DEBUG] Remote branch ai-dev on Gitea does not exist or could not be deleted: ${error.message}`);
}
if (GITHUB_REPO_URL) {
try {
await exec(`git push github --delete ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log(`[DEBUG] Remote branch ai-dev on GitHub deleted successfully`);
} catch (error) {
console.log(`[DEBUG] Remote branch ai-dev on GitHub does not exist or could not be deleted: ${error.message}`);
}
}
// Create an empty commit to ensure ai-dev branch is different from master
await exec(`git commit --allow-empty -m "Reset ai-dev branch to match master"`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log(`[DEBUG] Created empty commit on ai-dev branch`);
// Push new ai-dev branch to remote repositories
console.log(`[DEBUG] Pushing new ai-dev branch to Gitea...`);
try {
const { stdout: giteaPush, stderr: giteaPushErr } = await exec(`git push -u gitea ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log(`[DEBUG] Gitea push output: ${giteaPush}`);
if (giteaPushErr) {
console.log(`[DEBUG] Gitea push stderr: ${giteaPushErr}`);
}
} catch (error) {
console.error(`[ERROR] Failed to push to Gitea: ${error.message}`);
// Try with --force if regular push failed
try {
console.log(`[DEBUG] Trying force push to Gitea...`);
const { stdout, stderr } = await exec(`git push -u gitea ai-dev --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log(`[DEBUG] Gitea force push output: ${stdout}`);
if (stderr) {
console.log(`[DEBUG] Gitea force push stderr: ${stderr}`);
}
} catch (forceError) {
console.error(`[ERROR] Force push to Gitea also failed: ${forceError.message}`);
}
}
if (GITHUB_REPO_URL) {
console.log(`[DEBUG] Pushing new ai-dev branch to GitHub...`);
try {
const { stdout: githubPush, stderr: githubPushErr } = await exec(`git push -u github ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log(`[DEBUG] GitHub push output: ${githubPush}`);
if (githubPushErr) {
console.log(`[DEBUG] GitHub push stderr: ${githubPushErr}`);
}
} catch (error) {
console.error(`[ERROR] Failed to push to GitHub: ${error.message}`);
// Try with --force if regular push failed
try {
console.log(`[DEBUG] Trying force push to GitHub...`);
const { stdout, stderr } = await exec(`git push -u github ai-dev --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log(`[DEBUG] GitHub force push output: ${stdout}`);
if (stderr) {
console.log(`[DEBUG] GitHub force push stderr: ${stderr}`);
}
} catch (forceError) {
console.error(`[ERROR] Force push to GitHub also failed: ${forceError.message}`);
}
}
}
// Check remote branches after push
const { stdout: remoteBranches } = await exec(`git ls-remote --heads`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log(`[DEBUG] Remote branches after push: ${remoteBranches}`);
console.log(`[DEBUG] Reset of ai-dev branch completed successfully`);
return { message: "Branch ai-dev has been reset to match master" };
} catch (error) {
console.error(`[ERROR] Error during reset of dev branch: ${error.message}`);
throw new Error(`Error during reset of dev branch: ${error.message}`);
}
}
static async _resetDevBranchGitHub() {
try {
console.log('[DEBUG] Switching to branch "master" (GitHub)...');
await exec(`git checkout master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log('[DEBUG] Resetting branch "ai-dev" to be identical to "master" (GitHub)...');
await exec(`git checkout -B ai-dev master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log('[DEBUG] Pushing updated branch "ai-dev" to remote (GitHub, force push)...');
await exec(`git push -f github ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
return { message: 'ai-dev branch successfully reset to master (GitHub).' };
} catch (error) {
console.error("Error during resetting ai-dev branch (GitHub):", error.message);
if (error.stdout) {
console.error("Reset GitHub stdout:", error.stdout);
}
if (error.stderr) {
console.error("Reset GitHub stderr:", error.stderr);
}
throw new Error(`Error during resetting ai-dev branch (GitHub): ${error.message}`);
}
}
static async _pushChangesToGitea() {
try {
const { stdout, stderr } = await exec(`git push gitea ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
if (stdout) {
console.log("Git push Gitea stdout:", stdout);
}
if (stderr) {
console.error("Git push Gitea stderr:", stderr);
}
return { message: "Changes pushed to Gitea remote repository (ai-dev branch)" };
} catch (error) {
console.error("Git push Gitea error:", error.message);
if (error.stdout) {
console.error("Git push Gitea stdout:", error.stdout);
}
if (error.stderr) {
console.error("Git push Gitea stderr:", error.stderr);
}
throw error;
}
}
static async _pushChangesToGithub() {
try {
const { stdout, stderr } = await exec(`git push github ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
if (stdout) {
console.log("Git push GitHub stdout:", stdout);
}
if (stderr) {
console.error("Git push GitHub stderr:", stderr);
}
return { message: "Changes pushed to GitHub repository (ai-dev branch)" };
} catch (error) {
console.error("Git push GitHub error:", error.message);
if (error.stdout) {
console.error("Git push GitHub stdout:", error.stdout);
}
if (error.stderr) {
console.error("Git push GitHub stderr:", error.stderr);
}
throw error;
}
}
static async _addGithubRemote() {
if (GITHUB_REPO_URL) {
try {
const { stdout: remotes } = await exec(`git remote -v`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
if (!remotes.includes('github')) {
console.log(`[DEBUG] Adding GitHub remote: git remote add github ${GITHUB_REPO_URL}`);
await exec(`git remote add github ${GITHUB_REPO_URL}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log(`[DEBUG] GitHub remote added: ${GITHUB_REPO_URL}`);
} else {
console.log(`[DEBUG] GitHub remote already exists.`);
}
} catch (error) {
console.error(`[ERROR] Failed to add GitHub remote: ${error.message}`);
if (error.stdout) {
console.error(`[ERROR] git remote add stdout: ${error.stdout}`);
}
if (error.stderr) {
console.error(`[ERROR] git remote add stderr: ${error.stderr}`);
}
throw error;
}
}
}
static async _addGiteaRemote(giteaRemoteUrl) {
try {
const { stdout: remotes } = await exec(`git remote -v`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
if (!remotes.includes('gitea')) {
console.log(`[DEBUG] Adding Gitea remote: git remote add gitea ${giteaRemoteUrl}`);
await exec(`git remote add gitea ${giteaRemoteUrl}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
console.log(`[DEBUG] Gitea remote added: ${giteaRemoteUrl}`);
} else {
console.log(`[DEBUG] Gitea remote already exists.`);
}
} catch (error) {
console.error(`[ERROR] Failed to add Gitea remote: ${error.message}`);
if (error.stdout) {
console.error(`[ERROR] git remote add stdout: ${error.stdout}`);
}
if (error.stderr) {
console.error(`[ERROR] git remote add stderr: ${error.stderr}`);
}
throw error;
}
}
static async _revertGitHubChanges(branchName) {
try {
await exec(`git push -f github ${branchName}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
} catch (error) {
console.error("Error during revertGitHubChanges:", error.message);
if (error.stdout) {
console.error("revertGitHubChanges stdout:", error.stdout);
}
if (error.stderr) {
console.error("revertGitHubChanges stderr:", error.stderr);
}
throw new Error(`Error during revertGitHubChanges: ${error.message}`);
}
}
static async _ensureDevBranch() {
try {
console.log(`[DEBUG] Ensuring we are on 'ai-dev' branch...`);
const { stdout: branchList } = await exec(`git branch --list ai-dev`, {
cwd: ROOT_PATH,
maxBuffer: MAX_BUFFER,
});
if (!branchList || branchList.trim() === '') {
console.log(`[DEBUG] Branch 'ai-dev' not found. Creating branch 'ai-dev'.`);
await exec(`git checkout -b ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
} else {
const { stdout: currentBranchStdout } = await exec(`git rev-parse --abbrev-ref HEAD`, {
cwd: ROOT_PATH,
maxBuffer: MAX_BUFFER,
});
const currentBranch = currentBranchStdout.trim();
if (currentBranch !== 'ai-dev') {
console.log(`[DEBUG] Switching from branch '${currentBranch}' to 'ai-dev'.`);
await exec(`git checkout ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
} else {
console.log(`[DEBUG] Already on branch 'ai-dev'.`);
}
}
console.log(`[DEBUG] Successfully ensured we are on 'ai-dev' branch.`);
} catch (error) {
console.error(`[ERROR] Error ensuring branch 'ai-dev': ${error.message}`);
if (error.stdout) {
console.error(`[ERROR] stdout: ${error.stdout}`);
}
if (error.stderr) {
console.error(`[ERROR] stderr: ${error.stderr}`);
}
throw new Error(`Error ensuring branch 'ai-dev': ${error.message}`);
}
}
static async _ensureGitignore() {
try {
console.log(`[DEBUG] Checking .gitignore file...`);
const gitignorePath = path.join(ROOT_PATH, '.gitignore');
let gitignoreContent = '';
try {
gitignoreContent = await fs.readFile(gitignorePath, 'utf8');
console.log(`[DEBUG] Existing .gitignore found.`);
} catch (error) {
console.log(`[DEBUG] .gitignore file not found, creating new one.`);
}
const requiredPatterns = [
'node_modules/',
'*/node_modules/',
'**/node_modules/',
'*/build/',
'**/build/',
'.DS_Store',
'.env'
];
let needsUpdate = false;
for (const pattern of requiredPatterns) {
if (!gitignoreContent.includes(pattern)) {
gitignoreContent += `\n${pattern}`;
needsUpdate = true;
}
}
if (needsUpdate) {
console.log(`[DEBUG] Updating .gitignore file with missing patterns.`);
await fs.writeFile(gitignorePath, gitignoreContent.trim(), 'utf8');
console.log(`[DEBUG] .gitignore file updated successfully.`);
} else {
console.log(`[DEBUG] .gitignore file is up to date.`);
}
return true;
} catch (error) {
console.error(`[ERROR] Error ensuring .gitignore: ${error.message}`);
return false;
}
}
}
module.exports = VCS;