@kubohiroya/turbowarp-plus-editor-git 0.9.0 → 0.10.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,6 +1,6 @@
1
1
  import { a as gitHubRepositoryFromOrigin, c as projectPropertyLabel, g as useCostumeImage, h as useAssetImage, i as createGitHubPullRequests, l as ProjectSourceComparison, r as PullRequestError, s as organizeProjectChanges, t as createScratchBlocksDiffRenderer, u as ProjectSourceReview, y as parseProjectSource } from "./scratch-block-diff-renderer-DZR8ep-T.js";
2
2
  import { n as init_objectSpread2, r as __commonJSMin, t as _objectSpread2$1 } from "./objectSpread2-Dkd68nJH.js";
3
- import { n as GitPanelError, r as createReadOnlyFs, t as openRepository } from "./repository-VBp-UNWc.js";
3
+ import { i as createReadOnlyFs, r as GitPanelError, t as openRepository } from "./repository-CtvWYPQ8.js";
4
4
  import * as m from "react";
5
5
  import React, { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
6
6
  import { render, unmountComponentAtNode } from "react-dom";
@@ -2689,7 +2689,7 @@ function FileDiffView({ change, diff, labels, locale, renderBlock, loadAsset })
2689
2689
  const [view, setView] = useState("blocks");
2690
2690
  const names = locale.startsWith("ja") ? ja$3 : en$3;
2691
2691
  const parsed = (diff === null || diff === void 0 ? void 0 : diff.patch) ? parseDiff(diff.patch)[0] : null;
2692
- const text = /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("p", null, labels.mode, " · ", ((_change$oldMode = change.oldMode) === null || _change$oldMode === void 0 ? void 0 : _change$oldMode.toString(8)) || "—", " → ", ((_change$newMode = change.newMode) === null || _change$newMode === void 0 ? void 0 : _change$newMode.toString(8)) || "—"), diff && /* @__PURE__ */ React.createElement("p", null, diff.beforeSize, " → ", diff.afterSize, " ", labels.bytes), (diff === null || diff === void 0 ? void 0 : diff.binary) && /* @__PURE__ */ React.createElement("p", null, labels.binary), (diff === null || diff === void 0 ? void 0 : diff.tooLarge) && /* @__PURE__ */ React.createElement("p", null, labels.large), parsed && /* @__PURE__ */ React.createElement(index, {
2692
+ const text = /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("details", { className: "twgit-file-details" }, /* @__PURE__ */ React.createElement("summary", null, labels.details), /* @__PURE__ */ React.createElement("p", null, labels.mode, " · ", ((_change$oldMode = change.oldMode) === null || _change$oldMode === void 0 ? void 0 : _change$oldMode.toString(8)) || "—", " → ", ((_change$newMode = change.newMode) === null || _change$newMode === void 0 ? void 0 : _change$newMode.toString(8)) || "—"), diff && /* @__PURE__ */ React.createElement("p", null, diff.beforeSize, " → ", diff.afterSize, " ", labels.bytes)), (diff === null || diff === void 0 ? void 0 : diff.binary) && /* @__PURE__ */ React.createElement("p", null, labels.binary), (diff === null || diff === void 0 ? void 0 : diff.tooLarge) && /* @__PURE__ */ React.createElement("p", null, labels.large), parsed && /* @__PURE__ */ React.createElement(index, {
2693
2693
  viewType: "unified",
2694
2694
  diffType: parsed.type,
2695
2695
  hunks: parsed.hunks
@@ -2746,10 +2746,10 @@ var en$2 = {
2746
2746
  file: "File name",
2747
2747
  noImage: "No image",
2748
2748
  groups: {
2749
- staged: "Staged",
2750
- unstaged: "Unstaged",
2751
- untracked: "Untracked",
2752
- commit: "Commit"
2749
+ staged: "in the next record",
2750
+ unstaged: "not yet recorded",
2751
+ untracked: "new file",
2752
+ commit: "in this record"
2753
2753
  }
2754
2754
  };
2755
2755
  var ja$2 = {
@@ -2780,10 +2780,10 @@ var ja$2 = {
2780
2780
  file: "ファイル名",
2781
2781
  noImage: "画像なし",
2782
2782
  groups: {
2783
- staged: "ステージ済み",
2784
- unstaged: "未ステージ",
2785
- untracked: "未追跡",
2786
- commit: "コミット"
2783
+ staged: "次の記録に入る",
2784
+ unstaged: "まだ記録していない",
2785
+ untracked: "新しいファイル",
2786
+ commit: "この記録"
2787
2787
  }
2788
2788
  };
2789
2789
  var projectChangeLabels = (locale) => locale.startsWith("ja") ? ja$2 : en$2;
@@ -3039,15 +3039,15 @@ var messages = {
3039
3039
  replace: "Replace the open project and discard its unsaved changes?",
3040
3040
  dirty: "The Editor has unsaved changes.",
3041
3041
  saved: "The Editor has no unsaved changes.",
3042
- staged: "Staged",
3043
- unstaged: "Unstaged",
3044
- untracked: "Untracked",
3045
- commit: "Commit changes",
3046
- history: "History",
3047
- worktree: "Working tree",
3048
- clean: "No changes in the selected folder.",
3042
+ staged: "Changes in the next record",
3043
+ unstaged: "Saved, not yet recorded",
3044
+ untracked: "New files never recorded",
3045
+ commit: "Changes in this record",
3046
+ history: "Records (commits)",
3047
+ worktree: "Saved files",
3048
+ clean: "No saved changes in the selected folder.",
3049
3049
  branches: "Local branches",
3050
- unborn: "No commits yet",
3050
+ unborn: "No records yet",
3051
3051
  detached: "Detached HEAD",
3052
3052
  select: "Select a file to view its diff.",
3053
3053
  binary: "Binary asset",
@@ -3061,16 +3061,33 @@ var messages = {
3061
3061
  changed: "Files or Git state changed while reading. Refresh to read the current state.",
3062
3062
  outdated: "The repository changed outside the Editor. Refresh to read the current state.",
3063
3063
  unsupported: "This repository contains an entry the browser cannot represent.",
3064
- pullUnsafe: "Stop the project and clear unsaved or local Git changes before pulling.",
3065
- diverged: "Pull requires a fast-forward. Your local branch and files are intact.",
3064
+ pullUnsafe: "Stop the project, and save or undo unsaved edits and unrecorded changes, before getting changes from GitHub (pull).",
3065
+ 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.",
3066
3066
  session: "Reconnect GitHub for this repository and requested permission.",
3067
- pushUnknown: "The push result is unknown. Fetch and check the remote branch before retrying.",
3068
- pushRefresh: "The push succeeded. Fetch to refresh the local tracking branch.",
3069
- rollbackFailed: "Git could not restore all local changes. Check the working tree and Git status.",
3070
- remoteFailed: "The Git operation failed. Check the selected connection and retry.",
3071
- remoteDone: "Git operation completed.",
3072
- pulledProject: "Pull completed. Use Reload Project to open the updated files.",
3073
- confirmPush: "Push the current branch to its GitHub origin?",
3067
+ pushUnknown: "It is unknown whether GitHub received the records. Check GitHub (fetch) before sending again.",
3068
+ pushRefresh: "GitHub received the records. Check GitHub (fetch) to update the status.",
3069
+ rollbackFailed: "Some local changes could not be restored. Check the files and the Git details.",
3070
+ remoteFailed: "Could not complete the GitHub operation. Check the connection and try again.",
3071
+ remoteDone: "The GitHub operation finished.",
3072
+ pulledProject: "Got the changes from GitHub. Use Reload Project to open the updated files.",
3073
+ confirmPush: "Send the records on this branch to GitHub (push)?",
3074
+ details: "Git details",
3075
+ branch: "Branch",
3076
+ head: "HEAD",
3077
+ groups: "Git groups",
3078
+ added: "Added",
3079
+ modified: "Changed",
3080
+ deleted: "Deleted",
3081
+ gitStaged: "Staged",
3082
+ gitUnstaged: "Unstaged",
3083
+ gitUntracked: "Untracked",
3084
+ upstreamSame: "Same as GitHub",
3085
+ upstreamAhead: "Ahead of GitHub",
3086
+ upstreamBehind: "Behind GitHub",
3087
+ upstreamDiverged: "Diverged from GitHub",
3088
+ upstreamUnknown: "Not checked with GitHub yet",
3089
+ records: "records",
3090
+ checkedAt: "as of the last check",
3074
3091
  failed: "Could not read the repository. Check folder access and refresh."
3075
3092
  },
3076
3093
  ja: {
@@ -3090,18 +3107,18 @@ var messages = {
3090
3107
  reload: "作品を再読込",
3091
3108
  up: "上のフォルダ",
3092
3109
  browse: "リポジトリ内の作品を選択",
3093
- replace: "未保存の変更を破棄して、別の作品を開きますか?",
3094
- dirty: "Editor に未保存の変更があります。",
3095
- saved: "Editor に未保存の変更はありません。",
3096
- staged: "ステージ済み",
3097
- unstaged: "未ステージ",
3098
- untracked: "未追跡",
3099
- commit: "コミットの変更",
3100
- history: "履歴",
3101
- worktree: "作業ツリー",
3102
- clean: "対象フォルダに変更はありません。",
3110
+ replace: "保存していない編集を破棄して、別の作品を開きますか?",
3111
+ dirty: "Editor に保存していない編集があります。",
3112
+ saved: "Editor に保存していない編集はありません。",
3113
+ staged: "次の記録に入る変更",
3114
+ unstaged: "保存したが、まだ記録していない変更",
3115
+ untracked: "まだ記録したことのない新しいファイル",
3116
+ commit: "この記録の変更",
3117
+ history: "記録(コミット)の履歴",
3118
+ worktree: "保存したファイル",
3119
+ clean: "対象フォルダに、保存した変更はありません。",
3103
3120
  branches: "ローカルブランチ",
3104
- unborn: "コミットはまだありません",
3121
+ unborn: "まだ記録がありません",
3105
3122
  detached: "ブランチ未選択の HEAD",
3106
3123
  select: "ファイルを選ぶと差分を表示します。",
3107
3124
  binary: "バイナリアセット",
@@ -3115,16 +3132,33 @@ var messages = {
3115
3132
  changed: "読込中にファイルまたは Git の状態が変わりました。再取得してください。",
3116
3133
  outdated: "Editor の外でリポジトリが変更されました。再取得すると最新の状態を読み込みます。",
3117
3134
  unsupported: "ブラウザーで扱えない種類の Git エントリーがあります。",
3118
- pullUnsafe: "pull の前に実行を止め、未保存編集とローカルの Git 変更を解消してください。",
3119
- diverged: "fast-forward できません。ローカルブランチとファイルは保持されています。",
3135
+ pullUnsafe: "GitHub から取り込む前に、実行を止め、保存していない編集とまだ記録していない変更をなくしてください。",
3136
+ diverged: "このブランチと GitHub の両方に新しい記録があるため、ここでは取り込めません(pull は fast-forward のみ)。ブランチとファイルはそのままです。",
3120
3137
  session: "このリポジトリと操作に必要な権限で GitHub に接続してください。",
3121
- pushUnknown: "push の結果が不明です。再試行する前に fetch でリモートブランチを確認してください。",
3122
- pushRefresh: "push は成功しました。fetch でローカルの追跡ブランチを更新してください。",
3123
- rollbackFailed: "Git がローカルの変更をすべて復元できませんでした。作業ツリーと Git の状態を確認してください。",
3124
- remoteFailed: "Git 操作に失敗しました。選択中の接続を確認して再試行してください。",
3125
- remoteDone: "Git 操作が完了しました。",
3126
- pulledProject: "pull が完了しました。「作品を再読込」で更新済みのファイルを開けます。",
3127
- confirmPush: "現在のブランチを GitHub の origin に push しますか?",
3138
+ pushUnknown: "GitHub が記録を受け取ったか分かりません。送り直す前に「GitHub と確かめる」を使ってください。",
3139
+ pushRefresh: "GitHub が記録を受け取りました。「GitHub と確かめる」で状態を更新してください。",
3140
+ rollbackFailed: "一部のローカルの変更を元に戻せませんでした。ファイルと Git の詳細を確認してください。",
3141
+ remoteFailed: "GitHub との操作を完了できませんでした。接続を確認して、もう一度試してください。",
3142
+ remoteDone: "GitHub との操作が終わりました。",
3143
+ pulledProject: "GitHub から取り込みました。「作品を再読込」で更新したファイルを開けます。",
3144
+ confirmPush: "このブランチの記録を GitHub に送りますか(push)?",
3145
+ details: "Git の詳細",
3146
+ branch: "ブランチ",
3147
+ head: "HEAD",
3148
+ groups: "Git の区分",
3149
+ added: "追加",
3150
+ modified: "変更",
3151
+ deleted: "削除",
3152
+ gitStaged: "ステージ済み (staged)",
3153
+ gitUnstaged: "未ステージ (unstaged)",
3154
+ gitUntracked: "未追跡 (untracked)",
3155
+ upstreamSame: "GitHub と同じ",
3156
+ upstreamAhead: "GitHub より進んでいる",
3157
+ upstreamBehind: "GitHub より遅れている",
3158
+ upstreamDiverged: "GitHub と分かれている",
3159
+ upstreamUnknown: "まだ GitHub と確かめていない",
3160
+ records: "件",
3161
+ checkedAt: "最後に確かめた時点",
3128
3162
  failed: "リポジトリを読み込めませんでした。フォルダへのアクセスを確認し、再取得してください。"
3129
3163
  }
3130
3164
  };
@@ -3151,10 +3185,10 @@ var en$1 = {
3151
3185
  changed: "Switching disconnects the current session.",
3152
3186
  error: "GitHub connection failed. Retry using the selected connection.",
3153
3187
  invalid: "The selected repository needs a GitHub HTTPS origin.",
3154
- allowPush: "Authorize push",
3155
- fetch: "Fetch",
3156
- pull: "Pull (fast-forward only)",
3157
- push: "Push"
3188
+ allowPush: "Allow sending to GitHub (push)",
3189
+ fetch: "Check GitHub (fetch)",
3190
+ pull: "Get from GitHub (pull)",
3191
+ push: "Send to GitHub (push)"
3158
3192
  };
3159
3193
  var ja$1 = {
3160
3194
  title: "GitHub 接続",
@@ -3177,10 +3211,10 @@ var ja$1 = {
3177
3211
  changed: "方式を切り替えると現在のセッションを切断します。",
3178
3212
  error: "GitHub 接続に失敗しました。選択中の方式で再試行してください。",
3179
3213
  invalid: "選択したリポジトリには GitHub の HTTPS リモート origin が必要です。",
3180
- allowPush: "push を許可する",
3181
- fetch: "Fetch",
3182
- pull: "Pull(fast-forward のみ)",
3183
- push: "Push"
3214
+ allowPush: "GitHub に送ることを許可する(push)",
3215
+ fetch: "GitHub と確かめる(fetch)",
3216
+ pull: "GitHub から取り込む(pull)",
3217
+ push: "GitHub に送る(push)"
3184
3218
  };
3185
3219
  function ConnectionSettings({ settings, locale, readOrigin, runOperation }) {
3186
3220
  const labels = locale.startsWith("ja") ? ja$1 : en$1;
@@ -3216,7 +3250,16 @@ function ConnectionSettings({ settings, locale, readOrigin, runOperation }) {
3216
3250
  if (!remote) throw new Error("INVALID_REMOTE");
3217
3251
  await settings.connect(remote, allowPush);
3218
3252
  });
3219
- return /* @__PURE__ */ React.createElement("details", { className: "twgit-connection" }, /* @__PURE__ */ React.createElement("summary", null, labels.title), !settings.modes.length ? /* @__PURE__ */ React.createElement("p", null, labels.unavailable) : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("label", null, labels.mode, " ", /* @__PURE__ */ React.createElement("select", {
3253
+ const connected = !!mode && state.status === "connected";
3254
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, runOperation && connected && /* @__PURE__ */ React.createElement("span", { className: "twgit-sync" }, [
3255
+ "fetch",
3256
+ "pull",
3257
+ "push"
3258
+ ].map((operation) => /* @__PURE__ */ React.createElement("button", {
3259
+ key: operation,
3260
+ disabled: busy || !readOrigin || operation === "push" && !write,
3261
+ onClick: () => void run(() => runOperation(operation))
3262
+ }, labels[operation]))), /* @__PURE__ */ React.createElement("details", { className: "twgit-connection" }, /* @__PURE__ */ React.createElement("summary", null, labels.title), /* @__PURE__ */ React.createElement("div", { className: "twgit-connection-body" }, !settings.modes.length ? /* @__PURE__ */ React.createElement("p", null, labels.unavailable) : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("label", null, labels.mode, " ", /* @__PURE__ */ React.createElement("select", {
3220
3263
  value: mode !== null && mode !== void 0 ? mode : "",
3221
3264
  disabled: busy,
3222
3265
  onChange: (event) => {
@@ -3250,18 +3293,10 @@ function ConnectionSettings({ settings, locale, readOrigin, runOperation }) {
3250
3293
  }, labels.check), /* @__PURE__ */ React.createElement("button", {
3251
3294
  disabled: busy,
3252
3295
  onClick: () => void run(settings.disconnect)
3253
- }, labels.disconnect), runOperation && state.status === "connected" && [
3254
- "fetch",
3255
- "pull",
3256
- "push"
3257
- ].map((operation) => /* @__PURE__ */ React.createElement("button", {
3258
- key: operation,
3259
- disabled: busy || !readOrigin || operation === "push" && !write,
3260
- onClick: () => void run(() => runOperation(operation))
3261
- }, labels[operation]))), error && /* @__PURE__ */ React.createElement("p", {
3296
+ }, labels.disconnect))))), error && /* @__PURE__ */ React.createElement("p", {
3262
3297
  className: "twgit-error",
3263
3298
  role: "alert"
3264
- }, labels[error])));
3299
+ }, labels[error]));
3265
3300
  }
3266
3301
  // src/pull-request-panel.tsx
3267
3302
  var en = {
@@ -3456,7 +3491,7 @@ var initial = {
3456
3491
  var controllers = /* @__PURE__ */ new WeakMap();
3457
3492
  var watchInterval = 2e3;
3458
3493
  function GitPanel({ document, host, controller }) {
3459
- var _state$repository, _state$snapshot, _state$snapshot2, _state$snapshot3, _state$snapshot4, _state$snapshot5;
3494
+ var _state$repository, _state$snapshot$head$, _state$snapshot$head, _state$snapshot;
3460
3495
  const [visible, setVisible] = useState(false);
3461
3496
  const [review, setReview] = useState(false);
3462
3497
  const [locale, setLocale] = useState(document.documentElement.lang || "en");
@@ -3789,7 +3824,10 @@ function GitPanel({ document, host, controller }) {
3789
3824
  type: "button",
3790
3825
  disabled: state.busy,
3791
3826
  onClick: () => setReview(!review)
3792
- }, review ? labels.localChanges : labels.pullRequests), /* @__PURE__ */ React.createElement("button", { onClick: () => setVisible(false) }, labels.close)), /* @__PURE__ */ React.createElement("div", { className: "twgit-context" }, /* @__PURE__ */ React.createElement("span", null, labels.repository, " · ", ((_state$repository = state.repository) === null || _state$repository === void 0 ? void 0 : _state$repository.root.name) || "—"), /* @__PURE__ */ React.createElement("span", null, labels.project, " · ", state.project === null ? labels.none : state.project || labels.root), /* @__PURE__ */ React.createElement("strong", null, ((_state$snapshot = state.snapshot) === null || _state$snapshot === void 0 ? void 0 : _state$snapshot.branch) || (((_state$snapshot2 = state.snapshot) === null || _state$snapshot2 === void 0 ? void 0 : _state$snapshot2.head) ? labels.detached : labels.unborn)), /* @__PURE__ */ React.createElement("code", null, (_state$snapshot3 = state.snapshot) === null || _state$snapshot3 === void 0 || (_state$snapshot3 = _state$snapshot3.head) === null || _state$snapshot3 === void 0 ? void 0 : _state$snapshot3.slice(0, 12)), /* @__PURE__ */ React.createElement("span", { "data-git-dirty": "" }, dirty ? labels.dirty : labels.saved)), !review && host.connectionSettings && /* @__PURE__ */ React.createElement(ConnectionSettings, {
3827
+ }, review ? labels.localChanges : labels.pullRequests), /* @__PURE__ */ React.createElement("button", { onClick: () => setVisible(false) }, labels.close)), /* @__PURE__ */ React.createElement("div", { className: "twgit-context" }, /* @__PURE__ */ React.createElement("span", null, labels.repository, " · ", ((_state$repository = state.repository) === null || _state$repository === void 0 ? void 0 : _state$repository.root.name) || "—"), /* @__PURE__ */ React.createElement("span", null, labels.project, " · ", state.project === null ? labels.none : state.project || labels.root), /* @__PURE__ */ React.createElement("span", { "data-git-dirty": "" }, dirty ? labels.dirty : labels.saved), state.snapshot && /* @__PURE__ */ React.createElement("span", {
3828
+ "data-git-upstream": state.snapshot.upstream.state,
3829
+ title: labels.checkedAt
3830
+ }, upstreamText(state.snapshot, labels)), !review && host.connectionSettings && /* @__PURE__ */ React.createElement(ConnectionSettings, {
3793
3831
  settings: host.connectionSettings,
3794
3832
  locale,
3795
3833
  runOperation: host.connectionSettings.getSession ? remoteOperation : void 0,
@@ -3798,7 +3836,7 @@ function GitPanel({ document, host, controller }) {
3798
3836
  const remote = await state.repository.readOrigin();
3799
3837
  return epoch.current === ticket ? remote : void 0;
3800
3838
  } : void 0
3801
- }), state.error && /* @__PURE__ */ React.createElement("p", {
3839
+ }), state.snapshot && /* @__PURE__ */ React.createElement("details", { className: "twgit-git-details" }, /* @__PURE__ */ React.createElement("summary", null, labels.details), /* @__PURE__ */ React.createElement("div", { className: "twgit-connection-body" }, /* @__PURE__ */ React.createElement("p", null, labels.branch, " · ", /* @__PURE__ */ React.createElement("strong", null, state.snapshot.branch || (state.snapshot.head ? labels.detached : labels.unborn))), /* @__PURE__ */ React.createElement("p", null, labels.head, " · ", /* @__PURE__ */ React.createElement("code", null, (_state$snapshot$head$ = (_state$snapshot$head = state.snapshot.head) === null || _state$snapshot$head === void 0 ? void 0 : _state$snapshot$head.slice(0, 12)) !== null && _state$snapshot$head$ !== void 0 ? _state$snapshot$head$ : "—")), /* @__PURE__ */ React.createElement("p", null, labels.branches, " · ", state.snapshot.branches.join(", ") || "—"), /* @__PURE__ */ React.createElement("p", null, labels.groups, " · ", labels.gitStaged, " = ", labels.staged, " / ", labels.gitUnstaged, " = ", labels.unstaged, " / ", labels.gitUntracked, " = ", labels.untracked), /* @__PURE__ */ React.createElement("p", null, labels.permissions)))), state.error && /* @__PURE__ */ React.createElement("p", {
3802
3840
  className: "twgit-error",
3803
3841
  role: "alert"
3804
3842
  }, state.error), state.notice && /* @__PURE__ */ React.createElement("p", {
@@ -3828,7 +3866,7 @@ function GitPanel({ document, host, controller }) {
3828
3866
  disabled: state.busy,
3829
3867
  "aria-pressed": state.selected === change,
3830
3868
  onClick: () => openFile(change)
3831
- }, /* @__PURE__ */ React.createElement("span", null, !change.after ? "D" : !change.before ? "A" : "M"), " ", change.path))))));
3869
+ }, /* @__PURE__ */ React.createElement("small", { className: "twgit-change-kind" }, !change.after ? labels.deleted : !change.before ? labels.added : labels.modified), " ", change.path))))));
3832
3870
  return projectList ? /* @__PURE__ */ React.createElement(ProjectChangeListView, {
3833
3871
  changes: projectList,
3834
3872
  selected: selection ? element : null,
@@ -3840,12 +3878,12 @@ function GitPanel({ document, host, controller }) {
3840
3878
  fileListOpen,
3841
3879
  setFileListOpen
3842
3880
  }) : fileList;
3843
- })(), !state.changes.length && /* @__PURE__ */ React.createElement("p", null, labels.clean), /* @__PURE__ */ React.createElement("details", null, /* @__PURE__ */ React.createElement("summary", null, labels.branches), /* @__PURE__ */ React.createElement("ul", null, (_state$snapshot4 = state.snapshot) === null || _state$snapshot4 === void 0 ? void 0 : _state$snapshot4.branches.map((branch) => /* @__PURE__ */ React.createElement("li", { key: branch }, branch)))), /* @__PURE__ */ React.createElement("h3", null, labels.history), /* @__PURE__ */ React.createElement("ol", { className: "twgit-history" }, (_state$snapshot5 = state.snapshot) === null || _state$snapshot5 === void 0 ? void 0 : _state$snapshot5.commits.map((commit) => /* @__PURE__ */ React.createElement("li", { key: commit.oid }, /* @__PURE__ */ React.createElement("button", {
3881
+ })(), !state.changes.length && /* @__PURE__ */ React.createElement("p", null, labels.clean), /* @__PURE__ */ React.createElement("h3", null, labels.history), /* @__PURE__ */ React.createElement("ol", { className: "twgit-history" }, (_state$snapshot = state.snapshot) === null || _state$snapshot === void 0 ? void 0 : _state$snapshot.commits.map((commit) => /* @__PURE__ */ React.createElement("li", { key: commit.oid }, /* @__PURE__ */ React.createElement("button", {
3844
3882
  disabled: state.busy,
3845
3883
  "aria-pressed": state.commit === commit.oid,
3846
3884
  title: commit.oid,
3847
3885
  onClick: () => void selectCommit(commit.oid)
3848
- }, commit.message.split("\n")[0]), /* @__PURE__ */ React.createElement("small", null, /* @__PURE__ */ React.createElement("code", null, commit.oid.slice(0, 8)), " · ", commit.author, " · ", (/* @__PURE__ */ new Date(commit.timestamp * 1e3)).toLocaleString(locale)))))), /* @__PURE__ */ React.createElement("main", { className: "twgit-diff" }, selection && projectList ? /* @__PURE__ */ React.createElement(ProjectElementView, {
3886
+ }, commit.message.split("\n")[0]), /* @__PURE__ */ React.createElement("small", null, commit.author, " · ", (/* @__PURE__ */ new Date(commit.timestamp * 1e3)).toLocaleString(locale)))))), /* @__PURE__ */ React.createElement("main", { className: "twgit-diff" }, selection && projectList ? /* @__PURE__ */ React.createElement(ProjectElementView, {
3849
3887
  changes: projectList,
3850
3888
  selection,
3851
3889
  locale,
@@ -3858,7 +3896,17 @@ function GitPanel({ document, host, controller }) {
3858
3896
  locale,
3859
3897
  renderBlock: host.renderReviewBlock,
3860
3898
  loadAsset
3861
- }) : /* @__PURE__ */ React.createElement("p", null, labels.select))), /* @__PURE__ */ React.createElement("footer", null, labels.permissions)));
3899
+ }) : /* @__PURE__ */ React.createElement("p", null, labels.select)))));
3900
+ }
3901
+ /** The GitHub relation in plain words, with the number of records where it helps. */
3902
+ function upstreamText({ upstream }, labels) {
3903
+ switch (upstream.state) {
3904
+ case "same": return labels.upstreamSame;
3905
+ case "ahead": return `${labels.upstreamAhead} · ${upstream.ahead} ${labels.records}`;
3906
+ case "behind": return `${labels.upstreamBehind} · ${upstream.behind} ${labels.records}`;
3907
+ case "diverged": return `${labels.upstreamDiverged} · +${upstream.ahead} / −${upstream.behind}`;
3908
+ default: return labels.upstreamUnknown;
3909
+ }
3862
3910
  }
3863
3911
  function mountGitPanel({ vm, document, host }) {
3864
3912
  var _document$defaultView2;
@@ -51,6 +51,23 @@ export declare const messages: {
51
51
  remoteDone: string;
52
52
  pulledProject: string;
53
53
  confirmPush: string;
54
+ details: string;
55
+ branch: string;
56
+ head: string;
57
+ groups: string;
58
+ added: string;
59
+ modified: string;
60
+ deleted: string;
61
+ gitStaged: string;
62
+ gitUnstaged: string;
63
+ gitUntracked: string;
64
+ upstreamSame: string;
65
+ upstreamAhead: string;
66
+ upstreamBehind: string;
67
+ upstreamDiverged: string;
68
+ upstreamUnknown: string;
69
+ records: string;
70
+ checkedAt: string;
54
71
  failed: string;
55
72
  };
56
73
  ja: {
@@ -105,6 +122,23 @@ export declare const messages: {
105
122
  remoteDone: string;
106
123
  pulledProject: string;
107
124
  confirmPush: string;
125
+ details: string;
126
+ branch: string;
127
+ head: string;
128
+ groups: string;
129
+ added: string;
130
+ modified: string;
131
+ deleted: string;
132
+ gitStaged: string;
133
+ gitUnstaged: string;
134
+ gitUntracked: string;
135
+ upstreamSame: string;
136
+ upstreamAhead: string;
137
+ upstreamBehind: string;
138
+ upstreamDiverged: string;
139
+ upstreamUnknown: string;
140
+ records: string;
141
+ checkedAt: string;
108
142
  failed: string;
109
143
  };
110
144
  };
@@ -14562,6 +14562,37 @@ async function fetch({ fs, http, onProgress, onMessage, onAuth, onAuthSuccess, o
14562
14562
  }
14563
14563
  }
14564
14564
  /**
14565
+ * Find the merge base for a set of commits
14566
+ *
14567
+ * @param {object} args
14568
+ * @param {FsClient} args.fs - a file system client
14569
+ * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
14570
+ * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
14571
+ * @param {string[]} args.oids - Which commits
14572
+ * @param {object} [args.cache] - a [cache](cache.md) object
14573
+ *
14574
+ */
14575
+ async function findMergeBase({ fs, dir, gitdir = join(dir, ".git"), oids, cache = {} }) {
14576
+ try {
14577
+ assertParameter("fs", fs);
14578
+ assertParameter("gitdir", gitdir);
14579
+ assertParameter("oids", oids);
14580
+ const fsp = new FileSystem(fs);
14581
+ return await _findMergeBase({
14582
+ fs: fsp,
14583
+ cache,
14584
+ gitdir: await discoverGitdir({
14585
+ fsp,
14586
+ dotgit: gitdir
14587
+ }),
14588
+ oids
14589
+ });
14590
+ } catch (err) {
14591
+ err.caller = "git.findMergeBase";
14592
+ throw err;
14593
+ }
14594
+ }
14595
+ /**
14565
14596
  * Read an entry from the git config files.
14566
14597
  *
14567
14598
  * *Caveats:*
@@ -16800,6 +16831,9 @@ var emptyTree = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
16800
16831
  var textLimit = 1048576;
16801
16832
  var inScope = (path, scope) => !scope || path === scope || path.startsWith(scope + "/");
16802
16833
  var probeLimit = 5e3;
16834
+ /** Most commits read to count how far a branch is ahead of or behind its tracking branch. */
16835
+ var upstreamWalkLimit = 1e3;
16836
+ var trackingRef = (branchRef) => "refs/remotes/origin/" + branchRef.slice(11);
16803
16837
  var sameMetadata = (a, b) => a.size === b.size && [...a].every(([path, bytes]) => {
16804
16838
  var _b$get;
16805
16839
  return sameBytes(bytes, (_b$get = b.get(path)) !== null && _b$get !== void 0 ? _b$get : null);
@@ -16839,7 +16873,7 @@ async function openRepository(root) {
16839
16873
  if (text.startsWith("ref: refs/heads/")) {
16840
16874
  const ref = text.slice(5);
16841
16875
  pathParts(ref);
16842
- paths.push("/.git/" + ref);
16876
+ paths.push("/.git/" + ref, "/.git/" + trackingRef(ref));
16843
16877
  } else if (!/^[0-9a-f]{40}$/.test(text)) throw new GitPanelError("invalid");
16844
16878
  const result = /* @__PURE__ */ new Map();
16845
16879
  for (const path of paths) {
@@ -16885,7 +16919,7 @@ async function openRepository(root) {
16885
16919
  } catch (_unused) {
16886
16920
  return "locked";
16887
16921
  }
16888
- paths.push(".git/" + head.slice(5));
16922
+ paths.push(".git/" + head.slice(5), ".git/" + trackingRef(head.slice(5)));
16889
16923
  }
16890
16924
  const lines = [];
16891
16925
  for (const path of paths) {
@@ -16957,6 +16991,86 @@ async function openRepository(root) {
16957
16991
  mode
16958
16992
  }]));
16959
16993
  };
16994
+ /**
16995
+ * Compares `refs/heads/<branch>` with `refs/remotes/origin/<branch>` from local refs only. Counts by painting
16996
+ * commits from both tips in committer-time order until every queued commit is reachable from both, like
16997
+ * `git rev-list --left-right --count`, reading at most `upstreamWalkLimit` commits.
16998
+ */
16999
+ const upstreamRelation = async (branch, head) => {
17000
+ const unknown = {
17001
+ state: "unknown",
17002
+ ahead: 0,
17003
+ behind: 0
17004
+ };
17005
+ if (!branch || !head) return unknown;
17006
+ pathParts(trackingRef("refs/heads/" + branch));
17007
+ try {
17008
+ const remote = await resolveRef(_objectSpread2(_objectSpread2({}, options), {}, { ref: trackingRef("refs/heads/" + branch) }));
17009
+ if (remote === head) return {
17010
+ state: "same",
17011
+ ahead: 0,
17012
+ behind: 0
17013
+ };
17014
+ const bases = await findMergeBase(_objectSpread2(_objectSpread2({}, options), {}, {
17015
+ cache,
17016
+ oids: [head, remote]
17017
+ }));
17018
+ const hasAhead = !bases.includes(head), hasBehind = !bases.includes(remote);
17019
+ const local = 1, tracking = 2, both = 3;
17020
+ const flags = /* @__PURE__ */ new Map([[head, local], [remote, tracking]]);
17021
+ const time = async (oid) => (await readCommit(_objectSpread2(_objectSpread2({}, options), {}, {
17022
+ cache,
17023
+ oid
17024
+ }))).commit;
17025
+ const queue = [];
17026
+ const enqueue = async (oid) => {
17027
+ const commit = await time(oid);
17028
+ queue.push({
17029
+ oid,
17030
+ time: commit.committer.timestamp,
17031
+ parents: commit.parent
17032
+ });
17033
+ };
17034
+ await enqueue(head);
17035
+ await enqueue(remote);
17036
+ let read = 2;
17037
+ let complete = true;
17038
+ while (queue.some((item) => flags.get(item.oid) !== both)) {
17039
+ if (read >= 1e3) {
17040
+ complete = false;
17041
+ break;
17042
+ }
17043
+ queue.sort((a, b) => b.time - a.time);
17044
+ const item = queue.shift();
17045
+ const flag = flags.get(item.oid);
17046
+ for (const parent of item.parents) {
17047
+ var _flags$get;
17048
+ const previous = (_flags$get = flags.get(parent)) !== null && _flags$get !== void 0 ? _flags$get : 0;
17049
+ if ((previous | flag) === previous) continue;
17050
+ flags.set(parent, previous | flag);
17051
+ if (!queue.some((entry) => entry.oid === parent)) {
17052
+ await enqueue(parent);
17053
+ read++;
17054
+ }
17055
+ }
17056
+ }
17057
+ let ahead = 0, behind = 0;
17058
+ for (const flag of flags.values()) if (flag === local) ahead++;
17059
+ else if (flag === tracking) behind++;
17060
+ if (!complete) {
17061
+ ahead = hasAhead ? upstreamWalkLimit : 0;
17062
+ behind = hasBehind ? upstreamWalkLimit : 0;
17063
+ }
17064
+ return {
17065
+ state: hasAhead && hasBehind ? "diverged" : hasAhead ? "ahead" : hasBehind ? "behind" : "same",
17066
+ ahead: hasAhead ? Math.max(ahead, 1) : 0,
17067
+ behind: hasBehind ? Math.max(behind, 1) : 0
17068
+ };
17069
+ } catch (error) {
17070
+ if (errorCode(error) === "NotFoundError") return unknown;
17071
+ throw error;
17072
+ }
17073
+ };
16960
17074
  const assertSnapshot = async (snapshot) => {
16961
17075
  if (!sameMetadata(snapshot.metadata, await metadata())) throw new GitPanelError("changed");
16962
17076
  };
@@ -16985,6 +17099,7 @@ async function openRepository(root) {
16985
17099
  author: commit.author.name,
16986
17100
  timestamp: commit.author.timestamp
16987
17101
  })) : [];
17102
+ const upstream = await upstreamRelation(branch || void 0, head);
16988
17103
  const matrix = await statusMatrix(_objectSpread2(_objectSpread2({}, options), {}, {
16989
17104
  cache,
16990
17105
  refresh: false,
@@ -17033,7 +17148,8 @@ async function openRepository(root) {
17033
17148
  changes,
17034
17149
  scope,
17035
17150
  metadata: before,
17036
- probe: fingerprint
17151
+ probe: fingerprint,
17152
+ upstream
17037
17153
  };
17038
17154
  };
17039
17155
  const historyChanges = async (snapshot, oid) => {
@@ -17340,4 +17456,4 @@ async function openRepository(root) {
17340
17456
  projectAssetLoader: (snapshot, folder, comparison) => ({ version, md5ext, signal }) => !isAssetName(md5ext) || signal.aborted ? Promise.resolve(null) : serial(() => readFolderAsset(snapshot, pathParts(folder), () => comparisonSource(snapshot, comparison, version), md5ext, signal)).catch(() => null)
17341
17457
  };
17342
17458
  }
17343
- export { GitPanelError as n, createReadOnlyFs as r, openRepository as t };
17459
+ export { createReadOnlyFs as i, upstreamWalkLimit as n, GitPanelError as r, openRepository as t };
@@ -40,6 +40,21 @@ export interface Snapshot {
40
40
  metadata: Metadata;
41
41
  /** Size and time fingerprint taken before the read. Compare it with `probe(scope)` to see later changes. */
42
42
  probe: string;
43
+ /**
44
+ * How the current branch stands against `refs/remotes/origin/<branch>` as of the last fetch, from local refs only.
45
+ * `unknown` when there is no current branch or no tracking ref yet.
46
+ */
47
+ upstream: UpstreamRelation;
48
+ }
49
+ export interface UpstreamRelation {
50
+ state: 'same' | 'ahead' | 'behind' | 'diverged' | 'unknown';
51
+ /**
52
+ * Commits on the local branch that the tracking branch lacks, and the reverse. Zero when unknown. Counting reads at
53
+ * most `upstreamWalkLimit` (1000) commits; past that, a side that has commits of its own reports exactly that limit,
54
+ * meaning "this many or more". The state itself comes from the merge base and stays exact.
55
+ */
56
+ ahead: number;
57
+ behind: number;
43
58
  }
44
59
  export interface FileDiff {
45
60
  binary: boolean;
@@ -56,6 +71,8 @@ export interface FileDiff {
56
71
  afterSize: number;
57
72
  tooLarge: boolean;
58
73
  }
74
+ /** Most commits read to count how far a branch is ahead of or behind its tracking branch. */
75
+ export declare const upstreamWalkLimit = 1000;
59
76
  export declare function openRepository(root: FileSystemDirectoryHandle): Promise<{
60
77
  root: FileSystemDirectoryHandle;
61
78
  openProject: (path: string) => Promise<FileSystemDirectoryHandle>;
@@ -1 +1 @@
1
- export { n as GitPanelError, r as createReadOnlyFs, t as openRepository } from "./repository-VBp-UNWc.js";
1
+ export { r as GitPanelError, i as createReadOnlyFs, t as openRepository, n as upstreamWalkLimit } from "./repository-CtvWYPQ8.js";
package/dist/style.css CHANGED
@@ -116,14 +116,24 @@
116
116
  .twgit-panel button:focus-visible, .twgit-panel summary:focus-visible { outline: 2px solid Highlight; outline-offset: 2px; }
117
117
  .twgit-panel button[aria-pressed="true"] { color: HighlightText; background: Highlight; }
118
118
  .twgit-context { display: flex; gap: 8px 20px; flex-wrap: wrap; padding: 8px 16px; }
119
- .twgit-connection { margin: 0 16px 8px; max-height: 45%; overflow: auto; }
119
+ .twgit-context { align-items: center; }
120
+ .twgit-connection, .twgit-git-details { position: relative; }
121
+ .twgit-connection > summary, .twgit-git-details > summary { cursor: pointer; }
122
+ /* Settings and Git details open over the list, so the list and the diff stay where they are. */
123
+ .twgit-connection-body { position: absolute; z-index: 5; top: calc(100% + 4px); left: 0; width: min(440px, 80vw); max-height: 60vh;
124
+ overflow: auto; padding: 10px 12px; background: Canvas; color: CanvasText; border: 1px solid GrayText; border-radius: 6px;
125
+ box-shadow: 0 8px 24px #0004; }
126
+ .twgit-connection-body > p { margin: 4px 0; overflow-wrap: anywhere; }
127
+ .twgit-sync { display: inline-flex; flex-wrap: wrap; gap: 4px; }
128
+ .twgit-change-kind { display: inline-block; min-width: 2.6em; font-size: 11px; opacity: .8; }
129
+ .twgit-file-details { font-size: 12px; margin-bottom: 6px; }
130
+ .twgit-file-details > p { margin: 2px 0; }
120
131
  .twgit-content { display: grid; grid-template-columns: minmax(230px, 30%) minmax(0, 1fr); flex: 1; min-height: 0; }
121
132
  .twgit-panel aside { padding: 0 14px 14px; overflow: auto; border-right: 1px solid GrayText; }
122
133
  .twgit-panel aside ul { list-style: none; padding: 0; margin: 4px 0; }
123
134
  .twgit-panel aside li { overflow-wrap: anywhere; }
124
135
  .twgit-panel summary { cursor: pointer; }
125
136
  .twgit-panel .twgit-file { width: 100%; border: 0; margin: 1px 0; }
126
- .twgit-file span { display: inline-block; width: 1.4em; font-family: monospace; }
127
137
  .twgit-diff { padding: 0 16px 16px; min-width: 0; overflow: auto; }
128
138
  .twgit-diff .diff { font-size: 12px; }
129
139
  .twgit-history { list-style: none; margin: 0; padding: 0; }
@@ -152,4 +162,5 @@
152
162
  .twgit-element-values td { font-family: ui-monospace, monospace; }
153
163
  .twgit-element-files { margin-top: 12px; }
154
164
  .twgit-panel .twgit-element span.twgit-element-name { width: auto; font-family: inherit; }
165
+ .twgit-git-details > .twgit-connection-body { left: auto; right: 0; }
155
166
  /*$vite$:1*/
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubohiroya/turbowarp-plus-editor-git",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Read-only local Git panel for TurboWarp Plus Editor",
5
5
  "type": "module",
6
6
  "license": "MPL-2.0",
@@ -28,7 +28,7 @@ const en = {
28
28
  unavailable: 'No GitHub connection is configured.', changed: 'Switching disconnects the current session.',
29
29
  error: 'GitHub connection failed. Retry using the selected connection.',
30
30
  invalid: 'The selected repository needs a GitHub HTTPS origin.',
31
- allowPush: 'Authorize push', fetch: 'Fetch', pull: 'Pull (fast-forward only)', push: 'Push'
31
+ allowPush: 'Allow sending to GitHub (push)', fetch: 'Check GitHub (fetch)', pull: 'Get from GitHub (pull)', push: 'Send to GitHub (push)'
32
32
  };
33
33
  const ja: typeof en = {
34
34
  title: 'GitHub 接続', mode: '接続方式', select: '接続方式を選択',
@@ -39,7 +39,7 @@ const ja: typeof en = {
39
39
  unavailable: 'GitHub 接続先が設定されていません。', changed: '方式を切り替えると現在のセッションを切断します。',
40
40
  error: 'GitHub 接続に失敗しました。選択中の方式で再試行してください。',
41
41
  invalid: '選択したリポジトリには GitHub の HTTPS リモート origin が必要です。',
42
- allowPush: 'push を許可する', fetch: 'Fetch', pull: 'Pull(fast-forward のみ)', push: 'Push'
42
+ allowPush: 'GitHub に送ることを許可する(push)', fetch: 'GitHub と確かめる(fetch)', pull: 'GitHub から取り込む(pull)', push: 'GitHub に送る(push)'
43
43
  };
44
44
 
45
45
  export function ConnectionSettings({settings, locale, readOrigin, runOperation}: {
@@ -69,8 +69,15 @@ export function ConnectionSettings({settings, locale, readOrigin, runOperation}:
69
69
  if (!remote) throw new Error('INVALID_REMOTE');
70
70
  await settings.connect(remote, allowPush);
71
71
  });
72
- return <details className="twgit-connection">
72
+ const connected = !!mode && state.status === 'connected';
73
+ // Sync sits beside the GitHub status; the settings open over the list instead of pushing it down.
74
+ return <>
75
+ {runOperation && connected && <span className="twgit-sync">{(['fetch', 'pull', 'push'] as const).map(operation =>
76
+ <button key={operation} disabled={busy || !readOrigin || (operation === 'push' && !write)}
77
+ onClick={() => void run(() => runOperation(operation))}>{labels[operation]}</button>)}</span>}
78
+ <details className="twgit-connection">
73
79
  <summary>{labels.title}</summary>
80
+ <div className="twgit-connection-body">
74
81
  {!settings.modes.length ? <p>{labels.unavailable}</p> : <>
75
82
  <label>{labels.mode} <select value={mode ?? ''} disabled={busy}
76
83
  onChange={event => {const choice = event.currentTarget.value as Mode; void run(() => settings.select(choice));}}>
@@ -93,11 +100,10 @@ export function ConnectionSettings({settings, locale, readOrigin, runOperation}:
93
100
  {state.status !== 'disconnected' && <button disabled={busy}
94
101
  onClick={() => void run(settings.refresh)}>{labels.check}</button>}
95
102
  <button disabled={busy} onClick={() => void run(settings.disconnect)}>{labels.disconnect}</button>
96
- {runOperation && state.status === 'connected' && (['fetch', 'pull', 'push'] as const).map(operation =>
97
- <button key={operation} disabled={busy || !readOrigin || (operation === 'push' && !write)}
98
- onClick={() => void run(() => runOperation(operation))}>{labels[operation]}</button>)}
99
103
  </>}
100
- {error && <p className="twgit-error" role="alert">{labels[error]}</p>}
101
104
  </>}
102
- </details>;
105
+ </div>
106
+ </details>
107
+ {error && <p className="twgit-error" role="alert">{labels[error]}</p>}
108
+ </>;
103
109
  }
@@ -23,8 +23,10 @@ export function FileDiffView({change, diff, labels, locale, renderBlock, loadAss
23
23
  const names = locale.startsWith('ja') ? ja : en;
24
24
  const parsed = diff?.patch ? parseDiff(diff.patch)[0] : null;
25
25
  const text = <>
26
- <p>{labels.mode} · {change.oldMode?.toString(8) || '—'} → {change.newMode?.toString(8) || '—'}</p>
27
- {diff && <p>{diff.beforeSize} → {diff.afterSize} {labels.bytes}</p>}
26
+ <details className="twgit-file-details"><summary>{labels.details}</summary>
27
+ <p>{labels.mode} · {change.oldMode?.toString(8) || '—'} → {change.newMode?.toString(8) || '—'}</p>
28
+ {diff && <p>{diff.beforeSize} → {diff.afterSize} {labels.bytes}</p>}
29
+ </details>
28
30
  {diff?.binary && <p>{labels.binary}</p>}
29
31
  {diff?.tooLarge && <p>{labels.large}</p>}
30
32
  {parsed && <Diff viewType="unified" diffType={parsed.type} hunks={parsed.hunks}>
package/src/index.tsx CHANGED
@@ -287,17 +287,26 @@ function GitPanel({document, host, controller}: {document: Document; host: GitHo
287
287
  <div className="twgit-context">
288
288
  <span>{labels.repository} · {state.repository?.root.name || '—'}</span>
289
289
  <span>{labels.project} · {state.project === null ? labels.none : state.project || labels.root}</span>
290
- <strong>{state.snapshot?.branch || (state.snapshot?.head ? labels.detached : labels.unborn)}</strong>
291
- <code>{state.snapshot?.head?.slice(0, 12)}</code>
292
290
  <span data-git-dirty="">{dirty ? labels.dirty : labels.saved}</span>
291
+ {state.snapshot && <span data-git-upstream={state.snapshot.upstream.state} title={labels.checkedAt}>{upstreamText(state.snapshot, labels)}</span>}
292
+ {!review && host.connectionSettings && <ConnectionSettings settings={host.connectionSettings} locale={locale}
293
+ runOperation={host.connectionSettings.getSession ? remoteOperation : undefined}
294
+ readOrigin={state.repository && !state.busy ? async () => {
295
+ const ticket = epoch.current;
296
+ const remote = await state.repository!.readOrigin();
297
+ return epoch.current === ticket ? remote : undefined;
298
+ } : undefined}/>}
299
+ {state.snapshot && <details className="twgit-git-details">
300
+ <summary>{labels.details}</summary>
301
+ <div className="twgit-connection-body">
302
+ <p>{labels.branch} · <strong>{state.snapshot.branch || (state.snapshot.head ? labels.detached : labels.unborn)}</strong></p>
303
+ <p>{labels.head} · <code>{state.snapshot.head?.slice(0, 12) ?? '—'}</code></p>
304
+ <p>{labels.branches} · {state.snapshot.branches.join(', ') || '—'}</p>
305
+ <p>{labels.groups} · {labels.gitStaged} = {labels.staged} / {labels.gitUnstaged} = {labels.unstaged} / {labels.gitUntracked} = {labels.untracked}</p>
306
+ <p>{labels.permissions}</p>
307
+ </div>
308
+ </details>}
293
309
  </div>
294
- {!review && host.connectionSettings && <ConnectionSettings settings={host.connectionSettings} locale={locale}
295
- runOperation={host.connectionSettings.getSession ? remoteOperation : undefined}
296
- readOrigin={state.repository && !state.busy ? async () => {
297
- const ticket = epoch.current;
298
- const remote = await state.repository!.readOrigin();
299
- return epoch.current === ticket ? remote : undefined;
300
- } : undefined}/>}
301
310
  {state.error && <p className="twgit-error" role="alert">{state.error}</p>}
302
311
  {state.notice && <p className="twgit-context" role="status">{state.notice}</p>}
303
312
  {state.busy && <p role="status">{labels.loading}</p>}
@@ -323,7 +332,7 @@ function GitPanel({document, host, controller}: {document: Document; host: GitHo
323
332
  <ul>{state.changes.filter(change => change.group === group).map(change => <li key={change.path}>
324
333
  <button className="twgit-file" data-git-group={group} disabled={state.busy}
325
334
  aria-pressed={state.selected === change} onClick={() => openFile(change)}>
326
- <span>{!change.after ? 'D' : !change.before ? 'A' : 'M'}</span> {change.path}
335
+ <small className="twgit-change-kind">{!change.after ? labels.deleted : !change.before ? labels.added : labels.modified}</small> {change.path}
327
336
  </button>
328
337
  </li>)}</ul>
329
338
  </section>);
@@ -332,12 +341,11 @@ function GitPanel({document, host, controller}: {document: Document; host: GitHo
332
341
  fileListOpen={fileListOpen} setFileListOpen={setFileListOpen}/> : fileList;
333
342
  })()}
334
343
  {!state.changes.length && <p>{labels.clean}</p>}
335
- <details><summary>{labels.branches}</summary><ul>{state.snapshot?.branches.map(branch => <li key={branch}>{branch}</li>)}</ul></details>
336
344
  <h3>{labels.history}</h3>
337
345
  <ol className="twgit-history">{state.snapshot?.commits.map(commit => <li key={commit.oid}>
338
346
  <button disabled={state.busy} aria-pressed={state.commit === commit.oid} title={commit.oid}
339
347
  onClick={() => void selectCommit(commit.oid)}>{commit.message.split('\n')[0]}</button>
340
- <small><code>{commit.oid.slice(0, 8)}</code> · {commit.author} · {new Date(commit.timestamp * 1000).toLocaleString(locale)}</small>
348
+ <small>{commit.author} · {new Date(commit.timestamp * 1000).toLocaleString(locale)}</small>
341
349
  </li>)}</ol>
342
350
  </aside>
343
351
  <main className="twgit-diff">
@@ -347,11 +355,21 @@ function GitPanel({document, host, controller}: {document: Document; host: GitHo
347
355
  renderBlock={host.renderReviewBlock} loadAsset={loadAsset}/> : <p>{labels.select}</p>}
348
356
  </main>
349
357
  </div>}
350
- <footer>{labels.permissions}</footer>
351
358
  </div>
352
359
  </div>;
353
360
  }
354
361
 
362
+ /** The GitHub relation in plain words, with the number of records where it helps. */
363
+ function upstreamText({upstream}: Snapshot, labels: ReturnType<typeof labelsFor>) {
364
+ switch (upstream.state) {
365
+ case 'same': return labels.upstreamSame;
366
+ case 'ahead': return `${labels.upstreamAhead} · ${upstream.ahead} ${labels.records}`;
367
+ case 'behind': return `${labels.upstreamBehind} · ${upstream.behind} ${labels.records}`;
368
+ case 'diverged': return `${labels.upstreamDiverged} · +${upstream.ahead} / −${upstream.behind}`;
369
+ default: return labels.upstreamUnknown;
370
+ }
371
+ }
372
+
355
373
  export function mountGitPanel({vm, document, host}: {vm: object; document: Document; host: GitHost}) {
356
374
  if (!(document.defaultView as PickerWindow)?.showDirectoryPicker) return null;
357
375
  const root = document.createElement('div');
package/src/messages.ts CHANGED
@@ -7,9 +7,9 @@ export const messages = {
7
7
  open: 'Open This Project', reload: 'Reload Project', up: 'Parent folder', browse: 'Choose a project inside this repository',
8
8
  replace: 'Replace the open project and discard its unsaved changes?',
9
9
  dirty: 'The Editor has unsaved changes.', saved: 'The Editor has no unsaved changes.',
10
- staged: 'Staged', unstaged: 'Unstaged', untracked: 'Untracked', commit: 'Commit changes',
11
- history: 'History', worktree: 'Working tree', clean: 'No changes in the selected folder.',
12
- branches: 'Local branches', unborn: 'No commits yet', detached: 'Detached HEAD', select: 'Select a file to view its diff.',
10
+ staged: 'Changes in the next record', unstaged: 'Saved, not yet recorded', untracked: 'New files never recorded', commit: 'Changes in this record',
11
+ history: 'Records (commits)', worktree: 'Saved files', clean: 'No saved changes in the selected folder.',
12
+ branches: 'Local branches', unborn: 'No records yet', detached: 'Detached HEAD', select: 'Select a file to view its diff.',
13
13
  binary: 'Binary asset', large: 'Text preview exceeds its display limit.', bytes: 'bytes',
14
14
  mode: 'Git file mode', permissions: 'Working-tree executable permission changes are unavailable in the browser.',
15
15
  unchanged: 'The text is unchanged.', loading: 'Reading repository…',
@@ -17,16 +17,20 @@ export const messages = {
17
17
  changed: 'Files or Git state changed while reading. Refresh to read the current state.',
18
18
  outdated: 'The repository changed outside the Editor. Refresh to read the current state.',
19
19
  unsupported: 'This repository contains an entry the browser cannot represent.',
20
- pullUnsafe: 'Stop the project and clear unsaved or local Git changes before pulling.',
21
- diverged: 'Pull requires a fast-forward. Your local branch and files are intact.',
20
+ pullUnsafe: 'Stop the project, and save or undo unsaved edits and unrecorded changes, before getting changes from GitHub (pull).',
21
+ 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.',
22
22
  session: 'Reconnect GitHub for this repository and requested permission.',
23
- pushUnknown: 'The push result is unknown. Fetch and check the remote branch before retrying.',
24
- pushRefresh: 'The push succeeded. Fetch to refresh the local tracking branch.',
25
- rollbackFailed: 'Git could not restore all local changes. Check the working tree and Git status.',
26
- remoteFailed: 'The Git operation failed. Check the selected connection and retry.',
27
- remoteDone: 'Git operation completed.',
28
- pulledProject: 'Pull completed. Use Reload Project to open the updated files.',
29
- confirmPush: 'Push the current branch to its GitHub origin?',
23
+ pushUnknown: 'It is unknown whether GitHub received the records. Check GitHub (fetch) before sending again.',
24
+ pushRefresh: 'GitHub received the records. Check GitHub (fetch) to update the status.',
25
+ rollbackFailed: 'Some local changes could not be restored. Check the files and the Git details.',
26
+ remoteFailed: 'Could not complete the GitHub operation. Check the connection and try again.',
27
+ remoteDone: 'The GitHub operation finished.',
28
+ pulledProject: 'Got the changes from GitHub. Use Reload Project to open the updated files.',
29
+ confirmPush: 'Send the records on this branch to GitHub (push)?',
30
+ details: 'Git details', branch: 'Branch', head: 'HEAD', groups: 'Git groups', added: 'Added', modified: 'Changed', deleted: 'Deleted',
31
+ gitStaged: 'Staged', gitUnstaged: 'Unstaged', gitUntracked: 'Untracked',
32
+ upstreamSame: 'Same as GitHub', upstreamAhead: 'Ahead of GitHub', upstreamBehind: 'Behind GitHub', upstreamDiverged: 'Diverged from GitHub',
33
+ upstreamUnknown: 'Not checked with GitHub yet', records: 'records', checkedAt: 'as of the last check',
30
34
  failed: 'Could not read the repository. Check folder access and refresh.'
31
35
  },
32
36
  ja: {
@@ -35,11 +39,11 @@ export const messages = {
35
39
  empty: '既存の Git リポジトリを選び、ファイルの状態と履歴を確認できます。',
36
40
  repository: 'リポジトリ', project: '作品フォルダ', none: '作品は未選択', root: 'リポジトリルート',
37
41
  open: 'この作品を開く', reload: '作品を再読込', up: '上のフォルダ', browse: 'リポジトリ内の作品を選択',
38
- replace: '未保存の変更を破棄して、別の作品を開きますか?',
39
- dirty: 'Editor に未保存の変更があります。', saved: 'Editor に未保存の変更はありません。',
40
- staged: 'ステージ済み', unstaged: '未ステージ', untracked: '未追跡', commit: 'コミットの変更',
41
- history: '履歴', worktree: '作業ツリー', clean: '対象フォルダに変更はありません。',
42
- branches: 'ローカルブランチ', unborn: 'コミットはまだありません', detached: 'ブランチ未選択の HEAD', select: 'ファイルを選ぶと差分を表示します。',
42
+ replace: '保存していない編集を破棄して、別の作品を開きますか?',
43
+ dirty: 'Editor に保存していない編集があります。', saved: 'Editor に保存していない編集はありません。',
44
+ staged: '次の記録に入る変更', unstaged: '保存したが、まだ記録していない変更', untracked: 'まだ記録したことのない新しいファイル', commit: 'この記録の変更',
45
+ history: '記録(コミット)の履歴', worktree: '保存したファイル', clean: '対象フォルダに、保存した変更はありません。',
46
+ branches: 'ローカルブランチ', unborn: 'まだ記録がありません', detached: 'ブランチ未選択の HEAD', select: 'ファイルを選ぶと差分を表示します。',
43
47
  binary: 'バイナリアセット', large: 'テキスト差分の表示上限を超えています。', bytes: 'バイト',
44
48
  mode: 'Git のファイルモード', permissions: '作業ツリーの実行権限の変更はブラウザーから確認できません。',
45
49
  unchanged: 'テキストに変更はありません。', loading: 'リポジトリを読込中…',
@@ -47,16 +51,20 @@ export const messages = {
47
51
  changed: '読込中にファイルまたは Git の状態が変わりました。再取得してください。',
48
52
  outdated: 'Editor の外でリポジトリが変更されました。再取得すると最新の状態を読み込みます。',
49
53
  unsupported: 'ブラウザーで扱えない種類の Git エントリーがあります。',
50
- pullUnsafe: 'pull の前に実行を止め、未保存編集とローカルの Git 変更を解消してください。',
51
- diverged: 'fast-forward できません。ローカルブランチとファイルは保持されています。',
54
+ pullUnsafe: 'GitHub から取り込む前に、実行を止め、保存していない編集とまだ記録していない変更をなくしてください。',
55
+ diverged: 'このブランチと GitHub の両方に新しい記録があるため、ここでは取り込めません(pull は fast-forward のみ)。ブランチとファイルはそのままです。',
52
56
  session: 'このリポジトリと操作に必要な権限で GitHub に接続してください。',
53
- pushUnknown: 'push の結果が不明です。再試行する前に fetch でリモートブランチを確認してください。',
54
- pushRefresh: 'push は成功しました。fetch でローカルの追跡ブランチを更新してください。',
55
- rollbackFailed: 'Git がローカルの変更をすべて復元できませんでした。作業ツリーと Git の状態を確認してください。',
56
- remoteFailed: 'Git 操作に失敗しました。選択中の接続を確認して再試行してください。',
57
- remoteDone: 'Git 操作が完了しました。',
58
- pulledProject: 'pull が完了しました。「作品を再読込」で更新済みのファイルを開けます。',
59
- confirmPush: '現在のブランチを GitHub の origin に push しますか?',
57
+ pushUnknown: 'GitHub が記録を受け取ったか分かりません。送り直す前に「GitHub と確かめる」を使ってください。',
58
+ pushRefresh: 'GitHub が記録を受け取りました。「GitHub と確かめる」で状態を更新してください。',
59
+ rollbackFailed: '一部のローカルの変更を元に戻せませんでした。ファイルと Git の詳細を確認してください。',
60
+ remoteFailed: 'GitHub との操作を完了できませんでした。接続を確認して、もう一度試してください。',
61
+ remoteDone: 'GitHub との操作が終わりました。',
62
+ pulledProject: 'GitHub から取り込みました。「作品を再読込」で更新したファイルを開けます。',
63
+ confirmPush: 'このブランチの記録を GitHub に送りますか(push)?',
64
+ details: 'Git の詳細', branch: 'ブランチ', head: 'HEAD', groups: 'Git の区分', added: '追加', modified: '変更', deleted: '削除',
65
+ gitStaged: 'ステージ済み (staged)', gitUnstaged: '未ステージ (unstaged)', gitUntracked: '未追跡 (untracked)',
66
+ upstreamSame: 'GitHub と同じ', upstreamAhead: 'GitHub より進んでいる', upstreamBehind: 'GitHub より遅れている', upstreamDiverged: 'GitHub と分かれている',
67
+ upstreamUnknown: 'まだ GitHub と確かめていない', records: '件', checkedAt: '最後に確かめた時点',
60
68
  failed: 'リポジトリを読み込めませんでした。フォルダへのアクセスを確認し、再取得してください。'
61
69
  }
62
70
  };
@@ -15,7 +15,7 @@ const en = {
15
15
  files: 'Files and Git groups', before: 'Before', after: 'After', absent: 'Not in this version',
16
16
  loading: 'Reading the project changes…', unreadable: 'The project data could not be read, so the changes are listed by file.',
17
17
  fileList: 'List by file', property: 'Item', format: 'Format', rate: 'Sample rate', samples: 'Samples', file: 'File name',
18
- noImage: 'No image', groups: {staged: 'Staged', unstaged: 'Unstaged', untracked: 'Untracked', commit: 'Commit'} as Record<ChangeGroup, string>
18
+ noImage: 'No image', groups: {staged: 'in the next record', unstaged: 'not yet recorded', untracked: 'new file', commit: 'in this record'} as Record<ChangeGroup, string>
19
19
  };
20
20
  const ja: typeof en = {
21
21
  title: '作品の要素ごとの変更', stage: 'ステージ', code: count => `コード · ${count} か所`,
@@ -24,7 +24,7 @@ const ja: typeof en = {
24
24
  files: 'ファイルと Git の区分', before: '変更前', after: '変更後', absent: 'この版にはありません',
25
25
  loading: '作品の変更を読み込み中…', unreadable: '作品データを読み込めないため、ファイル単位で一覧にします。',
26
26
  fileList: 'ファイル単位の一覧', property: '項目', format: '形式', rate: 'サンプリング周波数', samples: 'サンプル数', file: 'ファイル名',
27
- noImage: '画像なし', groups: {staged: 'ステージ済み', unstaged: '未ステージ', untracked: '未追跡', commit: 'コミット'}
27
+ noImage: '画像なし', groups: {staged: '次の記録に入る', unstaged: 'まだ記録していない', untracked: '新しいファイル', commit: 'この記録'}
28
28
  };
29
29
  type Labels = typeof en;
30
30
  export const projectChangeLabels = (locale: string): Labels => locale.startsWith('ja') ? ja : en;
package/src/repository.ts CHANGED
@@ -45,6 +45,21 @@ export interface Snapshot {
45
45
  metadata: Metadata;
46
46
  /** Size and time fingerprint taken before the read. Compare it with `probe(scope)` to see later changes. */
47
47
  probe: string;
48
+ /**
49
+ * How the current branch stands against `refs/remotes/origin/<branch>` as of the last fetch, from local refs only.
50
+ * `unknown` when there is no current branch or no tracking ref yet.
51
+ */
52
+ upstream: UpstreamRelation;
53
+ }
54
+ export interface UpstreamRelation {
55
+ state: 'same' | 'ahead' | 'behind' | 'diverged' | 'unknown';
56
+ /**
57
+ * Commits on the local branch that the tracking branch lacks, and the reverse. Zero when unknown. Counting reads at
58
+ * most `upstreamWalkLimit` (1000) commits; past that, a side that has commits of its own reports exactly that limit,
59
+ * meaning "this many or more". The state itself comes from the merge base and stays exact.
60
+ */
61
+ ahead: number;
62
+ behind: number;
48
63
  }
49
64
  export interface FileDiff {
50
65
  binary: boolean;
@@ -63,6 +78,9 @@ const textLimit = 1024 * 1024;
63
78
  const inScope = (path: string, scope: string) => !scope || path === scope || path.startsWith(scope + '/');
64
79
  // Above this many work-tree files, probes only watch Git metadata. Edits then need a manual refresh.
65
80
  const probeLimit = 5000;
81
+ /** Most commits read to count how far a branch is ahead of or behind its tracking branch. */
82
+ export const upstreamWalkLimit = 1000;
83
+ const trackingRef = (branchRef: string) => 'refs/remotes/origin/' + branchRef.slice('refs/heads/'.length);
66
84
  const sameMetadata = (a: Metadata, b: Metadata) => a.size === b.size && [...a].every(([path, bytes]) => sameBytes(bytes, b.get(path) ?? null));
67
85
 
68
86
  export async function openRepository(root: FileSystemDirectoryHandle) {
@@ -89,7 +107,7 @@ export async function openRepository(root: FileSystemDirectoryHandle) {
89
107
  if (text.startsWith('ref: refs/heads/')) {
90
108
  const ref = text.slice(5);
91
109
  pathParts(ref);
92
- paths.push('/.git/' + ref);
110
+ paths.push('/.git/' + ref, '/.git/' + trackingRef(ref));
93
111
  } else if (!/^[0-9a-f]{40}$/.test(text)) throw new GitPanelError('invalid');
94
112
  const result: Metadata = new Map();
95
113
  for (const path of paths) {
@@ -124,7 +142,7 @@ export async function openRepository(root: FileSystemDirectoryHandle) {
124
142
  const paths = ['.git/HEAD', '.git/index', '.git/packed-refs', '.git/shallow', '.git/config'];
125
143
  if (head.startsWith('ref: refs/heads/')) {
126
144
  try {pathParts(head.slice(5));} catch {return 'locked';}
127
- paths.push('.git/' + head.slice(5));
145
+ paths.push('.git/' + head.slice(5), '.git/' + trackingRef(head.slice(5)));
128
146
  }
129
147
  const lines: string[] = [];
130
148
  for (const path of paths) {
@@ -169,6 +187,60 @@ export async function openRepository(root: FileSystemDirectoryHandle) {
169
187
  }) as {path: string; oid: string; mode: number}[];
170
188
  return new Map(result.map(({path, oid, mode}) => [path, {oid, mode}]));
171
189
  };
190
+ /**
191
+ * Compares `refs/heads/<branch>` with `refs/remotes/origin/<branch>` from local refs only. Counts by painting
192
+ * commits from both tips in committer-time order until every queued commit is reachable from both, like
193
+ * `git rev-list --left-right --count`, reading at most `upstreamWalkLimit` commits.
194
+ */
195
+ const upstreamRelation = async (branch: string | undefined, head: string | null): Promise<UpstreamRelation> => {
196
+ const unknown: UpstreamRelation = {state: 'unknown', ahead: 0, behind: 0};
197
+ if (!branch || !head) return unknown;
198
+ pathParts(trackingRef('refs/heads/' + branch));
199
+ try {
200
+ const remote = await git.resolveRef({...options, ref: trackingRef('refs/heads/' + branch)});
201
+ if (remote === head) return {state: 'same', ahead: 0, behind: 0};
202
+ const bases = await git.findMergeBase({...options, cache, oids: [head, remote]}) as string[];
203
+ const hasAhead = !bases.includes(head), hasBehind = !bases.includes(remote);
204
+ const local = 1, tracking = 2, both = 3;
205
+ const flags = new Map<string, number>([[head, local], [remote, tracking]]);
206
+ const time = async (oid: string) => (await git.readCommit({...options, cache, oid})).commit;
207
+ const queue: {oid: string; time: number; parents: string[]}[] = [];
208
+ const enqueue = async (oid: string) => {
209
+ const commit = await time(oid);
210
+ queue.push({oid, time: commit.committer.timestamp, parents: commit.parent});
211
+ };
212
+ await enqueue(head); await enqueue(remote);
213
+ let read = 2;
214
+ let complete = true;
215
+ while (queue.some(item => flags.get(item.oid) !== both)) {
216
+ if (read >= upstreamWalkLimit) {complete = false; break;}
217
+ queue.sort((a, b) => b.time - a.time);
218
+ const item = queue.shift()!;
219
+ const flag = flags.get(item.oid)!;
220
+ for (const parent of item.parents) {
221
+ const previous = flags.get(parent) ?? 0;
222
+ if ((previous | flag) === previous) continue;
223
+ flags.set(parent, previous | flag);
224
+ if (!queue.some(entry => entry.oid === parent)) {await enqueue(parent); read++;}
225
+ }
226
+ }
227
+ let ahead = 0, behind = 0;
228
+ for (const flag of flags.values()) {
229
+ if (flag === local) ahead++;
230
+ else if (flag === tracking) behind++;
231
+ }
232
+ if (!complete) {
233
+ ahead = hasAhead ? upstreamWalkLimit : 0;
234
+ behind = hasBehind ? upstreamWalkLimit : 0;
235
+ }
236
+ return {state: hasAhead && hasBehind ? 'diverged' : hasAhead ? 'ahead' : hasBehind ? 'behind' : 'same',
237
+ ahead: hasAhead ? Math.max(ahead, 1) : 0, behind: hasBehind ? Math.max(behind, 1) : 0};
238
+ } catch (error) {
239
+ // No tracking ref yet, or history missing from a shallow clone.
240
+ if (errorCode(error) === 'NotFoundError') return unknown;
241
+ throw error;
242
+ }
243
+ };
172
244
  const assertSnapshot = async (snapshot: Snapshot) => {
173
245
  if (!sameMetadata(snapshot.metadata, await metadata())) throw new GitPanelError('changed');
174
246
  };
@@ -189,6 +261,7 @@ export async function openRepository(root: FileSystemDirectoryHandle) {
189
261
  const commits = head ? (await git.log({...options, cache, ref: head, depth: 50})).map(({oid, commit}) => ({
190
262
  oid, message: commit.message, author: commit.author.name, timestamp: commit.author.timestamp
191
263
  })) : [];
264
+ const upstream = await upstreamRelation(branch || undefined, head);
192
265
  const matrix = await git.statusMatrix({...options, cache, refresh: false, filepaths: [scope || '.']});
193
266
  const staged = await entries(null, scope);
194
267
  const committed = await entries(head || emptyTree, scope);
@@ -210,7 +283,7 @@ export async function openRepository(root: FileSystemDirectoryHandle) {
210
283
  }
211
284
  if (!sameMetadata(before, await metadata())) throw new GitPanelError('changed');
212
285
  await fs.checkReads();
213
- return {branch: branch || undefined, head, branches, commits, changes, scope, metadata: before, probe: fingerprint};
286
+ return {branch: branch || undefined, head, branches, commits, changes, scope, metadata: before, probe: fingerprint, upstream};
214
287
  };
215
288
  const historyChanges = async (snapshot: Snapshot, oid: string): Promise<FileChange[]> => {
216
289
  fs.clearReads();
package/src/style.css CHANGED
@@ -8,14 +8,24 @@
8
8
  .twgit-panel button:focus-visible, .twgit-panel summary:focus-visible { outline: 2px solid Highlight; outline-offset: 2px; }
9
9
  .twgit-panel button[aria-pressed="true"] { color: HighlightText; background: Highlight; }
10
10
  .twgit-context { display: flex; gap: 8px 20px; flex-wrap: wrap; padding: 8px 16px; }
11
- .twgit-connection { margin: 0 16px 8px; max-height: 45%; overflow: auto; }
11
+ .twgit-context { align-items: center; }
12
+ .twgit-connection, .twgit-git-details { position: relative; }
13
+ .twgit-connection > summary, .twgit-git-details > summary { cursor: pointer; }
14
+ /* Settings and Git details open over the list, so the list and the diff stay where they are. */
15
+ .twgit-connection-body { position: absolute; z-index: 5; top: calc(100% + 4px); left: 0; width: min(440px, 80vw); max-height: 60vh;
16
+ overflow: auto; padding: 10px 12px; background: Canvas; color: CanvasText; border: 1px solid GrayText; border-radius: 6px;
17
+ box-shadow: 0 8px 24px #0004; }
18
+ .twgit-connection-body > p { margin: 4px 0; overflow-wrap: anywhere; }
19
+ .twgit-sync { display: inline-flex; flex-wrap: wrap; gap: 4px; }
20
+ .twgit-change-kind { display: inline-block; min-width: 2.6em; font-size: 11px; opacity: .8; }
21
+ .twgit-file-details { font-size: 12px; margin-bottom: 6px; }
22
+ .twgit-file-details > p { margin: 2px 0; }
12
23
  .twgit-content { display: grid; grid-template-columns: minmax(230px, 30%) minmax(0, 1fr); flex: 1; min-height: 0; }
13
24
  .twgit-panel aside { padding: 0 14px 14px; overflow: auto; border-right: 1px solid GrayText; }
14
25
  .twgit-panel aside ul { list-style: none; padding: 0; margin: 4px 0; }
15
26
  .twgit-panel aside li { overflow-wrap: anywhere; }
16
27
  .twgit-panel summary { cursor: pointer; }
17
28
  .twgit-panel .twgit-file { width: 100%; border: 0; margin: 1px 0; }
18
- .twgit-file span { display: inline-block; width: 1.4em; font-family: monospace; }
19
29
  .twgit-diff { padding: 0 16px 16px; min-width: 0; overflow: auto; }
20
30
  .twgit-diff .diff { font-size: 12px; }
21
31
  .twgit-history { list-style: none; margin: 0; padding: 0; }
@@ -44,3 +54,4 @@
44
54
  .twgit-element-values td { font-family: ui-monospace, monospace; }
45
55
  .twgit-element-files { margin-top: 12px; }
46
56
  .twgit-panel .twgit-element span.twgit-element-name { width: auto; font-family: inherit; }
57
+ .twgit-git-details > .twgit-connection-body { left: auto; right: 0; }