@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 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 <number>` fetches `origin/pull/<number>/head` and creates a linked `pr/<number>` worktree
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` currently uses GitHub-style PR refs for `gji pr`
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
  }
@@ -0,0 +1,3 @@
1
+ export type PathConflictChoice = 'abort' | 'reuse';
2
+ export declare function pathExists(path: string): Promise<boolean>;
3
+ export declare function promptForPathConflict(path: string): Promise<PathConflictChoice>;
@@ -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
@@ -1,4 +1,5 @@
1
- export type PathConflictChoice = 'abort' | 'reuse';
1
+ import { type PathConflictChoice } from './conflict.js';
2
+ export type { PathConflictChoice };
2
3
  export interface NewCommandOptions {
3
4
  branch?: string;
4
5
  cwd: string;
package/dist/new.js CHANGED
@@ -1,10 +1,10 @@
1
- import { access, mkdir } from 'node:fs/promises';
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, select, text } from '@clack/prompts';
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
- : ['worktree', 'add', '-b', worktreeName, worktreePath];
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 declare function runPrCommand(options: PrCommandOptions): Promise<number>;
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
- export async function runPrCommand(options) {
8
- const repository = await detectRepository(options.cwd);
9
- const branchName = `pr/${options.number}`;
10
- const remoteRef = `refs/remotes/origin/pull/${options.number}/head`;
11
- const worktreePath = resolveWorktreePath(repository.repoRoot, branchName);
12
- await execFileAsync('git', ['fetch', 'origin', `refs/pull/${options.number}/head:${remoteRef}`], { cwd: repository.repoRoot });
13
- await mkdir(dirname(worktreePath), { recursive: true });
14
- await execFileAsync('git', ['worktree', 'add', '-b', branchName, worktreePath, remoteRef], { cwd: repository.repoRoot });
15
- options.stdout(`${worktreePath}\n`);
16
- return 0;
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solaqua/gji",
3
- "version": "0.1.0-beta.7",
3
+ "version": "0.1.0-beta.9",
4
4
  "description": "Git worktree CLI for fast context switching.",
5
5
  "license": "MIT",
6
6
  "author": "sjquant",