xtralab 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +69 -0
  3. package/lib/fileBrowser/commands.d.ts +62 -0
  4. package/lib/fileBrowser/commands.js +418 -0
  5. package/lib/fileBrowser/contents.d.ts +28 -0
  6. package/lib/fileBrowser/contents.js +48 -0
  7. package/lib/fileBrowser/fileBrowser.d.ts +24 -0
  8. package/lib/fileBrowser/fileBrowser.js +535 -0
  9. package/lib/fileBrowser/icons.d.ts +37 -0
  10. package/lib/fileBrowser/icons.js +153 -0
  11. package/lib/fileBrowser/index.d.ts +7 -0
  12. package/lib/fileBrowser/index.js +38 -0
  13. package/lib/fileBrowser/widget.d.ts +97 -0
  14. package/lib/fileBrowser/widget.js +95 -0
  15. package/lib/git/api.d.ts +39 -0
  16. package/lib/git/api.js +164 -0
  17. package/lib/git/commands.d.ts +69 -0
  18. package/lib/git/commands.js +67 -0
  19. package/lib/git/diffWidget.d.ts +108 -0
  20. package/lib/git/diffWidget.js +624 -0
  21. package/lib/git/icons.d.ts +14 -0
  22. package/lib/git/icons.js +19 -0
  23. package/lib/git/index.d.ts +11 -0
  24. package/lib/git/index.js +74 -0
  25. package/lib/git/notebookDiff.d.ts +176 -0
  26. package/lib/git/notebookDiff.js +805 -0
  27. package/lib/git/panel.d.ts +29 -0
  28. package/lib/git/panel.js +42 -0
  29. package/lib/git/panelComponent.d.ts +26 -0
  30. package/lib/git/panelComponent.js +300 -0
  31. package/lib/git/tokens.d.ts +78 -0
  32. package/lib/git/tokens.js +6 -0
  33. package/lib/index.d.ts +10 -0
  34. package/lib/index.js +14 -0
  35. package/package.json +216 -0
  36. package/schema/plugin.json +67 -0
  37. package/src/fileBrowser/commands.ts +507 -0
  38. package/src/fileBrowser/contents.ts +66 -0
  39. package/src/fileBrowser/fileBrowser.tsx +635 -0
  40. package/src/fileBrowser/icons.ts +172 -0
  41. package/src/fileBrowser/index.ts +56 -0
  42. package/src/fileBrowser/widget.tsx +202 -0
  43. package/src/git/api.ts +207 -0
  44. package/src/git/commands.ts +143 -0
  45. package/src/git/diffWidget.tsx +903 -0
  46. package/src/git/icons.ts +20 -0
  47. package/src/git/index.ts +109 -0
  48. package/src/git/notebookDiff.tsx +1294 -0
  49. package/src/git/panel.tsx +56 -0
  50. package/src/git/panelComponent.tsx +558 -0
  51. package/src/git/tokens.ts +91 -0
  52. package/src/index.ts +18 -0
  53. package/style/base.css +175 -0
  54. package/style/git.css +770 -0
  55. package/style/index.css +2 -0
  56. package/style/index.js +2 -0
@@ -0,0 +1,56 @@
1
+ import * as React from 'react';
2
+
3
+ import { ReactWidget } from '@jupyterlab/ui-components';
4
+ import { Signal, ISignal } from '@lumino/signaling';
5
+
6
+ import { gitIcon } from './icons';
7
+ import { GitPanelComponent, IGitPanelHandlers } from './panelComponent';
8
+
9
+ /**
10
+ * The id and CSS class used for the git panel widget. The id ends up as the
11
+ * sidebar tab id (so layout restoration can find the widget) and the CSS
12
+ * class lets per-panel styles attach without leaking to other widgets.
13
+ */
14
+ export const GIT_PANEL_ID = 'xtralab:git-panel';
15
+ export const GIT_PANEL_CSS_CLASS = 'jp-xtralab-GitPanel';
16
+
17
+ /**
18
+ * The lumino host widget for the git changes panel. Owns the `refresh`
19
+ * signal that the React component listens to, and exposes a typed callback
20
+ * surface (`handlers`) the React component uses to react to user actions.
21
+ */
22
+ export class GitPanel extends ReactWidget {
23
+ constructor(handlers: IGitPanelHandlers) {
24
+ super();
25
+ this.id = GIT_PANEL_ID;
26
+ this.title.icon = gitIcon;
27
+ this.title.caption = 'Git Changes';
28
+ this.addClass(GIT_PANEL_CSS_CLASS);
29
+ this._handlers = handlers;
30
+ }
31
+
32
+ /**
33
+ * Public refresh hook called by the plugin when something happened that
34
+ * invalidates the current view (the user clicked the toolbar refresh, the
35
+ * file browser refreshed, a focused notebook saved, …).
36
+ */
37
+ refresh(): void {
38
+ this._refreshRequested.emit();
39
+ }
40
+
41
+ get refreshRequested(): ISignal<this, void> {
42
+ return this._refreshRequested;
43
+ }
44
+
45
+ protected render(): React.ReactElement {
46
+ return (
47
+ <GitPanelComponent
48
+ handlers={this._handlers}
49
+ refreshSignal={this._refreshRequested}
50
+ />
51
+ );
52
+ }
53
+
54
+ private _handlers: IGitPanelHandlers;
55
+ private _refreshRequested = new Signal<this, void>(this);
56
+ }
@@ -0,0 +1,558 @@
1
+ import * as React from 'react';
2
+
3
+ import { ISignal } from '@lumino/signaling';
4
+
5
+ import {
6
+ add,
7
+ addAll,
8
+ checkout,
9
+ porcelainToFileChanges,
10
+ reset,
11
+ resetAll,
12
+ showTopLevel,
13
+ status
14
+ } from './api';
15
+ import { FileChangeStatus, IFileChange, IGitStatusResult } from './tokens';
16
+
17
+ /**
18
+ * Callbacks the panel needs to delegate to (so the React component stays
19
+ * free of any JupyterLab application coupling). The plugin wires these up
20
+ * with the actual `JupyterFrontEnd` reference.
21
+ */
22
+ export interface IGitPanelHandlers {
23
+ /** Open a diff viewer for the given file in the main area. */
24
+ openDiff(change: IFileChange): void;
25
+ /** Show a confirmation dialog. Returns true when the user accepted. */
26
+ confirm(title: string, body: string, accept: string): Promise<boolean>;
27
+ /** Show an error dialog. */
28
+ showError(title: string, error: Error): Promise<void>;
29
+ }
30
+
31
+ interface IPanelState {
32
+ /** True while the initial status request is in flight. */
33
+ loading: boolean;
34
+ /** The raw result of the last status call, or `null` if it hasn't completed yet. */
35
+ result: IGitStatusResult | null;
36
+ /** Set when the status request errored or the path is not in a repo. */
37
+ error: string | null;
38
+ }
39
+
40
+ /**
41
+ * The polling interval for the panel. We refresh the status periodically so
42
+ * out-of-band changes (e.g. the user runs `git add` from a terminal) become
43
+ * visible without requiring a manual refresh.
44
+ */
45
+ const STATUS_POLL_INTERVAL_MS = 5000;
46
+
47
+ /**
48
+ * Top-level React component rendered inside the {@link GitPanel} widget.
49
+ * Owns the polling + refresh wiring around the `/git/status` endpoint and
50
+ * the small bit of UI state that controls section collapse and the
51
+ * in-flight per-file action spinners.
52
+ */
53
+ export function GitPanelComponent(props: {
54
+ handlers: IGitPanelHandlers;
55
+ refreshSignal: ISignal<unknown, void>;
56
+ }): React.ReactElement {
57
+ const { handlers, refreshSignal } = props;
58
+
59
+ const [state, setState] = React.useState<IPanelState>({
60
+ loading: true,
61
+ result: null,
62
+ error: null
63
+ });
64
+ // The absolute filesystem path of the resolved git top-level. Used purely
65
+ // for display in the panel header.
66
+ const [topLevel, setTopLevel] = React.useState<string | null>(null);
67
+ // Per-action spinner gating: keyed by `${group}:${path}`, value is the
68
+ // action currently in flight. Lets the row render a disabled state on
69
+ // exactly the file that is being mutated, without locking the whole panel.
70
+ const [pendingActions, setPendingActions] = React.useState<
71
+ Record<string, string>
72
+ >({});
73
+ const [stagedCollapsed, setStagedCollapsed] = React.useState(false);
74
+ const [changesCollapsed, setChangesCollapsed] = React.useState(false);
75
+
76
+ // The repo path passed to every API call. We use the JupyterLab server's
77
+ // root (the empty string) and let `git` resolve the enclosing repo on the
78
+ // server side. This works as long as the server was started inside a git
79
+ // repo — the common case.
80
+ const repoPath = '';
81
+
82
+ const refresh = React.useCallback(async (): Promise<void> => {
83
+ try {
84
+ const [topLevelResult, statusResult] = await Promise.all([
85
+ showTopLevel(repoPath),
86
+ status(repoPath)
87
+ ]);
88
+ setTopLevel(topLevelResult);
89
+ if (statusResult.code !== 0) {
90
+ setState({
91
+ loading: false,
92
+ result: null,
93
+ error: statusResult.message ?? 'Failed to load git status'
94
+ });
95
+ return;
96
+ }
97
+ setState({ loading: false, result: statusResult, error: null });
98
+ } catch (err) {
99
+ setState({
100
+ loading: false,
101
+ result: null,
102
+ error: err instanceof Error ? err.message : String(err)
103
+ });
104
+ }
105
+ }, [repoPath]);
106
+
107
+ // Initial load + polling. `refresh` is stable for a given `repoPath`, so
108
+ // the polling interval is only re-created when the repo path changes.
109
+ React.useEffect(() => {
110
+ void refresh();
111
+ const interval = window.setInterval(
112
+ () => void refresh(),
113
+ STATUS_POLL_INTERVAL_MS
114
+ );
115
+ return () => {
116
+ window.clearInterval(interval);
117
+ };
118
+ }, [refresh]);
119
+
120
+ // External refresh triggers (toolbar button, plugin-level events).
121
+ React.useEffect(() => {
122
+ const handler = (): void => {
123
+ void refresh();
124
+ };
125
+ refreshSignal.connect(handler);
126
+ return () => {
127
+ refreshSignal.disconnect(handler);
128
+ };
129
+ }, [refresh, refreshSignal]);
130
+
131
+ const runAction = React.useCallback(
132
+ async (
133
+ change: IFileChange,
134
+ action: string,
135
+ run: () => Promise<void>
136
+ ): Promise<void> => {
137
+ const key = `${change.group}:${change.path}`;
138
+ setPendingActions(prev => ({ ...prev, [key]: action }));
139
+ try {
140
+ await run();
141
+ await refresh();
142
+ } catch (err) {
143
+ await handlers.showError(
144
+ 'Git operation failed',
145
+ err instanceof Error ? err : new Error(String(err))
146
+ );
147
+ } finally {
148
+ setPendingActions(prev => {
149
+ const next = { ...prev };
150
+ delete next[key];
151
+ return next;
152
+ });
153
+ }
154
+ },
155
+ [handlers, refresh]
156
+ );
157
+
158
+ const onStage = React.useCallback(
159
+ (change: IFileChange) =>
160
+ runAction(change, 'stage', () => add(repoPath, change.path)),
161
+ [repoPath, runAction]
162
+ );
163
+
164
+ const onUnstage = React.useCallback(
165
+ (change: IFileChange) =>
166
+ runAction(change, 'unstage', () => reset(repoPath, change.path)),
167
+ [repoPath, runAction]
168
+ );
169
+
170
+ const onDiscard = React.useCallback(
171
+ async (change: IFileChange): Promise<void> => {
172
+ const verb = change.status === 'untracked' ? 'remove' : 'restore';
173
+ const accepted = await handlers.confirm(
174
+ verb === 'remove' ? 'Remove file' : 'Discard changes',
175
+ verb === 'remove'
176
+ ? `Permanently remove ${change.path}? This cannot be undone.`
177
+ : `Discard all changes to ${change.path}? This will revert the file to its last staged version and cannot be undone.`,
178
+ verb === 'remove' ? 'Remove' : 'Discard'
179
+ );
180
+ if (!accepted) {
181
+ return;
182
+ }
183
+ // The git extension's `checkout` endpoint without a branch reverts the
184
+ // working tree copy of the file to the index. For untracked files we
185
+ // can't checkout, so the user is told the action is unsupported and we
186
+ // bail out — they can delete the file via the file browser instead.
187
+ if (change.status === 'untracked') {
188
+ await handlers.showError(
189
+ 'Cannot discard untracked file',
190
+ new Error(
191
+ 'Untracked files cannot be discarded from the git panel — delete them from the file browser instead.'
192
+ )
193
+ );
194
+ return;
195
+ }
196
+ await runAction(change, 'discard', () => checkout(repoPath, change.path));
197
+ },
198
+ [handlers, repoPath, runAction]
199
+ );
200
+
201
+ const onStageAll = React.useCallback(async (): Promise<void> => {
202
+ try {
203
+ await addAll(repoPath);
204
+ await refresh();
205
+ } catch (err) {
206
+ await handlers.showError(
207
+ 'Stage all failed',
208
+ err instanceof Error ? err : new Error(String(err))
209
+ );
210
+ }
211
+ }, [handlers, refresh, repoPath]);
212
+
213
+ const onUnstageAll = React.useCallback(async (): Promise<void> => {
214
+ try {
215
+ await resetAll(repoPath);
216
+ await refresh();
217
+ } catch (err) {
218
+ await handlers.showError(
219
+ 'Unstage all failed',
220
+ err instanceof Error ? err : new Error(String(err))
221
+ );
222
+ }
223
+ }, [handlers, refresh, repoPath]);
224
+
225
+ // Categorize the porcelain entries into the two visible groups. Because
226
+ // `porcelainToFileChanges` may emit two changes per file (one staged, one
227
+ // unstaged), the same file can show up in both lists when it has been
228
+ // partially staged.
229
+ const { staged, unstaged } = React.useMemo(() => {
230
+ const stagedAcc: IFileChange[] = [];
231
+ const unstagedAcc: IFileChange[] = [];
232
+ if (state.result === null) {
233
+ return { staged: stagedAcc, unstaged: unstagedAcc };
234
+ }
235
+ for (const file of state.result.files) {
236
+ for (const change of porcelainToFileChanges(file)) {
237
+ if (change.group === 'staged') {
238
+ stagedAcc.push(change);
239
+ } else {
240
+ unstagedAcc.push(change);
241
+ }
242
+ }
243
+ }
244
+ stagedAcc.sort((a, b) => a.path.localeCompare(b.path));
245
+ unstagedAcc.sort((a, b) => a.path.localeCompare(b.path));
246
+ return { staged: stagedAcc, unstaged: unstagedAcc };
247
+ }, [state.result]);
248
+
249
+ return (
250
+ <div className="jp-xtralab-GitPanel-content">
251
+ <PanelHeader
252
+ topLevel={topLevel}
253
+ result={state.result}
254
+ loading={state.loading}
255
+ error={state.error}
256
+ onRefresh={() => void refresh()}
257
+ />
258
+ {state.error !== null ? (
259
+ <div className="jp-xtralab-GitPanel-error">{state.error}</div>
260
+ ) : (
261
+ <>
262
+ <ChangeSection
263
+ label="Staged Changes"
264
+ count={staged.length}
265
+ collapsed={stagedCollapsed}
266
+ onToggleCollapsed={() => setStagedCollapsed(c => !c)}
267
+ actions={
268
+ staged.length > 0
269
+ ? [
270
+ {
271
+ label: 'Unstage all changes',
272
+ symbol: '−',
273
+ onClick: onUnstageAll
274
+ }
275
+ ]
276
+ : undefined
277
+ }
278
+ >
279
+ {staged.map(change => (
280
+ <ChangeRow
281
+ key={`${change.group}:${change.path}`}
282
+ change={change}
283
+ pendingAction={pendingActions[`${change.group}:${change.path}`]}
284
+ onClick={() => handlers.openDiff(change)}
285
+ actions={[
286
+ {
287
+ label: 'Unstage changes',
288
+ symbol: '−',
289
+ onClick: () => void onUnstage(change)
290
+ }
291
+ ]}
292
+ />
293
+ ))}
294
+ </ChangeSection>
295
+ <ChangeSection
296
+ label="Changes"
297
+ count={unstaged.length}
298
+ collapsed={changesCollapsed}
299
+ onToggleCollapsed={() => setChangesCollapsed(c => !c)}
300
+ actions={
301
+ unstaged.length > 0
302
+ ? [
303
+ {
304
+ label: 'Stage all changes',
305
+ symbol: '+',
306
+ onClick: onStageAll
307
+ }
308
+ ]
309
+ : undefined
310
+ }
311
+ >
312
+ {unstaged.map(change => (
313
+ <ChangeRow
314
+ key={`${change.group}:${change.path}`}
315
+ change={change}
316
+ pendingAction={pendingActions[`${change.group}:${change.path}`]}
317
+ onClick={() => handlers.openDiff(change)}
318
+ actions={[
319
+ {
320
+ label: 'Discard changes',
321
+ symbol: '⟲',
322
+ onClick: () => void onDiscard(change)
323
+ },
324
+ {
325
+ label: 'Stage changes',
326
+ symbol: '+',
327
+ onClick: () => void onStage(change)
328
+ }
329
+ ]}
330
+ />
331
+ ))}
332
+ </ChangeSection>
333
+ {staged.length === 0 && unstaged.length === 0 && !state.loading ? (
334
+ <div className="jp-xtralab-GitPanel-empty">
335
+ No changes detected.
336
+ </div>
337
+ ) : null}
338
+ </>
339
+ )}
340
+ </div>
341
+ );
342
+ }
343
+
344
+ interface IRowAction {
345
+ label: string;
346
+ symbol: string;
347
+ onClick: () => void;
348
+ }
349
+
350
+ function PanelHeader(props: {
351
+ topLevel: string | null;
352
+ result: IGitStatusResult | null;
353
+ loading: boolean;
354
+ error: string | null;
355
+ onRefresh: () => void;
356
+ }): React.ReactElement {
357
+ const { topLevel, result, loading, error, onRefresh } = props;
358
+ const repoName =
359
+ topLevel !== null
360
+ ? (topLevel
361
+ .split('/')
362
+ .filter(s => s.length > 0)
363
+ .pop() ?? topLevel)
364
+ : null;
365
+ return (
366
+ <div className="jp-xtralab-GitPanel-header">
367
+ <div className="jp-xtralab-GitPanel-headerInfo">
368
+ <div className="jp-xtralab-GitPanel-title">Source Control</div>
369
+ {repoName !== null ? (
370
+ <div
371
+ className="jp-xtralab-GitPanel-repo"
372
+ title={topLevel ?? ''}
373
+ >
374
+ {repoName}
375
+ </div>
376
+ ) : null}
377
+ {result?.branch !== undefined && result?.branch !== null ? (
378
+ <div className="jp-xtralab-GitPanel-branch">
379
+ {result.branch}
380
+ {result.ahead > 0 ? <span> ↑{result.ahead}</span> : null}
381
+ {result.behind > 0 ? <span> ↓{result.behind}</span> : null}
382
+ </div>
383
+ ) : null}
384
+ </div>
385
+ <button
386
+ type="button"
387
+ className="jp-xtralab-GitPanel-iconButton"
388
+ title="Refresh"
389
+ disabled={loading && error === null}
390
+ onClick={onRefresh}
391
+ >
392
+ ⟳
393
+ </button>
394
+ </div>
395
+ );
396
+ }
397
+
398
+ function ChangeSection(props: {
399
+ label: string;
400
+ count: number;
401
+ collapsed: boolean;
402
+ onToggleCollapsed: () => void;
403
+ actions?: IRowAction[];
404
+ children: React.ReactNode;
405
+ }): React.ReactElement {
406
+ const { label, count, collapsed, onToggleCollapsed, actions, children } =
407
+ props;
408
+ return (
409
+ <div className="jp-xtralab-GitPanel-section">
410
+ <div
411
+ className="jp-xtralab-GitPanel-sectionHeader"
412
+ onClick={onToggleCollapsed}
413
+ >
414
+ <span className="jp-xtralab-GitPanel-sectionChevron">
415
+ {collapsed ? '▸' : '▾'}
416
+ </span>
417
+ <span className="jp-xtralab-GitPanel-sectionLabel">
418
+ {label}
419
+ </span>
420
+ {actions !== undefined ? (
421
+ <span className="jp-xtralab-GitPanel-sectionActions">
422
+ {actions.map(action => (
423
+ <button
424
+ key={action.label}
425
+ type="button"
426
+ className="jp-xtralab-GitPanel-iconButton"
427
+ title={action.label}
428
+ onClick={event => {
429
+ event.stopPropagation();
430
+ action.onClick();
431
+ }}
432
+ >
433
+ {action.symbol}
434
+ </button>
435
+ ))}
436
+ </span>
437
+ ) : null}
438
+ <span className="jp-xtralab-GitPanel-sectionCount">
439
+ {count}
440
+ </span>
441
+ </div>
442
+ {!collapsed ? (
443
+ <div className="jp-xtralab-GitPanel-sectionBody">
444
+ {children}
445
+ </div>
446
+ ) : null}
447
+ </div>
448
+ );
449
+ }
450
+
451
+ function ChangeRow(props: {
452
+ change: IFileChange;
453
+ pendingAction: string | undefined;
454
+ onClick: () => void;
455
+ actions: IRowAction[];
456
+ }): React.ReactElement {
457
+ const { change, pendingAction, onClick, actions } = props;
458
+ const filename = basename(change.path);
459
+ const dirname = dirnameOrEmpty(change.path);
460
+ return (
461
+ <div
462
+ className="jp-xtralab-GitPanel-row"
463
+ data-status={change.status}
464
+ title={
465
+ change.from !== undefined
466
+ ? `${change.from} → ${change.path}`
467
+ : change.path
468
+ }
469
+ onClick={onClick}
470
+ >
471
+ <span className="jp-xtralab-GitPanel-rowLabel">
472
+ <span className="jp-xtralab-GitPanel-rowName">{filename}</span>
473
+ {dirname !== '' ? (
474
+ <span className="jp-xtralab-GitPanel-rowPath">{dirname}</span>
475
+ ) : null}
476
+ </span>
477
+ <span
478
+ className="jp-xtralab-GitPanel-rowActions"
479
+ data-pending={pendingAction !== undefined}
480
+ >
481
+ {actions.map(action => (
482
+ <button
483
+ key={action.label}
484
+ type="button"
485
+ className="jp-xtralab-GitPanel-iconButton"
486
+ title={action.label}
487
+ disabled={pendingAction !== undefined}
488
+ onClick={event => {
489
+ event.stopPropagation();
490
+ action.onClick();
491
+ }}
492
+ >
493
+ {action.symbol}
494
+ </button>
495
+ ))}
496
+ </span>
497
+ <span
498
+ className="jp-xtralab-GitPanel-rowBadge"
499
+ data-status={change.status}
500
+ title={statusTooltip(change.status)}
501
+ >
502
+ {statusBadge(change.status)}
503
+ </span>
504
+ </div>
505
+ );
506
+ }
507
+
508
+ function statusBadge(status: FileChangeStatus): string {
509
+ switch (status) {
510
+ case 'modified':
511
+ return 'M';
512
+ case 'added':
513
+ return 'A';
514
+ case 'deleted':
515
+ return 'D';
516
+ case 'renamed':
517
+ return 'R';
518
+ case 'untracked':
519
+ return 'U';
520
+ case 'unmerged':
521
+ return '!';
522
+ case 'typechange':
523
+ return 'T';
524
+ default:
525
+ return '?';
526
+ }
527
+ }
528
+
529
+ function statusTooltip(status: FileChangeStatus): string {
530
+ switch (status) {
531
+ case 'modified':
532
+ return 'Modified';
533
+ case 'added':
534
+ return 'Added';
535
+ case 'deleted':
536
+ return 'Deleted';
537
+ case 'renamed':
538
+ return 'Renamed';
539
+ case 'untracked':
540
+ return 'Untracked';
541
+ case 'unmerged':
542
+ return 'Unmerged (conflict)';
543
+ case 'typechange':
544
+ return 'Type changed';
545
+ default:
546
+ return 'Unknown';
547
+ }
548
+ }
549
+
550
+ function basename(path: string): string {
551
+ const idx = path.lastIndexOf('/');
552
+ return idx < 0 ? path : path.slice(idx + 1);
553
+ }
554
+
555
+ function dirnameOrEmpty(path: string): string {
556
+ const idx = path.lastIndexOf('/');
557
+ return idx < 0 ? '' : path.slice(0, idx);
558
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Types shared by the git plugin. Mirror the response shapes returned by the
3
+ * `jupyterlab_git` server extension's REST API so the frontend can stay
4
+ * tightly coupled to a single version of those endpoints.
5
+ */
6
+
7
+ /**
8
+ * A single entry in the `files` array returned by `POST /git/<path>/status`.
9
+ * `x` and `y` are the porcelain-format index/worktree status codes (see
10
+ * `git status --porcelain`); `to` is the file's current path relative to the
11
+ * git repository root, `from` is the source path for renames (and equal to
12
+ * `to` for any other status).
13
+ */
14
+ export interface IGitStatusFile {
15
+ x: string;
16
+ y: string;
17
+ to: string;
18
+ from: string;
19
+ is_binary: boolean | null;
20
+ }
21
+
22
+ /** Response shape of `POST /git/<path>/status`. */
23
+ export interface IGitStatusResult {
24
+ code: number;
25
+ branch: string | null;
26
+ remote: string | null;
27
+ ahead: number;
28
+ behind: number;
29
+ files: IGitStatusFile[];
30
+ state?: number;
31
+ message?: string;
32
+ }
33
+
34
+ /**
35
+ * Reference accepted by `POST /git/<path>/content` to identify which version
36
+ * of a file to fetch. `WORKING` is the on-disk copy, `INDEX` is the staged
37
+ * copy, and a `git` value is any commit-ish (`HEAD`, a SHA, …).
38
+ */
39
+ export type GitReference =
40
+ | { special: 'WORKING' | 'INDEX' | 'BASE' }
41
+ | { git: string };
42
+
43
+ /**
44
+ * Response shape of `POST /git/<path>/content`. The server only returns text
45
+ * content here; binary files are reported as binary in the status response,
46
+ * and we surface them in the UI without attempting to render their diff.
47
+ */
48
+ export interface IGitContentResult {
49
+ code: number;
50
+ content: string;
51
+ message?: string;
52
+ }
53
+
54
+ /**
55
+ * Where a file's change lives relative to the index.
56
+ *
57
+ * - `staged` → present in the index, may differ from HEAD
58
+ * - `unstaged` → present in the worktree, differs from the index
59
+ *
60
+ * A single file can appear in both groups when the worktree has further
61
+ * changes on top of an already-staged version. The panel models that as two
62
+ * separate entries, one per group.
63
+ */
64
+ export type FileChangeGroup = 'staged' | 'unstaged';
65
+
66
+ /**
67
+ * The user-facing status of a file change. Drives the single-letter badge
68
+ * (M/A/D/R/U/?) shown next to each entry in the panel.
69
+ */
70
+ export type FileChangeStatus =
71
+ | 'modified'
72
+ | 'added'
73
+ | 'deleted'
74
+ | 'renamed'
75
+ | 'untracked'
76
+ | 'unmerged'
77
+ | 'typechange'
78
+ | 'unknown';
79
+
80
+ /**
81
+ * One row in the changes panel. `path` is the file's path relative to the
82
+ * git repository root. `from` is non-`undefined` only for renames, in which
83
+ * case it carries the original path.
84
+ */
85
+ export interface IFileChange {
86
+ path: string;
87
+ from?: string;
88
+ group: FileChangeGroup;
89
+ status: FileChangeStatus;
90
+ isBinary: boolean | null;
91
+ }