@kubohiroya/turbowarp-plus-editor-git 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/editor-block-locator.d.ts +27 -0
- package/dist/file-diff-view.d.ts +3 -1
- package/dist/index.d.ts +25 -0
- package/dist/index.js +243 -101
- package/dist/messages.d.ts +34 -0
- package/dist/project-change-list.d.ts +3 -1
- package/dist/project-source-diff.js +1 -1
- package/dist/project-source-review.d.ts +6 -1
- package/dist/pull-request-panel.d.ts +4 -1
- package/dist/{repository-VBp-UNWc.js → repository-CtvWYPQ8.js} +120 -4
- package/dist/repository.d.ts +17 -0
- package/dist/repository.js +1 -1
- package/dist/{scratch-block-diff-renderer-DZR8ep-T.js → scratch-block-diff-renderer-CgAGfmAc.js} +65 -9
- package/dist/style.css +15 -2
- package/package.json +1 -1
- package/src/connection-settings.tsx +14 -8
- package/src/editor-block-locator.ts +55 -0
- package/src/file-diff-view.tsx +8 -5
- package/src/index.tsx +105 -29
- package/src/messages.ts +34 -26
- package/src/project-change-list.tsx +6 -5
- package/src/project-source-review.tsx +57 -11
- package/src/pull-request-panel.tsx +9 -4
- package/src/repository.ts +76 -3
- package/src/style.css +15 -2
|
@@ -4,6 +4,7 @@ import type {GitHubPullRequests} from './github-pull-requests.js';
|
|
|
4
4
|
import {ProjectSourceReview} from './project-source-review.js';
|
|
5
5
|
import type {ProjectBlockRenderer} from './scratch-block-diff-renderer.js';
|
|
6
6
|
import type {Repository} from './repository.js';
|
|
7
|
+
import type {ProjectBlockLocator} from './block-diff-location.js';
|
|
7
8
|
|
|
8
9
|
const en = {
|
|
9
10
|
title: 'Pull requests', number: 'Pull request number', openNumber: 'Open pull request',
|
|
@@ -37,8 +38,9 @@ type Result<T> = {data: T; error?: never} | {data?: never; error: string};
|
|
|
37
38
|
type Listing = Awaited<ReturnType<GitHubPullRequests['list']>>;
|
|
38
39
|
type Pull = Awaited<ReturnType<GitHubPullRequests['load']>>;
|
|
39
40
|
|
|
40
|
-
function PullRequests({name, labels, locale, renderBlock}: {
|
|
41
|
+
function PullRequests({name, labels, locale, renderBlock, locateFor}: {
|
|
41
42
|
name: string; labels: typeof en; locale: string; renderBlock: ProjectBlockRenderer;
|
|
43
|
+
locateFor?(path: string, headSha: string): ProjectBlockLocator | undefined;
|
|
42
44
|
}) {
|
|
43
45
|
const client = useMemo(() => createGitHubPullRequests(name), [name]);
|
|
44
46
|
const [page, setPage] = useState(1);
|
|
@@ -108,14 +110,17 @@ function PullRequests({name, labels, locale, renderBlock}: {
|
|
|
108
110
|
<code title={pr.head.sha}>{pr.head.ref} → {pr.base.ref}</code>
|
|
109
111
|
<a href={pr.url} target="_blank" rel="noreferrer">{labels.onGitHub}</a>
|
|
110
112
|
</p>
|
|
111
|
-
<ProjectSourceReview title={pr.title} description={pr.description} files={pr.files
|
|
113
|
+
<ProjectSourceReview title={pr.title} description={pr.description} files={locateFor ? pr.files.map(file => 'patch' in file ? {...file, locateBlock: locateFor(file.path, pr.head.sha)} : file) : pr.files}
|
|
114
|
+
renderBlock={renderBlock} locale={locale}/>
|
|
112
115
|
</>}
|
|
113
116
|
</main>
|
|
114
117
|
</div>;
|
|
115
118
|
}
|
|
116
119
|
|
|
117
|
-
export function PullRequestReviewPanel({repository, locale, renderBlock}: {
|
|
120
|
+
export function PullRequestReviewPanel({repository, locale, renderBlock, locateFor}: {
|
|
118
121
|
repository: Repository; locale: string; renderBlock: ProjectBlockRenderer;
|
|
122
|
+
/** Returns a locator for a project file whose PR head matches the open project, or `undefined`. */
|
|
123
|
+
locateFor?(path: string, headSha: string): ProjectBlockLocator | undefined;
|
|
119
124
|
}) {
|
|
120
125
|
const labels = locale.startsWith('ja') ? ja : en;
|
|
121
126
|
const [origin, setOrigin] = useState<{repository: Repository; name: string | null; failed?: boolean}>();
|
|
@@ -130,5 +135,5 @@ export function PullRequestReviewPanel({repository, locale, renderBlock}: {
|
|
|
130
135
|
}, [repository]);
|
|
131
136
|
if (origin?.repository !== repository) return <p role="status" className="twgit-context">{labels.loading}</p>;
|
|
132
137
|
if (!origin.name) return <p className="twgit-context" role="status">{origin.failed ? labels.failed : labels.noOrigin}</p>;
|
|
133
|
-
return <PullRequests key={origin.name} name={origin.name} labels={labels} locale={locale} renderBlock={renderBlock}/>;
|
|
138
|
+
return <PullRequests key={origin.name} name={origin.name} labels={labels} locale={locale} renderBlock={renderBlock} locateFor={locateFor}/>;
|
|
134
139
|
}
|
package/src/repository.ts
CHANGED
|
@@ -45,6 +45,21 @@ export interface Snapshot {
|
|
|
45
45
|
metadata: Metadata;
|
|
46
46
|
/** Size and time fingerprint taken before the read. Compare it with `probe(scope)` to see later changes. */
|
|
47
47
|
probe: string;
|
|
48
|
+
/**
|
|
49
|
+
* How the current branch stands against `refs/remotes/origin/<branch>` as of the last fetch, from local refs only.
|
|
50
|
+
* `unknown` when there is no current branch or no tracking ref yet.
|
|
51
|
+
*/
|
|
52
|
+
upstream: UpstreamRelation;
|
|
53
|
+
}
|
|
54
|
+
export interface UpstreamRelation {
|
|
55
|
+
state: 'same' | 'ahead' | 'behind' | 'diverged' | 'unknown';
|
|
56
|
+
/**
|
|
57
|
+
* Commits on the local branch that the tracking branch lacks, and the reverse. Zero when unknown. Counting reads at
|
|
58
|
+
* most `upstreamWalkLimit` (1000) commits; past that, a side that has commits of its own reports exactly that limit,
|
|
59
|
+
* meaning "this many or more". The state itself comes from the merge base and stays exact.
|
|
60
|
+
*/
|
|
61
|
+
ahead: number;
|
|
62
|
+
behind: number;
|
|
48
63
|
}
|
|
49
64
|
export interface FileDiff {
|
|
50
65
|
binary: boolean;
|
|
@@ -63,6 +78,9 @@ const textLimit = 1024 * 1024;
|
|
|
63
78
|
const inScope = (path: string, scope: string) => !scope || path === scope || path.startsWith(scope + '/');
|
|
64
79
|
// Above this many work-tree files, probes only watch Git metadata. Edits then need a manual refresh.
|
|
65
80
|
const probeLimit = 5000;
|
|
81
|
+
/** Most commits read to count how far a branch is ahead of or behind its tracking branch. */
|
|
82
|
+
export const upstreamWalkLimit = 1000;
|
|
83
|
+
const trackingRef = (branchRef: string) => 'refs/remotes/origin/' + branchRef.slice('refs/heads/'.length);
|
|
66
84
|
const sameMetadata = (a: Metadata, b: Metadata) => a.size === b.size && [...a].every(([path, bytes]) => sameBytes(bytes, b.get(path) ?? null));
|
|
67
85
|
|
|
68
86
|
export async function openRepository(root: FileSystemDirectoryHandle) {
|
|
@@ -89,7 +107,7 @@ export async function openRepository(root: FileSystemDirectoryHandle) {
|
|
|
89
107
|
if (text.startsWith('ref: refs/heads/')) {
|
|
90
108
|
const ref = text.slice(5);
|
|
91
109
|
pathParts(ref);
|
|
92
|
-
paths.push('/.git/' + ref);
|
|
110
|
+
paths.push('/.git/' + ref, '/.git/' + trackingRef(ref));
|
|
93
111
|
} else if (!/^[0-9a-f]{40}$/.test(text)) throw new GitPanelError('invalid');
|
|
94
112
|
const result: Metadata = new Map();
|
|
95
113
|
for (const path of paths) {
|
|
@@ -124,7 +142,7 @@ export async function openRepository(root: FileSystemDirectoryHandle) {
|
|
|
124
142
|
const paths = ['.git/HEAD', '.git/index', '.git/packed-refs', '.git/shallow', '.git/config'];
|
|
125
143
|
if (head.startsWith('ref: refs/heads/')) {
|
|
126
144
|
try {pathParts(head.slice(5));} catch {return 'locked';}
|
|
127
|
-
paths.push('.git/' + head.slice(5));
|
|
145
|
+
paths.push('.git/' + head.slice(5), '.git/' + trackingRef(head.slice(5)));
|
|
128
146
|
}
|
|
129
147
|
const lines: string[] = [];
|
|
130
148
|
for (const path of paths) {
|
|
@@ -169,6 +187,60 @@ export async function openRepository(root: FileSystemDirectoryHandle) {
|
|
|
169
187
|
}) as {path: string; oid: string; mode: number}[];
|
|
170
188
|
return new Map(result.map(({path, oid, mode}) => [path, {oid, mode}]));
|
|
171
189
|
};
|
|
190
|
+
/**
|
|
191
|
+
* Compares `refs/heads/<branch>` with `refs/remotes/origin/<branch>` from local refs only. Counts by painting
|
|
192
|
+
* commits from both tips in committer-time order until every queued commit is reachable from both, like
|
|
193
|
+
* `git rev-list --left-right --count`, reading at most `upstreamWalkLimit` commits.
|
|
194
|
+
*/
|
|
195
|
+
const upstreamRelation = async (branch: string | undefined, head: string | null): Promise<UpstreamRelation> => {
|
|
196
|
+
const unknown: UpstreamRelation = {state: 'unknown', ahead: 0, behind: 0};
|
|
197
|
+
if (!branch || !head) return unknown;
|
|
198
|
+
pathParts(trackingRef('refs/heads/' + branch));
|
|
199
|
+
try {
|
|
200
|
+
const remote = await git.resolveRef({...options, ref: trackingRef('refs/heads/' + branch)});
|
|
201
|
+
if (remote === head) return {state: 'same', ahead: 0, behind: 0};
|
|
202
|
+
const bases = await git.findMergeBase({...options, cache, oids: [head, remote]}) as string[];
|
|
203
|
+
const hasAhead = !bases.includes(head), hasBehind = !bases.includes(remote);
|
|
204
|
+
const local = 1, tracking = 2, both = 3;
|
|
205
|
+
const flags = new Map<string, number>([[head, local], [remote, tracking]]);
|
|
206
|
+
const time = async (oid: string) => (await git.readCommit({...options, cache, oid})).commit;
|
|
207
|
+
const queue: {oid: string; time: number; parents: string[]}[] = [];
|
|
208
|
+
const enqueue = async (oid: string) => {
|
|
209
|
+
const commit = await time(oid);
|
|
210
|
+
queue.push({oid, time: commit.committer.timestamp, parents: commit.parent});
|
|
211
|
+
};
|
|
212
|
+
await enqueue(head); await enqueue(remote);
|
|
213
|
+
let read = 2;
|
|
214
|
+
let complete = true;
|
|
215
|
+
while (queue.some(item => flags.get(item.oid) !== both)) {
|
|
216
|
+
if (read >= upstreamWalkLimit) {complete = false; break;}
|
|
217
|
+
queue.sort((a, b) => b.time - a.time);
|
|
218
|
+
const item = queue.shift()!;
|
|
219
|
+
const flag = flags.get(item.oid)!;
|
|
220
|
+
for (const parent of item.parents) {
|
|
221
|
+
const previous = flags.get(parent) ?? 0;
|
|
222
|
+
if ((previous | flag) === previous) continue;
|
|
223
|
+
flags.set(parent, previous | flag);
|
|
224
|
+
if (!queue.some(entry => entry.oid === parent)) {await enqueue(parent); read++;}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
let ahead = 0, behind = 0;
|
|
228
|
+
for (const flag of flags.values()) {
|
|
229
|
+
if (flag === local) ahead++;
|
|
230
|
+
else if (flag === tracking) behind++;
|
|
231
|
+
}
|
|
232
|
+
if (!complete) {
|
|
233
|
+
ahead = hasAhead ? upstreamWalkLimit : 0;
|
|
234
|
+
behind = hasBehind ? upstreamWalkLimit : 0;
|
|
235
|
+
}
|
|
236
|
+
return {state: hasAhead && hasBehind ? 'diverged' : hasAhead ? 'ahead' : hasBehind ? 'behind' : 'same',
|
|
237
|
+
ahead: hasAhead ? Math.max(ahead, 1) : 0, behind: hasBehind ? Math.max(behind, 1) : 0};
|
|
238
|
+
} catch (error) {
|
|
239
|
+
// No tracking ref yet, or history missing from a shallow clone.
|
|
240
|
+
if (errorCode(error) === 'NotFoundError') return unknown;
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
};
|
|
172
244
|
const assertSnapshot = async (snapshot: Snapshot) => {
|
|
173
245
|
if (!sameMetadata(snapshot.metadata, await metadata())) throw new GitPanelError('changed');
|
|
174
246
|
};
|
|
@@ -189,6 +261,7 @@ export async function openRepository(root: FileSystemDirectoryHandle) {
|
|
|
189
261
|
const commits = head ? (await git.log({...options, cache, ref: head, depth: 50})).map(({oid, commit}) => ({
|
|
190
262
|
oid, message: commit.message, author: commit.author.name, timestamp: commit.author.timestamp
|
|
191
263
|
})) : [];
|
|
264
|
+
const upstream = await upstreamRelation(branch || undefined, head);
|
|
192
265
|
const matrix = await git.statusMatrix({...options, cache, refresh: false, filepaths: [scope || '.']});
|
|
193
266
|
const staged = await entries(null, scope);
|
|
194
267
|
const committed = await entries(head || emptyTree, scope);
|
|
@@ -210,7 +283,7 @@ export async function openRepository(root: FileSystemDirectoryHandle) {
|
|
|
210
283
|
}
|
|
211
284
|
if (!sameMetadata(before, await metadata())) throw new GitPanelError('changed');
|
|
212
285
|
await fs.checkReads();
|
|
213
|
-
return {branch: branch || undefined, head, branches, commits, changes, scope, metadata: before, probe: fingerprint};
|
|
286
|
+
return {branch: branch || undefined, head, branches, commits, changes, scope, metadata: before, probe: fingerprint, upstream};
|
|
214
287
|
};
|
|
215
288
|
const historyChanges = async (snapshot: Snapshot, oid: string): Promise<FileChange[]> => {
|
|
216
289
|
fs.clearReads();
|
package/src/style.css
CHANGED
|
@@ -8,14 +8,24 @@
|
|
|
8
8
|
.twgit-panel button:focus-visible, .twgit-panel summary:focus-visible { outline: 2px solid Highlight; outline-offset: 2px; }
|
|
9
9
|
.twgit-panel button[aria-pressed="true"] { color: HighlightText; background: Highlight; }
|
|
10
10
|
.twgit-context { display: flex; gap: 8px 20px; flex-wrap: wrap; padding: 8px 16px; }
|
|
11
|
-
.twgit-
|
|
11
|
+
.twgit-context { align-items: center; }
|
|
12
|
+
.twgit-connection, .twgit-git-details { position: relative; }
|
|
13
|
+
.twgit-connection > summary, .twgit-git-details > summary { cursor: pointer; }
|
|
14
|
+
/* Settings and Git details open over the list, so the list and the diff stay where they are. */
|
|
15
|
+
.twgit-connection-body { position: absolute; z-index: 5; top: calc(100% + 4px); left: 0; width: min(440px, 80vw); max-height: 60vh;
|
|
16
|
+
overflow: auto; padding: 10px 12px; background: Canvas; color: CanvasText; border: 1px solid GrayText; border-radius: 6px;
|
|
17
|
+
box-shadow: 0 8px 24px #0004; }
|
|
18
|
+
.twgit-connection-body > p { margin: 4px 0; overflow-wrap: anywhere; }
|
|
19
|
+
.twgit-sync { display: inline-flex; flex-wrap: wrap; gap: 4px; }
|
|
20
|
+
.twgit-change-kind { display: inline-block; min-width: 2.6em; font-size: 11px; opacity: .8; }
|
|
21
|
+
.twgit-file-details { font-size: 12px; margin-bottom: 6px; }
|
|
22
|
+
.twgit-file-details > p { margin: 2px 0; }
|
|
12
23
|
.twgit-content { display: grid; grid-template-columns: minmax(230px, 30%) minmax(0, 1fr); flex: 1; min-height: 0; }
|
|
13
24
|
.twgit-panel aside { padding: 0 14px 14px; overflow: auto; border-right: 1px solid GrayText; }
|
|
14
25
|
.twgit-panel aside ul { list-style: none; padding: 0; margin: 4px 0; }
|
|
15
26
|
.twgit-panel aside li { overflow-wrap: anywhere; }
|
|
16
27
|
.twgit-panel summary { cursor: pointer; }
|
|
17
28
|
.twgit-panel .twgit-file { width: 100%; border: 0; margin: 1px 0; }
|
|
18
|
-
.twgit-file span { display: inline-block; width: 1.4em; font-family: monospace; }
|
|
19
29
|
.twgit-diff { padding: 0 16px 16px; min-width: 0; overflow: auto; }
|
|
20
30
|
.twgit-diff .diff { font-size: 12px; }
|
|
21
31
|
.twgit-history { list-style: none; margin: 0; padding: 0; }
|
|
@@ -44,3 +54,6 @@
|
|
|
44
54
|
.twgit-element-values td { font-family: ui-monospace, monospace; }
|
|
45
55
|
.twgit-element-files { margin-top: 12px; }
|
|
46
56
|
.twgit-panel .twgit-element span.twgit-element-name { width: auto; font-family: inherit; }
|
|
57
|
+
.twgit-git-details > .twgit-connection-body { left: auto; right: 0; }
|
|
58
|
+
/* In a layout tab the panel fills the tab and leaves the Editor beside it. */
|
|
59
|
+
.twgit-panel.twgit-panel-tab { width: 100%; height: 100%; border: 0; border-radius: 0; box-shadow: none; }
|