diffity 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.
- package/.claude/settings.local.json +11 -0
- package/LICENSE +21 -0
- package/README.md +71 -0
- package/development.md +156 -0
- package/package.json +32 -0
- package/packages/cli/build.js +38 -0
- package/packages/cli/package.json +51 -0
- package/packages/cli/src/agent.ts +187 -0
- package/packages/cli/src/db.ts +58 -0
- package/packages/cli/src/index.ts +196 -0
- package/packages/cli/src/review-routes.ts +150 -0
- package/packages/cli/src/server.ts +370 -0
- package/packages/cli/src/session.ts +48 -0
- package/packages/cli/src/threads.ts +238 -0
- package/packages/cli/tsconfig.json +13 -0
- package/packages/git/package.json +24 -0
- package/packages/git/src/commits.ts +28 -0
- package/packages/git/src/diff.ts +97 -0
- package/packages/git/src/exec.ts +35 -0
- package/packages/git/src/index.ts +5 -0
- package/packages/git/src/repo.ts +63 -0
- package/packages/git/src/status.ts +9 -0
- package/packages/git/src/types.ts +12 -0
- package/packages/git/tsconfig.json +9 -0
- package/packages/parser/package.json +26 -0
- package/packages/parser/src/index.ts +12 -0
- package/packages/parser/src/parse.ts +299 -0
- package/packages/parser/src/types.ts +52 -0
- package/packages/parser/src/word-diff.ts +155 -0
- package/packages/parser/tests/fixtures/binary-deleted.diff +4 -0
- package/packages/parser/tests/fixtures/binary-file.diff +4 -0
- package/packages/parser/tests/fixtures/binary-modified.diff +3 -0
- package/packages/parser/tests/fixtures/copied-file.diff +12 -0
- package/packages/parser/tests/fixtures/deleted-file.diff +9 -0
- package/packages/parser/tests/fixtures/empty.diff +0 -0
- package/packages/parser/tests/fixtures/hunk-with-context.diff +12 -0
- package/packages/parser/tests/fixtures/mode-change-with-content.diff +10 -0
- package/packages/parser/tests/fixtures/mode-change.diff +3 -0
- package/packages/parser/tests/fixtures/multi-file.diff +22 -0
- package/packages/parser/tests/fixtures/new-file.diff +9 -0
- package/packages/parser/tests/fixtures/no-newline.diff +10 -0
- package/packages/parser/tests/fixtures/renamed-file.diff +12 -0
- package/packages/parser/tests/fixtures/single-file-additions.diff +11 -0
- package/packages/parser/tests/fixtures/single-file-deletions.diff +11 -0
- package/packages/parser/tests/fixtures/single-file-mixed.diff +15 -0
- package/packages/parser/tests/fixtures/single-file-multi-hunk.diff +22 -0
- package/packages/parser/tests/fixtures/spaces-in-path.diff +9 -0
- package/packages/parser/tests/fixtures/submodule.diff +7 -0
- package/packages/parser/tests/fixtures/unicode-content.diff +11 -0
- package/packages/parser/tests/parse.test.ts +312 -0
- package/packages/parser/tests/word-diff-integration.test.ts +52 -0
- package/packages/parser/tests/word-diff.test.ts +121 -0
- package/packages/parser/tsconfig.json +10 -0
- package/packages/skills/diffity-resolve/SKILL.md +55 -0
- package/packages/skills/diffity-review/SKILL.md +74 -0
- package/packages/skills/diffity-start/SKILL.md +25 -0
- package/packages/ui/index.html +13 -0
- package/packages/ui/package.json +35 -0
- package/packages/ui/public/brand.svg +12 -0
- package/packages/ui/public/favicon.svg +15 -0
- package/packages/ui/src/app.tsx +14 -0
- package/packages/ui/src/components/comment-bubble.tsx +78 -0
- package/packages/ui/src/components/comment-form-row.tsx +58 -0
- package/packages/ui/src/components/comment-form.tsx +78 -0
- package/packages/ui/src/components/comment-line-number.tsx +60 -0
- package/packages/ui/src/components/comment-thread.tsx +209 -0
- package/packages/ui/src/components/commit-list.tsx +100 -0
- package/packages/ui/src/components/dashboard.tsx +84 -0
- package/packages/ui/src/components/diff-line.tsx +90 -0
- package/packages/ui/src/components/diff-page.tsx +332 -0
- package/packages/ui/src/components/diff-stats.tsx +20 -0
- package/packages/ui/src/components/diff-view.tsx +278 -0
- package/packages/ui/src/components/expand-row.tsx +45 -0
- package/packages/ui/src/components/file-block.tsx +536 -0
- package/packages/ui/src/components/file-tree-item.tsx +84 -0
- package/packages/ui/src/components/file-tree.tsx +72 -0
- package/packages/ui/src/components/general-comments.tsx +174 -0
- package/packages/ui/src/components/hunk-block-split.tsx +357 -0
- package/packages/ui/src/components/hunk-block.tsx +161 -0
- package/packages/ui/src/components/hunk-header.tsx +144 -0
- package/packages/ui/src/components/hunk-with-gap.tsx +113 -0
- package/packages/ui/src/components/icons/arrow-down-icon.tsx +7 -0
- package/packages/ui/src/components/icons/arrow-up-icon.tsx +7 -0
- package/packages/ui/src/components/icons/check-circle-icon.tsx +8 -0
- package/packages/ui/src/components/icons/check-icon.tsx +9 -0
- package/packages/ui/src/components/icons/chevron-down-icon.tsx +11 -0
- package/packages/ui/src/components/icons/chevron-icon.tsx +20 -0
- package/packages/ui/src/components/icons/chevron-up-down-icon.tsx +7 -0
- package/packages/ui/src/components/icons/chevron-up-icon.tsx +11 -0
- package/packages/ui/src/components/icons/comment-icon.tsx +9 -0
- package/packages/ui/src/components/icons/copy-icon.tsx +10 -0
- package/packages/ui/src/components/icons/eye-icon.tsx +10 -0
- package/packages/ui/src/components/icons/eye-off-icon.tsx +12 -0
- package/packages/ui/src/components/icons/file-icon.tsx +7 -0
- package/packages/ui/src/components/icons/folder-icon.tsx +19 -0
- package/packages/ui/src/components/icons/git-branch-icon.tsx +13 -0
- package/packages/ui/src/components/icons/keyboard-icon.tsx +13 -0
- package/packages/ui/src/components/icons/moon-icon.tsx +9 -0
- package/packages/ui/src/components/icons/plus-icon.tsx +9 -0
- package/packages/ui/src/components/icons/search-icon.tsx +10 -0
- package/packages/ui/src/components/icons/sidebar-icon.tsx +10 -0
- package/packages/ui/src/components/icons/spinner.tsx +7 -0
- package/packages/ui/src/components/icons/split-view-icon.tsx +10 -0
- package/packages/ui/src/components/icons/sun-icon.tsx +17 -0
- package/packages/ui/src/components/icons/trash-icon.tsx +11 -0
- package/packages/ui/src/components/icons/undo-icon.tsx +9 -0
- package/packages/ui/src/components/icons/unified-view-icon.tsx +12 -0
- package/packages/ui/src/components/icons/x-icon.tsx +10 -0
- package/packages/ui/src/components/line-number-cell.tsx +18 -0
- package/packages/ui/src/components/markdown-content.tsx +139 -0
- package/packages/ui/src/components/orphaned-threads.tsx +80 -0
- package/packages/ui/src/components/overview-file-list.tsx +57 -0
- package/packages/ui/src/components/render-expansion-rows.tsx +47 -0
- package/packages/ui/src/components/shortcut-modal.tsx +93 -0
- package/packages/ui/src/components/sidebar.tsx +80 -0
- package/packages/ui/src/components/skeleton.tsx +9 -0
- package/packages/ui/src/components/stale-diff-banner.tsx +21 -0
- package/packages/ui/src/components/summary-bar.tsx +39 -0
- package/packages/ui/src/components/toolbar.tsx +246 -0
- package/packages/ui/src/components/ui/badge.tsx +17 -0
- package/packages/ui/src/components/ui/confirm-dialog.tsx +52 -0
- package/packages/ui/src/components/ui/icon-button.tsx +23 -0
- package/packages/ui/src/components/ui/status-badge.tsx +57 -0
- package/packages/ui/src/components/ui/thread-badge.tsx +35 -0
- package/packages/ui/src/components/word-diff.tsx +126 -0
- package/packages/ui/src/hooks/use-comment-actions.ts +97 -0
- package/packages/ui/src/hooks/use-commits.ts +12 -0
- package/packages/ui/src/hooks/use-copy.ts +18 -0
- package/packages/ui/src/hooks/use-diff-staleness.ts +58 -0
- package/packages/ui/src/hooks/use-diff.ts +12 -0
- package/packages/ui/src/hooks/use-highlighter.ts +190 -0
- package/packages/ui/src/hooks/use-info.ts +12 -0
- package/packages/ui/src/hooks/use-keyboard.ts +55 -0
- package/packages/ui/src/hooks/use-line-selection.ts +157 -0
- package/packages/ui/src/hooks/use-overview.ts +12 -0
- package/packages/ui/src/hooks/use-review-threads.ts +12 -0
- package/packages/ui/src/hooks/use-search-params.ts +26 -0
- package/packages/ui/src/hooks/use-theme.ts +34 -0
- package/packages/ui/src/hooks/use-thread-navigation.ts +43 -0
- package/packages/ui/src/lib/api.ts +232 -0
- package/packages/ui/src/lib/cn.ts +6 -0
- package/packages/ui/src/lib/context-expansion.ts +122 -0
- package/packages/ui/src/lib/diff-utils.ts +268 -0
- package/packages/ui/src/lib/dom-utils.ts +13 -0
- package/packages/ui/src/lib/file-tree.ts +122 -0
- package/packages/ui/src/lib/query-client.ts +10 -0
- package/packages/ui/src/lib/render-content.tsx +23 -0
- package/packages/ui/src/lib/syntax-token.ts +4 -0
- package/packages/ui/src/main.tsx +14 -0
- package/packages/ui/src/queries/commits.ts +9 -0
- package/packages/ui/src/queries/diff.ts +9 -0
- package/packages/ui/src/queries/file.ts +10 -0
- package/packages/ui/src/queries/info.ts +9 -0
- package/packages/ui/src/queries/overview.ts +9 -0
- package/packages/ui/src/styles/app.css +178 -0
- package/packages/ui/src/types/comment.ts +61 -0
- package/packages/ui/src/vite-env.d.ts +1 -0
- package/packages/ui/tests/context-expansion.test.ts +279 -0
- package/packages/ui/tests/diff-utils.test.ts +409 -0
- package/packages/ui/tsconfig.json +14 -0
- package/packages/ui/vite.config.ts +23 -0
- package/scripts/build-skills.ts +26 -0
- package/scripts/build.ts +15 -0
- package/scripts/dev.ts +32 -0
- package/scripts/lib/transformers/claude-code.ts +11 -0
- package/scripts/lib/transformers/codex.ts +17 -0
- package/scripts/lib/transformers/cursor.ts +17 -0
- package/scripts/lib/transformers/index.ts +3 -0
- package/scripts/lib/utils.ts +70 -0
- package/scripts/link-dev.ts +54 -0
- package/skills/diffity-resolve/SKILL.md +55 -0
- package/skills/diffity-review/SKILL.md +74 -0
- package/skills/diffity-start/SKILL.md +27 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import type { ParsedDiff } from '@diffity/parser';
|
|
2
|
+
import type { CommentThread, CommentAuthor, CommentSide, Comment } from '../types/comment';
|
|
3
|
+
|
|
4
|
+
export interface RepoInfo {
|
|
5
|
+
name: string;
|
|
6
|
+
branch: string;
|
|
7
|
+
root: string;
|
|
8
|
+
description: string;
|
|
9
|
+
capabilities?: { reviews: boolean };
|
|
10
|
+
sessionId?: string | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface Commit {
|
|
14
|
+
hash: string;
|
|
15
|
+
shortHash: string;
|
|
16
|
+
message: string;
|
|
17
|
+
relativeDate: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface OverviewFile {
|
|
21
|
+
path: string;
|
|
22
|
+
status: 'staged' | 'modified' | 'added';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface Overview {
|
|
26
|
+
files: OverviewFile[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface CommitsPage {
|
|
30
|
+
commits: Commit[];
|
|
31
|
+
hasMore: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function fetchDiff(hideWhitespace: boolean, ref?: string): Promise<ParsedDiff> {
|
|
35
|
+
const params = new URLSearchParams();
|
|
36
|
+
if (hideWhitespace) {
|
|
37
|
+
params.set('whitespace', 'hide');
|
|
38
|
+
}
|
|
39
|
+
if (ref) {
|
|
40
|
+
params.set('ref', ref);
|
|
41
|
+
}
|
|
42
|
+
const query = params.toString();
|
|
43
|
+
const url = query ? `/api/diff?${query}` : '/api/diff';
|
|
44
|
+
const res = await fetch(url);
|
|
45
|
+
if (!res.ok) {
|
|
46
|
+
throw new Error(`HTTP ${res.status}`);
|
|
47
|
+
}
|
|
48
|
+
return res.json();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function fetchDiffFingerprint(ref?: string): Promise<string> {
|
|
52
|
+
const params = new URLSearchParams();
|
|
53
|
+
if (ref) {
|
|
54
|
+
params.set('ref', ref);
|
|
55
|
+
}
|
|
56
|
+
const query = params.toString();
|
|
57
|
+
const url = query ? `/api/diff-fingerprint?${query}` : '/api/diff-fingerprint';
|
|
58
|
+
const res = await fetch(url);
|
|
59
|
+
if (!res.ok) {
|
|
60
|
+
throw new Error(`HTTP ${res.status}`);
|
|
61
|
+
}
|
|
62
|
+
const json = await res.json();
|
|
63
|
+
return json.fingerprint;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function fetchRepoInfo(ref?: string): Promise<RepoInfo> {
|
|
67
|
+
const params = new URLSearchParams();
|
|
68
|
+
if (ref) {
|
|
69
|
+
params.set('ref', ref);
|
|
70
|
+
}
|
|
71
|
+
const query = params.toString();
|
|
72
|
+
const url = query ? `/api/info?${query}` : '/api/info';
|
|
73
|
+
const res = await fetch(url);
|
|
74
|
+
if (!res.ok) {
|
|
75
|
+
throw new Error(`HTTP ${res.status}`);
|
|
76
|
+
}
|
|
77
|
+
return res.json();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function fetchOverview(): Promise<Overview> {
|
|
81
|
+
const res = await fetch('/api/overview');
|
|
82
|
+
if (!res.ok) {
|
|
83
|
+
throw new Error(`HTTP ${res.status}`);
|
|
84
|
+
}
|
|
85
|
+
return res.json();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function fetchCommits(skip = 0, count = 10, search?: string): Promise<CommitsPage> {
|
|
89
|
+
const params = new URLSearchParams({ skip: String(skip), count: String(count) });
|
|
90
|
+
if (search) {
|
|
91
|
+
params.set('search', search);
|
|
92
|
+
}
|
|
93
|
+
const res = await fetch(`/api/commits?${params}`);
|
|
94
|
+
if (!res.ok) {
|
|
95
|
+
throw new Error(`HTTP ${res.status}`);
|
|
96
|
+
}
|
|
97
|
+
return res.json();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function fetchSession(): Promise<{ id: string; ref: string; headHash: string } | null> {
|
|
101
|
+
const res = await fetch('/api/sessions/current');
|
|
102
|
+
if (!res.ok) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
return res.json();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function fetchThreads(sessionId: string, status?: string): Promise<CommentThread[]> {
|
|
109
|
+
const params = new URLSearchParams({ session: sessionId });
|
|
110
|
+
if (status) {
|
|
111
|
+
params.set('status', status);
|
|
112
|
+
}
|
|
113
|
+
const res = await fetch(`/api/threads?${params}`);
|
|
114
|
+
if (!res.ok) {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
return res.json();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function createThread(data: {
|
|
121
|
+
sessionId: string;
|
|
122
|
+
filePath: string;
|
|
123
|
+
side: CommentSide;
|
|
124
|
+
startLine: number;
|
|
125
|
+
endLine: number;
|
|
126
|
+
body: string;
|
|
127
|
+
author: CommentAuthor;
|
|
128
|
+
anchorContent?: string;
|
|
129
|
+
}): Promise<CommentThread> {
|
|
130
|
+
const res = await fetch('/api/threads', {
|
|
131
|
+
method: 'POST',
|
|
132
|
+
headers: { 'Content-Type': 'application/json' },
|
|
133
|
+
body: JSON.stringify(data),
|
|
134
|
+
});
|
|
135
|
+
if (!res.ok) {
|
|
136
|
+
throw new Error(`HTTP ${res.status}`);
|
|
137
|
+
}
|
|
138
|
+
return res.json();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function replyToThread(threadId: string, body: string, author: CommentAuthor): Promise<Comment> {
|
|
142
|
+
const res = await fetch(`/api/threads/${threadId}/reply`, {
|
|
143
|
+
method: 'POST',
|
|
144
|
+
headers: { 'Content-Type': 'application/json' },
|
|
145
|
+
body: JSON.stringify({ body, author }),
|
|
146
|
+
});
|
|
147
|
+
if (!res.ok) {
|
|
148
|
+
throw new Error(`HTTP ${res.status}`);
|
|
149
|
+
}
|
|
150
|
+
return res.json();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function updateThreadStatus(threadId: string, status: string, summary?: string): Promise<void> {
|
|
154
|
+
const res = await fetch(`/api/threads/${threadId}/status`, {
|
|
155
|
+
method: 'PATCH',
|
|
156
|
+
headers: { 'Content-Type': 'application/json' },
|
|
157
|
+
body: JSON.stringify({ status, summary }),
|
|
158
|
+
});
|
|
159
|
+
if (!res.ok) {
|
|
160
|
+
throw new Error(`HTTP ${res.status}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function deleteAllThreads(sessionId: string): Promise<void> {
|
|
165
|
+
const res = await fetch('/api/threads', {
|
|
166
|
+
method: 'DELETE',
|
|
167
|
+
headers: { 'Content-Type': 'application/json' },
|
|
168
|
+
body: JSON.stringify({ sessionId }),
|
|
169
|
+
});
|
|
170
|
+
if (!res.ok) {
|
|
171
|
+
throw new Error(`HTTP ${res.status}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function deleteThread(threadId: string): Promise<void> {
|
|
176
|
+
const res = await fetch(`/api/threads/${threadId}`, {
|
|
177
|
+
method: 'DELETE',
|
|
178
|
+
});
|
|
179
|
+
if (!res.ok) {
|
|
180
|
+
throw new Error(`HTTP ${res.status}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function deleteComment(commentId: string): Promise<void> {
|
|
185
|
+
const res = await fetch(`/api/comments/${commentId}`, {
|
|
186
|
+
method: 'DELETE',
|
|
187
|
+
});
|
|
188
|
+
if (!res.ok) {
|
|
189
|
+
throw new Error(`HTTP ${res.status}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function revertFile(filePath: string, isUntracked: boolean): Promise<void> {
|
|
194
|
+
const res = await fetch('/api/revert-file', {
|
|
195
|
+
method: 'POST',
|
|
196
|
+
headers: { 'Content-Type': 'application/json' },
|
|
197
|
+
body: JSON.stringify({ filePath, isUntracked }),
|
|
198
|
+
});
|
|
199
|
+
if (!res.ok) {
|
|
200
|
+
const json = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
|
|
201
|
+
throw new Error(json.error || `HTTP ${res.status}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function revertHunk(patch: string): Promise<void> {
|
|
206
|
+
const res = await fetch('/api/revert-hunk', {
|
|
207
|
+
method: 'POST',
|
|
208
|
+
headers: { 'Content-Type': 'application/json' },
|
|
209
|
+
body: JSON.stringify({ patch }),
|
|
210
|
+
});
|
|
211
|
+
if (!res.ok) {
|
|
212
|
+
const json = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
|
|
213
|
+
throw new Error(json.error || `HTTP ${res.status}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export async function fetchFileContent(filePath: string, ref?: string): Promise<string[]> {
|
|
218
|
+
const params = new URLSearchParams();
|
|
219
|
+
if (ref) {
|
|
220
|
+
params.set('ref', ref);
|
|
221
|
+
}
|
|
222
|
+
const query = params.toString();
|
|
223
|
+
const url = query
|
|
224
|
+
? `/api/file/${encodeURIComponent(filePath)}?${query}`
|
|
225
|
+
: `/api/file/${encodeURIComponent(filePath)}`;
|
|
226
|
+
const res = await fetch(url);
|
|
227
|
+
if (!res.ok) {
|
|
228
|
+
throw new Error(`HTTP ${res.status}`);
|
|
229
|
+
}
|
|
230
|
+
const json = await res.json();
|
|
231
|
+
return json.content as string[];
|
|
232
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { DiffHunk, DiffLine } from '@diffity/parser';
|
|
2
|
+
|
|
3
|
+
const EXPAND_CHUNK_SIZE = 20;
|
|
4
|
+
|
|
5
|
+
export interface ExpandableGap {
|
|
6
|
+
id: string;
|
|
7
|
+
position: 'top' | 'between' | 'bottom';
|
|
8
|
+
oldStart: number;
|
|
9
|
+
oldEnd: number;
|
|
10
|
+
newStart: number;
|
|
11
|
+
newEnd: number;
|
|
12
|
+
totalLines: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function computeGaps(hunks: DiffHunk[], fileLineCount: number | null): ExpandableGap[] {
|
|
16
|
+
if (hunks.length === 0) {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const gaps: ExpandableGap[] = [];
|
|
21
|
+
|
|
22
|
+
const firstHunk = hunks[0];
|
|
23
|
+
if (firstHunk.oldStart > 1) {
|
|
24
|
+
const hiddenLines = firstHunk.oldStart - 1;
|
|
25
|
+
gaps.push({
|
|
26
|
+
id: 'top',
|
|
27
|
+
position: 'top',
|
|
28
|
+
oldStart: 1,
|
|
29
|
+
oldEnd: firstHunk.oldStart - 1,
|
|
30
|
+
newStart: 1,
|
|
31
|
+
newEnd: firstHunk.newStart - 1,
|
|
32
|
+
totalLines: hiddenLines,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < hunks.length - 1; i++) {
|
|
37
|
+
const current = hunks[i];
|
|
38
|
+
const next = hunks[i + 1];
|
|
39
|
+
const currentEnd = current.oldStart + current.oldCount;
|
|
40
|
+
const nextStart = next.oldStart;
|
|
41
|
+
const hiddenLines = nextStart - currentEnd;
|
|
42
|
+
|
|
43
|
+
if (hiddenLines > 0) {
|
|
44
|
+
const currentNewEnd = current.newStart + current.newCount;
|
|
45
|
+
gaps.push({
|
|
46
|
+
id: `between-${i}`,
|
|
47
|
+
position: 'between',
|
|
48
|
+
oldStart: currentEnd,
|
|
49
|
+
oldEnd: nextStart - 1,
|
|
50
|
+
newStart: currentNewEnd,
|
|
51
|
+
newEnd: next.newStart - 1,
|
|
52
|
+
totalLines: hiddenLines,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const lastHunk = hunks[hunks.length - 1];
|
|
58
|
+
const lastOldEnd = lastHunk.oldStart + lastHunk.oldCount;
|
|
59
|
+
const lastNewEnd = lastHunk.newStart + lastHunk.newCount;
|
|
60
|
+
|
|
61
|
+
if (fileLineCount !== null) {
|
|
62
|
+
const remaining = fileLineCount - lastOldEnd + 1;
|
|
63
|
+
|
|
64
|
+
if (remaining > 0) {
|
|
65
|
+
gaps.push({
|
|
66
|
+
id: 'bottom',
|
|
67
|
+
position: 'bottom',
|
|
68
|
+
oldStart: lastOldEnd,
|
|
69
|
+
oldEnd: fileLineCount,
|
|
70
|
+
newStart: lastNewEnd,
|
|
71
|
+
newEnd: lastNewEnd + remaining - 1,
|
|
72
|
+
totalLines: remaining,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return gaps;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function createContextLines(
|
|
81
|
+
fileLines: string[],
|
|
82
|
+
oldStart: number,
|
|
83
|
+
oldEnd: number,
|
|
84
|
+
newOffset: number
|
|
85
|
+
): DiffLine[] {
|
|
86
|
+
const lines: DiffLine[] = [];
|
|
87
|
+
for (let i = oldStart; i <= oldEnd; i++) {
|
|
88
|
+
const content = fileLines[i - 1] ?? '';
|
|
89
|
+
lines.push({
|
|
90
|
+
type: 'context',
|
|
91
|
+
content,
|
|
92
|
+
oldLineNumber: i,
|
|
93
|
+
newLineNumber: i + newOffset,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return lines;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function getExpandRange(
|
|
100
|
+
gap: ExpandableGap,
|
|
101
|
+
direction: 'up' | 'down' | 'all',
|
|
102
|
+
alreadyExpanded: { fromTop: number; fromBottom: number }
|
|
103
|
+
): { oldStart: number; oldEnd: number } | null {
|
|
104
|
+
const remainingStart = gap.oldStart + alreadyExpanded.fromTop;
|
|
105
|
+
const remainingEnd = gap.oldEnd - alreadyExpanded.fromBottom;
|
|
106
|
+
|
|
107
|
+
if (remainingStart > remainingEnd) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const remaining = remainingEnd - remainingStart + 1;
|
|
112
|
+
|
|
113
|
+
if (direction === 'all' || remaining <= EXPAND_CHUNK_SIZE) {
|
|
114
|
+
return { oldStart: remainingStart, oldEnd: remainingEnd };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (direction === 'down') {
|
|
118
|
+
return { oldStart: remainingStart, oldEnd: Math.min(remainingStart + EXPAND_CHUNK_SIZE - 1, remainingEnd) };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { oldStart: Math.max(remainingEnd - EXPAND_CHUNK_SIZE + 1, remainingStart), oldEnd: remainingEnd };
|
|
122
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import type { DiffFile, DiffHunk } from '@diffity/parser';
|
|
2
|
+
import type { CommentSide } from '../types/comment';
|
|
3
|
+
|
|
4
|
+
export type ViewMode = 'unified' | 'split';
|
|
5
|
+
|
|
6
|
+
const WORKING_TREE_REFS = new Set(['work', 'staged', 'unstaged', 'working', 'untracked']);
|
|
7
|
+
|
|
8
|
+
export function isWorkingTreeRef(ref?: string): boolean {
|
|
9
|
+
return !!ref && WORKING_TREE_REFS.has(ref);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getFilePath(file: DiffFile): string {
|
|
13
|
+
if (file.status === 'deleted') {
|
|
14
|
+
return file.oldPath;
|
|
15
|
+
}
|
|
16
|
+
return file.newPath;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getLineBg(type: string): string {
|
|
20
|
+
switch (type) {
|
|
21
|
+
case 'add':
|
|
22
|
+
return 'bg-diff-add-bg';
|
|
23
|
+
case 'delete':
|
|
24
|
+
return 'bg-diff-del-bg';
|
|
25
|
+
default:
|
|
26
|
+
return 'bg-bg';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const LOCK_FILES = new Set([
|
|
31
|
+
'package-lock.json',
|
|
32
|
+
'pnpm-lock.yaml',
|
|
33
|
+
'pnpm-lock.yml',
|
|
34
|
+
'bun.lock',
|
|
35
|
+
'bun.lockb',
|
|
36
|
+
'yarn.lock',
|
|
37
|
+
'Cargo.lock',
|
|
38
|
+
'Gemfile.lock',
|
|
39
|
+
'composer.lock',
|
|
40
|
+
'poetry.lock',
|
|
41
|
+
'Pipfile.lock',
|
|
42
|
+
'go.sum',
|
|
43
|
+
'flake.lock',
|
|
44
|
+
'pubspec.lock',
|
|
45
|
+
'Podfile.lock',
|
|
46
|
+
'packages.lock.json',
|
|
47
|
+
'project.assets.json',
|
|
48
|
+
'paket.lock',
|
|
49
|
+
'pnpm-workspace.yaml',
|
|
50
|
+
'shrinkwrap.yaml',
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
const GENERATED_EXTENSIONS = [
|
|
54
|
+
'.min.js',
|
|
55
|
+
'.min.css',
|
|
56
|
+
'.min.mjs',
|
|
57
|
+
'.bundle.js',
|
|
58
|
+
'.bundle.css',
|
|
59
|
+
'.chunk.js',
|
|
60
|
+
'.chunk.css',
|
|
61
|
+
'.generated.ts',
|
|
62
|
+
'.generated.js',
|
|
63
|
+
'.g.dart',
|
|
64
|
+
'.freezed.dart',
|
|
65
|
+
'.pb.go',
|
|
66
|
+
'.pb.ts',
|
|
67
|
+
'.pb.js',
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
const GENERATED_PATTERNS = [
|
|
71
|
+
/\.d\.ts$/,
|
|
72
|
+
/\.map$/,
|
|
73
|
+
/\.snap$/,
|
|
74
|
+
/dist\//,
|
|
75
|
+
/build\//,
|
|
76
|
+
/generated\//,
|
|
77
|
+
/__generated__\//,
|
|
78
|
+
/\.lock$/,
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
function isAutoCollapsible(file: DiffFile): boolean {
|
|
82
|
+
if (file.status === 'deleted' || file.status === 'renamed') {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const path = getFilePath(file);
|
|
87
|
+
const fileName = path.split('/').pop() || '';
|
|
88
|
+
|
|
89
|
+
if (LOCK_FILES.has(fileName)) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const lowerPath = path.toLowerCase();
|
|
94
|
+
for (const ext of GENERATED_EXTENSIONS) {
|
|
95
|
+
if (lowerPath.endsWith(ext)) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (const pattern of GENERATED_PATTERNS) {
|
|
101
|
+
if (pattern.test(path)) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function getAutoCollapsedPaths(files: DiffFile[]): Set<string> {
|
|
110
|
+
const paths = new Set<string>();
|
|
111
|
+
for (const file of files) {
|
|
112
|
+
if (isAutoCollapsible(file)) {
|
|
113
|
+
paths.add(getFilePath(file));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return paths;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function buildHunkPatch(file: DiffFile, hunk: DiffHunk): string {
|
|
120
|
+
const oldPath = file.status === 'added' ? '/dev/null' : `a/${file.oldPath}`;
|
|
121
|
+
const newPath = file.status === 'deleted' ? '/dev/null' : `b/${file.newPath}`;
|
|
122
|
+
const lines: string[] = [
|
|
123
|
+
`--- ${oldPath}`,
|
|
124
|
+
`+++ ${newPath}`,
|
|
125
|
+
hunk.header,
|
|
126
|
+
];
|
|
127
|
+
for (const line of hunk.lines) {
|
|
128
|
+
const prefix = line.type === 'add' ? '+' : line.type === 'delete' ? '-' : ' ';
|
|
129
|
+
lines.push(`${prefix}${line.content}`);
|
|
130
|
+
if (line.noNewline) {
|
|
131
|
+
lines.push('\');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return lines.join('\n') + '\n';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface ChangeGroup {
|
|
138
|
+
startIndex: number;
|
|
139
|
+
endIndex: number;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function getChangeGroups(lines: { type: string }[]): ChangeGroup[] {
|
|
143
|
+
const groups: ChangeGroup[] = [];
|
|
144
|
+
let i = 0;
|
|
145
|
+
while (i < lines.length) {
|
|
146
|
+
if (lines[i].type !== 'context') {
|
|
147
|
+
const start = i;
|
|
148
|
+
while (i < lines.length && lines[i].type !== 'context') {
|
|
149
|
+
i++;
|
|
150
|
+
}
|
|
151
|
+
groups.push({ startIndex: start, endIndex: i - 1 });
|
|
152
|
+
} else {
|
|
153
|
+
i++;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return groups;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function buildChangeGroupPatch(file: DiffFile, hunk: DiffHunk, startIndex: number, endIndex: number): string {
|
|
160
|
+
const CONTEXT = 3;
|
|
161
|
+
const lines = hunk.lines;
|
|
162
|
+
|
|
163
|
+
const contextBefore: typeof lines = [];
|
|
164
|
+
for (let i = startIndex - 1; i >= Math.max(0, startIndex - CONTEXT); i--) {
|
|
165
|
+
if (lines[i].type === 'context') {
|
|
166
|
+
contextBefore.unshift(lines[i]);
|
|
167
|
+
} else {
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const changeLines = lines.slice(startIndex, endIndex + 1);
|
|
173
|
+
|
|
174
|
+
const contextAfter: typeof lines = [];
|
|
175
|
+
for (let i = endIndex + 1; i < Math.min(lines.length, endIndex + 1 + CONTEXT); i++) {
|
|
176
|
+
if (lines[i].type === 'context') {
|
|
177
|
+
contextAfter.push(lines[i]);
|
|
178
|
+
} else {
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const allLines = [...contextBefore, ...changeLines, ...contextAfter];
|
|
184
|
+
|
|
185
|
+
let oldStart = 0;
|
|
186
|
+
let oldCount = 0;
|
|
187
|
+
let newStart = 0;
|
|
188
|
+
let newCount = 0;
|
|
189
|
+
|
|
190
|
+
for (const line of allLines) {
|
|
191
|
+
if (line.oldLineNumber !== null && oldStart === 0) {
|
|
192
|
+
oldStart = line.oldLineNumber;
|
|
193
|
+
}
|
|
194
|
+
if (line.newLineNumber !== null && newStart === 0) {
|
|
195
|
+
newStart = line.newLineNumber;
|
|
196
|
+
}
|
|
197
|
+
if (line.type === 'context' || line.type === 'delete') {
|
|
198
|
+
oldCount++;
|
|
199
|
+
}
|
|
200
|
+
if (line.type === 'context' || line.type === 'add') {
|
|
201
|
+
newCount++;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (oldStart === 0) {
|
|
206
|
+
oldStart = hunk.oldStart;
|
|
207
|
+
}
|
|
208
|
+
if (newStart === 0) {
|
|
209
|
+
newStart = hunk.newStart;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const oldPath = file.status === 'added' ? '/dev/null' : `a/${file.oldPath}`;
|
|
213
|
+
const newPath = file.status === 'deleted' ? '/dev/null' : `b/${file.newPath}`;
|
|
214
|
+
const header = `@@ -${oldStart},${oldCount} +${newStart},${newCount} @@`;
|
|
215
|
+
|
|
216
|
+
const patchLines: string[] = [
|
|
217
|
+
`--- ${oldPath}`,
|
|
218
|
+
`+++ ${newPath}`,
|
|
219
|
+
header,
|
|
220
|
+
];
|
|
221
|
+
|
|
222
|
+
for (const line of allLines) {
|
|
223
|
+
const prefix = line.type === 'add' ? '+' : line.type === 'delete' ? '-' : ' ';
|
|
224
|
+
patchLines.push(`${prefix}${line.content}`);
|
|
225
|
+
if (line.noNewline) {
|
|
226
|
+
patchLines.push('\');
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return patchLines.join('\n') + '\n';
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
export function extractLinesFromDiff(
|
|
235
|
+
hunks: DiffHunk[],
|
|
236
|
+
side: CommentSide,
|
|
237
|
+
startLine: number,
|
|
238
|
+
endLine: number,
|
|
239
|
+
): string {
|
|
240
|
+
const result: string[] = [];
|
|
241
|
+
for (const hunk of hunks) {
|
|
242
|
+
for (const line of hunk.lines) {
|
|
243
|
+
const lineNum = side === 'old' ? line.oldLineNumber : line.newLineNumber;
|
|
244
|
+
if (lineNum === null || lineNum < startLine || lineNum > endLine) {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (side === 'old' && (line.type === 'delete' || line.type === 'context')) {
|
|
248
|
+
result.push(line.content);
|
|
249
|
+
} else if (side === 'new' && (line.type === 'add' || line.type === 'context')) {
|
|
250
|
+
result.push(line.content);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return result.join('\n');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function getStatusColor(status: string): string {
|
|
258
|
+
switch (status) {
|
|
259
|
+
case 'added':
|
|
260
|
+
return 'bg-added/15 text-added';
|
|
261
|
+
case 'deleted':
|
|
262
|
+
return 'bg-deleted/15 text-deleted';
|
|
263
|
+
case 'renamed':
|
|
264
|
+
return 'bg-renamed/15 text-renamed';
|
|
265
|
+
default:
|
|
266
|
+
return 'bg-modified/15 text-modified';
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function getFileBlocks(): HTMLElement[] {
|
|
2
|
+
return Array.from(document.querySelectorAll('[id^="file-"]'));
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function getHunkHeaders(): HTMLElement[] {
|
|
6
|
+
return Array.from(
|
|
7
|
+
document.querySelectorAll('tbody > tr:first-child')
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function scrollToElement(el: HTMLElement) {
|
|
12
|
+
el.scrollIntoView({ behavior: 'instant', block: 'start' });
|
|
13
|
+
}
|