@solaqua/gji 0.6.1 → 0.7.0
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/README.md +26 -1
- package/dist/back.d.ts +1 -1
- package/dist/back.js +23 -17
- package/dist/clean.d.ts +1 -1
- package/dist/clean.js +44 -35
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +264 -164
- package/dist/completion.js +3 -3
- package/dist/config-command.js +5 -5
- package/dist/config.js +41 -35
- package/dist/conflict.d.ts +1 -1
- package/dist/conflict.js +14 -6
- package/dist/editor.js +29 -9
- package/dist/file-sync.d.ts +1 -0
- package/dist/file-sync.js +15 -11
- package/dist/git.d.ts +1 -1
- package/dist/git.js +21 -19
- package/dist/gji-bundle.mjs +1709 -850
- package/dist/go.d.ts +2 -2
- package/dist/go.js +39 -26
- package/dist/headless.js +1 -1
- package/dist/history-command.js +3 -3
- package/dist/history.js +12 -12
- package/dist/hooks.js +16 -16
- package/dist/index.js +13 -9
- package/dist/init.d.ts +2 -2
- package/dist/init.js +106 -94
- package/dist/install-prompt.d.ts +3 -3
- package/dist/install-prompt.js +46 -28
- package/dist/ls.d.ts +2 -2
- package/dist/ls.js +29 -29
- package/dist/new.d.ts +2 -2
- package/dist/new.js +96 -81
- package/dist/open.d.ts +2 -2
- package/dist/open.js +24 -21
- package/dist/package-manager.js +96 -45
- package/dist/pr.d.ts +2 -2
- package/dist/pr.js +47 -34
- package/dist/remove.d.ts +1 -1
- package/dist/remove.js +39 -27
- package/dist/repo-registry.js +45 -19
- package/dist/repo.js +29 -28
- package/dist/root.js +3 -3
- package/dist/shell-completion.d.ts +1 -1
- package/dist/shell-completion.js +65 -37
- package/dist/shell-handoff.js +2 -2
- package/dist/shell.d.ts +1 -1
- package/dist/shell.js +4 -4
- package/dist/status.d.ts +5 -5
- package/dist/status.js +23 -23
- package/dist/sync-files-command.d.ts +10 -0
- package/dist/sync-files-command.js +137 -0
- package/dist/sync.js +23 -15
- package/dist/trigger-hook.js +9 -5
- package/dist/warp.js +66 -34
- package/dist/worktree-info.d.ts +9 -9
- package/dist/worktree-info.js +31 -29
- package/dist/worktree-management.d.ts +1 -1
- package/dist/worktree-management.js +26 -11
- package/dist/worktree-prompts.js +5 -5
- package/man/man1/gji-back.1 +1 -1
- package/man/man1/gji-clean.1 +1 -1
- package/man/man1/gji-completion.1 +1 -1
- package/man/man1/gji-config.1 +1 -1
- package/man/man1/gji-go.1 +1 -1
- package/man/man1/gji-history.1 +1 -1
- package/man/man1/gji-init.1 +1 -1
- package/man/man1/gji-ls.1 +1 -1
- package/man/man1/gji-new.1 +1 -1
- package/man/man1/gji-open.1 +1 -1
- package/man/man1/gji-pr.1 +1 -1
- package/man/man1/gji-remove.1 +1 -1
- package/man/man1/gji-root.1 +1 -1
- package/man/man1/gji-status.1 +1 -1
- package/man/man1/gji-sync-files.1 +23 -0
- package/man/man1/gji-sync.1 +1 -1
- package/man/man1/gji-trigger-hook.1 +1 -1
- package/man/man1/gji-warp.1 +1 -1
- package/man/man1/gji.1 +5 -1
- package/package.json +8 -2
package/dist/completion.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { resolveSupportedShell } from "./shell.js";
|
|
2
|
+
import { renderShellCompletion } from "./shell-completion.js";
|
|
3
3
|
export async function runCompletionCommand(options) {
|
|
4
4
|
const shell = resolveSupportedShell(options.shell, process.env.SHELL);
|
|
5
5
|
if (!shell) {
|
|
6
|
-
options.stderr?.(
|
|
6
|
+
options.stderr?.("Unable to detect a supported shell. Specify one explicitly: bash, fish, or zsh.\n");
|
|
7
7
|
return 1;
|
|
8
8
|
}
|
|
9
9
|
options.stdout(renderShellCompletion(shell));
|
package/dist/config-command.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { loadGlobalConfig, parseConfigValue, unsetGlobalConfigKey, updateGlobalConfigKey, } from
|
|
1
|
+
import { loadGlobalConfig, parseConfigValue, unsetGlobalConfigKey, updateGlobalConfigKey, } from "./config.js";
|
|
2
2
|
export async function runConfigCommand(options) {
|
|
3
3
|
switch (options.action) {
|
|
4
4
|
case undefined: {
|
|
@@ -6,25 +6,25 @@ export async function runConfigCommand(options) {
|
|
|
6
6
|
writeJson(options.stdout, loaded.config);
|
|
7
7
|
return 0;
|
|
8
8
|
}
|
|
9
|
-
case
|
|
9
|
+
case "get": {
|
|
10
10
|
const loaded = await loadGlobalConfig();
|
|
11
11
|
writeJson(options.stdout, options.key ? loaded.config[options.key] : loaded.config);
|
|
12
12
|
return 0;
|
|
13
13
|
}
|
|
14
|
-
case
|
|
14
|
+
case "set":
|
|
15
15
|
if (options.key && options.value !== undefined) {
|
|
16
16
|
await updateGlobalConfigKey(options.key, parseConfigValue(options.value));
|
|
17
17
|
return 0;
|
|
18
18
|
}
|
|
19
19
|
break;
|
|
20
|
-
case
|
|
20
|
+
case "unset":
|
|
21
21
|
if (options.key) {
|
|
22
22
|
await unsetGlobalConfigKey(options.key);
|
|
23
23
|
return 0;
|
|
24
24
|
}
|
|
25
25
|
break;
|
|
26
26
|
}
|
|
27
|
-
throw new Error(`Invalid config arguments: ${[options.action, options.key, options.value].filter(Boolean).join(
|
|
27
|
+
throw new Error(`Invalid config arguments: ${[options.action, options.key, options.value].filter(Boolean).join(" ")}`);
|
|
28
28
|
}
|
|
29
29
|
function writeJson(stdout, value) {
|
|
30
30
|
stdout(`${JSON.stringify(value, null, 2)}\n`);
|
package/dist/config.js
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from
|
|
2
|
-
import { homedir } from
|
|
3
|
-
import { dirname, join, resolve } from
|
|
4
|
-
export const CONFIG_FILE_NAME =
|
|
5
|
-
export const GLOBAL_CONFIG_DIRECTORY =
|
|
6
|
-
export const GLOBAL_CONFIG_NAME =
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
export const CONFIG_FILE_NAME = ".gji.json";
|
|
5
|
+
export const GLOBAL_CONFIG_DIRECTORY = ".config/gji";
|
|
6
|
+
export const GLOBAL_CONFIG_NAME = "config.json";
|
|
7
7
|
export const KNOWN_CONFIG_KEYS = new Set([
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
8
|
+
"branchPrefix",
|
|
9
|
+
"editor",
|
|
10
|
+
"hooks",
|
|
11
|
+
"installSaveTarget",
|
|
12
|
+
"shellIntegration",
|
|
13
|
+
"skipInstallPrompt",
|
|
14
|
+
"syncDefaultBranch",
|
|
15
|
+
"syncFiles",
|
|
16
|
+
"syncRemote",
|
|
17
|
+
"worktreePath",
|
|
18
18
|
]);
|
|
19
19
|
export const KNOWN_GLOBAL_CONFIG_KEYS = new Set([
|
|
20
20
|
...KNOWN_CONFIG_KEYS,
|
|
21
|
-
|
|
21
|
+
"repos",
|
|
22
22
|
]);
|
|
23
23
|
export const DEFAULT_CONFIG = Object.freeze({});
|
|
24
24
|
export async function loadConfig(root) {
|
|
@@ -55,18 +55,22 @@ export async function loadEffectiveConfig(root, home = homedir(), onWarning) {
|
|
|
55
55
|
// Warn about relative worktreePath: it must be absolute or tilde-prefixed.
|
|
56
56
|
const worktreePathValue = merged.worktreePath;
|
|
57
57
|
if (onWarning &&
|
|
58
|
-
typeof worktreePathValue ===
|
|
58
|
+
typeof worktreePathValue === "string" &&
|
|
59
59
|
worktreePathValue.length > 0 &&
|
|
60
|
-
!worktreePathValue.startsWith(
|
|
61
|
-
!worktreePathValue.startsWith(
|
|
60
|
+
!worktreePathValue.startsWith("/") &&
|
|
61
|
+
!worktreePathValue.startsWith("~")) {
|
|
62
62
|
onWarning(`gji: "worktreePath" must be an absolute path or start with ~, got "${worktreePathValue}" — using default\n`);
|
|
63
63
|
}
|
|
64
64
|
// Hooks are spread across all three layers so that different hook keys from
|
|
65
65
|
// different layers both apply (e.g. global afterEnter + local afterCreate).
|
|
66
66
|
// Within each key the higher-precedence layer wins (same spread order).
|
|
67
67
|
const globalHooks = isPlainObject(globalBase.hooks) ? globalBase.hooks : {};
|
|
68
|
-
const perRepoHooks = isPlainObject(perRepoConfig.hooks)
|
|
69
|
-
|
|
68
|
+
const perRepoHooks = isPlainObject(perRepoConfig.hooks)
|
|
69
|
+
? perRepoConfig.hooks
|
|
70
|
+
: {};
|
|
71
|
+
const localHooks = isPlainObject(localConfig.config.hooks)
|
|
72
|
+
? localConfig.config.hooks
|
|
73
|
+
: {};
|
|
70
74
|
if (Object.keys(globalHooks).length > 0 ||
|
|
71
75
|
Object.keys(perRepoHooks).length > 0 ||
|
|
72
76
|
Object.keys(localHooks).length > 0) {
|
|
@@ -79,7 +83,7 @@ export async function loadGlobalConfig(home = homedir()) {
|
|
|
79
83
|
}
|
|
80
84
|
export async function saveLocalConfig(root, config) {
|
|
81
85
|
const path = join(root, CONFIG_FILE_NAME);
|
|
82
|
-
await writeFile(path, `${JSON.stringify(config, null, 2)}\n`,
|
|
86
|
+
await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
83
87
|
return path;
|
|
84
88
|
}
|
|
85
89
|
export async function updateLocalConfigKey(root, key, value) {
|
|
@@ -94,7 +98,7 @@ export async function updateLocalConfigKey(root, key, value) {
|
|
|
94
98
|
export async function saveGlobalConfig(config, home = homedir()) {
|
|
95
99
|
const path = GLOBAL_CONFIG_FILE_PATH(home);
|
|
96
100
|
await mkdir(dirname(path), { recursive: true });
|
|
97
|
-
await writeFile(path, `${JSON.stringify(config, null, 2)}\n`,
|
|
101
|
+
await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
98
102
|
return path;
|
|
99
103
|
}
|
|
100
104
|
export async function unsetGlobalConfigKey(key, home = homedir()) {
|
|
@@ -115,8 +119,12 @@ export async function updateGlobalConfigKey(key, value, home = homedir()) {
|
|
|
115
119
|
}
|
|
116
120
|
export async function updateGlobalRepoConfigKey(repoRoot, key, value, home = homedir()) {
|
|
117
121
|
const loaded = await loadGlobalConfig(home);
|
|
118
|
-
const repos = isPlainObject(loaded.config.repos)
|
|
119
|
-
|
|
122
|
+
const repos = isPlainObject(loaded.config.repos)
|
|
123
|
+
? { ...loaded.config.repos }
|
|
124
|
+
: {};
|
|
125
|
+
const existing = isPlainObject(repos[repoRoot])
|
|
126
|
+
? repos[repoRoot]
|
|
127
|
+
: {};
|
|
120
128
|
repos[repoRoot] = { ...existing, [key]: value };
|
|
121
129
|
const nextConfig = { ...loaded.config, repos };
|
|
122
130
|
await saveGlobalConfig(nextConfig, home);
|
|
@@ -139,11 +147,11 @@ export function parseConfigValue(value) {
|
|
|
139
147
|
}
|
|
140
148
|
export function resolveConfigString(config, key) {
|
|
141
149
|
const value = config[key];
|
|
142
|
-
return typeof value ===
|
|
150
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
143
151
|
}
|
|
144
152
|
async function loadConfigFile(path) {
|
|
145
153
|
try {
|
|
146
|
-
const rawConfig = await readFile(path,
|
|
154
|
+
const rawConfig = await readFile(path, "utf8");
|
|
147
155
|
const parsedConfig = JSON.parse(rawConfig);
|
|
148
156
|
return {
|
|
149
157
|
config: mergeConfig(parsedConfig),
|
|
@@ -178,25 +186,23 @@ function findPerRepoConfig(repos, repoRoot, home) {
|
|
|
178
186
|
return {};
|
|
179
187
|
}
|
|
180
188
|
function expandTilde(value, home) {
|
|
181
|
-
if (value ===
|
|
189
|
+
if (value === "~")
|
|
182
190
|
return home;
|
|
183
|
-
if (value.startsWith(
|
|
191
|
+
if (value.startsWith("~/"))
|
|
184
192
|
return join(home, value.slice(2));
|
|
185
193
|
return value;
|
|
186
194
|
}
|
|
187
195
|
function isPlainObject(value) {
|
|
188
|
-
return typeof value ===
|
|
196
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
189
197
|
}
|
|
190
198
|
function isMissingFileError(error) {
|
|
191
|
-
return
|
|
192
|
-
'code' in error &&
|
|
193
|
-
error.code === 'ENOENT');
|
|
199
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
194
200
|
}
|
|
195
201
|
function warnUnknownKeys(config, filePath, knownKeys, onWarning) {
|
|
196
202
|
for (const key of Object.keys(config)) {
|
|
197
203
|
if (!knownKeys.has(key)) {
|
|
198
204
|
const suggestion = closestKey(key, knownKeys);
|
|
199
|
-
const hint = suggestion ? ` (did you mean "${suggestion}"?)` :
|
|
205
|
+
const hint = suggestion ? ` (did you mean "${suggestion}"?)` : "";
|
|
200
206
|
onWarning(`gji: unknown config key "${key}" in ${filePath}${hint}\n`);
|
|
201
207
|
}
|
|
202
208
|
}
|
package/dist/conflict.d.ts
CHANGED
package/dist/conflict.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { isCancel, select } from
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { access } from "node:fs/promises";
|
|
3
|
+
import { isCancel, select } from "@clack/prompts";
|
|
4
4
|
export async function pathExists(path) {
|
|
5
5
|
try {
|
|
6
6
|
await access(path, constants.F_OK);
|
|
@@ -14,12 +14,20 @@ export async function promptForPathConflict(path) {
|
|
|
14
14
|
const choice = await select({
|
|
15
15
|
message: `Target path already exists: ${path}`,
|
|
16
16
|
options: [
|
|
17
|
-
{
|
|
18
|
-
|
|
17
|
+
{
|
|
18
|
+
value: "abort",
|
|
19
|
+
label: "Abort",
|
|
20
|
+
hint: "Keep the existing directory untouched",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
value: "reuse",
|
|
24
|
+
label: "Reuse path",
|
|
25
|
+
hint: "Print the existing path and stop",
|
|
26
|
+
},
|
|
19
27
|
],
|
|
20
28
|
});
|
|
21
29
|
if (isCancel(choice)) {
|
|
22
|
-
return
|
|
30
|
+
return "abort";
|
|
23
31
|
}
|
|
24
32
|
return choice;
|
|
25
33
|
}
|
package/dist/editor.js
CHANGED
|
@@ -1,17 +1,37 @@
|
|
|
1
|
-
import { spawn } from
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
2
|
// Ordered by likely popularity among the target audience.
|
|
3
3
|
export const EDITORS = [
|
|
4
|
-
{
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
{
|
|
5
|
+
cli: "cursor",
|
|
6
|
+
name: "Cursor",
|
|
7
|
+
newWindowFlag: "--new-window",
|
|
8
|
+
supportsWorkspace: true,
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
cli: "code",
|
|
12
|
+
name: "VS Code",
|
|
13
|
+
newWindowFlag: "--new-window",
|
|
14
|
+
supportsWorkspace: true,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
cli: "windsurf",
|
|
18
|
+
name: "Windsurf",
|
|
19
|
+
newWindowFlag: "--new-window",
|
|
20
|
+
supportsWorkspace: true,
|
|
21
|
+
},
|
|
22
|
+
{ cli: "zed", name: "Zed", supportsWorkspace: false },
|
|
23
|
+
{
|
|
24
|
+
cli: "subl",
|
|
25
|
+
name: "Sublime Text",
|
|
26
|
+
newWindowFlag: "--new-window",
|
|
27
|
+
supportsWorkspace: false,
|
|
28
|
+
},
|
|
9
29
|
];
|
|
10
30
|
export async function defaultSpawnEditor(cli, args) {
|
|
11
|
-
const child = spawn(cli, args, { detached: true, stdio:
|
|
31
|
+
const child = spawn(cli, args, { detached: true, stdio: "ignore" });
|
|
12
32
|
await new Promise((resolve, reject) => {
|
|
13
|
-
child.once(
|
|
14
|
-
child.once(
|
|
33
|
+
child.once("error", reject);
|
|
34
|
+
child.once("spawn", resolve);
|
|
15
35
|
});
|
|
16
36
|
child.unref();
|
|
17
37
|
}
|
package/dist/file-sync.d.ts
CHANGED
package/dist/file-sync.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { copyFile, mkdir, stat } from
|
|
2
|
-
import { dirname, isAbsolute, join, normalize } from
|
|
1
|
+
import { copyFile, mkdir, stat } from "node:fs/promises";
|
|
2
|
+
import { dirname, isAbsolute, join, normalize } from "node:path";
|
|
3
3
|
/**
|
|
4
4
|
* Copies files matching each pattern (relative to mainRoot) into the equivalent
|
|
5
5
|
* relative path under targetPath, creating parent directories as needed.
|
|
@@ -10,13 +10,7 @@ import { dirname, isAbsolute, join, normalize } from 'node:path';
|
|
|
10
10
|
*/
|
|
11
11
|
export async function syncFiles(mainRoot, targetPath, patterns) {
|
|
12
12
|
for (const pattern of patterns) {
|
|
13
|
-
|
|
14
|
-
throw new Error(`syncFiles: pattern must be a relative path, got: ${pattern}`);
|
|
15
|
-
}
|
|
16
|
-
const normalized = normalize(pattern);
|
|
17
|
-
if (normalized.startsWith('..')) {
|
|
18
|
-
throw new Error(`syncFiles: pattern must not contain '..' segments, got: ${pattern}`);
|
|
19
|
-
}
|
|
13
|
+
const normalized = validateSyncFilePattern(pattern);
|
|
20
14
|
const sourcePath = join(mainRoot, normalized);
|
|
21
15
|
const destPath = join(targetPath, normalized);
|
|
22
16
|
// Skip silently if source does not exist
|
|
@@ -33,6 +27,16 @@ export async function syncFiles(mainRoot, targetPath, patterns) {
|
|
|
33
27
|
await copyFile(sourcePath, destPath);
|
|
34
28
|
}
|
|
35
29
|
}
|
|
30
|
+
export function validateSyncFilePattern(pattern) {
|
|
31
|
+
if (isAbsolute(pattern)) {
|
|
32
|
+
throw new Error(`syncFiles: pattern must be a relative path, got: ${pattern}`);
|
|
33
|
+
}
|
|
34
|
+
const normalized = normalize(pattern);
|
|
35
|
+
if (normalized.startsWith("..")) {
|
|
36
|
+
throw new Error(`syncFiles: pattern must not contain '..' segments, got: ${pattern}`);
|
|
37
|
+
}
|
|
38
|
+
return normalized;
|
|
39
|
+
}
|
|
36
40
|
async function fileExists(path) {
|
|
37
41
|
try {
|
|
38
42
|
await stat(path);
|
|
@@ -47,6 +51,6 @@ async function fileExists(path) {
|
|
|
47
51
|
}
|
|
48
52
|
function isNotFoundError(error) {
|
|
49
53
|
return (error instanceof Error &&
|
|
50
|
-
|
|
51
|
-
error.code ===
|
|
54
|
+
"code" in error &&
|
|
55
|
+
error.code === "ENOENT");
|
|
52
56
|
}
|
package/dist/git.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export interface WorktreeHealth {
|
|
|
3
3
|
behind: number;
|
|
4
4
|
hasUpstream: boolean;
|
|
5
5
|
upstreamGone: boolean;
|
|
6
|
-
status:
|
|
6
|
+
status: "clean" | "dirty";
|
|
7
7
|
}
|
|
8
8
|
export declare function runGit(cwd: string, args: string[]): Promise<string>;
|
|
9
9
|
export declare function readWorktreeHealth(cwd: string): Promise<WorktreeHealth>;
|
package/dist/git.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { execFile } from
|
|
2
|
-
import { promisify } from
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
3
|
const execFileAsync = promisify(execFile);
|
|
4
4
|
export async function runGit(cwd, args) {
|
|
5
5
|
try {
|
|
6
|
-
const { stdout } = await execFileAsync(
|
|
6
|
+
const { stdout } = await execFileAsync("git", args, { cwd });
|
|
7
7
|
return stdout.trim();
|
|
8
8
|
}
|
|
9
9
|
catch (error) {
|
|
@@ -12,16 +12,18 @@ export async function runGit(cwd, args) {
|
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
export async function readWorktreeHealth(cwd) {
|
|
15
|
-
const { stdout } = await execFileAsync(
|
|
15
|
+
const { stdout } = await execFileAsync("git", ["status", "--porcelain=v2", "--branch"], { cwd });
|
|
16
16
|
return parseWorktreeHealth(stdout);
|
|
17
17
|
}
|
|
18
18
|
export async function isDirtyWorktree(cwd) {
|
|
19
19
|
const health = await readWorktreeHealth(cwd);
|
|
20
|
-
return health.status ===
|
|
20
|
+
return health.status === "dirty";
|
|
21
21
|
}
|
|
22
|
-
export async function isBranchMergedInto(cwd, branch, base =
|
|
22
|
+
export async function isBranchMergedInto(cwd, branch, base = "HEAD") {
|
|
23
23
|
try {
|
|
24
|
-
await execFileAsync(
|
|
24
|
+
await execFileAsync("git", ["merge-base", "--is-ancestor", branch, base], {
|
|
25
|
+
cwd,
|
|
26
|
+
});
|
|
25
27
|
return true;
|
|
26
28
|
}
|
|
27
29
|
catch (error) {
|
|
@@ -32,10 +34,10 @@ export async function isBranchMergedInto(cwd, branch, base = 'HEAD') {
|
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
36
|
export async function resolveRemoteDefaultBranch(cwd, remote) {
|
|
35
|
-
const { stdout } = await execFileAsync(
|
|
37
|
+
const { stdout } = await execFileAsync("git", ["ls-remote", "--symref", remote, "HEAD"], { cwd });
|
|
36
38
|
const refLine = stdout
|
|
37
|
-
.split(
|
|
38
|
-
.find((line) => line.startsWith(
|
|
39
|
+
.split("\n")
|
|
40
|
+
.find((line) => line.startsWith("ref: refs/heads/"));
|
|
39
41
|
if (!refLine) {
|
|
40
42
|
return null;
|
|
41
43
|
}
|
|
@@ -44,7 +46,7 @@ export async function resolveRemoteDefaultBranch(cwd, remote) {
|
|
|
44
46
|
}
|
|
45
47
|
export async function readBranchLastCommitTimestamp(cwd, branch) {
|
|
46
48
|
try {
|
|
47
|
-
const { stdout } = await execFileAsync(
|
|
49
|
+
const { stdout } = await execFileAsync("git", ["log", "-1", "--format=%ct", branch], { cwd });
|
|
48
50
|
const timestamp = Number(stdout.trim());
|
|
49
51
|
return Number.isFinite(timestamp) ? timestamp : null;
|
|
50
52
|
}
|
|
@@ -58,12 +60,12 @@ function parseWorktreeHealth(output) {
|
|
|
58
60
|
let hasUpstream = false;
|
|
59
61
|
let hasAb = false;
|
|
60
62
|
let dirty = false;
|
|
61
|
-
for (const line of output.split(
|
|
62
|
-
if (line.startsWith(
|
|
63
|
+
for (const line of output.split("\n").filter(Boolean)) {
|
|
64
|
+
if (line.startsWith("# branch.upstream ")) {
|
|
63
65
|
hasUpstream = true;
|
|
64
66
|
continue;
|
|
65
67
|
}
|
|
66
|
-
if (line.startsWith(
|
|
68
|
+
if (line.startsWith("# branch.ab ")) {
|
|
67
69
|
const match = /^# branch\.ab \+(\d+) -(\d+)$/.exec(line);
|
|
68
70
|
if (!match) {
|
|
69
71
|
throw new Error(`Unexpected branch.ab output: '${line}'`);
|
|
@@ -73,7 +75,7 @@ function parseWorktreeHealth(output) {
|
|
|
73
75
|
behind = Number(match[2]);
|
|
74
76
|
continue;
|
|
75
77
|
}
|
|
76
|
-
if (!line.startsWith(
|
|
78
|
+
if (!line.startsWith("# ")) {
|
|
77
79
|
dirty = true;
|
|
78
80
|
}
|
|
79
81
|
}
|
|
@@ -82,11 +84,11 @@ function parseWorktreeHealth(output) {
|
|
|
82
84
|
behind,
|
|
83
85
|
hasUpstream,
|
|
84
86
|
upstreamGone: hasUpstream && !hasAb,
|
|
85
|
-
status: dirty ?
|
|
87
|
+
status: dirty ? "dirty" : "clean",
|
|
86
88
|
};
|
|
87
89
|
}
|
|
88
90
|
function hasExitCode(error, code) {
|
|
89
|
-
return error instanceof Error
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
return (error instanceof Error &&
|
|
92
|
+
"code" in error &&
|
|
93
|
+
error.code === code);
|
|
92
94
|
}
|