@solaqua/gji 0.8.0 → 0.9.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 +24 -30
- package/dist/browser.d.ts +7 -0
- package/dist/browser.js +37 -0
- package/dist/clean.js +34 -19
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +59 -4
- package/dist/doctor.d.ts +28 -0
- package/dist/doctor.js +454 -0
- package/dist/gji-bundle.mjs +2514 -378
- package/dist/go.d.ts +2 -1
- package/dist/go.js +4 -1
- package/dist/index.js +12 -6
- package/dist/init.d.ts +15 -0
- package/dist/init.js +232 -16
- package/dist/open.d.ts +2 -1
- package/dist/open.js +2 -1
- package/dist/pr-open.d.ts +19 -0
- package/dist/pr-open.js +290 -0
- package/dist/pull-requests.d.ts +33 -0
- package/dist/pull-requests.js +384 -0
- package/dist/remove.js +2 -2
- package/dist/repo-registry.d.ts +5 -0
- package/dist/repo-registry.js +87 -14
- package/dist/shell-completion.js +278 -10
- package/dist/shell-setup.d.ts +9 -0
- package/dist/shell-setup.js +56 -0
- package/dist/warp.d.ts +3 -0
- package/dist/warp.js +8 -3
- package/dist/worktree-management.d.ts +2 -1
- package/dist/worktree-management.js +23 -7
- package/dist/worktree-picker.d.ts +12 -1
- package/dist/worktree-picker.js +84 -6
- 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-doctor.1 +19 -0
- package/man/man1/gji-go.1 +1 -1
- package/man/man1/gji-history.1 +1 -1
- package/man/man1/gji-init.1 +6 -3
- 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 +5 -1
- package/man/man1/gji-remove.1 +1 -1
- package/man/man1/gji-root.1 +1 -1
- package/man/man1/gji-run-hook.1 +1 -1
- package/man/man1/gji-status.1 +1 -1
- package/man/man1/gji-sync-files.1 +1 -1
- package/man/man1/gji-sync.1 +1 -1
- package/man/man1/gji-warp.1 +1 -1
- package/man/man1/gji.1 +6 -2
- package/package.json +1 -1
package/dist/pr-open.js
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { isCancel, select } from "@clack/prompts";
|
|
2
|
+
import { openBrowser } from "./browser.js";
|
|
3
|
+
import { isHeadless } from "./headless.js";
|
|
4
|
+
import { createPullRequestQuery, } from "./pull-requests.js";
|
|
5
|
+
import { detectRepository, listWorktrees } from "./repo.js";
|
|
6
|
+
import { buildWorktreePromptEntries, promptForSingleWorktree, resolveWorktreeQuery, } from "./worktree-picker.js";
|
|
7
|
+
export function createPrOpenCommand(dependencies = {}) {
|
|
8
|
+
const query = createPullRequestQuery();
|
|
9
|
+
const findOpenPullRequest = dependencies.findOpenPullRequest ?? query.findOpenPullRequest;
|
|
10
|
+
const openInBrowser = dependencies.openBrowser ?? openBrowser;
|
|
11
|
+
const promptForPullRequest = dependencies.promptForPullRequest ?? defaultPromptForPullRequest;
|
|
12
|
+
const promptForWorktree = dependencies.promptForWorktree ?? defaultPromptForWorktree;
|
|
13
|
+
const queryPullRequests = dependencies.queryPullRequests ?? query.listOpenPullRequests;
|
|
14
|
+
const queryRepositoryPullRequests = dependencies.queryRepositoryPullRequests ??
|
|
15
|
+
(dependencies.queryPullRequests === undefined
|
|
16
|
+
? query.listOpenPullRequestsForRepository
|
|
17
|
+
: undefined);
|
|
18
|
+
return async function runPrOpenCommand(options) {
|
|
19
|
+
if (options.select && options.target !== undefined) {
|
|
20
|
+
options.stderr("gji pr open: --select cannot be used with a target\n");
|
|
21
|
+
return 1;
|
|
22
|
+
}
|
|
23
|
+
if (options.select && isHeadless()) {
|
|
24
|
+
options.stderr("gji pr open --select: selector is unavailable in non-interactive mode (GJI_NO_TUI=1)\n");
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
27
|
+
const repository = await detectRepository(options.cwd).catch((error) => {
|
|
28
|
+
options.stderr(`gji pr open: unable to detect the current repository: ${formatError(error)}\n`);
|
|
29
|
+
return null;
|
|
30
|
+
});
|
|
31
|
+
if (repository === null)
|
|
32
|
+
return 1;
|
|
33
|
+
if (options.target === undefined) {
|
|
34
|
+
if (!options.select) {
|
|
35
|
+
return openCurrentWorktree(repository.repoRoot, options, queryPullRequests, promptForPullRequest, openInBrowser);
|
|
36
|
+
}
|
|
37
|
+
return openFromWorktreeSelector(repository.repoRoot, repository.repoName, options, promptForWorktree, promptForPullRequest, queryPullRequests, queryRepositoryPullRequests, openInBrowser);
|
|
38
|
+
}
|
|
39
|
+
const number = parsePrNumberTarget(options.target);
|
|
40
|
+
if (number !== null) {
|
|
41
|
+
return openByNumber(repository.repoRoot, number, options, findOpenPullRequest, openInBrowser);
|
|
42
|
+
}
|
|
43
|
+
const worktrees = await listWorktrees(options.cwd).catch((error) => {
|
|
44
|
+
options.stderr(`gji pr open: unable to list worktrees: ${formatError(error)}\n`);
|
|
45
|
+
return null;
|
|
46
|
+
});
|
|
47
|
+
if (worktrees === null)
|
|
48
|
+
return 1;
|
|
49
|
+
const match = resolveWorktreeQuery(worktrees.map((worktree) => ({
|
|
50
|
+
repoRoot: repository.repoRoot,
|
|
51
|
+
repoName: repository.repoName,
|
|
52
|
+
worktree,
|
|
53
|
+
})), options.target);
|
|
54
|
+
if (match === null) {
|
|
55
|
+
options.stderr(`gji pr open: no worktree found matching: ${options.target}\n`);
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
return openForBranch(repository.repoRoot, match.worktree.branch, options, queryPullRequests, promptForPullRequest, openInBrowser);
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export const runPrOpenCommand = createPrOpenCommand();
|
|
62
|
+
async function openCurrentWorktree(repoRoot, options, queryPullRequests, promptForPullRequest, openInBrowser) {
|
|
63
|
+
const worktrees = await listWorktrees(options.cwd).catch((error) => {
|
|
64
|
+
options.stderr(`gji pr open: unable to list worktrees: ${formatError(error)}\n`);
|
|
65
|
+
return null;
|
|
66
|
+
});
|
|
67
|
+
if (worktrees === null)
|
|
68
|
+
return 1;
|
|
69
|
+
const currentWorktree = worktrees.find((worktree) => worktree.isCurrent);
|
|
70
|
+
if (currentWorktree === undefined) {
|
|
71
|
+
options.stderr("gji pr open: unable to identify the current worktree\n");
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
if (currentWorktree.branch === null) {
|
|
75
|
+
options.stderr("gji pr open: current worktree is detached and has no PR branch\n");
|
|
76
|
+
return 1;
|
|
77
|
+
}
|
|
78
|
+
let pullRequests;
|
|
79
|
+
try {
|
|
80
|
+
pullRequests = await queryPullRequests(repoRoot, currentWorktree.branch);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
options.stderr(`gji pr open: failed to look up PRs for current branch ${currentWorktree.branch}: ${formatError(error)}\n`);
|
|
84
|
+
return 1;
|
|
85
|
+
}
|
|
86
|
+
if (pullRequests.length === 0) {
|
|
87
|
+
options.stderr(isHeadless()
|
|
88
|
+
? "gji pr open: no open PR found for the current worktree; pass an explicit branch or PR number (for example, gji pr open '#123')\n"
|
|
89
|
+
: "gji pr open: no open PR found for the current worktree; use gji pr open --select to choose another\n");
|
|
90
|
+
return 1;
|
|
91
|
+
}
|
|
92
|
+
return openPullRequests(pullRequests, options, promptForPullRequest, openInBrowser);
|
|
93
|
+
}
|
|
94
|
+
async function openFromWorktreeSelector(repoRoot, repoName, options, promptForWorktree, promptForPullRequest, queryPullRequests, queryRepositoryPullRequests, openInBrowser) {
|
|
95
|
+
const worktrees = await listWorktrees(options.cwd).catch((error) => {
|
|
96
|
+
options.stderr(`gji pr open: unable to list worktrees: ${formatError(error)}\n`);
|
|
97
|
+
return null;
|
|
98
|
+
});
|
|
99
|
+
if (worktrees === null)
|
|
100
|
+
return 1;
|
|
101
|
+
let lookupError = null;
|
|
102
|
+
const queryForSelector = async (root, branch) => {
|
|
103
|
+
try {
|
|
104
|
+
return await queryPullRequests(root, branch);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
lookupError ??= error;
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
const queryRepositoryForSelector = queryRepositoryPullRequests === undefined
|
|
112
|
+
? undefined
|
|
113
|
+
: async (root) => {
|
|
114
|
+
try {
|
|
115
|
+
return await queryRepositoryPullRequests(root);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
lookupError ??= error;
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
let connectedWorktrees;
|
|
123
|
+
try {
|
|
124
|
+
connectedWorktrees = await readConnectedWorktreeSources(repoRoot, repoName, worktrees, queryForSelector, queryRepositoryForSelector);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
connectedWorktrees = { pullRequestsByBranch: new Map(), sources: [] };
|
|
128
|
+
}
|
|
129
|
+
const { pullRequestsByBranch, sources } = connectedWorktrees;
|
|
130
|
+
const entries = await buildWorktreePromptEntries(sources, {
|
|
131
|
+
queryPullRequests: async (_root, branch) => pullRequestsByBranch.get(branch) ?? [],
|
|
132
|
+
});
|
|
133
|
+
const connectedEntries = entries.filter((entry) => (entry.pullRequestNumbers?.length ?? 0) > 0);
|
|
134
|
+
if (connectedEntries.length === 0) {
|
|
135
|
+
if (lookupError !== null) {
|
|
136
|
+
options.stderr(`gji pr open: failed to look up open PRs: ${formatError(lookupError)}\n`);
|
|
137
|
+
return 1;
|
|
138
|
+
}
|
|
139
|
+
options.stderr("gji pr open: no open PR worktrees found\n");
|
|
140
|
+
return 1;
|
|
141
|
+
}
|
|
142
|
+
const selectedPath = await promptForWorktree(connectedEntries);
|
|
143
|
+
if (selectedPath === null) {
|
|
144
|
+
options.stderr("Aborted\n");
|
|
145
|
+
return 1;
|
|
146
|
+
}
|
|
147
|
+
const selectedEntry = connectedEntries.find((entry) => entry.path === selectedPath);
|
|
148
|
+
if (selectedEntry === undefined) {
|
|
149
|
+
options.stderr("gji pr open: selected worktree is no longer available\n");
|
|
150
|
+
return 1;
|
|
151
|
+
}
|
|
152
|
+
return openSelectedEntry(selectedEntry, options, promptForPullRequest, openInBrowser);
|
|
153
|
+
}
|
|
154
|
+
async function readConnectedWorktreeSources(repoRoot, repoName, worktrees, queryPullRequests, queryRepositoryPullRequests) {
|
|
155
|
+
const pullRequestsByBranch = new Map();
|
|
156
|
+
if (queryRepositoryPullRequests !== undefined) {
|
|
157
|
+
const pullRequests = await queryRepositoryPullRequests(repoRoot);
|
|
158
|
+
for (const worktree of worktrees) {
|
|
159
|
+
if (worktree.branch === null)
|
|
160
|
+
continue;
|
|
161
|
+
const branchPullRequests = pullRequests.filter((pullRequest) => pullRequest.sourceBranch === worktree.branch);
|
|
162
|
+
if (branchPullRequests.length > 0) {
|
|
163
|
+
pullRequestsByBranch.set(worktree.branch, branchPullRequests);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
const branchResults = await Promise.all(worktrees.map(async (worktree) => {
|
|
169
|
+
if (worktree.branch === null) {
|
|
170
|
+
return { pullRequests: [], worktree };
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
return {
|
|
174
|
+
pullRequests: await queryPullRequests(repoRoot, worktree.branch),
|
|
175
|
+
worktree,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return { pullRequests: [], worktree };
|
|
180
|
+
}
|
|
181
|
+
}));
|
|
182
|
+
for (const { pullRequests, worktree } of branchResults) {
|
|
183
|
+
if (worktree.branch !== null && pullRequests.length > 0) {
|
|
184
|
+
pullRequestsByBranch.set(worktree.branch, pullRequests);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const sources = worktrees.flatMap((worktree) => {
|
|
189
|
+
if (worktree.branch === null ||
|
|
190
|
+
!pullRequestsByBranch.has(worktree.branch)) {
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
return [{ repoRoot, repoName, worktree }];
|
|
194
|
+
});
|
|
195
|
+
return { pullRequestsByBranch, sources };
|
|
196
|
+
}
|
|
197
|
+
async function openByNumber(repoRoot, number, options, findOpenPullRequest, openInBrowser) {
|
|
198
|
+
let pullRequest;
|
|
199
|
+
try {
|
|
200
|
+
pullRequest = await findOpenPullRequest(repoRoot, number);
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
options.stderr(`gji pr open: failed to look up PR #${number}: ${formatError(error)}\n`);
|
|
204
|
+
return 1;
|
|
205
|
+
}
|
|
206
|
+
if (pullRequest === null) {
|
|
207
|
+
options.stderr(`gji pr open: no open PR found for #${number}\n`);
|
|
208
|
+
return 1;
|
|
209
|
+
}
|
|
210
|
+
return launchBrowser(pullRequest, options, openInBrowser);
|
|
211
|
+
}
|
|
212
|
+
async function openForBranch(repoRoot, branch, options, queryPullRequests, promptForPullRequest, openInBrowser) {
|
|
213
|
+
if (branch === null) {
|
|
214
|
+
options.stderr("gji pr open: detached worktrees have no PR branch\n");
|
|
215
|
+
return 1;
|
|
216
|
+
}
|
|
217
|
+
let pullRequests;
|
|
218
|
+
try {
|
|
219
|
+
pullRequests = await queryPullRequests(repoRoot, branch);
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
options.stderr(`gji pr open: failed to look up PRs for branch ${branch}: ${formatError(error)}\n`);
|
|
223
|
+
return 1;
|
|
224
|
+
}
|
|
225
|
+
return openPullRequests(pullRequests, options, promptForPullRequest, openInBrowser);
|
|
226
|
+
}
|
|
227
|
+
async function openSelectedEntry(entry, options, promptForPullRequest, openInBrowser) {
|
|
228
|
+
const numbers = entry.pullRequestNumbers ?? [];
|
|
229
|
+
const urls = entry.pullRequestUrls ?? [];
|
|
230
|
+
const pullRequests = numbers
|
|
231
|
+
.map((number, index) => ({
|
|
232
|
+
number,
|
|
233
|
+
sourceBranch: entry.branch ?? "",
|
|
234
|
+
url: urls[index] ?? "",
|
|
235
|
+
}))
|
|
236
|
+
.filter((pullRequest) => pullRequest.url.length > 0);
|
|
237
|
+
return openPullRequests(pullRequests, options, promptForPullRequest, openInBrowser);
|
|
238
|
+
}
|
|
239
|
+
async function openPullRequests(pullRequests, options, promptForPullRequest, openInBrowser) {
|
|
240
|
+
if (pullRequests.length === 0) {
|
|
241
|
+
options.stderr("gji pr open: no open PR found for the selected branch\n");
|
|
242
|
+
return 1;
|
|
243
|
+
}
|
|
244
|
+
if (pullRequests.length > 1 && isHeadless()) {
|
|
245
|
+
options.stderr("gji pr open: multiple open PRs found; pass an explicit PR number in non-interactive mode (GJI_NO_TUI=1)\n");
|
|
246
|
+
return 1;
|
|
247
|
+
}
|
|
248
|
+
const selected = pullRequests.length === 1
|
|
249
|
+
? pullRequests[0]
|
|
250
|
+
: await promptForPullRequest(pullRequests);
|
|
251
|
+
if (selected === null || selected === undefined) {
|
|
252
|
+
options.stderr("Aborted\n");
|
|
253
|
+
return 1;
|
|
254
|
+
}
|
|
255
|
+
return launchBrowser(selected, options, openInBrowser);
|
|
256
|
+
}
|
|
257
|
+
async function launchBrowser(pullRequest, options, openInBrowser) {
|
|
258
|
+
try {
|
|
259
|
+
await openInBrowser(pullRequest.url);
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
options.stderr(`gji pr open: failed to open PR #${pullRequest.number} in the browser: ${formatError(error)}\n`);
|
|
263
|
+
return 1;
|
|
264
|
+
}
|
|
265
|
+
options.stdout(`Opened PR #${pullRequest.number}: ${pullRequest.url}\n`);
|
|
266
|
+
return 0;
|
|
267
|
+
}
|
|
268
|
+
function parsePrNumberTarget(target) {
|
|
269
|
+
const match = target.match(/^#?(\d+)$/);
|
|
270
|
+
return match === null ? null : Number(match[1]);
|
|
271
|
+
}
|
|
272
|
+
async function defaultPromptForWorktree(worktrees) {
|
|
273
|
+
return promptForSingleWorktree("Choose a worktree with an open PR", worktrees);
|
|
274
|
+
}
|
|
275
|
+
async function defaultPromptForPullRequest(pullRequests) {
|
|
276
|
+
const choice = await select({
|
|
277
|
+
message: "Choose a pull request",
|
|
278
|
+
options: pullRequests.map((pullRequest) => ({
|
|
279
|
+
value: pullRequest.number,
|
|
280
|
+
label: `#${pullRequest.number}`,
|
|
281
|
+
hint: pullRequest.url,
|
|
282
|
+
})),
|
|
283
|
+
});
|
|
284
|
+
if (isCancel(choice))
|
|
285
|
+
return null;
|
|
286
|
+
return (pullRequests.find((pullRequest) => pullRequest.number === choice) ?? null);
|
|
287
|
+
}
|
|
288
|
+
function formatError(error) {
|
|
289
|
+
return error instanceof Error ? error.message : String(error);
|
|
290
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type PullRequestForge = "bitbucket" | "github" | "gitlab";
|
|
2
|
+
export interface PullRequestInfo {
|
|
3
|
+
number: number;
|
|
4
|
+
sourceBranch: string;
|
|
5
|
+
url: string;
|
|
6
|
+
}
|
|
7
|
+
export interface PullRequestRemote {
|
|
8
|
+
forge: PullRequestForge;
|
|
9
|
+
host: string;
|
|
10
|
+
namespace: string;
|
|
11
|
+
repository: string;
|
|
12
|
+
webBaseUrl: string;
|
|
13
|
+
}
|
|
14
|
+
export interface PullRequestCommandResult {
|
|
15
|
+
stderr?: string;
|
|
16
|
+
stdout: string;
|
|
17
|
+
}
|
|
18
|
+
export type PullRequestCommandRunner = (command: string, args: string[], options: {
|
|
19
|
+
cwd: string;
|
|
20
|
+
env: NodeJS.ProcessEnv;
|
|
21
|
+
timeout: number;
|
|
22
|
+
}) => Promise<PullRequestCommandResult>;
|
|
23
|
+
export interface PullRequestQueryDependencies {
|
|
24
|
+
fetch?: typeof fetch;
|
|
25
|
+
runCommand?: PullRequestCommandRunner;
|
|
26
|
+
}
|
|
27
|
+
export interface PullRequestQuery {
|
|
28
|
+
findOpenPullRequest: (repoRoot: string, number: number) => Promise<PullRequestInfo | null>;
|
|
29
|
+
listOpenPullRequests: (repoRoot: string, sourceBranch: string) => Promise<PullRequestInfo[]>;
|
|
30
|
+
listOpenPullRequestsForRepository: (repoRoot: string) => Promise<PullRequestInfo[]>;
|
|
31
|
+
}
|
|
32
|
+
export declare function parsePullRequestRemote(remoteUrl: string): PullRequestRemote | null;
|
|
33
|
+
export declare function createPullRequestQuery(dependencies?: PullRequestQueryDependencies): PullRequestQuery;
|