@kubohiroya/turbowarp-plus-editor-git 0.16.1 → 0.17.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.
@@ -1,4 +1,5 @@
1
- import { S as createProjectSourceDiff, _ as usePreviewSync, a as openProjectSourceTabs, c as gitHubRepositoryFromOrigin, d as assetFileName, f as organizeProjectChanges, g as createPreviewSync, h as BlockPreview, i as createScratchBlocksDiffLocator, l as ProjectSourceComparison, m as compareProjectForReview, n as openProjectComparison, p as projectPropertyLabel, r as createScratchBlocksDiffRenderer, s as createGitHubPullRequests, t as ProjectComparisonDialog, u as ProjectSourceReview, v as currentCostume, w as readProjectSourceDiff, x as compareProjectBlocks } from "./project-comparison-1rggIJse.js";
1
+ import { a as compareProjectBlocks, o as createProjectSourceDiff, u as readProjectSourceDiff } from "./project-asset-paths-nL5sV35A.js";
2
+ import { _ as usePreviewSync, a as openProjectSourceTabs, c as gitHubRepositoryFromOrigin, d as assetFileName, f as organizeProjectChanges, g as createPreviewSync, h as BlockPreview, i as createScratchBlocksDiffLocator, l as ProjectSourceComparison, m as compareProjectForReview, n as openProjectComparison, p as projectPropertyLabel, r as createScratchBlocksDiffRenderer, s as createGitHubPullRequests, t as ProjectComparisonDialog, u as ProjectSourceReview, v as currentCostume } from "./project-comparison-DjMlwyp8.js";
2
3
  import { n as init_objectSpread2, t as _objectSpread2 } from "./objectSpread2-Dkd68nJH.js";
3
4
  import React, { useEffect, useMemo, useRef, useState } from "react";
4
5
  // src/project-source-diff.tsx
@@ -3,11 +3,35 @@ import type { ProjectBlockRenderer } from './scratch-block-diff-renderer.js';
3
3
  import type { Repository } from './repository.js';
4
4
  import type { ProjectBlockLocator } from './block-diff-location.js';
5
5
  import type { ProjectSourceTabsFile } from './project-source-tabs.js';
6
+ import type { GetRemoteSession } from './remote-operations.js';
6
7
  type OpenTabs = (key: string, files: readonly ProjectSourceTabsFile[], rememberAs?: string) => number;
7
- export declare function PullRequestReviewPanel({ repository, locale, renderBlock, locateFor, openTabs }: {
8
+ /**
9
+ * Opens a pull request version fetched over the connection in the work tree: the host creates (or reuses) the local
10
+ * branch `pullRequestBranch(number)` at `head` and switches to it. `busy` disables the button.
11
+ */
12
+ export interface PullRequestOpenLocally {
13
+ open(number: number, head: string): unknown;
14
+ busy: boolean;
15
+ }
16
+ /** The person's GitHub connection: while it is connected, pull request contents are read over Git Smart HTTP. */
17
+ export interface PullRequestConnection {
18
+ getSession: GetRemoteSession;
19
+ connected(): boolean;
20
+ }
21
+ export declare function PullRequestReviewPanel({ repository, locale, renderBlock, locateFor, openTabs, connection, openLocally }: {
8
22
  repository: Repository;
9
23
  locale: string;
10
24
  renderBlock: ProjectBlockRenderer;
25
+ /**
26
+ * While connected, pull request contents are fetched over Git Smart HTTP (`refs/pull/<number>/head`) and compared
27
+ * from local Git objects, so private repositories can be reviewed. Without it, the public GitHub API is read.
28
+ */
29
+ connection?: PullRequestConnection;
30
+ /**
31
+ * Offers to open a pull request read over the connection in the work tree, on the branch `pr/<number>`. Pull
32
+ * requests read from the public API have no local objects, so the button is not shown for them.
33
+ */
34
+ openLocally?: PullRequestOpenLocally;
11
35
  /** Opens a pull request's project changes as tabs of the host's layout. */
12
36
  openTabs?: OpenTabs;
13
37
  /** Returns a locator for a project file whose PR head matches the open project, or `undefined`. */
@@ -2,4 +2,27 @@ import type { GitSmartHttpOptions } from './smart-http.js';
2
2
  export type RemoteOperation = 'fetch' | 'pull' | 'push';
3
3
  export type RemoteSession = NonNullable<Awaited<ReturnType<GitSmartHttpOptions['getSession']>>>;
4
4
  export type GetRemoteSession = (remote: string, write: boolean) => Promise<RemoteSession | null>;
5
+ /**
6
+ * Where a fetched pull request head is kept: outside `refs/heads`, `refs/remotes` and `refs/tags`, so Git CLI lists it
7
+ * with `git for-each-ref refs/pull` and `git log --all` but not as a branch or tag, `git fetch --prune` leaves it alone,
8
+ * and `git gc` keeps its objects. Each fetch of the same pull request replaces it.
9
+ */
10
+ export declare const pullRequestRef: (number: number) => string;
11
+ export interface PullRequestFetch {
12
+ readonly number: number;
13
+ /** The commit at GitHub's `refs/pull/<number>/head`, kept locally at `ref`. */
14
+ readonly head: string;
15
+ readonly ref: string;
16
+ /** GitHub's default branch, from the advertised `HEAD`, or `null` when it is not advertised. */
17
+ readonly defaultBranch: string | null;
18
+ /** The branches on GitHub, as `refs/remotes/origin/<name>` after this fetch. */
19
+ readonly branches: readonly string[];
20
+ }
21
+ /**
22
+ * Fetches GitHub's `refs/pull/<number>/head` together with every branch, over the connection's Git Smart HTTP, like
23
+ * `git fetch origin refs/pull/<number>/head`. The branches update `refs/remotes/origin/*` as fetch does, and the head
24
+ * is kept at `pullRequestRef(number)`. The work tree, the index, HEAD and `.git/config` are never written. The writes
25
+ * are staged first and applied together, under the same lock as fetch, pull and push.
26
+ */
27
+ export declare function runPullRequestFetch(root: FileSystemDirectoryHandle, number: number, getSession: GetRemoteSession, guard: () => void, signal?: AbortSignal): Promise<PullRequestFetch>;
5
28
  export declare function runRemoteOperation(root: FileSystemDirectoryHandle, operation: RemoteOperation, getSession: GetRemoteSession, guard: () => void, signal?: AbortSignal): Promise<void>;
@@ -1,5 +1,5 @@
1
1
  import { i as __toESM, n as init_objectSpread2, r as __commonJSMin, t as _objectSpread2 } from "./objectSpread2-Dkd68nJH.js";
2
- import { a as createTwoFilesPatch, i as isAssetName, n as assetsDirectoryParts, r as folderSettingsLimit, t as assetLimit } from "./project-asset-paths-KV715791.js";
2
+ import { f as createTwoFilesPatch, i as isAssetName, n as assetsDirectoryParts, o as createProjectSourceDiff, r as folderSettingsLimit, t as assetLimit } from "./project-asset-paths-nL5sV35A.js";
3
3
  import { a as _OverloadYield, i as _wrapAsyncGenerator, o as _asyncIterator, r as createGitSmartHttp, t as GitSmartHttpError } from "./smart-http-BmHEkE7K.js";
4
4
  // node_modules/inherits/inherits_browser.js
5
5
  var require_inherits_browser = /* @__PURE__ */ __commonJSMin(((exports, module) => {
@@ -16929,6 +16929,79 @@ async function walk({ fs, dir, gitdir = join(dir, ".git"), trees, map, reduce, i
16929
16929
  throw err;
16930
16930
  }
16931
16931
  }
16932
+ /**
16933
+ * Write a ref which refers to the specified SHA-1 object id, or a symbolic ref which refers to the specified ref.
16934
+ *
16935
+ * @param {object} args
16936
+ * @param {FsClient} args.fs - a file system client
16937
+ * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
16938
+ * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
16939
+ * @param {string} args.ref - The name of the ref to write
16940
+ * @param {string} args.value - When `symbolic` is false, a ref or an SHA-1 object id. When true, a ref starting with `refs/`.
16941
+ * @param {boolean} [args.force = false] - Instead of throwing an error if a ref named `ref` already exists, overwrite the existing ref.
16942
+ * @param {boolean} [args.symbolic = false] - Whether the ref is symbolic or not.
16943
+ *
16944
+ * @returns {Promise<void>} Resolves successfully when filesystem operations are complete
16945
+ *
16946
+ * @example
16947
+ * await git.writeRef({
16948
+ * fs,
16949
+ * dir: '/tutorial',
16950
+ * ref: 'refs/heads/another-branch',
16951
+ * value: 'HEAD'
16952
+ * })
16953
+ * await git.writeRef({
16954
+ * fs,
16955
+ * dir: '/tutorial',
16956
+ * ref: 'HEAD',
16957
+ * value: 'refs/heads/another-branch',
16958
+ * force: true,
16959
+ * symbolic: true
16960
+ * })
16961
+ * console.log('done')
16962
+ *
16963
+ */
16964
+ async function writeRef({ fs: _fs, dir, gitdir = join(dir, ".git"), ref, value, force = false, symbolic = false }) {
16965
+ try {
16966
+ assertParameter("fs", _fs);
16967
+ assertParameter("gitdir", gitdir);
16968
+ assertParameter("ref", ref);
16969
+ assertParameter("value", value);
16970
+ const fs = new FileSystem(_fs);
16971
+ if (!isValidRef(ref, true)) throw new InvalidRefNameError(ref, import_lib.default.clean(ref));
16972
+ const updatedGitdir = await discoverGitdir({
16973
+ fsp: fs,
16974
+ dotgit: gitdir
16975
+ });
16976
+ if (!force && await GitRefManager.exists({
16977
+ fs,
16978
+ gitdir: updatedGitdir,
16979
+ ref
16980
+ })) throw new AlreadyExistsError("ref", ref);
16981
+ if (symbolic) await GitRefManager.writeSymbolicRef({
16982
+ fs,
16983
+ gitdir: updatedGitdir,
16984
+ ref,
16985
+ value
16986
+ });
16987
+ else {
16988
+ value = await GitRefManager.resolve({
16989
+ fs,
16990
+ gitdir: updatedGitdir,
16991
+ ref: value
16992
+ });
16993
+ await GitRefManager.writeRef({
16994
+ fs,
16995
+ gitdir: updatedGitdir,
16996
+ ref,
16997
+ value
16998
+ });
16999
+ }
17000
+ } catch (err) {
17001
+ err.caller = "git.writeRef";
17002
+ throw err;
17003
+ }
17004
+ }
16932
17005
  // src/filesystem.ts
16933
17006
  var GitPanelError = class extends Error {
16934
17007
  constructor(code, message = code) {
@@ -17342,8 +17415,104 @@ function _asyncGeneratorDelegate(t) {
17342
17415
  }
17343
17416
  // src/remote-operations.ts
17344
17417
  init_objectSpread2();
17345
- async function runRemoteOperation(root, operation, getSession, guard, signal) {
17418
+ var sessionCodes = [
17419
+ "SESSION_REQUIRED",
17420
+ "SESSION_UNAVAILABLE",
17421
+ "SESSION_EXPIRED",
17422
+ "REAUTH_REQUIRED",
17423
+ "INVALID_SESSION"
17424
+ ];
17425
+ async function checkRefspecs(options) {
17426
+ if ((await getConfigAll(_objectSpread2(_objectSpread2({}, options), {}, { path: "remote.origin.fetch" }))).some((spec) => !/^\+?refs\/heads\/[^:\s]+:refs\/remotes\/origin\/[^:\s]+$/u.test(spec))) throw new GitPanelError("unsupported");
17427
+ }
17428
+ /**
17429
+ * Where a fetched pull request head is kept: outside `refs/heads`, `refs/remotes` and `refs/tags`, so Git CLI lists it
17430
+ * with `git for-each-ref refs/pull` and `git log --all` but not as a branch or tag, `git fetch --prune` leaves it alone,
17431
+ * and `git gc` keeps its objects. Each fetch of the same pull request replaces it.
17432
+ */
17433
+ var pullRequestRef = (number) => `refs/pull/origin/${number}/head`;
17434
+ /**
17435
+ * Fetches GitHub's `refs/pull/<number>/head` together with every branch, over the connection's Git Smart HTTP, like
17436
+ * `git fetch origin refs/pull/<number>/head`. The branches update `refs/remotes/origin/*` as fetch does, and the head
17437
+ * is kept at `pullRequestRef(number)`. The work tree, the index, HEAD and `.git/config` are never written. The writes
17438
+ * are staged first and applied together, under the same lock as fetch, pull and push.
17439
+ */
17440
+ async function runPullRequestFetch(root, number, getSession, guard, signal) {
17346
17441
  var _globalThis$navigator;
17442
+ if (!Number.isSafeInteger(number) || number < 1) throw new GitPanelError("pullRequestNotFound");
17443
+ const execute = async () => {
17444
+ var _result$defaultBranch;
17445
+ guard();
17446
+ const transaction = createMutationFs(root, false);
17447
+ const options = {
17448
+ fs: transaction.fs,
17449
+ dir: "/",
17450
+ gitdir: "/.git",
17451
+ cache: {}
17452
+ };
17453
+ const remote = await getConfig(_objectSpread2(_objectSpread2({}, options), {}, { path: "remote.origin.url" }));
17454
+ if (typeof remote !== "string") throw new GitPanelError("session");
17455
+ await checkRefspecs(options);
17456
+ const session = await getSession(remote, false);
17457
+ if (!session) throw new GitPanelError("session");
17458
+ guard();
17459
+ const transport = createGitSmartHttp({
17460
+ remote,
17461
+ connection: session.connection,
17462
+ getSession: () => getSession(remote, false),
17463
+ signal
17464
+ });
17465
+ const source = `refs/pull/${number}/head`;
17466
+ let result;
17467
+ try {
17468
+ result = await fetch(_objectSpread2(_objectSpread2(_objectSpread2({}, options), transport), {}, {
17469
+ remote: "origin",
17470
+ remoteRef: source,
17471
+ tags: false
17472
+ }));
17473
+ } catch (error) {
17474
+ const { code, data } = error;
17475
+ if (code === "NotFoundError" && (data === null || data === void 0 ? void 0 : data.what) === source) throw new GitPanelError("pullRequestNotFound");
17476
+ throw remoteError(error);
17477
+ }
17478
+ const head = result.fetchHead;
17479
+ if (!head || !/^[0-9a-f]{40}$/u.test(head)) throw new GitPanelError("remoteFailed");
17480
+ const ref = pullRequestRef(number);
17481
+ await writeRef(_objectSpread2(_objectSpread2({}, options), {}, {
17482
+ ref,
17483
+ value: head,
17484
+ force: true
17485
+ }));
17486
+ const branches = (await listBranches(_objectSpread2(_objectSpread2({}, options), {}, { remote: "origin" }))).filter((name) => name !== "HEAD").sort();
17487
+ const advertised = ((_result$defaultBranch = result.defaultBranch) === null || _result$defaultBranch === void 0 ? void 0 : _result$defaultBranch.startsWith("refs/heads/")) ? result.defaultBranch.slice(11) : null;
17488
+ await transaction.commit(guard);
17489
+ return {
17490
+ number,
17491
+ head,
17492
+ ref,
17493
+ defaultBranch: advertised && branches.includes(advertised) ? advertised : null,
17494
+ branches
17495
+ };
17496
+ };
17497
+ return ((_globalThis$navigator = globalThis.navigator) === null || _globalThis$navigator === void 0 ? void 0 : _globalThis$navigator.locks) ? navigator.locks.request("tw-plus-git-mutation", execute) : execute();
17498
+ }
17499
+ /** Maps a failed read from GitHub to what the person can act on: the connection, access, or the network. */
17500
+ function remoteError(error) {
17501
+ if (error instanceof GitPanelError) return error;
17502
+ if (error instanceof GitSmartHttpError) {
17503
+ if (sessionCodes.includes(error.code)) return new GitPanelError("session");
17504
+ if (error.code === "FORBIDDEN") return new GitPanelError("forbidden");
17505
+ if ([
17506
+ "NETWORK_ERROR",
17507
+ "UPSTREAM_FAILURE",
17508
+ "UPSTREAM_TIMEOUT",
17509
+ "RATE_LIMITED"
17510
+ ].includes(error.code)) return new GitPanelError("network");
17511
+ }
17512
+ return new GitPanelError("remoteFailed");
17513
+ }
17514
+ async function runRemoteOperation(root, operation, getSession, guard, signal) {
17515
+ var _globalThis$navigator2;
17347
17516
  const execute = async () => {
17348
17517
  guard();
17349
17518
  const transaction = createMutationFs(root, operation === "pull");
@@ -17361,7 +17530,7 @@ async function runRemoteOperation(root, operation, getSession, guard, signal) {
17361
17530
  const trackingRemote = await config("branch." + ref + ".remote");
17362
17531
  const trackingRef = await config("branch." + ref + ".merge");
17363
17532
  if (trackingRemote && trackingRemote !== "origin" || trackingRef && !trackingRef.startsWith("refs/heads/")) throw new GitPanelError("unsupported");
17364
- if ((await getConfigAll(_objectSpread2(_objectSpread2({}, options), {}, { path: "remote.origin.fetch" }))).some((spec) => !/^\+?refs\/heads\/[^:\s]+:refs\/remotes\/origin\/[^:\s]+$/u.test(spec))) throw new GitPanelError("unsupported");
17533
+ await checkRefspecs(options);
17365
17534
  if (operation === "pull") {
17366
17535
  if ((await statusMatrix(_objectSpread2(_objectSpread2({}, options), {}, { refresh: false }))).some(([, head, worktree, stage]) => head !== worktree || head !== stage)) throw new GitPanelError("pullUnsafe");
17367
17536
  await walk(_objectSpread2(_objectSpread2({}, options), {}, {
@@ -17435,10 +17604,15 @@ async function runRemoteOperation(root, operation, getSession, guard, signal) {
17435
17604
  throw error;
17436
17605
  }
17437
17606
  };
17438
- return ((_globalThis$navigator = globalThis.navigator) === null || _globalThis$navigator === void 0 ? void 0 : _globalThis$navigator.locks) ? navigator.locks.request("tw-plus-git-mutation", execute) : execute();
17607
+ return ((_globalThis$navigator2 = globalThis.navigator) === null || _globalThis$navigator2 === void 0 ? void 0 : _globalThis$navigator2.locks) ? navigator.locks.request("tw-plus-git-mutation", execute) : execute();
17439
17608
  }
17440
17609
  // src/local-operations.ts
17441
17610
  init_objectSpread2();
17611
+ /**
17612
+ * The local branch a pull request is opened on: `pr/<number>`, a name `git check-ref-format --branch` accepts. It is
17613
+ * created at the fetched head (`pullRequestRef(number)`) and is an ordinary branch afterwards.
17614
+ */
17615
+ var pullRequestBranch = (number) => `pr/${number}`;
17442
17616
  var missing = (error) => {
17443
17617
  var _errorCode;
17444
17618
  return [
@@ -17490,6 +17664,7 @@ async function runLocalOperation(root, operation, guard) {
17490
17664
  }
17491
17665
  if (operation.kind === "createBranch") {
17492
17666
  let object;
17667
+ if (operation.reuse && operation.at === void 0) throw new GitPanelError("invalid");
17493
17668
  if (operation.at !== void 0) {
17494
17669
  if (operation.fromRemote || !/^[0-9a-f]{40}$/.test(operation.at)) throw new GitPanelError("invalid");
17495
17670
  object = await readCommit(_objectSpread2(_objectSpread2({}, options), {}, { oid: operation.at })).then(() => operation.at, () => {
@@ -17503,7 +17678,15 @@ async function runLocalOperation(root, operation, guard) {
17503
17678
  else object = await resolveRef(_objectSpread2(_objectSpread2({}, options), {}, { ref: "HEAD" })).catch(() => {
17504
17679
  throw new GitPanelError("unsupported");
17505
17680
  });
17506
- try {
17681
+ let existing;
17682
+ if (operation.reuse && (await listBranches(options)).includes(name)) {
17683
+ existing = await resolveRef(_objectSpread2(_objectSpread2({}, options), {}, { ref: local }));
17684
+ if (existing !== object) throw new GitPanelError("pullRequestBranch");
17685
+ }
17686
+ if (existing === void 0) {
17687
+ if ((await listBranches(options)).some((branch) => branch !== name && (name.startsWith(`${branch}/`) || branch.startsWith(`${name}/`)))) throw new GitPanelError("branchConflict");
17688
+ }
17689
+ if (existing === void 0) try {
17507
17690
  await branch(_objectSpread2(_objectSpread2({}, options), {}, {
17508
17691
  ref: name,
17509
17692
  object,
@@ -17583,6 +17766,8 @@ init_objectSpread2();
17583
17766
  var historyPageSize = 50;
17584
17767
  var projectSourceLimit = 16777216;
17585
17768
  var emptyTree = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
17769
+ var isSha = (value) => typeof value === "string" && /^[0-9a-f]{40}$/u.test(value);
17770
+ var validBranch = (name) => /^[^\s~^:?*[\\]+$/u.test(name) && !name.startsWith("/") && !name.endsWith("/") && !name.endsWith(".lock") && !name.includes("..") && !name.includes("//") && !name.includes("@{");
17586
17771
  var textLimit = 1048576;
17587
17772
  var inScope = (path, scope) => !scope || path === scope || path.startsWith(scope + "/");
17588
17773
  var probeLimit = 5e3;
@@ -18258,6 +18443,146 @@ async function openRepository(root) {
18258
18443
  }
18259
18444
  };
18260
18445
  };
18446
+ const hasCommit = async (oid) => {
18447
+ try {
18448
+ await readCommit(_objectSpread2(_objectSpread2({}, options), {}, {
18449
+ cache,
18450
+ oid
18451
+ }));
18452
+ return true;
18453
+ } catch (_unused5) {
18454
+ return false;
18455
+ }
18456
+ };
18457
+ /** Lists the paths that differ between two trees, skipping subtrees with the same object. */
18458
+ const treeChanges = async (before, after) => {
18459
+ const file = async (entry) => entry && await entry.type() !== "tree" ? {
18460
+ oid: await entry.oid(),
18461
+ mode: await entry.mode()
18462
+ } : null;
18463
+ return (await walk(_objectSpread2(_objectSpread2({}, options), {}, {
18464
+ cache,
18465
+ trees: [TREE({ ref: before }), TREE({ ref: after })],
18466
+ map: async (path, [a, b]) => {
18467
+ if (path === ".") return;
18468
+ if (a && b && await a.oid() === await b.oid() && await a.mode() === await b.mode()) return null;
18469
+ const change = {
18470
+ path,
18471
+ before: await file(a),
18472
+ after: await file(b)
18473
+ };
18474
+ return change.before || change.after ? change : void 0;
18475
+ }
18476
+ }))).sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
18477
+ };
18478
+ const readBlobText = async (entry) => {
18479
+ if (!entry) return "{\"targets\":[]}\n";
18480
+ if (entry.mode !== 33188 && entry.mode !== 33261) throw new GitPanelError("unsupported");
18481
+ const { blob } = await readBlob(_objectSpread2(_objectSpread2({}, options), {}, {
18482
+ cache,
18483
+ oid: entry.oid
18484
+ }));
18485
+ if (blob.byteLength > projectSourceLimit) throw new GitPanelError("large");
18486
+ return new TextDecoder("utf-8", { fatal: true }).decode(blob);
18487
+ };
18488
+ const readTreeAsset = async (ref, folder, md5ext, signal) => {
18489
+ if (signal.aborted) return null;
18490
+ const source = {
18491
+ kind: "tree",
18492
+ ref
18493
+ };
18494
+ const settings = await readVersion(source, [...folder, "sb3-source.json"].join("/"), folderSettingsLimit);
18495
+ const directory = assetsDirectoryParts(settings && new TextDecoder().decode(settings));
18496
+ if (!directory || signal.aborted) return null;
18497
+ const bytes = await readVersion(source, [
18498
+ ...folder,
18499
+ ...directory,
18500
+ md5ext
18501
+ ].join("/"), assetLimit);
18502
+ return bytes && !signal.aborted ? new Blob([bytes]) : null;
18503
+ };
18504
+ const pullRequestAssets = (versions) => ({ version, md5ext, signal }) => {
18505
+ const location = versions[version];
18506
+ if (!location || !isAssetName(md5ext) || signal.aborted) return Promise.resolve(null);
18507
+ return serial(() => readTreeAsset(location.ref, pathParts(location.path).slice(0, -1), md5ext, signal)).catch(() => null);
18508
+ };
18509
+ /** Compares a fetched pull request head with its merge base, from local Git objects only. */
18510
+ const readPullRequest = async (head, base) => {
18511
+ var _added$get, _base$branch;
18512
+ if (!isSha(head) || !await hasCommit(head)) throw new GitPanelError("pullRequestNotFound");
18513
+ let sha;
18514
+ if (isSha(base.sha) && await hasCommit(base.sha)) sha = base.sha;
18515
+ else if (base.branch !== void 0 && validBranch(base.branch)) try {
18516
+ sha = await resolveRef(_objectSpread2(_objectSpread2({}, options), {}, { ref: "refs/remotes/origin/" + base.branch }));
18517
+ } catch (_unused6) {}
18518
+ if (!sha || !await hasCommit(sha)) throw new GitPanelError("baseMissing");
18519
+ const [mergeBase = null] = await findMergeBase(_objectSpread2(_objectSpread2({}, options), {}, {
18520
+ cache,
18521
+ oids: [sha, head]
18522
+ }));
18523
+ const changes = await treeChanges(mergeBase !== null && mergeBase !== void 0 ? mergeBase : emptyTree, head);
18524
+ const added = /* @__PURE__ */ new Map();
18525
+ for (const change of changes) if (!change.before && change.after) added.set(change.after.oid, [...(_added$get = added.get(change.after.oid)) !== null && _added$get !== void 0 ? _added$get : [], change]);
18526
+ const renamed = /* @__PURE__ */ new Map();
18527
+ for (const change of changes) {
18528
+ const candidates = change.before && !change.after ? added.get(change.before.oid) : void 0;
18529
+ const target = candidates === null || candidates === void 0 ? void 0 : candidates.find((candidate) => !renamed.has(candidate));
18530
+ if (target) renamed.set(target, change);
18531
+ }
18532
+ const removedByRename = new Set(renamed.values());
18533
+ const files = [];
18534
+ for (const change of changes) {
18535
+ var _from$before;
18536
+ if (removedByRename.has(change)) continue;
18537
+ const from = renamed.get(change);
18538
+ const before = (_from$before = from === null || from === void 0 ? void 0 : from.before) !== null && _from$before !== void 0 ? _from$before : change.before;
18539
+ const previousPath = from === null || from === void 0 ? void 0 : from.path;
18540
+ const status = !before ? "added" : !change.after ? "removed" : "updated";
18541
+ if (change.path.split("/").pop() !== "project.source.json") {
18542
+ files.push({
18543
+ path: change.path,
18544
+ previousPath,
18545
+ status
18546
+ });
18547
+ continue;
18548
+ }
18549
+ try {
18550
+ const patch = createProjectSourceDiff(await readBlobText(before), await readBlobText(change.after));
18551
+ const loadAsset = pullRequestAssets({
18552
+ before: before && mergeBase ? {
18553
+ ref: mergeBase,
18554
+ path: previousPath !== null && previousPath !== void 0 ? previousPath : change.path
18555
+ } : null,
18556
+ after: change.after ? {
18557
+ ref: head,
18558
+ path: change.path
18559
+ } : null
18560
+ });
18561
+ files.push({
18562
+ path: change.path,
18563
+ previousPath,
18564
+ patch,
18565
+ loadAsset
18566
+ });
18567
+ } catch (_unused7) {
18568
+ files.push({
18569
+ path: change.path,
18570
+ previousPath,
18571
+ status,
18572
+ unavailable: true
18573
+ });
18574
+ }
18575
+ }
18576
+ return {
18577
+ base: {
18578
+ ref: (_base$branch = base.branch) !== null && _base$branch !== void 0 ? _base$branch : null,
18579
+ sha
18580
+ },
18581
+ head,
18582
+ mergeBase,
18583
+ files
18584
+ };
18585
+ };
18261
18586
  const directoryAt = async (path) => {
18262
18587
  let directory = root;
18263
18588
  for (const part of pathParts(path)) directory = await directory.getDirectoryHandle(part);
@@ -18335,6 +18660,20 @@ async function openRepository(root) {
18335
18660
  throw error;
18336
18661
  }
18337
18662
  }),
18663
+ /**
18664
+ * Fetches a pull request's head (`refs/pull/<number>/head`, which also covers pull requests from forks) and every
18665
+ * branch over the connection; see `runPullRequestFetch`. Only Git objects and refs are written.
18666
+ */
18667
+ fetchPullRequest: (number, getSession, guard, signal) => serial(async () => {
18668
+ const result = await runPullRequestFetch(root, number, getSession, guard, signal);
18669
+ cache = {};
18670
+ return result;
18671
+ }),
18672
+ /**
18673
+ * Reads the changes of a fetched pull request head against a base, from local Git objects: the changed paths
18674
+ * from the merge base to the head, and each project file's diff with assets read from the same commits.
18675
+ */
18676
+ readPullRequest: (head, base) => serial(() => readPullRequest(head, base)),
18338
18677
  /** Stages, unstages or commits; see `runLocalOperation`. `guard` throws to stop before each write. */
18339
18678
  localOperation: (operation, guard) => serial(() => runLocalOperation(root, operation, guard)),
18340
18679
  /**
@@ -18350,7 +18689,7 @@ async function openRepository(root) {
18350
18689
  filepath: path
18351
18690
  }));
18352
18691
  return tree.length > 0;
18353
- } catch (_unused5) {
18692
+ } catch (_unused8) {
18354
18693
  return false;
18355
18694
  }
18356
18695
  }),
@@ -18378,4 +18717,4 @@ async function openRepository(root) {
18378
18717
  projectAssetLoader: (snapshot, folder, comparison) => ({ version, md5ext, signal }) => !isAssetName(md5ext) || signal.aborted ? Promise.resolve(null) : serial(() => readFolderAsset(snapshot, pathParts(folder), () => comparisonSource(snapshot, comparison, version), md5ext, signal)).catch(() => null)
18379
18718
  };
18380
18719
  }
18381
- export { createReadOnlyFs as a, GitPanelError as i, openRepository as n, upstreamWalkLimit as r, historyPageSize as t };
18720
+ export { pullRequestRef as a, pullRequestBranch as i, openRepository as n, GitPanelError as o, upstreamWalkLimit as r, createReadOnlyFs as s, historyPageSize as t };
@@ -1,7 +1,10 @@
1
+ import type { ProjectSourceReviewFile } from './project-source-review.js';
1
2
  import type { LocalOperation } from './local-operations.js';
2
3
  import type { GetRemoteSession, RemoteOperation } from './remote-operations.js';
3
4
  import type { ProjectAssetLoader } from './project-assets.js';
4
5
  export { createReadOnlyFs, GitPanelError } from './filesystem.js';
6
+ export { pullRequestRef } from './remote-operations.js';
7
+ export type { PullRequestFetch } from './remote-operations.js';
5
8
  export type ChangeGroup = 'staged' | 'unstaged' | 'untracked' | 'commit';
6
9
  export interface FileChange {
7
10
  path: string;
@@ -14,6 +17,25 @@ export interface FileChange {
14
17
  /** For `commit` changes, the commit compared with its first parent. */
15
18
  commit?: string;
16
19
  }
20
+ /** A version of the pull request's base to compare with: GitHub's base commit when known, else a branch on origin. */
21
+ export interface PullRequestBase {
22
+ /** The base commit GitHub reported, used when the fetch brought it. */
23
+ sha?: string;
24
+ /** The base branch, read from `refs/remotes/origin/<branch>` when `sha` is missing or not local. */
25
+ branch?: string;
26
+ }
27
+ export interface PullRequestFiles {
28
+ /** The base that was compared: the branch it was named by, if any, and its commit. */
29
+ base: {
30
+ ref: string | null;
31
+ sha: string;
32
+ };
33
+ head: string;
34
+ /** The merge base of `base.sha` and `head`, like GitHub's three-dot comparison; `null` for unrelated histories. */
35
+ mergeBase: string | null;
36
+ /** Every changed path from the merge base to the head, with project data as a project-source diff. */
37
+ files: ProjectSourceReviewFile[];
38
+ }
17
39
  export interface Commit {
18
40
  oid: string;
19
41
  message: string;
@@ -116,6 +138,16 @@ export declare function openRepository(root: FileSystemDirectoryHandle): Promise
116
138
  probe: (scope?: string) => Promise<string>;
117
139
  readOrigin: () => Promise<any>;
118
140
  remoteOperation: (operation: RemoteOperation, getSession: GetRemoteSession, guard: () => void, signal?: AbortSignal) => Promise<void>;
141
+ /**
142
+ * Fetches a pull request's head (`refs/pull/<number>/head`, which also covers pull requests from forks) and every
143
+ * branch over the connection; see `runPullRequestFetch`. Only Git objects and refs are written.
144
+ */
145
+ fetchPullRequest: (number: number, getSession: GetRemoteSession, guard: () => void, signal?: AbortSignal) => Promise<import("./remote-operations.js").PullRequestFetch>;
146
+ /**
147
+ * Reads the changes of a fetched pull request head against a base, from local Git objects: the changed paths
148
+ * from the merge base to the head, and each project file's diff with assets read from the same commits.
149
+ */
150
+ readPullRequest: (head: string, base: PullRequestBase) => Promise<PullRequestFiles>;
119
151
  /** Stages, unstages or commits; see `runLocalOperation`. `guard` throws to stop before each write. */
120
152
  localOperation: (operation: LocalOperation, guard: () => void) => Promise<void>;
121
153
  /**
@@ -1 +1 @@
1
- export { i as GitPanelError, a as createReadOnlyFs, t as historyPageSize, n as openRepository, r as upstreamWalkLimit } from "./repository-BYno-5f8.js";
1
+ export { o as GitPanelError, s as createReadOnlyFs, t as historyPageSize, n as openRepository, a as pullRequestRef, r as upstreamWalkLimit } from "./repository-xIv0iZkj.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubohiroya/turbowarp-plus-editor-git",
3
- "version": "0.16.1",
3
+ "version": "0.17.0",
4
4
  "description": "Read-only local Git panel for TurboWarp Plus Editor",
5
5
  "type": "module",
6
6
  "license": "MPL-2.0",
package/src/filesystem.ts CHANGED
@@ -2,7 +2,8 @@ export class GitPanelError extends Error {
2
2
  constructor(public code: 'invalid' | 'changed' | 'large' | 'unsupported' | 'pullUnsafe' | 'diverged' |
3
3
  'session' | 'pushUnknown' | 'pushRefresh' | 'rollbackFailed' | 'remoteFailed' |
4
4
  'saveFirst' | 'author' | 'nothingStaged' | 'commitMessage' |
5
- 'switchUnsafe' | 'branchExists' | 'branchName' | 'locked', message: string = code) {
5
+ 'switchUnsafe' | 'branchExists' | 'branchConflict' | 'pullRequestBranch' | 'branchName' | 'locked' |
6
+ 'pullRequestNotFound' | 'forbidden' | 'network' | 'baseMissing', message: string = code) {
6
7
  super(message);
7
8
  this.name = 'GitPanelError';
8
9
  }
@@ -165,6 +165,13 @@ export function createGitHubPullRequests(repository: string, send: typeof fetch
165
165
  await request(repoPath(name) + '/contents/' + contentPath(path) + '?ref=' + revision, signal, 'application/vnd.github.raw+json'), sourceLimit);
166
166
 
167
167
  return {
168
+ /** Reads one pull request's title, description, state and both revisions, without its files (one request). */
169
+ async metadata(number: number, signal: AbortSignal) {
170
+ if (!Number.isSafeInteger(number) || number < 1) throw invalidResponse();
171
+ const pr = await metadata(number, signal);
172
+ if (pr.number !== number) throw invalidResponse();
173
+ return {...pr, url: 'https://github.com/' + repository + '/pull/' + number};
174
+ },
168
175
  async list(page: number, signal: AbortSignal) {
169
176
  const result = await json(repo + '/pulls?state=all&sort=updated&per_page=20&page=' + page, signal);
170
177
  if (!Array.isArray(result)) throw invalidResponse();