/**
* Git Command Executor
*
* Specialized utility for executing Git commands with proper error handling,
* repository validation, and Git-specific operations.
*/
import { TerminalController, CommandResult } from './terminal-controller.js';
import path from 'path';
export interface GitCommandResult extends CommandResult {
isGitRepository?: boolean;
gitError?: {
type: 'not_a_repository' | 'command_not_found' | 'authentication' | 'network' | 'merge_conflict' | 'invalid_ref' | 'unknown';
suggestion?: string;
};
}
export interface GitStatus {
branch: string;
ahead: number;
behind: number;
staged: string[];
unstaged: string[];
untracked: string[];
conflicted: string[];
modified: string[];
added: string[];
clean: boolean;
}
export class GitCommandExecutor {
private terminal: TerminalController;
constructor() {
this.terminal = new TerminalController();
}
/**
* Execute Git command with enhanced error handling
*/
async executeGitCommand(
command: string,
args: string[] = [],
projectPath?: string,
options: { timeout?: number; env?: Record<string, string> } = {}
): Promise<GitCommandResult> {
// Validate Git is available
const gitAvailable = await this.terminal.commandExists('git');
if (!gitAvailable) {
return {
success: false,
stdout: '',
stderr: 'Git command not found. Please install Git.',
exitCode: 127,
executionTime: 0,
command: `git ${command} ${args.join(' ')}`,
gitError: {
type: 'command_not_found',
suggestion: 'Install Git from https://git-scm.com/'
}
};
}
// Validate project path if provided
if (projectPath) {
const dirValidation = await this.terminal.validateDirectory(projectPath);
if (!dirValidation.exists) {
return {
success: false,
stdout: '',
stderr: `Directory does not exist: ${projectPath}`,
exitCode: 1,
executionTime: 0,
command: `git ${command} ${args.join(' ')}`,
gitError: {
type: 'not_a_repository',
suggestion: 'Ensure the project path exists and is accessible'
}
};
}
}
// Execute Git command
const result = await this.terminal.executeCommand(
'git',
[command, ...args],
{
cwd: projectPath,
timeout: options.timeout || 30000,
env: options.env
}
);
// Enhance result with Git-specific error analysis
const gitResult: GitCommandResult = {
...result,
isGitRepository: await this.isGitRepository(projectPath),
gitError: this.analyzeGitError(result)
};
return gitResult;
}
/**
* Check if directory is a Git repository
*/
async isGitRepository(projectPath?: string): Promise<boolean> {
try {
const result = await this.terminal.executeCommand(
'git',
['rev-parse', '--git-dir'],
{ cwd: projectPath }
);
return result.success;
} catch {
return false;
}
}
/**
* Initialize Git repository
*/
async initRepository(projectPath: string, bare: boolean = false): Promise<GitCommandResult> {
const args = bare ? ['--bare'] : [];
return this.executeGitCommand('init', args, projectPath);
}
/**
* Get repository status
*/
async getStatus(projectPath: string): Promise<GitCommandResult & { parsedStatus?: GitStatus }> {
const result = await this.executeGitCommand('status', ['--porcelain', '-b'], projectPath);
if (result.success) {
const parsedStatus = this.parseGitStatus(result.stdout);
return {
...result,
parsedStatus
};
}
return result;
}
/**
* Add files to staging area
*/
async addFiles(projectPath: string, files: string[] = ['.']): Promise<GitCommandResult> {
return this.executeGitCommand('add', files, projectPath);
}
/**
* Commit changes
*/
async commit(
projectPath: string,
message: string,
options: {
amend?: boolean;
signoff?: boolean;
author?: string;
} = {}
): Promise<GitCommandResult> {
const args = ['-m', message];
if (options.amend) args.push('--amend');
if (options.signoff) args.push('--signoff');
if (options.author) args.push('--author', options.author);
return this.executeGitCommand('commit', args, projectPath);
}
/**
* Push changes to remote
*/
async push(
projectPath: string,
remote: string = 'origin',
branch?: string,
options: {
force?: boolean;
setUpstream?: boolean;
} = {}
): Promise<GitCommandResult> {
const args = [remote];
if (branch) args.push(branch);
if (options.force) args.push('--force');
if (options.setUpstream) args.push('--set-upstream');
return this.executeGitCommand('push', args, projectPath);
}
/**
* Pull changes from remote
*/
async pull(
projectPath: string,
remote: string = 'origin',
branch?: string,
options: {
rebase?: boolean;
force?: boolean;
} = {}
): Promise<GitCommandResult> {
const args = [remote];
if (branch) args.push(branch);
if (options.rebase) args.push('--rebase');
if (options.force) args.push('--force');
return this.executeGitCommand('pull', args, projectPath);
}
/**
* Fetch from remote
*/
async fetch(
projectPath: string,
remote: string = 'origin',
options: {
all?: boolean;
prune?: boolean;
} = {}
): Promise<GitCommandResult> {
const args = [remote];
if (options.all) args.push('--all');
if (options.prune) args.push('--prune');
return this.executeGitCommand('fetch', args, projectPath);
}
/**
* Get current branch name
*/
async getCurrentBranch(projectPath: string): Promise<GitCommandResult & { branch?: string }> {
const result = await this.executeGitCommand('branch', ['--show-current'], projectPath);
return {
...result,
branch: result.success ? result.stdout.trim() : undefined
};
}
/**
* Get remote URL
*/
async getRemoteUrl(projectPath: string, remote: string = 'origin'): Promise<GitCommandResult & { url?: string }> {
const result = await this.executeGitCommand('remote', ['get-url', remote], projectPath);
return {
...result,
url: result.success ? result.stdout.trim() : undefined
};
}
/**
* Clone repository
*/
async cloneRepository(
url: string,
targetPath: string,
options: {
branch?: string;
depth?: number;
recursive?: boolean;
} = {}
): Promise<GitCommandResult> {
const args = [url, targetPath];
if (options.branch) args.push('--branch', options.branch);
if (options.depth) args.push('--depth', options.depth.toString());
if (options.recursive) args.push('--recursive');
return this.executeGitCommand('clone', args);
}
/**
* Create and archive repository backup
*/
async createBackup(
projectPath: string,
backupPath: string,
format: 'tar' | 'zip' = 'tar'
): Promise<GitCommandResult> {
const extension = format === 'zip' ? 'zip' : 'tar.gz';
const outputFile = `${backupPath}.${extension}`;
const formatArg = format === 'zip' ? 'zip' : 'tar.gz';
return this.executeGitCommand(
'archive',
['--format', formatArg, '--output', outputFile, 'HEAD'],
projectPath
);
}
/**
* Verify that a ref (branch/tag/commit) exists in the repository
*/
async verifyRef(projectPath: string, ref: string): Promise<GitCommandResult> {
return this.executeGitCommand('rev-parse', ['--verify', `${ref}^{commit}`], projectPath);
}
/**
* List branches
*/
async listBranches(
projectPath: string,
options: {
all?: boolean;
remote?: string;
} = {}
): Promise<GitCommandResult & { branches?: string[]; remoteBranches?: string[] }> {
const args = [];
if (options.all) args.push('-a');
if (options.remote) args.push('-r');
const result = await this.executeGitCommand('branch', args, projectPath);
if (result.success) {
const lines = result.stdout.split('\n').filter(line => line.trim());
const branches: string[] = [];
const remoteBranches: string[] = [];
for (const line of lines) {
const cleanLine = line.replace(/^\*?\s+/, '').trim();
if (cleanLine.startsWith('remotes/')) {
remoteBranches.push(cleanLine.replace('remotes/', ''));
} else if (cleanLine && !cleanLine.includes('->')) {
branches.push(cleanLine);
}
}
return {
...result,
branches: options.remote ? undefined : branches,
remoteBranches: options.all || options.remote ? remoteBranches : undefined
};
}
return result;
}
/**
* Create branch
*/
async createBranch(
projectPath: string,
branchName: string,
sourceBranch?: string
): Promise<GitCommandResult> {
const args = [branchName];
if (sourceBranch) args.push(sourceBranch);
return this.executeGitCommand('branch', args, projectPath);
}
/**
* Delete branch
*/
async deleteBranch(
projectPath: string,
branchName: string,
force: boolean = false
): Promise<GitCommandResult> {
const args = [force ? '-D' : '-d', branchName];
return this.executeGitCommand('branch', args, projectPath);
}
/**
* Checkout branch
*/
async checkoutBranch(
projectPath: string,
branchName: string,
options: {
create?: boolean;
force?: boolean;
} = {}
): Promise<GitCommandResult> {
const args = [branchName];
if (options.create) args.unshift('-b');
if (options.force) args.push('--force');
return this.executeGitCommand('checkout', args, projectPath);
}
/**
* Get branch information
*/
async getBranchInfo(
projectPath: string,
branchName: string
): Promise<GitCommandResult & {
exists?: boolean;
isRemote?: boolean;
lastCommit?: string;
upstream?: string;
ahead?: number;
behind?: number;
}> {
// Check if branch exists
const listResult = await this.listBranches(projectPath, { all: true });
if (!listResult.success) {
return listResult;
}
const allBranches = [...(listResult.branches || []), ...(listResult.remoteBranches || [])];
const exists = allBranches.some(branch =>
branch === branchName || branch.endsWith(`/${branchName}`)
);
if (!exists) {
return {
success: true,
stdout: '',
stderr: '',
exitCode: 0,
executionTime: 0,
command: `git branch info ${branchName}`,
exists: false
};
}
// Get last commit
const logResult = await this.executeGitCommand(
'log',
['-1', '--format=%H %s', branchName],
projectPath
);
// Get upstream info
const upstreamResult = await this.executeGitCommand(
'rev-parse',
['--abbrev-ref', `${branchName}@{upstream}`],
projectPath
);
// Get ahead/behind info if upstream exists
let ahead = 0;
let behind = 0;
if (upstreamResult.success) {
const countResult = await this.executeGitCommand(
'rev-list',
['--left-right', '--count', `${branchName}...${upstreamResult.stdout.trim()}`],
projectPath
);
if (countResult.success) {
const counts = countResult.stdout.trim().split('\t');
ahead = parseInt(counts[0]) || 0;
behind = parseInt(counts[1]) || 0;
}
}
const isRemote = (listResult.remoteBranches || []).some(branch =>
branch === branchName || branch.endsWith(`/${branchName}`)
);
return {
success: true,
stdout: logResult.stdout,
stderr: '',
exitCode: 0,
executionTime: logResult.executionTime,
command: `git branch info ${branchName}`,
exists: true,
isRemote,
lastCommit: logResult.success ? logResult.stdout.trim() : undefined,
upstream: upstreamResult.success ? upstreamResult.stdout.trim() : undefined,
ahead,
behind
};
}
/**
* Get branch commits
*/
async getBranchCommits(
projectPath: string,
branchName: string,
limit: number = 10
): Promise<GitCommandResult & { commits?: Array<{ hash: string; message: string; author: string; date: string }> }> {
const result = await this.executeGitCommand(
'log',
['-n', limit.toString(), '--format=%H|%s|%an|%ad', '--date=iso', branchName],
projectPath
);
if (result.success) {
const commits = result.stdout
.split('\n')
.filter(line => line.trim())
.map(line => {
const [hash, message, author, date] = line.split('|');
return { hash, message, author, date };
});
return {
...result,
commits
};
}
return result;
}
/**
* Merge branch
*/
async mergeBranch(
projectPath: string,
branchName: string,
options: {
force?: boolean;
noFf?: boolean;
squash?: boolean;
} = {}
): Promise<GitCommandResult & { mergeCommit?: string; conflicts?: string[] }> {
const args = [branchName];
if (options.force) args.push('--force');
if (options.noFf) args.push('--no-ff');
if (options.squash) args.push('--squash');
const result = await this.executeGitCommand('merge', args, projectPath);
if (result.success) {
// Get merge commit hash
const commitResult = await this.executeGitCommand('rev-parse', ['HEAD'], projectPath);
return {
...result,
mergeCommit: commitResult.success ? commitResult.stdout.trim() : undefined,
conflicts: []
};
} else {
// Check for conflicts
const statusResult = await this.getStatus(projectPath);
const conflicts = statusResult.parsedStatus?.conflicted || [];
return {
...result,
conflicts
};
}
}
/**
* Compare branches
*/
async compareBranches(
projectPath: string,
baseBranch: string,
compareBranch: string
): Promise<GitCommandResult & {
ahead?: number;
behind?: number;
commits?: Array<{ hash: string; message: string; author: string; date: string }>;
files?: Array<{ file: string; status: string; insertions: number; deletions: number }>;
insertions?: number;
deletions?: number;
}> {
// Get ahead/behind count
const countResult = await this.executeGitCommand(
'rev-list',
['--left-right', '--count', `${baseBranch}...${compareBranch}`],
projectPath
);
let behind = 0;
let ahead = 0;
if (countResult.success) {
const counts = countResult.stdout.trim().split('\t');
behind = parseInt(counts[0]) || 0;
ahead = parseInt(counts[1]) || 0;
}
// Get commits
const commitsResult = await this.executeGitCommand(
'log',
['--format=%H|%s|%an|%ad', '--date=iso', `${baseBranch}..${compareBranch}`],
projectPath
);
const commits = commitsResult.success
? commitsResult.stdout
.split('\n')
.filter(line => line.trim())
.map(line => {
const [hash, message, author, date] = line.split('|');
return { hash, message, author, date };
})
: [];
// Get file changes
const diffResult = await this.executeGitCommand(
'diff',
['--numstat', `${baseBranch}...${compareBranch}`],
projectPath
);
const files: Array<{ file: string; status: string; insertions: number; deletions: number }> = [];
let totalInsertions = 0;
let totalDeletions = 0;
if (diffResult.success) {
const lines = diffResult.stdout.split('\n').filter(line => line.trim());
for (const line of lines) {
const parts = line.split('\t');
if (parts.length >= 3) {
const insertions = parseInt(parts[0]) || 0;
const deletions = parseInt(parts[1]) || 0;
const file = parts[2];
files.push({
file,
status: insertions > 0 && deletions > 0 ? 'modified' : insertions > 0 ? 'added' : 'deleted',
insertions,
deletions
});
totalInsertions += insertions;
totalDeletions += deletions;
}
}
}
return {
success: true,
stdout: `${ahead} commits ahead, ${behind} commits behind`,
stderr: '',
exitCode: 0,
executionTime: countResult.executionTime + commitsResult.executionTime + diffResult.executionTime,
command: `git compare ${baseBranch}...${compareBranch}`,
ahead,
behind,
commits,
files,
insertions: totalInsertions,
deletions: totalDeletions
};
}
/**
* List tags
*/
async listTags(
projectPath: string,
options: {
pattern?: string;
sort?: string;
} = {}
): Promise<GitCommandResult & { tags?: string[] }> {
const args = ['tag'];
if (options.pattern) args.push('-l', options.pattern);
if (options.sort) args.push('--sort', options.sort);
const result = await this.executeGitCommand('tag', args.slice(1), projectPath);
if (result.success) {
const tags = result.stdout
.split('\n')
.map(line => line.trim())
.filter(line => line);
return {
...result,
tags
};
}
return result;
}
/**
* Create tag
*/
async createTag(
projectPath: string,
tagName: string,
options: {
message?: string;
commit?: string;
annotated?: boolean;
force?: boolean;
} = {}
): Promise<GitCommandResult> {
const args = [tagName];
if (options.force) args.unshift('-f');
if (options.annotated || options.message) {
args.unshift('-a');
if (options.message) {
args.push('-m', options.message);
}
}
if (options.commit) args.push(options.commit);
return this.executeGitCommand('tag', args, projectPath);
}
/**
* Delete tag
*/
async deleteTag(
projectPath: string,
tagName: string,
options: {
force?: boolean;
} = {}
): Promise<GitCommandResult> {
// Note: git tag -d doesn't have a --force option
// The force parameter is kept for API compatibility but ignored
const args = ['-d', tagName];
return this.executeGitCommand('tag', args, projectPath);
}
/**
* Delete remote tag
*/
async deleteRemoteTag(
projectPath: string,
remote: string,
tagName: string
): Promise<GitCommandResult> {
return this.executeGitCommand('push', [remote, '--delete', tagName], projectPath);
}
/**
* Get tag information
*/
async getTagInfo(
projectPath: string,
tagName: string
): Promise<GitCommandResult & {
exists?: boolean;
type?: 'lightweight' | 'annotated';
commit?: string;
message?: string;
tagger?: string;
date?: string;
}> {
// Check if tag exists
const listResult = await this.listTags(projectPath);
if (!listResult.success) {
return listResult;
}
const exists = (listResult.tags || []).includes(tagName);
if (!exists) {
return {
success: true,
stdout: '',
stderr: '',
exitCode: 0,
executionTime: 0,
command: `git tag info ${tagName}`,
exists: false
};
}
// Get tag object type
const typeResult = await this.executeGitCommand(
'cat-file',
['-t', tagName],
projectPath
);
const isAnnotated = typeResult.success && typeResult.stdout.trim() === 'tag';
// Get commit hash
const commitResult = await this.executeGitCommand(
'rev-list',
['-n', '1', tagName],
projectPath
);
let message = '';
let tagger = '';
let date = '';
if (isAnnotated) {
// Get annotated tag information
const showResult = await this.executeGitCommand(
'show',
['-s', '--format=%B%n---TAGGER---%n%an%n---DATE---%n%ad', '--date=iso', tagName],
projectPath
);
if (showResult.success) {
const parts = showResult.stdout.split('---TAGGER---');
if (parts.length > 1) {
message = parts[0].trim();
const taggerParts = parts[1].split('---DATE---');
if (taggerParts.length > 1) {
tagger = taggerParts[0].trim();
date = taggerParts[1].trim();
}
}
}
}
return {
success: true,
stdout: `Tag: ${tagName}`,
stderr: '',
exitCode: 0,
executionTime: typeResult.executionTime + commitResult.executionTime,
command: `git tag info ${tagName}`,
exists: true,
type: isAnnotated ? 'annotated' : 'lightweight',
commit: commitResult.success ? commitResult.stdout.trim() : undefined,
message: message || undefined,
tagger: tagger || undefined,
date: date || undefined
};
}
/**
* Get commit information
*/
async getCommitInfo(
projectPath: string,
commitHash: string
): Promise<GitCommandResult & {
hash?: string;
author?: string;
date?: string;
message?: string;
}> {
const result = await this.executeGitCommand(
'show',
['-s', '--format=%H|%an|%ad|%s', '--date=iso', commitHash],
projectPath
);
if (result.success) {
const [hash, author, date, message] = result.stdout.trim().split('|');
return {
...result,
hash,
author,
date,
message
};
}
return result;
}
/**
* Get Git configuration value
*/
async getConfig(
projectPath: string,
key: string,
options: {
global?: boolean;
local?: boolean;
system?: boolean;
} = {}
): Promise<GitCommandResult & { value?: string }> {
const args = ['config'];
if (options.global) args.push('--global');
if (options.local) args.push('--local');
if (options.system) args.push('--system');
args.push(key);
const result = await this.executeGitCommand('config', args.slice(1), projectPath);
return {
...result,
value: result.success ? result.stdout.trim() : undefined
};
}
/**
* Set Git configuration value
*/
async setConfig(
projectPath: string,
key: string,
value: string,
options: {
global?: boolean;
local?: boolean;
system?: boolean;
} = {}
): Promise<GitCommandResult> {
const args = ['config'];
if (options.global) args.push('--global');
if (options.local) args.push('--local');
if (options.system) args.push('--system');
args.push(key, value);
return this.executeGitCommand('config', args.slice(1), projectPath);
}
/**
* Unset Git configuration value
*/
async unsetConfig(
projectPath: string,
key: string,
options: {
global?: boolean;
local?: boolean;
system?: boolean;
} = {}
): Promise<GitCommandResult> {
const args = ['config'];
if (options.global) args.push('--global');
if (options.local) args.push('--local');
if (options.system) args.push('--system');
args.push('--unset', key);
return this.executeGitCommand('config', args.slice(1), projectPath);
}
/**
* List Git configuration
*/
async listConfig(
projectPath: string,
options: {
global?: boolean;
local?: boolean;
system?: boolean;
showOrigin?: boolean;
} = {}
): Promise<GitCommandResult & {
configs?: Array<{ key: string; value: string; origin?: string }>
}> {
const args = ['config'];
if (options.global) args.push('--global');
if (options.local) args.push('--local');
if (options.system) args.push('--system');
if (options.showOrigin) args.push('--show-origin');
args.push('--list');
const result = await this.executeGitCommand('config', args.slice(1), projectPath);
if (result.success) {
const configs: Array<{ key: string; value: string; origin?: string }> = [];
const lines = result.stdout.split('\n').filter(line => line.trim());
for (const line of lines) {
if (options.showOrigin) {
const match = line.match(/^([^\t]+)\t(.+?)=(.*)$/);
if (match) {
const [, origin, key, value] = match;
configs.push({ key, value, origin });
}
} else {
const equalIndex = line.indexOf('=');
if (equalIndex > 0) {
const key = line.substring(0, equalIndex);
const value = line.substring(equalIndex + 1);
configs.push({ key, value });
}
}
}
return {
...result,
configs
};
}
return result;
}
/**
* Edit Git configuration
*/
async editConfig(
projectPath: string,
options: {
global?: boolean;
local?: boolean;
system?: boolean;
} = {}
): Promise<GitCommandResult> {
const args = ['config'];
if (options.global) args.push('--global');
if (options.local) args.push('--local');
if (options.system) args.push('--system');
args.push('--edit');
return this.executeGitCommand('config', args.slice(1), projectPath);
}
/**
* Show Git configuration with details
*/
async showConfig(
projectPath: string,
key?: string,
options: {
global?: boolean;
local?: boolean;
system?: boolean;
showOrigin?: boolean;
showScope?: boolean;
} = {}
): Promise<GitCommandResult & {
configs?: Array<{
key: string;
value: string;
origin?: string;
scope?: string;
}>
}> {
const args = ['config'];
if (options.global) args.push('--global');
if (options.local) args.push('--local');
if (options.system) args.push('--system');
if (options.showOrigin) args.push('--show-origin');
if (options.showScope) args.push('--show-scope');
if (key) {
args.push(key);
} else {
args.push('--list');
}
const result = await this.executeGitCommand('config', args.slice(1), projectPath);
if (result.success) {
const configs: Array<{ key: string; value: string; origin?: string; scope?: string }> = [];
if (key) {
// Single key result
configs.push({
key,
value: result.stdout.trim(),
origin: options.showOrigin ? 'unknown' : undefined,
scope: options.showScope ? 'unknown' : undefined
});
} else {
// List result
const lines = result.stdout.split('\n').filter(line => line.trim());
for (const line of lines) {
if (options.showOrigin && options.showScope) {
const match = line.match(/^([^\t]+)\t([^\t]+)\t(.+?)=(.*)$/);
if (match) {
const [, origin, scope, key, value] = match;
configs.push({ key, value, origin, scope });
}
} else if (options.showOrigin) {
const match = line.match(/^([^\t]+)\t(.+?)=(.*)$/);
if (match) {
const [, origin, key, value] = match;
configs.push({ key, value, origin });
}
} else if (options.showScope) {
const match = line.match(/^([^\t]+)\t(.+?)=(.*)$/);
if (match) {
const [, scope, key, value] = match;
configs.push({ key, value, scope });
}
} else {
const equalIndex = line.indexOf('=');
if (equalIndex > 0) {
const key = line.substring(0, equalIndex);
const value = line.substring(equalIndex + 1);
configs.push({ key, value });
}
}
}
}
return {
...result,
configs
};
}
return result;
}
/**
* Parse Git status output
*/
private parseGitStatus(statusOutput: string): GitStatus {
const lines = statusOutput.split('\n').filter(line => line.trim());
const branchLine = lines[0];
// Parse branch info
let branch = 'main';
let ahead = 0;
let behind = 0;
if (branchLine.startsWith('## ')) {
const branchInfo = branchLine.substring(3);
const parts = branchInfo.split('...');
branch = parts[0];
if (parts[1]) {
const trackingInfo = parts[1];
const aheadMatch = trackingInfo.match(/ahead (\d+)/);
const behindMatch = trackingInfo.match(/behind (\d+)/);
if (aheadMatch) ahead = parseInt(aheadMatch[1]);
if (behindMatch) behind = parseInt(behindMatch[1]);
}
}
// Parse file status
const staged: string[] = [];
const unstaged: string[] = [];
const untracked: string[] = [];
const conflicted: string[] = [];
const modified: string[] = [];
const added: string[] = [];
for (let i = 1; i < lines.length; i++) {
const line = lines[i];
if (line.length < 3) continue;
const indexStatus = line[0];
const workTreeStatus = line[1];
const fileName = line.substring(3);
if (indexStatus === 'U' || workTreeStatus === 'U' ||
(indexStatus === 'A' && workTreeStatus === 'A') ||
(indexStatus === 'D' && workTreeStatus === 'D')) {
conflicted.push(fileName);
} else if (indexStatus !== ' ' && indexStatus !== '?') {
staged.push(fileName);
if (indexStatus === 'A') {
added.push(fileName);
} else if (indexStatus === 'M') {
modified.push(fileName);
}
} else if (workTreeStatus !== ' ' && workTreeStatus !== '?') {
unstaged.push(fileName);
if (workTreeStatus === 'M') {
modified.push(fileName);
}
} else if (indexStatus === '?' && workTreeStatus === '?') {
untracked.push(fileName);
}
}
return {
branch,
ahead,
behind,
staged,
unstaged,
untracked,
conflicted,
modified,
added,
clean: staged.length === 0 && unstaged.length === 0 && untracked.length === 0
};
}
/**
* Reset repository to specific commit/state
*/
async reset(
projectPath: string,
options: {
type: 'soft' | 'mixed' | 'hard';
commit?: string;
paths?: string[];
options?: {
keepIndex?: boolean;
noRefresh?: boolean;
quiet?: boolean;
merge?: boolean;
keep?: boolean;
};
}
): Promise<GitCommandResult> {
const args: string[] = [options.type];
// Add commit reference if specified
if (options.commit) {
args.push(options.commit);
}
// Add paths for mixed reset with specific files
if (options.type === 'mixed' && options.paths && options.paths.length > 0) {
args.push('--');
args.push(...options.paths);
}
// Add additional options
if (options.options) {
if (options.options.keepIndex) args.push('--keep-index');
if (options.options.noRefresh) args.push('--no-refresh');
if (options.options.quiet) args.push('--quiet');
if (options.options.merge) args.push('--merge');
if (options.options.keep) args.push('--keep');
}
return await this.executeGitCommand('reset', args, projectPath);
}
/**
* Analyze Git error and provide suggestions
*/
private analyzeGitError(result: CommandResult): GitCommandResult['gitError'] {
if (result.success) return undefined;
const stderr = result.stderr.toLowerCase();
const stdout = result.stdout.toLowerCase();
const errorText = `${stderr} ${stdout}`;
if (errorText.includes('not a git repository')) {
return {
type: 'not_a_repository',
suggestion: 'Initialize a Git repository with "git init" or navigate to an existing repository'
};
}
if (errorText.includes('not a valid object name')) {
return {
type: 'invalid_ref',
suggestion: 'The specified branch/tag/ref does not exist. Verify the ref name and try again.'
};
}
if (errorText.includes('authentication failed') || errorText.includes('permission denied')) {
return {
type: 'authentication',
suggestion: 'Check your Git credentials and ensure you have access to the repository'
};
}
if (errorText.includes('network') || errorText.includes('connection') || errorText.includes('timeout')) {
return {
type: 'network',
suggestion: 'Check your internet connection and repository URL'
};
}
if (errorText.includes('merge conflict') || errorText.includes('conflict')) {
return {
type: 'merge_conflict',
suggestion: 'Resolve merge conflicts and commit the changes'
};
}
if (errorText.includes('push cannot contain secrets') || errorText.includes('repository rule violations')) {
return {
type: 'authentication',
suggestion: 'Remove sensitive data from commit history using git reset or git filter-branch'
};
}
if (errorText.includes('fetch --all does not take a repository argument')) {
return {
type: 'invalid_ref',
suggestion: 'Use git fetch <remote> <branch> instead of git fetch --all <args>'
};
}
return {
type: 'unknown',
suggestion: 'Check the error message for more details'
};
}
}
// Export singleton instance
export const gitCommandExecutor = new GitCommandExecutor();