@kubohiroya/turbowarp-plus-editor-git 0.14.0 → 0.15.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/README.ja.md +3 -1
- package/README.md +3 -1
- package/dist/filesystem.d.ts +2 -2
- package/dist/index.d.ts +9 -0
- package/dist/index.js +92 -10
- package/dist/local-operations.d.ts +22 -0
- package/dist/messages.d.ts +22 -0
- package/dist/{repository-CtvWYPQ8.js → repository-Cx-mxT9l.js} +439 -4
- package/dist/repository.d.ts +3 -0
- package/dist/repository.js +1 -1
- package/dist/style.css +4 -0
- package/package.json +1 -1
- package/src/filesystem.ts +2 -1
- package/src/index.tsx +48 -5
- package/src/local-operations.ts +79 -0
- package/src/messages.ts +14 -0
- package/src/repository.ts +5 -0
- package/src/style.css +4 -0
package/README.ja.md
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md)
|
|
4
4
|
|
|
5
|
-
TurboWarp Plus Editor
|
|
5
|
+
TurboWarp Plus Editor のファイルメニューに「ローカル Git」を追加します。リポジトリルートを一度選び、配下の sb3-toolchain v1 作品フォルダをパネルから開きます。作品の編集と保存には Editor の project-folder 接続を使います。
|
|
6
6
|
|
|
7
7
|
ステージ済み・未ステージ・未追跡の変更、現在のブランチ、HEAD、ローカルブランチと直近50件の履歴を表示します。ファイルを選ぶとテキスト差分またはバイナリのサイズを確認できます。作品データ(`project.source.json`)は、ホストがブロックの描画関数を渡していればステージ・スプライト別、コード別のブロックで比べ、「テキスト」に切り替えると行単位の差分を表示します。ステージ・スプライトの見出しには、その版の現在のコスチューム(ステージは背景)のサムネイルを添えます。素材は比較する版から読みます(ステージ済みは HEAD とインデックス、未ステージはインデックスと作業ツリー、未追跡は作業ツリー、コミットは第一親とそのコミット)。素材のフォルダはその版の `sb3-source.json` の `assetsDirectory`(既定は `assets`)です。読めない版では名前だけを示します。コミットを選ぶと第一親との差分を表示します。作品を選んでいるときは、変更一覧と差分をそのフォルダに絞ります。
|
|
8
8
|
|
|
9
9
|
パネルを開いている間は、2 秒ごと、ウィンドウにフォーカスが戻ったとき、対応環境では `FileSystemObserver` の通知でリポジトリを確認します。HEAD、インデックス、`packed-refs`、`config`、現在のブランチの参照、選択中の作品フォルダのファイル(作品を選んでいない場合は作業ツリー全体、5,000 件まで)のサイズと更新時刻を比べます。変更があり、差分やコミットを開いていなければ読み直します。差分・コミット・プルリクエストのレビューを開いている間は表示を保ち、**再取得** を案内します。Git がメタデータをロックしている間は確認を待ちます。**作品を再読込** では、Editor の未保存編集を確認してから同じ作品を開き直します。Git の閲覧には読み取り専用のアダプターを使い、インデックスの更新を無効にします。読み取り中にファイルや Git の状態が変わった場合は、外部操作の完了後に再取得します。
|
|
10
10
|
|
|
11
|
+
ホストが `localWrite` を渡すと、現在のブランチで、ファイルを次の記録に入れる(ステージ)、次の記録から外す(アンステージ)、入れた変更を記録する(コミット)操作も使えます。pull と同じく、Git の書き込みを先にまとめ、置き換えるファイルを確かめ直してから適用し、途中で失敗したら適用した分を戻します。書き込むのは `.git/index`、オブジェクト、現在のブランチの参照だけで、作業ツリーと `.git/config` は書きません。コミットの作者はリポジトリの `user.name` と `user.email` を使い、足りない分だけ `localWrite.author()` で補います。補った値を設定には書きません。Editor に保存していない編集があるときは、ステージを待ちます。ブラウザーからは実行権限とシンボリックリンクが見えないため、Git が `100644` 以外のモードで記録しているファイルはステージできません。ほかのツールが `index.lock` を持っているときや、マージ・cherry-pick・revert・rebase の途中では操作しません。`localWrite` がなければ、パネルは読むだけです。
|
|
12
|
+
|
|
11
13
|
File System Access に対応する Chromium 系ブラウザーが必要です。初版は通常の `.git` ディレクトリとファイル内容の変更を扱います。linked worktree、サブモジュール、シンボリックリンクは対象外です。作業ツリーの実行権限の変更はブラウザーから検出できません。Git に記録済みのファイルモードは表示します。バイナリアセットはサイズを示し、テキスト差分の生成には上限があります。
|
|
12
14
|
|
|
13
15
|
Node.js 26 で `npm ci` と `npm test` を実行します。Vite でライブラリとスタイルシート、TypeScript で型宣言を生成します。`npm pack` で Editor 用の配布物を作成します。`npm run test:browser` は、最小 editor runtime(`@kubohiroya/turbowarp-plus-editor-test-runtime`)の上で、比較ダイアログのブロックの描画を Chromium で試します。Editor が固定する VM と Scratch Blocks を使い、Editor のビルドは要りません。Editor では既定で有効です。起動 URL に `twPlusLocalGit=0` を指定するとパネルの接続を解除できます。
|
package/README.md
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
[日本語](README.ja.md)
|
|
4
4
|
|
|
5
|
-
Local Git adds a
|
|
5
|
+
Local Git adds a panel to the TurboWarp Plus Editor File menu. Select the repository root once, then browse to an sb3-toolchain v1 project folder inside it. Project editing and saving are supplied by the Editor's project-folder integration.
|
|
6
6
|
|
|
7
7
|
The panel separates staged, unstaged, and untracked changes. It shows the current branch, HEAD, local branches, and the latest 50 commits. Select a file to inspect its text diff or binary size. When the host supplies a block renderer, project data (`project.source.json`) is compared as blocks by stage, sprite and code, and **Text** switches to the line diff. Each stage and sprite heading shows a thumbnail of its current costume (the backdrop for the stage) in each version. Assets are read from the compared versions: HEAD and the index for staged changes, the index and the working tree for unstaged changes, the working tree for untracked files, and the first parent and the commit for history. The assets folder is the `assetsDirectory` of that version's `sb3-source.json` (`assets` by default). When an asset cannot be read, only the name is shown. Pull request reviews read the same thumbnails from raw.githubusercontent.com at the merge base and head, without using the GitHub API rate limit. A review's **Reviewed** marks are kept in browser storage per repository and pull request, together with each script's content: they survive closing the panel and reloading the Editor, and only the marks of scripts whose content changed are cleared when the pull request head moves. They are never written to project files or Git. Select a commit to compare its files with its first parent. The selected project limits the file list and diffs to that folder.
|
|
8
8
|
|
|
9
9
|
While the panel is open, it checks the repository every two seconds, when the window regains focus, and on `FileSystemObserver` notifications where available. It compares the sizes and modification times of HEAD, the index, `packed-refs`, `config`, the current branch ref, and the files in the selected project folder (the whole work tree when no project is selected, up to 5,000 files). When something changed and no diff or commit is open, the panel reads the repository again. While a diff, a commit or a pull request review is open, it keeps the view and asks you to **Refresh** instead. Checks wait while Git holds a lock on its metadata. **Reload Project** opens the selected project again using the Editor's unsaved-edit confirmation. Git inspection uses a read-only adapter and isomorphic-git with index refresh disabled. If the repository changes during a read, refresh after the other operation completes.
|
|
10
10
|
|
|
11
|
+
When the host passes `localWrite`, the panel can also add files to the next record (stage), remove them from it (unstage) and record the staged changes (commit) on the current branch. Like pull, these operations collect Git's writes first and apply them only after the files they replace are checked again; a failure part way puts back what was applied. They write only `.git/index`, objects and the current branch ref; the work tree and `.git/config` are never written. Commits use the repository's `user.name` and `user.email`; `localWrite.author()` fills in only what the config lacks, and is never written to it. Staging waits until the Editor's edits are saved. The browser cannot see executable bits or symbolic links, so files that Git records with another mode than `100644` cannot be staged here. Operations stop while another tool holds `index.lock` or is in the middle of a merge, cherry-pick, revert or rebase. Without `localWrite`, the panel only reads.
|
|
12
|
+
|
|
11
13
|
A Chromium-based browser with File System Access support is required. The first version supports ordinary `.git` directories and content changes. Linked worktrees, submodules, and symbolic links are outside this scope. The browser cannot detect working-tree executable-bit changes. Recorded index and tree modes remain visible. Binary assets show sizes, and text diff generation is bounded.
|
|
12
14
|
|
|
13
15
|
Use Node.js 26, run `npm ci`, then `npm test`. Vite builds the library and stylesheet, and TypeScript emits declarations. `npm pack` prepares the artifact used by the Editor build. `npm run test:browser` runs the comparison dialog's block drawing in Chromium on the minimal editor runtime (`@kubohiroya/turbowarp-plus-editor-test-runtime`), with the Editor's pinned VM and Scratch Blocks and without building the Editor. The Editor enables the panel by default. Start it with `twPlusLocalGit=0` in its URL to disable the integration.
|
package/dist/filesystem.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export declare class GitPanelError extends Error {
|
|
2
|
-
code: 'invalid' | 'changed' | 'large' | 'unsupported' | 'pullUnsafe' | 'diverged' | 'session' | 'pushUnknown' | 'pushRefresh' | 'rollbackFailed' | 'remoteFailed';
|
|
3
|
-
constructor(code: 'invalid' | 'changed' | 'large' | 'unsupported' | 'pullUnsafe' | 'diverged' | 'session' | 'pushUnknown' | 'pushRefresh' | 'rollbackFailed' | 'remoteFailed', message?: string);
|
|
2
|
+
code: 'invalid' | 'changed' | 'large' | 'unsupported' | 'pullUnsafe' | 'diverged' | 'session' | 'pushUnknown' | 'pushRefresh' | 'rollbackFailed' | 'remoteFailed' | 'saveFirst' | 'author' | 'nothingStaged' | 'commitMessage';
|
|
3
|
+
constructor(code: 'invalid' | 'changed' | 'large' | 'unsupported' | 'pullUnsafe' | 'diverged' | 'session' | 'pushUnknown' | 'pushRefresh' | 'rollbackFailed' | 'remoteFailed' | 'saveFirst' | 'author' | 'nothingStaged' | 'commitMessage', message?: string);
|
|
4
4
|
}
|
|
5
5
|
export declare const fileError: (code: string, path: string) => Error & {
|
|
6
6
|
code: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { GitConnectionSettings } from './connection-settings.js';
|
|
2
|
+
import type { GitAuthor } from './local-operations.js';
|
|
2
3
|
import type { ProjectBlockRenderer } from './scratch-block-diff-renderer.js';
|
|
3
4
|
import type { ScratchBlockLocationWorkspace } from './block-diff-location.js';
|
|
4
5
|
export { createScratchBlocksDiffLocator } from './block-diff-location.js';
|
|
@@ -9,6 +10,7 @@ export { openRepository, createReadOnlyFs, GitPanelError } from './repository.js
|
|
|
9
10
|
export { openProjectComparison } from './project-comparison.js';
|
|
10
11
|
export type { ProjectComparisonAction, ProjectComparisonOptions, ProjectComparisonSection } from './project-comparison.js';
|
|
11
12
|
export type { Repository, Snapshot, FileChange, FileDiff } from './repository.js';
|
|
13
|
+
export type { GitAuthor, LocalOperation } from './local-operations.js';
|
|
12
14
|
export interface GitHost {
|
|
13
15
|
connectionSettings?: GitConnectionSettings;
|
|
14
16
|
renderReviewBlock?: ProjectBlockRenderer;
|
|
@@ -25,6 +27,13 @@ export interface GitHost {
|
|
|
25
27
|
openTargetCode?(targetId: string): void | Promise<void>;
|
|
26
28
|
/** The Editor's code workspace as it is now. */
|
|
27
29
|
getWorkspace?(): ScratchBlockLocationWorkspace | null | Promise<ScratchBlockLocationWorkspace | null>;
|
|
30
|
+
/**
|
|
31
|
+
* Offers staging, unstaging and commits. Without it the panel only reads. `author` is used only where the
|
|
32
|
+
* repository's Git config has no user.name or user.email; the panel never writes the config.
|
|
33
|
+
*/
|
|
34
|
+
localWrite?: {
|
|
35
|
+
author?(): GitAuthor | undefined;
|
|
36
|
+
};
|
|
28
37
|
}
|
|
29
38
|
export interface GitPanelRequest {
|
|
30
39
|
reference: {
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { S as parseProjectSource, a as PullRequestError, c as ProjectSourceComparison, d as organizeProjectChanges, f as projectPropertyLabel, i as createScratchBlocksDiffLocator, l as ProjectSourceReview, n as openProjectComparison, o as createGitHubPullRequests, r as createScratchBlocksDiffRenderer, s as gitHubRepositoryFromOrigin, v as useAssetImage, y as useCostumeImage } from "./project-comparison-LAyP_beL.js";
|
|
2
2
|
import { n as init_objectSpread2, r as __commonJSMin, t as _objectSpread2$1 } from "./objectSpread2-Dkd68nJH.js";
|
|
3
|
-
import { i as createReadOnlyFs, r as GitPanelError, t as openRepository } from "./repository-
|
|
3
|
+
import { i as createReadOnlyFs, r as GitPanelError, t as openRepository } from "./repository-Cx-mxT9l.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 { createPortal, render, unmountComponentAtNode } from "react-dom";
|
|
@@ -3063,6 +3063,7 @@ var messages = {
|
|
|
3063
3063
|
changed: "Files or Git state changed while reading. Refresh to read the current state.",
|
|
3064
3064
|
outdated: "The repository changed outside the Editor. Refresh to read the current state.",
|
|
3065
3065
|
unsupported: "This repository contains an entry the browser cannot represent.",
|
|
3066
|
+
commitMessage: "Write a message for this record.",
|
|
3066
3067
|
pullUnsafe: "Stop the project, and save or undo unsaved edits and unrecorded changes, before getting changes from GitHub (pull).",
|
|
3067
3068
|
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.",
|
|
3068
3069
|
session: "Reconnect GitHub for this repository and requested permission.",
|
|
@@ -3071,6 +3072,16 @@ var messages = {
|
|
|
3071
3072
|
rollbackFailed: "Some local changes could not be restored. Check the files and the Git details.",
|
|
3072
3073
|
remoteFailed: "Could not complete the GitHub operation. Check the connection and try again.",
|
|
3073
3074
|
remoteDone: "The GitHub operation finished.",
|
|
3075
|
+
stage: "Add to Next Record",
|
|
3076
|
+
unstage: "Remove from Next Record",
|
|
3077
|
+
stageAll: "Add All Changes to Next Record",
|
|
3078
|
+
commitLabel: "Message for this record",
|
|
3079
|
+
commitButton: "Record (Commit)",
|
|
3080
|
+
committed: "Recorded the changes (commit).",
|
|
3081
|
+
commitAuthor: "Recorded as",
|
|
3082
|
+
saveFirst: "Save the project before adding its changes to the next record.",
|
|
3083
|
+
author: "Set user.name and user.email for this repository in Git before recording. The Editor does not change Git settings.",
|
|
3084
|
+
nothingStaged: "Add changes to the next record first.",
|
|
3074
3085
|
pulledProject: "Got the changes from GitHub. Use Reload Project to open the updated files.",
|
|
3075
3086
|
confirmPush: "Send the records on this branch to GitHub (push)?",
|
|
3076
3087
|
details: "Git details",
|
|
@@ -3134,6 +3145,7 @@ var messages = {
|
|
|
3134
3145
|
changed: "読込中にファイルまたは Git の状態が変わりました。再取得してください。",
|
|
3135
3146
|
outdated: "Editor の外でリポジトリが変更されました。再取得すると最新の状態を読み込みます。",
|
|
3136
3147
|
unsupported: "ブラウザーで扱えない種類の Git エントリーがあります。",
|
|
3148
|
+
commitMessage: "この記録の説明を書いてください。",
|
|
3137
3149
|
pullUnsafe: "GitHub から取り込む前に、実行を止め、保存していない編集とまだ記録していない変更をなくしてください。",
|
|
3138
3150
|
diverged: "このブランチと GitHub の両方に新しい記録があるため、ここでは取り込めません(pull は fast-forward のみ)。ブランチとファイルはそのままです。",
|
|
3139
3151
|
session: "このリポジトリと操作に必要な権限で GitHub に接続してください。",
|
|
@@ -3142,6 +3154,16 @@ var messages = {
|
|
|
3142
3154
|
rollbackFailed: "一部のローカルの変更を元に戻せませんでした。ファイルと Git の詳細を確認してください。",
|
|
3143
3155
|
remoteFailed: "GitHub との操作を完了できませんでした。接続を確認して、もう一度試してください。",
|
|
3144
3156
|
remoteDone: "GitHub との操作が終わりました。",
|
|
3157
|
+
stage: "次の記録に入れる",
|
|
3158
|
+
unstage: "次の記録から外す",
|
|
3159
|
+
stageAll: "すべての変更を次の記録に入れる",
|
|
3160
|
+
commitLabel: "この記録の説明",
|
|
3161
|
+
commitButton: "記録する(コミット)",
|
|
3162
|
+
committed: "変更を記録しました(コミット)。",
|
|
3163
|
+
commitAuthor: "記録する人",
|
|
3164
|
+
saveFirst: "次の記録に入れる前に、作品を保存してください。",
|
|
3165
|
+
author: "記録する前に、Git でこのリポジトリの user.name と user.email を設定してください。Editor は Git の設定を変えません。",
|
|
3166
|
+
nothingStaged: "先に、変更を次の記録に入れてください。",
|
|
3145
3167
|
pulledProject: "GitHub から取り込みました。「作品を再読込」で更新したファイルを開けます。",
|
|
3146
3168
|
confirmPush: "このブランチの記録を GitHub に送りますか(push)?",
|
|
3147
3169
|
details: "Git の詳細",
|
|
@@ -3532,7 +3554,7 @@ var initial = {
|
|
|
3532
3554
|
var controllers = /* @__PURE__ */ new WeakMap();
|
|
3533
3555
|
var watchInterval = 2e3;
|
|
3534
3556
|
function GitPanel({ vm, document, host, controller }) {
|
|
3535
|
-
var _state$repository, _state$snapshot$head$, _state$snapshot$head, _state$
|
|
3557
|
+
var _state$snapshot, _state$repository, _state$snapshot$head$, _state$snapshot$head, _state$snapshot3;
|
|
3536
3558
|
const [visible, setVisible] = useState(false);
|
|
3537
3559
|
const [slot, setSlot] = useState(null);
|
|
3538
3560
|
const shown = visible || slot !== null;
|
|
@@ -3574,26 +3596,25 @@ function GitPanel({ vm, document, host, controller }) {
|
|
|
3574
3596
|
});
|
|
3575
3597
|
running.current = true;
|
|
3576
3598
|
const ticket = ++epoch.current;
|
|
3577
|
-
|
|
3599
|
+
if (!automatic) setState((previous) => _objectSpread2$1(_objectSpread2$1({}, previous), {}, {
|
|
3578
3600
|
busy: true,
|
|
3579
3601
|
error: null,
|
|
3580
3602
|
notice: void 0
|
|
3581
3603
|
}));
|
|
3582
3604
|
try {
|
|
3583
3605
|
const update = await operation();
|
|
3584
|
-
if (epoch.current === ticket) setState((previous) => _objectSpread2$1(_objectSpread2$1(_objectSpread2$1({}, previous), update), {}, { busy: false }));
|
|
3606
|
+
if (epoch.current === ticket) setState((previous) => _objectSpread2$1(_objectSpread2$1(_objectSpread2$1({}, previous), update), {}, { busy: automatic ? previous.busy : false }));
|
|
3585
3607
|
} catch (error) {
|
|
3586
3608
|
if (epoch.current !== ticket) return;
|
|
3587
3609
|
const code = error instanceof GitPanelError ? error.code : "failed";
|
|
3588
3610
|
if (automatic && code === "changed") {
|
|
3589
3611
|
handled.current = null;
|
|
3590
|
-
setState((previous) => _objectSpread2$1(_objectSpread2$1({}, previous), {}, { busy: false }));
|
|
3591
3612
|
return;
|
|
3592
3613
|
}
|
|
3593
3614
|
const message = labels[code] || labels.failed;
|
|
3594
3615
|
setState((previous) => _objectSpread2$1(_objectSpread2$1({}, previous), {}, {
|
|
3595
3616
|
error: automatic && previous.error ? previous.error : message,
|
|
3596
|
-
busy: false,
|
|
3617
|
+
busy: automatic ? previous.busy : false,
|
|
3597
3618
|
diff: null
|
|
3598
3619
|
}));
|
|
3599
3620
|
} finally {
|
|
@@ -3877,6 +3898,29 @@ function GitPanel({ vm, document, host, controller }) {
|
|
|
3877
3898
|
};
|
|
3878
3899
|
});
|
|
3879
3900
|
};
|
|
3901
|
+
const [message, setMessage] = useState("");
|
|
3902
|
+
const localOperation = (operation, notice) => {
|
|
3903
|
+
const repository = current.current.repository;
|
|
3904
|
+
if (!repository || !host.localWrite) return Promise.resolve();
|
|
3905
|
+
return run(async () => {
|
|
3906
|
+
var _current$current$proj2;
|
|
3907
|
+
const ticket = epoch.current;
|
|
3908
|
+
await repository.localOperation(operation, () => {
|
|
3909
|
+
if (epoch.current !== ticket) throw new GitPanelError("changed");
|
|
3910
|
+
if (operation.kind === "stage" && host.isProjectDirty()) throw new GitPanelError("saveFirst");
|
|
3911
|
+
});
|
|
3912
|
+
if (operation.kind === "commit") setMessage("");
|
|
3913
|
+
const snapshot = await repository.read((_current$current$proj2 = current.current.project) !== null && _current$current$proj2 !== void 0 ? _current$current$proj2 : "");
|
|
3914
|
+
return {
|
|
3915
|
+
snapshot,
|
|
3916
|
+
changes: snapshot.changes,
|
|
3917
|
+
selected: null,
|
|
3918
|
+
diff: null,
|
|
3919
|
+
commit: null,
|
|
3920
|
+
notice
|
|
3921
|
+
};
|
|
3922
|
+
});
|
|
3923
|
+
};
|
|
3880
3924
|
const focusTrap = (event) => {
|
|
3881
3925
|
if (event.key === "Escape") {
|
|
3882
3926
|
event.stopPropagation();
|
|
@@ -3905,12 +3949,15 @@ function GitPanel({ vm, document, host, controller }) {
|
|
|
3905
3949
|
"unstaged",
|
|
3906
3950
|
"untracked"
|
|
3907
3951
|
];
|
|
3952
|
+
const writable = !!host.localWrite && !state.commit && !!((_state$snapshot = state.snapshot) === null || _state$snapshot === void 0 ? void 0 : _state$snapshot.branch);
|
|
3953
|
+
const unrecorded = [...new Set(state.changes.filter((change) => change.group === "unstaged" || change.group === "untracked").map((change) => change.path))];
|
|
3954
|
+
const staged = state.changes.some((change) => change.group === "staged");
|
|
3908
3955
|
const projectList = typeof projectChanges === "object" ? projectChanges : null;
|
|
3909
3956
|
const projectFile = state.project === null ? null : [state.project, "project.source.json"].filter(Boolean).join("/");
|
|
3910
3957
|
const fileLocator = (change) => locator && change.path === projectFile && (change.group === "unstaged" || change.group === "untracked" || change.group === "staged" && !state.changes.some((item) => item.path === change.path && item.group === "unstaged")) ? locator : void 0;
|
|
3911
3958
|
const reviewLocator = (path, headSha) => {
|
|
3912
|
-
var _state$
|
|
3913
|
-
return locator && path === projectFile && ((_state$
|
|
3959
|
+
var _state$snapshot2;
|
|
3960
|
+
return locator && path === projectFile && ((_state$snapshot2 = state.snapshot) === null || _state$snapshot2 === void 0 ? void 0 : _state$snapshot2.head) === headSha && !state.changes.length ? locator : void 0;
|
|
3914
3961
|
};
|
|
3915
3962
|
const selection = projectList && !state.selected ? findElement(projectList.list, element) : null;
|
|
3916
3963
|
const content = /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("header", null, /* @__PURE__ */ React.createElement("h2", null, labels.title), /* @__PURE__ */ React.createElement("button", {
|
|
@@ -3969,7 +4016,16 @@ function GitPanel({ vm, document, host, controller }) {
|
|
|
3969
4016
|
disabled: state.busy,
|
|
3970
4017
|
"aria-pressed": state.selected === change,
|
|
3971
4018
|
onClick: () => openFile(change)
|
|
3972
|
-
}, /* @__PURE__ */ React.createElement("small", { className: "twgit-change-kind" }, !change.after ? labels.deleted : !change.before ? labels.added : labels.modified), " ", change.path)
|
|
4019
|
+
}, /* @__PURE__ */ React.createElement("small", { className: "twgit-change-kind" }, !change.after ? labels.deleted : !change.before ? labels.added : labels.modified), " ", change.path), writable && /* @__PURE__ */ React.createElement("button", {
|
|
4020
|
+
type: "button",
|
|
4021
|
+
className: "twgit-stage",
|
|
4022
|
+
disabled: state.busy,
|
|
4023
|
+
"aria-label": `${group === "staged" ? labels.unstage : labels.stage}: ${change.path}`,
|
|
4024
|
+
onClick: () => void localOperation({
|
|
4025
|
+
kind: group === "staged" ? "unstage" : "stage",
|
|
4026
|
+
paths: [change.path]
|
|
4027
|
+
})
|
|
4028
|
+
}, group === "staged" ? labels.unstage : labels.stage))))));
|
|
3973
4029
|
return projectList ? /* @__PURE__ */ React.createElement(ProjectChangeListView, {
|
|
3974
4030
|
changes: projectList,
|
|
3975
4031
|
selected: selection ? element : null,
|
|
@@ -3981,7 +4037,33 @@ function GitPanel({ vm, document, host, controller }) {
|
|
|
3981
4037
|
fileListOpen,
|
|
3982
4038
|
setFileListOpen
|
|
3983
4039
|
}) : fileList;
|
|
3984
|
-
})(), !state.changes.length && /* @__PURE__ */ React.createElement("p", null, labels.clean),
|
|
4040
|
+
})(), !state.changes.length && /* @__PURE__ */ React.createElement("p", null, labels.clean), writable && /* @__PURE__ */ React.createElement("section", {
|
|
4041
|
+
className: "twgit-commit",
|
|
4042
|
+
"aria-label": labels.commitButton
|
|
4043
|
+
}, /* @__PURE__ */ React.createElement("button", {
|
|
4044
|
+
type: "button",
|
|
4045
|
+
disabled: state.busy || !unrecorded.length,
|
|
4046
|
+
onClick: () => void localOperation({
|
|
4047
|
+
kind: "stage",
|
|
4048
|
+
paths: unrecorded
|
|
4049
|
+
})
|
|
4050
|
+
}, labels.stageAll), /* @__PURE__ */ React.createElement("label", null, labels.commitLabel, /* @__PURE__ */ React.createElement("textarea", {
|
|
4051
|
+
value: message,
|
|
4052
|
+
rows: 3,
|
|
4053
|
+
disabled: state.busy,
|
|
4054
|
+
onChange: (event) => setMessage(event.currentTarget.value)
|
|
4055
|
+
})), /* @__PURE__ */ React.createElement("button", {
|
|
4056
|
+
type: "button",
|
|
4057
|
+
disabled: state.busy || !staged || !message.trim(),
|
|
4058
|
+
onClick: () => {
|
|
4059
|
+
var _host$localWrite, _host$localWrite$auth;
|
|
4060
|
+
localOperation({
|
|
4061
|
+
kind: "commit",
|
|
4062
|
+
message,
|
|
4063
|
+
author: (_host$localWrite = host.localWrite) === null || _host$localWrite === void 0 || (_host$localWrite$auth = _host$localWrite.author) === null || _host$localWrite$auth === void 0 ? void 0 : _host$localWrite$auth.call(_host$localWrite)
|
|
4064
|
+
}, labels.committed);
|
|
4065
|
+
}
|
|
4066
|
+
}, labels.commitButton)), /* @__PURE__ */ React.createElement("h3", null, labels.history), /* @__PURE__ */ React.createElement("ol", { className: "twgit-history" }, (_state$snapshot3 = state.snapshot) === null || _state$snapshot3 === void 0 ? void 0 : _state$snapshot3.commits.map((commit) => /* @__PURE__ */ React.createElement("li", { key: commit.oid }, /* @__PURE__ */ React.createElement("button", {
|
|
3985
4067
|
disabled: state.busy,
|
|
3986
4068
|
"aria-pressed": state.commit === commit.oid,
|
|
3987
4069
|
title: commit.oid,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** The name and email a commit records. */
|
|
2
|
+
export type GitAuthor = {
|
|
3
|
+
name: string;
|
|
4
|
+
email: string;
|
|
5
|
+
};
|
|
6
|
+
export type LocalOperation = {
|
|
7
|
+
kind: 'stage';
|
|
8
|
+
paths: readonly string[];
|
|
9
|
+
} | {
|
|
10
|
+
kind: 'unstage';
|
|
11
|
+
paths: readonly string[];
|
|
12
|
+
} | {
|
|
13
|
+
kind: 'commit';
|
|
14
|
+
message: string;
|
|
15
|
+
author?: GitAuthor;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Stages, unstages or commits in the granted repository. Like pull, Git's writes are collected first and applied
|
|
19
|
+
* after the files they replace are checked again; a failure part way puts back what was applied. Only `.git/index`,
|
|
20
|
+
* objects and the current branch ref are written. The work tree and `.git/config` are never written.
|
|
21
|
+
*/
|
|
22
|
+
export declare function runLocalOperation(root: FileSystemDirectoryHandle, operation: LocalOperation, guard: () => void): Promise<void>;
|
package/dist/messages.d.ts
CHANGED
|
@@ -41,6 +41,7 @@ export declare const messages: {
|
|
|
41
41
|
changed: string;
|
|
42
42
|
outdated: string;
|
|
43
43
|
unsupported: string;
|
|
44
|
+
commitMessage: string;
|
|
44
45
|
pullUnsafe: string;
|
|
45
46
|
diverged: string;
|
|
46
47
|
session: string;
|
|
@@ -49,6 +50,16 @@ export declare const messages: {
|
|
|
49
50
|
rollbackFailed: string;
|
|
50
51
|
remoteFailed: string;
|
|
51
52
|
remoteDone: string;
|
|
53
|
+
stage: string;
|
|
54
|
+
unstage: string;
|
|
55
|
+
stageAll: string;
|
|
56
|
+
commitLabel: string;
|
|
57
|
+
commitButton: string;
|
|
58
|
+
committed: string;
|
|
59
|
+
commitAuthor: string;
|
|
60
|
+
saveFirst: string;
|
|
61
|
+
author: string;
|
|
62
|
+
nothingStaged: string;
|
|
52
63
|
pulledProject: string;
|
|
53
64
|
confirmPush: string;
|
|
54
65
|
details: string;
|
|
@@ -112,6 +123,7 @@ export declare const messages: {
|
|
|
112
123
|
changed: string;
|
|
113
124
|
outdated: string;
|
|
114
125
|
unsupported: string;
|
|
126
|
+
commitMessage: string;
|
|
115
127
|
pullUnsafe: string;
|
|
116
128
|
diverged: string;
|
|
117
129
|
session: string;
|
|
@@ -120,6 +132,16 @@ export declare const messages: {
|
|
|
120
132
|
rollbackFailed: string;
|
|
121
133
|
remoteFailed: string;
|
|
122
134
|
remoteDone: string;
|
|
135
|
+
stage: string;
|
|
136
|
+
unstage: string;
|
|
137
|
+
stageAll: string;
|
|
138
|
+
commitLabel: string;
|
|
139
|
+
commitButton: string;
|
|
140
|
+
committed: string;
|
|
141
|
+
commitAuthor: string;
|
|
142
|
+
saveFirst: string;
|
|
143
|
+
author: string;
|
|
144
|
+
nothingStaged: string;
|
|
123
145
|
pulledProject: string;
|
|
124
146
|
confirmPush: string;
|
|
125
147
|
details: string;
|
|
@@ -11822,6 +11822,132 @@ async function _writeObject({ fs, gitdir, type, object, format = "content", oid
|
|
|
11822
11822
|
});
|
|
11823
11823
|
return oid;
|
|
11824
11824
|
}
|
|
11825
|
+
function posixifyPathBuffer(buffer) {
|
|
11826
|
+
let idx;
|
|
11827
|
+
while (~(idx = buffer.indexOf(92))) buffer[idx] = 47;
|
|
11828
|
+
return buffer;
|
|
11829
|
+
}
|
|
11830
|
+
/**
|
|
11831
|
+
* Add a file to the git index (aka staging area)
|
|
11832
|
+
*
|
|
11833
|
+
* @param {object} args
|
|
11834
|
+
* @param {FsClient} args.fs - a file system implementation
|
|
11835
|
+
* @param {string} args.dir - The [working tree](dir-vs-gitdir.md) directory path
|
|
11836
|
+
* @param {string} [args.gitdir=join(dir, '.git')] - [required] The [git directory](dir-vs-gitdir.md) path
|
|
11837
|
+
* @param {string|string[]} args.filepath - The path to the file to add to the index
|
|
11838
|
+
* @param {object} [args.cache] - a [cache](cache.md) object
|
|
11839
|
+
* @param {boolean} [args.force=false] - add to index even if matches gitignore. Think `git add --force`
|
|
11840
|
+
* @param {boolean} [args.parallel=false] - process each input file in parallel. Parallel processing will result in more memory consumption but less process time
|
|
11841
|
+
*
|
|
11842
|
+
* @returns {Promise<void>} Resolves successfully once the git index has been updated
|
|
11843
|
+
*
|
|
11844
|
+
* @example
|
|
11845
|
+
* await fs.promises.writeFile('/tutorial/README.md', `# TEST`)
|
|
11846
|
+
* await git.add({ fs, dir: '/tutorial', filepath: 'README.md' })
|
|
11847
|
+
* console.log('done')
|
|
11848
|
+
*
|
|
11849
|
+
*/
|
|
11850
|
+
async function add({ fs: _fs, dir, gitdir = join(dir, ".git"), filepath, cache = {}, force = false, parallel = true }) {
|
|
11851
|
+
try {
|
|
11852
|
+
assertParameter("fs", _fs);
|
|
11853
|
+
assertParameter("dir", dir);
|
|
11854
|
+
assertParameter("gitdir", gitdir);
|
|
11855
|
+
assertParameter("filepath", filepath);
|
|
11856
|
+
const fs = new FileSystem(_fs);
|
|
11857
|
+
const updatedGitdir = await discoverGitdir({
|
|
11858
|
+
fsp: fs,
|
|
11859
|
+
dotgit: gitdir
|
|
11860
|
+
});
|
|
11861
|
+
await GitIndexManager.acquire({
|
|
11862
|
+
fs,
|
|
11863
|
+
gitdir: updatedGitdir,
|
|
11864
|
+
cache
|
|
11865
|
+
}, async (index) => {
|
|
11866
|
+
const autocrlf = await (await GitConfigManager.get({
|
|
11867
|
+
fs,
|
|
11868
|
+
gitdir: updatedGitdir
|
|
11869
|
+
})).get("core.autocrlf");
|
|
11870
|
+
return addToIndex({
|
|
11871
|
+
dir,
|
|
11872
|
+
gitdir: updatedGitdir,
|
|
11873
|
+
fs,
|
|
11874
|
+
filepath,
|
|
11875
|
+
index,
|
|
11876
|
+
force,
|
|
11877
|
+
parallel,
|
|
11878
|
+
autocrlf
|
|
11879
|
+
});
|
|
11880
|
+
});
|
|
11881
|
+
} catch (err) {
|
|
11882
|
+
err.caller = "git.add";
|
|
11883
|
+
throw err;
|
|
11884
|
+
}
|
|
11885
|
+
}
|
|
11886
|
+
function isTracked(index, filepath) {
|
|
11887
|
+
if (index.has({ filepath })) return true;
|
|
11888
|
+
const prefix = `${filepath}/`;
|
|
11889
|
+
for (const entry of index.entriesMap.keys()) if (entry.startsWith(prefix)) return true;
|
|
11890
|
+
return false;
|
|
11891
|
+
}
|
|
11892
|
+
async function addToIndex({ dir, gitdir, fs, filepath, index, force, parallel, autocrlf }) {
|
|
11893
|
+
filepath = Array.isArray(filepath) ? filepath : [filepath];
|
|
11894
|
+
const promises = filepath.map(async (currentFilepath) => {
|
|
11895
|
+
if (!force && !isTracked(index, currentFilepath)) {
|
|
11896
|
+
if (await GitIgnoreManager.isIgnored({
|
|
11897
|
+
fs,
|
|
11898
|
+
dir,
|
|
11899
|
+
gitdir,
|
|
11900
|
+
filepath: currentFilepath
|
|
11901
|
+
})) return;
|
|
11902
|
+
}
|
|
11903
|
+
const stats = await fs.lstat(join(dir, currentFilepath));
|
|
11904
|
+
if (!stats) throw new NotFoundError(currentFilepath);
|
|
11905
|
+
if (stats.isDirectory()) {
|
|
11906
|
+
const children = await fs.readdir(join(dir, currentFilepath));
|
|
11907
|
+
if (parallel) {
|
|
11908
|
+
const promises = children.map((child) => addToIndex({
|
|
11909
|
+
dir,
|
|
11910
|
+
gitdir,
|
|
11911
|
+
fs,
|
|
11912
|
+
filepath: [join(currentFilepath, child)],
|
|
11913
|
+
index,
|
|
11914
|
+
force,
|
|
11915
|
+
parallel,
|
|
11916
|
+
autocrlf
|
|
11917
|
+
}));
|
|
11918
|
+
await Promise.all(promises);
|
|
11919
|
+
} else for (const child of children) await addToIndex({
|
|
11920
|
+
dir,
|
|
11921
|
+
gitdir,
|
|
11922
|
+
fs,
|
|
11923
|
+
filepath: [join(currentFilepath, child)],
|
|
11924
|
+
index,
|
|
11925
|
+
force,
|
|
11926
|
+
parallel,
|
|
11927
|
+
autocrlf
|
|
11928
|
+
});
|
|
11929
|
+
} else {
|
|
11930
|
+
const object = stats.isSymbolicLink() ? await fs.readlink(join(dir, currentFilepath)).then(posixifyPathBuffer) : await fs.read(join(dir, currentFilepath), { autocrlf });
|
|
11931
|
+
if (object === null) throw new NotFoundError(currentFilepath);
|
|
11932
|
+
const oid = await _writeObject({
|
|
11933
|
+
fs,
|
|
11934
|
+
gitdir,
|
|
11935
|
+
type: "blob",
|
|
11936
|
+
object
|
|
11937
|
+
});
|
|
11938
|
+
index.insert({
|
|
11939
|
+
filepath: currentFilepath,
|
|
11940
|
+
stats,
|
|
11941
|
+
oid
|
|
11942
|
+
});
|
|
11943
|
+
}
|
|
11944
|
+
});
|
|
11945
|
+
const settledPromises = await Promise.allSettled(promises);
|
|
11946
|
+
const rejectedPromises = settledPromises.filter((settle) => settle.status === "rejected").map((settle) => settle.reason);
|
|
11947
|
+
if (rejectedPromises.length > 1) throw new MultipleGitError(rejectedPromises);
|
|
11948
|
+
if (rejectedPromises.length === 1) throw rejectedPromises[0];
|
|
11949
|
+
return settledPromises.filter((settle) => settle.status === "fulfilled" && settle.value).map((settle) => settle.value);
|
|
11950
|
+
}
|
|
11825
11951
|
async function hasObjectLoose({ fs, gitdir, oid }) {
|
|
11826
11952
|
const source = `objects/${oid.slice(0, 2)}/${oid.slice(2)}`;
|
|
11827
11953
|
return fs.exists(`${gitdir}/${source}`);
|
|
@@ -14037,6 +14163,81 @@ async function _fetch({ fs, cache, http, onProgress, onMessage, onAuth, onAuthSu
|
|
|
14037
14163
|
return res;
|
|
14038
14164
|
}
|
|
14039
14165
|
/**
|
|
14166
|
+
* Create a new commit
|
|
14167
|
+
*
|
|
14168
|
+
* @param {Object} args
|
|
14169
|
+
* @param {FsClient} args.fs - a file system implementation
|
|
14170
|
+
* @param {SignCallback} [args.onSign] - a PGP signing implementation
|
|
14171
|
+
* @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
|
|
14172
|
+
* @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
|
|
14173
|
+
* @param {string} [args.message] - The commit message to use. Required, unless `amend === true`
|
|
14174
|
+
* @param {Object} [args.author] - The details about the author.
|
|
14175
|
+
* @param {string} [args.author.name] - Default is `user.name` config.
|
|
14176
|
+
* @param {string} [args.author.email] - Default is `user.email` config.
|
|
14177
|
+
* @param {number} [args.author.timestamp=Math.floor(Date.now()/1000)] - Set the author timestamp field. This is the integer number of seconds since the Unix epoch (1970-01-01 00:00:00).
|
|
14178
|
+
* @param {number} [args.author.timezoneOffset] - Set the author timezone offset field. This is the difference, in minutes, from the current timezone to UTC. Default is `(new Date()).getTimezoneOffset()`.
|
|
14179
|
+
* @param {Object} [args.committer = author] - The details about the commit committer, in the same format as the author parameter. If not specified, the author details are used.
|
|
14180
|
+
* @param {string} [args.committer.name] - Default is `user.name` config.
|
|
14181
|
+
* @param {string} [args.committer.email] - Default is `user.email` config.
|
|
14182
|
+
* @param {number} [args.committer.timestamp=Math.floor(Date.now()/1000)] - Set the committer timestamp field. This is the integer number of seconds since the Unix epoch (1970-01-01 00:00:00).
|
|
14183
|
+
* @param {number} [args.committer.timezoneOffset] - Set the committer timezone offset field. This is the difference, in minutes, from the current timezone to UTC. Default is `(new Date()).getTimezoneOffset()`.
|
|
14184
|
+
* @param {string} [args.signingKey] - Sign the tag object using this private PGP key.
|
|
14185
|
+
* @param {boolean} [args.amend = false] - If true, replaces the last commit pointed to by `ref` with a new commit.
|
|
14186
|
+
* @param {boolean} [args.dryRun = false] - If true, simulates making a commit so you can test whether it would succeed. Implies `noUpdateBranch`.
|
|
14187
|
+
* @param {boolean} [args.noUpdateBranch = false] - If true, does not update the branch pointer after creating the commit.
|
|
14188
|
+
* @param {boolean} [args.disallowEmpty = false] - If true, throws an error instead of creating a commit with no staged changes.
|
|
14189
|
+
* @param {string} [args.ref] - The fully expanded name of the branch to commit to. Default is the current branch pointed to by HEAD. (TODO: fix it so it can expand branch names without throwing if the branch doesn't exist yet.)
|
|
14190
|
+
* @param {string[]} [args.parent] - The SHA-1 object ids of the commits to use as parents. If not specified, the commit pointed to by `ref` is used.
|
|
14191
|
+
* @param {string} [args.tree] - The SHA-1 object id of the tree to use. If not specified, a new tree object is created from the current git index.
|
|
14192
|
+
* @param {object} [args.cache] - a [cache](cache.md) object
|
|
14193
|
+
*
|
|
14194
|
+
* @returns {Promise<string>} Resolves successfully with the SHA-1 object id of the newly created commit.
|
|
14195
|
+
*
|
|
14196
|
+
* @example
|
|
14197
|
+
* let sha = await git.commit({
|
|
14198
|
+
* fs,
|
|
14199
|
+
* dir: '/tutorial',
|
|
14200
|
+
* author: {
|
|
14201
|
+
* name: 'Mr. Test',
|
|
14202
|
+
* email: 'mrtest@example.com',
|
|
14203
|
+
* },
|
|
14204
|
+
* message: 'Added the a.txt file'
|
|
14205
|
+
* })
|
|
14206
|
+
* console.log(sha)
|
|
14207
|
+
*
|
|
14208
|
+
*/
|
|
14209
|
+
async function commit({ fs: _fs, onSign, dir, gitdir = join(dir, ".git"), message, author, committer, signingKey, amend = false, dryRun = false, noUpdateBranch = false, disallowEmpty = false, ref, parent, tree, cache = {} }) {
|
|
14210
|
+
try {
|
|
14211
|
+
assertParameter("fs", _fs);
|
|
14212
|
+
if (!amend) assertParameter("message", message);
|
|
14213
|
+
if (signingKey) assertParameter("onSign", onSign);
|
|
14214
|
+
const fs = new FileSystem(_fs);
|
|
14215
|
+
return await _commit({
|
|
14216
|
+
fs,
|
|
14217
|
+
cache,
|
|
14218
|
+
onSign,
|
|
14219
|
+
gitdir: await discoverGitdir({
|
|
14220
|
+
fsp: fs,
|
|
14221
|
+
dotgit: gitdir
|
|
14222
|
+
}),
|
|
14223
|
+
message,
|
|
14224
|
+
author,
|
|
14225
|
+
committer,
|
|
14226
|
+
signingKey,
|
|
14227
|
+
amend,
|
|
14228
|
+
dryRun,
|
|
14229
|
+
noUpdateBranch,
|
|
14230
|
+
disallowEmpty,
|
|
14231
|
+
ref,
|
|
14232
|
+
parent,
|
|
14233
|
+
tree
|
|
14234
|
+
});
|
|
14235
|
+
} catch (err) {
|
|
14236
|
+
err.caller = "git.commit";
|
|
14237
|
+
throw err;
|
|
14238
|
+
}
|
|
14239
|
+
}
|
|
14240
|
+
/**
|
|
14040
14241
|
* Get the name of the branch currently pointed to by .git/HEAD
|
|
14041
14242
|
*
|
|
14042
14243
|
* @param {Object} args
|
|
@@ -15795,6 +15996,148 @@ async function readCommit({ fs, dir, gitdir = join(dir, ".git"), oid, cache = {}
|
|
|
15795
15996
|
}
|
|
15796
15997
|
}
|
|
15797
15998
|
/**
|
|
15999
|
+
* Remove a file from the git index (aka staging area)
|
|
16000
|
+
*
|
|
16001
|
+
* Note that this does NOT delete the file in the working directory.
|
|
16002
|
+
*
|
|
16003
|
+
* @param {object} args
|
|
16004
|
+
* @param {FsClient} args.fs - a file system client
|
|
16005
|
+
* @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
|
|
16006
|
+
* @param {string} [args.gitdir=join(dir, '.git')] - [required] The [git directory](dir-vs-gitdir.md) path
|
|
16007
|
+
* @param {string|string[]} args.filepath - The path to the file to remove from the index, or an array of paths
|
|
16008
|
+
* @param {object} [args.cache] - a [cache](cache.md) object
|
|
16009
|
+
*
|
|
16010
|
+
* @returns {Promise<void>} Resolves successfully once the git index has been updated
|
|
16011
|
+
*
|
|
16012
|
+
* @example
|
|
16013
|
+
* await git.remove({ fs, dir: '/tutorial', filepath: 'README.md' })
|
|
16014
|
+
* console.log('done')
|
|
16015
|
+
*
|
|
16016
|
+
* @example
|
|
16017
|
+
* // remove several files from the index with a single write of the index
|
|
16018
|
+
* await git.remove({ fs, dir: '/tutorial', filepath: ['README.md', 'docs/old.md'] })
|
|
16019
|
+
* console.log('done')
|
|
16020
|
+
*
|
|
16021
|
+
*/
|
|
16022
|
+
async function remove({ fs: _fs, dir, gitdir = join(dir, ".git"), filepath, cache = {} }) {
|
|
16023
|
+
try {
|
|
16024
|
+
assertParameter("fs", _fs);
|
|
16025
|
+
assertParameter("gitdir", gitdir);
|
|
16026
|
+
assertParameter("filepath", filepath);
|
|
16027
|
+
const fsp = new FileSystem(_fs);
|
|
16028
|
+
const updatedGitdir = await discoverGitdir({
|
|
16029
|
+
fsp,
|
|
16030
|
+
dotgit: gitdir
|
|
16031
|
+
});
|
|
16032
|
+
await GitIndexManager.acquire({
|
|
16033
|
+
fs: fsp,
|
|
16034
|
+
gitdir: updatedGitdir,
|
|
16035
|
+
cache
|
|
16036
|
+
}, async function(index) {
|
|
16037
|
+
const filepaths = Array.isArray(filepath) ? filepath : [filepath];
|
|
16038
|
+
for (const currentFilepath of filepaths) index.delete({ filepath: currentFilepath });
|
|
16039
|
+
});
|
|
16040
|
+
} catch (err) {
|
|
16041
|
+
err.caller = "git.remove";
|
|
16042
|
+
throw err;
|
|
16043
|
+
}
|
|
16044
|
+
}
|
|
16045
|
+
async function hashObject$1({ gitdir, type, object }) {
|
|
16046
|
+
return shasum(GitObject.wrap({
|
|
16047
|
+
type,
|
|
16048
|
+
object
|
|
16049
|
+
}));
|
|
16050
|
+
}
|
|
16051
|
+
/**
|
|
16052
|
+
* Reset a file in the git index (aka staging area)
|
|
16053
|
+
*
|
|
16054
|
+
* Note that this does NOT modify the file in the working directory.
|
|
16055
|
+
*
|
|
16056
|
+
* @param {object} args
|
|
16057
|
+
* @param {FsClient} args.fs - a file system client
|
|
16058
|
+
* @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
|
|
16059
|
+
* @param {string} [args.gitdir=join(dir, '.git')] - [required] The [git directory](dir-vs-gitdir.md) path
|
|
16060
|
+
* @param {string} args.filepath - The path to the file to reset in the index
|
|
16061
|
+
* @param {string} [args.ref = 'HEAD'] - A ref to the commit to use
|
|
16062
|
+
* @param {object} [args.cache] - a [cache](cache.md) object
|
|
16063
|
+
*
|
|
16064
|
+
* @returns {Promise<void>} Resolves successfully once the git index has been updated
|
|
16065
|
+
*
|
|
16066
|
+
* @example
|
|
16067
|
+
* await git.resetIndex({ fs, dir: '/tutorial', filepath: 'README.md' })
|
|
16068
|
+
* console.log('done')
|
|
16069
|
+
*
|
|
16070
|
+
*/
|
|
16071
|
+
async function resetIndex({ fs: _fs, dir, gitdir = join(dir, ".git"), filepath, ref, cache = {} }) {
|
|
16072
|
+
try {
|
|
16073
|
+
assertParameter("fs", _fs);
|
|
16074
|
+
assertParameter("gitdir", gitdir);
|
|
16075
|
+
assertParameter("filepath", filepath);
|
|
16076
|
+
const fs = new FileSystem(_fs);
|
|
16077
|
+
const updatedGitdir = await discoverGitdir({
|
|
16078
|
+
fsp: fs,
|
|
16079
|
+
dotgit: gitdir
|
|
16080
|
+
});
|
|
16081
|
+
let oid;
|
|
16082
|
+
let workdirOid;
|
|
16083
|
+
try {
|
|
16084
|
+
oid = await GitRefManager.resolve({
|
|
16085
|
+
fs,
|
|
16086
|
+
gitdir: updatedGitdir,
|
|
16087
|
+
ref: ref || "HEAD"
|
|
16088
|
+
});
|
|
16089
|
+
} catch (e) {
|
|
16090
|
+
if (ref || !(e instanceof NotFoundError)) throw e;
|
|
16091
|
+
}
|
|
16092
|
+
if (oid) try {
|
|
16093
|
+
oid = await resolveFilepath({
|
|
16094
|
+
fs,
|
|
16095
|
+
cache,
|
|
16096
|
+
gitdir: updatedGitdir,
|
|
16097
|
+
oid,
|
|
16098
|
+
filepath
|
|
16099
|
+
});
|
|
16100
|
+
} catch (e) {
|
|
16101
|
+
if (!(e instanceof NotFoundError) && !(e instanceof InvalidFilepathError) && !(e instanceof ObjectTypeError && e.data.filepath)) throw e;
|
|
16102
|
+
oid = null;
|
|
16103
|
+
}
|
|
16104
|
+
let stats = {
|
|
16105
|
+
ctime: /* @__PURE__ */ new Date(0),
|
|
16106
|
+
mtime: /* @__PURE__ */ new Date(0),
|
|
16107
|
+
dev: 0,
|
|
16108
|
+
ino: 0,
|
|
16109
|
+
mode: 0,
|
|
16110
|
+
uid: 0,
|
|
16111
|
+
gid: 0,
|
|
16112
|
+
size: 0
|
|
16113
|
+
};
|
|
16114
|
+
const object = dir && await fs.read(join(dir, filepath));
|
|
16115
|
+
if (object) {
|
|
16116
|
+
workdirOid = await hashObject$1({
|
|
16117
|
+
gitdir: updatedGitdir,
|
|
16118
|
+
type: "blob",
|
|
16119
|
+
object
|
|
16120
|
+
});
|
|
16121
|
+
if (oid === workdirOid) stats = await fs.lstat(join(dir, filepath));
|
|
16122
|
+
}
|
|
16123
|
+
await GitIndexManager.acquire({
|
|
16124
|
+
fs,
|
|
16125
|
+
gitdir: updatedGitdir,
|
|
16126
|
+
cache
|
|
16127
|
+
}, async function(index) {
|
|
16128
|
+
index.delete({ filepath });
|
|
16129
|
+
if (oid) index.insert({
|
|
16130
|
+
filepath,
|
|
16131
|
+
stats,
|
|
16132
|
+
oid
|
|
16133
|
+
});
|
|
16134
|
+
});
|
|
16135
|
+
} catch (err) {
|
|
16136
|
+
err.caller = "git.reset";
|
|
16137
|
+
throw err;
|
|
16138
|
+
}
|
|
16139
|
+
}
|
|
16140
|
+
/**
|
|
15798
16141
|
* Get the value of a symbolic ref or resolve a ref to its SHA-1 object id
|
|
15799
16142
|
*
|
|
15800
16143
|
* @param {object} args
|
|
@@ -16494,7 +16837,7 @@ async function readOptional(fs, path) {
|
|
|
16494
16837
|
}
|
|
16495
16838
|
var sameBytes = (left, right) => left === right || Boolean(left && right && left.length === right.length && left.every((byte, i) => byte === right[i]));
|
|
16496
16839
|
// src/mutation-fs.ts
|
|
16497
|
-
var missing = (error) => {
|
|
16840
|
+
var missing$1 = (error) => {
|
|
16498
16841
|
var _errorCode;
|
|
16499
16842
|
return ["ENOENT", "ENOTDIR"].includes((_errorCode = errorCode(error)) !== null && _errorCode !== void 0 ? _errorCode : "");
|
|
16500
16843
|
};
|
|
@@ -16538,7 +16881,7 @@ function createMutationFs(root, worktree) {
|
|
|
16538
16881
|
time: stat.mtimeMs
|
|
16539
16882
|
};
|
|
16540
16883
|
} catch (error) {
|
|
16541
|
-
if (missing(error)) return null;
|
|
16884
|
+
if (missing$1(error)) return null;
|
|
16542
16885
|
throw error;
|
|
16543
16886
|
}
|
|
16544
16887
|
}
|
|
@@ -16549,7 +16892,7 @@ function createMutationFs(root, worktree) {
|
|
|
16549
16892
|
try {
|
|
16550
16893
|
return (await base.promises.stat(path)).isDirectory() ? "directory" : "file";
|
|
16551
16894
|
} catch (error) {
|
|
16552
|
-
if (!missing(error)) throw error;
|
|
16895
|
+
if (!missing$1(error)) throw error;
|
|
16553
16896
|
}
|
|
16554
16897
|
}
|
|
16555
16898
|
async function stage(path, after) {
|
|
@@ -16570,7 +16913,7 @@ function createMutationFs(root, worktree) {
|
|
|
16570
16913
|
try {
|
|
16571
16914
|
for (const entry of await base.promises.readdir(path)) names.add(entry);
|
|
16572
16915
|
} catch (error) {
|
|
16573
|
-
if (!missing(error)) throw error;
|
|
16916
|
+
if (!missing$1(error)) throw error;
|
|
16574
16917
|
}
|
|
16575
16918
|
for (const change of changes.values()) {
|
|
16576
16919
|
if (parent(change.path) !== path) continue;
|
|
@@ -16824,6 +17167,96 @@ async function runRemoteOperation(root, operation, getSession, guard, signal) {
|
|
|
16824
17167
|
};
|
|
16825
17168
|
return ((_globalThis$navigator = globalThis.navigator) === null || _globalThis$navigator === void 0 ? void 0 : _globalThis$navigator.locks) ? navigator.locks.request("tw-plus-git-mutation", execute) : execute();
|
|
16826
17169
|
}
|
|
17170
|
+
// src/local-operations.ts
|
|
17171
|
+
init_objectSpread2();
|
|
17172
|
+
var missing = (error) => {
|
|
17173
|
+
var _errorCode;
|
|
17174
|
+
return [
|
|
17175
|
+
"ENOENT",
|
|
17176
|
+
"ENOTDIR",
|
|
17177
|
+
"NotFoundError"
|
|
17178
|
+
].includes((_errorCode = errorCode(error)) !== null && _errorCode !== void 0 ? _errorCode : "");
|
|
17179
|
+
};
|
|
17180
|
+
/**
|
|
17181
|
+
* Stages, unstages or commits in the granted repository. Like pull, Git's writes are collected first and applied
|
|
17182
|
+
* after the files they replace are checked again; a failure part way puts back what was applied. Only `.git/index`,
|
|
17183
|
+
* objects and the current branch ref are written. The work tree and `.git/config` are never written.
|
|
17184
|
+
*/
|
|
17185
|
+
async function runLocalOperation(root, operation, guard) {
|
|
17186
|
+
var _globalThis$navigator;
|
|
17187
|
+
const execute = async () => {
|
|
17188
|
+
guard();
|
|
17189
|
+
const transaction = createMutationFs(root, false);
|
|
17190
|
+
const options = {
|
|
17191
|
+
fs: transaction.fs,
|
|
17192
|
+
dir: "/",
|
|
17193
|
+
gitdir: "/.git",
|
|
17194
|
+
cache: {}
|
|
17195
|
+
};
|
|
17196
|
+
const read = createReadOnlyFs(root).promises;
|
|
17197
|
+
const exists = async (path) => {
|
|
17198
|
+
try {
|
|
17199
|
+
await read.stat(path);
|
|
17200
|
+
return true;
|
|
17201
|
+
} catch (error) {
|
|
17202
|
+
if (missing(error)) return false;
|
|
17203
|
+
throw error;
|
|
17204
|
+
}
|
|
17205
|
+
};
|
|
17206
|
+
for (const path of [
|
|
17207
|
+
"/.git/index.lock",
|
|
17208
|
+
"/.git/MERGE_HEAD",
|
|
17209
|
+
"/.git/CHERRY_PICK_HEAD",
|
|
17210
|
+
"/.git/REVERT_HEAD",
|
|
17211
|
+
"/.git/rebase-merge",
|
|
17212
|
+
"/.git/rebase-apply"
|
|
17213
|
+
]) if (await exists(path)) throw new GitPanelError("changed");
|
|
17214
|
+
if (operation.kind === "stage") {
|
|
17215
|
+
const modes = /* @__PURE__ */ new Map();
|
|
17216
|
+
await walk(_objectSpread2(_objectSpread2({}, options), {}, {
|
|
17217
|
+
trees: [STAGE()],
|
|
17218
|
+
map: async (path, [entry]) => {
|
|
17219
|
+
if (entry && path !== "." && await entry.type() === "blob") modes.set(path, await entry.mode());
|
|
17220
|
+
return true;
|
|
17221
|
+
}
|
|
17222
|
+
}));
|
|
17223
|
+
for (const path of operation.paths) {
|
|
17224
|
+
pathParts(path);
|
|
17225
|
+
const mode = modes.get(path);
|
|
17226
|
+
if (mode !== void 0 && mode !== 33188) throw new GitPanelError("unsupported");
|
|
17227
|
+
if (await exists("/" + path)) await add(_objectSpread2(_objectSpread2({}, options), {}, { filepath: path }));
|
|
17228
|
+
else await remove(_objectSpread2(_objectSpread2({}, options), {}, { filepath: path }));
|
|
17229
|
+
}
|
|
17230
|
+
} else if (operation.kind === "unstage") for (const path of operation.paths) {
|
|
17231
|
+
pathParts(path);
|
|
17232
|
+
await resetIndex(_objectSpread2(_objectSpread2({}, options), {}, { filepath: path }));
|
|
17233
|
+
}
|
|
17234
|
+
else {
|
|
17235
|
+
var _await$configured, _operation$author, _await$configured2, _operation$author2;
|
|
17236
|
+
const message = operation.message.trim();
|
|
17237
|
+
if (!message) throw new GitPanelError("commitMessage");
|
|
17238
|
+
if (!await currentBranch(options)) throw new GitPanelError("unsupported");
|
|
17239
|
+
const configured = async (path) => {
|
|
17240
|
+
const value = await getConfig(_objectSpread2(_objectSpread2({}, options), {}, { path }));
|
|
17241
|
+
return typeof value === "string" && value.trim() ? value : void 0;
|
|
17242
|
+
};
|
|
17243
|
+
const name = (_await$configured = await configured("user.name")) !== null && _await$configured !== void 0 ? _await$configured : (_operation$author = operation.author) === null || _operation$author === void 0 || (_operation$author = _operation$author.name) === null || _operation$author === void 0 ? void 0 : _operation$author.trim();
|
|
17244
|
+
const email = (_await$configured2 = await configured("user.email")) !== null && _await$configured2 !== void 0 ? _await$configured2 : (_operation$author2 = operation.author) === null || _operation$author2 === void 0 || (_operation$author2 = _operation$author2.email) === null || _operation$author2 === void 0 ? void 0 : _operation$author2.trim();
|
|
17245
|
+
if (!name || !email) throw new GitPanelError("author");
|
|
17246
|
+
if (!(await statusMatrix(_objectSpread2(_objectSpread2({}, options), {}, { refresh: false }))).some(([, head, , stage]) => head !== stage)) throw new GitPanelError("nothingStaged");
|
|
17247
|
+
await commit(_objectSpread2(_objectSpread2({}, options), {}, {
|
|
17248
|
+
message: message + "\n",
|
|
17249
|
+
author: {
|
|
17250
|
+
name,
|
|
17251
|
+
email
|
|
17252
|
+
}
|
|
17253
|
+
}));
|
|
17254
|
+
}
|
|
17255
|
+
guard();
|
|
17256
|
+
await transaction.commit(guard);
|
|
17257
|
+
};
|
|
17258
|
+
return ((_globalThis$navigator = globalThis.navigator) === null || _globalThis$navigator === void 0 ? void 0 : _globalThis$navigator.locks) ? navigator.locks.request("tw-plus-git-mutation", execute) : execute();
|
|
17259
|
+
}
|
|
16827
17260
|
// src/repository.ts
|
|
16828
17261
|
init_objectSpread2();
|
|
16829
17262
|
var projectSourceLimit = 16777216;
|
|
@@ -17437,6 +17870,8 @@ async function openRepository(root) {
|
|
|
17437
17870
|
throw error;
|
|
17438
17871
|
}
|
|
17439
17872
|
}),
|
|
17873
|
+
/** Stages, unstages or commits; see `runLocalOperation`. `guard` throws to stop before each write. */
|
|
17874
|
+
localOperation: (operation, guard) => serial(() => runLocalOperation(root, operation, guard)),
|
|
17440
17875
|
read: (scope) => serial(() => read(scope)),
|
|
17441
17876
|
readDiff: (snapshot, change) => serial(() => readDiff(snapshot, change)),
|
|
17442
17877
|
historyChanges: (snapshot, oid) => serial(() => historyChanges(snapshot, oid)),
|
package/dist/repository.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { LocalOperation } from './local-operations.js';
|
|
1
2
|
import type { GetRemoteSession, RemoteOperation } from './remote-operations.js';
|
|
2
3
|
import type { ProjectAssetLoader } from './project-assets.js';
|
|
3
4
|
export { createReadOnlyFs, GitPanelError } from './filesystem.js';
|
|
@@ -84,6 +85,8 @@ export declare function openRepository(root: FileSystemDirectoryHandle): Promise
|
|
|
84
85
|
probe: (scope?: string) => Promise<string>;
|
|
85
86
|
readOrigin: () => Promise<any>;
|
|
86
87
|
remoteOperation: (operation: RemoteOperation, getSession: GetRemoteSession, guard: () => void, signal?: AbortSignal) => Promise<void>;
|
|
88
|
+
/** Stages, unstages or commits; see `runLocalOperation`. `guard` throws to stop before each write. */
|
|
89
|
+
localOperation: (operation: LocalOperation, guard: () => void) => Promise<void>;
|
|
87
90
|
read: (scope?: string) => Promise<Snapshot>;
|
|
88
91
|
readDiff: (snapshot: Snapshot, change: FileChange) => Promise<FileDiff>;
|
|
89
92
|
historyChanges: (snapshot: Snapshot, oid: string) => Promise<FileChange[]>;
|
package/dist/repository.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { r as GitPanelError, i as createReadOnlyFs, t as openRepository, n as upstreamWalkLimit } from "./repository-
|
|
1
|
+
export { r as GitPanelError, i as createReadOnlyFs, t as openRepository, n as upstreamWalkLimit } from "./repository-Cx-mxT9l.js";
|
package/dist/style.css
CHANGED
|
@@ -148,6 +148,10 @@
|
|
|
148
148
|
.twgit-panel aside li { overflow-wrap: anywhere; }
|
|
149
149
|
.twgit-panel summary { cursor: pointer; }
|
|
150
150
|
.twgit-panel .twgit-file { width: 100%; border: 0; margin: 1px 0; }
|
|
151
|
+
.twgit-panel .twgit-stage { font-size: 11px; margin: 0 0 4px; }
|
|
152
|
+
.twgit-commit { display: grid; gap: 6px; margin: 10px 0; padding: 8px; border: 1px solid color-mix(in srgb, CanvasText 18%, Canvas); border-radius: 6px; }
|
|
153
|
+
.twgit-commit label { display: grid; gap: 4px; font-size: 12px; }
|
|
154
|
+
.twgit-commit textarea { font: inherit; resize: vertical; min-height: 3em; }
|
|
151
155
|
.twgit-diff { padding: 0 16px 16px; min-width: 0; overflow: auto; }
|
|
152
156
|
.twgit-diff .diff { font-size: 12px; }
|
|
153
157
|
.twgit-history { list-style: none; margin: 0; padding: 0; }
|
package/package.json
CHANGED
package/src/filesystem.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export class GitPanelError extends Error {
|
|
2
2
|
constructor(public code: 'invalid' | 'changed' | 'large' | 'unsupported' | 'pullUnsafe' | 'diverged' |
|
|
3
|
-
'session' | 'pushUnknown' | 'pushRefresh' | 'rollbackFailed' | 'remoteFailed'
|
|
3
|
+
'session' | 'pushUnknown' | 'pushRefresh' | 'rollbackFailed' | 'remoteFailed' |
|
|
4
|
+
'saveFirst' | 'author' | 'nothingStaged' | 'commitMessage', message: string = code) {
|
|
4
5
|
super(message);
|
|
5
6
|
this.name = 'GitPanelError';
|
|
6
7
|
}
|
package/src/index.tsx
CHANGED
|
@@ -10,6 +10,7 @@ import {GitPanelError} from './filesystem.js';
|
|
|
10
10
|
import {ConnectionSettings} from './connection-settings.js';
|
|
11
11
|
import type {GitConnectionSettings} from './connection-settings.js';
|
|
12
12
|
import type {RemoteOperation} from './remote-operations.js';
|
|
13
|
+
import type {GitAuthor, LocalOperation} from './local-operations.js';
|
|
13
14
|
import {PullRequestReviewPanel} from './pull-request-panel.js';
|
|
14
15
|
import type {ProjectBlockRenderer} from './scratch-block-diff-renderer.js';
|
|
15
16
|
import type {ScratchBlockLocationWorkspace} from './block-diff-location.js';
|
|
@@ -25,6 +26,7 @@ export {openRepository, createReadOnlyFs, GitPanelError} from './repository.js';
|
|
|
25
26
|
export {openProjectComparison} from './project-comparison.js';
|
|
26
27
|
export type {ProjectComparisonAction, ProjectComparisonOptions, ProjectComparisonSection} from './project-comparison.js';
|
|
27
28
|
export type {Repository, Snapshot, FileChange, FileDiff} from './repository.js';
|
|
29
|
+
export type {GitAuthor, LocalOperation} from './local-operations.js';
|
|
28
30
|
|
|
29
31
|
export interface GitHost {
|
|
30
32
|
connectionSettings?: GitConnectionSettings;
|
|
@@ -42,6 +44,11 @@ export interface GitHost {
|
|
|
42
44
|
openTargetCode?(targetId: string): void | Promise<void>;
|
|
43
45
|
/** The Editor's code workspace as it is now. */
|
|
44
46
|
getWorkspace?(): ScratchBlockLocationWorkspace | null | Promise<ScratchBlockLocationWorkspace | null>;
|
|
47
|
+
/**
|
|
48
|
+
* Offers staging, unstaging and commits. Without it the panel only reads. `author` is used only where the
|
|
49
|
+
* repository's Git config has no user.name or user.email; the panel never writes the config.
|
|
50
|
+
*/
|
|
51
|
+
localWrite?: {author?(): GitAuthor | undefined};
|
|
45
52
|
}
|
|
46
53
|
export interface GitPanelRequest {
|
|
47
54
|
reference: {providerId: string; documentId: string};
|
|
@@ -112,22 +119,24 @@ function GitPanel({vm, document, host, controller}: {vm: object; document: Docum
|
|
|
112
119
|
if (automatic) background.current = new Promise(resolve => {settle = resolve;});
|
|
113
120
|
running.current = true;
|
|
114
121
|
const ticket = ++epoch.current;
|
|
115
|
-
// An automatic refresh keeps the current error and notice, such as an uncertain push result.
|
|
116
|
-
|
|
122
|
+
// An automatic refresh keeps the current error and notice, such as an uncertain push result. It also
|
|
123
|
+
// leaves the controls enabled: an operation clicked meanwhile waits for it above, whereas a button
|
|
124
|
+
// that turned disabled under the pointer would swallow the click.
|
|
125
|
+
if (!automatic) setState(previous => ({...previous, busy: true, error: null, notice: undefined}));
|
|
117
126
|
try {
|
|
118
127
|
const update = await operation();
|
|
119
|
-
if (epoch.current === ticket) setState(previous => ({...previous, ...update, busy: false}));
|
|
128
|
+
if (epoch.current === ticket) setState(previous => ({...previous, ...update, busy: automatic ? previous.busy : false}));
|
|
120
129
|
} catch (error) {
|
|
121
130
|
if (epoch.current !== ticket) return;
|
|
122
131
|
const code = error instanceof GitPanelError ? error.code : 'failed';
|
|
123
132
|
// A background read that met another tool still writing tries again on the next check, without an alert.
|
|
124
133
|
if (automatic && code === 'changed') {
|
|
125
134
|
handled.current = null;
|
|
126
|
-
setState(previous => ({...previous, busy: false}));
|
|
127
135
|
return;
|
|
128
136
|
}
|
|
129
137
|
const message = labels[code as keyof typeof labels] || labels.failed;
|
|
130
|
-
setState(previous => ({...previous, error: automatic && previous.error ? previous.error : message,
|
|
138
|
+
setState(previous => ({...previous, error: automatic && previous.error ? previous.error : message,
|
|
139
|
+
busy: automatic ? previous.busy : false, diff: null}));
|
|
131
140
|
} finally {
|
|
132
141
|
running.current = false;
|
|
133
142
|
// A result dropped by a newer load still ends the busy state, unless a refresh follows at once.
|
|
@@ -335,6 +344,22 @@ function GitPanel({vm, document, host, controller}: {vm: object; document: Docum
|
|
|
335
344
|
notice: operation === 'pull' && current.current.project !== null ? labels.pulledProject : labels.remoteDone};
|
|
336
345
|
});
|
|
337
346
|
};
|
|
347
|
+
const [message, setMessage] = useState('');
|
|
348
|
+
const localOperation = (operation: LocalOperation, notice?: string) => {
|
|
349
|
+
const repository = current.current.repository;
|
|
350
|
+
if (!repository || !host.localWrite) return Promise.resolve();
|
|
351
|
+
return run(async () => {
|
|
352
|
+
const ticket = epoch.current;
|
|
353
|
+
await repository.localOperation(operation, () => {
|
|
354
|
+
if (epoch.current !== ticket) throw new GitPanelError('changed');
|
|
355
|
+
// Staging records the saved files; edits still in the Editor would be left out without a word.
|
|
356
|
+
if (operation.kind === 'stage' && host.isProjectDirty()) throw new GitPanelError('saveFirst');
|
|
357
|
+
});
|
|
358
|
+
if (operation.kind === 'commit') setMessage('');
|
|
359
|
+
const snapshot = await repository.read(current.current.project ?? '');
|
|
360
|
+
return {snapshot, changes: snapshot.changes, selected: null, diff: null, commit: null, notice};
|
|
361
|
+
});
|
|
362
|
+
};
|
|
338
363
|
const focusTrap = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
339
364
|
if (event.key === 'Escape') {event.stopPropagation(); closePanel(); return;}
|
|
340
365
|
if (event.key !== 'Tab') return;
|
|
@@ -350,6 +375,11 @@ function GitPanel({vm, document, host, controller}: {vm: object; document: Docum
|
|
|
350
375
|
state.repository.assetLoader(state.snapshot, state.selected) : undefined, [state.repository, state.snapshot, state.selected]);
|
|
351
376
|
if (!shown) return null;
|
|
352
377
|
const groups: ChangeGroup[] = state.commit ? ['commit'] : ['staged', 'unstaged', 'untracked'];
|
|
378
|
+
// Writing works on the current changes, not on a record from the history, and only on a branch.
|
|
379
|
+
const writable = !!host.localWrite && !state.commit && !!state.snapshot?.branch;
|
|
380
|
+
const unrecorded = [...new Set(state.changes.filter(change => change.group === 'unstaged' || change.group === 'untracked')
|
|
381
|
+
.map(change => change.path))];
|
|
382
|
+
const staged = state.changes.some(change => change.group === 'staged');
|
|
353
383
|
const projectList = typeof projectChanges === 'object' ? projectChanges : null;
|
|
354
384
|
// The open project matches the working tree, so only the working-tree side of its project file can be located.
|
|
355
385
|
const projectFile = state.project === null ? null : [state.project, 'project.source.json'].filter(Boolean).join('/');
|
|
@@ -421,6 +451,10 @@ function GitPanel({vm, document, host, controller}: {vm: object; document: Docum
|
|
|
421
451
|
aria-pressed={state.selected === change} onClick={() => openFile(change)}>
|
|
422
452
|
<small className="twgit-change-kind">{!change.after ? labels.deleted : !change.before ? labels.added : labels.modified}</small> {change.path}
|
|
423
453
|
</button>
|
|
454
|
+
{writable && <button type="button" className="twgit-stage" disabled={state.busy}
|
|
455
|
+
aria-label={`${group === 'staged' ? labels.unstage : labels.stage}: ${change.path}`}
|
|
456
|
+
onClick={() => void localOperation({kind: group === 'staged' ? 'unstage' : 'stage', paths: [change.path]})}>
|
|
457
|
+
{group === 'staged' ? labels.unstage : labels.stage}</button>}
|
|
424
458
|
</li>)}</ul>
|
|
425
459
|
</section>);
|
|
426
460
|
return projectList ? <ProjectChangeListView changes={projectList} selected={selection ? element : null}
|
|
@@ -428,6 +462,15 @@ function GitPanel({vm, document, host, controller}: {vm: object; document: Docum
|
|
|
428
462
|
fileListOpen={fileListOpen} setFileListOpen={setFileListOpen}/> : fileList;
|
|
429
463
|
})()}
|
|
430
464
|
{!state.changes.length && <p>{labels.clean}</p>}
|
|
465
|
+
{writable && <section className="twgit-commit" aria-label={labels.commitButton}>
|
|
466
|
+
<button type="button" disabled={state.busy || !unrecorded.length}
|
|
467
|
+
onClick={() => void localOperation({kind: 'stage', paths: unrecorded})}>{labels.stageAll}</button>
|
|
468
|
+
<label>{labels.commitLabel}
|
|
469
|
+
<textarea value={message} rows={3} disabled={state.busy} onChange={event => setMessage(event.currentTarget.value)}/></label>
|
|
470
|
+
<button type="button" disabled={state.busy || !staged || !message.trim()}
|
|
471
|
+
onClick={() => void localOperation({kind: 'commit', message, author: host.localWrite?.author?.()},
|
|
472
|
+
labels.committed)}>{labels.commitButton}</button>
|
|
473
|
+
</section>}
|
|
431
474
|
<h3>{labels.history}</h3>
|
|
432
475
|
<ol className="twgit-history">{state.snapshot?.commits.map(commit => <li key={commit.oid}>
|
|
433
476
|
<button disabled={state.busy} aria-pressed={state.commit === commit.oid} title={commit.oid}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import * as git from 'isomorphic-git';
|
|
2
|
+
import {createReadOnlyFs, errorCode, GitPanelError, pathParts} from './filesystem.js';
|
|
3
|
+
import {createMutationFs} from './mutation-fs.js';
|
|
4
|
+
|
|
5
|
+
/** The name and email a commit records. */
|
|
6
|
+
export type GitAuthor = {name: string; email: string};
|
|
7
|
+
export type LocalOperation =
|
|
8
|
+
| {kind: 'stage'; paths: readonly string[]}
|
|
9
|
+
| {kind: 'unstage'; paths: readonly string[]}
|
|
10
|
+
| {kind: 'commit'; message: string; author?: GitAuthor};
|
|
11
|
+
|
|
12
|
+
const missing = (error: unknown) => ['ENOENT', 'ENOTDIR', 'NotFoundError'].includes(errorCode(error) ?? '');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Stages, unstages or commits in the granted repository. Like pull, Git's writes are collected first and applied
|
|
16
|
+
* after the files they replace are checked again; a failure part way puts back what was applied. Only `.git/index`,
|
|
17
|
+
* objects and the current branch ref are written. The work tree and `.git/config` are never written.
|
|
18
|
+
*/
|
|
19
|
+
export async function runLocalOperation(root: FileSystemDirectoryHandle, operation: LocalOperation, guard: () => void) {
|
|
20
|
+
const execute = async () => {
|
|
21
|
+
guard();
|
|
22
|
+
const transaction = createMutationFs(root, false);
|
|
23
|
+
const options = {fs: transaction.fs, dir: '/', gitdir: '/.git', cache: {}};
|
|
24
|
+
const read = createReadOnlyFs(root).promises;
|
|
25
|
+
const exists = async (path: string) => {
|
|
26
|
+
try {
|
|
27
|
+
await read.stat(path);
|
|
28
|
+
return true;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (missing(error)) return false;
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
// Another tool is in the middle of a merge, rebase or its own write: leave the repository to it.
|
|
35
|
+
for (const path of ['/.git/index.lock', '/.git/MERGE_HEAD', '/.git/CHERRY_PICK_HEAD', '/.git/REVERT_HEAD',
|
|
36
|
+
'/.git/rebase-merge', '/.git/rebase-apply']) {
|
|
37
|
+
if (await exists(path)) throw new GitPanelError('changed');
|
|
38
|
+
}
|
|
39
|
+
if (operation.kind === 'stage') {
|
|
40
|
+
// The browser cannot see executable bits or links: staging such a file would change its mode in Git.
|
|
41
|
+
const modes = new Map<string, number>();
|
|
42
|
+
await git.walk({...options, trees: [git.STAGE()], map: async (path, [entry]) => {
|
|
43
|
+
if (entry && path !== '.' && await entry.type() === 'blob') modes.set(path, await entry.mode());
|
|
44
|
+
return true;
|
|
45
|
+
}});
|
|
46
|
+
for (const path of operation.paths) {
|
|
47
|
+
pathParts(path);
|
|
48
|
+
const mode = modes.get(path);
|
|
49
|
+
if (mode !== undefined && mode !== 0o100644) throw new GitPanelError('unsupported');
|
|
50
|
+
if (await exists('/' + path)) await git.add({...options, filepath: path});
|
|
51
|
+
else await git.remove({...options, filepath: path});
|
|
52
|
+
}
|
|
53
|
+
} else if (operation.kind === 'unstage') {
|
|
54
|
+
for (const path of operation.paths) {
|
|
55
|
+
pathParts(path);
|
|
56
|
+
await git.resetIndex({...options, filepath: path});
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
const message = operation.message.trim();
|
|
60
|
+
if (!message) throw new GitPanelError('commitMessage');
|
|
61
|
+
const ref = await git.currentBranch(options);
|
|
62
|
+
if (!ref) throw new GitPanelError('unsupported');
|
|
63
|
+
const configured = async (path: string) => {
|
|
64
|
+
const value = await git.getConfig({...options, path});
|
|
65
|
+
return typeof value === 'string' && value.trim() ? value : undefined;
|
|
66
|
+
};
|
|
67
|
+
// The repository's own identity first; the Editor's setting only fills in what Git does not have.
|
|
68
|
+
const name = await configured('user.name') ?? operation.author?.name?.trim();
|
|
69
|
+
const email = await configured('user.email') ?? operation.author?.email?.trim();
|
|
70
|
+
if (!name || !email) throw new GitPanelError('author');
|
|
71
|
+
const staged = (await git.statusMatrix({...options, refresh: false})).some(([, head, , stage]) => head !== stage);
|
|
72
|
+
if (!staged) throw new GitPanelError('nothingStaged');
|
|
73
|
+
await git.commit({...options, message: message + '\n', author: {name, email}});
|
|
74
|
+
}
|
|
75
|
+
guard();
|
|
76
|
+
await transaction.commit(guard);
|
|
77
|
+
};
|
|
78
|
+
return globalThis.navigator?.locks ? navigator.locks.request('tw-plus-git-mutation', execute) : execute();
|
|
79
|
+
}
|
package/src/messages.ts
CHANGED
|
@@ -17,6 +17,7 @@ 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
|
+
commitMessage: 'Write a message for this record.',
|
|
20
21
|
pullUnsafe: 'Stop the project, and save or undo unsaved edits and unrecorded changes, before getting changes from GitHub (pull).',
|
|
21
22
|
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
23
|
session: 'Reconnect GitHub for this repository and requested permission.',
|
|
@@ -25,6 +26,12 @@ export const messages = {
|
|
|
25
26
|
rollbackFailed: 'Some local changes could not be restored. Check the files and the Git details.',
|
|
26
27
|
remoteFailed: 'Could not complete the GitHub operation. Check the connection and try again.',
|
|
27
28
|
remoteDone: 'The GitHub operation finished.',
|
|
29
|
+
stage: 'Add to Next Record', unstage: 'Remove from Next Record', stageAll: 'Add All Changes to Next Record',
|
|
30
|
+
commitLabel: 'Message for this record', commitButton: 'Record (Commit)', committed: 'Recorded the changes (commit).',
|
|
31
|
+
commitAuthor: 'Recorded as',
|
|
32
|
+
saveFirst: 'Save the project before adding its changes to the next record.',
|
|
33
|
+
author: 'Set user.name and user.email for this repository in Git before recording. The Editor does not change Git settings.',
|
|
34
|
+
nothingStaged: 'Add changes to the next record first.',
|
|
28
35
|
pulledProject: 'Got the changes from GitHub. Use Reload Project to open the updated files.',
|
|
29
36
|
confirmPush: 'Send the records on this branch to GitHub (push)?',
|
|
30
37
|
details: 'Git details', branch: 'Branch', head: 'HEAD', groups: 'Git groups', added: 'Added', modified: 'Changed', deleted: 'Deleted',
|
|
@@ -51,6 +58,7 @@ export const messages = {
|
|
|
51
58
|
changed: '読込中にファイルまたは Git の状態が変わりました。再取得してください。',
|
|
52
59
|
outdated: 'Editor の外でリポジトリが変更されました。再取得すると最新の状態を読み込みます。',
|
|
53
60
|
unsupported: 'ブラウザーで扱えない種類の Git エントリーがあります。',
|
|
61
|
+
commitMessage: 'この記録の説明を書いてください。',
|
|
54
62
|
pullUnsafe: 'GitHub から取り込む前に、実行を止め、保存していない編集とまだ記録していない変更をなくしてください。',
|
|
55
63
|
diverged: 'このブランチと GitHub の両方に新しい記録があるため、ここでは取り込めません(pull は fast-forward のみ)。ブランチとファイルはそのままです。',
|
|
56
64
|
session: 'このリポジトリと操作に必要な権限で GitHub に接続してください。',
|
|
@@ -59,6 +67,12 @@ export const messages = {
|
|
|
59
67
|
rollbackFailed: '一部のローカルの変更を元に戻せませんでした。ファイルと Git の詳細を確認してください。',
|
|
60
68
|
remoteFailed: 'GitHub との操作を完了できませんでした。接続を確認して、もう一度試してください。',
|
|
61
69
|
remoteDone: 'GitHub との操作が終わりました。',
|
|
70
|
+
stage: '次の記録に入れる', unstage: '次の記録から外す', stageAll: 'すべての変更を次の記録に入れる',
|
|
71
|
+
commitLabel: 'この記録の説明', commitButton: '記録する(コミット)', committed: '変更を記録しました(コミット)。',
|
|
72
|
+
commitAuthor: '記録する人',
|
|
73
|
+
saveFirst: '次の記録に入れる前に、作品を保存してください。',
|
|
74
|
+
author: '記録する前に、Git でこのリポジトリの user.name と user.email を設定してください。Editor は Git の設定を変えません。',
|
|
75
|
+
nothingStaged: '先に、変更を次の記録に入れてください。',
|
|
62
76
|
pulledProject: 'GitHub から取り込みました。「作品を再読込」で更新したファイルを開けます。',
|
|
63
77
|
confirmPush: 'このブランチの記録を GitHub に送りますか(push)?',
|
|
64
78
|
details: 'Git の詳細', branch: 'ブランチ', head: 'HEAD', groups: 'Git の区分', added: '追加', modified: '変更', deleted: '削除',
|
package/src/repository.ts
CHANGED
|
@@ -2,6 +2,8 @@ import * as git from 'isomorphic-git';
|
|
|
2
2
|
import {createTwoFilesPatch} from 'diff';
|
|
3
3
|
import {createReadOnlyFs, errorCode, GitPanelError, pathParts, readOptional, sameBytes} from './filesystem.js';
|
|
4
4
|
import {runRemoteOperation} from './remote-operations.js';
|
|
5
|
+
import {runLocalOperation} from './local-operations.js';
|
|
6
|
+
import type {LocalOperation} from './local-operations.js';
|
|
5
7
|
import type {GetRemoteSession, RemoteOperation} from './remote-operations.js';
|
|
6
8
|
import {assetLimit, assetsDirectoryParts, folderSettingsLimit, isAssetName} from './project-asset-paths.js';
|
|
7
9
|
import type {ProjectAssetLoader, ProjectVersion} from './project-assets.js';
|
|
@@ -458,6 +460,9 @@ export async function openRepository(root: FileSystemDirectoryHandle) {
|
|
|
458
460
|
throw error;
|
|
459
461
|
}
|
|
460
462
|
}),
|
|
463
|
+
/** Stages, unstages or commits; see `runLocalOperation`. `guard` throws to stop before each write. */
|
|
464
|
+
localOperation: (operation: LocalOperation, guard: () => void) =>
|
|
465
|
+
serial(() => runLocalOperation(root, operation, guard)),
|
|
461
466
|
read: (scope?: string) => serial(() => read(scope)),
|
|
462
467
|
readDiff: (snapshot: Snapshot, change: FileChange) => serial(() => readDiff(snapshot, change)),
|
|
463
468
|
historyChanges: (snapshot: Snapshot, oid: string) => serial(() => historyChanges(snapshot, oid)),
|
package/src/style.css
CHANGED
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
.twgit-panel aside li { overflow-wrap: anywhere; }
|
|
27
27
|
.twgit-panel summary { cursor: pointer; }
|
|
28
28
|
.twgit-panel .twgit-file { width: 100%; border: 0; margin: 1px 0; }
|
|
29
|
+
.twgit-panel .twgit-stage { font-size: 11px; margin: 0 0 4px; }
|
|
30
|
+
.twgit-commit { display: grid; gap: 6px; margin: 10px 0; padding: 8px; border: 1px solid color-mix(in srgb, CanvasText 18%, Canvas); border-radius: 6px; }
|
|
31
|
+
.twgit-commit label { display: grid; gap: 4px; font-size: 12px; }
|
|
32
|
+
.twgit-commit textarea { font: inherit; resize: vertical; min-height: 3em; }
|
|
29
33
|
.twgit-diff { padding: 0 16px 16px; min-width: 0; overflow: auto; }
|
|
30
34
|
.twgit-diff .diff { font-size: 12px; }
|
|
31
35
|
.twgit-history { list-style: none; margin: 0; padding: 0; }
|