ccmanager 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/App.js +2 -2
- package/dist/components/Configuration.js +11 -0
- package/dist/components/ConfigureWorktree.d.ts +6 -0
- package/dist/components/ConfigureWorktree.js +99 -0
- package/dist/components/NewWorktree.d.ts +1 -1
- package/dist/components/NewWorktree.js +59 -6
- package/dist/services/configurationManager.d.ts +3 -1
- package/dist/services/configurationManager.js +14 -0
- package/dist/services/worktreeService.d.ts +5 -1
- package/dist/services/worktreeService.js +101 -5
- package/dist/services/worktreeService.test.d.ts +1 -0
- package/dist/services/worktreeService.test.js +147 -0
- package/dist/types/index.d.ts +4 -5
- package/dist/types/index.js +0 -4
- package/dist/utils/worktreeUtils.d.ts +5 -0
- package/dist/utils/worktreeUtils.js +29 -0
- package/dist/utils/worktreeUtils.test.d.ts +1 -0
- package/dist/utils/worktreeUtils.test.js +74 -0
- package/package.json +1 -1
package/dist/components/App.js
CHANGED
|
@@ -102,11 +102,11 @@ const App = () => {
|
|
|
102
102
|
}
|
|
103
103
|
}, 50); // Small delay to ensure proper cleanup
|
|
104
104
|
};
|
|
105
|
-
const handleCreateWorktree = async (path, branch) => {
|
|
105
|
+
const handleCreateWorktree = async (path, branch, baseBranch) => {
|
|
106
106
|
setView('creating-worktree');
|
|
107
107
|
setError(null);
|
|
108
108
|
// Create the worktree
|
|
109
|
-
const result = worktreeService.createWorktree(path, branch);
|
|
109
|
+
const result = worktreeService.createWorktree(path, branch, baseBranch);
|
|
110
110
|
if (result.success) {
|
|
111
111
|
// Success - return to menu
|
|
112
112
|
handleReturnToMenu();
|
|
@@ -3,6 +3,7 @@ import { Box, Text } from 'ink';
|
|
|
3
3
|
import SelectInput from 'ink-select-input';
|
|
4
4
|
import ConfigureShortcuts from './ConfigureShortcuts.js';
|
|
5
5
|
import ConfigureHooks from './ConfigureHooks.js';
|
|
6
|
+
import ConfigureWorktree from './ConfigureWorktree.js';
|
|
6
7
|
const Configuration = ({ onComplete }) => {
|
|
7
8
|
const [view, setView] = useState('menu');
|
|
8
9
|
const menuItems = [
|
|
@@ -14,6 +15,10 @@ const Configuration = ({ onComplete }) => {
|
|
|
14
15
|
label: '🔧 Configure Status Hooks',
|
|
15
16
|
value: 'hooks',
|
|
16
17
|
},
|
|
18
|
+
{
|
|
19
|
+
label: '📁 Configure Worktree Settings',
|
|
20
|
+
value: 'worktree',
|
|
21
|
+
},
|
|
17
22
|
{
|
|
18
23
|
label: '← Back to Main Menu',
|
|
19
24
|
value: 'back',
|
|
@@ -29,6 +34,9 @@ const Configuration = ({ onComplete }) => {
|
|
|
29
34
|
else if (item.value === 'hooks') {
|
|
30
35
|
setView('hooks');
|
|
31
36
|
}
|
|
37
|
+
else if (item.value === 'worktree') {
|
|
38
|
+
setView('worktree');
|
|
39
|
+
}
|
|
32
40
|
};
|
|
33
41
|
const handleSubMenuComplete = () => {
|
|
34
42
|
setView('menu');
|
|
@@ -39,6 +47,9 @@ const Configuration = ({ onComplete }) => {
|
|
|
39
47
|
if (view === 'hooks') {
|
|
40
48
|
return React.createElement(ConfigureHooks, { onComplete: handleSubMenuComplete });
|
|
41
49
|
}
|
|
50
|
+
if (view === 'worktree') {
|
|
51
|
+
return React.createElement(ConfigureWorktree, { onComplete: handleSubMenuComplete });
|
|
52
|
+
}
|
|
42
53
|
return (React.createElement(Box, { flexDirection: "column" },
|
|
43
54
|
React.createElement(Box, { marginBottom: 1 },
|
|
44
55
|
React.createElement(Text, { bold: true, color: "green" }, "Configuration")),
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import SelectInput from 'ink-select-input';
|
|
4
|
+
import TextInput from 'ink-text-input';
|
|
5
|
+
import { configurationManager } from '../services/configurationManager.js';
|
|
6
|
+
import { shortcutManager } from '../services/shortcutManager.js';
|
|
7
|
+
const ConfigureWorktree = ({ onComplete }) => {
|
|
8
|
+
const worktreeConfig = configurationManager.getWorktreeConfig();
|
|
9
|
+
const [autoDirectory, setAutoDirectory] = useState(worktreeConfig.autoDirectory);
|
|
10
|
+
const [pattern, setPattern] = useState(worktreeConfig.autoDirectoryPattern || '../{branch}');
|
|
11
|
+
const [editMode, setEditMode] = useState('menu');
|
|
12
|
+
const [tempPattern, setTempPattern] = useState(pattern);
|
|
13
|
+
useInput((input, key) => {
|
|
14
|
+
if (editMode === 'menu' &&
|
|
15
|
+
shortcutManager.matchesShortcut('cancel', input, key)) {
|
|
16
|
+
onComplete();
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
const menuItems = [
|
|
20
|
+
{
|
|
21
|
+
label: `Auto Directory: ${autoDirectory ? '✅ Enabled' : '❌ Disabled'}`,
|
|
22
|
+
value: 'toggle',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
label: `Pattern: ${pattern}`,
|
|
26
|
+
value: 'pattern',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
label: '💾 Save Changes',
|
|
30
|
+
value: 'save',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
label: '← Cancel',
|
|
34
|
+
value: 'cancel',
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
const handleMenuSelect = (item) => {
|
|
38
|
+
switch (item.value) {
|
|
39
|
+
case 'toggle':
|
|
40
|
+
setAutoDirectory(!autoDirectory);
|
|
41
|
+
break;
|
|
42
|
+
case 'pattern':
|
|
43
|
+
setTempPattern(pattern);
|
|
44
|
+
setEditMode('pattern');
|
|
45
|
+
break;
|
|
46
|
+
case 'save':
|
|
47
|
+
// Save the configuration
|
|
48
|
+
configurationManager.setWorktreeConfig({
|
|
49
|
+
autoDirectory,
|
|
50
|
+
autoDirectoryPattern: pattern,
|
|
51
|
+
});
|
|
52
|
+
onComplete();
|
|
53
|
+
break;
|
|
54
|
+
case 'cancel':
|
|
55
|
+
onComplete();
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const handlePatternSubmit = (value) => {
|
|
60
|
+
if (value.trim()) {
|
|
61
|
+
setPattern(value.trim());
|
|
62
|
+
}
|
|
63
|
+
setEditMode('menu');
|
|
64
|
+
};
|
|
65
|
+
if (editMode === 'pattern') {
|
|
66
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
67
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
68
|
+
React.createElement(Text, { bold: true, color: "green" }, "Configure Directory Pattern")),
|
|
69
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
70
|
+
React.createElement(Text, null, "Enter the pattern for automatic directory generation:")),
|
|
71
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
72
|
+
React.createElement(Text, { dimColor: true },
|
|
73
|
+
"Available placeholders: ",
|
|
74
|
+
'{branch}',
|
|
75
|
+
" - full branch name")),
|
|
76
|
+
React.createElement(Box, null,
|
|
77
|
+
React.createElement(Text, { color: "cyan" }, '> '),
|
|
78
|
+
React.createElement(TextInput, { value: tempPattern, onChange: setTempPattern, onSubmit: handlePatternSubmit, placeholder: "../{branch}" })),
|
|
79
|
+
React.createElement(Box, { marginTop: 1 },
|
|
80
|
+
React.createElement(Text, { dimColor: true }, "Press Enter to save or Escape to cancel"))));
|
|
81
|
+
}
|
|
82
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
83
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
84
|
+
React.createElement(Text, { bold: true, color: "green" }, "Configure Worktree Settings")),
|
|
85
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
86
|
+
React.createElement(Text, { dimColor: true }, "Configure automatic worktree directory generation")),
|
|
87
|
+
autoDirectory && (React.createElement(Box, { marginBottom: 1 },
|
|
88
|
+
React.createElement(Text, null,
|
|
89
|
+
"Example: branch \"feature/my-feature\" \u2192 directory \"",
|
|
90
|
+
pattern.replace('{branch}', 'feature-my-feature'),
|
|
91
|
+
"\""))),
|
|
92
|
+
React.createElement(SelectInput, { items: menuItems, onSelect: handleMenuSelect, isFocused: true }),
|
|
93
|
+
React.createElement(Box, { marginTop: 1 },
|
|
94
|
+
React.createElement(Text, { dimColor: true },
|
|
95
|
+
"Press ",
|
|
96
|
+
shortcutManager.getShortcutDisplay('cancel'),
|
|
97
|
+
" to cancel without saving"))));
|
|
98
|
+
};
|
|
99
|
+
export default ConfigureWorktree;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
interface NewWorktreeProps {
|
|
3
|
-
onComplete: (path: string, branch: string) => void;
|
|
3
|
+
onComplete: (path: string, branch: string, baseBranch: string) => void;
|
|
4
4
|
onCancel: () => void;
|
|
5
5
|
}
|
|
6
6
|
declare const NewWorktree: React.FC<NewWorktreeProps>;
|
|
@@ -1,11 +1,30 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
2
|
import { Box, Text, useInput } from 'ink';
|
|
3
3
|
import TextInput from 'ink-text-input';
|
|
4
|
+
import SelectInput from 'ink-select-input';
|
|
4
5
|
import { shortcutManager } from '../services/shortcutManager.js';
|
|
6
|
+
import { configurationManager } from '../services/configurationManager.js';
|
|
7
|
+
import { generateWorktreeDirectory } from '../utils/worktreeUtils.js';
|
|
8
|
+
import { WorktreeService } from '../services/worktreeService.js';
|
|
5
9
|
const NewWorktree = ({ onComplete, onCancel }) => {
|
|
6
|
-
const
|
|
10
|
+
const worktreeConfig = configurationManager.getWorktreeConfig();
|
|
11
|
+
const isAutoDirectory = worktreeConfig.autoDirectory;
|
|
12
|
+
// Adjust initial step based on auto directory mode
|
|
13
|
+
const [step, setStep] = useState(isAutoDirectory ? 'branch' : 'path');
|
|
7
14
|
const [path, setPath] = useState('');
|
|
8
15
|
const [branch, setBranch] = useState('');
|
|
16
|
+
const [generatedPath, setGeneratedPath] = useState('');
|
|
17
|
+
// Initialize worktree service and load branches
|
|
18
|
+
const worktreeService = new WorktreeService();
|
|
19
|
+
const branches = worktreeService.getAllBranches();
|
|
20
|
+
const defaultBranch = worktreeService.getDefaultBranch();
|
|
21
|
+
// Create branch items with default branch first
|
|
22
|
+
const branchItems = [
|
|
23
|
+
{ label: `${defaultBranch} (default)`, value: defaultBranch },
|
|
24
|
+
...branches
|
|
25
|
+
.filter(br => br !== defaultBranch)
|
|
26
|
+
.map(br => ({ label: br, value: br })),
|
|
27
|
+
];
|
|
9
28
|
useInput((input, key) => {
|
|
10
29
|
if (shortcutManager.matchesShortcut('cancel', input, key)) {
|
|
11
30
|
onCancel();
|
|
@@ -20,18 +39,35 @@ const NewWorktree = ({ onComplete, onCancel }) => {
|
|
|
20
39
|
const handleBranchSubmit = (value) => {
|
|
21
40
|
if (value.trim()) {
|
|
22
41
|
setBranch(value.trim());
|
|
23
|
-
|
|
42
|
+
setStep('base-branch');
|
|
24
43
|
}
|
|
25
44
|
};
|
|
45
|
+
const handleBaseBranchSelect = (item) => {
|
|
46
|
+
if (isAutoDirectory) {
|
|
47
|
+
// Generate path from branch name
|
|
48
|
+
const autoPath = generateWorktreeDirectory(branch, worktreeConfig.autoDirectoryPattern);
|
|
49
|
+
onComplete(autoPath, branch, item.value);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
onComplete(path, branch, item.value);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
// Update generated path preview when branch changes in auto mode
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (isAutoDirectory && branch) {
|
|
58
|
+
const autoPath = generateWorktreeDirectory(branch, worktreeConfig.autoDirectoryPattern);
|
|
59
|
+
setGeneratedPath(autoPath);
|
|
60
|
+
}
|
|
61
|
+
}, [branch, isAutoDirectory, worktreeConfig.autoDirectoryPattern]);
|
|
26
62
|
return (React.createElement(Box, { flexDirection: "column" },
|
|
27
63
|
React.createElement(Box, { marginBottom: 1 },
|
|
28
64
|
React.createElement(Text, { bold: true, color: "green" }, "Create New Worktree")),
|
|
29
|
-
step === 'path' ? (React.createElement(Box, { flexDirection: "column" },
|
|
65
|
+
step === 'path' && !isAutoDirectory ? (React.createElement(Box, { flexDirection: "column" },
|
|
30
66
|
React.createElement(Box, { marginBottom: 1 },
|
|
31
67
|
React.createElement(Text, null, "Enter worktree path (relative to repository root):")),
|
|
32
68
|
React.createElement(Box, null,
|
|
33
69
|
React.createElement(Text, { color: "cyan" }, '> '),
|
|
34
|
-
React.createElement(TextInput, { value: path, onChange: setPath, onSubmit: handlePathSubmit, placeholder: "e.g., ../myproject-feature" })))) : (React.createElement(Box, { flexDirection: "column" },
|
|
70
|
+
React.createElement(TextInput, { value: path, onChange: setPath, onSubmit: handlePathSubmit, placeholder: "e.g., ../myproject-feature" })))) : step === 'branch' && !isAutoDirectory ? (React.createElement(Box, { flexDirection: "column" },
|
|
35
71
|
React.createElement(Box, { marginBottom: 1 },
|
|
36
72
|
React.createElement(Text, null,
|
|
37
73
|
"Enter branch name for worktree at ",
|
|
@@ -39,7 +75,24 @@ const NewWorktree = ({ onComplete, onCancel }) => {
|
|
|
39
75
|
":")),
|
|
40
76
|
React.createElement(Box, null,
|
|
41
77
|
React.createElement(Text, { color: "cyan" }, '> '),
|
|
42
|
-
React.createElement(TextInput, { value: branch, onChange: setBranch, onSubmit: handleBranchSubmit, placeholder: "e.g., feature/new-feature" })))),
|
|
78
|
+
React.createElement(TextInput, { value: branch, onChange: setBranch, onSubmit: handleBranchSubmit, placeholder: "e.g., feature/new-feature" })))) : (React.createElement(Box, { flexDirection: "column" },
|
|
79
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
80
|
+
React.createElement(Text, null, "Enter branch name (directory will be auto-generated):")),
|
|
81
|
+
React.createElement(Box, null,
|
|
82
|
+
React.createElement(Text, { color: "cyan" }, '> '),
|
|
83
|
+
React.createElement(TextInput, { value: branch, onChange: setBranch, onSubmit: handleBranchSubmit, placeholder: "e.g., feature/new-feature" })),
|
|
84
|
+
generatedPath && (React.createElement(Box, { marginTop: 1 },
|
|
85
|
+
React.createElement(Text, { dimColor: true },
|
|
86
|
+
"Worktree will be created at:",
|
|
87
|
+
' ',
|
|
88
|
+
React.createElement(Text, { color: "green" }, generatedPath)))))),
|
|
89
|
+
step === 'base-branch' && (React.createElement(Box, { flexDirection: "column" },
|
|
90
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
91
|
+
React.createElement(Text, null,
|
|
92
|
+
"Select base branch for ",
|
|
93
|
+
React.createElement(Text, { color: "cyan" }, branch),
|
|
94
|
+
":")),
|
|
95
|
+
React.createElement(SelectInput, { items: branchItems, onSelect: handleBaseBranchSelect, initialIndex: 0 }))),
|
|
43
96
|
React.createElement(Box, { marginTop: 1 },
|
|
44
97
|
React.createElement(Text, { dimColor: true },
|
|
45
98
|
"Press ",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ConfigurationData, StatusHookConfig, ShortcutConfig } from '../types/index.js';
|
|
1
|
+
import { ConfigurationData, StatusHookConfig, ShortcutConfig, WorktreeConfig } from '../types/index.js';
|
|
2
2
|
export declare class ConfigurationManager {
|
|
3
3
|
private configPath;
|
|
4
4
|
private legacyShortcutsPath;
|
|
@@ -13,5 +13,7 @@ export declare class ConfigurationManager {
|
|
|
13
13
|
setStatusHooks(hooks: StatusHookConfig): void;
|
|
14
14
|
getConfiguration(): ConfigurationData;
|
|
15
15
|
setConfiguration(config: ConfigurationData): void;
|
|
16
|
+
getWorktreeConfig(): WorktreeConfig;
|
|
17
|
+
setWorktreeConfig(worktreeConfig: WorktreeConfig): void;
|
|
16
18
|
}
|
|
17
19
|
export declare const configurationManager: ConfigurationManager;
|
|
@@ -63,6 +63,11 @@ export class ConfigurationManager {
|
|
|
63
63
|
if (!this.config.statusHooks) {
|
|
64
64
|
this.config.statusHooks = {};
|
|
65
65
|
}
|
|
66
|
+
if (!this.config.worktree) {
|
|
67
|
+
this.config.worktree = {
|
|
68
|
+
autoDirectory: false,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
66
71
|
}
|
|
67
72
|
migrateLegacyShortcuts() {
|
|
68
73
|
if (existsSync(this.legacyShortcutsPath)) {
|
|
@@ -111,5 +116,14 @@ export class ConfigurationManager {
|
|
|
111
116
|
this.config = config;
|
|
112
117
|
this.saveConfig();
|
|
113
118
|
}
|
|
119
|
+
getWorktreeConfig() {
|
|
120
|
+
return (this.config.worktree || {
|
|
121
|
+
autoDirectory: false,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
setWorktreeConfig(worktreeConfig) {
|
|
125
|
+
this.config.worktree = worktreeConfig;
|
|
126
|
+
this.saveConfig();
|
|
127
|
+
}
|
|
114
128
|
}
|
|
115
129
|
export const configurationManager = new ConfigurationManager();
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { Worktree } from '../types/index.js';
|
|
2
2
|
export declare class WorktreeService {
|
|
3
3
|
private rootPath;
|
|
4
|
+
private gitRootPath;
|
|
4
5
|
constructor(rootPath?: string);
|
|
6
|
+
private getGitRepositoryRoot;
|
|
5
7
|
getWorktrees(): Worktree[];
|
|
6
8
|
private getCurrentBranch;
|
|
7
9
|
isGitRepository(): boolean;
|
|
8
|
-
|
|
10
|
+
getDefaultBranch(): string;
|
|
11
|
+
getAllBranches(): string[];
|
|
12
|
+
createWorktree(worktreePath: string, branch: string, baseBranch: string): {
|
|
9
13
|
success: boolean;
|
|
10
14
|
error?: string;
|
|
11
15
|
};
|
|
@@ -9,7 +9,30 @@ export class WorktreeService {
|
|
|
9
9
|
writable: true,
|
|
10
10
|
value: void 0
|
|
11
11
|
});
|
|
12
|
+
Object.defineProperty(this, "gitRootPath", {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
configurable: true,
|
|
15
|
+
writable: true,
|
|
16
|
+
value: void 0
|
|
17
|
+
});
|
|
12
18
|
this.rootPath = rootPath || process.cwd();
|
|
19
|
+
// Get the actual git repository root for worktree operations
|
|
20
|
+
this.gitRootPath = this.getGitRepositoryRoot();
|
|
21
|
+
}
|
|
22
|
+
getGitRepositoryRoot() {
|
|
23
|
+
try {
|
|
24
|
+
// Get the common git directory
|
|
25
|
+
const gitCommonDir = execSync('git rev-parse --git-common-dir', {
|
|
26
|
+
cwd: this.rootPath,
|
|
27
|
+
encoding: 'utf8',
|
|
28
|
+
}).trim();
|
|
29
|
+
// The parent of .git is the actual repository root
|
|
30
|
+
return path.dirname(gitCommonDir);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Fallback to current directory if command fails
|
|
34
|
+
return this.rootPath;
|
|
35
|
+
}
|
|
13
36
|
}
|
|
14
37
|
getWorktrees() {
|
|
15
38
|
try {
|
|
@@ -79,8 +102,71 @@ export class WorktreeService {
|
|
|
79
102
|
isGitRepository() {
|
|
80
103
|
return existsSync(path.join(this.rootPath, '.git'));
|
|
81
104
|
}
|
|
82
|
-
|
|
105
|
+
getDefaultBranch() {
|
|
83
106
|
try {
|
|
107
|
+
// Try to get the default branch from origin
|
|
108
|
+
const defaultBranch = execSync("git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@'", {
|
|
109
|
+
cwd: this.rootPath,
|
|
110
|
+
encoding: 'utf8',
|
|
111
|
+
shell: '/bin/bash',
|
|
112
|
+
}).trim();
|
|
113
|
+
return defaultBranch || 'main';
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// Fallback to common default branch names
|
|
117
|
+
try {
|
|
118
|
+
execSync('git rev-parse --verify main', {
|
|
119
|
+
cwd: this.rootPath,
|
|
120
|
+
encoding: 'utf8',
|
|
121
|
+
});
|
|
122
|
+
return 'main';
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
try {
|
|
126
|
+
execSync('git rev-parse --verify master', {
|
|
127
|
+
cwd: this.rootPath,
|
|
128
|
+
encoding: 'utf8',
|
|
129
|
+
});
|
|
130
|
+
return 'master';
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return 'main';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
getAllBranches() {
|
|
139
|
+
try {
|
|
140
|
+
const output = execSync("git branch -a --format='%(refname:short)' | grep -v HEAD | sort -u", {
|
|
141
|
+
cwd: this.rootPath,
|
|
142
|
+
encoding: 'utf8',
|
|
143
|
+
shell: '/bin/bash',
|
|
144
|
+
});
|
|
145
|
+
const branches = output
|
|
146
|
+
.trim()
|
|
147
|
+
.split('\n')
|
|
148
|
+
.filter(branch => branch && !branch.startsWith('origin/'))
|
|
149
|
+
.map(branch => branch.trim());
|
|
150
|
+
// Also include remote branches without origin/ prefix
|
|
151
|
+
const remoteBranches = output
|
|
152
|
+
.trim()
|
|
153
|
+
.split('\n')
|
|
154
|
+
.filter(branch => branch.startsWith('origin/'))
|
|
155
|
+
.map(branch => branch.replace('origin/', ''));
|
|
156
|
+
// Merge and deduplicate
|
|
157
|
+
const allBranches = [...new Set([...branches, ...remoteBranches])];
|
|
158
|
+
return allBranches.filter(branch => branch);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
createWorktree(worktreePath, branch, baseBranch) {
|
|
165
|
+
try {
|
|
166
|
+
// Resolve the worktree path relative to the git repository root
|
|
167
|
+
const resolvedPath = path.isAbsolute(worktreePath)
|
|
168
|
+
? worktreePath
|
|
169
|
+
: path.join(this.gitRootPath, worktreePath);
|
|
84
170
|
// Check if branch exists
|
|
85
171
|
let branchExists = false;
|
|
86
172
|
try {
|
|
@@ -94,11 +180,16 @@ export class WorktreeService {
|
|
|
94
180
|
// Branch doesn't exist
|
|
95
181
|
}
|
|
96
182
|
// Create the worktree
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
183
|
+
let command;
|
|
184
|
+
if (branchExists) {
|
|
185
|
+
command = `git worktree add "${resolvedPath}" "${branch}"`;
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
// Create new branch from specified base branch
|
|
189
|
+
command = `git worktree add -b "${branch}" "${resolvedPath}" "${baseBranch}"`;
|
|
190
|
+
}
|
|
100
191
|
execSync(command, {
|
|
101
|
-
cwd: this.
|
|
192
|
+
cwd: this.gitRootPath, // Execute from git root to ensure proper resolution
|
|
102
193
|
encoding: 'utf8',
|
|
103
194
|
});
|
|
104
195
|
return { success: true };
|
|
@@ -179,6 +270,11 @@ export class WorktreeService {
|
|
|
179
270
|
cwd: sourceWorktree.path,
|
|
180
271
|
encoding: 'utf8',
|
|
181
272
|
});
|
|
273
|
+
// After rebase, merge the rebased source branch into target branch
|
|
274
|
+
execSync(`git merge --ff-only "${sourceBranch}"`, {
|
|
275
|
+
cwd: targetWorktree.path,
|
|
276
|
+
encoding: 'utf8',
|
|
277
|
+
});
|
|
182
278
|
}
|
|
183
279
|
else {
|
|
184
280
|
// Regular merge
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { WorktreeService } from './worktreeService.js';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
// Mock child_process module
|
|
5
|
+
vi.mock('child_process');
|
|
6
|
+
// Get the mocked function with proper typing
|
|
7
|
+
const mockedExecSync = vi.mocked(execSync);
|
|
8
|
+
describe('WorktreeService', () => {
|
|
9
|
+
let service;
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
vi.clearAllMocks();
|
|
12
|
+
// Mock git rev-parse --git-common-dir to return a predictable path
|
|
13
|
+
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
14
|
+
if (typeof cmd === 'string' && cmd === 'git rev-parse --git-common-dir') {
|
|
15
|
+
return '/fake/path/.git\n';
|
|
16
|
+
}
|
|
17
|
+
throw new Error('Command not mocked: ' + cmd);
|
|
18
|
+
});
|
|
19
|
+
service = new WorktreeService('/fake/path');
|
|
20
|
+
});
|
|
21
|
+
describe('getDefaultBranch', () => {
|
|
22
|
+
it('should return default branch from origin', () => {
|
|
23
|
+
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
24
|
+
if (typeof cmd === 'string') {
|
|
25
|
+
if (cmd === 'git rev-parse --git-common-dir') {
|
|
26
|
+
return '/fake/path/.git\n';
|
|
27
|
+
}
|
|
28
|
+
if (cmd.includes('symbolic-ref')) {
|
|
29
|
+
return 'main\n';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
throw new Error('Command not mocked: ' + cmd);
|
|
33
|
+
});
|
|
34
|
+
const result = service.getDefaultBranch();
|
|
35
|
+
expect(result).toBe('main');
|
|
36
|
+
expect(execSync).toHaveBeenCalledWith("git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@'", expect.objectContaining({
|
|
37
|
+
cwd: '/fake/path',
|
|
38
|
+
encoding: 'utf8',
|
|
39
|
+
shell: '/bin/bash',
|
|
40
|
+
}));
|
|
41
|
+
});
|
|
42
|
+
it('should fallback to main if origin HEAD fails', () => {
|
|
43
|
+
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
44
|
+
if (typeof cmd === 'string') {
|
|
45
|
+
if (cmd === 'git rev-parse --git-common-dir') {
|
|
46
|
+
return '/fake/path/.git\n';
|
|
47
|
+
}
|
|
48
|
+
if (cmd.includes('symbolic-ref')) {
|
|
49
|
+
throw new Error('No origin');
|
|
50
|
+
}
|
|
51
|
+
if (cmd.includes('rev-parse --verify main')) {
|
|
52
|
+
return 'hash';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
throw new Error('Not found');
|
|
56
|
+
});
|
|
57
|
+
const result = service.getDefaultBranch();
|
|
58
|
+
expect(result).toBe('main');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
describe('getAllBranches', () => {
|
|
62
|
+
it('should return all branches without duplicates', () => {
|
|
63
|
+
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
64
|
+
if (typeof cmd === 'string') {
|
|
65
|
+
if (cmd === 'git rev-parse --git-common-dir') {
|
|
66
|
+
return '/fake/path/.git\n';
|
|
67
|
+
}
|
|
68
|
+
if (cmd.includes('branch -a')) {
|
|
69
|
+
return `main
|
|
70
|
+
feature/test
|
|
71
|
+
origin/main
|
|
72
|
+
origin/feature/remote
|
|
73
|
+
origin/feature/test
|
|
74
|
+
`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
throw new Error('Command not mocked: ' + cmd);
|
|
78
|
+
});
|
|
79
|
+
const result = service.getAllBranches();
|
|
80
|
+
expect(result).toEqual(['main', 'feature/test', 'feature/remote']);
|
|
81
|
+
});
|
|
82
|
+
it('should return empty array on error', () => {
|
|
83
|
+
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
84
|
+
if (typeof cmd === 'string' &&
|
|
85
|
+
cmd === 'git rev-parse --git-common-dir') {
|
|
86
|
+
return '/fake/path/.git\n';
|
|
87
|
+
}
|
|
88
|
+
throw new Error('Git error');
|
|
89
|
+
});
|
|
90
|
+
const result = service.getAllBranches();
|
|
91
|
+
expect(result).toEqual([]);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
describe('createWorktree', () => {
|
|
95
|
+
it('should create worktree with base branch when branch does not exist', () => {
|
|
96
|
+
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
97
|
+
if (typeof cmd === 'string') {
|
|
98
|
+
if (cmd === 'git rev-parse --git-common-dir') {
|
|
99
|
+
return '/fake/path/.git\n';
|
|
100
|
+
}
|
|
101
|
+
if (cmd.includes('rev-parse --verify')) {
|
|
102
|
+
throw new Error('Branch not found');
|
|
103
|
+
}
|
|
104
|
+
return '';
|
|
105
|
+
}
|
|
106
|
+
throw new Error('Unexpected command');
|
|
107
|
+
});
|
|
108
|
+
const result = service.createWorktree('/path/to/worktree', 'new-feature', 'develop');
|
|
109
|
+
expect(result).toEqual({ success: true });
|
|
110
|
+
expect(execSync).toHaveBeenCalledWith('git worktree add -b "new-feature" "/path/to/worktree" "develop"', expect.any(Object));
|
|
111
|
+
});
|
|
112
|
+
it('should create worktree without base branch when branch exists', () => {
|
|
113
|
+
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
114
|
+
if (typeof cmd === 'string') {
|
|
115
|
+
if (cmd === 'git rev-parse --git-common-dir') {
|
|
116
|
+
return '/fake/path/.git\n';
|
|
117
|
+
}
|
|
118
|
+
if (cmd.includes('rev-parse --verify')) {
|
|
119
|
+
return 'hash';
|
|
120
|
+
}
|
|
121
|
+
return '';
|
|
122
|
+
}
|
|
123
|
+
throw new Error('Unexpected command');
|
|
124
|
+
});
|
|
125
|
+
const result = service.createWorktree('/path/to/worktree', 'existing-feature', 'main');
|
|
126
|
+
expect(result).toEqual({ success: true });
|
|
127
|
+
expect(execSync).toHaveBeenCalledWith('git worktree add "/path/to/worktree" "existing-feature"', expect.any(Object));
|
|
128
|
+
});
|
|
129
|
+
it('should create worktree from specified base branch when branch does not exist', () => {
|
|
130
|
+
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
131
|
+
if (typeof cmd === 'string') {
|
|
132
|
+
if (cmd === 'git rev-parse --git-common-dir') {
|
|
133
|
+
return '/fake/path/.git\n';
|
|
134
|
+
}
|
|
135
|
+
if (cmd.includes('rev-parse --verify')) {
|
|
136
|
+
throw new Error('Branch not found');
|
|
137
|
+
}
|
|
138
|
+
return '';
|
|
139
|
+
}
|
|
140
|
+
throw new Error('Unexpected command');
|
|
141
|
+
});
|
|
142
|
+
const result = service.createWorktree('/path/to/worktree', 'new-feature', 'main');
|
|
143
|
+
expect(result).toEqual({ success: true });
|
|
144
|
+
expect(execSync).toHaveBeenCalledWith('git worktree add -b "new-feature" "/path/to/worktree" "main"', expect.any(Object));
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
package/dist/types/index.d.ts
CHANGED
|
@@ -47,13 +47,12 @@ export interface StatusHookConfig {
|
|
|
47
47
|
busy?: StatusHook;
|
|
48
48
|
waiting_input?: StatusHook;
|
|
49
49
|
}
|
|
50
|
-
export interface
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
export interface WorktreeConfig {
|
|
51
|
+
autoDirectory: boolean;
|
|
52
|
+
autoDirectoryPattern?: string;
|
|
53
53
|
}
|
|
54
|
-
export declare const DEFAULT_TERMINAL_PADDING: TerminalPadding;
|
|
55
54
|
export interface ConfigurationData {
|
|
56
55
|
shortcuts?: ShortcutConfig;
|
|
57
56
|
statusHooks?: StatusHookConfig;
|
|
58
|
-
|
|
57
|
+
worktree?: WorktreeConfig;
|
|
59
58
|
}
|
package/dist/types/index.js
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
export function generateWorktreeDirectory(branchName, pattern) {
|
|
3
|
+
// Default pattern if not specified
|
|
4
|
+
const defaultPattern = '../{branch}';
|
|
5
|
+
const activePattern = pattern || defaultPattern;
|
|
6
|
+
// Sanitize branch name for filesystem
|
|
7
|
+
// Replace slashes with dashes, remove special characters
|
|
8
|
+
const sanitizedBranch = branchName
|
|
9
|
+
.replace(/\//g, '-') // Replace forward slashes with dashes
|
|
10
|
+
.replace(/[^a-zA-Z0-9-_.]/g, '') // Remove special characters except dash, dot, underscore
|
|
11
|
+
.replace(/^-+|-+$/g, '') // Remove leading/trailing dashes
|
|
12
|
+
.toLowerCase(); // Convert to lowercase for consistency
|
|
13
|
+
// Replace placeholders in pattern
|
|
14
|
+
const directory = activePattern
|
|
15
|
+
.replace('{branch}', sanitizedBranch)
|
|
16
|
+
.replace('{branch-name}', sanitizedBranch);
|
|
17
|
+
// Ensure the path is relative to the repository root
|
|
18
|
+
return path.normalize(directory);
|
|
19
|
+
}
|
|
20
|
+
export function extractBranchParts(branchName) {
|
|
21
|
+
const parts = branchName.split('/');
|
|
22
|
+
if (parts.length > 1) {
|
|
23
|
+
return {
|
|
24
|
+
prefix: parts[0],
|
|
25
|
+
name: parts.slice(1).join('/'),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return { name: branchName };
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { generateWorktreeDirectory, extractBranchParts, } from './worktreeUtils.js';
|
|
3
|
+
describe('generateWorktreeDirectory', () => {
|
|
4
|
+
describe('with default pattern', () => {
|
|
5
|
+
it('should generate directory with sanitized branch name', () => {
|
|
6
|
+
expect(generateWorktreeDirectory('feature/my-feature')).toBe('../feature-my-feature');
|
|
7
|
+
expect(generateWorktreeDirectory('bugfix/fix-123')).toBe('../bugfix-fix-123');
|
|
8
|
+
expect(generateWorktreeDirectory('release/v1.0.0')).toBe('../release-v1.0.0');
|
|
9
|
+
});
|
|
10
|
+
it('should handle branch names without slashes', () => {
|
|
11
|
+
expect(generateWorktreeDirectory('main')).toBe('../main');
|
|
12
|
+
expect(generateWorktreeDirectory('develop')).toBe('../develop');
|
|
13
|
+
expect(generateWorktreeDirectory('my-feature')).toBe('../my-feature');
|
|
14
|
+
});
|
|
15
|
+
it('should remove special characters', () => {
|
|
16
|
+
expect(generateWorktreeDirectory('feature/my@feature!')).toBe('../feature-myfeature');
|
|
17
|
+
expect(generateWorktreeDirectory('bugfix/#123')).toBe('../bugfix-123');
|
|
18
|
+
expect(generateWorktreeDirectory('release/v1.0.0-beta')).toBe('../release-v1.0.0-beta');
|
|
19
|
+
});
|
|
20
|
+
it('should handle edge cases', () => {
|
|
21
|
+
expect(generateWorktreeDirectory('//feature//')).toBe('../feature');
|
|
22
|
+
expect(generateWorktreeDirectory('-feature-')).toBe('../feature');
|
|
23
|
+
expect(generateWorktreeDirectory('FEATURE/UPPERCASE')).toBe('../feature-uppercase');
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
describe('with custom patterns', () => {
|
|
27
|
+
it('should use custom pattern with {branch} placeholder', () => {
|
|
28
|
+
expect(generateWorktreeDirectory('feature/my-feature', '../worktrees/{branch}')).toBe('../worktrees/feature-my-feature');
|
|
29
|
+
expect(generateWorktreeDirectory('bugfix/123', '/tmp/{branch}-wt')).toBe('/tmp/bugfix-123-wt');
|
|
30
|
+
});
|
|
31
|
+
it('should handle patterns without placeholders', () => {
|
|
32
|
+
expect(generateWorktreeDirectory('feature/test', '../fixed-directory')).toBe('../fixed-directory');
|
|
33
|
+
});
|
|
34
|
+
it('should normalize paths', () => {
|
|
35
|
+
expect(generateWorktreeDirectory('feature/test', '../foo/../bar/{branch}')).toBe('../bar/feature-test');
|
|
36
|
+
expect(generateWorktreeDirectory('feature/test', './worktrees/{branch}')).toBe('worktrees/feature-test');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
describe('extractBranchParts', () => {
|
|
41
|
+
it('should extract prefix and name from branch with slash', () => {
|
|
42
|
+
expect(extractBranchParts('feature/my-feature')).toEqual({
|
|
43
|
+
prefix: 'feature',
|
|
44
|
+
name: 'my-feature',
|
|
45
|
+
});
|
|
46
|
+
expect(extractBranchParts('bugfix/fix-123')).toEqual({
|
|
47
|
+
prefix: 'bugfix',
|
|
48
|
+
name: 'fix-123',
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
it('should handle branches with multiple slashes', () => {
|
|
52
|
+
expect(extractBranchParts('feature/user/profile-page')).toEqual({
|
|
53
|
+
prefix: 'feature',
|
|
54
|
+
name: 'user/profile-page',
|
|
55
|
+
});
|
|
56
|
+
expect(extractBranchParts('release/v1.0/final')).toEqual({
|
|
57
|
+
prefix: 'release',
|
|
58
|
+
name: 'v1.0/final',
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
it('should handle branches without slashes', () => {
|
|
62
|
+
expect(extractBranchParts('main')).toEqual({
|
|
63
|
+
name: 'main',
|
|
64
|
+
});
|
|
65
|
+
expect(extractBranchParts('develop')).toEqual({
|
|
66
|
+
name: 'develop',
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
it('should handle empty branch name', () => {
|
|
70
|
+
expect(extractBranchParts('')).toEqual({
|
|
71
|
+
name: '',
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|