@kubohiroya/turbowarp-plus-editor-git 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +4 -2
- package/README.md +3 -1
- package/dist/branch-button.d.ts +32 -0
- package/dist/index.d.ts +10 -5
- package/dist/index.js +566 -148
- package/dist/messages.d.ts +8 -2
- package/dist/{project-asset-paths-nL5sV35A.js → project-asset-paths-BzwVf9HE.js} +1 -1
- package/dist/{project-comparison-DjMlwyp8.js → project-comparison-BXOQcZ73.js} +1 -1
- package/dist/project-source-diff.js +2 -2
- package/dist/{repository-xIv0iZkj.js → repository-ByKRBftJ.js} +25 -8
- package/dist/repository.d.ts +7 -0
- package/dist/repository.js +1 -1
- package/dist/style.css +28 -0
- package/package.json +1 -1
- package/src/branch-button.tsx +216 -0
- package/src/index.tsx +183 -65
- package/src/messages.ts +6 -4
- package/src/repository.ts +22 -8
- package/src/style.css +28 -0
package/src/index.tsx
CHANGED
|
@@ -5,7 +5,9 @@ import {FileDiffView} from './file-diff-view.js';
|
|
|
5
5
|
import {findElement, ProjectChangeListView, ProjectElementView, projectChangeLabels, useProjectChanges} from './project-change-list.js';
|
|
6
6
|
import type {ChangedFile} from './project-change-model.js';
|
|
7
7
|
import {openRepository} from './repository.js';
|
|
8
|
-
import type {Repository, Snapshot, FileChange, FileDiff, ChangeGroup, HistoryCommit, HistoryCursor} from './repository.js';
|
|
8
|
+
import type {BranchRefs, Repository, Snapshot, FileChange, FileDiff, ChangeGroup, HistoryCommit, HistoryCursor} from './repository.js';
|
|
9
|
+
import {BranchButton} from './branch-button.js';
|
|
10
|
+
import type {BranchButtonState} from './branch-button.js';
|
|
9
11
|
import {HistoryGraph} from './history-graph-view.js';
|
|
10
12
|
import type {HistoryActionResult} from './history-graph-view.js';
|
|
11
13
|
import {labelsFor} from './messages.js';
|
|
@@ -34,7 +36,9 @@ export {openProjectComparison} from './project-comparison.js';
|
|
|
34
36
|
export {openProjectSourceTabs} from './project-source-tabs.js';
|
|
35
37
|
export type {ProjectSourceTabsFile, ProjectSourceTabsOptions} from './project-source-tabs.js';
|
|
36
38
|
export type {ProjectComparisonAction, ProjectComparisonOptions, ProjectComparisonSection} from './project-comparison.js';
|
|
37
|
-
export type {PullRequestBase, PullRequestFetch, PullRequestFiles, Repository, Snapshot, FileChange, FileDiff, HistoryCommit, HistoryCursor, HistoryPage, HistoryRef} from './repository.js';
|
|
39
|
+
export type {BranchRefs, PullRequestBase, PullRequestFetch, PullRequestFiles, Repository, Snapshot, FileChange, FileDiff, HistoryCommit, HistoryCursor, HistoryPage, HistoryRef} from './repository.js';
|
|
40
|
+
export {BranchButton} from './branch-button.js';
|
|
41
|
+
export type {BranchButtonActions, BranchButtonProps, BranchButtonState} from './branch-button.js';
|
|
38
42
|
export {HistoryGraph, laneColors} from './history-graph-view.js';
|
|
39
43
|
export type {HistoryActionResult, HistoryGraphProps} from './history-graph-view.js';
|
|
40
44
|
export {layoutHistory} from './history-graph.js';
|
|
@@ -77,7 +81,46 @@ export interface GitPanelRequest {
|
|
|
77
81
|
inactive?: boolean;
|
|
78
82
|
mount(element: HTMLElement, close: () => void): {requestClose(): void; dispose(): void};
|
|
79
83
|
}
|
|
80
|
-
|
|
84
|
+
type BranchOperation = Extract<LocalOperation, {kind: 'createBranch' | 'switchBranch'}>;
|
|
85
|
+
/** The panel's side of its entries: set by the panel as it renders, used by the branch buttons and `mountGitPanel`'s result. */
|
|
86
|
+
interface Controller {
|
|
87
|
+
open(locale: string): void;
|
|
88
|
+
refresh(automatic?: boolean): Promise<void>;
|
|
89
|
+
/** Asks for a repository, then opens the panel on it. */
|
|
90
|
+
choose(locale: string): Promise<void>;
|
|
91
|
+
changeBranch(operation: BranchOperation): Promise<HistoryActionResult>;
|
|
92
|
+
/** What the branch buttons show. */
|
|
93
|
+
branch: BranchButtonState;
|
|
94
|
+
publish(state: BranchButtonState): void;
|
|
95
|
+
/** Follows `branch`. While anything follows it, the closed panel keeps watching the branches. */
|
|
96
|
+
follow(listener: () => void): () => void;
|
|
97
|
+
/** How many follow `branch`. */
|
|
98
|
+
followers(): number;
|
|
99
|
+
/** Set by the panel while it is mounted, to hear when `followers()` changes. */
|
|
100
|
+
onFollowers(count: number): void;
|
|
101
|
+
}
|
|
102
|
+
function createController(): Controller {
|
|
103
|
+
const listeners = new Set<() => void>();
|
|
104
|
+
const controller: Controller = {
|
|
105
|
+
open() {}, async refresh() {}, async choose() {}, async changeBranch() {return {done: false};},
|
|
106
|
+
branch: {repository: false, branch: undefined, head: null, branches: [], remoteBranches: [], writable: false, busy: false},
|
|
107
|
+
publish(state) {
|
|
108
|
+
controller.branch = state;
|
|
109
|
+
for (const listener of [...listeners]) listener();
|
|
110
|
+
},
|
|
111
|
+
follow(listener) {
|
|
112
|
+
listeners.add(listener);
|
|
113
|
+
controller.onFollowers(listeners.size);
|
|
114
|
+
return () => {
|
|
115
|
+
listeners.delete(listener);
|
|
116
|
+
controller.onFollowers(listeners.size);
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
followers: () => listeners.size,
|
|
120
|
+
onFollowers() {}
|
|
121
|
+
};
|
|
122
|
+
return controller;
|
|
123
|
+
}
|
|
81
124
|
interface State {
|
|
82
125
|
repository: Repository | null;
|
|
83
126
|
snapshot: Snapshot | null;
|
|
@@ -94,6 +137,8 @@ interface State {
|
|
|
94
137
|
const initial: State = {repository: null, snapshot: null, project: null, browser: null,
|
|
95
138
|
changes: [], selected: null, diff: null, commit: null, error: null, busy: false};
|
|
96
139
|
const controllers = new WeakMap<object, Controller>();
|
|
140
|
+
// Branch buttons rendered before the panel is mounted, or after it is disposed, wait for the VM's controller here.
|
|
141
|
+
const registrations = new Set<() => void>();
|
|
97
142
|
type PickerWindow = Window & {showDirectoryPicker?: (options: {id: string; mode: 'readwrite'}) => Promise<FileSystemDirectoryHandle>};
|
|
98
143
|
type ObserverWindow = Window & {FileSystemObserver?: new (callback: () => void) => {
|
|
99
144
|
observe(handle: FileSystemDirectoryHandle, options: {recursive: boolean}): Promise<void>;
|
|
@@ -101,6 +146,53 @@ type ObserverWindow = Window & {FileSystemObserver?: new (callback: () => void)
|
|
|
101
146
|
}};
|
|
102
147
|
const watchInterval = 2000;
|
|
103
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Calls `check` every `watchInterval`, when the window regains focus and shortly after a File System Observer reports
|
|
151
|
+
* a change under `root`, one call at a time and never while the page is hidden. Returns the function that stops it.
|
|
152
|
+
*/
|
|
153
|
+
function watchRepository(document: Document, root: FileSystemDirectoryHandle, check: (stopped: () => boolean) => Promise<void>) {
|
|
154
|
+
const view = document.defaultView as ObserverWindow;
|
|
155
|
+
let stopped = false;
|
|
156
|
+
let checking = false;
|
|
157
|
+
const run = async () => {
|
|
158
|
+
if (stopped || checking || document.visibilityState === 'hidden') return;
|
|
159
|
+
checking = true;
|
|
160
|
+
try {
|
|
161
|
+
await check(() => stopped);
|
|
162
|
+
} catch {
|
|
163
|
+
// Entries can disappear while another tool writes. The next check reads them again.
|
|
164
|
+
} finally {
|
|
165
|
+
checking = false;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
let soon: number | undefined;
|
|
169
|
+
const schedule = () => {view.clearTimeout(soon); soon = view.setTimeout(() => void run(), 300);};
|
|
170
|
+
const interval = view.setInterval(() => void run(), watchInterval);
|
|
171
|
+
const onFocus = () => void run();
|
|
172
|
+
view.addEventListener('focus', onFocus);
|
|
173
|
+
document.addEventListener('visibilitychange', onFocus);
|
|
174
|
+
let observer: InstanceType<NonNullable<ObserverWindow['FileSystemObserver']>> | null = null;
|
|
175
|
+
try {
|
|
176
|
+
if (view.FileSystemObserver) {
|
|
177
|
+
observer = new view.FileSystemObserver(schedule);
|
|
178
|
+
observer.observe(root, {recursive: true}).catch(() => {});
|
|
179
|
+
}
|
|
180
|
+
} catch {
|
|
181
|
+
observer = null;
|
|
182
|
+
}
|
|
183
|
+
void run();
|
|
184
|
+
return () => {
|
|
185
|
+
stopped = true;
|
|
186
|
+
view.clearInterval(interval);
|
|
187
|
+
view.clearTimeout(soon);
|
|
188
|
+
view.removeEventListener('focus', onFocus);
|
|
189
|
+
document.removeEventListener('visibilitychange', onFocus);
|
|
190
|
+
observer?.disconnect();
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
const sameRefs = (a: BranchRefs | null, b: BranchRefs) => !!a && a.branch === b.branch && a.head === b.head &&
|
|
194
|
+
a.branches.join('\n') === b.branches.join('\n') && a.remoteBranches.join('\n') === b.remoteBranches.join('\n');
|
|
195
|
+
|
|
104
196
|
function GitPanel({vm, document, host, controller}: {vm: object; document: Document; host: GitHost; controller: Controller}) {
|
|
105
197
|
const [visible, setVisible] = useState(false);
|
|
106
198
|
// A layout tab that holds the panel. The panel component stays mounted, so the repository survives closing the tab.
|
|
@@ -233,61 +325,48 @@ function GitPanel({vm, document, host, controller}: {vm: object; document: Docum
|
|
|
233
325
|
void controller.refresh();
|
|
234
326
|
}
|
|
235
327
|
}, [shown]);
|
|
328
|
+
// The branches the branch buttons show: those of the last snapshot, or newer ones read while the panel is not in view.
|
|
329
|
+
const [refs, setRefs] = useState<BranchRefs | null>(null);
|
|
330
|
+
useEffect(() => {
|
|
331
|
+
if (!state.repository) setRefs(null);
|
|
332
|
+
else if (state.snapshot) setRefs(refsOf(state.snapshot));
|
|
333
|
+
}, [state.repository, state.snapshot]);
|
|
334
|
+
const [followers, setFollowers] = useState(controller.followers());
|
|
335
|
+
useEffect(() => {
|
|
336
|
+
controller.onFollowers = setFollowers;
|
|
337
|
+
// A button may have started following before the panel was mounted.
|
|
338
|
+
setFollowers(controller.followers());
|
|
339
|
+
return () => {controller.onFollowers = () => {};};
|
|
340
|
+
}, []);
|
|
236
341
|
// While the panel is open, notice Git commands, agents and editors that change the repository outside the Editor.
|
|
342
|
+
// While it is out of view and a branch button follows it, read only the refs at the same moments, so the button
|
|
343
|
+
// follows branch changes; the panel reads everything again when it comes into view.
|
|
344
|
+
const following = followers > 0;
|
|
237
345
|
useEffect(() => {
|
|
238
346
|
const repository = state.repository;
|
|
239
|
-
if (!
|
|
240
|
-
|
|
241
|
-
let stopped = false;
|
|
242
|
-
let checking = false;
|
|
243
|
-
const check = async () => {
|
|
347
|
+
if (!repository || !shown && !following) return;
|
|
348
|
+
return watchRepository(document, repository.root, async stopped => {
|
|
244
349
|
const {snapshot} = current.current;
|
|
350
|
+
if (running.current || current.current.repository !== repository) return;
|
|
245
351
|
// A tab behind another one has no boxes; it reads again when it is shown.
|
|
246
|
-
const
|
|
247
|
-
if (
|
|
248
|
-
!
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (stopped || probe === 'locked' || probe === snapshot.probe || probe === handled.current ||
|
|
253
|
-
current.current.snapshot !== snapshot) return;
|
|
254
|
-
handled.current = probe;
|
|
255
|
-
const {selected, commit} = current.current;
|
|
256
|
-
// Keep an open diff, commit or review in place and ask for a refresh instead.
|
|
257
|
-
if (selected || commit || reviewing.current) {
|
|
258
|
-
setState(previous => previous.snapshot === snapshot ? {...previous, notice: labelsFor(locale).outdated} : previous);
|
|
259
|
-
} else await controller.refresh(true);
|
|
260
|
-
} catch {
|
|
261
|
-
// Entries can disappear while another tool writes. The next check reads them again.
|
|
262
|
-
} finally {
|
|
263
|
-
checking = false;
|
|
264
|
-
}
|
|
265
|
-
};
|
|
266
|
-
let soon: number | undefined;
|
|
267
|
-
const schedule = () => {view.clearTimeout(soon); soon = view.setTimeout(() => void check(), 300);};
|
|
268
|
-
const interval = view.setInterval(() => void check(), watchInterval);
|
|
269
|
-
const onFocus = () => void check();
|
|
270
|
-
view.addEventListener('focus', onFocus);
|
|
271
|
-
document.addEventListener('visibilitychange', onFocus);
|
|
272
|
-
let observer: InstanceType<NonNullable<ObserverWindow['FileSystemObserver']>> | null = null;
|
|
273
|
-
try {
|
|
274
|
-
if (view.FileSystemObserver) {
|
|
275
|
-
observer = new view.FileSystemObserver(schedule);
|
|
276
|
-
observer.observe(repository.root, {recursive: true}).catch(() => {});
|
|
352
|
+
const inView = shown && !!dialog.current && dialog.current.getClientRects().length > 0;
|
|
353
|
+
if (!inView || !snapshot) {
|
|
354
|
+
if (!following) return;
|
|
355
|
+
const next = await repository.refs();
|
|
356
|
+
if (!stopped() && current.current.repository === repository) setRefs(previous => sameRefs(previous, next) ? previous : next);
|
|
357
|
+
return;
|
|
277
358
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
};
|
|
290
|
-
}, [shown, state.repository, locale]);
|
|
359
|
+
const probe = await repository.probe(snapshot.scope);
|
|
360
|
+
if (stopped() || probe === 'locked' || probe === snapshot.probe || probe === handled.current ||
|
|
361
|
+
current.current.snapshot !== snapshot) return;
|
|
362
|
+
handled.current = probe;
|
|
363
|
+
const {selected, commit} = current.current;
|
|
364
|
+
// Keep an open diff, commit or review in place and ask for a refresh instead.
|
|
365
|
+
if (selected || commit || reviewing.current) {
|
|
366
|
+
setState(previous => previous.snapshot === snapshot ? {...previous, notice: labelsFor(locale).outdated} : previous);
|
|
367
|
+
} else await controller.refresh(true);
|
|
368
|
+
});
|
|
369
|
+
}, [shown, following, state.repository, locale]);
|
|
291
370
|
useEffect(() => {
|
|
292
371
|
if (!visible || slot) return;
|
|
293
372
|
const previous = document.activeElement as HTMLElement | null;
|
|
@@ -296,7 +375,7 @@ function GitPanel({vm, document, host, controller}: {vm: object; document: Docum
|
|
|
296
375
|
}, [visible, slot]);
|
|
297
376
|
const chooseRoot = async () => {
|
|
298
377
|
const view = document.defaultView as PickerWindow;
|
|
299
|
-
|
|
378
|
+
return run(async () => {
|
|
300
379
|
let root: FileSystemDirectoryHandle;
|
|
301
380
|
try {root = await view.showDirectoryPicker!({id: 'turbowarp-plus-git-root', mode: 'readwrite'});} catch (error) {
|
|
302
381
|
if ((error as DOMException).name === 'AbortError') return {};
|
|
@@ -394,7 +473,7 @@ function GitPanel({vm, document, host, controller}: {vm: object; document: Docum
|
|
|
394
473
|
};
|
|
395
474
|
const [newBranch, setNewBranch] = useState('');
|
|
396
475
|
const [switchAfterCreate, setSwitchAfterCreate] = useState(true);
|
|
397
|
-
const changeBranch = async (operation:
|
|
476
|
+
const changeBranch = async (operation: BranchOperation): Promise<HistoryActionResult> => {
|
|
398
477
|
const repository = current.current.repository;
|
|
399
478
|
const project = current.current.project;
|
|
400
479
|
const switching = operation.kind === 'switchBranch' || operation.checkout;
|
|
@@ -472,6 +551,18 @@ function GitPanel({vm, document, host, controller}: {vm: object; document: Docum
|
|
|
472
551
|
// One loader per selected change, so thumbnails read each asset once.
|
|
473
552
|
const loadAsset = useMemo(() => state.repository && state.snapshot && state.selected ?
|
|
474
553
|
state.repository.assetLoader(state.snapshot, state.selected) : undefined, [state.repository, state.snapshot, state.selected]);
|
|
554
|
+
controller.changeBranch = changeBranch;
|
|
555
|
+
controller.choose = async language => {
|
|
556
|
+
setLocale(language);
|
|
557
|
+
const before = current.current.repository;
|
|
558
|
+
const result = await chooseRoot();
|
|
559
|
+
// The panel shows the chosen repository, or why it could not be read; a cancelled choice opens nothing.
|
|
560
|
+
if (!result.done && result.error || current.current.repository !== before) controller.open(language);
|
|
561
|
+
};
|
|
562
|
+
const branchState: BranchButtonState = {repository: !!state.repository, branch: refs?.branch, head: refs?.head ?? null,
|
|
563
|
+
branches: refs?.branches ?? [], remoteBranches: refs?.remoteBranches ?? [], writable: !!host.localWrite && !!refs, busy: state.busy};
|
|
564
|
+
const published = JSON.stringify(branchState);
|
|
565
|
+
useEffect(() => controller.publish(branchState), [published]);
|
|
475
566
|
if (!shown) return null;
|
|
476
567
|
const groups: ChangeGroup[] = state.commit ? ['commit'] : ['staged', 'unstaged', 'untracked'];
|
|
477
568
|
// Branches can be switched and created whenever writes are allowed and the repository has been read.
|
|
@@ -656,28 +747,55 @@ function upstreamText({upstream}: Snapshot, labels: ReturnType<typeof labelsFor>
|
|
|
656
747
|
}
|
|
657
748
|
}
|
|
658
749
|
|
|
750
|
+
const refsOf = ({branch, head, branches, remoteBranches}: Snapshot): BranchRefs => ({branch, head, branches, remoteBranches});
|
|
751
|
+
const announce = () => {for (const listener of [...registrations]) listener();};
|
|
752
|
+
|
|
659
753
|
export function mountGitPanel({vm, document, host}: {vm: object; document: Document; host: GitHost}) {
|
|
660
754
|
if (!(document.defaultView as PickerWindow)?.showDirectoryPicker) return null;
|
|
661
755
|
const root = document.createElement('div');
|
|
662
756
|
document.body.append(root);
|
|
663
|
-
const controller
|
|
757
|
+
const controller = createController();
|
|
664
758
|
controllers.set(vm, controller);
|
|
665
759
|
render(<GitPanel vm={vm} document={document} host={host} controller={controller}/>, root);
|
|
666
|
-
|
|
760
|
+
announce();
|
|
761
|
+
return {refresh: () => controller.refresh(), open: (locale: string) => controller.open(locale), dispose() {
|
|
667
762
|
if (controllers.get(vm) === controller) controllers.delete(vm);
|
|
668
763
|
void host.connectionSettings?.disconnect().catch(() => {});
|
|
669
764
|
unmountComponentAtNode(root);
|
|
670
765
|
root.remove();
|
|
766
|
+
announce();
|
|
671
767
|
}};
|
|
672
768
|
}
|
|
673
769
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
},
|
|
770
|
+
/** The branch button of the panel mounted for `vm`, or nothing while no panel is mounted for it. */
|
|
771
|
+
function BranchEntry({vm, locale}: {vm: object; locale: string}) {
|
|
772
|
+
const [controller, setController] = useState(() => controllers.get(vm));
|
|
773
|
+
useEffect(() => {
|
|
774
|
+
const update = () => setController(controllers.get(vm));
|
|
775
|
+
registrations.add(update);
|
|
776
|
+
update();
|
|
777
|
+
return () => {registrations.delete(update);};
|
|
778
|
+
}, [vm]);
|
|
779
|
+
const [state, setState] = useState(controller?.branch);
|
|
780
|
+
useEffect(() => {
|
|
781
|
+
if (!controller) return;
|
|
782
|
+
const update = () => setState(controller.branch);
|
|
783
|
+
update();
|
|
784
|
+
return controller.follow(update);
|
|
785
|
+
}, [controller]);
|
|
786
|
+
if (!controller || !state) return null;
|
|
787
|
+
return <BranchButton state={state} locale={locale} open={() => controller.open(locale)} choose={() => void controller.choose(locale)}
|
|
788
|
+
switchBranch={name => controller.changeBranch({kind: 'switchBranch', name})}
|
|
789
|
+
createBranch={(name, checkout) => controller.changeBranch({kind: 'createBranch', name, checkout})}
|
|
790
|
+
createFromRemote={name => controller.changeBranch({kind: 'createBranch', name, fromRemote: true, checkout: true})}/>;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Renders the button for the host's menu bar that shows the current branch of the panel mounted for `vm` and opens the
|
|
795
|
+
* menu of branches, the panel and the repository choice. It renders nothing while no panel is mounted for `vm`, such as
|
|
796
|
+
* when the host does not offer local Git, and shares the panel's repository, operations and checks.
|
|
797
|
+
*/
|
|
798
|
+
export function renderBranchButton(createElement: (component: any, props: any, ...children: any[]) => any,
|
|
799
|
+
{vm, locale = 'en'}: {vm: object; locale?: string}) {
|
|
800
|
+
return createElement(BranchEntry, {vm, locale});
|
|
683
801
|
}
|
package/src/messages.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const messages = {
|
|
2
2
|
en: {
|
|
3
3
|
pullRequests: 'Pull requests', localChanges: 'Local changes',
|
|
4
|
-
title: 'Local Git',
|
|
4
|
+
title: 'Local Git', close: 'Close', choose: 'Choose Repository…', refresh: 'Refresh',
|
|
5
5
|
empty: 'Choose an existing Git repository to inspect its files and history.',
|
|
6
6
|
repository: 'Repository', project: 'Project folder', none: 'No project selected', root: 'Repository root',
|
|
7
7
|
open: 'Open This Project', reload: 'Reload Project', up: 'Parent folder', browse: 'Choose a project inside this repository',
|
|
@@ -54,11 +54,12 @@ export const messages = {
|
|
|
54
54
|
historyMore: 'Show more', historyLoading: 'Reading records…', historyFailed: 'Could not read the records.',
|
|
55
55
|
historyActions: 'Actions', historyCreateHere: 'Create branch here…', historySwitch: 'Switch to {name}',
|
|
56
56
|
historyCreateFromRemote: 'Create and switch to {name}', historyCopy: 'Copy commit ID', historyCopied: 'Copied the commit ID.',
|
|
57
|
-
historyCopyFailed: 'Could not copy the commit ID.', historyRefs: 'Branches', cancel: 'Cancel'
|
|
57
|
+
historyCopyFailed: 'Could not copy the commit ID.', historyRefs: 'Branches', cancel: 'Cancel',
|
|
58
|
+
branchMenu: 'Branches', openPanel: 'Open Local Git', newBranchItem: 'New Branch…', newBranchTitle: 'New Branch'
|
|
58
59
|
},
|
|
59
60
|
ja: {
|
|
60
61
|
pullRequests: 'プルリクエスト', localChanges: 'ローカルの変更',
|
|
61
|
-
title: 'ローカル Git',
|
|
62
|
+
title: 'ローカル Git', close: '閉じる', choose: 'リポジトリを選択…', refresh: '再取得',
|
|
62
63
|
empty: '既存の Git リポジトリを選び、ファイルの状態と履歴を確認できます。',
|
|
63
64
|
repository: 'リポジトリ', project: '作品フォルダ', none: '作品は未選択', root: 'リポジトリルート',
|
|
64
65
|
open: 'この作品を開く', reload: '作品を再読込', up: '上のフォルダ', browse: 'リポジトリ内の作品を選択',
|
|
@@ -111,7 +112,8 @@ export const messages = {
|
|
|
111
112
|
historyMore: 'さらに表示', historyLoading: '記録を読込中…', historyFailed: '記録を読み込めませんでした。',
|
|
112
113
|
historyActions: '操作', historyCreateHere: 'ここからブランチを作る…', historySwitch: '{name} に切り替える',
|
|
113
114
|
historyCreateFromRemote: '{name} を作って切り替える', historyCopy: 'コミットの ID をコピー', historyCopied: 'コミットの ID をコピーしました。',
|
|
114
|
-
historyCopyFailed: 'コミットの ID をコピーできませんでした。', historyRefs: 'ブランチ', cancel: 'キャンセル'
|
|
115
|
+
historyCopyFailed: 'コミットの ID をコピーできませんでした。', historyRefs: 'ブランチ', cancel: 'キャンセル',
|
|
116
|
+
branchMenu: 'ブランチ', openPanel: 'ローカル Git を開く', newBranchItem: '新しいブランチ…', newBranchTitle: '新しいブランチ'
|
|
115
117
|
}
|
|
116
118
|
};
|
|
117
119
|
export type Labels = typeof messages.en;
|
package/src/repository.ts
CHANGED
|
@@ -84,6 +84,8 @@ export interface HistoryPage {
|
|
|
84
84
|
export const historyPageSize = 50;
|
|
85
85
|
const projectSourceLimit = 16 * 1024 * 1024;
|
|
86
86
|
type Metadata = Map<string, Uint8Array | null>;
|
|
87
|
+
/** The branch fields of a snapshot. */
|
|
88
|
+
export type BranchRefs = Pick<Snapshot, 'branch' | 'head' | 'branches' | 'remoteBranches'>;
|
|
87
89
|
export interface Snapshot {
|
|
88
90
|
branch: string | undefined;
|
|
89
91
|
head: string | null;
|
|
@@ -298,13 +300,7 @@ export async function openRepository(root: FileSystemDirectoryHandle) {
|
|
|
298
300
|
const assertSnapshot = async (snapshot: Snapshot) => {
|
|
299
301
|
if (!sameMetadata(snapshot.metadata, await metadata())) throw new GitPanelError('changed');
|
|
300
302
|
};
|
|
301
|
-
const
|
|
302
|
-
scope = pathParts(scope).join('/');
|
|
303
|
-
fs.clearReads();
|
|
304
|
-
cache = {};
|
|
305
|
-
// Taken first, so a change during the read differs from the stored value and triggers another read.
|
|
306
|
-
const fingerprint = await probe(scope);
|
|
307
|
-
const before = await metadata();
|
|
303
|
+
const readRefs = async (): Promise<BranchRefs> => {
|
|
308
304
|
let head: string | null;
|
|
309
305
|
try {head = await git.resolveRef({...options, ref: 'HEAD'});} catch (error) {
|
|
310
306
|
if (errorCode(error) !== 'NotFoundError') throw error;
|
|
@@ -314,6 +310,16 @@ export async function openRepository(root: FileSystemDirectoryHandle) {
|
|
|
314
310
|
const branches = await git.listBranches(options);
|
|
315
311
|
const remoteBranches = (await git.listBranches({...options, remote: 'origin'}))
|
|
316
312
|
.filter(name => name !== 'HEAD' && !branches.includes(name)).sort();
|
|
313
|
+
return {branch: branch || undefined, head, branches, remoteBranches};
|
|
314
|
+
};
|
|
315
|
+
const read = async (scope = ''): Promise<Snapshot> => {
|
|
316
|
+
scope = pathParts(scope).join('/');
|
|
317
|
+
fs.clearReads();
|
|
318
|
+
cache = {};
|
|
319
|
+
// Taken first, so a change during the read differs from the stored value and triggers another read.
|
|
320
|
+
const fingerprint = await probe(scope);
|
|
321
|
+
const before = await metadata();
|
|
322
|
+
const {branch, head, branches, remoteBranches} = await readRefs();
|
|
317
323
|
const commits = head ? (await git.log({...options, cache, ref: head, depth: 50})).map(({oid, commit}) => ({
|
|
318
324
|
oid, message: commit.message, author: commit.author.name, timestamp: commit.author.timestamp
|
|
319
325
|
})) : [];
|
|
@@ -339,7 +345,7 @@ export async function openRepository(root: FileSystemDirectoryHandle) {
|
|
|
339
345
|
}
|
|
340
346
|
if (!sameMetadata(before, await metadata())) throw new GitPanelError('changed');
|
|
341
347
|
await fs.checkReads();
|
|
342
|
-
return {branch
|
|
348
|
+
return {branch, head, branches, remoteBranches, commits, changes, scope, metadata: before, probe: fingerprint,
|
|
343
349
|
upstream};
|
|
344
350
|
};
|
|
345
351
|
interface HistoryNode {commit: HistoryCommit; time: number; children: number; expanded: boolean}
|
|
@@ -745,6 +751,14 @@ export async function openRepository(root: FileSystemDirectoryHandle) {
|
|
|
745
751
|
}
|
|
746
752
|
}),
|
|
747
753
|
read: (scope?: string) => serial(() => read(scope)),
|
|
754
|
+
/**
|
|
755
|
+
* The current branch, HEAD and the branch names, read from refs only. Cheap enough to follow branch changes made
|
|
756
|
+
* by other tools while nothing else is read; `read` returns the same fields.
|
|
757
|
+
*/
|
|
758
|
+
refs: () => serial(() => {
|
|
759
|
+
fs.clearReads();
|
|
760
|
+
return readRefs();
|
|
761
|
+
}),
|
|
748
762
|
readDiff: (snapshot: Snapshot, change: FileChange) => serial(() => readDiff(snapshot, change)),
|
|
749
763
|
historyChanges: (snapshot: Snapshot, oid: string) => serial(() => historyChanges(snapshot, oid)),
|
|
750
764
|
/**
|
package/src/style.css
CHANGED
|
@@ -92,3 +92,31 @@
|
|
|
92
92
|
.twgit-graph-status { margin: 4px 0; font-size: 11px; min-height: 1em; }
|
|
93
93
|
.twgit-panel .twgit-graph-popup [role="menuitem"]:focus-visible, .twgit-graph-popup [role="menuitem"]:focus-visible { outline-offset: -2px; }
|
|
94
94
|
.twgit-graph-form-error { margin: 0; color: #a11919; overflow-wrap: anywhere; }
|
|
95
|
+
/* The branch button sits in the host's menu bar and takes its text color; its menu uses the host's menu colors. */
|
|
96
|
+
.twgit-branch-entry { position: relative; flex: 0 1 auto; align-self: center; display: flex; min-width: 0; font: inherit; }
|
|
97
|
+
.twgit-branch-button { display: flex; align-items: center; gap: 4px; min-width: 0; max-width: 16rem; min-height: 2rem; padding: 0 8px;
|
|
98
|
+
color: inherit; background: transparent; border: 0; border-radius: 4px; font: inherit; white-space: nowrap; cursor: pointer; }
|
|
99
|
+
.twgit-branch-button:hover, .twgit-branch-button[aria-expanded="true"] { background: color-mix(in srgb, currentColor 15%, transparent); }
|
|
100
|
+
.twgit-branch-button:focus-visible { outline: 2px solid currentColor; outline-offset: -2px; }
|
|
101
|
+
.twgit-branch-icon { flex: none; }
|
|
102
|
+
.twgit-branch-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
103
|
+
.twgit-branch-unborn { flex: none; font-size: 11px; opacity: .85; }
|
|
104
|
+
.twgit-branch-caret { flex: none; font-size: 10px; }
|
|
105
|
+
.twgit-branch-popup { position: absolute; z-index: 510; top: calc(100% + 4px); right: 0; display: grid; gap: 2px; min-width: 220px;
|
|
106
|
+
max-width: min(360px, 90vw); max-height: 70vh; overflow: auto; box-sizing: border-box; padding: 4px;
|
|
107
|
+
color: var(--text-primary, CanvasText); background: var(--ui-white, Canvas); border: 1px solid var(--ui-black-transparent, GrayText);
|
|
108
|
+
border-radius: 6px; box-shadow: 0 8px 24px #0004; font: 14px/1.5 system-ui, sans-serif; white-space: normal; }
|
|
109
|
+
.twgit-branch-popup [role^="menuitem"] { display: flex; align-items: center; gap: 6px; width: 100%; padding: 6px 10px; color: inherit;
|
|
110
|
+
background: transparent; border: 0; border-radius: 4px; font: inherit; text-align: start; overflow-wrap: anywhere; cursor: pointer; }
|
|
111
|
+
.twgit-branch-popup [role^="menuitem"]:hover, .twgit-branch-popup [role^="menuitem"]:focus-visible {
|
|
112
|
+
background: color-mix(in srgb, currentColor 12%, transparent); }
|
|
113
|
+
.twgit-branch-popup [role^="menuitem"]:focus-visible { outline: 2px solid var(--looks-secondary, Highlight); outline-offset: -2px; }
|
|
114
|
+
.twgit-branch-popup [aria-disabled="true"]:not([aria-checked="true"]) { opacity: .55; cursor: default; }
|
|
115
|
+
.twgit-branch-popup [aria-checked="true"] { font-weight: 600; }
|
|
116
|
+
.twgit-branch-popup [data-separator-before="true"] { margin-top: 3px; box-shadow: 0 -3px 0 -2px color-mix(in srgb, currentColor 25%, transparent); }
|
|
117
|
+
.twgit-branch-check { flex: none; width: 1em; text-align: center; }
|
|
118
|
+
.twgit-branch-popup form { display: grid; gap: 6px; padding: 6px; font-size: 12px; }
|
|
119
|
+
.twgit-branch-popup form label { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
|
|
120
|
+
.twgit-branch-popup form input:not([type]) { flex: 1; min-width: 10em; font: inherit; }
|
|
121
|
+
.twgit-branch-popup form button { color: inherit; background: transparent; border: 1px solid currentColor; border-radius: 4px; padding: 3px 8px; font: inherit; cursor: pointer; }
|
|
122
|
+
.twgit-branch-popup form button:disabled { opacity: .5; cursor: default; }
|