@kubohiroya/turbowarp-plus-editor-git 0.16.1 → 0.18.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/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
- import { C as parseProjectSource, T as CloseButton, a as openProjectSourceTabs, b as useCostumeImage, c as gitHubRepositoryFromOrigin, f as organizeProjectChanges, i as createScratchBlocksDiffLocator, l as ProjectSourceComparison, n as openProjectComparison, o as PullRequestError, p as projectPropertyLabel, r as createScratchBlocksDiffRenderer, s as createGitHubPullRequests, u as ProjectSourceReview, w as readProjectSourceDiff, y as useAssetImage } from "./project-comparison-1rggIJse.js";
1
+ import { a as openProjectSourceTabs, b as useCostumeImage, c as gitHubRepositoryFromOrigin, f as organizeProjectChanges, i as createScratchBlocksDiffLocator, l as ProjectSourceComparison, n as openProjectComparison, o as PullRequestError, p as projectPropertyLabel, r as createScratchBlocksDiffRenderer, s as createGitHubPullRequests, u as ProjectSourceReview, x as CloseButton, y as useAssetImage } from "./project-comparison-BXOQcZ73.js";
2
2
  import { n as init_objectSpread2, r as __commonJSMin, t as _objectSpread2$1 } from "./objectSpread2-Dkd68nJH.js";
3
- import { a as createReadOnlyFs, i as GitPanelError, n as openRepository } from "./repository-BYno-5f8.js";
3
+ import { f as _objectWithoutProperties$1, l as parseProjectSource, u as readProjectSourceDiff } from "./project-asset-paths-BzwVf9HE.js";
4
+ import { a as pullRequestRef, i as pullRequestBranch, n as openRepository, o as GitPanelError, s as createReadOnlyFs } from "./repository-ByKRBftJ.js";
4
5
  import * as m from "react";
5
- import React, { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
6
+ import React, { createContext, memo, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
6
7
  import { createPortal, render, unmountComponentAtNode } from "react-dom";
7
8
  // builtin:esm-external-require-react
8
9
  var require_builtin_esm_external_require_react = /* @__PURE__ */ __commonJSMin(((exports, module) => {
@@ -3025,91 +3026,12 @@ function ProjectElementView({ changes, selection, locale, renderBlock, onSelectF
3025
3026
  onClick: () => onSelectFile(file)
3026
3027
  }, file.path, " ", /* @__PURE__ */ React.createElement("small", null, labels.groups[file.group])))))));
3027
3028
  }
3028
- /**
3029
- * Lays out commits ordered newest first with every child above its parents. Parents missing from `commits`, such as
3030
- * those on a page not loaded yet or cut off by a shallow clone, keep their lane running to the bottom.
3031
- */
3032
- function layoutHistory(commits) {
3033
- const lanes = [];
3034
- let nextColor = 0;
3035
- const free = () => {
3036
- const index = lanes.indexOf(null);
3037
- return index === -1 ? lanes.length : index;
3038
- };
3039
- const rows = [];
3040
- for (const commit of commits) {
3041
- const segments = [];
3042
- const before = lanes.slice();
3043
- const waiting = before.flatMap((lane, index) => (lane === null || lane === void 0 ? void 0 : lane.oid) === commit.oid ? [index] : []);
3044
- const column = waiting.length ? waiting[0] : free();
3045
- const color = waiting.length ? before[column].color : nextColor++ % 8;
3046
- for (const [index, lane] of before.entries()) {
3047
- if (!lane) continue;
3048
- segments.push({
3049
- part: "top",
3050
- from: index,
3051
- to: lane.oid === commit.oid ? column : index,
3052
- color: lane.color
3053
- });
3054
- }
3055
- for (const index of waiting) lanes[index] = null;
3056
- const parents = [...new Set(commit.parents)];
3057
- lanes[column] = parents.length ? {
3058
- oid: parents[0],
3059
- color
3060
- } : null;
3061
- const joins = parents.length ? [{
3062
- index: column,
3063
- color
3064
- }] : [];
3065
- for (const parent of parents.slice(1)) {
3066
- let index = lanes.findIndex((lane) => (lane === null || lane === void 0 ? void 0 : lane.oid) === parent);
3067
- if (index === -1) {
3068
- index = free();
3069
- lanes[index] = {
3070
- oid: parent,
3071
- color: nextColor++ % 8
3072
- };
3073
- }
3074
- joins.push({
3075
- index,
3076
- color: lanes[index].color
3077
- });
3078
- }
3079
- while (lanes.length && lanes[lanes.length - 1] === null) lanes.pop();
3080
- for (const [index, lane] of before.entries()) if (lane && lane.oid !== commit.oid) segments.push({
3081
- part: "bottom",
3082
- from: index,
3083
- to: index,
3084
- color: lane.color
3085
- });
3086
- for (const join of joins) segments.push({
3087
- part: "bottom",
3088
- from: column,
3089
- to: join.index,
3090
- color: join.color
3091
- });
3092
- const width = Math.max(column + 1, ...segments.map((segment) => Math.max(segment.from, segment.to) + 1));
3093
- rows.push({
3094
- oid: commit.oid,
3095
- column,
3096
- color,
3097
- segments,
3098
- width
3099
- });
3100
- }
3101
- return {
3102
- rows,
3103
- width: Math.max(0, ...rows.map((row) => row.width))
3104
- };
3105
- }
3106
3029
  // src/messages.ts
3107
3030
  var messages = {
3108
3031
  en: {
3109
3032
  pullRequests: "Pull requests",
3110
3033
  localChanges: "Local changes",
3111
3034
  title: "Local Git",
3112
- menu: "Local Git…",
3113
3035
  close: "Close",
3114
3036
  choose: "Choose Repository…",
3115
3037
  refresh: "Refresh",
@@ -3150,6 +3072,10 @@ var messages = {
3150
3072
  commitMessage: "Write a message for this record.",
3151
3073
  switchUnsafe: "Stop the project, and save or record (or undo) every change, before switching branches.",
3152
3074
  branchExists: "A branch with this name already exists.",
3075
+ branchConflict: "This name clashes with an existing branch: Git cannot keep both a branch and another branch under its name followed by /, such as pr and pr/7. Rename or delete the other branch with Git, or choose another name.",
3076
+ pullRequestBranch: "The branch pr/<number> already exists with other records, so this version was not opened. The branch is left unchanged. Switch to it in Local changes, or rename or delete it with Git, then open the pull request here again.",
3077
+ openedPullRequest: "Opened pull request #{number} on the local branch {branch}. To go back, switch to {previous} in Local changes.",
3078
+ openedPullRequestDetached: "Opened pull request #{number} on the local branch {branch}. To go back, switch to another branch in Local changes.",
3153
3079
  locked: "Another Git tool is working on this repository (a lock file, or a merge, cherry-pick, revert or rebase in progress). Try again after it finishes.",
3154
3080
  branchName: "Git cannot use this name for a branch.",
3155
3081
  pullUnsafe: "Stop the project, and save or undo unsaved edits and unrecorded changes, before getting changes from GitHub (pull).",
@@ -3211,13 +3137,16 @@ var messages = {
3211
3137
  historyCopied: "Copied the commit ID.",
3212
3138
  historyCopyFailed: "Could not copy the commit ID.",
3213
3139
  historyRefs: "Branches",
3214
- cancel: "Cancel"
3140
+ cancel: "Cancel",
3141
+ branchMenu: "Branches",
3142
+ openPanel: "Open Local Git",
3143
+ newBranchItem: "New Branch…",
3144
+ newBranchTitle: "New Branch"
3215
3145
  },
3216
3146
  ja: {
3217
3147
  pullRequests: "プルリクエスト",
3218
3148
  localChanges: "ローカルの変更",
3219
3149
  title: "ローカル Git",
3220
- menu: "ローカル Git…",
3221
3150
  close: "閉じる",
3222
3151
  choose: "リポジトリを選択…",
3223
3152
  refresh: "再取得",
@@ -3258,6 +3187,10 @@ var messages = {
3258
3187
  commitMessage: "この記録の説明を書いてください。",
3259
3188
  switchUnsafe: "ブランチを切り替える前に、実行を止め、すべての変更を保存して記録(または取り消し)してください。",
3260
3189
  branchExists: "同じ名前のブランチがすでにあります。",
3190
+ branchConflict: "この名前は既存のブランチとぶつかります。Git では、pr と pr/7 のように、あるブランチと、その名前に / を続けたブランチを両方は持てません。Git でもう一方の名前を変えるか消すか、別の名前にしてください。",
3191
+ pullRequestBranch: "ブランチ pr/<番号> が、別の記録を指してすでにあるため、この版を開きませんでした。ブランチはそのままです。「ローカルの変更」で切り替えるか、Git で名前を変えるか消してから、もう一度ここで開いてください。",
3192
+ openedPullRequest: "プルリクエスト #{number} を、ローカルブランチ {branch} で開きました。戻るときは「ローカルの変更」で {previous} に切り替えてください。",
3193
+ openedPullRequestDetached: "プルリクエスト #{number} を、ローカルブランチ {branch} で開きました。戻るときは「ローカルの変更」で別のブランチに切り替えてください。",
3261
3194
  locked: "ほかの Git の操作が、このリポジトリを使っています(ロックファイル、またはマージ・cherry-pick・revert・rebase の途中)。終わってから、もう一度試してください。",
3262
3195
  branchName: "この名前は Git のブランチ名に使えません。",
3263
3196
  pullUnsafe: "GitHub から取り込む前に、実行を止め、保存していない編集とまだ記録していない変更をなくしてください。",
@@ -3319,10 +3252,369 @@ var messages = {
3319
3252
  historyCopied: "コミットの ID をコピーしました。",
3320
3253
  historyCopyFailed: "コミットの ID をコピーできませんでした。",
3321
3254
  historyRefs: "ブランチ",
3322
- cancel: "キャンセル"
3255
+ cancel: "キャンセル",
3256
+ branchMenu: "ブランチ",
3257
+ openPanel: "ローカル Git を開く",
3258
+ newBranchItem: "新しいブランチ…",
3259
+ newBranchTitle: "新しいブランチ"
3323
3260
  }
3324
3261
  };
3325
3262
  var labelsFor = (locale) => locale.startsWith("ja") ? messages.ja : messages.en;
3263
+ // src/branch-button.tsx
3264
+ var _excluded = ["state", "locale"];
3265
+ var menuItems = "[role=\"menuitem\"], [role=\"menuitemradio\"]";
3266
+ /** Git's branch glyph, drawn in the text color. */
3267
+ function BranchIcon() {
3268
+ return /* @__PURE__ */ React.createElement("svg", {
3269
+ className: "twgit-branch-icon",
3270
+ viewBox: "0 0 16 16",
3271
+ width: "16",
3272
+ height: "16",
3273
+ "aria-hidden": "true",
3274
+ focusable: "false"
3275
+ }, /* @__PURE__ */ React.createElement("g", {
3276
+ fill: "none",
3277
+ stroke: "currentColor",
3278
+ strokeWidth: "1.5"
3279
+ }, /* @__PURE__ */ React.createElement("circle", {
3280
+ cx: "4.5",
3281
+ cy: "3.5",
3282
+ r: "1.75"
3283
+ }), /* @__PURE__ */ React.createElement("circle", {
3284
+ cx: "4.5",
3285
+ cy: "12.5",
3286
+ r: "1.75"
3287
+ }), /* @__PURE__ */ React.createElement("circle", {
3288
+ cx: "11.5",
3289
+ cy: "5.5",
3290
+ r: "1.75"
3291
+ }), /* @__PURE__ */ React.createElement("path", { d: "M4.5 5.25v5.5M11.5 7.25c0 2.5-2.5 2.75-6 3.75" })));
3292
+ }
3293
+ /**
3294
+ * The current branch as a menu button for the host's menu bar, with the menu of branches. Before a repository is
3295
+ * chosen it reads "Git" and offers to choose one. A detached HEAD shows its short commit ID. The menu moves with the
3296
+ * arrow keys, Home and End; Escape closes it and returns to the button. Without `writable` it lists the branches only.
3297
+ */
3298
+ function BranchButton(_ref) {
3299
+ var _ref3;
3300
+ let { state, locale = "en" } = _ref, actions = _objectWithoutProperties$1(_ref, _excluded);
3301
+ const labels = labelsFor(locale);
3302
+ const [mode, setMode] = useState("closed");
3303
+ const [initial, setInitial] = useState("checked");
3304
+ const [name, setName] = useState("");
3305
+ const [checkout, setCheckout] = useState(true);
3306
+ const [creating, setCreating] = useState(false);
3307
+ const [createError, setCreateError] = useState("");
3308
+ const root = useRef(null);
3309
+ const trigger = useRef(null);
3310
+ const popup = useRef(null);
3311
+ const mounted = useRef(true);
3312
+ useEffect(() => () => {
3313
+ mounted.current = false;
3314
+ }, []);
3315
+ useLayoutEffect(() => {
3316
+ var _ref2;
3317
+ const element = popup.current;
3318
+ if (!element) return;
3319
+ if (mode === "create") {
3320
+ var _element$querySelecto;
3321
+ (_element$querySelecto = element.querySelector("input")) === null || _element$querySelecto === void 0 || _element$querySelecto.focus();
3322
+ return;
3323
+ }
3324
+ const items = [...element.querySelectorAll(menuItems)];
3325
+ const checked = items.find((item) => item.getAttribute("aria-checked") === "true");
3326
+ (_ref2 = initial === "last" ? items[items.length - 1] : initial === "checked" && checked || items[0]) === null || _ref2 === void 0 || _ref2.focus();
3327
+ }, [mode]);
3328
+ useEffect(() => {
3329
+ if (mode === "closed") return;
3330
+ const node = root.current;
3331
+ const dismiss = (event) => {
3332
+ if (!node.contains(event.target) && !creating) setMode("closed");
3333
+ };
3334
+ node.ownerDocument.addEventListener("pointerdown", dismiss, true);
3335
+ return () => node.ownerDocument.removeEventListener("pointerdown", dismiss, true);
3336
+ }, [mode, creating]);
3337
+ useEffect(() => {
3338
+ var _popup$current;
3339
+ if (!createError || creating) return;
3340
+ const input = (_popup$current = popup.current) === null || _popup$current === void 0 ? void 0 : _popup$current.querySelector("input:not([type])");
3341
+ input === null || input === void 0 || input.focus();
3342
+ input === null || input === void 0 || input.select();
3343
+ }, [createError, creating]);
3344
+ const { branch, head } = state;
3345
+ const unborn = state.repository && !head;
3346
+ const text = !state.repository ? "Git" : branch !== null && branch !== void 0 ? branch : head ? head.slice(0, 7) : labels.unborn;
3347
+ const accessibleName = !state.repository ? labels.title : `${branch ? labels.branch : labels.detached}: ${text}${unborn && branch ? ` (${labels.unborn})` : ""}`;
3348
+ const close = (focus = true) => {
3349
+ var _trigger$current;
3350
+ setMode("closed");
3351
+ if (focus) (_trigger$current = trigger.current) === null || _trigger$current === void 0 || _trigger$current.focus({ preventScroll: true });
3352
+ };
3353
+ const openMenu = (from) => {
3354
+ setInitial(from);
3355
+ setMode("menu");
3356
+ };
3357
+ /** Runs a branch operation and shows its result, or why it did not run, in the panel. */
3358
+ const change = async (operation) => {
3359
+ const result = await operation();
3360
+ if (result.done || result.error) actions.open();
3361
+ };
3362
+ const items = [];
3363
+ if (!state.repository) items.push({
3364
+ id: "choose",
3365
+ label: labels.choose,
3366
+ run: () => {
3367
+ close();
3368
+ actions.choose();
3369
+ }
3370
+ });
3371
+ else {
3372
+ const switching = state.writable && !state.busy;
3373
+ for (const local of state.branches) items.push({
3374
+ id: "branch:" + local,
3375
+ label: local,
3376
+ checked: local === branch,
3377
+ disabled: !state.writable || state.busy && local !== branch,
3378
+ run: () => {
3379
+ close();
3380
+ if (local !== branch && switching) change(() => actions.switchBranch(local));
3381
+ }
3382
+ });
3383
+ if (state.writable) {
3384
+ state.remoteBranches.forEach((remote, index) => items.push({
3385
+ id: "remote:" + remote,
3386
+ label: labels.historyCreateFromRemote.replace("{name}", remote),
3387
+ disabled: state.busy,
3388
+ separatorBefore: index === 0,
3389
+ run: () => {
3390
+ close();
3391
+ if (!state.busy) change(() => actions.createFromRemote(remote));
3392
+ }
3393
+ }));
3394
+ if (head) items.push({
3395
+ id: "create",
3396
+ label: labels.newBranchItem,
3397
+ disabled: state.busy,
3398
+ separatorBefore: true,
3399
+ run: () => {
3400
+ if (state.busy) return;
3401
+ setName("");
3402
+ setCheckout(true);
3403
+ setCreateError("");
3404
+ setMode("create");
3405
+ }
3406
+ });
3407
+ }
3408
+ }
3409
+ items.push({
3410
+ id: "open",
3411
+ label: labels.openPanel,
3412
+ separatorBefore: items.length > 0 && items[items.length - 1].id !== "create",
3413
+ run: () => {
3414
+ close();
3415
+ actions.open();
3416
+ }
3417
+ });
3418
+ const onMenuKey = (event) => {
3419
+ if (event.key === "Escape") {
3420
+ event.preventDefault();
3421
+ event.stopPropagation();
3422
+ if (!creating) close();
3423
+ return;
3424
+ }
3425
+ if (mode !== "menu") return;
3426
+ if (event.key === "Tab") {
3427
+ close(false);
3428
+ return;
3429
+ }
3430
+ const elements = [...event.currentTarget.querySelectorAll(menuItems)];
3431
+ const index = elements.indexOf(event.target);
3432
+ const target = {
3433
+ ArrowDown: index + 1,
3434
+ ArrowUp: index - 1,
3435
+ Home: 0,
3436
+ End: elements.length - 1
3437
+ }[event.key];
3438
+ if (target === void 0 || !elements.length) return;
3439
+ event.preventDefault();
3440
+ elements[(target + elements.length) % elements.length].focus();
3441
+ };
3442
+ const submit = async (event) => {
3443
+ var _result$error;
3444
+ event.preventDefault();
3445
+ const branchName = name.trim();
3446
+ if (!branchName || creating) return;
3447
+ setCreating(true);
3448
+ setCreateError("");
3449
+ let result;
3450
+ try {
3451
+ result = await actions.createBranch(branchName, checkout);
3452
+ } catch (error) {
3453
+ result = {
3454
+ done: false,
3455
+ error: error instanceof Error && error.message ? error.message : labels.failed
3456
+ };
3457
+ }
3458
+ if (!mounted.current) return;
3459
+ setCreating(false);
3460
+ if (result.done) {
3461
+ close();
3462
+ actions.open();
3463
+ } else setCreateError((_result$error = result.error) !== null && _result$error !== void 0 ? _result$error : "");
3464
+ };
3465
+ return /* @__PURE__ */ React.createElement("div", {
3466
+ className: "twgit-branch-entry",
3467
+ ref: root,
3468
+ onBlur: (event) => {
3469
+ const next = event.relatedTarget;
3470
+ if (next && !event.currentTarget.contains(next) && !creating) setMode("closed");
3471
+ }
3472
+ }, /* @__PURE__ */ React.createElement("button", {
3473
+ type: "button",
3474
+ className: "twgit-branch-button",
3475
+ ref: trigger,
3476
+ "aria-label": accessibleName,
3477
+ title: accessibleName,
3478
+ "aria-haspopup": "menu",
3479
+ "aria-expanded": mode === "menu",
3480
+ "data-git-branch": state.repository ? (_ref3 = branch !== null && branch !== void 0 ? branch : head) !== null && _ref3 !== void 0 ? _ref3 : "" : void 0,
3481
+ onClick: () => mode === "closed" ? openMenu("checked") : close(false),
3482
+ onKeyDown: (event) => {
3483
+ if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
3484
+ event.preventDefault();
3485
+ openMenu(event.key === "ArrowUp" ? "last" : "checked");
3486
+ }
3487
+ }, /* @__PURE__ */ React.createElement(BranchIcon, null), /* @__PURE__ */ React.createElement("span", { className: "twgit-branch-name" }, text), unborn && branch && /* @__PURE__ */ React.createElement("small", { className: "twgit-branch-unborn" }, labels.unborn), /* @__PURE__ */ React.createElement("span", {
3488
+ className: "twgit-branch-caret",
3489
+ "aria-hidden": "true"
3490
+ }, "▾")), mode === "menu" && /* @__PURE__ */ React.createElement("div", {
3491
+ ref: popup,
3492
+ className: "twgit-branch-popup",
3493
+ role: "menu",
3494
+ "aria-label": labels.branchMenu,
3495
+ onKeyDown: onMenuKey
3496
+ }, items.map((item) => /* @__PURE__ */ React.createElement("button", {
3497
+ key: item.id,
3498
+ type: "button",
3499
+ tabIndex: -1,
3500
+ "data-action": item.id,
3501
+ role: item.checked === void 0 ? "menuitem" : "menuitemradio",
3502
+ "aria-checked": item.checked,
3503
+ "aria-disabled": item.disabled || void 0,
3504
+ "data-separator-before": item.separatorBefore || void 0,
3505
+ onClick: () => {
3506
+ if (!item.disabled || item.checked) item.run();
3507
+ }
3508
+ }, item.checked !== void 0 && /* @__PURE__ */ React.createElement("span", {
3509
+ className: "twgit-branch-check",
3510
+ "aria-hidden": "true"
3511
+ }, item.checked ? "✓" : ""), item.label))), mode === "create" && /* @__PURE__ */ React.createElement("div", {
3512
+ ref: popup,
3513
+ className: "twgit-branch-popup",
3514
+ role: "dialog",
3515
+ "aria-label": labels.newBranchTitle,
3516
+ onKeyDown: onMenuKey
3517
+ }, /* @__PURE__ */ React.createElement("form", { onSubmit: (event) => void submit(event) }, /* @__PURE__ */ React.createElement("label", null, labels.newBranch, " ", /* @__PURE__ */ React.createElement("input", {
3518
+ value: name,
3519
+ required: true,
3520
+ disabled: creating,
3521
+ "aria-invalid": createError ? true : void 0,
3522
+ onChange: (event) => setName(event.currentTarget.value)
3523
+ })), /* @__PURE__ */ React.createElement("label", null, /* @__PURE__ */ React.createElement("input", {
3524
+ type: "checkbox",
3525
+ checked: checkout,
3526
+ disabled: creating,
3527
+ onChange: (event) => setCheckout(event.currentTarget.checked)
3528
+ }), " ", labels.switchAfterCreate), createError && /* @__PURE__ */ React.createElement("p", {
3529
+ className: "twgit-graph-form-error",
3530
+ role: "alert"
3531
+ }, createError), /* @__PURE__ */ React.createElement("span", { className: "twgit-graph-form-actions" }, /* @__PURE__ */ React.createElement("button", {
3532
+ type: "submit",
3533
+ disabled: creating || state.busy || !name.trim()
3534
+ }, labels.createBranch), /* @__PURE__ */ React.createElement("button", {
3535
+ type: "button",
3536
+ disabled: creating,
3537
+ onClick: () => close()
3538
+ }, labels.cancel)))));
3539
+ }
3540
+ /**
3541
+ * Lays out commits ordered newest first with every child above its parents. Parents missing from `commits`, such as
3542
+ * those on a page not loaded yet or cut off by a shallow clone, keep their lane running to the bottom.
3543
+ */
3544
+ function layoutHistory(commits) {
3545
+ const lanes = [];
3546
+ let nextColor = 0;
3547
+ const free = () => {
3548
+ const index = lanes.indexOf(null);
3549
+ return index === -1 ? lanes.length : index;
3550
+ };
3551
+ const rows = [];
3552
+ for (const commit of commits) {
3553
+ const segments = [];
3554
+ const before = lanes.slice();
3555
+ const waiting = before.flatMap((lane, index) => (lane === null || lane === void 0 ? void 0 : lane.oid) === commit.oid ? [index] : []);
3556
+ const column = waiting.length ? waiting[0] : free();
3557
+ const color = waiting.length ? before[column].color : nextColor++ % 8;
3558
+ for (const [index, lane] of before.entries()) {
3559
+ if (!lane) continue;
3560
+ segments.push({
3561
+ part: "top",
3562
+ from: index,
3563
+ to: lane.oid === commit.oid ? column : index,
3564
+ color: lane.color
3565
+ });
3566
+ }
3567
+ for (const index of waiting) lanes[index] = null;
3568
+ const parents = [...new Set(commit.parents)];
3569
+ lanes[column] = parents.length ? {
3570
+ oid: parents[0],
3571
+ color
3572
+ } : null;
3573
+ const joins = parents.length ? [{
3574
+ index: column,
3575
+ color
3576
+ }] : [];
3577
+ for (const parent of parents.slice(1)) {
3578
+ let index = lanes.findIndex((lane) => (lane === null || lane === void 0 ? void 0 : lane.oid) === parent);
3579
+ if (index === -1) {
3580
+ index = free();
3581
+ lanes[index] = {
3582
+ oid: parent,
3583
+ color: nextColor++ % 8
3584
+ };
3585
+ }
3586
+ joins.push({
3587
+ index,
3588
+ color: lanes[index].color
3589
+ });
3590
+ }
3591
+ while (lanes.length && lanes[lanes.length - 1] === null) lanes.pop();
3592
+ for (const [index, lane] of before.entries()) if (lane && lane.oid !== commit.oid) segments.push({
3593
+ part: "bottom",
3594
+ from: index,
3595
+ to: index,
3596
+ color: lane.color
3597
+ });
3598
+ for (const join of joins) segments.push({
3599
+ part: "bottom",
3600
+ from: column,
3601
+ to: join.index,
3602
+ color: join.color
3603
+ });
3604
+ const width = Math.max(column + 1, ...segments.map((segment) => Math.max(segment.from, segment.to) + 1));
3605
+ rows.push({
3606
+ oid: commit.oid,
3607
+ column,
3608
+ color,
3609
+ segments,
3610
+ width
3611
+ });
3612
+ }
3613
+ return {
3614
+ rows,
3615
+ width: Math.max(0, ...rows.map((row) => row.width))
3616
+ };
3617
+ }
3326
3618
  // src/history-graph-view.tsx
3327
3619
  /** Lane colors that read on light and dark backgrounds. */
3328
3620
  var laneColors = [
@@ -3812,7 +4104,20 @@ var en = {
3812
4104
  failed: "Could not read GitHub. Try again.",
3813
4105
  changed: "The pull request changed while it was being read. Open it again.",
3814
4106
  tooManyFiles: "GitHub only returns the first 3,000 changed files. This pull request exceeds that limit.",
3815
- openTabs: "Open as tabs"
4107
+ openTabs: "Open as tabs",
4108
+ session: "Connect to GitHub again (GitHub connection) to read this pull request.",
4109
+ forbidden: "The GitHub connection is not allowed to read this repository.",
4110
+ pullRequestNotFound: "This repository has no pull request with this number.",
4111
+ network: "Could not reach GitHub through the connection. Check the network and try again.",
4112
+ remoteFailed: "Could not read the pull request through the GitHub connection. Try again.",
4113
+ baseMissing: "The base branch is not on GitHub. Choose another base branch.",
4114
+ viaConnection: "Read through the GitHub connection.",
4115
+ listPrivate: "The list uses the public GitHub API. Enter a pull request number to read it through the connection.",
4116
+ noMetadata: "The title and base branch are not available without the public GitHub API. Choose the base branch to compare with.",
4117
+ base: "Base branch",
4118
+ pullRequest: "Pull request",
4119
+ openLocally: "Open here as {branch}",
4120
+ openLocallyHint: "Creates the local branch {branch} at this version, switches to it and opens the project, so it can be run. Unsaved edits and unrecorded changes stop it. Switch back in Local changes."
3816
4121
  };
3817
4122
  var ja = {
3818
4123
  title: "プルリクエスト",
@@ -3834,12 +4139,40 @@ var ja = {
3834
4139
  failed: "GitHub から取得できませんでした。再試行してください。",
3835
4140
  changed: "取得中にプルリクエストが更新されました。もう一度開いてください。",
3836
4141
  tooManyFiles: "変更ファイルが GitHub API の取得上限である 3,000 件を超えています。",
3837
- openTabs: "タブで開く"
4142
+ openTabs: "タブで開く",
4143
+ session: "この PR を読むには、GitHub 接続でもう一度接続してください。",
4144
+ forbidden: "GitHub 接続に、このリポジトリを読む権限がありません。",
4145
+ pullRequestNotFound: "このリポジトリに、この番号のプルリクエストはありません。",
4146
+ network: "GitHub 接続で GitHub に届きませんでした。ネットワークを確認して、もう一度試してください。",
4147
+ remoteFailed: "GitHub 接続でプルリクエストを読めませんでした。もう一度試してください。",
4148
+ baseMissing: "ベースのブランチが GitHub にありません。別のベースを選んでください。",
4149
+ viaConnection: "GitHub 接続で取得しました。",
4150
+ listPrivate: "一覧は公開 GitHub API を使います。番号を入れると、GitHub 接続で読みます。",
4151
+ noMetadata: "公開 GitHub API を使えないため、題名とベースのブランチがわかりません。比べるベースのブランチを選んでください。",
4152
+ base: "ベースのブランチ",
4153
+ pullRequest: "プルリクエスト",
4154
+ openLocally: "{branch} として手元で開く",
4155
+ openLocallyHint: "この版にローカルブランチ {branch} を作って切り替え、作品を開き直して実行できるようにします。保存していない編集や記録していない変更があるときは行いません。元に戻すときは「ローカルの変更」で切り替えてください。"
3838
4156
  };
4157
+ var gitErrors = [
4158
+ "session",
4159
+ "forbidden",
4160
+ "pullRequestNotFound",
4161
+ "network",
4162
+ "remoteFailed",
4163
+ "baseMissing",
4164
+ "changed"
4165
+ ];
3839
4166
  function errorMessage(error, labels) {
3840
- return error instanceof PullRequestError ? labels[error.code] : labels.failed;
4167
+ if (error instanceof PullRequestError) return labels[error.code];
4168
+ if (error instanceof GitPanelError) {
4169
+ const code = gitErrors.find((code) => code === error.code);
4170
+ return code ? labels[code] : labels.failed;
4171
+ }
4172
+ return labels.failed;
3841
4173
  }
3842
- function PullRequests({ name, labels, locale, renderBlock, locateFor, openTabs }) {
4174
+ function PullRequests({ name, labels, locale, renderBlock, locateFor, openTabs, repository, connection, openLocally }) {
4175
+ var _pr$base$ref, _pr$git, _pr$base$ref2;
3843
4176
  const client = useMemo(() => createGitHubPullRequests(name), [name]);
3844
4177
  const [page, setPage] = useState(1);
3845
4178
  const [listing, setListing] = useState();
@@ -3847,10 +4180,12 @@ function PullRequests({ name, labels, locale, renderBlock, locateFor, openTabs }
3847
4180
  const [selection, setSelection] = useState();
3848
4181
  const [loaded, setLoaded] = useState();
3849
4182
  const list = (listing === null || listing === void 0 ? void 0 : listing.page) === page ? listing.result : void 0;
4183
+ const [listConnected, setListConnected] = useState(false);
3850
4184
  const detail = (loaded === null || loaded === void 0 ? void 0 : loaded.selection) === selection ? loaded === null || loaded === void 0 ? void 0 : loaded.result : void 0;
3851
4185
  useEffect(() => {
3852
4186
  const controller = new AbortController();
3853
4187
  setListing(void 0);
4188
+ setListConnected(!!(connection === null || connection === void 0 ? void 0 : connection.connected()));
3854
4189
  client.list(page, controller.signal).then((data) => {
3855
4190
  if (!controller.signal.aborted) setListing({
3856
4191
  page,
@@ -3871,7 +4206,8 @@ function PullRequests({ name, labels, locale, renderBlock, locateFor, openTabs }
3871
4206
  useEffect(() => {
3872
4207
  if (!selection) return;
3873
4208
  const controller = new AbortController();
3874
- client.load(selection.number, controller.signal).then((data) => {
4209
+ const load = () => (connection === null || connection === void 0 ? void 0 : connection.connected()) ? loadThroughConnection(selection.number, selection.base, controller.signal) : client.load(selection.number, controller.signal);
4210
+ load().then((data) => {
3875
4211
  if (!controller.signal.aborted) setLoaded({
3876
4212
  selection,
3877
4213
  result: { data }
@@ -3888,6 +4224,56 @@ function PullRequests({ name, labels, locale, renderBlock, locateFor, openTabs }
3888
4224
  selection,
3889
4225
  labels
3890
4226
  ]);
4227
+ const fetched = useRef();
4228
+ /**
4229
+ * Fetches `refs/pull/<number>/head` and the branches over Git Smart HTTP and compares them from local objects. The
4230
+ * public API only adds the title, description and base when it can read the repository; the contents never come
4231
+ * from it, so a private repository is read only through the connection.
4232
+ */
4233
+ const loadThroughConnection = async (number, chosenBase, signal) => {
4234
+ var _fetched$current, _ref, _ref2, _metadata$title, _metadata$description, _metadata$head$ref;
4235
+ const guard = () => {
4236
+ if (signal.aborted) throw new GitPanelError("changed");
4237
+ };
4238
+ let head, branches, defaultBranch, metadata = null;
4239
+ if (chosenBase !== void 0 && ((_fetched$current = fetched.current) === null || _fetched$current === void 0 ? void 0 : _fetched$current.number) === number) ({head, branches, defaultBranch} = fetched.current);
4240
+ else {
4241
+ metadata = await client.metadata(number, signal).catch((error) => {
4242
+ if (signal.aborted) throw error;
4243
+ return null;
4244
+ });
4245
+ const result = await repository.fetchPullRequest(number, connection.getSession, guard, signal);
4246
+ if (metadata && metadata.head.sha !== result.head) throw new PullRequestError("changed");
4247
+ ({head, branches, defaultBranch} = result);
4248
+ fetched.current = {
4249
+ number,
4250
+ head,
4251
+ branches,
4252
+ defaultBranch
4253
+ };
4254
+ }
4255
+ const branch = (_ref = (_ref2 = chosenBase !== null && chosenBase !== void 0 ? chosenBase : metadata === null || metadata === void 0 ? void 0 : metadata.base.ref) !== null && _ref2 !== void 0 ? _ref2 : defaultBranch) !== null && _ref !== void 0 ? _ref : branches[0];
4256
+ const read = await repository.readPullRequest(head, {
4257
+ sha: metadata === null || metadata === void 0 ? void 0 : metadata.base.sha,
4258
+ branch
4259
+ });
4260
+ guard();
4261
+ return {
4262
+ number,
4263
+ title: (_metadata$title = metadata === null || metadata === void 0 ? void 0 : metadata.title) !== null && _metadata$title !== void 0 ? _metadata$title : `${labels.pullRequest} #${number}`,
4264
+ description: (_metadata$description = metadata === null || metadata === void 0 ? void 0 : metadata.description) !== null && _metadata$description !== void 0 ? _metadata$description : "",
4265
+ author: metadata === null || metadata === void 0 ? void 0 : metadata.author,
4266
+ state: metadata === null || metadata === void 0 ? void 0 : metadata.state,
4267
+ head: {
4268
+ sha: head,
4269
+ ref: (_metadata$head$ref = metadata === null || metadata === void 0 ? void 0 : metadata.head.ref) !== null && _metadata$head$ref !== void 0 ? _metadata$head$ref : `pull/${number}/head`
4270
+ },
4271
+ base: read.base,
4272
+ files: read.files,
4273
+ url: "https://github.com/" + name + "/pull/" + number,
4274
+ git: { branches: metadata ? null : branches }
4275
+ };
4276
+ };
3891
4277
  const choose = (number) => {
3892
4278
  if (!Number.isSafeInteger(number) || number < 1) return;
3893
4279
  setSelection({ number });
@@ -3907,7 +4293,7 @@ function PullRequests({ name, labels, locale, renderBlock, locateFor, openTabs }
3907
4293
  max: Number.MAX_SAFE_INTEGER,
3908
4294
  value: input,
3909
4295
  onChange: (event) => setInput(event.currentTarget.value)
3910
- })), /* @__PURE__ */ React.createElement("button", { type: "submit" }, labels.openNumber)), !list && /* @__PURE__ */ React.createElement("p", { role: "status" }, labels.loading), (list === null || list === void 0 ? void 0 : list.error) && /* @__PURE__ */ React.createElement("p", { role: "alert" }, list.error), (list === null || list === void 0 ? void 0 : list.data) && /* @__PURE__ */ React.createElement(React.Fragment, null, !list.data.items.length && /* @__PURE__ */ React.createElement("p", null, labels.empty), /* @__PURE__ */ React.createElement("ul", null, list.data.items.map((item) => /* @__PURE__ */ React.createElement("li", { key: item.number }, /* @__PURE__ */ React.createElement("button", {
4296
+ })), /* @__PURE__ */ React.createElement("button", { type: "submit" }, labels.openNumber)), !list && /* @__PURE__ */ React.createElement("p", { role: "status" }, labels.loading), (list === null || list === void 0 ? void 0 : list.error) && (listConnected && list.error === labels.unavailable ? /* @__PURE__ */ React.createElement("p", { role: "status" }, labels.listPrivate) : /* @__PURE__ */ React.createElement("p", { role: "alert" }, list.error)), (list === null || list === void 0 ? void 0 : list.data) && /* @__PURE__ */ React.createElement(React.Fragment, null, !list.data.items.length && /* @__PURE__ */ React.createElement("p", null, labels.empty), /* @__PURE__ */ React.createElement("ul", null, list.data.items.map((item) => /* @__PURE__ */ React.createElement("li", { key: item.number }, /* @__PURE__ */ React.createElement("button", {
3911
4297
  className: "twgit-file",
3912
4298
  type: "button",
3913
4299
  "aria-pressed": (selection === null || selection === void 0 ? void 0 : selection.number) === item.number,
@@ -3920,7 +4306,17 @@ function PullRequests({ name, labels, locale, renderBlock, locateFor, openTabs }
3920
4306
  type: "button",
3921
4307
  disabled: !list.data.more,
3922
4308
  onClick: () => setPage(page + 1)
3923
- }, labels.next)))), /* @__PURE__ */ React.createElement("main", { className: "twgit-diff" }, !selection && /* @__PURE__ */ React.createElement("p", null, labels.select), selection && !detail && /* @__PURE__ */ React.createElement("p", { role: "status" }, labels.loading), (detail === null || detail === void 0 ? void 0 : detail.error) && /* @__PURE__ */ React.createElement("p", { role: "alert" }, detail.error), pr && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("p", { className: "twgit-pr-revision" }, /* @__PURE__ */ React.createElement("strong", null, "#", pr.number, " · ", labels[pr.state]), pr.author && /* @__PURE__ */ React.createElement("span", null, pr.author), /* @__PURE__ */ React.createElement("code", { title: pr.head.sha }, pr.head.ref, " → ", pr.base.ref), /* @__PURE__ */ React.createElement("a", {
4309
+ }, labels.next)))), /* @__PURE__ */ React.createElement("main", { className: "twgit-diff" }, !selection && /* @__PURE__ */ React.createElement("p", null, labels.select), selection && !detail && /* @__PURE__ */ React.createElement("p", { role: "status" }, labels.loading), (detail === null || detail === void 0 ? void 0 : detail.error) && /* @__PURE__ */ React.createElement("p", { role: "alert" }, detail.error), pr && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("p", { className: "twgit-pr-revision" }, /* @__PURE__ */ React.createElement("strong", null, "#", pr.number, pr.state && /* @__PURE__ */ React.createElement(React.Fragment, null, " · ", labels[pr.state])), pr.author && /* @__PURE__ */ React.createElement("span", null, pr.author), /* @__PURE__ */ React.createElement("code", { title: pr.head.sha }, pr.head.ref, " → ", (_pr$base$ref = pr.base.ref) !== null && _pr$base$ref !== void 0 ? _pr$base$ref : pr.base.sha.slice(0, 12)), ((_pr$git = pr.git) === null || _pr$git === void 0 ? void 0 : _pr$git.branches) && /* @__PURE__ */ React.createElement("label", null, labels.base, " ", /* @__PURE__ */ React.createElement("select", {
4310
+ "data-git-pr-base": "",
4311
+ value: (_pr$base$ref2 = pr.base.ref) !== null && _pr$base$ref2 !== void 0 ? _pr$base$ref2 : "",
4312
+ onChange: (event) => setSelection({
4313
+ number: pr.number,
4314
+ base: event.currentTarget.value
4315
+ })
4316
+ }, pr.git.branches.map((branch) => /* @__PURE__ */ React.createElement("option", {
4317
+ key: branch,
4318
+ value: branch
4319
+ }, branch)))), /* @__PURE__ */ React.createElement("a", {
3924
4320
  href: pr.url,
3925
4321
  target: "_blank",
3926
4322
  rel: "noreferrer"
@@ -3943,7 +4339,16 @@ function PullRequests({ name, labels, locale, renderBlock, locateFor, openTabs }
3943
4339
  locateBlock: locateFor === null || locateFor === void 0 ? void 0 : locateFor(file.path, pr.head.sha)
3944
4340
  }] : [];
3945
4341
  }), `${name}#${pr.number}`)
3946
- }, labels.openTabs)), /* @__PURE__ */ React.createElement(ProjectSourceReview, {
4342
+ }, labels.openTabs), pr.git && openLocally && /* @__PURE__ */ React.createElement("button", {
4343
+ type: "button",
4344
+ "data-git-open-pr-locally": "",
4345
+ disabled: openLocally.busy,
4346
+ title: labels.openLocallyHint.replace("{branch}", pullRequestBranch(pr.number)),
4347
+ onClick: () => void openLocally.open(pr.number, pr.head.sha)
4348
+ }, labels.openLocally.replace("{branch}", pullRequestBranch(pr.number)))), pr.git && /* @__PURE__ */ React.createElement("p", {
4349
+ className: "twgit-context",
4350
+ role: "status"
4351
+ }, labels.viaConnection, pr.git.branches && /* @__PURE__ */ React.createElement(React.Fragment, null, " ", labels.noMetadata)), /* @__PURE__ */ React.createElement(ProjectSourceReview, {
3947
4352
  title: pr.title,
3948
4353
  description: pr.description,
3949
4354
  files: locateFor ? pr.files.map((file) => "patch" in file ? _objectSpread2$1(_objectSpread2$1({}, file), {}, { locateBlock: locateFor(file.path, pr.head.sha) }) : file) : pr.files,
@@ -3952,7 +4357,7 @@ function PullRequests({ name, labels, locale, renderBlock, locateFor, openTabs }
3952
4357
  rememberAs: `${name}#${pr.number}`
3953
4358
  }))));
3954
4359
  }
3955
- function PullRequestReviewPanel({ repository, locale, renderBlock, locateFor, openTabs }) {
4360
+ function PullRequestReviewPanel({ repository, locale, renderBlock, locateFor, openTabs, connection, openLocally }) {
3956
4361
  const labels = locale.startsWith("ja") ? ja : en;
3957
4362
  const [origin, setOrigin] = useState();
3958
4363
  useEffect(() => {
@@ -3988,7 +4393,10 @@ function PullRequestReviewPanel({ repository, locale, renderBlock, locateFor, op
3988
4393
  locale,
3989
4394
  renderBlock,
3990
4395
  locateFor,
3991
- openTabs
4396
+ openTabs,
4397
+ repository,
4398
+ connection,
4399
+ openLocally
3992
4400
  });
3993
4401
  }
3994
4402
  // src/editor-block-locator.ts
@@ -4029,6 +4437,41 @@ function nextFrame(signal) {
4029
4437
  }
4030
4438
  // src/index.tsx
4031
4439
  init_objectSpread2();
4440
+ function createController() {
4441
+ const listeners = /* @__PURE__ */ new Set();
4442
+ const controller = {
4443
+ open() {},
4444
+ async refresh() {},
4445
+ async choose() {},
4446
+ async changeBranch() {
4447
+ return { done: false };
4448
+ },
4449
+ branch: {
4450
+ repository: false,
4451
+ branch: void 0,
4452
+ head: null,
4453
+ branches: [],
4454
+ remoteBranches: [],
4455
+ writable: false,
4456
+ busy: false
4457
+ },
4458
+ publish(state) {
4459
+ controller.branch = state;
4460
+ for (const listener of [...listeners]) listener();
4461
+ },
4462
+ follow(listener) {
4463
+ listeners.add(listener);
4464
+ controller.onFollowers(listeners.size);
4465
+ return () => {
4466
+ listeners.delete(listener);
4467
+ controller.onFollowers(listeners.size);
4468
+ };
4469
+ },
4470
+ followers: () => listeners.size,
4471
+ onFollowers() {}
4472
+ };
4473
+ return controller;
4474
+ }
4032
4475
  var initial = {
4033
4476
  repository: null,
4034
4477
  snapshot: null,
@@ -4042,9 +4485,56 @@ var initial = {
4042
4485
  busy: false
4043
4486
  };
4044
4487
  var controllers = /* @__PURE__ */ new WeakMap();
4488
+ var registrations = /* @__PURE__ */ new Set();
4045
4489
  var watchInterval = 2e3;
4490
+ /**
4491
+ * Calls `check` every `watchInterval`, when the window regains focus and shortly after a File System Observer reports
4492
+ * a change under `root`, one call at a time and never while the page is hidden. Returns the function that stops it.
4493
+ */
4494
+ function watchRepository(document, root, check) {
4495
+ const view = document.defaultView;
4496
+ let stopped = false;
4497
+ let checking = false;
4498
+ const run = async () => {
4499
+ if (stopped || checking || document.visibilityState === "hidden") return;
4500
+ checking = true;
4501
+ try {
4502
+ await check(() => stopped);
4503
+ } catch (_unused) {} finally {
4504
+ checking = false;
4505
+ }
4506
+ };
4507
+ let soon;
4508
+ const schedule = () => {
4509
+ view.clearTimeout(soon);
4510
+ soon = view.setTimeout(() => void run(), 300);
4511
+ };
4512
+ const interval = view.setInterval(() => void run(), watchInterval);
4513
+ const onFocus = () => void run();
4514
+ view.addEventListener("focus", onFocus);
4515
+ document.addEventListener("visibilitychange", onFocus);
4516
+ let observer = null;
4517
+ try {
4518
+ if (view.FileSystemObserver) {
4519
+ observer = new view.FileSystemObserver(schedule);
4520
+ observer.observe(root, { recursive: true }).catch(() => {});
4521
+ }
4522
+ } catch (_unused2) {
4523
+ observer = null;
4524
+ }
4525
+ run();
4526
+ return () => {
4527
+ stopped = true;
4528
+ view.clearInterval(interval);
4529
+ view.clearTimeout(soon);
4530
+ view.removeEventListener("focus", onFocus);
4531
+ document.removeEventListener("visibilitychange", onFocus);
4532
+ observer === null || observer === void 0 || observer.disconnect();
4533
+ };
4534
+ }
4535
+ var sameRefs = (a, b) => !!a && a.branch === b.branch && a.head === b.head && a.branches.join("\n") === b.branches.join("\n") && a.remoteBranches.join("\n") === b.remoteBranches.join("\n");
4046
4536
  function GitPanel({ vm, document, host, controller }) {
4047
- var _state$snapshot, _state$repository, _state$snapshot$head$, _state$snapshot$head;
4537
+ var _refs$head, _refs$branches, _refs$remoteBranches, _state$snapshot, _state$repository, _state$snapshot$head$, _state$snapshot$head;
4048
4538
  const [visible, setVisible] = useState(false);
4049
4539
  const [slot, setSlot] = useState(null);
4050
4540
  const shown = visible || slot !== null;
@@ -4217,57 +4707,42 @@ function GitPanel({ vm, document, host, controller }) {
4217
4707
  controller.refresh();
4218
4708
  }
4219
4709
  }, [shown]);
4710
+ const [refs, setRefs] = useState(null);
4711
+ useEffect(() => {
4712
+ if (!state.repository) setRefs(null);
4713
+ else if (state.snapshot) setRefs(refsOf(state.snapshot));
4714
+ }, [state.repository, state.snapshot]);
4715
+ const [followers, setFollowers] = useState(controller.followers());
4716
+ useEffect(() => {
4717
+ controller.onFollowers = setFollowers;
4718
+ setFollowers(controller.followers());
4719
+ return () => {
4720
+ controller.onFollowers = () => {};
4721
+ };
4722
+ }, []);
4723
+ const following = followers > 0;
4220
4724
  useEffect(() => {
4221
4725
  const repository = state.repository;
4222
- if (!shown || !repository) return;
4223
- const view = document.defaultView;
4224
- let stopped = false;
4225
- let checking = false;
4226
- const check = async () => {
4726
+ if (!repository || !shown && !following) return;
4727
+ return watchRepository(document, repository.root, async (stopped) => {
4227
4728
  const { snapshot } = current.current;
4228
- const hidden = !dialog.current || !dialog.current.getClientRects().length;
4229
- if (stopped || checking || running.current || document.visibilityState === "hidden" || hidden || !snapshot || current.current.repository !== repository) return;
4230
- checking = true;
4231
- try {
4232
- const probe = await repository.probe(snapshot.scope);
4233
- if (stopped || probe === "locked" || probe === snapshot.probe || probe === handled.current || current.current.snapshot !== snapshot) return;
4234
- handled.current = probe;
4235
- const { selected, commit } = current.current;
4236
- if (selected || commit || reviewing.current) setState((previous) => previous.snapshot === snapshot ? _objectSpread2$1(_objectSpread2$1({}, previous), {}, { notice: labelsFor(locale).outdated }) : previous);
4237
- else await controller.refresh(true);
4238
- } catch (_unused) {} finally {
4239
- checking = false;
4240
- }
4241
- };
4242
- let soon;
4243
- const schedule = () => {
4244
- view.clearTimeout(soon);
4245
- soon = view.setTimeout(() => void check(), 300);
4246
- };
4247
- const interval = view.setInterval(() => void check(), watchInterval);
4248
- const onFocus = () => void check();
4249
- view.addEventListener("focus", onFocus);
4250
- document.addEventListener("visibilitychange", onFocus);
4251
- let observer = null;
4252
- try {
4253
- if (view.FileSystemObserver) {
4254
- observer = new view.FileSystemObserver(schedule);
4255
- observer.observe(repository.root, { recursive: true }).catch(() => {});
4729
+ if (running.current || current.current.repository !== repository) return;
4730
+ if (!(shown && !!dialog.current && dialog.current.getClientRects().length > 0) || !snapshot) {
4731
+ if (!following) return;
4732
+ const next = await repository.refs();
4733
+ if (!stopped() && current.current.repository === repository) setRefs((previous) => sameRefs(previous, next) ? previous : next);
4734
+ return;
4256
4735
  }
4257
- } catch (_unused2) {
4258
- observer = null;
4259
- }
4260
- check();
4261
- return () => {
4262
- stopped = true;
4263
- view.clearInterval(interval);
4264
- view.clearTimeout(soon);
4265
- view.removeEventListener("focus", onFocus);
4266
- document.removeEventListener("visibilitychange", onFocus);
4267
- observer === null || observer === void 0 || observer.disconnect();
4268
- };
4736
+ const probe = await repository.probe(snapshot.scope);
4737
+ if (stopped() || probe === "locked" || probe === snapshot.probe || probe === handled.current || current.current.snapshot !== snapshot) return;
4738
+ handled.current = probe;
4739
+ const { selected, commit } = current.current;
4740
+ if (selected || commit || reviewing.current) setState((previous) => previous.snapshot === snapshot ? _objectSpread2$1(_objectSpread2$1({}, previous), {}, { notice: labelsFor(locale).outdated }) : previous);
4741
+ else await controller.refresh(true);
4742
+ });
4269
4743
  }, [
4270
4744
  shown,
4745
+ following,
4271
4746
  state.repository,
4272
4747
  locale
4273
4748
  ]);
@@ -4282,7 +4757,7 @@ function GitPanel({ vm, document, host, controller }) {
4282
4757
  }, [visible, slot]);
4283
4758
  const chooseRoot = async () => {
4284
4759
  const view = document.defaultView;
4285
- await run(async () => {
4760
+ return run(async () => {
4286
4761
  var _host$connectionSetti;
4287
4762
  let root;
4288
4763
  try {
@@ -4394,6 +4869,14 @@ function GitPanel({ vm, document, host, controller }) {
4394
4869
  };
4395
4870
  });
4396
4871
  };
4872
+ const pullRequestConnection = useMemo(() => {
4873
+ const settings = host.connectionSettings;
4874
+ const getSession = settings === null || settings === void 0 ? void 0 : settings.getSession;
4875
+ return getSession ? {
4876
+ getSession,
4877
+ connected: () => settings.getView().state.status === "connected"
4878
+ } : void 0;
4879
+ }, [host]);
4397
4880
  const [message, setMessage] = useState("");
4398
4881
  const localOperation = (operation, notice) => {
4399
4882
  const repository = current.current.repository;
@@ -4433,6 +4916,31 @@ function GitPanel({ vm, document, host, controller }) {
4433
4916
  if (operation.kind === "createBranch" && !operation.fromRemote && !at) setNewBranch("");
4434
4917
  return result;
4435
4918
  };
4919
+ /**
4920
+ * Opens a pull request version fetched over the connection: creates `pr/<number>` at its head (or reuses that
4921
+ * branch when it already points there), switches to it with the same refusals as any switch, and opens the project
4922
+ * folder again from it. The branch the person was on stays, so switching back is an ordinary switch.
4923
+ */
4924
+ const openPullRequest = async (number, head) => {
4925
+ var _current$current$snap, _current$current$snap2;
4926
+ const repository = current.current.repository;
4927
+ if (!repository) return { done: false };
4928
+ const original = (_current$current$snap = (_current$current$snap2 = current.current.snapshot) === null || _current$current$snap2 === void 0 ? void 0 : _current$current$snap2.branch) !== null && _current$current$snap !== void 0 ? _current$current$snap : null;
4929
+ const name = pullRequestBranch(number);
4930
+ const result = await changeBranch({
4931
+ kind: "createBranch",
4932
+ name,
4933
+ at: head,
4934
+ checkout: true,
4935
+ reuse: true
4936
+ });
4937
+ if (!result.done) return result;
4938
+ const project = current.current.project;
4939
+ if (project !== null && await repository.hasFolder(name, project)) await openProject(project);
4940
+ const notice = (original && original !== name ? labels.openedPullRequest : labels.openedPullRequestDetached).replace("{number}", String(number)).replace("{branch}", name).replace("{previous}", original !== null && original !== void 0 ? original : "");
4941
+ if (alive.current) setState((previous) => previous.error ? previous : _objectSpread2$1(_objectSpread2$1({}, previous), {}, { notice }));
4942
+ return result;
4943
+ };
4436
4944
  const [history, setHistory] = useState({
4437
4945
  commits: [],
4438
4946
  cursor: null,
@@ -4515,6 +5023,24 @@ function GitPanel({ vm, document, host, controller }) {
4515
5023
  state.snapshot,
4516
5024
  state.selected
4517
5025
  ]);
5026
+ controller.changeBranch = changeBranch;
5027
+ controller.choose = async (language) => {
5028
+ setLocale(language);
5029
+ const before = current.current.repository;
5030
+ const result = await chooseRoot();
5031
+ if (!result.done && result.error || current.current.repository !== before) controller.open(language);
5032
+ };
5033
+ const branchState = {
5034
+ repository: !!state.repository,
5035
+ branch: refs === null || refs === void 0 ? void 0 : refs.branch,
5036
+ head: (_refs$head = refs === null || refs === void 0 ? void 0 : refs.head) !== null && _refs$head !== void 0 ? _refs$head : null,
5037
+ branches: (_refs$branches = refs === null || refs === void 0 ? void 0 : refs.branches) !== null && _refs$branches !== void 0 ? _refs$branches : [],
5038
+ remoteBranches: (_refs$remoteBranches = refs === null || refs === void 0 ? void 0 : refs.remoteBranches) !== null && _refs$remoteBranches !== void 0 ? _refs$remoteBranches : [],
5039
+ writable: !!host.localWrite && !!refs,
5040
+ busy: state.busy
5041
+ };
5042
+ const published = JSON.stringify(branchState);
5043
+ useEffect(() => controller.publish(branchState), [published]);
4518
5044
  if (!shown) return null;
4519
5045
  const groups = state.commit ? ["commit"] : [
4520
5046
  "staged",
@@ -4595,7 +5121,12 @@ function GitPanel({ vm, document, host, controller }) {
4595
5121
  locale,
4596
5122
  renderBlock: host.renderReviewBlock,
4597
5123
  locateFor: reviewLocator,
4598
- openTabs
5124
+ openTabs,
5125
+ connection: pullRequestConnection,
5126
+ openLocally: branching ? {
5127
+ open: openPullRequest,
5128
+ busy: state.busy
5129
+ } : void 0
4599
5130
  }), !review && state.repository && /* @__PURE__ */ React.createElement("div", { className: "twgit-content" }, /* @__PURE__ */ React.createElement("aside", null, state.browser && /* @__PURE__ */ React.createElement("details", null, /* @__PURE__ */ React.createElement("summary", null, labels.browse), /* @__PURE__ */ React.createElement("p", null, /* @__PURE__ */ React.createElement("code", null, state.browser.path || "/")), /* @__PURE__ */ React.createElement("button", {
4600
5131
  disabled: state.busy || !state.browser.path,
4601
5132
  onClick: () => void browse(state.browser.path.split("/").slice(0, -1).join("/"))
@@ -4776,15 +5307,21 @@ function upstreamText({ upstream }, labels) {
4776
5307
  default: return labels.upstreamUnknown;
4777
5308
  }
4778
5309
  }
5310
+ var refsOf = ({ branch, head, branches, remoteBranches }) => ({
5311
+ branch,
5312
+ head,
5313
+ branches,
5314
+ remoteBranches
5315
+ });
5316
+ var announce = () => {
5317
+ for (const listener of [...registrations]) listener();
5318
+ };
4779
5319
  function mountGitPanel({ vm, document, host }) {
4780
5320
  var _document$defaultView3;
4781
5321
  if (!((_document$defaultView3 = document.defaultView) === null || _document$defaultView3 === void 0 ? void 0 : _document$defaultView3.showDirectoryPicker)) return null;
4782
5322
  const root = document.createElement("div");
4783
5323
  document.body.append(root);
4784
- const controller = {
4785
- open() {},
4786
- async refresh() {}
4787
- };
5324
+ const controller = createController();
4788
5325
  controllers.set(vm, controller);
4789
5326
  render(/* @__PURE__ */ React.createElement(GitPanel, {
4790
5327
  vm,
@@ -4792,26 +5329,70 @@ function mountGitPanel({ vm, document, host }) {
4792
5329
  host,
4793
5330
  controller
4794
5331
  }), root);
5332
+ announce();
4795
5333
  return {
4796
5334
  refresh: () => controller.refresh(),
5335
+ open: (locale) => controller.open(locale),
4797
5336
  dispose() {
4798
5337
  var _host$connectionSetti3;
4799
5338
  if (controllers.get(vm) === controller) controllers.delete(vm);
4800
5339
  (_host$connectionSetti3 = host.connectionSettings) === null || _host$connectionSetti3 === void 0 || _host$connectionSetti3.disconnect().catch(() => {});
4801
5340
  unmountComponentAtNode(root);
4802
5341
  root.remove();
5342
+ announce();
4803
5343
  }
4804
5344
  };
4805
5345
  }
4806
- function renderGitMenu(createElement, { vm, locale = "en", MenuItem, MenuSection, onRequestClose }) {
4807
- const controller = controllers.get(vm);
4808
- if (!controller) return null;
4809
- return createElement(MenuSection, null, createElement(MenuItem, {
4810
- command: "local-git",
4811
- onClick() {
4812
- onRequestClose();
4813
- controller.open(locale);
4814
- }
4815
- }, labelsFor(locale).menu));
5346
+ /** The branch button of the panel mounted for `vm`, or nothing while no panel is mounted for it. */
5347
+ function BranchEntry({ vm, locale }) {
5348
+ const [controller, setController] = useState(() => controllers.get(vm));
5349
+ useEffect(() => {
5350
+ const update = () => setController(controllers.get(vm));
5351
+ registrations.add(update);
5352
+ update();
5353
+ return () => {
5354
+ registrations.delete(update);
5355
+ };
5356
+ }, [vm]);
5357
+ const [state, setState] = useState(controller === null || controller === void 0 ? void 0 : controller.branch);
5358
+ useEffect(() => {
5359
+ if (!controller) return;
5360
+ const update = () => setState(controller.branch);
5361
+ update();
5362
+ return controller.follow(update);
5363
+ }, [controller]);
5364
+ if (!controller || !state) return null;
5365
+ return /* @__PURE__ */ React.createElement(BranchButton, {
5366
+ state,
5367
+ locale,
5368
+ open: () => controller.open(locale),
5369
+ choose: () => void controller.choose(locale),
5370
+ switchBranch: (name) => controller.changeBranch({
5371
+ kind: "switchBranch",
5372
+ name
5373
+ }),
5374
+ createBranch: (name, checkout) => controller.changeBranch({
5375
+ kind: "createBranch",
5376
+ name,
5377
+ checkout
5378
+ }),
5379
+ createFromRemote: (name) => controller.changeBranch({
5380
+ kind: "createBranch",
5381
+ name,
5382
+ fromRemote: true,
5383
+ checkout: true
5384
+ })
5385
+ });
5386
+ }
5387
+ /**
5388
+ * Renders the button for the host's menu bar that shows the current branch of the panel mounted for `vm` and opens the
5389
+ * menu of branches, the panel and the repository choice. It renders nothing while no panel is mounted for `vm`, such as
5390
+ * when the host does not offer local Git, and shares the panel's repository, operations and checks.
5391
+ */
5392
+ function renderBranchButton(createElement, { vm, locale = "en" }) {
5393
+ return createElement(BranchEntry, {
5394
+ vm,
5395
+ locale
5396
+ });
4816
5397
  }
4817
- export { GitPanelError, HistoryGraph, createEditorBlockLocator, createReadOnlyFs, createScratchBlocksDiffLocator, createScratchBlocksDiffRenderer, laneColors, layoutHistory, mountGitPanel, openProjectComparison, openProjectSourceTabs, openRepository, renderGitMenu };
5398
+ export { BranchButton, GitPanelError, HistoryGraph, createEditorBlockLocator, createReadOnlyFs, createScratchBlocksDiffLocator, createScratchBlocksDiffRenderer, laneColors, layoutHistory, mountGitPanel, openProjectComparison, openProjectSourceTabs, openRepository, pullRequestBranch, pullRequestRef, renderBranchButton };