@humanu/orchestra 0.5.2 → 0.5.3
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/install.js +151 -102
- package/package.json +5 -4
- package/resources/api/git.sh +359 -0
- package/resources/api/tmux.sh +1266 -0
- package/resources/prebuilt/macos-arm64/orchestra +0 -0
- package/resources/prebuilt/macos-intel/orchestra +0 -0
- package/resources/scripts/commands.sh +227 -0
- package/resources/scripts/gw-bridge.sh +1184 -0
- package/resources/scripts/gw.sh +148 -0
- package/resources/scripts/gwr.sh +171 -0
package/install.js
CHANGED
|
@@ -3,86 +3,166 @@
|
|
|
3
3
|
const { execSync } = require('child_process');
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
|
-
const os = require('os');
|
|
7
6
|
|
|
8
|
-
const BINARY_NAME = '
|
|
7
|
+
const BINARY_NAME = 'orchestra';
|
|
8
|
+
const SHELL_SCRIPTS = ['gwr.sh', 'gw.sh', 'gw-bridge.sh', 'commands.sh'];
|
|
9
9
|
const SUPPORTED_PLATFORMS = {
|
|
10
|
-
'darwin-x64': 'macos-
|
|
10
|
+
'darwin-x64': 'macos-intel',
|
|
11
11
|
'darwin-arm64': 'macos-arm64',
|
|
12
12
|
'linux-x64': 'linux-x64',
|
|
13
13
|
'linux-arm64': 'linux-arm64'
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
const packageRoot = __dirname;
|
|
17
|
+
const resourcesDir = path.join(packageRoot, 'resources');
|
|
18
|
+
const distDir = path.join(packageRoot, 'dist');
|
|
19
|
+
const scriptsDir = path.join(resourcesDir, 'scripts');
|
|
20
|
+
const apiDir = path.join(resourcesDir, 'api');
|
|
21
|
+
const prebuiltDir = path.join(resourcesDir, 'prebuilt');
|
|
22
|
+
const projectRoot = path.resolve(packageRoot, '..', '..');
|
|
23
|
+
const hasLocalSource = fs.existsSync(path.join(projectRoot, 'gw-tui'));
|
|
24
|
+
|
|
25
|
+
function ensureDir(dir) {
|
|
26
|
+
if (!fs.existsSync(dir)) {
|
|
27
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function cleanDir(dir) {
|
|
32
|
+
if (fs.existsSync(dir)) {
|
|
33
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
34
|
+
}
|
|
35
|
+
ensureDir(dir);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getPlatformKey() {
|
|
39
|
+
const key = `${process.platform}-${process.arch}`;
|
|
40
|
+
const mapped = SUPPORTED_PLATFORMS[key];
|
|
41
|
+
if (!mapped) {
|
|
42
|
+
console.error(`Unsupported platform: ${key}`);
|
|
43
|
+
console.error(`Supported platforms: ${Object.keys(SUPPORTED_PLATFORMS).join(', ')}`);
|
|
21
44
|
process.exit(1);
|
|
22
45
|
}
|
|
23
|
-
return
|
|
46
|
+
return mapped;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function copyShellScripts() {
|
|
50
|
+
let sourceBase = scriptsDir;
|
|
51
|
+
if (!fs.existsSync(sourceBase)) {
|
|
52
|
+
if (!hasLocalSource) {
|
|
53
|
+
console.error('Shell scripts are missing from the package.');
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
sourceBase = projectRoot;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const script of SHELL_SCRIPTS) {
|
|
60
|
+
const sourcePath = path.join(sourceBase, script);
|
|
61
|
+
if (!fs.existsSync(sourcePath)) {
|
|
62
|
+
console.error(`Required script missing: ${sourcePath}`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
const destinationPath = path.join(distDir, script);
|
|
66
|
+
fs.copyFileSync(sourcePath, destinationPath);
|
|
67
|
+
fs.chmodSync(destinationPath, 0o755);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function copyApiScripts() {
|
|
72
|
+
let sourceBase = apiDir;
|
|
73
|
+
if (!fs.existsSync(sourceBase)) {
|
|
74
|
+
if (!hasLocalSource) {
|
|
75
|
+
console.error('API scripts are missing from the package.');
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
sourceBase = path.join(projectRoot, 'api');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
ensureDir(path.join(distDir, 'api'));
|
|
82
|
+
const apiFiles = fs.existsSync(sourceBase) ? fs.readdirSync(sourceBase) : [];
|
|
83
|
+
for (const file of apiFiles) {
|
|
84
|
+
if (!file.endsWith('.sh')) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const sourcePath = path.join(sourceBase, file);
|
|
88
|
+
const destinationPath = path.join(distDir, 'api', file);
|
|
89
|
+
fs.copyFileSync(sourcePath, destinationPath);
|
|
90
|
+
fs.chmodSync(destinationPath, 0o755);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function linkCompatibilityBinary(binaryPath) {
|
|
95
|
+
const gwTuiPath = path.join(distDir, 'gw-tui');
|
|
96
|
+
try {
|
|
97
|
+
fs.rmSync(gwTuiPath, { force: true });
|
|
98
|
+
} catch (err) {
|
|
99
|
+
if (err.code !== 'ENOENT') {
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
fs.symlinkSync(binaryPath, gwTuiPath);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
// Fallback for filesystems that disallow symlinks
|
|
108
|
+
fs.copyFileSync(binaryPath, gwTuiPath);
|
|
109
|
+
fs.chmodSync(gwTuiPath, 0o755);
|
|
110
|
+
}
|
|
24
111
|
}
|
|
25
112
|
|
|
26
|
-
function
|
|
27
|
-
const platform =
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// Check if prebuilt binary exists
|
|
32
|
-
if (fs.existsSync(prebuiltPath)) {
|
|
33
|
-
console.log(`Using prebuilt binary for ${platform}`);
|
|
34
|
-
fs.copyFileSync(prebuiltPath, binaryPath);
|
|
35
|
-
fs.chmodSync(binaryPath, 0o755);
|
|
36
|
-
return true;
|
|
113
|
+
function installPrebuiltBinary() {
|
|
114
|
+
const platform = getPlatformKey();
|
|
115
|
+
const source = path.join(prebuiltDir, platform, BINARY_NAME);
|
|
116
|
+
if (!fs.existsSync(source)) {
|
|
117
|
+
return false;
|
|
37
118
|
}
|
|
38
|
-
|
|
39
|
-
|
|
119
|
+
|
|
120
|
+
const destination = path.join(distDir, BINARY_NAME);
|
|
121
|
+
fs.copyFileSync(source, destination);
|
|
122
|
+
fs.chmodSync(destination, 0o755);
|
|
123
|
+
linkCompatibilityBinary(destination);
|
|
124
|
+
return true;
|
|
40
125
|
}
|
|
41
126
|
|
|
42
127
|
function buildFromSource() {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
128
|
+
if (!hasLocalSource) {
|
|
129
|
+
console.error('No prebuilt binary available and source tree not found.');
|
|
130
|
+
console.error('Clone the repository and run `cargo build --release` to build locally.');
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
|
|
46
134
|
try {
|
|
47
135
|
execSync('cargo --version', { stdio: 'ignore' });
|
|
48
136
|
} catch (error) {
|
|
49
|
-
console.error('Rust
|
|
137
|
+
console.error('Rust toolchain not found. Install Rust from https://rustup.rs/.');
|
|
50
138
|
process.exit(1);
|
|
51
139
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
stdio: 'inherit',
|
|
60
|
-
cwd: projectRoot
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
// Copy the built binary to dist
|
|
64
|
-
const sourcePath = path.join(projectRoot, 'gw-tui', 'target', 'release', BINARY_NAME);
|
|
65
|
-
const destPath = path.join(__dirname, 'dist', BINARY_NAME);
|
|
66
|
-
|
|
67
|
-
fs.copyFileSync(sourcePath, destPath);
|
|
68
|
-
fs.chmodSync(destPath, 0o755);
|
|
69
|
-
|
|
70
|
-
console.log('Successfully built from source');
|
|
71
|
-
} catch (error) {
|
|
72
|
-
console.error('Failed to build from source:', error.message);
|
|
140
|
+
|
|
141
|
+
console.log('Building from source...');
|
|
142
|
+
execSync('cargo build --release', { stdio: 'inherit', cwd: path.join(projectRoot, 'gw-tui') });
|
|
143
|
+
|
|
144
|
+
const builtBinary = path.join(projectRoot, 'gw-tui', 'target', 'release', 'gw-tui');
|
|
145
|
+
if (!fs.existsSync(builtBinary)) {
|
|
146
|
+
console.error('Build finished but gw-tui binary was not produced.');
|
|
73
147
|
process.exit(1);
|
|
74
148
|
}
|
|
149
|
+
|
|
150
|
+
const destination = path.join(distDir, BINARY_NAME);
|
|
151
|
+
fs.copyFileSync(builtBinary, destination);
|
|
152
|
+
fs.chmodSync(destination, 0o755);
|
|
153
|
+
linkCompatibilityBinary(destination);
|
|
75
154
|
}
|
|
76
155
|
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
156
|
+
function printShellWrapperInstructions(binaryPath) {
|
|
157
|
+
const distPathEscaped = distDir.replace(/\/g, '\\');
|
|
158
|
+
const binaryPathEscaped = binaryPath.replace(/\/g, '\\');
|
|
159
|
+
|
|
160
|
+
console.log('\n📝 Shell wrapper functions needed for directory switching:\n');
|
|
83
161
|
console.log(`# GW Orchestrator shell wrappers
|
|
84
162
|
gwr() {
|
|
85
|
-
local
|
|
163
|
+
local dist_path="${distPathEscaped}"
|
|
164
|
+
local bin_path="${binaryPathEscaped}"
|
|
165
|
+
local out="$(GW_TUI_BIN="$bin_path" bash "$dist_path/gwr.sh" "$@")"
|
|
86
166
|
local status=$?
|
|
87
167
|
local cd_line="$(echo "$out" | grep -m1 '^cd')"
|
|
88
168
|
[[ -n $cd_line ]] && eval "$cd_line"
|
|
@@ -91,65 +171,34 @@ gwr() {
|
|
|
91
171
|
}
|
|
92
172
|
|
|
93
173
|
gw() {
|
|
94
|
-
local
|
|
174
|
+
local dist_path="${distPathEscaped}"
|
|
175
|
+
local out="$(bash "$dist_path/gw.sh" "$@")"
|
|
95
176
|
local status=$?
|
|
96
177
|
local cd_line="$(echo "$out" | grep -m1 '^cd')"
|
|
97
178
|
[[ -n $cd_line ]] && eval "$cd_line"
|
|
98
179
|
echo "$out" | grep -v '^cd'
|
|
99
180
|
return $status
|
|
100
181
|
}`);
|
|
101
|
-
|
|
102
|
-
console.log('\nThen run: source ~/.bashrc (or source ~/.zshrc)');
|
|
182
|
+
console.log('\nAdd these to your ~/.bashrc or ~/.zshrc, then run: source ~/.bashrc (or source ~/.zshrc)\n');
|
|
103
183
|
}
|
|
104
184
|
|
|
105
185
|
function main() {
|
|
106
|
-
console.log('Installing
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const projectRoot = path.join(__dirname, '..', '..');
|
|
116
|
-
const shellScripts = ['gwr.sh', 'gw.sh', 'gw-bridge.sh', 'commands.sh'];
|
|
117
|
-
const apiDir = path.join(projectRoot, 'api');
|
|
118
|
-
|
|
119
|
-
shellScripts.forEach(script => {
|
|
120
|
-
const sourcePath = path.join(projectRoot, script);
|
|
121
|
-
const destPath = path.join(distDir, script);
|
|
122
|
-
if (fs.existsSync(sourcePath)) {
|
|
123
|
-
fs.copyFileSync(sourcePath, destPath);
|
|
124
|
-
fs.chmodSync(destPath, 0o755);
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
// Copy api directory
|
|
129
|
-
const distApiDir = path.join(distDir, 'api');
|
|
130
|
-
if (!fs.existsSync(distApiDir)) {
|
|
131
|
-
fs.mkdirSync(distApiDir, { recursive: true });
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (fs.existsSync(apiDir)) {
|
|
135
|
-
fs.readdirSync(apiDir).forEach(file => {
|
|
136
|
-
const sourcePath = path.join(apiDir, file);
|
|
137
|
-
const destPath = path.join(distApiDir, file);
|
|
138
|
-
fs.copyFileSync(sourcePath, destPath);
|
|
139
|
-
fs.chmodSync(destPath, 0o755);
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Try to use prebuilt binary, otherwise build from source
|
|
144
|
-
if (!downloadPrebuiltBinary()) {
|
|
186
|
+
console.log('Installing Orchestra...\n');
|
|
187
|
+
|
|
188
|
+
cleanDir(distDir);
|
|
189
|
+
|
|
190
|
+
copyShellScripts();
|
|
191
|
+
copyApiScripts();
|
|
192
|
+
|
|
193
|
+
const binaryInstalled = installPrebuiltBinary();
|
|
194
|
+
if (!binaryInstalled) {
|
|
145
195
|
buildFromSource();
|
|
146
196
|
}
|
|
147
|
-
|
|
197
|
+
|
|
198
|
+
const binaryPath = path.join(distDir, BINARY_NAME);
|
|
199
|
+
|
|
148
200
|
console.log('\n✅ Installation complete!');
|
|
149
|
-
|
|
150
|
-
// Setup shell wrappers
|
|
151
|
-
setupShellWrappers();
|
|
201
|
+
printShellWrapperInstructions(binaryPath);
|
|
152
202
|
}
|
|
153
203
|
|
|
154
|
-
|
|
155
|
-
main();
|
|
204
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanu/orchestra",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "AI-powered Git worktree and tmux session manager with modern TUI",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"git",
|
|
@@ -26,12 +26,13 @@
|
|
|
26
26
|
"author": "Human Unsupervised",
|
|
27
27
|
"main": "install.js",
|
|
28
28
|
"bin": {
|
|
29
|
-
"orchestra": "
|
|
30
|
-
"gwr": "
|
|
31
|
-
"gw": "
|
|
29
|
+
"orchestra": "install.js",
|
|
30
|
+
"gwr": "install.js",
|
|
31
|
+
"gw": "install.js"
|
|
32
32
|
},
|
|
33
33
|
"files": [
|
|
34
34
|
"bin/",
|
|
35
|
+
"resources/",
|
|
35
36
|
"install.js",
|
|
36
37
|
"README.md"
|
|
37
38
|
],
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
###############################################################################
|
|
4
|
+
# git.sh – Git Worktree API Module
|
|
5
|
+
# ---------------------------------------------------------------------------
|
|
6
|
+
# This module provides a centralized API for all Git operations used by
|
|
7
|
+
# gw.sh, commands.sh, and gw-bridge.sh. It abstracts Git worktree management
|
|
8
|
+
# and other Git operations into a clean, consistent interface.
|
|
9
|
+
###############################################################################
|
|
10
|
+
|
|
11
|
+
# --------------------------- Repository Information -------------------------
|
|
12
|
+
|
|
13
|
+
# Get the Git repository root directory
|
|
14
|
+
git_repo_root() {
|
|
15
|
+
git rev-parse --show-toplevel 2>/dev/null || true
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
# Check if current directory is in a Git repository
|
|
19
|
+
git_is_repo() {
|
|
20
|
+
git rev-parse --git-dir >/dev/null 2>&1
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# Get current absolute path (resolved)
|
|
24
|
+
git_current_path() {
|
|
25
|
+
pwd -P
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Convert branch name to slug (replace / with -)
|
|
29
|
+
git_branch_to_slug() {
|
|
30
|
+
echo "$1" | tr '/' '-'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# --------------------------- Branch Operations ------------------------------
|
|
34
|
+
|
|
35
|
+
# Check if a branch exists
|
|
36
|
+
git_branch_exists() {
|
|
37
|
+
local branch_name="$1"
|
|
38
|
+
git rev-parse --verify "$branch_name" >/dev/null 2>&1
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Get current branch name (or empty if detached HEAD)
|
|
42
|
+
git_current_branch() {
|
|
43
|
+
git symbolic-ref --short HEAD 2>/dev/null || true
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Get current commit SHA (short format)
|
|
47
|
+
git_current_commit_short() {
|
|
48
|
+
git rev-parse --short HEAD 2>/dev/null || true
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Delete a local branch (force delete)
|
|
52
|
+
git_delete_branch() {
|
|
53
|
+
local branch_name="$1"
|
|
54
|
+
git branch -D "$branch_name" 2>/dev/null || true
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# --------------------------- Worktree Operations ----------------------------
|
|
58
|
+
|
|
59
|
+
# Ensure ignore rules for worktrees directory exist in both .gitignore and .git/info/exclude
|
|
60
|
+
# Non-destructive and idempotent; creates files if needed
|
|
61
|
+
git_ensure_ignore_worktrees() {
|
|
62
|
+
local root; root="$(git_repo_root)"
|
|
63
|
+
[[ -z "$root" ]] && return 1
|
|
64
|
+
|
|
65
|
+
# .git/info/exclude (local-only)
|
|
66
|
+
local exclude_file="$root/.git/info/exclude"
|
|
67
|
+
mkdir -p "$root/.git/info" 2>/dev/null || true
|
|
68
|
+
if [[ -f "$exclude_file" ]]; then
|
|
69
|
+
if ! grep -q -E '^worktrees/(\s*)?$' "$exclude_file" 2>/dev/null; then
|
|
70
|
+
echo 'worktrees/' >> "$exclude_file"
|
|
71
|
+
fi
|
|
72
|
+
else
|
|
73
|
+
echo 'worktrees/' >> "$exclude_file"
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
# Top-level .gitignore (shared)
|
|
77
|
+
local gi_file="$root/.gitignore"
|
|
78
|
+
if [[ -f "$gi_file" ]]; then
|
|
79
|
+
if ! grep -q -E '^worktrees/(\s*)?$' "$gi_file" 2>/dev/null; then
|
|
80
|
+
echo 'worktrees/' >> "$gi_file"
|
|
81
|
+
fi
|
|
82
|
+
else
|
|
83
|
+
echo 'worktrees/' > "$gi_file"
|
|
84
|
+
fi
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# List all worktrees: returns lines with "path\tbranch\tsha_short"
|
|
88
|
+
git_list_worktrees() {
|
|
89
|
+
local root; root="$(git_repo_root)"
|
|
90
|
+
[[ -z "$root" ]] && return 1
|
|
91
|
+
|
|
92
|
+
(cd "$root" && git worktree list) | awk '
|
|
93
|
+
{
|
|
94
|
+
p=""; for (i=1; i<=NF-2; i++) p=(p==""?$i:p" " $i);
|
|
95
|
+
sha_full=$(NF-1); sha=substr(sha_full,1,7);
|
|
96
|
+
b=$NF; gsub(/^\[|\]$/, "", b);
|
|
97
|
+
print p "\t" b "\t" sha;
|
|
98
|
+
}'
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# Get branch name for a given worktree path
|
|
102
|
+
git_worktree_path_to_branch() {
|
|
103
|
+
local path="$1"
|
|
104
|
+
local root; root="$(git_repo_root)"
|
|
105
|
+
[[ -z "$root" ]] && return 1
|
|
106
|
+
|
|
107
|
+
(cd "$root" && git worktree list) | awk -v target="$path" '
|
|
108
|
+
{
|
|
109
|
+
p=""; for (i=1; i<=NF-2; i++) p=(p==""?$i:p" " $i);
|
|
110
|
+
b=$NF; gsub(/^\[|\]$/, "", b);
|
|
111
|
+
if (p==target) {print b; exit}
|
|
112
|
+
}'
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# Get worktree path for a given branch name
|
|
116
|
+
git_branch_to_worktree_path() {
|
|
117
|
+
local branch_name="$1"
|
|
118
|
+
local root; root="$(git_repo_root)"
|
|
119
|
+
[[ -z "$root" ]] && return 1
|
|
120
|
+
|
|
121
|
+
(cd "$root" && git worktree list) | awk -v target="[$branch_name]" '
|
|
122
|
+
{
|
|
123
|
+
p=""; for (i=1; i<=NF-2; i++) p=(p==""?$i:p" " $i);
|
|
124
|
+
if ($NF==target) {print p; exit}
|
|
125
|
+
}'
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# Create a new worktree with a new branch
|
|
129
|
+
git_create_worktree_with_branch() {
|
|
130
|
+
local branch_name="$1"
|
|
131
|
+
local worktree_path="$2"
|
|
132
|
+
local root; root="$(git_repo_root)"
|
|
133
|
+
[[ -z "$root" ]] && return 1
|
|
134
|
+
|
|
135
|
+
local relative_path="${worktree_path#$root/}"
|
|
136
|
+
(cd "$root" && git worktree add "$relative_path" -b "$branch_name" >&2)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# Create a new worktree from existing branch
|
|
140
|
+
git_create_worktree_from_branch() {
|
|
141
|
+
local branch_name="$1"
|
|
142
|
+
local worktree_path="$2"
|
|
143
|
+
local root; root="$(git_repo_root)"
|
|
144
|
+
[[ -z "$root" ]] && return 1
|
|
145
|
+
|
|
146
|
+
local relative_path="${worktree_path#$root/}"
|
|
147
|
+
(cd "$root" && git worktree add "$relative_path" "$branch_name" >&2)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
git_create_worktree_for_existing_branch() {
|
|
151
|
+
local branch_name="$1"
|
|
152
|
+
local root; root="$(git_repo_root)"
|
|
153
|
+
[[ -z "$root" ]] && return 1
|
|
154
|
+
|
|
155
|
+
git_branch_exists "$branch_name" || { echo "Branch does not exist: $branch_name" >&2; return 1; }
|
|
156
|
+
|
|
157
|
+
git_ensure_ignore_worktrees >/dev/null 2>&1 || true
|
|
158
|
+
|
|
159
|
+
local worktree_path; worktree_path="$(git_build_worktree_path "$branch_name")"
|
|
160
|
+
mkdir -p "$(dirname "$worktree_path")"
|
|
161
|
+
|
|
162
|
+
git_create_worktree_from_branch "$branch_name" "$worktree_path" || return 1
|
|
163
|
+
|
|
164
|
+
echo "$worktree_path"
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
git_create_worktree_from_remote_branch() {
|
|
168
|
+
local branch_name="$1"
|
|
169
|
+
local remote_name="${2:-origin}"
|
|
170
|
+
local root; root="$(git_repo_root)"
|
|
171
|
+
[[ -z "$root" ]] && return 1
|
|
172
|
+
|
|
173
|
+
git_ensure_ignore_worktrees >/dev/null 2>&1 || true
|
|
174
|
+
|
|
175
|
+
local worktree_path; worktree_path="$(git_build_worktree_path "$branch_name")"
|
|
176
|
+
mkdir -p "$(dirname "$worktree_path")"
|
|
177
|
+
|
|
178
|
+
local relative_path="${worktree_path#$root/}"
|
|
179
|
+
(cd "$root" && git worktree add "$relative_path" -b "$branch_name" "$remote_name/$branch_name" >&2) || return 1
|
|
180
|
+
|
|
181
|
+
echo "$worktree_path"
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
# Remove a worktree (with force fallback)
|
|
185
|
+
git_remove_worktree() {
|
|
186
|
+
local worktree_path="$1"
|
|
187
|
+
local root; root="$(git_repo_root)"
|
|
188
|
+
[[ -z "$root" ]] && return 1
|
|
189
|
+
|
|
190
|
+
local relative_path="${worktree_path#$root/}"
|
|
191
|
+
(cd "$root" && {
|
|
192
|
+
git worktree remove "$relative_path" 2>/dev/null || \
|
|
193
|
+
git worktree remove --force "$relative_path" 2>/dev/null || true
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
# Get Git status for current directory
|
|
198
|
+
git_status() {
|
|
199
|
+
git status "$@"
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
# Get short Git status for current directory
|
|
203
|
+
git_status_short() {
|
|
204
|
+
git status --short
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
# --------------------------- Worktree Path Utilities ------------------------
|
|
208
|
+
|
|
209
|
+
# Build standard worktree path for a branch
|
|
210
|
+
git_build_worktree_path() {
|
|
211
|
+
local branch_name="$1"
|
|
212
|
+
local root; root="$(git_repo_root)"
|
|
213
|
+
[[ -z "$root" ]] && return 1
|
|
214
|
+
|
|
215
|
+
local slug; slug="$(git_branch_to_slug "$branch_name")"
|
|
216
|
+
echo "$root/worktrees/$slug"
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
# Check if a worktree exists for a branch
|
|
220
|
+
git_worktree_exists_for_branch() {
|
|
221
|
+
local branch_name="$1"
|
|
222
|
+
local path; path="$(git_branch_to_worktree_path "$branch_name")"
|
|
223
|
+
[[ -n "$path" ]]
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
# Get or create worktree path for a branch
|
|
227
|
+
git_ensure_worktree_for_branch() {
|
|
228
|
+
local branch_name="$1"
|
|
229
|
+
local root; root="$(git_repo_root)"
|
|
230
|
+
[[ -z "$root" ]] && return 1
|
|
231
|
+
|
|
232
|
+
# Check if worktree already exists
|
|
233
|
+
local existing_path; existing_path="$(git_branch_to_worktree_path "$branch_name")"
|
|
234
|
+
if [[ -n "$existing_path" ]]; then
|
|
235
|
+
echo "$existing_path"
|
|
236
|
+
return 0
|
|
237
|
+
fi
|
|
238
|
+
|
|
239
|
+
# Create worktree if branch exists
|
|
240
|
+
if git_branch_exists "$branch_name"; then
|
|
241
|
+
# Ensure ignores before creating worktree
|
|
242
|
+
git_ensure_ignore_worktrees >/dev/null 2>&1 || true
|
|
243
|
+
local worktree_path; worktree_path="$(git_build_worktree_path "$branch_name")"
|
|
244
|
+
mkdir -p "$(dirname "$worktree_path")"
|
|
245
|
+
git_create_worktree_from_branch "$branch_name" "$worktree_path"
|
|
246
|
+
echo "$worktree_path"
|
|
247
|
+
return 0
|
|
248
|
+
fi
|
|
249
|
+
|
|
250
|
+
return 1
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
# Create new branch and worktree
|
|
254
|
+
git_create_branch_and_worktree() {
|
|
255
|
+
local branch_name="$1"
|
|
256
|
+
local root; root="$(git_repo_root)"
|
|
257
|
+
[[ -z "$root" ]] && return 1
|
|
258
|
+
|
|
259
|
+
# Ensure ignores before creating worktree
|
|
260
|
+
git_ensure_ignore_worktrees >/dev/null 2>&1 || true
|
|
261
|
+
local worktree_path; worktree_path="$(git_build_worktree_path "$branch_name")"
|
|
262
|
+
mkdir -p "$(dirname "$worktree_path")"
|
|
263
|
+
git_create_worktree_with_branch "$branch_name" "$worktree_path"
|
|
264
|
+
echo "$worktree_path"
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
# --------------------------- Validation Functions ---------------------------
|
|
268
|
+
|
|
269
|
+
# Validate that we're in a Git repository
|
|
270
|
+
git_require_repo() {
|
|
271
|
+
git_is_repo || { echo "❌ Not a git repository or git command not found." >&2; return 1; }
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
# Get repository root with validation
|
|
275
|
+
git_require_repo_root() {
|
|
276
|
+
local root; root="$(git_repo_root)"
|
|
277
|
+
[[ -z "$root" ]] && { echo "❌ Not a git repository" >&2; return 1; }
|
|
278
|
+
echo "$root"
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
# --------------------------- Merge/Rebase Helpers ----------------------------
|
|
282
|
+
|
|
283
|
+
# Detect the primary branch name (default remote HEAD), fall back to common names
|
|
284
|
+
# Echoes the branch name on success; returns non-zero on failure
|
|
285
|
+
git_primary_branch() {
|
|
286
|
+
local head_ref branch
|
|
287
|
+
# Try origin/HEAD symbolic ref
|
|
288
|
+
head_ref=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null || true)
|
|
289
|
+
if [[ -n "$head_ref" ]]; then
|
|
290
|
+
# refs/remotes/origin/main -> extract last component
|
|
291
|
+
branch="${head_ref##*/}"
|
|
292
|
+
[[ -n "$branch" ]] && { echo "$branch"; return 0; }
|
|
293
|
+
fi
|
|
294
|
+
# Try parsing `git remote show origin`
|
|
295
|
+
branch=$(git remote show origin 2>/dev/null | awk -F': ' '/HEAD branch/ {print $2; exit}')
|
|
296
|
+
if [[ -n "$branch" && "$branch" != "(unknown)" ]]; then
|
|
297
|
+
echo "$branch"; return 0
|
|
298
|
+
fi
|
|
299
|
+
# Fallback candidates
|
|
300
|
+
for cand in main master trunk; do
|
|
301
|
+
if git rev-parse --verify "$cand" >/dev/null 2>&1 || git rev-parse --verify "origin/$cand" >/dev/null 2>&1; then
|
|
302
|
+
echo "$cand"; return 0
|
|
303
|
+
fi
|
|
304
|
+
done
|
|
305
|
+
return 1
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
# Check if a worktree at path is clean (no changes)
|
|
309
|
+
# Usage: git_is_worktree_clean <path>
|
|
310
|
+
git_is_worktree_clean() {
|
|
311
|
+
local path="$1"
|
|
312
|
+
[[ -z "$path" ]] && return 1
|
|
313
|
+
local out
|
|
314
|
+
out=$(git -C "$path" status --porcelain 2>/dev/null || true)
|
|
315
|
+
[[ -z "$out" ]]
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
# Fetch from origin with prune for a given path
|
|
319
|
+
git_fetch_prune() {
|
|
320
|
+
local path="$1"
|
|
321
|
+
git -C "$path" fetch origin --prune 2>&1
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
# Fast-forward-only pull for the currently checked out branch at path
|
|
325
|
+
# Returns non-zero if not a fast-forward
|
|
326
|
+
# Usage: git_pull_ff_only <path>
|
|
327
|
+
git_pull_ff_only() {
|
|
328
|
+
local path="$1"
|
|
329
|
+
git -C "$path" pull --ff-only 2>&1
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
# Merge a ref into the currently checked out branch at target path
|
|
333
|
+
# Usage: git_merge_into <target_path> <ref>
|
|
334
|
+
git_merge_into() {
|
|
335
|
+
local target="$1"; local ref="$2"
|
|
336
|
+
git -C "$target" merge "$ref" 2>&1
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
# Rebase the current branch at target path onto upstream ref
|
|
340
|
+
# Usage: git_rebase_onto <target_path> <upstream_ref>
|
|
341
|
+
git_rebase_onto() {
|
|
342
|
+
local target="$1"; local upstream="$2"
|
|
343
|
+
git -C "$target" rebase "$upstream" 2>&1
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
# Squash-merge branch into the primary worktree path, then commit if changes staged
|
|
347
|
+
# Usage: git_squash_merge_into <primary_path> <branch>
|
|
348
|
+
git_squash_merge_into() {
|
|
349
|
+
local primary_path="$1"; local branch="$2"
|
|
350
|
+
git -C "$primary_path" merge --squash "$branch" 2>&1
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
# Ensure a primary worktree exists; echoes the path
|
|
354
|
+
# Usage: git_ensure_primary_worktree
|
|
355
|
+
git_ensure_primary_worktree() {
|
|
356
|
+
local primary
|
|
357
|
+
primary=$(git_primary_branch) || return 1
|
|
358
|
+
git_ensure_worktree_for_branch "$primary"
|
|
359
|
+
}
|