This commit is contained in:
@@ -43,9 +43,7 @@ export async function createWorktree(
|
|||||||
mainBranch: string
|
mainBranch: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const git = createGit(basePath);
|
const git = createGit(basePath);
|
||||||
|
|
||||||
await fs.ensureDir(workspacePath);
|
await fs.ensureDir(workspacePath);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await git.raw(['worktree', 'add', workspacePath, '-b', branchName]);
|
await git.raw(['worktree', 'add', workspacePath, '-b', branchName]);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -58,7 +56,6 @@ export async function createWorktree(
|
|||||||
|
|
||||||
export async function removeWorktree(basePath: string, workspacePath: string, branchName: string): Promise<void> {
|
export async function removeWorktree(basePath: string, workspacePath: string, branchName: string): Promise<void> {
|
||||||
const git = createGit(basePath);
|
const git = createGit(basePath);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await git.raw(['worktree', 'remove', workspacePath, '--force']);
|
await git.raw(['worktree', 'remove', workspacePath, '--force']);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -81,10 +78,8 @@ export async function deleteBranch(git: SimpleGit, branchName: string, force: bo
|
|||||||
export async function listWorktrees(basePath: string): Promise<{ path: string; branch: string }[]> {
|
export async function listWorktrees(basePath: string): Promise<{ path: string; branch: string }[]> {
|
||||||
const git = createGit(basePath);
|
const git = createGit(basePath);
|
||||||
const result = await git.raw(['worktree', 'list', '--porcelain']);
|
const result = await git.raw(['worktree', 'list', '--porcelain']);
|
||||||
|
|
||||||
const worktrees: { path: string; branch: string }[] = [];
|
const worktrees: { path: string; branch: string }[] = [];
|
||||||
const lines = result.split('\n').filter(Boolean);
|
const lines = result.split('\n').filter(Boolean);
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const match = line.match(/^worktree (.+)$/);
|
const match = line.match(/^worktree (.+)$/);
|
||||||
if (match) {
|
if (match) {
|
||||||
@@ -96,84 +91,35 @@ export async function listWorktrees(basePath: string): Promise<{ path: string; b
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return worktrees;
|
return worktrees;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getWorktreeStatus(workspacePath: string): Promise<WorkspaceChange> {
|
export async function getWorktreeStatus(workspacePath: string): Promise<WorkspaceChange> {
|
||||||
const git = createGit(workspacePath);
|
const git = createGit(workspacePath);
|
||||||
|
|
||||||
const status = await git.status();
|
const status = await git.status();
|
||||||
const branch = await getCurrentBranch(git);
|
const branch = await getCurrentBranch(git);
|
||||||
const agentName = extractAgentName(branch);
|
const agentName = extractAgentName(branch);
|
||||||
|
|
||||||
const changes: FileChange[] = [];
|
const changes: FileChange[] = [];
|
||||||
|
|
||||||
for (const file of status.created) {
|
for (const file of status.created) {
|
||||||
changes.push({
|
changes.push({ file, status: 'added', staged: true, author: 'agent', timestamp: new Date().toISOString(), diff: '' });
|
||||||
file,
|
|
||||||
status: 'added',
|
|
||||||
staged: true,
|
|
||||||
author: 'agent',
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
diff: ''
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const file of status.modified) {
|
for (const file of status.modified) {
|
||||||
const diff = await git.diff(['--no-color', file]);
|
const diff = await git.diff(['--no-color', file]);
|
||||||
changes.push({
|
changes.push({ file, status: 'modified', staged: status.staged.includes(file), author: 'agent', timestamp: new Date().toISOString(), diff });
|
||||||
file,
|
|
||||||
status: 'modified',
|
|
||||||
staged: status.staged.includes(file),
|
|
||||||
author: 'agent',
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
diff
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const file of status.deleted) {
|
for (const file of status.deleted) {
|
||||||
changes.push({
|
changes.push({ file, status: 'deleted', staged: status.staged.includes(file), author: 'agent', timestamp: new Date().toISOString(), diff: '' });
|
||||||
file,
|
|
||||||
status: 'deleted',
|
|
||||||
staged: status.staged.includes(file),
|
|
||||||
author: 'agent',
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
diff: ''
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
return { agentName, branch, path: workspacePath, changes, lastUpdated: new Date().toISOString(), uncommittedCount: status.modified.length + status.created.length + status.deleted.length };
|
||||||
return {
|
|
||||||
agentName,
|
|
||||||
branch,
|
|
||||||
path: workspacePath,
|
|
||||||
changes,
|
|
||||||
lastUpdated: new Date().toISOString(),
|
|
||||||
uncommittedCount: status.modified.length + status.created.length + status.deleted.length
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateDiff(
|
export async function generateDiff(basePath: string, workspacePath: string, agentName: string, comparedBranch: string = 'main'): Promise<DiffResult> {
|
||||||
basePath: string,
|
|
||||||
workspacePath: string,
|
|
||||||
agentName: string,
|
|
||||||
comparedBranch: string = 'main'
|
|
||||||
): Promise<DiffResult> {
|
|
||||||
const git = createGit(workspacePath);
|
const git = createGit(workspacePath);
|
||||||
const branch = await getCurrentBranch(git);
|
const branch = await getCurrentBranch(git);
|
||||||
|
|
||||||
const diffOutput = await git.diff(['--no-color', '--unified=3', comparedBranch]);
|
const diffOutput = await git.diff(['--no-color', '--unified=3', comparedBranch]);
|
||||||
|
|
||||||
const files = parseDiffOutput(diffOutput);
|
const files = parseDiffOutput(diffOutput);
|
||||||
const summary = calculateDiffSummary(files);
|
const summary = calculateDiffSummary(files);
|
||||||
|
return { agentName, branch, comparedBranch, files, summary };
|
||||||
return {
|
|
||||||
agentName,
|
|
||||||
branch,
|
|
||||||
comparedBranch,
|
|
||||||
files,
|
|
||||||
summary
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseDiffOutput(output: string): DiffFile[] {
|
export function parseDiffOutput(output: string): DiffFile[] {
|
||||||
@@ -183,239 +129,84 @@ export function parseDiffOutput(output: string): DiffFile[] {
|
|||||||
let currentChanges: DiffLine[] = [];
|
let currentChanges: DiffLine[] = [];
|
||||||
let oldLineNum: number | null = null;
|
let oldLineNum: number | null = null;
|
||||||
let newLineNum: number | null = null;
|
let newLineNum: number | null = null;
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const fileMatch = line.match(/^diff --git a\/(.+) b\/(.+)$/);
|
const fileMatch = line.match(/^diff --git a\/(.+) b\/(.+)$/);
|
||||||
if (fileMatch) {
|
if (fileMatch) {
|
||||||
if (currentFile) {
|
if (currentFile) { currentFile.changes = currentChanges; files.push(currentFile); }
|
||||||
currentFile.changes = currentChanges;
|
currentFile = { file: fileMatch[2] || fileMatch[1], oldFile: fileMatch[1] !== fileMatch[2] ? fileMatch[1] : undefined, changes: [], additions: 0, deletions: 0 };
|
||||||
files.push(currentFile);
|
currentChanges = []; oldLineNum = null; newLineNum = null; continue;
|
||||||
}
|
|
||||||
currentFile = {
|
|
||||||
file: fileMatch[2] || fileMatch[1],
|
|
||||||
oldFile: fileMatch[1] !== fileMatch[2] ? fileMatch[1] : undefined,
|
|
||||||
changes: [],
|
|
||||||
additions: 0,
|
|
||||||
deletions: 0
|
|
||||||
};
|
|
||||||
currentChanges = [];
|
|
||||||
oldLineNum = null;
|
|
||||||
newLineNum = null;
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const indexMatch = line.match(/^index [a-f0-9]+\.\.[a-f0-9]+/);
|
|
||||||
if (indexMatch && currentFile) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newFileMatch = line.match(/^new file mode .+$/);
|
|
||||||
if (newFileMatch && currentFile) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldFileMatch = line.match(/^deleted file mode .+$/);
|
|
||||||
if (oldFileMatch && currentFile) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hunkMatch = line.match(/^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@/);
|
const hunkMatch = line.match(/^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@/);
|
||||||
if (hunkMatch && currentFile) {
|
if (hunkMatch && currentFile) {
|
||||||
oldLineNum = parseInt(hunkMatch[1], 10);
|
oldLineNum = parseInt(hunkMatch[1], 10);
|
||||||
newLineNum = parseInt(hunkMatch[3], 10);
|
newLineNum = parseInt(hunkMatch[3], 10);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentFile) {
|
if (currentFile) {
|
||||||
let lineType: 'context' | 'added' | 'deleted' = 'context';
|
let lineType: 'context' | 'added' | 'deleted' = 'context';
|
||||||
|
if (line.startsWith('+') && !line.startsWith('+++')) { lineType = 'added'; currentFile.additions++; }
|
||||||
if (line.startsWith('+') && !line.startsWith('+++')) {
|
else if (line.startsWith('-') && !line.startsWith('---')) { lineType = 'deleted'; currentFile.deletions++; }
|
||||||
lineType = 'added';
|
currentChanges.push({ type: lineType, lineNumber: { old: line.startsWith('+') || line.startsWith(' ') ? oldLineNum : null, new: line.startsWith('-') || line.startsWith(' ') ? newLineNum : null }, content: line });
|
||||||
currentFile.additions++;
|
if (lineType !== 'added' && line !== '') oldLineNum = oldLineNum ? oldLineNum + 1 : null;
|
||||||
} else if (line.startsWith('-') && !line.startsWith('---')) {
|
if (lineType !== 'deleted' && line !== '') newLineNum = newLineNum ? newLineNum + 1 : null;
|
||||||
lineType = 'deleted';
|
|
||||||
currentFile.deletions++;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentChanges.push({
|
|
||||||
type: lineType,
|
|
||||||
lineNumber: {
|
|
||||||
old: line.startsWith('+') || line.startsWith(' ') ? oldLineNum : null,
|
|
||||||
new: line.startsWith('-') || line.startsWith(' ') ? newLineNum : null
|
|
||||||
},
|
|
||||||
content: line
|
|
||||||
});
|
|
||||||
|
|
||||||
if (lineType !== 'added' && line !== '') {
|
|
||||||
oldLineNum = oldLineNum ? oldLineNum + 1 : null;
|
|
||||||
}
|
|
||||||
if (lineType !== 'deleted' && line !== '') {
|
|
||||||
newLineNum = newLineNum ? newLineNum + 1 : null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (currentFile) { currentFile.changes = currentChanges; files.push(currentFile); }
|
||||||
if (currentFile) {
|
|
||||||
currentFile.changes = currentChanges;
|
|
||||||
files.push(currentFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateDiffSummary(files: DiffFile[]): { filesChanged: number; additions: number; deletions: number } {
|
function calculateDiffSummary(files: DiffFile[]): { filesChanged: number; additions: number; deletions: number } {
|
||||||
let additions = 0;
|
let additions = 0, deletions = 0;
|
||||||
let deletions = 0;
|
for (const file of files) { additions += file.additions; deletions += file.deletions; }
|
||||||
|
return { filesChanged: files.length, additions, deletions };
|
||||||
for (const file of files) {
|
|
||||||
additions += file.additions;
|
|
||||||
deletions += file.deletions;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
filesChanged: files.length,
|
|
||||||
additions,
|
|
||||||
deletions
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function mergeToMain(
|
export async function mergeToMain(basePath: string, workspacePath: string, agentName: string, mainBranch: string, message?: string, dryRun: boolean = false): Promise<MergeResult> {
|
||||||
basePath: string,
|
|
||||||
workspacePath: string,
|
|
||||||
agentName: string,
|
|
||||||
mainBranch: string,
|
|
||||||
message?: string,
|
|
||||||
dryRun: boolean = false
|
|
||||||
): Promise<MergeResult> {
|
|
||||||
const git = createGit(workspacePath);
|
|
||||||
const workspaceGit = createGit(workspacePath);
|
const workspaceGit = createGit(workspacePath);
|
||||||
const branch = await getCurrentBranch(workspaceGit);
|
const branch = await getCurrentBranch(workspaceGit);
|
||||||
|
const commitMessage = message || `Merge agent-${agentName} changes into ${mainBranch}\n\nAgent: ${agentName}\nBranch: ${branch}\nDate: ${new Date().toISOString()}`;
|
||||||
const commitMessage = message || `Merge agent-${agentName} changes into ${mainBranch}
|
if (dryRun) return { success: true, message: `[DRY RUN] Would merge ${branch} into ${mainBranch}`, commitSha: 'dry-run' };
|
||||||
|
|
||||||
Agent: ${agentName}
|
|
||||||
Branch: ${branch}
|
|
||||||
Date: ${new Date().toISOString()}
|
|
||||||
|
|
||||||
This commit was automatically generated by git-agent-sync`;
|
|
||||||
|
|
||||||
if (dryRun) {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: `[DRY RUN] Would merge ${branch} into ${mainBranch}`,
|
|
||||||
commitSha: 'dry-run'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
await workspaceGit.checkout(mainBranch);
|
await workspaceGit.checkout(mainBranch);
|
||||||
await workspaceGit.pull();
|
await workspaceGit.pull();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const mergeResult = await git.raw([
|
const git = createGit(workspacePath);
|
||||||
'merge',
|
const mergeResult = await git.raw(['merge', '--no-ff', '-m', commitMessage, branch]);
|
||||||
'--no-ff',
|
return { success: true, message: mergeResult, commitSha: '' };
|
||||||
'-m',
|
|
||||||
commitMessage,
|
|
||||||
branch
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: mergeResult,
|
|
||||||
commitSha: ''
|
|
||||||
};
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.message?.includes('conflict')) {
|
if (error.message?.includes('conflict')) {
|
||||||
const conflicts = await detectConflicts(workspacePath);
|
const conflicts = await detectConflicts(workspacePath);
|
||||||
return {
|
return { success: false, conflicts, error: 'Merge conflicts detected' };
|
||||||
success: false,
|
|
||||||
conflicts,
|
|
||||||
error: 'Merge conflicts detected'
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
throw new GitAgentSyncError(`Merge failed: ${error.message}`);
|
throw new GitAgentSyncError(`Merge failed: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function detectConflicts(workspacePath: string): Promise<ConflictInfo[]> {
|
async function detectConflicts(workspacePath: string): Promise<ConflictInfo[]> {
|
||||||
const git = createGit(workspacePath);
|
const git = createGit(workspacePath);
|
||||||
const status = await git.status();
|
const status = await git.status();
|
||||||
|
|
||||||
const conflicts: ConflictInfo[] = [];
|
const conflicts: ConflictInfo[] = [];
|
||||||
|
|
||||||
for (const file of status.conflicted) {
|
for (const file of status.conflicted) {
|
||||||
const content = await fs.readFile(path.join(workspacePath, file), 'utf-8');
|
const content = await fs.readFile(path.join(workspacePath, file), 'utf-8');
|
||||||
conflicts.push({
|
conflicts.push({ file, content });
|
||||||
file,
|
|
||||||
content
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return conflicts;
|
return conflicts;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function hasUncommittedChanges(workspacePath: string): Promise<boolean> {
|
export async function hasUncommittedChanges(workspacePath: string): Promise<boolean> {
|
||||||
const git = createGit(workspacePath);
|
const git = createGit(workspacePath);
|
||||||
const status = await git.status();
|
const status = await git.status();
|
||||||
return status.modified.length > 0 ||
|
return status.modified.length > 0 || status.created.length > 0 || status.deleted.length > 0;
|
||||||
status.created.length > 0 ||
|
|
||||||
status.deleted.length > 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLastCommitInfo(workspacePath: string): Promise<{ hash: string; message: string }> {
|
export async function getLastCommitInfo(workspacePath: string): Promise<{ hash: string; message: string }> {
|
||||||
const git = createGit(workspacePath);
|
const git = createGit(workspacePath);
|
||||||
const log = await git.log({ maxCount: 1 });
|
const log = await git.log({ maxCount: 1 });
|
||||||
|
if (log.latest) return { hash: log.latest.hash, message: log.latest.message || '' };
|
||||||
if (log.latest) {
|
return { hash: '', message: '' };
|
||||||
return {
|
|
||||||
hash: log.latest.hash,
|
|
||||||
message: log.latest.message || ''
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
hash: '',
|
|
||||||
message: ''
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
export function extractAgentName(branch: string): string { return branch.replace(/^agent-/, ''); }
|
||||||
export function extractAgentName(branch: string): string {
|
|
||||||
return branch.replace(/^agent-/, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function stashChanges(workspacePath: string, message: string): Promise<void> {
|
export async function stashChanges(workspacePath: string, message: string): Promise<void> {
|
||||||
const git = createGit(workspacePath);
|
const git = createGit(workspacePath);
|
||||||
await git.stash(['push', '-m', message]);
|
await git.stash(['push', '-m', message]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function applyStash(workspacePath: string, stashRef: string): Promise<void> {
|
|
||||||
const git = createGit(workspacePath);
|
|
||||||
await git.stash(['apply', stashRef]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getStashList(workspacePath: string): Promise<{ hash: string; message: string }[]> {
|
|
||||||
const git = createGit(workspacePath);
|
|
||||||
const stashList = await git.stash(['list']);
|
|
||||||
|
|
||||||
const stashes: { hash: string; message: string }[] = [];
|
|
||||||
const lines = stashList.split('\n').filter(Boolean);
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
const match = line.match(/^(\w+@.+): (.+)$/);
|
|
||||||
if (match) {
|
|
||||||
stashes.push({
|
|
||||||
hash: match[1],
|
|
||||||
message: match[2]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return stashes;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function exportChangesAsPatch(workspacePath: string, outputPath: string): Promise<void> {
|
export async function exportChangesAsPatch(workspacePath: string, outputPath: string): Promise<void> {
|
||||||
const git = createGit(workspacePath);
|
const git = createGit(workspacePath);
|
||||||
const diff = await git.diff(['--no-color', '--unified=3']);
|
const diff = await git.diff(['--no-color', '--unified=3']);
|
||||||
|
|
||||||
await fs.writeFile(outputPath, diff, 'utf-8');
|
await fs.writeFile(outputPath, diff, 'utf-8');
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user