takt worktreeのBugFix
This commit is contained in:
parent
91731981d3
commit
fabf4bcd27
185
src/__tests__/it-worktree-delete.test.ts
Normal file
185
src/__tests__/it-worktree-delete.test.ts
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
/**
|
||||||
|
* Integration test for worktree branch deletion
|
||||||
|
*
|
||||||
|
* Tests that worktree branches can be properly deleted,
|
||||||
|
* including cleanup of worktree directory and session file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { mkdirSync, writeFileSync, rmSync, existsSync } from 'node:fs';
|
||||||
|
import { join, resolve } from 'node:path';
|
||||||
|
import { execFileSync } from 'node:child_process';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { listTaktBranches } from '../infra/task/branchList.js';
|
||||||
|
import { deleteBranch } from '../features/tasks/list/taskActions.js';
|
||||||
|
|
||||||
|
describe('worktree branch deletion', () => {
|
||||||
|
let testDir: string;
|
||||||
|
let worktreeDir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Create temporary git repository
|
||||||
|
testDir = join(tmpdir(), `takt-test-${Date.now()}`);
|
||||||
|
mkdirSync(testDir, { recursive: true });
|
||||||
|
|
||||||
|
// Initialize git repo
|
||||||
|
execFileSync('git', ['init'], { cwd: testDir });
|
||||||
|
execFileSync('git', ['config', 'user.name', 'Test User'], { cwd: testDir });
|
||||||
|
execFileSync('git', ['config', 'user.email', 'test@example.com'], { cwd: testDir });
|
||||||
|
|
||||||
|
// Create initial commit
|
||||||
|
writeFileSync(join(testDir, 'README.md'), '# Test');
|
||||||
|
execFileSync('git', ['add', '.'], { cwd: testDir });
|
||||||
|
execFileSync('git', ['commit', '-m', 'Initial commit'], { cwd: testDir });
|
||||||
|
|
||||||
|
// Create .takt directory structure
|
||||||
|
const taktDir = join(testDir, '.takt');
|
||||||
|
mkdirSync(taktDir, { recursive: true });
|
||||||
|
mkdirSync(join(taktDir, 'worktree-sessions'), { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Cleanup
|
||||||
|
if (worktreeDir && existsSync(worktreeDir)) {
|
||||||
|
rmSync(worktreeDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
if (existsSync(testDir)) {
|
||||||
|
rmSync(testDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete worktree branch and cleanup files', () => {
|
||||||
|
// Create worktree
|
||||||
|
const branchSlug = '20260203T1000-test-deletion';
|
||||||
|
worktreeDir = join(tmpdir(), branchSlug);
|
||||||
|
execFileSync('git', ['clone', '--shared', testDir, worktreeDir]);
|
||||||
|
|
||||||
|
const branchName = `takt/${branchSlug}`;
|
||||||
|
execFileSync('git', ['checkout', '-b', branchName], { cwd: worktreeDir });
|
||||||
|
|
||||||
|
// Make a change
|
||||||
|
writeFileSync(join(worktreeDir, 'test.txt'), 'test content');
|
||||||
|
execFileSync('git', ['add', 'test.txt'], { cwd: worktreeDir });
|
||||||
|
execFileSync('git', ['commit', '-m', 'Test change'], { cwd: worktreeDir });
|
||||||
|
|
||||||
|
// Create worktree-session file
|
||||||
|
const resolvedPath = resolve(worktreeDir);
|
||||||
|
const sessionFilename = resolvedPath.replace(/[/\\:]/g, '-') + '.json';
|
||||||
|
const sessionPath = join(testDir, '.takt', 'worktree-sessions', sessionFilename);
|
||||||
|
const sessionData = {
|
||||||
|
agentSessions: {},
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
provider: 'claude',
|
||||||
|
};
|
||||||
|
writeFileSync(sessionPath, JSON.stringify(sessionData, null, 2));
|
||||||
|
|
||||||
|
// Verify branch is listed
|
||||||
|
const branchesBefore = listTaktBranches(testDir);
|
||||||
|
const foundBefore = branchesBefore.find(b => b.branch === branchName);
|
||||||
|
expect(foundBefore).toBeDefined();
|
||||||
|
expect(foundBefore?.worktreePath).toBe(worktreeDir);
|
||||||
|
|
||||||
|
// Verify worktree directory and session file exist
|
||||||
|
expect(existsSync(worktreeDir)).toBe(true);
|
||||||
|
expect(existsSync(sessionPath)).toBe(true);
|
||||||
|
|
||||||
|
// Delete branch
|
||||||
|
const result = deleteBranch(testDir, {
|
||||||
|
info: foundBefore!,
|
||||||
|
filesChanged: 1,
|
||||||
|
taskSlug: branchSlug,
|
||||||
|
originalInstruction: 'Test instruction',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify deletion succeeded
|
||||||
|
expect(result).toBe(true);
|
||||||
|
|
||||||
|
// Verify worktree directory was removed
|
||||||
|
expect(existsSync(worktreeDir)).toBe(false);
|
||||||
|
|
||||||
|
// Verify session file was removed
|
||||||
|
expect(existsSync(sessionPath)).toBe(false);
|
||||||
|
|
||||||
|
// Verify branch is no longer listed
|
||||||
|
const branchesAfter = listTaktBranches(testDir);
|
||||||
|
const foundAfter = branchesAfter.find(b => b.branch === branchName);
|
||||||
|
expect(foundAfter).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle deletion when worktree directory is already deleted', () => {
|
||||||
|
// Create worktree
|
||||||
|
const branchSlug = '20260203T1001-already-deleted';
|
||||||
|
worktreeDir = join(tmpdir(), branchSlug);
|
||||||
|
execFileSync('git', ['clone', '--shared', testDir, worktreeDir]);
|
||||||
|
|
||||||
|
const branchName = `takt/${branchSlug}`;
|
||||||
|
execFileSync('git', ['checkout', '-b', branchName], { cwd: worktreeDir });
|
||||||
|
|
||||||
|
// Create worktree-session file
|
||||||
|
const resolvedPath = resolve(worktreeDir);
|
||||||
|
const sessionFilename = resolvedPath.replace(/[/\\:]/g, '-') + '.json';
|
||||||
|
const sessionPath = join(testDir, '.takt', 'worktree-sessions', sessionFilename);
|
||||||
|
const sessionData = {
|
||||||
|
agentSessions: {},
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
writeFileSync(sessionPath, JSON.stringify(sessionData, null, 2));
|
||||||
|
|
||||||
|
// Manually delete worktree directory before deletion
|
||||||
|
rmSync(worktreeDir, { recursive: true, force: true });
|
||||||
|
|
||||||
|
// Delete branch (should not fail even though worktree is gone)
|
||||||
|
const result = deleteBranch(testDir, {
|
||||||
|
info: {
|
||||||
|
branch: branchName,
|
||||||
|
commit: 'worktree',
|
||||||
|
worktreePath: worktreeDir,
|
||||||
|
},
|
||||||
|
filesChanged: 0,
|
||||||
|
taskSlug: branchSlug,
|
||||||
|
originalInstruction: 'Test instruction',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify deletion succeeded
|
||||||
|
expect(result).toBe(true);
|
||||||
|
|
||||||
|
// Verify session file was still removed
|
||||||
|
expect(existsSync(sessionPath)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete regular (non-worktree) branches normally', () => {
|
||||||
|
// Create a regular local branch
|
||||||
|
const branchName = 'takt/20260203T1002-regular-branch';
|
||||||
|
execFileSync('git', ['checkout', '-b', branchName], { cwd: testDir });
|
||||||
|
|
||||||
|
// Make a change
|
||||||
|
writeFileSync(join(testDir, 'test.txt'), 'test content');
|
||||||
|
execFileSync('git', ['add', 'test.txt'], { cwd: testDir });
|
||||||
|
execFileSync('git', ['commit', '-m', 'Test change'], { cwd: testDir });
|
||||||
|
|
||||||
|
// Switch back to main
|
||||||
|
execFileSync('git', ['checkout', 'master'], { cwd: testDir });
|
||||||
|
|
||||||
|
// Verify branch exists
|
||||||
|
const branchesBefore = listTaktBranches(testDir);
|
||||||
|
const foundBefore = branchesBefore.find(b => b.branch === branchName);
|
||||||
|
expect(foundBefore).toBeDefined();
|
||||||
|
expect(foundBefore?.worktreePath).toBeUndefined();
|
||||||
|
|
||||||
|
// Delete branch
|
||||||
|
const result = deleteBranch(testDir, {
|
||||||
|
info: foundBefore!,
|
||||||
|
filesChanged: 1,
|
||||||
|
taskSlug: '20260203T1002-regular-branch',
|
||||||
|
originalInstruction: 'Test instruction',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify deletion succeeded
|
||||||
|
expect(result).toBe(true);
|
||||||
|
|
||||||
|
// Verify branch is no longer listed
|
||||||
|
const branchesAfter = listTaktBranches(testDir);
|
||||||
|
const foundAfter = branchesAfter.find(b => b.branch === branchName);
|
||||||
|
expect(foundAfter).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
131
src/__tests__/it-worktree-sessions.test.ts
Normal file
131
src/__tests__/it-worktree-sessions.test.ts
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
/**
|
||||||
|
* Integration test for worktree-sessions recognition in takt list
|
||||||
|
*
|
||||||
|
* Tests that branches created in isolated worktrees (shared clones)
|
||||||
|
* are properly recognized by `takt list` through worktree-sessions tracking.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { mkdirSync, writeFileSync, rmSync, existsSync } from 'node:fs';
|
||||||
|
import { join, resolve } from 'node:path';
|
||||||
|
import { execFileSync } from 'node:child_process';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { listTaktBranches } from '../infra/task/branchList.js';
|
||||||
|
|
||||||
|
describe('worktree-sessions recognition', () => {
|
||||||
|
let testDir: string;
|
||||||
|
let worktreeDir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Create temporary git repository
|
||||||
|
testDir = join(tmpdir(), `takt-test-${Date.now()}`);
|
||||||
|
mkdirSync(testDir, { recursive: true });
|
||||||
|
|
||||||
|
// Initialize git repo
|
||||||
|
execFileSync('git', ['init'], { cwd: testDir });
|
||||||
|
execFileSync('git', ['config', 'user.name', 'Test User'], { cwd: testDir });
|
||||||
|
execFileSync('git', ['config', 'user.email', 'test@example.com'], { cwd: testDir });
|
||||||
|
|
||||||
|
// Create initial commit
|
||||||
|
writeFileSync(join(testDir, 'README.md'), '# Test');
|
||||||
|
execFileSync('git', ['add', '.'], { cwd: testDir });
|
||||||
|
execFileSync('git', ['commit', '-m', 'Initial commit'], { cwd: testDir });
|
||||||
|
|
||||||
|
// Create .takt directory structure
|
||||||
|
const taktDir = join(testDir, '.takt');
|
||||||
|
mkdirSync(taktDir, { recursive: true });
|
||||||
|
mkdirSync(join(taktDir, 'worktree-sessions'), { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Cleanup
|
||||||
|
if (worktreeDir && existsSync(worktreeDir)) {
|
||||||
|
rmSync(worktreeDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
if (existsSync(testDir)) {
|
||||||
|
rmSync(testDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should recognize branches from worktree-sessions', () => {
|
||||||
|
// Simulate worktree creation (directory name includes timestamp-slug)
|
||||||
|
const branchSlug = '20260203T0900-test-feature';
|
||||||
|
worktreeDir = join(tmpdir(), branchSlug);
|
||||||
|
|
||||||
|
// Create shared clone
|
||||||
|
execFileSync('git', ['clone', '--shared', testDir, worktreeDir]);
|
||||||
|
|
||||||
|
// Create and checkout takt branch in worktree
|
||||||
|
const branchName = `takt/${branchSlug}`;
|
||||||
|
execFileSync('git', ['checkout', '-b', branchName], { cwd: worktreeDir });
|
||||||
|
|
||||||
|
// Make a change
|
||||||
|
writeFileSync(join(worktreeDir, 'test.txt'), 'test content');
|
||||||
|
execFileSync('git', ['add', 'test.txt'], { cwd: worktreeDir });
|
||||||
|
execFileSync('git', ['commit', '-m', 'Test change'], { cwd: worktreeDir });
|
||||||
|
|
||||||
|
// Create worktree-session file (using same encoding as encodeWorktreePath)
|
||||||
|
const resolvedPath = resolve(worktreeDir);
|
||||||
|
const sessionFilename = resolvedPath.replace(/[/\\:]/g, '-') + '.json';
|
||||||
|
const sessionPath = join(testDir, '.takt', 'worktree-sessions', sessionFilename);
|
||||||
|
const sessionData = {
|
||||||
|
agentSessions: {},
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
provider: 'claude',
|
||||||
|
};
|
||||||
|
writeFileSync(sessionPath, JSON.stringify(sessionData, null, 2));
|
||||||
|
|
||||||
|
// Test: listTaktBranches should find the worktree branch
|
||||||
|
const branches = listTaktBranches(testDir);
|
||||||
|
|
||||||
|
expect(branches.length).toBeGreaterThan(0);
|
||||||
|
const found = branches.find(b => b.branch === branchName);
|
||||||
|
expect(found).toBeDefined();
|
||||||
|
expect(found?.worktreePath).toBe(worktreeDir);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip worktree-sessions when worktree directory is deleted', () => {
|
||||||
|
// Create worktree-session file for non-existent directory
|
||||||
|
worktreeDir = '/nonexistent/path/20260203T0900-test';
|
||||||
|
const resolvedPath = resolve(worktreeDir);
|
||||||
|
const sessionFilename = resolvedPath.replace(/[/\\:]/g, '-') + '.json';
|
||||||
|
const sessionPath = join(testDir, '.takt', 'worktree-sessions', sessionFilename);
|
||||||
|
const sessionData = {
|
||||||
|
agentSessions: {},
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
writeFileSync(sessionPath, JSON.stringify(sessionData, null, 2));
|
||||||
|
|
||||||
|
// Test: listTaktBranches should not include the non-existent worktree
|
||||||
|
const branches = listTaktBranches(testDir);
|
||||||
|
|
||||||
|
const found = branches.find(b => b.worktreePath === worktreeDir);
|
||||||
|
expect(found).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract correct branch name from session filename', () => {
|
||||||
|
// Create worktree (directory name includes timestamp-slug)
|
||||||
|
const branchSlug = '20260203T0851-unify-debug-log';
|
||||||
|
worktreeDir = join(tmpdir(), branchSlug);
|
||||||
|
execFileSync('git', ['clone', '--shared', testDir, worktreeDir]);
|
||||||
|
|
||||||
|
const branchName = `takt/${branchSlug}`;
|
||||||
|
execFileSync('git', ['checkout', '-b', branchName], { cwd: worktreeDir });
|
||||||
|
|
||||||
|
// Create session file with proper path encoding
|
||||||
|
const resolvedPath = resolve(worktreeDir);
|
||||||
|
const sessionFilename = resolvedPath.replace(/[/\\:]/g, '-') + '.json';
|
||||||
|
const sessionPath = join(testDir, '.takt', 'worktree-sessions', sessionFilename);
|
||||||
|
const sessionData = {
|
||||||
|
agentSessions: {},
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
writeFileSync(sessionPath, JSON.stringify(sessionData, null, 2));
|
||||||
|
|
||||||
|
const branches = listTaktBranches(testDir);
|
||||||
|
|
||||||
|
const found = branches.find(b => b.branch === branchName);
|
||||||
|
expect(found).toBeDefined();
|
||||||
|
expect(found?.worktreePath).toBe(worktreeDir);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -6,6 +6,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { execFileSync, spawnSync } from 'node:child_process';
|
import { execFileSync, spawnSync } from 'node:child_process';
|
||||||
|
import { rmSync, existsSync, unlinkSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import {
|
import {
|
||||||
createTempCloneForBranch,
|
createTempCloneForBranch,
|
||||||
@ -25,6 +27,7 @@ import { executeTask } from '../execute/taskExecution.js';
|
|||||||
import type { TaskExecutionOptions } from '../execute/types.js';
|
import type { TaskExecutionOptions } from '../execute/types.js';
|
||||||
import { listWorkflows, getCurrentWorkflow } from '../../../infra/config/index.js';
|
import { listWorkflows, getCurrentWorkflow } from '../../../infra/config/index.js';
|
||||||
import { DEFAULT_WORKFLOW_NAME } from '../../../shared/constants.js';
|
import { DEFAULT_WORKFLOW_NAME } from '../../../shared/constants.js';
|
||||||
|
import { encodeWorktreePath } from '../../../infra/config/project/sessionStore.js';
|
||||||
|
|
||||||
const log = createLogger('list-tasks');
|
const log = createLogger('list-tasks');
|
||||||
|
|
||||||
@ -178,11 +181,34 @@ export function mergeBranch(projectDir: string, item: BranchListItem): boolean {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a branch (discard changes).
|
* Delete a branch (discard changes).
|
||||||
|
* For worktree branches, removes the worktree directory and session file.
|
||||||
*/
|
*/
|
||||||
export function deleteBranch(projectDir: string, item: BranchListItem): boolean {
|
export function deleteBranch(projectDir: string, item: BranchListItem): boolean {
|
||||||
const { branch } = item.info;
|
const { branch, worktreePath } = item.info;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// If this is a worktree branch, remove the worktree directory and session file
|
||||||
|
if (worktreePath) {
|
||||||
|
// Remove worktree directory if it exists
|
||||||
|
if (existsSync(worktreePath)) {
|
||||||
|
rmSync(worktreePath, { recursive: true, force: true });
|
||||||
|
log.info('Removed worktree directory', { worktreePath });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove worktree-session file
|
||||||
|
const encodedPath = encodeWorktreePath(worktreePath);
|
||||||
|
const sessionFile = join(projectDir, '.takt', 'worktree-sessions', `${encodedPath}.json`);
|
||||||
|
if (existsSync(sessionFile)) {
|
||||||
|
unlinkSync(sessionFile);
|
||||||
|
log.info('Removed worktree-session file', { sessionFile });
|
||||||
|
}
|
||||||
|
|
||||||
|
success(`Deleted worktree ${branch}`);
|
||||||
|
log.info('Worktree branch deleted', { branch, worktreePath });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For regular branches, use git branch -D
|
||||||
execFileSync('git', ['branch', '-D', branch], {
|
execFileSync('git', ['branch', '-D', branch], {
|
||||||
cwd: projectDir,
|
cwd: projectDir,
|
||||||
encoding: 'utf-8',
|
encoding: 'utf-8',
|
||||||
|
|||||||
@ -7,6 +7,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { execFileSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
|
import { readdirSync, existsSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
import { createLogger } from '../../shared/utils/index.js';
|
import { createLogger } from '../../shared/utils/index.js';
|
||||||
|
|
||||||
import type { BranchInfo, BranchListItem } from './types.js';
|
import type { BranchInfo, BranchListItem } from './types.js';
|
||||||
@ -49,20 +51,105 @@ export class BranchManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** List all takt-managed branches */
|
/** List all takt-managed branches (local + remote + worktree-sessions) */
|
||||||
listTaktBranches(projectDir: string): BranchInfo[] {
|
listTaktBranches(projectDir: string): BranchInfo[] {
|
||||||
try {
|
try {
|
||||||
const output = execFileSync(
|
// Get local branches
|
||||||
|
const localOutput = execFileSync(
|
||||||
'git', ['branch', '--list', 'takt/*', '--format=%(refname:short) %(objectname:short)'],
|
'git', ['branch', '--list', 'takt/*', '--format=%(refname:short) %(objectname:short)'],
|
||||||
{ cwd: projectDir, encoding: 'utf-8', stdio: 'pipe' },
|
{ cwd: projectDir, encoding: 'utf-8', stdio: 'pipe' },
|
||||||
);
|
);
|
||||||
return BranchManager.parseTaktBranches(output);
|
const localBranches = BranchManager.parseTaktBranches(localOutput);
|
||||||
|
|
||||||
|
// Get remote branches
|
||||||
|
const remoteOutput = execFileSync(
|
||||||
|
'git', ['branch', '-r', '--list', 'origin/takt/*', '--format=%(refname:short) %(objectname:short)'],
|
||||||
|
{ cwd: projectDir, encoding: 'utf-8', stdio: 'pipe' },
|
||||||
|
);
|
||||||
|
const remoteBranches = BranchManager.parseTaktBranches(remoteOutput)
|
||||||
|
.map(info => ({
|
||||||
|
...info,
|
||||||
|
branch: info.branch.replace(/^origin\//, ''), // Strip origin/ prefix
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Get branches from worktree-sessions (for isolated worktrees without remote)
|
||||||
|
const worktreeBranches = this.listWorktreeSessions(projectDir);
|
||||||
|
|
||||||
|
// Merge and deduplicate (local > remote > worktree-sessions)
|
||||||
|
const branchMap = new Map<string, BranchInfo>();
|
||||||
|
for (const info of worktreeBranches) {
|
||||||
|
branchMap.set(info.branch, info);
|
||||||
|
}
|
||||||
|
for (const info of remoteBranches) {
|
||||||
|
branchMap.set(info.branch, info);
|
||||||
|
}
|
||||||
|
for (const info of localBranches) {
|
||||||
|
branchMap.set(info.branch, info);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(branchMap.values());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('Failed to list takt branches', { error: String(err) });
|
log.error('Failed to list takt branches', { error: String(err) });
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** List branches from worktree-sessions directory */
|
||||||
|
private listWorktreeSessions(projectDir: string): BranchInfo[] {
|
||||||
|
const sessionsDir = join(projectDir, '.takt', 'worktree-sessions');
|
||||||
|
if (!existsSync(sessionsDir)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const files = readdirSync(sessionsDir);
|
||||||
|
const branches: BranchInfo[] = [];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
if (!file.endsWith('.json')) continue;
|
||||||
|
|
||||||
|
// Extract branch slug from filename using timestamp pattern
|
||||||
|
// Filename format: -path-to-parent-dir-{timestamp-slug}.json
|
||||||
|
const nameWithoutExt = file.slice(0, -5); // Remove .json
|
||||||
|
const match = nameWithoutExt.match(/(\d{8}T\d{4}-.+)$/);
|
||||||
|
if (!match || match.index === undefined || !match[1]) continue;
|
||||||
|
|
||||||
|
const branchSlug = match[1];
|
||||||
|
const branch = `${TAKT_BRANCH_PREFIX}${branchSlug}`;
|
||||||
|
|
||||||
|
// Extract parent directory path (everything before the branch slug)
|
||||||
|
// Remove trailing dash before converting dashes to slashes
|
||||||
|
let encodedPath = nameWithoutExt.slice(0, match.index);
|
||||||
|
if (encodedPath.endsWith('-')) {
|
||||||
|
encodedPath = encodedPath.slice(0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode parent directory path (dashes back to slashes)
|
||||||
|
const parentPath = encodedPath.replace(/-/g, '/');
|
||||||
|
|
||||||
|
// Construct full worktree path
|
||||||
|
const worktreePath = join(parentPath, branchSlug);
|
||||||
|
|
||||||
|
// Check if worktree directory still exists
|
||||||
|
if (!existsSync(worktreePath)) {
|
||||||
|
continue; // Skip if worktree was deleted
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use placeholder commit hash (worktree sessions don't track commit)
|
||||||
|
branches.push({
|
||||||
|
branch,
|
||||||
|
commit: 'worktree',
|
||||||
|
worktreePath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return branches;
|
||||||
|
} catch (err) {
|
||||||
|
log.error('Failed to list worktree sessions', { error: String(err) });
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Parse `git branch --list` formatted output into BranchInfo entries */
|
/** Parse `git branch --list` formatted output into BranchInfo entries */
|
||||||
static parseTaktBranches(output: string): BranchInfo[] {
|
static parseTaktBranches(output: string): BranchInfo[] {
|
||||||
const entries: BranchInfo[] = [];
|
const entries: BranchInfo[] = [];
|
||||||
@ -87,14 +174,24 @@ export class BranchManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Get the number of files changed between the default branch and a given branch */
|
/** Get the number of files changed between the default branch and a given branch */
|
||||||
getFilesChanged(cwd: string, defaultBranch: string, branch: string): number {
|
getFilesChanged(cwd: string, defaultBranch: string, branch: string, worktreePath?: string): number {
|
||||||
try {
|
try {
|
||||||
|
// If worktreePath is provided, use it for git diff (for worktree-sessions branches)
|
||||||
|
const gitCwd = worktreePath && existsSync(worktreePath) ? worktreePath : cwd;
|
||||||
|
|
||||||
|
log.debug('getFilesChanged', { gitCwd, defaultBranch, branch, worktreePath });
|
||||||
|
|
||||||
const output = execFileSync(
|
const output = execFileSync(
|
||||||
'git', ['diff', '--numstat', `${defaultBranch}...${branch}`],
|
'git', ['diff', '--numstat', `${defaultBranch}...${branch}`],
|
||||||
{ cwd, encoding: 'utf-8', stdio: 'pipe' },
|
{ cwd: gitCwd, encoding: 'utf-8', stdio: 'pipe' },
|
||||||
);
|
);
|
||||||
return output.trim().split('\n').filter(l => l.length > 0).length;
|
|
||||||
} catch {
|
const fileCount = output.trim().split('\n').filter(l => l.length > 0).length;
|
||||||
|
log.debug('getFilesChanged result', { fileCount, outputLength: output.length });
|
||||||
|
|
||||||
|
return fileCount;
|
||||||
|
} catch (err) {
|
||||||
|
log.error('getFilesChanged failed', { error: String(err), branch, worktreePath });
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -144,7 +241,7 @@ export class BranchManager {
|
|||||||
): BranchListItem[] {
|
): BranchListItem[] {
|
||||||
return branches.map(br => ({
|
return branches.map(br => ({
|
||||||
info: br,
|
info: br,
|
||||||
filesChanged: this.getFilesChanged(projectDir, defaultBranch, br.branch),
|
filesChanged: this.getFilesChanged(projectDir, defaultBranch, br.branch, br.worktreePath),
|
||||||
taskSlug: BranchManager.extractTaskSlug(br.branch),
|
taskSlug: BranchManager.extractTaskSlug(br.branch),
|
||||||
originalInstruction: this.getOriginalInstruction(projectDir, defaultBranch, br.branch),
|
originalInstruction: this.getOriginalInstruction(projectDir, defaultBranch, br.branch),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@ -46,6 +46,7 @@ export interface WorktreeResult {
|
|||||||
export interface BranchInfo {
|
export interface BranchInfo {
|
||||||
branch: string;
|
branch: string;
|
||||||
commit: string;
|
commit: string;
|
||||||
|
worktreePath?: string; // Path to worktree directory (for worktree-sessions branches)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Branch with list metadata */
|
/** Branch with list metadata */
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user