UNPKG

stacked-pr-sync

Version:

A Node.js tool for syncing stacked pull requests with advanced conflict detection and resolution

268 lines (234 loc) 8.45 kB
const { execSync } = require('child_process') const { logStep, logSuccess, logError, logWarning, logInfo, logCommand } = require('./logger') // Check if we're in a git repository function checkGitRepo() { try { execSync('git rev-parse --git-dir', { stdio: 'ignore' }) return true } catch (error) { return false } } // Get current branch name function getCurrentBranch() { try { return execSync('git branch --show-current', { encoding: 'utf8' }).trim() } catch (error) { throw new Error('Failed to get current branch name') } } // Check if working directory is clean (excluding the script files) function isWorkingDirectoryClean() { try { const status = execSync('git status --porcelain', { encoding: 'utf8' }) const lines = status .trim() .split('\n') .filter((line) => line.trim() !== '') // Filter out changes to our script files const scriptFiles = ['scripts/stacked-pr-sync.js', 'scripts/stacked-pr-config.json'] const filteredLines = lines.filter((line) => { const fileName = line.substring(3) // Remove status prefix (e.g., " M ") return !scriptFiles.some((scriptFile) => fileName.includes(scriptFile)) }) return filteredLines.length === 0 } catch (error) { throw new Error('Failed to check git status') } } // Check if origin remote exists function checkOriginExists() { try { execSync('git remote get-url origin', { stdio: 'ignore' }) return true } catch (error) { return false } } // Fetch latest changes from remote function fetchLatest() { try { // First check if origin remote exists if (!checkOriginExists()) { logWarning('No origin remote found. Skipping fetch.') logInfo('This repository appears to be local-only.') return false } logStep('Fetching', 'Fetching latest changes from remote...') logCommand('git fetch origin') execSync('git fetch origin', { stdio: 'inherit' }) logSuccess('Fetched latest changes') return true } catch (error) { logWarning('Fetch failed, but continuing with local branches...') logInfo('This might be due to network issues, authentication problems, or no remote access.') logInfo('The script will continue with your local branch state.') return false } } // Check if a branch is in sync with origin function isBranchInSync(branchName) { try { // Get local commit hash const localCommit = execSync(`git rev-parse ${branchName}`, { encoding: 'utf8' }).trim() // Check if remote branch exists try { const remoteCommit = execSync(`git rev-parse origin/${branchName}`, { encoding: 'utf8' }).trim() return localCommit === remoteCommit } catch (error) { // Remote branch doesn't exist, consider it in sync logInfo(`Remote branch origin/${branchName} doesn't exist, skipping sync check`) return true } } catch (error) { logWarning(`Could not check sync status for ${branchName}: ${error.message}`) return false } } // Switch to a branch function switchToBranch(branchName) { try { logStep('Switching', `Switching to branch: ${branchName}`) // Check if branch exists locally try { execSync(`git show-ref --verify --quiet refs/heads/${branchName}`) logCommand(`git checkout ${branchName}`) execSync(`git checkout ${branchName}`, { stdio: 'inherit' }) } catch (error) { // Branch doesn't exist locally, try to checkout from remote logInfo(`Branch ${branchName} doesn't exist locally, checking out from remote...`) logCommand(`git checkout -b ${branchName} origin/${branchName}`) execSync(`git checkout -b ${branchName} origin/${branchName}`, { stdio: 'inherit' }) } logSuccess(`Switched to branch: ${branchName}`) } catch (error) { throw new Error(`Failed to switch to branch: ${branchName}`) } } // Merge changes from a source branch locally function mergeFromBranch(sourceBranch) { try { logStep('Merging', `Merging changes from ${sourceBranch}...`) logCommand(`git merge ${sourceBranch}`) execSync(`git merge ${sourceBranch}`, { stdio: 'inherit' }) logSuccess(`Successfully merged changes from ${sourceBranch}`) return true } catch (error) { logError(`Failed to merge changes from ${sourceBranch}`) logWarning('This might be due to conflicts. Please resolve conflicts manually and then continue.') return false } } // Check for merge conflicts function hasConflicts() { try { const status = execSync('git status --porcelain', { encoding: 'utf8' }) return status.includes('UU') || status.includes('AA') || status.includes('DD') } catch (error) { return false } } // Perform dry-run merge to check for potential conflicts function performDryRunMerge(sourceBranch, targetBranch) { const tempBranchName = `temp-dry-run-${Date.now()}` let currentBranch = null try { logInfo(`Checking potential conflicts: ${sourceBranch}${targetBranch}`) // Get current branch before starting currentBranch = getCurrentBranch() // Create a temporary branch for dry-run execSync(`git checkout -b ${tempBranchName} ${targetBranch}`, { stdio: 'ignore' }) // Try to merge source into temp branch try { execSync(`git merge ${sourceBranch} --no-commit --no-ff`, { stdio: 'ignore' }) // If we get here, no conflicts occurred execSync(`git merge --abort`, { stdio: 'ignore' }) return { hasConflicts: false } } catch (mergeError) { // Check if this is a conflict error const status = execSync('git status --porcelain', { encoding: 'utf8' }) const hasConflicts = status.includes('UU') || status.includes('AA') || status.includes('DD') // Clean up temp branch execSync(`git merge --abort`, { stdio: 'ignore' }) return { hasConflicts, error: mergeError.message } } } catch (error) { return { hasConflicts: false, error: error.message } } finally { // Always clean up and return to original branch try { execSync(`git checkout ${currentBranch}`, { stdio: 'ignore' }) execSync(`git branch -D ${tempBranchName}`, { stdio: 'ignore' }) } catch (cleanupError) { // Ignore cleanup errors } } } // Check branch status for smart origin detection function getBranchStatus(branchName) { try { // Check if branch exists locally const localExists = execSync(`git show-ref --verify --quiet refs/heads/${branchName}`, { stdio: 'ignore' }) // Check if remote branch exists let remoteExists = false let isInSync = false let syncStatus = 'unknown' try { execSync(`git rev-parse origin/${branchName}`, { stdio: 'ignore' }) remoteExists = true // Compare local and remote commits const localCommit = execSync(`git rev-parse ${branchName}`, { encoding: 'utf8' }).trim() const remoteCommit = execSync(`git rev-parse origin/${branchName}`, { encoding: 'utf8' }).trim() isInSync = localCommit === remoteCommit syncStatus = isInSync ? 'in-sync' : 'out-of-sync' } catch (error) { remoteExists = false syncStatus = 'no-remote' } return { branch: branchName, localExists: true, remoteExists, isInSync, syncStatus } } catch (error) { return { branch: branchName, localExists: false, remoteExists: false, isInSync: false, syncStatus: 'not-found' } } } // Get detailed sync information for a branch function getSyncDetails(branchName) { try { const localCommit = execSync(`git rev-parse ${branchName}`, { encoding: 'utf8' }).trim() const remoteCommit = execSync(`git rev-parse origin/${branchName}`, { encoding: 'utf8' }).trim() // Count commits ahead/behind const ahead = execSync(`git rev-list --count origin/${branchName}..${branchName}`, { encoding: 'utf8' }).trim() const behind = execSync(`git rev-list --count ${branchName}..origin/${branchName}`, { encoding: 'utf8' }).trim() return { ahead: parseInt(ahead), behind: parseInt(behind) } } catch (error) { return { ahead: 0, behind: 0 } } } module.exports = { checkGitRepo, getCurrentBranch, isWorkingDirectoryClean, fetchLatest, isBranchInSync, switchToBranch, mergeFromBranch, hasConflicts, performDryRunMerge, getBranchStatus, getSyncDetails, checkOriginExists, }