@kubohiroya/turbowarp-plus-editor-git 0.17.0 → 0.19.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.
Files changed (44) hide show
  1. package/README.ja.md +13 -3
  2. package/README.md +12 -2
  3. package/dist/branch-button.d.ts +47 -0
  4. package/dist/change-marks.d.ts +8 -0
  5. package/dist/connection-settings.d.ts +3 -1
  6. package/dist/github-pull-requests.d.ts +2 -2
  7. package/dist/github-review-api.d.ts +112 -0
  8. package/dist/index.d.ts +29 -5
  9. package/dist/index.js +830 -303
  10. package/dist/messages.d.ts +109 -2
  11. package/dist/{project-asset-paths-nL5sV35A.js → project-asset-paths-DNaoWs3E.js} +2 -2
  12. package/dist/{project-comparison-DjMlwyp8.js → project-comparison-B_3QZ6M-.js} +1414 -33
  13. package/dist/project-source-diff.d.ts +5 -0
  14. package/dist/project-source-diff.js +4 -4
  15. package/dist/project-source-review.d.ts +6 -1
  16. package/dist/pull-request-panel.d.ts +2 -0
  17. package/dist/remote-operations.d.ts +1 -1
  18. package/dist/{repository-xIv0iZkj.js → repository-DXRZEtYk.js} +26 -10
  19. package/dist/repository.d.ts +7 -0
  20. package/dist/repository.js +1 -1
  21. package/dist/review-composer.d.ts +23 -0
  22. package/dist/review-submit.d.ts +144 -0
  23. package/dist/{smart-http-BmHEkE7K.js → smart-http-eARzlXHO.js} +101 -2
  24. package/dist/smart-http.d.ts +2 -0
  25. package/dist/smart-http.js +1 -1
  26. package/dist/style.css +51 -0
  27. package/package.json +1 -1
  28. package/src/branch-button.tsx +289 -0
  29. package/src/change-marks.ts +22 -0
  30. package/src/connection-settings.tsx +9 -2
  31. package/src/github-review-api.ts +298 -0
  32. package/src/index.tsx +322 -66
  33. package/src/messages.ts +80 -4
  34. package/src/project-source-diff.css +10 -0
  35. package/src/project-source-diff.tsx +6 -0
  36. package/src/project-source-review.tsx +33 -5
  37. package/src/pull-request-panel.tsx +27 -6
  38. package/src/remote-operations.ts +1 -1
  39. package/src/repository.ts +22 -8
  40. package/src/review-composer.tsx +166 -0
  41. package/src/review-submit.ts +519 -0
  42. package/src/smart-http.ts +2 -2
  43. package/src/style.css +41 -0
  44. package/dist/objectSpread2-Dkd68nJH.js +0 -101
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
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-DjMlwyp8.js";
2
- import { n as init_objectSpread2, r as __commonJSMin, t as _objectSpread2$1 } from "./objectSpread2-Dkd68nJH.js";
3
- import { l as parseProjectSource, u as readProjectSourceDiff } from "./project-asset-paths-nL5sV35A.js";
4
- import { a as pullRequestRef, i as pullRequestBranch, n as openRepository, o as GitPanelError, s as createReadOnlyFs } from "./repository-xIv0iZkj.js";
1
+ import { B as CloseButton, E as reviewLabelsFor, M as projectPropertyLabel, O as createGitHubReviewApi, R as useAssetImage, T as labelsFor, a as openProjectSourceTabs, c as gitHubRepositoryFromOrigin, d as PullRequestReviewComposer, i as createScratchBlocksDiffLocator, j as organizeProjectChanges, l as ProjectSourceComparison, n as openProjectComparison, o as PullRequestError, r as createScratchBlocksDiffRenderer, s as createGitHubPullRequests, u as ProjectSourceReview, z as useCostumeImage } from "./project-comparison-B_3QZ6M-.js";
2
+ import { d as __commonJSMin, l as _objectSpread2$1, u as init_objectSpread2 } from "./smart-http-eARzlXHO.js";
3
+ import { f as _objectWithoutProperties$1, l as parseProjectSource, u as readProjectSourceDiff } from "./project-asset-paths-DNaoWs3E.js";
4
+ import { a as pullRequestRef, i as pullRequestBranch, n as openRepository, o as GitPanelError, s as createReadOnlyFs } from "./repository-DXRZEtYk.js";
5
5
  import * as m from "react";
6
- 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";
7
7
  import { createPortal, render, unmountComponentAtNode } from "react-dom";
8
8
  // builtin:esm-external-require-react
9
9
  var require_builtin_esm_external_require_react = /* @__PURE__ */ __commonJSMin(((exports, module) => {
@@ -3026,6 +3026,397 @@ function ProjectElementView({ changes, selection, locale, renderBlock, onSelectF
3026
3026
  onClick: () => onSelectFile(file)
3027
3027
  }, file.path, " ", /* @__PURE__ */ React.createElement("small", null, labels.groups[file.group])))))));
3028
3028
  }
3029
+ // src/branch-button.tsx
3030
+ init_objectSpread2();
3031
+ var _excluded = ["state", "locale"];
3032
+ var menuItems = "[role=\"menuitem\"], [role=\"menuitemradio\"]";
3033
+ /** How long a snackbar stays when it is neither hovered nor focused. */
3034
+ var snackbarDuration = 6e3;
3035
+ /**
3036
+ * A short result at the bottom of the window, with the project folder's status look. It stays while hovered or
3037
+ * focused, Escape or the close button dismisses it, and focus is never moved into it.
3038
+ */
3039
+ function Snackbar({ snack, closeLabel, onClose }) {
3040
+ const held = useRef({
3041
+ hover: false,
3042
+ focus: false
3043
+ });
3044
+ const timer = useRef();
3045
+ const hold = (change) => {
3046
+ held.current = _objectSpread2$1(_objectSpread2$1({}, held.current), change);
3047
+ clearTimeout(timer.current);
3048
+ if (!held.current.hover && !held.current.focus) timer.current = setTimeout(() => onClose(false), snackbarDuration);
3049
+ };
3050
+ useEffect(() => {
3051
+ hold({});
3052
+ return () => clearTimeout(timer.current);
3053
+ }, []);
3054
+ return createPortal(/* @__PURE__ */ React.createElement("div", {
3055
+ className: "twgit-snackbar",
3056
+ "data-git-snackbar": "",
3057
+ onMouseEnter: () => hold({ hover: true }),
3058
+ onMouseLeave: () => hold({ hover: false }),
3059
+ onFocus: () => hold({ focus: true }),
3060
+ onBlur: (event) => {
3061
+ if (!event.currentTarget.contains(event.relatedTarget)) hold({ focus: false });
3062
+ },
3063
+ onKeyDown: (event) => {
3064
+ if (event.key !== "Escape") return;
3065
+ event.preventDefault();
3066
+ event.stopPropagation();
3067
+ onClose(true);
3068
+ }
3069
+ }, /* @__PURE__ */ React.createElement("span", { role: snack.alert ? "alert" : "status" }, snack.text), snack.action && /* @__PURE__ */ React.createElement("button", {
3070
+ type: "button",
3071
+ onClick: () => {
3072
+ onClose(true);
3073
+ snack.action.run();
3074
+ }
3075
+ }, snack.action.label), /* @__PURE__ */ React.createElement(CloseButton, {
3076
+ label: closeLabel,
3077
+ onClick: () => onClose(true)
3078
+ })), document.body);
3079
+ }
3080
+ /**
3081
+ * The results of branch operations in a snackbar, as the branch button and the history view tell them: a switch with
3082
+ * Reload Project while a project folder is selected, a new branch, or the reason for a refusal with Open Local Git.
3083
+ * A declined confirmation tells nothing. `restoreFocus` takes the focus back when the snackbar closes from inside.
3084
+ */
3085
+ function useBranchSnackbar(locale, project, actions, restoreFocus) {
3086
+ const labels = labelsFor(locale);
3087
+ const [snack, setSnack] = useState(null);
3088
+ const snacks = useRef(0);
3089
+ const mounted = useRef(true);
3090
+ useEffect(() => () => {
3091
+ mounted.current = false;
3092
+ }, []);
3093
+ const show = (text, alert, action) => {
3094
+ if (mounted.current) setSnack({
3095
+ id: ++snacks.current,
3096
+ text,
3097
+ alert,
3098
+ action
3099
+ });
3100
+ };
3101
+ const report = (result, switched) => {
3102
+ if (result.done) {
3103
+ if (!switched) show(labels.branchCreated, false);
3104
+ else show(labels.switchedShort, false, project ? {
3105
+ label: labels.reload,
3106
+ run: actions.reload
3107
+ } : void 0);
3108
+ } else if (result.error) show(result.error, true, {
3109
+ label: labels.openPanel,
3110
+ run: actions.open
3111
+ });
3112
+ };
3113
+ return {
3114
+ report,
3115
+ element: snack && /* @__PURE__ */ React.createElement(Snackbar, {
3116
+ key: snack.id,
3117
+ snack,
3118
+ closeLabel: labels.close,
3119
+ onClose: (focus) => {
3120
+ setSnack((previous) => (previous === null || previous === void 0 ? void 0 : previous.id) === snack.id ? null : previous);
3121
+ if (focus) restoreFocus === null || restoreFocus === void 0 || restoreFocus();
3122
+ }
3123
+ })
3124
+ };
3125
+ }
3126
+ /** Git's branch glyph, drawn in the text color. */
3127
+ function BranchIcon() {
3128
+ return /* @__PURE__ */ React.createElement("svg", {
3129
+ className: "twgit-branch-icon",
3130
+ viewBox: "0 0 16 16",
3131
+ width: "16",
3132
+ height: "16",
3133
+ "aria-hidden": "true",
3134
+ focusable: "false"
3135
+ }, /* @__PURE__ */ React.createElement("g", {
3136
+ fill: "none",
3137
+ stroke: "currentColor",
3138
+ strokeWidth: "1.5"
3139
+ }, /* @__PURE__ */ React.createElement("circle", {
3140
+ cx: "4.5",
3141
+ cy: "3.5",
3142
+ r: "1.75"
3143
+ }), /* @__PURE__ */ React.createElement("circle", {
3144
+ cx: "4.5",
3145
+ cy: "12.5",
3146
+ r: "1.75"
3147
+ }), /* @__PURE__ */ React.createElement("circle", {
3148
+ cx: "11.5",
3149
+ cy: "5.5",
3150
+ r: "1.75"
3151
+ }), /* @__PURE__ */ React.createElement("path", { d: "M4.5 5.25v5.5M11.5 7.25c0 2.5-2.5 2.75-6 3.75" })));
3152
+ }
3153
+ /**
3154
+ * The current branch as a menu button for the host's menu bar, with the menu of branches. Before a repository is
3155
+ * chosen it reads "Git" and offers to choose one. A detached HEAD shows its short commit ID. The menu moves with the
3156
+ * arrow keys, Home and End; Escape closes it and returns to the button. Without `writable` it lists the branches only.
3157
+ */
3158
+ function BranchButton(_ref) {
3159
+ var _ref3;
3160
+ let { state, locale = "en" } = _ref, actions = _objectWithoutProperties$1(_ref, _excluded);
3161
+ const labels = labelsFor(locale);
3162
+ const [mode, setMode] = useState("closed");
3163
+ const [initial, setInitial] = useState("checked");
3164
+ const [name, setName] = useState("");
3165
+ const [checkout, setCheckout] = useState(true);
3166
+ const [creating, setCreating] = useState(false);
3167
+ const [createError, setCreateError] = useState("");
3168
+ const root = useRef(null);
3169
+ const trigger = useRef(null);
3170
+ const popup = useRef(null);
3171
+ const mounted = useRef(true);
3172
+ useEffect(() => () => {
3173
+ mounted.current = false;
3174
+ }, []);
3175
+ useLayoutEffect(() => {
3176
+ var _ref2;
3177
+ const element = popup.current;
3178
+ if (!element) return;
3179
+ if (mode === "create") {
3180
+ var _element$querySelecto;
3181
+ (_element$querySelecto = element.querySelector("input")) === null || _element$querySelecto === void 0 || _element$querySelecto.focus();
3182
+ return;
3183
+ }
3184
+ const items = [...element.querySelectorAll(menuItems)];
3185
+ const checked = items.find((item) => item.getAttribute("aria-checked") === "true");
3186
+ (_ref2 = initial === "last" ? items[items.length - 1] : initial === "checked" && checked || items[0]) === null || _ref2 === void 0 || _ref2.focus();
3187
+ }, [mode]);
3188
+ useEffect(() => {
3189
+ if (mode === "closed") return;
3190
+ const node = root.current;
3191
+ const dismiss = (event) => {
3192
+ if (!node.contains(event.target) && !creating) setMode("closed");
3193
+ };
3194
+ node.ownerDocument.addEventListener("pointerdown", dismiss, true);
3195
+ return () => node.ownerDocument.removeEventListener("pointerdown", dismiss, true);
3196
+ }, [mode, creating]);
3197
+ useEffect(() => {
3198
+ var _popup$current;
3199
+ if (!createError || creating) return;
3200
+ const input = (_popup$current = popup.current) === null || _popup$current === void 0 ? void 0 : _popup$current.querySelector("input:not([type])");
3201
+ input === null || input === void 0 || input.focus();
3202
+ input === null || input === void 0 || input.select();
3203
+ }, [createError, creating]);
3204
+ const { branch, head } = state;
3205
+ const unborn = state.repository && !head;
3206
+ const text = !state.repository ? "Git" : branch !== null && branch !== void 0 ? branch : head ? head.slice(0, 7) : labels.unborn;
3207
+ const accessibleName = !state.repository ? labels.title : `${branch ? labels.branch : labels.detached}: ${text}${unborn && branch ? ` (${labels.unborn})` : ""}`;
3208
+ const close = (focus = true) => {
3209
+ var _trigger$current;
3210
+ setMode("closed");
3211
+ if (focus) (_trigger$current = trigger.current) === null || _trigger$current === void 0 || _trigger$current.focus({ preventScroll: true });
3212
+ };
3213
+ const openMenu = (from) => {
3214
+ setInitial(from);
3215
+ setMode("menu");
3216
+ };
3217
+ const { report, element: snackbar } = useBranchSnackbar(locale, state.project, actions, () => {
3218
+ var _trigger$current2;
3219
+ return (_trigger$current2 = trigger.current) === null || _trigger$current2 === void 0 ? void 0 : _trigger$current2.focus({ preventScroll: true });
3220
+ });
3221
+ const change = async (operation) => report(await operation(), true);
3222
+ const items = [];
3223
+ if (!state.repository) items.push({
3224
+ id: "choose",
3225
+ label: labels.choose,
3226
+ run: () => {
3227
+ close();
3228
+ actions.choose();
3229
+ }
3230
+ });
3231
+ else {
3232
+ const switching = state.writable && !state.busy;
3233
+ for (const local of state.branches) items.push({
3234
+ id: "branch:" + local,
3235
+ label: local,
3236
+ checked: local === branch,
3237
+ disabled: !state.writable || state.busy && local !== branch,
3238
+ run: () => {
3239
+ close();
3240
+ if (local !== branch && switching) change(() => actions.switchBranch(local));
3241
+ }
3242
+ });
3243
+ if (state.writable) {
3244
+ state.remoteBranches.forEach((remote, index) => items.push({
3245
+ id: "remote:" + remote,
3246
+ label: labels.historyCreateFromRemote.replace("{name}", remote),
3247
+ disabled: state.busy,
3248
+ separatorBefore: index === 0,
3249
+ run: () => {
3250
+ close();
3251
+ if (!state.busy) change(() => actions.createFromRemote(remote));
3252
+ }
3253
+ }));
3254
+ if (head) items.push({
3255
+ id: "create",
3256
+ label: labels.newBranchItem,
3257
+ disabled: state.busy,
3258
+ separatorBefore: true,
3259
+ run: () => {
3260
+ if (state.busy) return;
3261
+ setName("");
3262
+ setCheckout(true);
3263
+ setCreateError("");
3264
+ setMode("create");
3265
+ }
3266
+ });
3267
+ }
3268
+ }
3269
+ items.push({
3270
+ id: "open",
3271
+ label: labels.openPanel,
3272
+ separatorBefore: items.length > 0 && items[items.length - 1].id !== "create",
3273
+ run: () => {
3274
+ close();
3275
+ actions.open();
3276
+ }
3277
+ });
3278
+ const onMenuKey = (event) => {
3279
+ if (event.key === "Escape") {
3280
+ event.preventDefault();
3281
+ event.stopPropagation();
3282
+ if (!creating) close();
3283
+ return;
3284
+ }
3285
+ if (mode !== "menu") return;
3286
+ if (event.key === "Tab") {
3287
+ close(false);
3288
+ return;
3289
+ }
3290
+ const elements = [...event.currentTarget.querySelectorAll(menuItems)];
3291
+ const index = elements.indexOf(event.target);
3292
+ const target = {
3293
+ ArrowDown: index + 1,
3294
+ ArrowUp: index - 1,
3295
+ Home: 0,
3296
+ End: elements.length - 1
3297
+ }[event.key];
3298
+ if (target === void 0 || !elements.length) return;
3299
+ event.preventDefault();
3300
+ elements[(target + elements.length) % elements.length].focus();
3301
+ };
3302
+ const submit = async (event) => {
3303
+ var _result$error;
3304
+ event.preventDefault();
3305
+ const branchName = name.trim();
3306
+ if (!branchName || creating) return;
3307
+ setCreating(true);
3308
+ setCreateError("");
3309
+ let result;
3310
+ try {
3311
+ result = await actions.createBranch(branchName, checkout);
3312
+ } catch (error) {
3313
+ result = {
3314
+ done: false,
3315
+ error: error instanceof Error && error.message ? error.message : labels.failed
3316
+ };
3317
+ }
3318
+ if (!mounted.current) return;
3319
+ setCreating(false);
3320
+ if (result.done) {
3321
+ close();
3322
+ report(result, checkout);
3323
+ } else setCreateError((_result$error = result.error) !== null && _result$error !== void 0 ? _result$error : "");
3324
+ };
3325
+ return /* @__PURE__ */ React.createElement("div", {
3326
+ className: "twgit-branch-entry",
3327
+ ref: root,
3328
+ onBlur: (event) => {
3329
+ const next = event.relatedTarget;
3330
+ if (next && !event.currentTarget.contains(next) && !creating) setMode("closed");
3331
+ }
3332
+ }, /* @__PURE__ */ React.createElement("button", {
3333
+ type: "button",
3334
+ className: "twgit-branch-button",
3335
+ ref: trigger,
3336
+ "aria-label": accessibleName,
3337
+ title: accessibleName,
3338
+ "aria-haspopup": "menu",
3339
+ "aria-expanded": mode === "menu",
3340
+ "data-git-branch": state.repository ? (_ref3 = branch !== null && branch !== void 0 ? branch : head) !== null && _ref3 !== void 0 ? _ref3 : "" : void 0,
3341
+ onClick: () => mode === "closed" ? openMenu("checked") : close(false),
3342
+ onKeyDown: (event) => {
3343
+ if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
3344
+ event.preventDefault();
3345
+ openMenu(event.key === "ArrowUp" ? "last" : "checked");
3346
+ }
3347
+ }, /* @__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", {
3348
+ className: "twgit-branch-caret",
3349
+ "aria-hidden": "true"
3350
+ }, "▾")), mode === "menu" && /* @__PURE__ */ React.createElement("div", {
3351
+ ref: popup,
3352
+ className: "twgit-branch-popup",
3353
+ role: "menu",
3354
+ "aria-label": labels.branchMenu,
3355
+ onKeyDown: onMenuKey
3356
+ }, items.map((item) => /* @__PURE__ */ React.createElement("button", {
3357
+ key: item.id,
3358
+ type: "button",
3359
+ tabIndex: -1,
3360
+ "data-action": item.id,
3361
+ role: item.checked === void 0 ? "menuitem" : "menuitemradio",
3362
+ "aria-checked": item.checked,
3363
+ "aria-disabled": item.disabled || void 0,
3364
+ "data-separator-before": item.separatorBefore || void 0,
3365
+ onClick: () => {
3366
+ if (!item.disabled || item.checked) item.run();
3367
+ }
3368
+ }, item.checked !== void 0 && /* @__PURE__ */ React.createElement("span", {
3369
+ className: "twgit-branch-check",
3370
+ "aria-hidden": "true"
3371
+ }, item.checked ? "✓" : ""), item.label))), mode === "create" && /* @__PURE__ */ React.createElement("div", {
3372
+ ref: popup,
3373
+ className: "twgit-branch-popup",
3374
+ role: "dialog",
3375
+ "aria-label": labels.newBranchTitle,
3376
+ onKeyDown: onMenuKey
3377
+ }, /* @__PURE__ */ React.createElement("form", { onSubmit: (event) => void submit(event) }, /* @__PURE__ */ React.createElement("label", null, labels.newBranch, " ", /* @__PURE__ */ React.createElement("input", {
3378
+ value: name,
3379
+ required: true,
3380
+ disabled: creating,
3381
+ "aria-invalid": createError ? true : void 0,
3382
+ onChange: (event) => setName(event.currentTarget.value)
3383
+ })), /* @__PURE__ */ React.createElement("label", null, /* @__PURE__ */ React.createElement("input", {
3384
+ type: "checkbox",
3385
+ checked: checkout,
3386
+ disabled: creating,
3387
+ onChange: (event) => setCheckout(event.currentTarget.checked)
3388
+ }), " ", labels.switchAfterCreate), createError && /* @__PURE__ */ React.createElement("p", {
3389
+ className: "twgit-graph-form-error",
3390
+ role: "alert"
3391
+ }, createError), /* @__PURE__ */ React.createElement("span", { className: "twgit-graph-form-actions" }, /* @__PURE__ */ React.createElement("button", {
3392
+ type: "submit",
3393
+ disabled: creating || state.busy || !name.trim()
3394
+ }, labels.createBranch), /* @__PURE__ */ React.createElement("button", {
3395
+ type: "button",
3396
+ disabled: creating,
3397
+ onClick: () => close()
3398
+ }, labels.cancel)))), snackbar);
3399
+ }
3400
+ // src/change-marks.ts
3401
+ /**
3402
+ * The marks of the working tree's changes under `folder` (a path inside the repository, '' for its root), by path
3403
+ * relative to `folder`. A file with both staged and unstaged changes is marked as its working tree stands.
3404
+ */
3405
+ function changeMarks(changes, folder) {
3406
+ const prefix = folder.replace(/^\/+|\/+$/gu, "");
3407
+ const marks = /* @__PURE__ */ new Map();
3408
+ for (const group of [
3409
+ "staged",
3410
+ "unstaged",
3411
+ "untracked"
3412
+ ]) for (const change of changes) {
3413
+ if (change.group !== group) continue;
3414
+ if (prefix && !change.path.startsWith(prefix + "/")) continue;
3415
+ const path = prefix ? change.path.slice(prefix.length + 1) : change.path;
3416
+ marks.set(path, group === "untracked" ? "untracked" : !change.after ? "deleted" : !change.before ? "added" : "modified");
3417
+ }
3418
+ return marks;
3419
+ }
3029
3420
  /**
3030
3421
  * Lays out commits ordered newest first with every child above its parents. Parents missing from `commits`, such as
3031
3422
  * those on a page not loaded yet or cut off by a shallow clone, keep their lane running to the bottom.
@@ -3104,234 +3495,6 @@ function layoutHistory(commits) {
3104
3495
  width: Math.max(0, ...rows.map((row) => row.width))
3105
3496
  };
3106
3497
  }
3107
- // src/messages.ts
3108
- var messages = {
3109
- en: {
3110
- pullRequests: "Pull requests",
3111
- localChanges: "Local changes",
3112
- title: "Local Git",
3113
- menu: "Local Git…",
3114
- close: "Close",
3115
- choose: "Choose Repository…",
3116
- refresh: "Refresh",
3117
- empty: "Choose an existing Git repository to inspect its files and history.",
3118
- repository: "Repository",
3119
- project: "Project folder",
3120
- none: "No project selected",
3121
- root: "Repository root",
3122
- open: "Open This Project",
3123
- reload: "Reload Project",
3124
- up: "Parent folder",
3125
- browse: "Choose a project inside this repository",
3126
- replace: "Replace the open project and discard its unsaved changes?",
3127
- dirty: "The Editor has unsaved changes.",
3128
- saved: "The Editor has no unsaved changes.",
3129
- staged: "Changes in the next record",
3130
- unstaged: "Saved, not yet recorded",
3131
- untracked: "New files never recorded",
3132
- commit: "Changes in this record",
3133
- history: "Records (commits)",
3134
- worktree: "Saved files",
3135
- clean: "No saved changes in the selected folder.",
3136
- branches: "Local branches",
3137
- unborn: "No records yet",
3138
- detached: "Detached HEAD",
3139
- select: "Select a file to view its diff.",
3140
- binary: "Binary asset",
3141
- large: "Text preview exceeds its display limit.",
3142
- bytes: "bytes",
3143
- mode: "Git file mode",
3144
- permissions: "Working-tree executable permission changes are unavailable in the browser.",
3145
- unchanged: "The text is unchanged.",
3146
- loading: "Reading repository…",
3147
- invalid: "Choose a repository with a .git directory, and a project inside that repository.",
3148
- changed: "Files or Git state changed while reading. Refresh to read the current state.",
3149
- outdated: "The repository changed outside the Editor. Refresh to read the current state.",
3150
- unsupported: "This repository contains an entry the browser cannot represent.",
3151
- commitMessage: "Write a message for this record.",
3152
- switchUnsafe: "Stop the project, and save or record (or undo) every change, before switching branches.",
3153
- branchExists: "A branch with this name already exists.",
3154
- 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.",
3155
- 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.",
3156
- openedPullRequest: "Opened pull request #{number} on the local branch {branch}. To go back, switch to {previous} in Local changes.",
3157
- openedPullRequestDetached: "Opened pull request #{number} on the local branch {branch}. To go back, switch to another branch in Local changes.",
3158
- 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.",
3159
- branchName: "Git cannot use this name for a branch.",
3160
- pullUnsafe: "Stop the project, and save or undo unsaved edits and unrecorded changes, before getting changes from GitHub (pull).",
3161
- diverged: "This branch and GitHub's have both gained records, so they cannot be combined here (pull only fast-forwards). Your branch and files are unchanged.",
3162
- session: "Reconnect GitHub for this repository and requested permission.",
3163
- pushUnknown: "It is unknown whether GitHub received the records. Check GitHub (fetch) before sending again.",
3164
- pushRefresh: "GitHub received the records. Check GitHub (fetch) to update the status.",
3165
- rollbackFailed: "Some local changes could not be restored. Check the files and the Git details.",
3166
- remoteFailed: "Could not complete the GitHub operation. Check the connection and try again.",
3167
- remoteDone: "The GitHub operation finished.",
3168
- stage: "Add to Next Record",
3169
- unstage: "Remove from Next Record",
3170
- stageAll: "Add All Changes to Next Record",
3171
- commitLabel: "Message for this record",
3172
- commitButton: "Record (Commit)",
3173
- committed: "Recorded the changes (commit).",
3174
- commitAuthor: "Recorded as",
3175
- saveFirst: "Save the project before adding its changes to the next record.",
3176
- author: "Set user.name and user.email for this repository in Git before recording. The Editor does not change Git settings.",
3177
- nothingStaged: "Add changes to the next record first.",
3178
- switchTo: "Switch",
3179
- currentBranch: "current",
3180
- createBranch: "Create Branch",
3181
- newBranch: "New branch name",
3182
- switchAfterCreate: "Switch to it",
3183
- createFromRemote: "Create and Switch",
3184
- remoteBranches: "Branches on GitHub (origin)",
3185
- switched: "Switched branches. Use Reload Project to open the files of this branch.",
3186
- branchCreated: "Created the branch.",
3187
- projectMissing: "The open project folder is not on this branch. Switch anyway?",
3188
- pulledProject: "Got the changes from GitHub. Use Reload Project to open the updated files.",
3189
- confirmPush: "Send the records on this branch to GitHub (push)?",
3190
- details: "Git details",
3191
- branch: "Branch",
3192
- head: "HEAD",
3193
- groups: "Git groups",
3194
- added: "Added",
3195
- modified: "Changed",
3196
- deleted: "Deleted",
3197
- gitStaged: "Staged",
3198
- gitUnstaged: "Unstaged",
3199
- gitUntracked: "Untracked",
3200
- upstreamSame: "Same as GitHub",
3201
- upstreamAhead: "Ahead of GitHub",
3202
- upstreamBehind: "Behind GitHub",
3203
- upstreamDiverged: "Diverged from GitHub",
3204
- upstreamUnknown: "Not checked with GitHub yet",
3205
- records: "records",
3206
- checkedAt: "as of the last check",
3207
- failed: "Could not read the repository. Check folder access and refresh.",
3208
- historyMore: "Show more",
3209
- historyLoading: "Reading records…",
3210
- historyFailed: "Could not read the records.",
3211
- historyActions: "Actions",
3212
- historyCreateHere: "Create branch here…",
3213
- historySwitch: "Switch to {name}",
3214
- historyCreateFromRemote: "Create and switch to {name}",
3215
- historyCopy: "Copy commit ID",
3216
- historyCopied: "Copied the commit ID.",
3217
- historyCopyFailed: "Could not copy the commit ID.",
3218
- historyRefs: "Branches",
3219
- cancel: "Cancel"
3220
- },
3221
- ja: {
3222
- pullRequests: "プルリクエスト",
3223
- localChanges: "ローカルの変更",
3224
- title: "ローカル Git",
3225
- menu: "ローカル Git…",
3226
- close: "閉じる",
3227
- choose: "リポジトリを選択…",
3228
- refresh: "再取得",
3229
- empty: "既存の Git リポジトリを選び、ファイルの状態と履歴を確認できます。",
3230
- repository: "リポジトリ",
3231
- project: "作品フォルダ",
3232
- none: "作品は未選択",
3233
- root: "リポジトリルート",
3234
- open: "この作品を開く",
3235
- reload: "作品を再読込",
3236
- up: "上のフォルダ",
3237
- browse: "リポジトリ内の作品を選択",
3238
- replace: "保存していない編集を破棄して、別の作品を開きますか?",
3239
- dirty: "Editor に保存していない編集があります。",
3240
- saved: "Editor に保存していない編集はありません。",
3241
- staged: "次の記録に入る変更",
3242
- unstaged: "保存したが、まだ記録していない変更",
3243
- untracked: "まだ記録したことのない新しいファイル",
3244
- commit: "この記録の変更",
3245
- history: "記録(コミット)の履歴",
3246
- worktree: "保存したファイル",
3247
- clean: "対象フォルダに、保存した変更はありません。",
3248
- branches: "ローカルブランチ",
3249
- unborn: "まだ記録がありません",
3250
- detached: "ブランチ未選択の HEAD",
3251
- select: "ファイルを選ぶと差分を表示します。",
3252
- binary: "バイナリアセット",
3253
- large: "テキスト差分の表示上限を超えています。",
3254
- bytes: "バイト",
3255
- mode: "Git のファイルモード",
3256
- permissions: "作業ツリーの実行権限の変更はブラウザーから確認できません。",
3257
- unchanged: "テキストに変更はありません。",
3258
- loading: "リポジトリを読込中…",
3259
- invalid: ".git ディレクトリを持つリポジトリと、その配下の作品を選んでください。",
3260
- changed: "読込中にファイルまたは Git の状態が変わりました。再取得してください。",
3261
- outdated: "Editor の外でリポジトリが変更されました。再取得すると最新の状態を読み込みます。",
3262
- unsupported: "ブラウザーで扱えない種類の Git エントリーがあります。",
3263
- commitMessage: "この記録の説明を書いてください。",
3264
- switchUnsafe: "ブランチを切り替える前に、実行を止め、すべての変更を保存して記録(または取り消し)してください。",
3265
- branchExists: "同じ名前のブランチがすでにあります。",
3266
- branchConflict: "この名前は既存のブランチとぶつかります。Git では、pr と pr/7 のように、あるブランチと、その名前に / を続けたブランチを両方は持てません。Git でもう一方の名前を変えるか消すか、別の名前にしてください。",
3267
- pullRequestBranch: "ブランチ pr/<番号> が、別の記録を指してすでにあるため、この版を開きませんでした。ブランチはそのままです。「ローカルの変更」で切り替えるか、Git で名前を変えるか消してから、もう一度ここで開いてください。",
3268
- openedPullRequest: "プルリクエスト #{number} を、ローカルブランチ {branch} で開きました。戻るときは「ローカルの変更」で {previous} に切り替えてください。",
3269
- openedPullRequestDetached: "プルリクエスト #{number} を、ローカルブランチ {branch} で開きました。戻るときは「ローカルの変更」で別のブランチに切り替えてください。",
3270
- locked: "ほかの Git の操作が、このリポジトリを使っています(ロックファイル、またはマージ・cherry-pick・revert・rebase の途中)。終わってから、もう一度試してください。",
3271
- branchName: "この名前は Git のブランチ名に使えません。",
3272
- pullUnsafe: "GitHub から取り込む前に、実行を止め、保存していない編集とまだ記録していない変更をなくしてください。",
3273
- diverged: "このブランチと GitHub の両方に新しい記録があるため、ここでは取り込めません(pull は fast-forward のみ)。ブランチとファイルはそのままです。",
3274
- session: "このリポジトリと操作に必要な権限で GitHub に接続してください。",
3275
- pushUnknown: "GitHub が記録を受け取ったか分かりません。送り直す前に「GitHub と確かめる」を使ってください。",
3276
- pushRefresh: "GitHub が記録を受け取りました。「GitHub と確かめる」で状態を更新してください。",
3277
- rollbackFailed: "一部のローカルの変更を元に戻せませんでした。ファイルと Git の詳細を確認してください。",
3278
- remoteFailed: "GitHub との操作を完了できませんでした。接続を確認して、もう一度試してください。",
3279
- remoteDone: "GitHub との操作が終わりました。",
3280
- stage: "次の記録に入れる",
3281
- unstage: "次の記録から外す",
3282
- stageAll: "すべての変更を次の記録に入れる",
3283
- commitLabel: "この記録の説明",
3284
- commitButton: "記録する(コミット)",
3285
- committed: "変更を記録しました(コミット)。",
3286
- commitAuthor: "記録する人",
3287
- saveFirst: "次の記録に入れる前に、作品を保存してください。",
3288
- author: "記録する前に、Git でこのリポジトリの user.name と user.email を設定してください。Editor は Git の設定を変えません。",
3289
- nothingStaged: "先に、変更を次の記録に入れてください。",
3290
- switchTo: "切り替える",
3291
- currentBranch: "現在",
3292
- createBranch: "ブランチを作る",
3293
- newBranch: "新しいブランチの名前",
3294
- switchAfterCreate: "作ったら切り替える",
3295
- createFromRemote: "作って切り替える",
3296
- remoteBranches: "GitHub(origin)のブランチ",
3297
- switched: "ブランチを切り替えました。「作品を再読込」で、このブランチのファイルを開けます。",
3298
- branchCreated: "ブランチを作りました。",
3299
- projectMissing: "開いている作品フォルダは、このブランチにありません。切り替えますか?",
3300
- pulledProject: "GitHub から取り込みました。「作品を再読込」で更新したファイルを開けます。",
3301
- confirmPush: "このブランチの記録を GitHub に送りますか(push)?",
3302
- details: "Git の詳細",
3303
- branch: "ブランチ",
3304
- head: "HEAD",
3305
- groups: "Git の区分",
3306
- added: "追加",
3307
- modified: "変更",
3308
- deleted: "削除",
3309
- gitStaged: "ステージ済み (staged)",
3310
- gitUnstaged: "未ステージ (unstaged)",
3311
- gitUntracked: "未追跡 (untracked)",
3312
- upstreamSame: "GitHub と同じ",
3313
- upstreamAhead: "GitHub より進んでいる",
3314
- upstreamBehind: "GitHub より遅れている",
3315
- upstreamDiverged: "GitHub と分かれている",
3316
- upstreamUnknown: "まだ GitHub と確かめていない",
3317
- records: "件",
3318
- checkedAt: "最後に確かめた時点",
3319
- failed: "リポジトリを読み込めませんでした。フォルダへのアクセスを確認し、再取得してください。",
3320
- historyMore: "さらに表示",
3321
- historyLoading: "記録を読込中…",
3322
- historyFailed: "記録を読み込めませんでした。",
3323
- historyActions: "操作",
3324
- historyCreateHere: "ここからブランチを作る…",
3325
- historySwitch: "{name} に切り替える",
3326
- historyCreateFromRemote: "{name} を作って切り替える",
3327
- historyCopy: "コミットの ID をコピー",
3328
- historyCopied: "コミットの ID をコピーしました。",
3329
- historyCopyFailed: "コミットの ID をコピーできませんでした。",
3330
- historyRefs: "ブランチ",
3331
- cancel: "キャンセル"
3332
- }
3333
- };
3334
- var labelsFor = (locale) => locale.startsWith("ja") ? messages.ja : messages.en;
3335
3498
  // src/history-graph-view.tsx
3336
3499
  /** Lane colors that read on light and dark backgrounds. */
3337
3500
  var laneColors = [
@@ -3722,6 +3885,7 @@ function ConnectionSettings({ settings, locale, readOrigin, runOperation }) {
3722
3885
  const [, update] = useState(0);
3723
3886
  const [busy, setBusy] = useState(false);
3724
3887
  const [allowPush, setAllowPush] = useState(false);
3888
+ const [allowReview, setAllowReview] = useState(false);
3725
3889
  const [error, setError] = useState(null);
3726
3890
  const active = useRef(true);
3727
3891
  useEffect(() => {
@@ -3749,7 +3913,7 @@ function ConnectionSettings({ settings, locale, readOrigin, runOperation }) {
3749
3913
  const connect = () => run(async () => {
3750
3914
  const remote = await (readOrigin === null || readOrigin === void 0 ? void 0 : readOrigin());
3751
3915
  if (!remote) throw new Error("INVALID_REMOTE");
3752
- await settings.connect(remote, allowPush);
3916
+ await (allowReview ? settings.connect(remote, allowPush, true) : settings.connect(remote, allowPush));
3753
3917
  });
3754
3918
  const connected = !!mode && state.status === "connected";
3755
3919
  return /* @__PURE__ */ React.createElement(React.Fragment, null, runOperation && connected && /* @__PURE__ */ React.createElement("span", { className: "twgit-sync" }, [
@@ -3781,7 +3945,13 @@ function ConnectionSettings({ settings, locale, readOrigin, runOperation }) {
3781
3945
  checked: allowPush,
3782
3946
  disabled: busy || state.status === "connecting" || state.status === "connected",
3783
3947
  onChange: (event) => setAllowPush(event.currentTarget.checked)
3784
- }), " ", labels.allowPush), /* @__PURE__ */ React.createElement("button", {
3948
+ }), " ", labels.allowPush), /* @__PURE__ */ React.createElement("label", null, /* @__PURE__ */ React.createElement("input", {
3949
+ type: "checkbox",
3950
+ "data-git-allow-review": "",
3951
+ checked: allowReview,
3952
+ disabled: busy || state.status === "connecting" || state.status === "connected",
3953
+ onChange: (event) => setAllowReview(event.currentTarget.checked)
3954
+ }), " ", reviewLabelsFor(locale).allowReview), /* @__PURE__ */ React.createElement("button", {
3785
3955
  disabled: busy || !readOrigin || state.status === "connecting" || state.status === "connected",
3786
3956
  onClick: connect
3787
3957
  }, labels.connect), authorization && /* @__PURE__ */ React.createElement("p", null, authorization.userCode && /* @__PURE__ */ React.createElement("code", null, authorization.userCode, " "), /* @__PURE__ */ React.createElement("a", {
@@ -3888,9 +4058,20 @@ function errorMessage(error, labels) {
3888
4058
  }
3889
4059
  return labels.failed;
3890
4060
  }
3891
- function PullRequests({ name, labels, locale, renderBlock, locateFor, openTabs, repository, connection, openLocally }) {
3892
- var _pr$base$ref, _pr$git, _pr$base$ref2;
4061
+ function PullRequests({ name, remote, labels, locale, renderBlock, locateFor, openTabs, repository, connection, openLocally }) {
4062
+ var _connection$reviewGra, _pr$base$ref, _pr$git, _pr$base$ref2, _connection$reviewGra2;
3893
4063
  const client = useMemo(() => createGitHubPullRequests(name), [name]);
4064
+ const reviewApi = useMemo(() => {
4065
+ if (!remote || !connection) return null;
4066
+ try {
4067
+ return createGitHubReviewApi({
4068
+ remote,
4069
+ getSession: connection.getSession
4070
+ });
4071
+ } catch (_unused) {
4072
+ return null;
4073
+ }
4074
+ }, [remote, connection]);
3894
4075
  const [page, setPage] = useState(1);
3895
4076
  const [listing, setListing] = useState();
3896
4077
  const [input, setInput] = useState("");
@@ -3997,6 +4178,7 @@ function PullRequests({ name, labels, locale, renderBlock, locateFor, openTabs,
3997
4178
  setInput(String(number));
3998
4179
  };
3999
4180
  const pr = detail === null || detail === void 0 ? void 0 : detail.data;
4181
+ const reviewing = !!(pr === null || pr === void 0 ? void 0 : pr.git) && !!reviewApi && !!(connection === null || connection === void 0 ? void 0 : connection.connected()) && !!((_connection$reviewGra = connection.reviewGranted) === null || _connection$reviewGra === void 0 ? void 0 : _connection$reviewGra.call(connection));
4000
4182
  return /* @__PURE__ */ React.createElement("div", { className: "twgit-content" }, /* @__PURE__ */ React.createElement("aside", null, /* @__PURE__ */ React.createElement("h3", null, labels.title), /* @__PURE__ */ React.createElement("p", null, name), /* @__PURE__ */ React.createElement("form", {
4001
4183
  className: "twgit-pr-number",
4002
4184
  onSubmit: (event) => {
@@ -4045,7 +4227,7 @@ function PullRequests({ name, labels, locale, renderBlock, locateFor, openTabs,
4045
4227
  let projects;
4046
4228
  try {
4047
4229
  projects = readProjectSourceDiff(file.patch, file.beforeSource);
4048
- } catch (_unused) {
4230
+ } catch (_unused2) {
4049
4231
  return [];
4050
4232
  }
4051
4233
  return projects ? [{
@@ -4065,13 +4247,26 @@ function PullRequests({ name, labels, locale, renderBlock, locateFor, openTabs,
4065
4247
  }, labels.openLocally.replace("{branch}", pullRequestBranch(pr.number)))), pr.git && /* @__PURE__ */ React.createElement("p", {
4066
4248
  className: "twgit-context",
4067
4249
  role: "status"
4068
- }, labels.viaConnection, pr.git.branches && /* @__PURE__ */ React.createElement(React.Fragment, null, " ", labels.noMetadata)), /* @__PURE__ */ React.createElement(ProjectSourceReview, {
4250
+ }, labels.viaConnection, pr.git.branches && /* @__PURE__ */ React.createElement(React.Fragment, null, " ", labels.noMetadata)), pr.git && (connection === null || connection === void 0 ? void 0 : connection.connected()) && !((_connection$reviewGra2 = connection.reviewGranted) === null || _connection$reviewGra2 === void 0 ? void 0 : _connection$reviewGra2.call(connection)) && /* @__PURE__ */ React.createElement("p", {
4251
+ className: "twgit-context",
4252
+ role: "status"
4253
+ }, reviewLabelsFor(locale).needsGrant), /* @__PURE__ */ React.createElement(ProjectSourceReview, {
4069
4254
  title: pr.title,
4070
4255
  description: pr.description,
4071
4256
  files: locateFor ? pr.files.map((file) => "patch" in file ? _objectSpread2$1(_objectSpread2$1({}, file), {}, { locateBlock: locateFor(file.path, pr.head.sha) }) : file) : pr.files,
4072
4257
  renderBlock,
4073
4258
  locale,
4074
- rememberAs: `${name}#${pr.number}`
4259
+ rememberAs: `${name}#${pr.number}`,
4260
+ commentAs: reviewing ? `${name}#${pr.number}` : void 0
4261
+ }), reviewing && reviewApi && /* @__PURE__ */ React.createElement(PullRequestReviewComposer, {
4262
+ key: `${name}#${pr.number}`,
4263
+ api: reviewApi,
4264
+ name: `${name}#${pr.number}`,
4265
+ number: pr.number,
4266
+ head: pr.head.sha,
4267
+ changedFiles: pr.files.map((file) => file.path),
4268
+ url: pr.url,
4269
+ locale
4075
4270
  }))));
4076
4271
  }
4077
4272
  function PullRequestReviewPanel({ repository, locale, renderBlock, locateFor, openTabs, connection, openLocally }) {
@@ -4082,7 +4277,8 @@ function PullRequestReviewPanel({ repository, locale, renderBlock, locateFor, op
4082
4277
  repository.readOrigin().then((value) => {
4083
4278
  if (active) setOrigin({
4084
4279
  repository,
4085
- name: gitHubRepositoryFromOrigin(value)
4280
+ name: gitHubRepositoryFromOrigin(value),
4281
+ remote: value
4086
4282
  });
4087
4283
  }, () => {
4088
4284
  if (active) setOrigin({
@@ -4106,6 +4302,7 @@ function PullRequestReviewPanel({ repository, locale, renderBlock, locateFor, op
4106
4302
  return /* @__PURE__ */ React.createElement(PullRequests, {
4107
4303
  key: origin.name,
4108
4304
  name: origin.name,
4305
+ remote: origin.remote,
4109
4306
  labels,
4110
4307
  locale,
4111
4308
  renderBlock,
@@ -4154,6 +4351,47 @@ function nextFrame(signal) {
4154
4351
  }
4155
4352
  // src/index.tsx
4156
4353
  init_objectSpread2();
4354
+ function createController() {
4355
+ const listeners = /* @__PURE__ */ new Set();
4356
+ const controller = {
4357
+ open() {},
4358
+ async refresh() {},
4359
+ async choose() {},
4360
+ async changeBranch() {
4361
+ return { done: false };
4362
+ },
4363
+ reload() {},
4364
+ branch: {
4365
+ repository: false,
4366
+ branch: void 0,
4367
+ head: null,
4368
+ branches: [],
4369
+ remoteBranches: [],
4370
+ writable: false,
4371
+ busy: false,
4372
+ project: false
4373
+ },
4374
+ repository: null,
4375
+ snapshot: null,
4376
+ publish(state, repository, snapshot) {
4377
+ controller.branch = state;
4378
+ controller.repository = repository;
4379
+ controller.snapshot = snapshot;
4380
+ for (const listener of [...listeners]) listener();
4381
+ },
4382
+ follow(listener) {
4383
+ listeners.add(listener);
4384
+ controller.onFollowers(listeners.size);
4385
+ return () => {
4386
+ listeners.delete(listener);
4387
+ controller.onFollowers(listeners.size);
4388
+ };
4389
+ },
4390
+ followers: () => listeners.size,
4391
+ onFollowers() {}
4392
+ };
4393
+ return controller;
4394
+ }
4157
4395
  var initial = {
4158
4396
  repository: null,
4159
4397
  snapshot: null,
@@ -4167,9 +4405,56 @@ var initial = {
4167
4405
  busy: false
4168
4406
  };
4169
4407
  var controllers = /* @__PURE__ */ new WeakMap();
4408
+ var registrations = /* @__PURE__ */ new Set();
4170
4409
  var watchInterval = 2e3;
4410
+ /**
4411
+ * Calls `check` every `watchInterval`, when the window regains focus and shortly after a File System Observer reports
4412
+ * a change under `root`, one call at a time and never while the page is hidden. Returns the function that stops it.
4413
+ */
4414
+ function watchRepository(document, root, check) {
4415
+ const view = document.defaultView;
4416
+ let stopped = false;
4417
+ let checking = false;
4418
+ const run = async () => {
4419
+ if (stopped || checking || document.visibilityState === "hidden") return;
4420
+ checking = true;
4421
+ try {
4422
+ await check(() => stopped);
4423
+ } catch (_unused) {} finally {
4424
+ checking = false;
4425
+ }
4426
+ };
4427
+ let soon;
4428
+ const schedule = () => {
4429
+ view.clearTimeout(soon);
4430
+ soon = view.setTimeout(() => void run(), 300);
4431
+ };
4432
+ const interval = view.setInterval(() => void run(), watchInterval);
4433
+ const onFocus = () => void run();
4434
+ view.addEventListener("focus", onFocus);
4435
+ document.addEventListener("visibilitychange", onFocus);
4436
+ let observer = null;
4437
+ try {
4438
+ if (view.FileSystemObserver) {
4439
+ observer = new view.FileSystemObserver(schedule);
4440
+ observer.observe(root, { recursive: true }).catch(() => {});
4441
+ }
4442
+ } catch (_unused2) {
4443
+ observer = null;
4444
+ }
4445
+ run();
4446
+ return () => {
4447
+ stopped = true;
4448
+ view.clearInterval(interval);
4449
+ view.clearTimeout(soon);
4450
+ view.removeEventListener("focus", onFocus);
4451
+ document.removeEventListener("visibilitychange", onFocus);
4452
+ observer === null || observer === void 0 || observer.disconnect();
4453
+ };
4454
+ }
4455
+ 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");
4171
4456
  function GitPanel({ vm, document, host, controller }) {
4172
- var _state$snapshot, _state$repository, _state$snapshot$head$, _state$snapshot$head;
4457
+ var _refs$head, _refs$branches, _refs$remoteBranches, _state$snapshot, _state$repository, _state$snapshot$head$, _state$snapshot$head;
4173
4458
  const [visible, setVisible] = useState(false);
4174
4459
  const [slot, setSlot] = useState(null);
4175
4460
  const shown = visible || slot !== null;
@@ -4342,57 +4627,42 @@ function GitPanel({ vm, document, host, controller }) {
4342
4627
  controller.refresh();
4343
4628
  }
4344
4629
  }, [shown]);
4630
+ const [refs, setRefs] = useState(null);
4631
+ useEffect(() => {
4632
+ if (!state.repository) setRefs(null);
4633
+ else if (state.snapshot) setRefs(refsOf(state.snapshot));
4634
+ }, [state.repository, state.snapshot]);
4635
+ const [followers, setFollowers] = useState(controller.followers());
4636
+ useEffect(() => {
4637
+ controller.onFollowers = setFollowers;
4638
+ setFollowers(controller.followers());
4639
+ return () => {
4640
+ controller.onFollowers = () => {};
4641
+ };
4642
+ }, []);
4643
+ const following = followers > 0;
4345
4644
  useEffect(() => {
4346
4645
  const repository = state.repository;
4347
- if (!shown || !repository) return;
4348
- const view = document.defaultView;
4349
- let stopped = false;
4350
- let checking = false;
4351
- const check = async () => {
4646
+ if (!repository || !shown && !following) return;
4647
+ return watchRepository(document, repository.root, async (stopped) => {
4352
4648
  const { snapshot } = current.current;
4353
- const hidden = !dialog.current || !dialog.current.getClientRects().length;
4354
- if (stopped || checking || running.current || document.visibilityState === "hidden" || hidden || !snapshot || current.current.repository !== repository) return;
4355
- checking = true;
4356
- try {
4357
- const probe = await repository.probe(snapshot.scope);
4358
- if (stopped || probe === "locked" || probe === snapshot.probe || probe === handled.current || current.current.snapshot !== snapshot) return;
4359
- handled.current = probe;
4360
- const { selected, commit } = current.current;
4361
- if (selected || commit || reviewing.current) setState((previous) => previous.snapshot === snapshot ? _objectSpread2$1(_objectSpread2$1({}, previous), {}, { notice: labelsFor(locale).outdated }) : previous);
4362
- else await controller.refresh(true);
4363
- } catch (_unused) {} finally {
4364
- checking = false;
4365
- }
4366
- };
4367
- let soon;
4368
- const schedule = () => {
4369
- view.clearTimeout(soon);
4370
- soon = view.setTimeout(() => void check(), 300);
4371
- };
4372
- const interval = view.setInterval(() => void check(), watchInterval);
4373
- const onFocus = () => void check();
4374
- view.addEventListener("focus", onFocus);
4375
- document.addEventListener("visibilitychange", onFocus);
4376
- let observer = null;
4377
- try {
4378
- if (view.FileSystemObserver) {
4379
- observer = new view.FileSystemObserver(schedule);
4380
- observer.observe(repository.root, { recursive: true }).catch(() => {});
4649
+ if (running.current || current.current.repository !== repository) return;
4650
+ if (!(shown && !!dialog.current && dialog.current.getClientRects().length > 0) || !snapshot) {
4651
+ if (!following) return;
4652
+ const next = await repository.refs();
4653
+ if (!stopped() && current.current.repository === repository) setRefs((previous) => sameRefs(previous, next) ? previous : next);
4654
+ return;
4381
4655
  }
4382
- } catch (_unused2) {
4383
- observer = null;
4384
- }
4385
- check();
4386
- return () => {
4387
- stopped = true;
4388
- view.clearInterval(interval);
4389
- view.clearTimeout(soon);
4390
- view.removeEventListener("focus", onFocus);
4391
- document.removeEventListener("visibilitychange", onFocus);
4392
- observer === null || observer === void 0 || observer.disconnect();
4393
- };
4656
+ const probe = await repository.probe(snapshot.scope);
4657
+ if (stopped() || probe === "locked" || probe === snapshot.probe || probe === handled.current || current.current.snapshot !== snapshot) return;
4658
+ handled.current = probe;
4659
+ const { selected, commit } = current.current;
4660
+ if (selected || commit || reviewing.current) setState((previous) => previous.snapshot === snapshot ? _objectSpread2$1(_objectSpread2$1({}, previous), {}, { notice: labelsFor(locale).outdated }) : previous);
4661
+ else await controller.refresh(true);
4662
+ });
4394
4663
  }, [
4395
4664
  shown,
4665
+ following,
4396
4666
  state.repository,
4397
4667
  locale
4398
4668
  ]);
@@ -4407,7 +4677,7 @@ function GitPanel({ vm, document, host, controller }) {
4407
4677
  }, [visible, slot]);
4408
4678
  const chooseRoot = async () => {
4409
4679
  const view = document.defaultView;
4410
- await run(async () => {
4680
+ return run(async () => {
4411
4681
  var _host$connectionSetti;
4412
4682
  let root;
4413
4683
  try {
@@ -4524,7 +4794,8 @@ function GitPanel({ vm, document, host, controller }) {
4524
4794
  const getSession = settings === null || settings === void 0 ? void 0 : settings.getSession;
4525
4795
  return getSession ? {
4526
4796
  getSession,
4527
- connected: () => settings.getView().state.status === "connected"
4797
+ connected: () => settings.getView().state.status === "connected",
4798
+ reviewGranted: () => settings.getView().review === true
4528
4799
  } : void 0;
4529
4800
  }, [host]);
4530
4801
  const [message, setMessage] = useState("");
@@ -4673,6 +4944,32 @@ function GitPanel({ vm, document, host, controller }) {
4673
4944
  state.snapshot,
4674
4945
  state.selected
4675
4946
  ]);
4947
+ controller.changeBranch = changeBranch;
4948
+ controller.reload = () => {
4949
+ if (current.current.project !== null) openProject(current.current.project);
4950
+ };
4951
+ controller.choose = async (language) => {
4952
+ setLocale(language);
4953
+ const before = current.current.repository;
4954
+ const result = await chooseRoot();
4955
+ if (!result.done && result.error || current.current.repository !== before) controller.open(language);
4956
+ };
4957
+ const branchState = {
4958
+ repository: !!state.repository,
4959
+ branch: refs === null || refs === void 0 ? void 0 : refs.branch,
4960
+ head: (_refs$head = refs === null || refs === void 0 ? void 0 : refs.head) !== null && _refs$head !== void 0 ? _refs$head : null,
4961
+ branches: (_refs$branches = refs === null || refs === void 0 ? void 0 : refs.branches) !== null && _refs$branches !== void 0 ? _refs$branches : [],
4962
+ remoteBranches: (_refs$remoteBranches = refs === null || refs === void 0 ? void 0 : refs.remoteBranches) !== null && _refs$remoteBranches !== void 0 ? _refs$remoteBranches : [],
4963
+ writable: !!host.localWrite && !!refs,
4964
+ busy: state.busy,
4965
+ project: state.project !== null
4966
+ };
4967
+ const published = JSON.stringify(branchState);
4968
+ useEffect(() => controller.publish(branchState, state.repository, state.snapshot), [
4969
+ published,
4970
+ state.repository,
4971
+ state.snapshot
4972
+ ]);
4676
4973
  if (!shown) return null;
4677
4974
  const groups = state.commit ? ["commit"] : [
4678
4975
  "staged",
@@ -4939,15 +5236,21 @@ function upstreamText({ upstream }, labels) {
4939
5236
  default: return labels.upstreamUnknown;
4940
5237
  }
4941
5238
  }
5239
+ var refsOf = ({ branch, head, branches, remoteBranches }) => ({
5240
+ branch,
5241
+ head,
5242
+ branches,
5243
+ remoteBranches
5244
+ });
5245
+ var announce = () => {
5246
+ for (const listener of [...registrations]) listener();
5247
+ };
4942
5248
  function mountGitPanel({ vm, document, host }) {
4943
5249
  var _document$defaultView3;
4944
5250
  if (!((_document$defaultView3 = document.defaultView) === null || _document$defaultView3 === void 0 ? void 0 : _document$defaultView3.showDirectoryPicker)) return null;
4945
5251
  const root = document.createElement("div");
4946
5252
  document.body.append(root);
4947
- const controller = {
4948
- open() {},
4949
- async refresh() {}
4950
- };
5253
+ const controller = createController();
4951
5254
  controllers.set(vm, controller);
4952
5255
  render(/* @__PURE__ */ React.createElement(GitPanel, {
4953
5256
  vm,
@@ -4955,26 +5258,250 @@ function mountGitPanel({ vm, document, host }) {
4955
5258
  host,
4956
5259
  controller
4957
5260
  }), root);
5261
+ announce();
4958
5262
  return {
4959
5263
  refresh: () => controller.refresh(),
5264
+ open: (locale) => controller.open(locale),
4960
5265
  dispose() {
4961
5266
  var _host$connectionSetti3;
4962
5267
  if (controllers.get(vm) === controller) controllers.delete(vm);
4963
5268
  (_host$connectionSetti3 = host.connectionSettings) === null || _host$connectionSetti3 === void 0 || _host$connectionSetti3.disconnect().catch(() => {});
4964
5269
  unmountComponentAtNode(root);
4965
5270
  root.remove();
5271
+ announce();
4966
5272
  }
4967
5273
  };
4968
5274
  }
4969
- function renderGitMenu(createElement, { vm, locale = "en", MenuItem, MenuSection, onRequestClose }) {
4970
- const controller = controllers.get(vm);
4971
- if (!controller) return null;
4972
- return createElement(MenuSection, null, createElement(MenuItem, {
4973
- command: "local-git",
4974
- onClick() {
4975
- onRequestClose();
4976
- controller.open(locale);
5275
+ /** The branch button of the panel mounted for `vm`, or nothing while no panel is mounted for it. */
5276
+ /** Calls `listener` with the controller of the panel mounted for `vm`, now and whenever it is mounted or disposed. */
5277
+ function findController(vm, listener) {
5278
+ const update = () => listener(controllers.get(vm));
5279
+ registrations.add(update);
5280
+ update();
5281
+ return () => {
5282
+ registrations.delete(update);
5283
+ };
5284
+ }
5285
+ /** The panel mounted for `vm` and what it last published. Following it keeps the closed panel watching the branches. */
5286
+ function useController(vm) {
5287
+ const [controller, setController] = useState(() => controllers.get(vm));
5288
+ useEffect(() => findController(vm, setController), [vm]);
5289
+ const [state, setState] = useState(controller === null || controller === void 0 ? void 0 : controller.branch);
5290
+ useEffect(() => {
5291
+ if (!controller) return;
5292
+ const update = () => setState(controller.branch);
5293
+ update();
5294
+ return controller.follow(update);
5295
+ }, [controller]);
5296
+ return {
5297
+ controller,
5298
+ state
5299
+ };
5300
+ }
5301
+ function BranchEntry({ vm, locale }) {
5302
+ const { controller, state } = useController(vm);
5303
+ if (!controller || !state) return null;
5304
+ return /* @__PURE__ */ React.createElement(BranchButton, {
5305
+ state,
5306
+ locale,
5307
+ open: () => controller.open(locale),
5308
+ choose: () => void controller.choose(locale),
5309
+ reload: () => controller.reload(),
5310
+ switchBranch: (name) => controller.changeBranch({
5311
+ kind: "switchBranch",
5312
+ name
5313
+ }),
5314
+ createBranch: (name, checkout) => controller.changeBranch({
5315
+ kind: "createBranch",
5316
+ name,
5317
+ checkout
5318
+ }),
5319
+ createFromRemote: (name) => controller.changeBranch({
5320
+ kind: "createBranch",
5321
+ name,
5322
+ fromRemote: true,
5323
+ checkout: true
5324
+ })
5325
+ });
5326
+ }
5327
+ /**
5328
+ * Renders the button for the host's menu bar that shows the current branch of the panel mounted for `vm` and opens the
5329
+ * menu of branches, the panel and the repository choice. It renders nothing while no panel is mounted for `vm`, such as
5330
+ * when the host does not offer local Git, and shares the panel's repository, operations and checks.
5331
+ */
5332
+ function renderBranchButton(createElement, { vm, locale = "en" }) {
5333
+ return createElement(BranchEntry, {
5334
+ vm,
5335
+ locale
5336
+ });
5337
+ }
5338
+ /** The history of the panel's repository, read again whenever its branches move. */
5339
+ function HistoryEntry({ vm, locale }) {
5340
+ const { controller, state } = useController(vm);
5341
+ const labels = labelsFor(locale);
5342
+ const repository = state && (controller === null || controller === void 0 ? void 0 : controller.repository) || null;
5343
+ const [history, setHistory] = useState({
5344
+ commits: [],
5345
+ cursor: null,
5346
+ loading: false,
5347
+ failed: false
5348
+ });
5349
+ const [selected, setSelected] = useState(null);
5350
+ const ticket = useRef(0);
5351
+ const root = useRef(null);
5352
+ const refs = state ? [
5353
+ state.head,
5354
+ ...state.branches,
5355
+ "",
5356
+ ...state.remoteBranches
5357
+ ].join("\n") : "";
5358
+ useEffect(() => {
5359
+ const current = ++ticket.current;
5360
+ if (!repository) {
5361
+ setHistory({
5362
+ commits: [],
5363
+ cursor: null,
5364
+ loading: false,
5365
+ failed: false
5366
+ });
5367
+ return;
4977
5368
  }
4978
- }, labelsFor(locale).menu));
5369
+ setHistory((previous) => _objectSpread2$1(_objectSpread2$1({}, previous), {}, {
5370
+ cursor: null,
5371
+ loading: true,
5372
+ failed: false
5373
+ }));
5374
+ repository.history().then((page) => {
5375
+ if (ticket.current === current) setHistory({
5376
+ commits: page.commits,
5377
+ cursor: page.cursor,
5378
+ loading: false,
5379
+ failed: false
5380
+ });
5381
+ }, () => {
5382
+ if (ticket.current === current) setHistory({
5383
+ commits: [],
5384
+ cursor: null,
5385
+ loading: false,
5386
+ failed: true
5387
+ });
5388
+ });
5389
+ }, [repository, refs]);
5390
+ const showMore = () => {
5391
+ if (!repository || !history.cursor || history.loading) return;
5392
+ const current = ticket.current;
5393
+ setHistory((previous) => _objectSpread2$1(_objectSpread2$1({}, previous), {}, { loading: true }));
5394
+ repository.history({ after: history.cursor }).then((page) => {
5395
+ if (ticket.current === current) setHistory((previous) => ({
5396
+ commits: [...previous.commits, ...page.commits],
5397
+ cursor: page.cursor,
5398
+ loading: false,
5399
+ failed: false
5400
+ }));
5401
+ }, () => {
5402
+ if (ticket.current === current) setHistory((previous) => _objectSpread2$1(_objectSpread2$1({}, previous), {}, {
5403
+ cursor: null,
5404
+ loading: false,
5405
+ failed: true
5406
+ }));
5407
+ });
5408
+ };
5409
+ const { report, element: snackbar } = useBranchSnackbar(locale, !!(state === null || state === void 0 ? void 0 : state.project), {
5410
+ open: () => controller === null || controller === void 0 ? void 0 : controller.open(locale),
5411
+ reload: () => controller === null || controller === void 0 ? void 0 : controller.reload()
5412
+ }, () => {
5413
+ var _root$current;
5414
+ return (_root$current = root.current) === null || _root$current === void 0 || (_root$current = _root$current.querySelector("[tabindex=\"0\"]")) === null || _root$current === void 0 ? void 0 : _root$current.focus({ preventScroll: true });
5415
+ });
5416
+ if (!controller || !state) return null;
5417
+ if (!repository) return /* @__PURE__ */ React.createElement("div", {
5418
+ className: "twgit-history-view",
5419
+ ref: root
5420
+ }, /* @__PURE__ */ React.createElement("p", null, labels.historyViewEmpty), /* @__PURE__ */ React.createElement("button", {
5421
+ type: "button",
5422
+ onClick: () => void controller.choose(locale)
5423
+ }, labels.choose));
5424
+ const change = async (operation) => {
5425
+ const result = await controller.changeBranch(operation);
5426
+ report(result, operation.kind === "switchBranch" || !!operation.checkout);
5427
+ return result;
5428
+ };
5429
+ return /* @__PURE__ */ React.createElement("div", {
5430
+ className: "twgit-history-view",
5431
+ ref: root
5432
+ }, /* @__PURE__ */ React.createElement(HistoryGraph, _objectSpread2$1({
5433
+ commits: history.commits,
5434
+ selected,
5435
+ locale,
5436
+ busy: state.busy,
5437
+ onSelect: setSelected,
5438
+ hasMore: !!history.cursor,
5439
+ loadingMore: history.loading,
5440
+ onShowMore: showMore,
5441
+ localBranches: state.branches
5442
+ }, state.writable ? {
5443
+ onCreateBranch: async (oid, name, checkout) => {
5444
+ const result = await controller.changeBranch({
5445
+ kind: "createBranch",
5446
+ name,
5447
+ at: oid,
5448
+ checkout
5449
+ });
5450
+ if (result.done) report(result, checkout);
5451
+ return result;
5452
+ },
5453
+ onSwitchBranch: (name) => change({
5454
+ kind: "switchBranch",
5455
+ name
5456
+ }),
5457
+ onCreateFromRemote: (name) => change({
5458
+ kind: "createBranch",
5459
+ name,
5460
+ fromRemote: true,
5461
+ checkout: true
5462
+ })
5463
+ } : {})), history.loading && !history.commits.length && /* @__PURE__ */ React.createElement("p", { role: "status" }, labels.historyLoading), history.failed && /* @__PURE__ */ React.createElement("p", null, labels.historyFailed), snackbar);
5464
+ }
5465
+ /**
5466
+ * Renders the history graph of the repository chosen in the panel mounted for `vm`, for a side panel of the host. It
5467
+ * follows branch changes made anywhere, offers the graph's branch actions when the panel allows writes and tells their
5468
+ * results in the branch button's snackbar. Before a repository is chosen it offers to choose one; it renders nothing
5469
+ * while no panel is mounted for `vm`.
5470
+ */
5471
+ function renderHistoryView(createElement, { vm, locale = "en" }) {
5472
+ return createElement(HistoryEntry, {
5473
+ vm,
5474
+ locale
5475
+ });
5476
+ }
5477
+ /**
5478
+ * Follows the Git status of the files under `directory`, as the panel mounted for `vm` last read it, and calls
5479
+ * `listener` with their marks by path relative to `directory`. The marks are empty while no repository is chosen or
5480
+ * `directory` is outside it. Returns the function that stops following.
5481
+ */
5482
+ function followChangeMarks(vm, directory, listener) {
5483
+ let stopped = false;
5484
+ let read;
5485
+ let unfollow = () => {};
5486
+ const update = async (controller) => {
5487
+ const { repository, snapshot } = controller !== null && controller !== void 0 ? controller : {
5488
+ repository: null,
5489
+ snapshot: null
5490
+ };
5491
+ if (snapshot === read) return;
5492
+ read = snapshot;
5493
+ const path = repository && snapshot ? await repository.root.resolve(directory).catch(() => null) : null;
5494
+ if (!stopped && read === snapshot) listener(path ? changeMarks(snapshot.changes, path.join("/")) : /* @__PURE__ */ new Map());
5495
+ };
5496
+ const unregister = findController(vm, (controller) => {
5497
+ unfollow();
5498
+ unfollow = controller ? controller.follow(() => void update(controller)) : () => {};
5499
+ update(controller);
5500
+ });
5501
+ return () => {
5502
+ stopped = true;
5503
+ unregister();
5504
+ unfollow();
5505
+ };
4979
5506
  }
4980
- export { GitPanelError, HistoryGraph, createEditorBlockLocator, createReadOnlyFs, createScratchBlocksDiffLocator, createScratchBlocksDiffRenderer, laneColors, layoutHistory, mountGitPanel, openProjectComparison, openProjectSourceTabs, openRepository, pullRequestBranch, pullRequestRef, renderGitMenu };
5507
+ export { BranchButton, GitPanelError, HistoryGraph, changeMarks, createEditorBlockLocator, createReadOnlyFs, createScratchBlocksDiffLocator, createScratchBlocksDiffRenderer, followChangeMarks, laneColors, layoutHistory, mountGitPanel, openProjectComparison, openProjectSourceTabs, openRepository, pullRequestBranch, pullRequestRef, renderBranchButton, renderHistoryView };