@dharshansr/gitgenius
Version:
AI-powered commit message generator with enhanced features
394 lines ⢠14.4 kB
JavaScript
/**
* Git State Manager - Handles detection and management of various Git states
* including detached HEAD, merge conflicts, worktrees, submodules, and dirty workspace
*/
import simpleGit from 'simple-git';
import chalk from 'chalk';
import { ErrorHandler } from './ErrorHandler.js';
export class GitStateManager {
constructor() {
this.git = simpleGit();
}
/**
* Get comprehensive Git state information
*/
async getState() {
try {
const status = await this.git.status();
const isDetached = await this.isDetachedHead();
const hasConflicts = this.checkConflicts(status);
const hasMerge = await this.hasMergeInProgress();
const hasRebase = await this.hasRebaseInProgress();
return {
isDetachedHead: isDetached,
hasConflicts,
hasMergeInProgress: hasMerge,
hasRebaseInProgress: hasRebase,
isDirty: status.files.length > 0,
hasUncommittedChanges: status.modified.length > 0 || status.deleted.length > 0,
hasUntrackedFiles: status.not_added.length > 0,
hasStagedChanges: status.staged.length > 0 || status.created.length > 0 || status.renamed.length > 0,
currentBranch: status.current || null,
currentCommit: status.tracking || 'HEAD'
};
}
catch (error) {
throw ErrorHandler.gitError(`Failed to get Git state: ${error instanceof Error ? error.message : String(error)}`, ['Ensure you are in a Git repository', 'Check Git installation']);
}
}
/**
* Check if HEAD is detached
*/
async isDetachedHead() {
try {
const status = await this.git.status();
return status.detached || status.current === null;
}
catch (error) {
return false;
}
}
/**
* Check for merge conflicts
*/
checkConflicts(status) {
return status.conflicted.length > 0;
}
/**
* Check if a merge is in progress
*/
async hasMergeInProgress() {
try {
// Check for MERGE_HEAD file which indicates ongoing merge
const result = await this.git.raw(['rev-parse', '--verify', 'MERGE_HEAD']);
return result.trim().length > 0;
}
catch (error) {
return false;
}
}
/**
* Check if a rebase is in progress
*/
async hasRebaseInProgress() {
try {
// Check if REBASE_HEAD exists (indicates rebase in progress)
await this.git.raw(['rev-parse', '--verify', 'REBASE_HEAD']);
return true;
}
catch {
return false;
}
}
/**
* Ensure workspace is clean before dangerous operations
*/
async ensureCleanWorkspace(allowStaged = false) {
const state = await this.getState();
if (state.hasConflicts) {
throw ErrorHandler.gitError('Cannot proceed: merge conflicts detected', [
'Resolve conflicts using: git status',
'After resolving, stage changes: git add <file>',
'Then commit or continue merge/rebase'
]);
}
if (state.hasMergeInProgress) {
throw ErrorHandler.gitError('Cannot proceed: merge in progress', [
'Complete the merge: git commit',
'Or abort the merge: git merge --abort'
]);
}
if (state.hasRebaseInProgress) {
throw ErrorHandler.gitError('Cannot proceed: rebase in progress', [
'Continue the rebase: git rebase --continue',
'Or abort the rebase: git rebase --abort'
]);
}
if (state.hasUncommittedChanges && !allowStaged) {
throw ErrorHandler.gitError('Cannot proceed: uncommitted changes detected', [
'Commit your changes: git commit -am "message"',
'Or stash them: git stash',
'Or discard them: git checkout -- <file>'
]);
}
if (state.hasStagedChanges && !allowStaged) {
throw ErrorHandler.gitError('Cannot proceed: staged changes detected', [
'Commit your changes: git commit -m "message"',
'Or unstage them: git reset HEAD'
]);
}
}
/**
* Handle detached HEAD state
*/
async handleDetachedHead() {
const isDetached = await this.isDetachedHead();
if (!isDetached) {
return;
}
console.log(chalk.yellow('ā Detached HEAD state detected'));
console.log(chalk.yellow('You are not on any branch. Commits made in this state will be lost unless you create a branch.'));
throw ErrorHandler.gitError('Operation blocked: detached HEAD state', [
'Create a new branch: git checkout -b <branch-name>',
'Switch to an existing branch: git checkout <branch-name>',
'To continue anyway, manually perform the operation'
]);
}
/**
* Get worktree information
*/
async getWorktrees() {
try {
const result = await this.git.raw(['worktree', 'list', '--porcelain']);
const worktrees = [];
if (!result.trim()) {
return worktrees;
}
const lines = result.split('\n');
let currentWorktree = {};
for (const line of lines) {
if (line.startsWith('worktree ')) {
if (currentWorktree.path) {
worktrees.push(currentWorktree);
}
currentWorktree = { path: line.substring(9) };
}
else if (line.startsWith('HEAD ')) {
currentWorktree.commit = line.substring(5);
}
else if (line.startsWith('branch ')) {
currentWorktree.branch = line.substring(7);
}
else if (line === 'bare') {
// Skip bare repositories
}
else if (line === '') {
if (currentWorktree.path) {
worktrees.push({
path: currentWorktree.path,
branch: currentWorktree.branch || null,
commit: currentWorktree.commit || '',
isMain: worktrees.length === 0
});
currentWorktree = {};
}
}
}
// Add last worktree if exists
if (currentWorktree.path) {
worktrees.push({
path: currentWorktree.path,
branch: currentWorktree.branch || null,
commit: currentWorktree.commit || '',
isMain: worktrees.length === 0
});
}
return worktrees;
}
catch (error) {
// Worktrees not supported or no worktrees exist
return [];
}
}
/**
* Check if worktrees are in use
*/
async hasWorktrees() {
const worktrees = await this.getWorktrees();
return worktrees.length > 1; // More than just the main worktree
}
/**
* Get submodule information
*/
async getSubmodules() {
try {
const result = await this.git.raw(['submodule', 'status']);
if (!result.trim()) {
return [];
}
const submodules = [];
const lines = result.split('\n').filter(line => line.trim());
for (const line of lines) {
const match = line.match(/^([ +-U])([a-f0-9]+) (.+?)(?: \((.+)\))?$/);
if (match) {
const [, status, commit, path, branch] = match;
submodules.push({
path,
url: '', // URL requires additional query
branch: branch || null,
commit,
isInitialized: status !== '-'
});
}
}
// Get URLs for each submodule
for (const submodule of submodules) {
try {
const url = await this.git.raw(['config', '--get', `submodule.${submodule.path}.url`]);
submodule.url = url.trim();
}
catch {
// URL not found
}
}
return submodules;
}
catch (error) {
// No submodules or command failed
return [];
}
}
/**
* Check if repository has submodules
*/
async hasSubmodules() {
const submodules = await this.getSubmodules();
return submodules.length > 0;
}
/**
* Initialize submodules
*/
async initializeSubmodules() {
try {
await this.git.raw(['submodule', 'update', '--init', '--recursive']);
console.log(chalk.green('ā Submodules initialized'));
}
catch (error) {
throw ErrorHandler.gitError(`Failed to initialize submodules: ${error instanceof Error ? error.message : String(error)}`, ['Check .gitmodules file', 'Verify submodule URLs', 'Ensure network connectivity']);
}
}
/**
* Update submodules
*/
async updateSubmodules() {
try {
await this.git.raw(['submodule', 'update', '--remote', '--merge']);
console.log(chalk.green('ā Submodules updated'));
}
catch (error) {
throw ErrorHandler.gitError(`Failed to update submodules: ${error instanceof Error ? error.message : String(error)}`, ['Check for conflicts in submodules', 'Verify network connectivity', 'Check submodule configuration']);
}
}
/**
* Get detailed conflict information
*/
async getConflictDetails() {
try {
const status = await this.git.status();
return status.conflicted;
}
catch (error) {
return [];
}
}
/**
* Provide actionable conflict resolution hints
*/
async getConflictResolutionHints() {
const conflicts = await this.getConflictDetails();
if (conflicts.length === 0) {
return [];
}
return [
`Found ${conflicts.length} conflicted file(s): ${conflicts.join(', ')}`,
'To resolve conflicts:',
' 1. Edit each conflicted file and resolve markers (<<<<<<< ======= >>>>>>>)',
' 2. Stage resolved files: git add <file>',
' 3. Complete the merge: git commit',
'Or abort the merge: git merge --abort'
];
}
/**
* Validate Git environment for operations
*/
async validateEnvironment() {
const errors = [];
try {
// Check if in a Git repository
const isRepo = await this.git.checkIsRepo();
if (!isRepo) {
errors.push('Not in a Git repository');
}
// Check Git version
try {
const version = await this.git.version();
if (!version.installed) {
errors.push('Git is not properly installed');
}
}
catch {
errors.push('Unable to determine Git version');
}
// Check for Git configuration
try {
await this.git.getConfig('user.name');
}
catch {
errors.push('Git user.name not configured');
}
try {
await this.git.getConfig('user.email');
}
catch {
errors.push('Git user.email not configured');
}
}
catch (error) {
errors.push(`Environment validation failed: ${error instanceof Error ? error.message : String(error)}`);
}
return {
valid: errors.length === 0,
errors
};
}
/**
* Display current Git state in a friendly format
*/
async displayState() {
const state = await this.getState();
console.log(chalk.blue('\nš Git State:'));
if (state.currentBranch) {
console.log(chalk.white(` Branch: ${chalk.green(state.currentBranch)}`));
}
else {
console.log(chalk.yellow(' ā Detached HEAD'));
}
if (state.hasConflicts) {
const conflicts = await this.getConflictDetails();
console.log(chalk.red(` ā Conflicts: ${conflicts.length} file(s)`));
}
if (state.hasMergeInProgress) {
console.log(chalk.yellow(' ā Merge in progress'));
}
if (state.hasRebaseInProgress) {
console.log(chalk.yellow(' ā Rebase in progress'));
}
if (state.hasStagedChanges) {
console.log(chalk.green(' ā Staged changes'));
}
if (state.hasUncommittedChanges) {
console.log(chalk.yellow(' ā Uncommitted changes'));
}
if (state.hasUntrackedFiles) {
console.log(chalk.gray(' ⢠Untracked files'));
}
if (!state.isDirty && !state.hasConflicts && !state.hasMergeInProgress && !state.hasRebaseInProgress) {
console.log(chalk.green(' ā Clean workspace'));
}
// Show worktree info
const hasWorktrees = await this.hasWorktrees();
if (hasWorktrees) {
const worktrees = await this.getWorktrees();
console.log(chalk.blue(` š Worktrees: ${worktrees.length}`));
}
// Show submodule info
const hasSubmodules = await this.hasSubmodules();
if (hasSubmodules) {
const submodules = await this.getSubmodules();
const initialized = submodules.filter(s => s.isInitialized).length;
console.log(chalk.blue(` š¦ Submodules: ${initialized}/${submodules.length} initialized`));
}
console.log('');
}
}
//# sourceMappingURL=GitStateManager.js.map