@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 +121 -73
- package/dist/messages.d.ts +34 -0
- package/dist/{repository-VBp-UNWc.js → repository-CtvWYPQ8.js} +120 -4
- package/dist/repository.d.ts +17 -0
- package/dist/repository.js +1 -1
- package/dist/style.css +13 -2
- package/package.json +1 -1
- package/src/connection-settings.tsx +14 -8
- package/src/file-diff-view.tsx +4 -2
- package/src/index.tsx +31 -13
- package/src/messages.ts +34 -26
- package/src/project-change-list.tsx +2 -2
- package/src/repository.ts +76 -3
- package/src/style.css +13 -2
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 {
|
|
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: "
|
|
2750
|
-
unstaged: "
|
|
2751
|
-
untracked: "
|
|
2752
|
-
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: "
|
|
3043
|
-
unstaged: "
|
|
3044
|
-
untracked: "
|
|
3045
|
-
commit: "
|
|
3046
|
-
history: "
|
|
3047
|
-
worktree: "
|
|
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
|
|
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
|
|
3065
|
-
diverged: "
|
|
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: "
|
|
3068
|
-
pushRefresh: "
|
|
3069
|
-
rollbackFailed: "
|
|
3070
|
-
remoteFailed: "
|
|
3071
|
-
remoteDone: "
|
|
3072
|
-
pulledProject: "
|
|
3073
|
-
confirmPush: "
|
|
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: "
|
|
3119
|
-
diverged: "fast-forward
|
|
3135
|
+
pullUnsafe: "GitHub から取り込む前に、実行を止め、保存していない編集とまだ記録していない変更をなくしてください。",
|
|
3136
|
+
diverged: "このブランチと GitHub の両方に新しい記録があるため、ここでは取り込めません(pull は fast-forward のみ)。ブランチとファイルはそのままです。",
|
|
3120
3137
|
session: "このリポジトリと操作に必要な権限で GitHub に接続してください。",
|
|
3121
|
-
pushUnknown: "
|
|
3122
|
-
pushRefresh: "
|
|
3123
|
-
rollbackFailed: "
|
|
3124
|
-
remoteFailed: "
|
|
3125
|
-
remoteDone: "
|
|
3126
|
-
pulledProject: "
|
|
3127
|
-
confirmPush: "
|
|
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: "
|
|
3155
|
-
fetch: "
|
|
3156
|
-
pull: "
|
|
3157
|
-
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: "
|
|
3182
|
-
pull: "
|
|
3183
|
-
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
|
-
|
|
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),
|
|
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
|
|
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("
|
|
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("
|
|
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("
|
|
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,
|
|
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)))
|
|
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;
|
package/dist/messages.d.ts
CHANGED
|
@@ -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 {
|
|
17459
|
+
export { createReadOnlyFs as i, upstreamWalkLimit as n, GitPanelError as r, openRepository as t };
|
package/dist/repository.d.ts
CHANGED
|
@@ -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>;
|
package/dist/repository.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
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-
|
|
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
|
@@ -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: '
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
105
|
+
</div>
|
|
106
|
+
</details>
|
|
107
|
+
{error && <p className="twgit-error" role="alert">{labels[error]}</p>}
|
|
108
|
+
</>;
|
|
103
109
|
}
|
package/src/file-diff-view.tsx
CHANGED
|
@@ -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
|
-
<
|
|
27
|
-
|
|
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
|
-
<
|
|
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
|
|
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: '
|
|
11
|
-
history: '
|
|
12
|
-
branches: 'Local branches', unborn: 'No
|
|
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
|
|
21
|
-
diverged: '
|
|
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: '
|
|
24
|
-
pushRefresh: '
|
|
25
|
-
rollbackFailed: '
|
|
26
|
-
remoteFailed: '
|
|
27
|
-
remoteDone: '
|
|
28
|
-
pulledProject: '
|
|
29
|
-
confirmPush: '
|
|
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
|
|
40
|
-
staged: '
|
|
41
|
-
history: '
|
|
42
|
-
branches: 'ローカルブランチ', unborn: '
|
|
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: '
|
|
51
|
-
diverged: 'fast-forward
|
|
54
|
+
pullUnsafe: 'GitHub から取り込む前に、実行を止め、保存していない編集とまだ記録していない変更をなくしてください。',
|
|
55
|
+
diverged: 'このブランチと GitHub の両方に新しい記録があるため、ここでは取り込めません(pull は fast-forward のみ)。ブランチとファイルはそのままです。',
|
|
52
56
|
session: 'このリポジトリと操作に必要な権限で GitHub に接続してください。',
|
|
53
|
-
pushUnknown: '
|
|
54
|
-
pushRefresh: '
|
|
55
|
-
rollbackFailed: '
|
|
56
|
-
remoteFailed: '
|
|
57
|
-
remoteDone: '
|
|
58
|
-
pulledProject: '
|
|
59
|
-
confirmPush: '
|
|
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: '
|
|
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: '
|
|
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-
|
|
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; }
|