@solaqua/gji 0.1.0-beta.7 → 0.1.0-beta.9
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 +4 -2
- package/dist/cli.js +1 -1
- package/dist/conflict.d.ts +3 -0
- package/dist/conflict.js +25 -0
- package/dist/init.js +7 -0
- package/dist/new.d.ts +2 -1
- package/dist/new.js +15 -26
- package/dist/pr.d.ts +9 -1
- package/dist/pr.js +64 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -110,6 +110,8 @@ Pull a PR into its own worktree:
|
|
|
110
110
|
|
|
111
111
|
```sh
|
|
112
112
|
gji pr 123
|
|
113
|
+
gji pr #123
|
|
114
|
+
gji pr https://github.com/owner/repo/pull/123
|
|
113
115
|
```
|
|
114
116
|
|
|
115
117
|
Sync the current worktree with the latest default branch:
|
|
@@ -145,7 +147,7 @@ After removal, the shell-integrated command returns you to the repository root.
|
|
|
145
147
|
- `gji --version` prints the installed CLI version
|
|
146
148
|
- `gji init [shell]` prints shell integration for `zsh`, `bash`, or `fish`
|
|
147
149
|
- `gji new [branch] [--detached]` creates a branch and linked worktree; with shell integration it moves into the new worktree, and `--detached` creates a detached worktree instead
|
|
148
|
-
- `gji pr <
|
|
150
|
+
- `gji pr <ref>` accepts `123`, `#123`, or a full PR/MR URL, extracts the numeric ID, then fetches `origin/pull/<number>/head` and creates a linked `pr/<number>` worktree
|
|
149
151
|
- `gji go [branch] [--print]` jumps to an existing worktree when shell integration is installed, or prints the matching worktree path otherwise
|
|
150
152
|
- `gji root [--print]` jumps to the main repository root when shell integration is installed, or prints it otherwise
|
|
151
153
|
- `gji status [--json]` prints repository metadata, worktree health, and upstream divergence
|
|
@@ -214,7 +216,7 @@ Each worktree entry contains:
|
|
|
214
216
|
|
|
215
217
|
- `gji` works from either the main repository root or any linked worktree
|
|
216
218
|
- the current worktree is never offered as a `gji clean` removal candidate
|
|
217
|
-
- `gji`
|
|
219
|
+
- `gji pr` accepts GitHub, GitLab, and Bitbucket-style PR/MR links, but still fetches from `origin` using GitHub-style `refs/pull/<number>/head`
|
|
218
220
|
|
|
219
221
|
## License
|
|
220
222
|
|
package/dist/cli.js
CHANGED
|
@@ -145,7 +145,7 @@ function attachCommandActions(program, options) {
|
|
|
145
145
|
program.commands
|
|
146
146
|
.find((command) => command.name() === 'pr')
|
|
147
147
|
?.action(async (number) => {
|
|
148
|
-
const exitCode = await runPrCommand({ cwd: options.cwd, number, stdout: options.stdout });
|
|
148
|
+
const exitCode = await runPrCommand({ cwd: options.cwd, number, stderr: options.stderr, stdout: options.stdout });
|
|
149
149
|
if (exitCode !== 0) {
|
|
150
150
|
throw commanderExit(exitCode);
|
|
151
151
|
}
|
package/dist/conflict.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { access } from 'node:fs/promises';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { isCancel, select } from '@clack/prompts';
|
|
4
|
+
export async function pathExists(path) {
|
|
5
|
+
try {
|
|
6
|
+
await access(path, constants.F_OK);
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export async function promptForPathConflict(path) {
|
|
14
|
+
const choice = await select({
|
|
15
|
+
message: `Target path already exists: ${path}`,
|
|
16
|
+
options: [
|
|
17
|
+
{ value: 'abort', label: 'Abort', hint: 'Keep the existing directory untouched' },
|
|
18
|
+
{ value: 'reuse', label: 'Reuse path', hint: 'Print the existing path and stop' },
|
|
19
|
+
],
|
|
20
|
+
});
|
|
21
|
+
if (isCancel(choice)) {
|
|
22
|
+
return 'abort';
|
|
23
|
+
}
|
|
24
|
+
return choice;
|
|
25
|
+
}
|
package/dist/init.js
CHANGED
|
@@ -11,6 +11,13 @@ const SHELL_WRAPPED_COMMANDS = [
|
|
|
11
11
|
names: ['new'],
|
|
12
12
|
tempPrefix: 'gji-new',
|
|
13
13
|
},
|
|
14
|
+
{
|
|
15
|
+
bypassOption: '--help',
|
|
16
|
+
commandName: 'pr',
|
|
17
|
+
envVar: 'GJI_PR_OUTPUT_FILE',
|
|
18
|
+
names: ['pr'],
|
|
19
|
+
tempPrefix: 'gji-pr',
|
|
20
|
+
},
|
|
14
21
|
{
|
|
15
22
|
bypassOption: '--print',
|
|
16
23
|
commandName: 'go',
|
package/dist/new.d.ts
CHANGED
package/dist/new.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { constants } from 'node:fs';
|
|
1
|
+
import { mkdir } from 'node:fs/promises';
|
|
3
2
|
import { dirname } from 'node:path';
|
|
4
3
|
import { execFile } from 'node:child_process';
|
|
5
4
|
import { promisify } from 'node:util';
|
|
6
|
-
import { isCancel,
|
|
5
|
+
import { isCancel, text } from '@clack/prompts';
|
|
7
6
|
import { loadEffectiveConfig } from './config.js';
|
|
7
|
+
import { pathExists, promptForPathConflict } from './conflict.js';
|
|
8
8
|
import { detectRepository, resolveWorktreePath } from './repo.js';
|
|
9
9
|
import { writeShellOutput } from './shell-handoff.js';
|
|
10
10
|
const execFileAsync = promisify(execFile);
|
|
@@ -42,21 +42,14 @@ export function createNewCommand(dependencies = {}) {
|
|
|
42
42
|
await mkdir(dirname(worktreePath), { recursive: true });
|
|
43
43
|
const gitArgs = options.detached
|
|
44
44
|
? ['worktree', 'add', '--detach', worktreePath]
|
|
45
|
-
:
|
|
45
|
+
: await localBranchExists(repository.repoRoot, worktreeName)
|
|
46
|
+
? ['worktree', 'add', worktreePath, worktreeName]
|
|
47
|
+
: ['worktree', 'add', '-b', worktreeName, worktreePath];
|
|
46
48
|
await execFileAsync('git', gitArgs, { cwd: repository.repoRoot });
|
|
47
49
|
await writeOutput(worktreePath, options.stdout);
|
|
48
50
|
return 0;
|
|
49
51
|
};
|
|
50
52
|
}
|
|
51
|
-
async function pathExists(path) {
|
|
52
|
-
try {
|
|
53
|
-
await access(path, constants.F_OK);
|
|
54
|
-
return true;
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
53
|
export const runNewCommand = createNewCommand();
|
|
61
54
|
export function generateBranchPlaceholder(random = Math.random) {
|
|
62
55
|
const roots = [
|
|
@@ -120,19 +113,6 @@ async function resolveUniqueDetachedWorktreePath(repoRoot, baseName) {
|
|
|
120
113
|
attempt += 1;
|
|
121
114
|
}
|
|
122
115
|
}
|
|
123
|
-
async function promptForPathConflict(path) {
|
|
124
|
-
const choice = await select({
|
|
125
|
-
message: `Target path already exists: ${path}`,
|
|
126
|
-
options: [
|
|
127
|
-
{ value: 'abort', label: 'Abort', hint: 'Keep the existing directory untouched' },
|
|
128
|
-
{ value: 'reuse', label: 'Reuse path', hint: 'Print the existing path and stop' },
|
|
129
|
-
],
|
|
130
|
-
});
|
|
131
|
-
if (isCancel(choice)) {
|
|
132
|
-
return 'abort';
|
|
133
|
-
}
|
|
134
|
-
return choice;
|
|
135
|
-
}
|
|
136
116
|
async function defaultPromptForBranch(placeholder) {
|
|
137
117
|
const choice = await text({
|
|
138
118
|
defaultValue: placeholder,
|
|
@@ -149,6 +129,15 @@ function pickRandom(values, random) {
|
|
|
149
129
|
const index = Math.floor(random() * values.length);
|
|
150
130
|
return values[Math.min(index, values.length - 1)];
|
|
151
131
|
}
|
|
132
|
+
async function localBranchExists(repoRoot, branchName) {
|
|
133
|
+
try {
|
|
134
|
+
await execFileAsync('git', ['show-ref', '--verify', '--quiet', `refs/heads/${branchName}`], { cwd: repoRoot });
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
152
141
|
async function writeOutput(worktreePath, stdout) {
|
|
153
142
|
await writeShellOutput(NEW_OUTPUT_FILE_ENV, worktreePath, stdout);
|
|
154
143
|
}
|
package/dist/pr.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
+
import { type PathConflictChoice } from './conflict.js';
|
|
2
|
+
export type { PathConflictChoice };
|
|
1
3
|
export interface PrCommandOptions {
|
|
2
4
|
cwd: string;
|
|
3
5
|
number: string;
|
|
6
|
+
stderr: (chunk: string) => void;
|
|
4
7
|
stdout: (chunk: string) => void;
|
|
5
8
|
}
|
|
6
|
-
export
|
|
9
|
+
export interface PrCommandDependencies {
|
|
10
|
+
promptForPathConflict: (path: string) => Promise<PathConflictChoice>;
|
|
11
|
+
}
|
|
12
|
+
export declare function parsePrInput(input: string): string | null;
|
|
13
|
+
export declare function createPrCommand(dependencies?: Partial<PrCommandDependencies>): (options: PrCommandOptions) => Promise<number>;
|
|
14
|
+
export declare const runPrCommand: (options: PrCommandOptions) => Promise<number>;
|
package/dist/pr.js
CHANGED
|
@@ -2,16 +2,70 @@ import { mkdir } from 'node:fs/promises';
|
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
3
|
import { execFile } from 'node:child_process';
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
|
+
import { pathExists, promptForPathConflict } from './conflict.js';
|
|
5
6
|
import { detectRepository, resolveWorktreePath } from './repo.js';
|
|
7
|
+
import { writeShellOutput } from './shell-handoff.js';
|
|
6
8
|
const execFileAsync = promisify(execFile);
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
const PR_OUTPUT_FILE_ENV = 'GJI_PR_OUTPUT_FILE';
|
|
10
|
+
export function parsePrInput(input) {
|
|
11
|
+
if (/^\d+$/.test(input))
|
|
12
|
+
return input;
|
|
13
|
+
const hashMatch = input.match(/^#(\d+)$/);
|
|
14
|
+
if (hashMatch)
|
|
15
|
+
return hashMatch[1];
|
|
16
|
+
const urlMatch = input.match(/\/(?:pull|pull-requests|merge_requests)\/(\d+)/);
|
|
17
|
+
if (urlMatch)
|
|
18
|
+
return urlMatch[1];
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
export function createPrCommand(dependencies = {}) {
|
|
22
|
+
const prompt = dependencies.promptForPathConflict ?? promptForPathConflict;
|
|
23
|
+
return async function runPrCommand(options) {
|
|
24
|
+
const prNumber = parsePrInput(options.number);
|
|
25
|
+
if (!prNumber) {
|
|
26
|
+
options.stderr(`Invalid PR reference: ${options.number}\n`);
|
|
27
|
+
return 1;
|
|
28
|
+
}
|
|
29
|
+
const repository = await detectRepository(options.cwd);
|
|
30
|
+
const branchName = `pr/${prNumber}`;
|
|
31
|
+
const remoteRef = `refs/remotes/origin/pull/${prNumber}/head`;
|
|
32
|
+
const worktreePath = resolveWorktreePath(repository.repoRoot, branchName);
|
|
33
|
+
if (await pathExists(worktreePath)) {
|
|
34
|
+
const choice = await prompt(worktreePath);
|
|
35
|
+
if (choice === 'reuse') {
|
|
36
|
+
await writeOutput(worktreePath, options.stdout);
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
options.stderr(`Aborted because target worktree path already exists: ${worktreePath}\n`);
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
await execFileAsync('git', ['fetch', 'origin', `refs/pull/${prNumber}/head:${remoteRef}`], { cwd: repository.repoRoot });
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
options.stderr(`Failed to fetch PR #${prNumber} from origin\n`);
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
await mkdir(dirname(worktreePath), { recursive: true });
|
|
50
|
+
const branchAlreadyExists = await localBranchExists(repository.repoRoot, branchName);
|
|
51
|
+
const worktreeArgs = branchAlreadyExists
|
|
52
|
+
? ['worktree', 'add', worktreePath, branchName]
|
|
53
|
+
: ['worktree', 'add', '-b', branchName, worktreePath, remoteRef];
|
|
54
|
+
await execFileAsync('git', worktreeArgs, { cwd: repository.repoRoot });
|
|
55
|
+
await writeOutput(worktreePath, options.stdout);
|
|
56
|
+
return 0;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async function localBranchExists(repoRoot, branchName) {
|
|
60
|
+
try {
|
|
61
|
+
await execFileAsync('git', ['show-ref', '--verify', '--quiet', `refs/heads/${branchName}`], { cwd: repoRoot });
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export const runPrCommand = createPrCommand();
|
|
69
|
+
async function writeOutput(worktreePath, stdout) {
|
|
70
|
+
await writeShellOutput(PR_OUTPUT_FILE_ENV, worktreePath, stdout);
|
|
17
71
|
}
|