feat(03-02): install simple-git and create WorktreeManager adapter with CRUD
- Install simple-git dependency for git operations - Create SimpleGitWorktreeManager class implementing WorktreeManager port - Implement create(), remove(), list(), get() methods - EventBus optional dependency injection for emitting worktree events - Worktrees stored in .cw-worktrees directory
This commit is contained in:
64
package-lock.json
generated
64
package-lock.json
generated
@@ -15,6 +15,8 @@
|
|||||||
"commander": "^12.1.0",
|
"commander": "^12.1.0",
|
||||||
"drizzle-orm": "^0.45.1",
|
"drizzle-orm": "^0.45.1",
|
||||||
"execa": "^9.5.2",
|
"execa": "^9.5.2",
|
||||||
|
"nanoid": "^5.1.6",
|
||||||
|
"simple-git": "^3.30.0",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -945,6 +947,21 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@kwsites/file-exists": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@kwsites/promise-deferred": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.57.1",
|
"version": "4.57.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz",
|
||||||
@@ -1639,7 +1656,6 @@
|
|||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "^2.1.3"
|
"ms": "^2.1.3"
|
||||||
@@ -2710,14 +2726,12 @@
|
|||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "5.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz",
|
||||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
"integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -2726,10 +2740,10 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"nanoid": "bin/nanoid.cjs"
|
"nanoid": "bin/nanoid.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
"node": "^18 || >=20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/napi-build-utils": {
|
"node_modules/napi-build-utils": {
|
||||||
@@ -2899,6 +2913,25 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/postcss/node_modules/nanoid": {
|
||||||
|
"version": "3.3.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
|
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"nanoid": "bin/nanoid.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prebuild-install": {
|
"node_modules/prebuild-install": {
|
||||||
"version": "7.1.3",
|
"version": "7.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||||
@@ -3171,6 +3204,21 @@
|
|||||||
"simple-concat": "^1.0.0"
|
"simple-concat": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/simple-git": {
|
||||||
|
"version": "3.30.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.30.0.tgz",
|
||||||
|
"integrity": "sha512-q6lxyDsCmEal/MEGhP1aVyQ3oxnagGlBDOVSIB4XUVLl1iZh0Pah6ebC9V4xBap/RfgP2WlI8EKs0WS0rMEJHg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@kwsites/file-exists": "^1.1.1",
|
||||||
|
"@kwsites/promise-deferred": "^1.1.1",
|
||||||
|
"debug": "^4.4.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/steveukx/git-js?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map": {
|
"node_modules/source-map": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
|
|||||||
@@ -30,6 +30,8 @@
|
|||||||
"commander": "^12.1.0",
|
"commander": "^12.1.0",
|
||||||
"drizzle-orm": "^0.45.1",
|
"drizzle-orm": "^0.45.1",
|
||||||
"execa": "^9.5.2",
|
"execa": "^9.5.2",
|
||||||
|
"nanoid": "^5.1.6",
|
||||||
|
"simple-git": "^3.30.0",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* Git Module - Public API
|
* Git Module - Public API
|
||||||
*
|
*
|
||||||
* Exports the WorktreeManager port interface and types.
|
* Exports the WorktreeManager port interface, types, and adapters.
|
||||||
* All modules should import from this index file.
|
* All modules should import from this index file.
|
||||||
*
|
*
|
||||||
* Pattern follows EventBus module:
|
* Pattern follows EventBus module:
|
||||||
* - Port interface (WorktreeManager) is what consumers depend on
|
* - Port interface (WorktreeManager) is what consumers depend on
|
||||||
* - Adapter implementations will be added in future plans
|
* - Adapter implementation (SimpleGitWorktreeManager) uses simple-git
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Port interface (what consumers depend on)
|
// Port interface (what consumers depend on)
|
||||||
@@ -14,3 +14,6 @@ export type { WorktreeManager } from './types.js';
|
|||||||
|
|
||||||
// Domain types
|
// Domain types
|
||||||
export type { Worktree, WorktreeDiff, MergeResult } from './types.js';
|
export type { Worktree, WorktreeDiff, MergeResult } from './types.js';
|
||||||
|
|
||||||
|
// Adapters
|
||||||
|
export { SimpleGitWorktreeManager } from './manager.js';
|
||||||
|
|||||||
372
src/git/manager.ts
Normal file
372
src/git/manager.ts
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
/**
|
||||||
|
* SimpleGit WorktreeManager Adapter
|
||||||
|
*
|
||||||
|
* Implementation of WorktreeManager port interface using simple-git.
|
||||||
|
* This is the ADAPTER for the WorktreeManager PORT.
|
||||||
|
*
|
||||||
|
* Manages git worktrees for isolated agent workspaces.
|
||||||
|
* Each agent gets its own worktree to avoid file conflicts.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import path from 'node:path';
|
||||||
|
import { simpleGit, type SimpleGit } from 'simple-git';
|
||||||
|
import type { EventBus } from '../events/types.js';
|
||||||
|
import type {
|
||||||
|
WorktreeManager,
|
||||||
|
Worktree,
|
||||||
|
WorktreeDiff,
|
||||||
|
MergeResult,
|
||||||
|
} from './types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SimpleGit-based implementation of the WorktreeManager interface.
|
||||||
|
*
|
||||||
|
* Wraps simple-git to provide git worktree operations
|
||||||
|
* that conform to the WorktreeManager port interface.
|
||||||
|
*/
|
||||||
|
export class SimpleGitWorktreeManager implements WorktreeManager {
|
||||||
|
private git: SimpleGit;
|
||||||
|
private repoPath: string;
|
||||||
|
private worktreesDir: string;
|
||||||
|
private eventBus?: EventBus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new SimpleGitWorktreeManager.
|
||||||
|
*
|
||||||
|
* @param repoPath - Absolute path to the git repository
|
||||||
|
* @param eventBus - Optional EventBus for emitting git events
|
||||||
|
*/
|
||||||
|
constructor(repoPath: string, eventBus?: EventBus) {
|
||||||
|
this.repoPath = repoPath;
|
||||||
|
this.git = simpleGit(repoPath);
|
||||||
|
this.worktreesDir = path.join(repoPath, '.cw-worktrees');
|
||||||
|
this.eventBus = eventBus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new worktree for isolated agent work.
|
||||||
|
*
|
||||||
|
* Creates a new branch and worktree directory.
|
||||||
|
* The worktree will be ready for the agent to start working.
|
||||||
|
*/
|
||||||
|
async create(
|
||||||
|
id: string,
|
||||||
|
branch: string,
|
||||||
|
baseBranch: string = 'main'
|
||||||
|
): Promise<Worktree> {
|
||||||
|
const worktreePath = path.join(this.worktreesDir, id);
|
||||||
|
|
||||||
|
// Create worktree with new branch
|
||||||
|
// git worktree add -b <branch> <path> <base-branch>
|
||||||
|
await this.git.raw([
|
||||||
|
'worktree',
|
||||||
|
'add',
|
||||||
|
'-b',
|
||||||
|
branch,
|
||||||
|
worktreePath,
|
||||||
|
baseBranch,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const worktree: Worktree = {
|
||||||
|
id,
|
||||||
|
branch,
|
||||||
|
path: worktreePath,
|
||||||
|
isMainWorktree: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Emit event if eventBus provided
|
||||||
|
this.eventBus?.emit({
|
||||||
|
type: 'worktree:created',
|
||||||
|
timestamp: new Date(),
|
||||||
|
payload: {
|
||||||
|
worktreeId: id,
|
||||||
|
branch,
|
||||||
|
path: worktreePath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return worktree;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a worktree and optionally its branch.
|
||||||
|
*
|
||||||
|
* Cleans up the worktree directory and removes it from git's tracking.
|
||||||
|
*/
|
||||||
|
async remove(id: string): Promise<void> {
|
||||||
|
const worktree = await this.get(id);
|
||||||
|
if (!worktree) {
|
||||||
|
throw new Error(`Worktree not found: ${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const branch = worktree.branch;
|
||||||
|
|
||||||
|
// Remove worktree with force to handle any uncommitted changes
|
||||||
|
// git worktree remove <path> --force
|
||||||
|
await this.git.raw(['worktree', 'remove', worktree.path, '--force']);
|
||||||
|
|
||||||
|
// Emit event if eventBus provided
|
||||||
|
this.eventBus?.emit({
|
||||||
|
type: 'worktree:removed',
|
||||||
|
timestamp: new Date(),
|
||||||
|
payload: {
|
||||||
|
worktreeId: id,
|
||||||
|
branch,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all worktrees in the repository.
|
||||||
|
*
|
||||||
|
* Returns all worktrees including the main one.
|
||||||
|
*/
|
||||||
|
async list(): Promise<Worktree[]> {
|
||||||
|
// git worktree list --porcelain
|
||||||
|
const output = await this.git.raw(['worktree', 'list', '--porcelain']);
|
||||||
|
|
||||||
|
return this.parseWorktreeList(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific worktree by ID.
|
||||||
|
*
|
||||||
|
* Finds worktree by matching path ending with id.
|
||||||
|
*/
|
||||||
|
async get(id: string): Promise<Worktree | null> {
|
||||||
|
const worktrees = await this.list();
|
||||||
|
return worktrees.find((wt) => wt.path.endsWith(id)) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the diff/changes in a worktree.
|
||||||
|
*
|
||||||
|
* Shows what files have changed compared to HEAD.
|
||||||
|
*/
|
||||||
|
async diff(id: string): Promise<WorktreeDiff> {
|
||||||
|
const worktree = await this.get(id);
|
||||||
|
if (!worktree) {
|
||||||
|
throw new Error(`Worktree not found: ${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create git instance for the worktree directory
|
||||||
|
const worktreeGit = simpleGit(worktree.path);
|
||||||
|
|
||||||
|
// Get name-status diff against HEAD
|
||||||
|
// git diff --name-status HEAD
|
||||||
|
let diffOutput: string;
|
||||||
|
try {
|
||||||
|
diffOutput = await worktreeGit.raw(['diff', '--name-status', 'HEAD']);
|
||||||
|
} catch {
|
||||||
|
// If HEAD doesn't exist or other issues, return empty diff
|
||||||
|
diffOutput = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also get staged changes
|
||||||
|
let stagedOutput: string;
|
||||||
|
try {
|
||||||
|
stagedOutput = await worktreeGit.raw([
|
||||||
|
'diff',
|
||||||
|
'--name-status',
|
||||||
|
'--cached',
|
||||||
|
]);
|
||||||
|
} catch {
|
||||||
|
stagedOutput = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine and parse outputs
|
||||||
|
const combined = diffOutput + stagedOutput;
|
||||||
|
const files = this.parseDiffNameStatus(combined);
|
||||||
|
|
||||||
|
// Get summary
|
||||||
|
const fileCount = files.length;
|
||||||
|
const summary =
|
||||||
|
fileCount === 0 ? 'No changes' : `${fileCount} file(s) changed`;
|
||||||
|
|
||||||
|
return { files, summary };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge worktree changes into target branch.
|
||||||
|
*
|
||||||
|
* Attempts to merge the worktree's branch into the target branch.
|
||||||
|
* Returns conflict information if merge cannot be completed cleanly.
|
||||||
|
*/
|
||||||
|
async merge(id: string, targetBranch: string): Promise<MergeResult> {
|
||||||
|
const worktree = await this.get(id);
|
||||||
|
if (!worktree) {
|
||||||
|
throw new Error(`Worktree not found: ${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store current branch to restore later
|
||||||
|
const currentBranch = await this.git.revparse(['--abbrev-ref', 'HEAD']);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Checkout target branch in main repo
|
||||||
|
await this.git.checkout(targetBranch);
|
||||||
|
|
||||||
|
// Attempt merge with no-edit (no interactive editor)
|
||||||
|
await this.git.merge([worktree.branch, '--no-edit']);
|
||||||
|
|
||||||
|
// Emit success event
|
||||||
|
this.eventBus?.emit({
|
||||||
|
type: 'worktree:merged',
|
||||||
|
timestamp: new Date(),
|
||||||
|
payload: {
|
||||||
|
worktreeId: id,
|
||||||
|
sourceBranch: worktree.branch,
|
||||||
|
targetBranch,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Merged successfully',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
// Check if it's a merge conflict
|
||||||
|
const status = await this.git.status();
|
||||||
|
|
||||||
|
if (status.conflicted.length > 0) {
|
||||||
|
const conflicts = status.conflicted;
|
||||||
|
|
||||||
|
// Emit conflict event
|
||||||
|
this.eventBus?.emit({
|
||||||
|
type: 'worktree:conflict',
|
||||||
|
timestamp: new Date(),
|
||||||
|
payload: {
|
||||||
|
worktreeId: id,
|
||||||
|
sourceBranch: worktree.branch,
|
||||||
|
targetBranch,
|
||||||
|
conflictingFiles: conflicts,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Abort merge to clean up
|
||||||
|
await this.git.merge(['--abort']);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
conflicts,
|
||||||
|
message: 'Merge conflicts detected',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some other error occurred, rethrow
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
// Restore original branch if different from target
|
||||||
|
try {
|
||||||
|
const nowBranch = await this.git.revparse(['--abbrev-ref', 'HEAD']);
|
||||||
|
if (nowBranch.trim() !== currentBranch.trim()) {
|
||||||
|
await this.git.checkout(currentBranch.trim());
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore errors restoring branch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the porcelain output of git worktree list.
|
||||||
|
*/
|
||||||
|
private parseWorktreeList(output: string): Worktree[] {
|
||||||
|
const worktrees: Worktree[] = [];
|
||||||
|
const lines = output.trim().split('\n');
|
||||||
|
|
||||||
|
let currentWorktree: Partial<Worktree> = {};
|
||||||
|
let isFirst = true;
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith('worktree ')) {
|
||||||
|
// Start of a new worktree entry
|
||||||
|
if (currentWorktree.path) {
|
||||||
|
// Derive ID from path
|
||||||
|
const id = isFirst ? 'main' : path.basename(currentWorktree.path);
|
||||||
|
worktrees.push({
|
||||||
|
id,
|
||||||
|
branch: currentWorktree.branch || '',
|
||||||
|
path: currentWorktree.path,
|
||||||
|
isMainWorktree: isFirst,
|
||||||
|
});
|
||||||
|
isFirst = false;
|
||||||
|
}
|
||||||
|
currentWorktree = { path: line.substring('worktree '.length) };
|
||||||
|
} else if (line.startsWith('branch ')) {
|
||||||
|
// Branch reference (e.g., "branch refs/heads/main")
|
||||||
|
const branchRef = line.substring('branch '.length);
|
||||||
|
currentWorktree.branch = branchRef.replace('refs/heads/', '');
|
||||||
|
} else if (line.startsWith('HEAD ')) {
|
||||||
|
// Detached HEAD, skip
|
||||||
|
} else if (line === 'bare') {
|
||||||
|
// Bare worktree, skip
|
||||||
|
} else if (line === '') {
|
||||||
|
// Empty line between worktrees
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't forget the last worktree
|
||||||
|
if (currentWorktree.path) {
|
||||||
|
const id = isFirst ? 'main' : path.basename(currentWorktree.path);
|
||||||
|
worktrees.push({
|
||||||
|
id,
|
||||||
|
branch: currentWorktree.branch || '',
|
||||||
|
path: currentWorktree.path,
|
||||||
|
isMainWorktree: isFirst,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return worktrees;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the output of git diff --name-status.
|
||||||
|
*/
|
||||||
|
private parseDiffNameStatus(
|
||||||
|
output: string
|
||||||
|
): Array<{ path: string; status: 'added' | 'modified' | 'deleted' }> {
|
||||||
|
if (!output.trim()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = output.trim().split('\n');
|
||||||
|
const files: Array<{
|
||||||
|
path: string;
|
||||||
|
status: 'added' | 'modified' | 'deleted';
|
||||||
|
}> = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (!line.trim()) continue;
|
||||||
|
|
||||||
|
// Format: "M\tpath/to/file" or "A\tpath/to/file" or "D\tpath/to/file"
|
||||||
|
const match = line.match(/^([AMD])\t(.+)$/);
|
||||||
|
if (match) {
|
||||||
|
const [, statusLetter, filePath] = match;
|
||||||
|
|
||||||
|
// Skip duplicates (can happen when combining diff + cached)
|
||||||
|
if (seen.has(filePath)) continue;
|
||||||
|
seen.add(filePath);
|
||||||
|
|
||||||
|
let status: 'added' | 'modified' | 'deleted';
|
||||||
|
switch (statusLetter) {
|
||||||
|
case 'A':
|
||||||
|
status = 'added';
|
||||||
|
break;
|
||||||
|
case 'M':
|
||||||
|
status = 'modified';
|
||||||
|
break;
|
||||||
|
case 'D':
|
||||||
|
status = 'deleted';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
files.push({ path: filePath, status });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user