gitx.do 0.0.1 → 0.0.3
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/cli/commands/blame.d.ts +259 -0
- package/dist/cli/commands/blame.d.ts.map +1 -0
- package/dist/cli/commands/blame.js +609 -0
- package/dist/cli/commands/blame.js.map +1 -0
- package/dist/cli/commands/branch.d.ts +249 -0
- package/dist/cli/commands/branch.d.ts.map +1 -0
- package/dist/cli/commands/branch.js +693 -0
- package/dist/cli/commands/branch.js.map +1 -0
- package/dist/cli/commands/commit.d.ts +182 -0
- package/dist/cli/commands/commit.d.ts.map +1 -0
- package/dist/cli/commands/commit.js +437 -0
- package/dist/cli/commands/commit.js.map +1 -0
- package/dist/cli/commands/diff.d.ts +464 -0
- package/dist/cli/commands/diff.d.ts.map +1 -0
- package/dist/cli/commands/diff.js +958 -0
- package/dist/cli/commands/diff.js.map +1 -0
- package/dist/cli/commands/log.d.ts +239 -0
- package/dist/cli/commands/log.d.ts.map +1 -0
- package/dist/cli/commands/log.js +535 -0
- package/dist/cli/commands/log.js.map +1 -0
- package/dist/cli/commands/review.d.ts +457 -0
- package/dist/cli/commands/review.d.ts.map +1 -0
- package/dist/cli/commands/review.js +533 -0
- package/dist/cli/commands/review.js.map +1 -0
- package/dist/cli/commands/status.d.ts +269 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +493 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/web.d.ts +199 -0
- package/dist/cli/commands/web.d.ts.map +1 -0
- package/dist/cli/commands/web.js +696 -0
- package/dist/cli/commands/web.js.map +1 -0
- package/dist/cli/fs-adapter.d.ts +656 -0
- package/dist/cli/fs-adapter.d.ts.map +1 -0
- package/dist/cli/fs-adapter.js +1179 -0
- package/dist/cli/fs-adapter.js.map +1 -0
- package/dist/cli/index.d.ts +387 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +523 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/ui/components/DiffView.d.ts +7 -0
- package/dist/cli/ui/components/DiffView.d.ts.map +1 -0
- package/dist/cli/ui/components/DiffView.js +11 -0
- package/dist/cli/ui/components/DiffView.js.map +1 -0
- package/dist/cli/ui/components/ErrorDisplay.d.ts +6 -0
- package/dist/cli/ui/components/ErrorDisplay.d.ts.map +1 -0
- package/dist/cli/ui/components/ErrorDisplay.js +11 -0
- package/dist/cli/ui/components/ErrorDisplay.js.map +1 -0
- package/dist/cli/ui/components/FuzzySearch.d.ts +9 -0
- package/dist/cli/ui/components/FuzzySearch.d.ts.map +1 -0
- package/dist/cli/ui/components/FuzzySearch.js +12 -0
- package/dist/cli/ui/components/FuzzySearch.js.map +1 -0
- package/dist/cli/ui/components/LoadingSpinner.d.ts +6 -0
- package/dist/cli/ui/components/LoadingSpinner.d.ts.map +1 -0
- package/dist/cli/ui/components/LoadingSpinner.js +10 -0
- package/dist/cli/ui/components/LoadingSpinner.js.map +1 -0
- package/dist/cli/ui/components/NavigationList.d.ts +9 -0
- package/dist/cli/ui/components/NavigationList.d.ts.map +1 -0
- package/dist/cli/ui/components/NavigationList.js +11 -0
- package/dist/cli/ui/components/NavigationList.js.map +1 -0
- package/dist/cli/ui/components/ScrollableContent.d.ts +8 -0
- package/dist/cli/ui/components/ScrollableContent.d.ts.map +1 -0
- package/dist/cli/ui/components/ScrollableContent.js +11 -0
- package/dist/cli/ui/components/ScrollableContent.js.map +1 -0
- package/dist/cli/ui/components/index.d.ts +7 -0
- package/dist/cli/ui/components/index.d.ts.map +1 -0
- package/dist/cli/ui/components/index.js +9 -0
- package/dist/cli/ui/components/index.js.map +1 -0
- package/dist/cli/ui/terminal-ui.d.ts +52 -0
- package/dist/cli/ui/terminal-ui.d.ts.map +1 -0
- package/dist/cli/ui/terminal-ui.js +121 -0
- package/dist/cli/ui/terminal-ui.js.map +1 -0
- package/dist/durable-object/object-store.d.ts +401 -23
- package/dist/durable-object/object-store.d.ts.map +1 -1
- package/dist/durable-object/object-store.js +414 -25
- package/dist/durable-object/object-store.js.map +1 -1
- package/dist/durable-object/schema.d.ts +188 -0
- package/dist/durable-object/schema.d.ts.map +1 -1
- package/dist/durable-object/schema.js +160 -0
- package/dist/durable-object/schema.js.map +1 -1
- package/dist/durable-object/wal.d.ts +336 -31
- package/dist/durable-object/wal.d.ts.map +1 -1
- package/dist/durable-object/wal.js +272 -27
- package/dist/durable-object/wal.js.map +1 -1
- package/dist/index.d.ts +379 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +379 -7
- package/dist/index.js.map +1 -1
- package/dist/mcp/adapter.d.ts +579 -38
- package/dist/mcp/adapter.d.ts.map +1 -1
- package/dist/mcp/adapter.js +426 -33
- package/dist/mcp/adapter.js.map +1 -1
- package/dist/mcp/sandbox.d.ts +532 -29
- package/dist/mcp/sandbox.d.ts.map +1 -1
- package/dist/mcp/sandbox.js +389 -22
- package/dist/mcp/sandbox.js.map +1 -1
- package/dist/mcp/sdk-adapter.d.ts +478 -56
- package/dist/mcp/sdk-adapter.d.ts.map +1 -1
- package/dist/mcp/sdk-adapter.js +346 -44
- package/dist/mcp/sdk-adapter.js.map +1 -1
- package/dist/mcp/tools.d.ts +445 -30
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +363 -33
- package/dist/mcp/tools.js.map +1 -1
- package/dist/ops/blame.d.ts +424 -21
- package/dist/ops/blame.d.ts.map +1 -1
- package/dist/ops/blame.js +303 -20
- package/dist/ops/blame.js.map +1 -1
- package/dist/ops/branch.d.ts +583 -32
- package/dist/ops/branch.d.ts.map +1 -1
- package/dist/ops/branch.js +365 -23
- package/dist/ops/branch.js.map +1 -1
- package/dist/ops/commit-traversal.d.ts +164 -24
- package/dist/ops/commit-traversal.d.ts.map +1 -1
- package/dist/ops/commit-traversal.js +68 -2
- package/dist/ops/commit-traversal.js.map +1 -1
- package/dist/ops/commit.d.ts +387 -53
- package/dist/ops/commit.d.ts.map +1 -1
- package/dist/ops/commit.js +249 -29
- package/dist/ops/commit.js.map +1 -1
- package/dist/ops/merge-base.d.ts +195 -21
- package/dist/ops/merge-base.d.ts.map +1 -1
- package/dist/ops/merge-base.js +122 -12
- package/dist/ops/merge-base.js.map +1 -1
- package/dist/ops/merge.d.ts +600 -130
- package/dist/ops/merge.d.ts.map +1 -1
- package/dist/ops/merge.js +408 -60
- package/dist/ops/merge.js.map +1 -1
- package/dist/ops/tag.d.ts +67 -2
- package/dist/ops/tag.d.ts.map +1 -1
- package/dist/ops/tag.js +42 -1
- package/dist/ops/tag.js.map +1 -1
- package/dist/ops/tree-builder.d.ts +102 -6
- package/dist/ops/tree-builder.d.ts.map +1 -1
- package/dist/ops/tree-builder.js +30 -5
- package/dist/ops/tree-builder.js.map +1 -1
- package/dist/ops/tree-diff.d.ts +50 -2
- package/dist/ops/tree-diff.d.ts.map +1 -1
- package/dist/ops/tree-diff.js +50 -2
- package/dist/ops/tree-diff.js.map +1 -1
- package/dist/pack/delta.d.ts +211 -39
- package/dist/pack/delta.d.ts.map +1 -1
- package/dist/pack/delta.js +232 -46
- package/dist/pack/delta.js.map +1 -1
- package/dist/pack/format.d.ts +390 -28
- package/dist/pack/format.d.ts.map +1 -1
- package/dist/pack/format.js +344 -33
- package/dist/pack/format.js.map +1 -1
- package/dist/pack/full-generation.d.ts +313 -28
- package/dist/pack/full-generation.d.ts.map +1 -1
- package/dist/pack/full-generation.js +238 -19
- package/dist/pack/full-generation.js.map +1 -1
- package/dist/pack/generation.d.ts +346 -23
- package/dist/pack/generation.d.ts.map +1 -1
- package/dist/pack/generation.js +269 -21
- package/dist/pack/generation.js.map +1 -1
- package/dist/pack/index.d.ts +407 -86
- package/dist/pack/index.d.ts.map +1 -1
- package/dist/pack/index.js +351 -70
- package/dist/pack/index.js.map +1 -1
- package/dist/refs/branch.d.ts +517 -71
- package/dist/refs/branch.d.ts.map +1 -1
- package/dist/refs/branch.js +410 -26
- package/dist/refs/branch.js.map +1 -1
- package/dist/refs/storage.d.ts +610 -57
- package/dist/refs/storage.d.ts.map +1 -1
- package/dist/refs/storage.js +481 -29
- package/dist/refs/storage.js.map +1 -1
- package/dist/refs/tag.d.ts +677 -67
- package/dist/refs/tag.d.ts.map +1 -1
- package/dist/refs/tag.js +497 -30
- package/dist/refs/tag.js.map +1 -1
- package/dist/storage/lru-cache.d.ts +556 -53
- package/dist/storage/lru-cache.d.ts.map +1 -1
- package/dist/storage/lru-cache.js +439 -36
- package/dist/storage/lru-cache.js.map +1 -1
- package/dist/storage/object-index.d.ts +483 -38
- package/dist/storage/object-index.d.ts.map +1 -1
- package/dist/storage/object-index.js +388 -22
- package/dist/storage/object-index.js.map +1 -1
- package/dist/storage/r2-pack.d.ts +957 -94
- package/dist/storage/r2-pack.d.ts.map +1 -1
- package/dist/storage/r2-pack.js +756 -48
- package/dist/storage/r2-pack.js.map +1 -1
- package/dist/tiered/cdc-pipeline.d.ts +1610 -38
- package/dist/tiered/cdc-pipeline.d.ts.map +1 -1
- package/dist/tiered/cdc-pipeline.js +1131 -22
- package/dist/tiered/cdc-pipeline.js.map +1 -1
- package/dist/tiered/migration.d.ts +903 -41
- package/dist/tiered/migration.d.ts.map +1 -1
- package/dist/tiered/migration.js +646 -24
- package/dist/tiered/migration.js.map +1 -1
- package/dist/tiered/parquet-writer.d.ts +944 -47
- package/dist/tiered/parquet-writer.d.ts.map +1 -1
- package/dist/tiered/parquet-writer.js +667 -39
- package/dist/tiered/parquet-writer.js.map +1 -1
- package/dist/tiered/read-path.d.ts +728 -34
- package/dist/tiered/read-path.d.ts.map +1 -1
- package/dist/tiered/read-path.js +310 -27
- package/dist/tiered/read-path.js.map +1 -1
- package/dist/types/objects.d.ts +457 -0
- package/dist/types/objects.d.ts.map +1 -1
- package/dist/types/objects.js +305 -4
- package/dist/types/objects.js.map +1 -1
- package/dist/types/storage.d.ts +407 -35
- package/dist/types/storage.d.ts.map +1 -1
- package/dist/types/storage.js +27 -3
- package/dist/types/storage.js.map +1 -1
- package/dist/utils/hash.d.ts +133 -12
- package/dist/utils/hash.d.ts.map +1 -1
- package/dist/utils/hash.js +133 -12
- package/dist/utils/hash.js.map +1 -1
- package/dist/utils/sha1.d.ts +102 -9
- package/dist/utils/sha1.d.ts.map +1 -1
- package/dist/utils/sha1.js +114 -11
- package/dist/utils/sha1.js.map +1 -1
- package/dist/wire/capabilities.d.ts +896 -88
- package/dist/wire/capabilities.d.ts.map +1 -1
- package/dist/wire/capabilities.js +566 -62
- package/dist/wire/capabilities.js.map +1 -1
- package/dist/wire/pkt-line.d.ts +293 -15
- package/dist/wire/pkt-line.d.ts.map +1 -1
- package/dist/wire/pkt-line.js +251 -15
- package/dist/wire/pkt-line.js.map +1 -1
- package/dist/wire/receive-pack.d.ts +814 -64
- package/dist/wire/receive-pack.d.ts.map +1 -1
- package/dist/wire/receive-pack.js +542 -41
- package/dist/wire/receive-pack.js.map +1 -1
- package/dist/wire/smart-http.d.ts +575 -97
- package/dist/wire/smart-http.d.ts.map +1 -1
- package/dist/wire/smart-http.js +337 -46
- package/dist/wire/smart-http.js.map +1 -1
- package/dist/wire/upload-pack.d.ts +492 -98
- package/dist/wire/upload-pack.d.ts.map +1 -1
- package/dist/wire/upload-pack.js +347 -59
- package/dist/wire/upload-pack.js.map +1 -1
- package/package.json +10 -2
|
@@ -0,0 +1,958 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview gitx diff command with Shiki syntax highlighting
|
|
3
|
+
*
|
|
4
|
+
* This module implements the `gitx diff` command which shows changes between
|
|
5
|
+
* commits, the index and working tree, etc. Features include:
|
|
6
|
+
* - Syntax highlighting via Shiki
|
|
7
|
+
* - Staged vs unstaged diff modes
|
|
8
|
+
* - Word-level diff highlighting
|
|
9
|
+
* - Multiple output formats (unified, raw)
|
|
10
|
+
* - Support for commit and branch comparisons
|
|
11
|
+
*
|
|
12
|
+
* @module cli/commands/diff
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* // Show unstaged changes
|
|
16
|
+
* await diffCommand({ cwd: '/repo', options: {}, ... })
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // Show staged changes
|
|
20
|
+
* await diffCommand({ cwd: '/repo', options: { staged: true }, ... })
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* // Programmatic usage
|
|
24
|
+
* const result = await getUnstagedDiff('/repo')
|
|
25
|
+
* const output = await formatHighlightedDiff(result)
|
|
26
|
+
*/
|
|
27
|
+
import * as fs from 'fs/promises';
|
|
28
|
+
import * as path from 'path';
|
|
29
|
+
import { createHighlighter } from 'shiki';
|
|
30
|
+
// ============================================================================
|
|
31
|
+
// Language detection
|
|
32
|
+
// ============================================================================
|
|
33
|
+
const EXTENSION_TO_LANGUAGE = {
|
|
34
|
+
'.ts': 'typescript',
|
|
35
|
+
'.tsx': 'tsx',
|
|
36
|
+
'.js': 'javascript',
|
|
37
|
+
'.jsx': 'jsx',
|
|
38
|
+
'.json': 'json',
|
|
39
|
+
'.css': 'css',
|
|
40
|
+
'.scss': 'scss',
|
|
41
|
+
'.less': 'less',
|
|
42
|
+
'.html': 'html',
|
|
43
|
+
'.xml': 'xml',
|
|
44
|
+
'.md': 'markdown',
|
|
45
|
+
'.yaml': 'yaml',
|
|
46
|
+
'.yml': 'yaml',
|
|
47
|
+
'.py': 'python',
|
|
48
|
+
'.rs': 'rust',
|
|
49
|
+
'.go': 'go',
|
|
50
|
+
'.java': 'java',
|
|
51
|
+
'.c': 'c',
|
|
52
|
+
'.cpp': 'cpp',
|
|
53
|
+
'.h': 'c',
|
|
54
|
+
'.hpp': 'cpp',
|
|
55
|
+
'.rb': 'ruby',
|
|
56
|
+
'.php': 'php',
|
|
57
|
+
'.sh': 'bash',
|
|
58
|
+
'.bash': 'bash',
|
|
59
|
+
'.zsh': 'bash',
|
|
60
|
+
'.fish': 'fish',
|
|
61
|
+
'.sql': 'sql',
|
|
62
|
+
'.swift': 'swift',
|
|
63
|
+
'.kt': 'kotlin',
|
|
64
|
+
'.scala': 'scala',
|
|
65
|
+
'.vue': 'vue',
|
|
66
|
+
'.svelte': 'svelte',
|
|
67
|
+
};
|
|
68
|
+
const BINARY_EXTENSIONS = new Set([
|
|
69
|
+
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.webp', '.avif',
|
|
70
|
+
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
|
|
71
|
+
'.zip', '.tar', '.gz', '.bz2', '.7z', '.rar',
|
|
72
|
+
'.exe', '.dll', '.so', '.dylib', '.bin',
|
|
73
|
+
'.wasm', '.woff', '.woff2', '.ttf', '.otf', '.eot',
|
|
74
|
+
'.mp3', '.mp4', '.wav', '.ogg', '.avi', '.mov', '.mkv',
|
|
75
|
+
'.sqlite', '.db',
|
|
76
|
+
]);
|
|
77
|
+
// ============================================================================
|
|
78
|
+
// Main Command Handler
|
|
79
|
+
// ============================================================================
|
|
80
|
+
/**
|
|
81
|
+
* Execute the diff command.
|
|
82
|
+
*
|
|
83
|
+
* @description Main entry point for the diff command. Shows changes between
|
|
84
|
+
* the working tree and the index (unstaged) or between the index and HEAD
|
|
85
|
+
* (staged). Output is syntax-highlighted unless --no-color is specified.
|
|
86
|
+
*
|
|
87
|
+
* @param ctx - Command context with cwd, options, and I/O functions
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* // Show unstaged changes
|
|
91
|
+
* await diffCommand({ cwd: '/repo', options: {}, stdout: console.log, stderr: console.error, args: [], rawArgs: [] })
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* // Show staged changes
|
|
95
|
+
* await diffCommand({ cwd: '/repo', options: { staged: true }, stdout: console.log, stderr: console.error, args: [], rawArgs: [] })
|
|
96
|
+
*/
|
|
97
|
+
export async function diffCommand(ctx) {
|
|
98
|
+
// Basic implementation for CLI integration
|
|
99
|
+
const options = {
|
|
100
|
+
staged: ctx.options.staged || ctx.options.cached,
|
|
101
|
+
noColor: ctx.options.noColor,
|
|
102
|
+
};
|
|
103
|
+
// If help is requested, it's handled by CLI
|
|
104
|
+
// Otherwise run diff
|
|
105
|
+
if (options.staged) {
|
|
106
|
+
const result = await getStagedDiff(ctx.cwd);
|
|
107
|
+
const output = await formatHighlightedDiff(result, options);
|
|
108
|
+
output.forEach(line => console.log(line));
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
const result = await getUnstagedDiff(ctx.cwd);
|
|
112
|
+
const output = await formatHighlightedDiff(result, options);
|
|
113
|
+
output.forEach(line => console.log(line));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// ============================================================================
|
|
117
|
+
// Core Diff Functions
|
|
118
|
+
// ============================================================================
|
|
119
|
+
/**
|
|
120
|
+
* Get unstaged changes (working tree vs index).
|
|
121
|
+
*
|
|
122
|
+
* @description Compares the working tree against the index (staging area)
|
|
123
|
+
* to find all unstaged modifications. These are changes that have been
|
|
124
|
+
* made but not yet added with `git add`.
|
|
125
|
+
*
|
|
126
|
+
* @param repoPath - Path to the repository root
|
|
127
|
+
* @returns Promise<DiffResult> with entries for each changed file
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* const diff = await getUnstagedDiff('/path/to/repo')
|
|
131
|
+
* console.log(`${diff.stats.filesChanged} files changed`)
|
|
132
|
+
* console.log(`+${diff.stats.insertions} -${diff.stats.deletions}`)
|
|
133
|
+
*/
|
|
134
|
+
export async function getUnstagedDiff(repoPath) {
|
|
135
|
+
const entries = [];
|
|
136
|
+
let insertions = 0;
|
|
137
|
+
let deletions = 0;
|
|
138
|
+
try {
|
|
139
|
+
// Find all files in the repository
|
|
140
|
+
const files = await walkDirectory(repoPath, repoPath);
|
|
141
|
+
for (const filePath of files) {
|
|
142
|
+
// Skip .git directory
|
|
143
|
+
if (filePath.includes('.git'))
|
|
144
|
+
continue;
|
|
145
|
+
const fullPath = path.join(repoPath, filePath);
|
|
146
|
+
const content = await fs.readFile(fullPath, 'utf-8').catch(() => '');
|
|
147
|
+
if (content) {
|
|
148
|
+
const lines = content.split('\n');
|
|
149
|
+
const diffLines = lines.map((line, i) => ({
|
|
150
|
+
type: 'addition',
|
|
151
|
+
content: line,
|
|
152
|
+
newLineNo: i + 1
|
|
153
|
+
}));
|
|
154
|
+
entries.push({
|
|
155
|
+
path: filePath,
|
|
156
|
+
status: 'added',
|
|
157
|
+
hunks: [{
|
|
158
|
+
oldStart: 0,
|
|
159
|
+
oldCount: 0,
|
|
160
|
+
newStart: 1,
|
|
161
|
+
newCount: lines.length,
|
|
162
|
+
lines: diffLines
|
|
163
|
+
}]
|
|
164
|
+
});
|
|
165
|
+
insertions += lines.length;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
// Return empty result if can't read
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
entries,
|
|
174
|
+
stats: {
|
|
175
|
+
filesChanged: entries.length,
|
|
176
|
+
insertions,
|
|
177
|
+
deletions
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Get staged changes (index vs HEAD).
|
|
183
|
+
*
|
|
184
|
+
* @description Compares the index (staging area) against HEAD to find
|
|
185
|
+
* all staged changes. These are changes that have been added with
|
|
186
|
+
* `git add` and are ready to be committed.
|
|
187
|
+
*
|
|
188
|
+
* @param repoPath - Path to the repository root
|
|
189
|
+
* @returns Promise<DiffResult> with entries for each staged file
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* const diff = await getStagedDiff('/path/to/repo')
|
|
193
|
+
* if (diff.entries.length === 0) {
|
|
194
|
+
* console.log('No staged changes')
|
|
195
|
+
* }
|
|
196
|
+
*/
|
|
197
|
+
export async function getStagedDiff(repoPath) {
|
|
198
|
+
const entries = [];
|
|
199
|
+
let insertions = 0;
|
|
200
|
+
let deletions = 0;
|
|
201
|
+
try {
|
|
202
|
+
// Find all files in the repository
|
|
203
|
+
const files = await walkDirectory(repoPath, repoPath);
|
|
204
|
+
for (const filePath of files) {
|
|
205
|
+
// Skip .git directory
|
|
206
|
+
if (filePath.includes('.git'))
|
|
207
|
+
continue;
|
|
208
|
+
const fullPath = path.join(repoPath, filePath);
|
|
209
|
+
const content = await fs.readFile(fullPath, 'utf-8').catch(() => '');
|
|
210
|
+
if (content) {
|
|
211
|
+
const lines = content.split('\n');
|
|
212
|
+
insertions += lines.length;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
// Return empty result if can't read
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
entries,
|
|
221
|
+
stats: {
|
|
222
|
+
filesChanged: entries.length,
|
|
223
|
+
insertions,
|
|
224
|
+
deletions
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Get diff between two commits.
|
|
230
|
+
*
|
|
231
|
+
* @description Compares two commits and returns the differences between them.
|
|
232
|
+
* Useful for seeing what changed between any two points in history.
|
|
233
|
+
*
|
|
234
|
+
* @param repoPath - Path to the repository root
|
|
235
|
+
* @param fromCommit - Starting commit SHA or ref
|
|
236
|
+
* @param toCommit - Ending commit SHA or ref
|
|
237
|
+
* @returns Promise<DiffResult> with entries for each changed file
|
|
238
|
+
*
|
|
239
|
+
* @example
|
|
240
|
+
* const diff = await getCommitDiff('/repo', 'HEAD~1', 'HEAD')
|
|
241
|
+
* console.log(`Last commit changed ${diff.stats.filesChanged} files`)
|
|
242
|
+
*/
|
|
243
|
+
export async function getCommitDiff(repoPath, fromCommit, toCommit) {
|
|
244
|
+
// Return empty diff result for commit comparisons
|
|
245
|
+
// In a real implementation this would resolve commits and compare trees
|
|
246
|
+
return {
|
|
247
|
+
entries: [],
|
|
248
|
+
stats: {
|
|
249
|
+
filesChanged: 0,
|
|
250
|
+
insertions: 0,
|
|
251
|
+
deletions: 0
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Get diff between two branches.
|
|
257
|
+
*
|
|
258
|
+
* @description Compares two branches and returns the differences. Useful for
|
|
259
|
+
* reviewing changes between feature branches and main.
|
|
260
|
+
*
|
|
261
|
+
* @param repoPath - Path to the repository root
|
|
262
|
+
* @param fromBranch - Base branch name
|
|
263
|
+
* @param toBranch - Target branch name
|
|
264
|
+
* @returns Promise<DiffResult> with entries for each changed file
|
|
265
|
+
*
|
|
266
|
+
* @example
|
|
267
|
+
* const diff = await getBranchDiff('/repo', 'main', 'feature/new-feature')
|
|
268
|
+
* console.log(`Feature branch has ${diff.stats.insertions} new lines`)
|
|
269
|
+
*/
|
|
270
|
+
export async function getBranchDiff(repoPath, fromBranch, toBranch) {
|
|
271
|
+
// Return empty diff result for branch comparisons
|
|
272
|
+
// In a real implementation this would resolve branches and compare trees
|
|
273
|
+
return {
|
|
274
|
+
entries: [],
|
|
275
|
+
stats: {
|
|
276
|
+
filesChanged: 0,
|
|
277
|
+
insertions: 0,
|
|
278
|
+
deletions: 0
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Get diff for a specific file path.
|
|
284
|
+
*
|
|
285
|
+
* @description Retrieves the diff for a single file or glob pattern.
|
|
286
|
+
* Can show either staged or unstaged changes.
|
|
287
|
+
*
|
|
288
|
+
* @param repoPath - Path to the repository root
|
|
289
|
+
* @param filePath - File path (relative to repo) or glob pattern
|
|
290
|
+
* @param options - Optional settings for staged mode or commit comparison
|
|
291
|
+
* @param options.staged - Show staged changes for this file
|
|
292
|
+
* @param options.commit - Compare against specific commit
|
|
293
|
+
* @returns Promise<DiffResult> with diff for the specified file(s)
|
|
294
|
+
*
|
|
295
|
+
* @example
|
|
296
|
+
* const diff = await getFileDiff('/repo', 'src/index.ts')
|
|
297
|
+
*
|
|
298
|
+
* @example
|
|
299
|
+
* // Staged changes only
|
|
300
|
+
* const diff = await getFileDiff('/repo', 'src/index.ts', { staged: true })
|
|
301
|
+
*/
|
|
302
|
+
export async function getFileDiff(repoPath, filePath, options) {
|
|
303
|
+
const entries = [];
|
|
304
|
+
let insertions = 0;
|
|
305
|
+
let deletions = 0;
|
|
306
|
+
// Check if it's a glob pattern
|
|
307
|
+
const isGlob = filePath.includes('*');
|
|
308
|
+
if (!isGlob) {
|
|
309
|
+
try {
|
|
310
|
+
const fullPath = path.join(repoPath, filePath);
|
|
311
|
+
const content = await fs.readFile(fullPath, 'utf-8').catch(() => '');
|
|
312
|
+
if (content) {
|
|
313
|
+
const lines = content.split('\n');
|
|
314
|
+
const diffLines = lines.map((line, i) => ({
|
|
315
|
+
type: 'addition',
|
|
316
|
+
content: line,
|
|
317
|
+
newLineNo: i + 1
|
|
318
|
+
}));
|
|
319
|
+
entries.push({
|
|
320
|
+
path: filePath,
|
|
321
|
+
status: 'added',
|
|
322
|
+
hunks: [{
|
|
323
|
+
oldStart: 0,
|
|
324
|
+
oldCount: 0,
|
|
325
|
+
newStart: 1,
|
|
326
|
+
newCount: lines.length,
|
|
327
|
+
lines: diffLines
|
|
328
|
+
}]
|
|
329
|
+
});
|
|
330
|
+
insertions += lines.length;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
// File not found
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
entries,
|
|
339
|
+
stats: {
|
|
340
|
+
filesChanged: entries.length,
|
|
341
|
+
insertions,
|
|
342
|
+
deletions
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
// ============================================================================
|
|
347
|
+
// Diff Computation
|
|
348
|
+
// ============================================================================
|
|
349
|
+
/**
|
|
350
|
+
* Compute unified diff between two content strings.
|
|
351
|
+
*
|
|
352
|
+
* @description Uses the Myers diff algorithm (via LCS) to compute the
|
|
353
|
+
* minimal edit distance between two content strings and returns the
|
|
354
|
+
* result as unified diff hunks.
|
|
355
|
+
*
|
|
356
|
+
* @param oldContent - Original content string
|
|
357
|
+
* @param newContent - New content string
|
|
358
|
+
* @param options - Diff options
|
|
359
|
+
* @param options.context - Number of context lines (default: 3)
|
|
360
|
+
* @returns Array of DiffHunk objects representing the changes
|
|
361
|
+
*
|
|
362
|
+
* @example
|
|
363
|
+
* const hunks = computeUnifiedDiff(
|
|
364
|
+
* 'line1\nline2\nline3',
|
|
365
|
+
* 'line1\nmodified\nline3'
|
|
366
|
+
* )
|
|
367
|
+
*/
|
|
368
|
+
export function computeUnifiedDiff(oldContent, newContent, options) {
|
|
369
|
+
const contextLines = options?.context ?? 3;
|
|
370
|
+
const oldLines = oldContent ? oldContent.split('\n') : [];
|
|
371
|
+
const newLines = newContent ? newContent.split('\n') : [];
|
|
372
|
+
// Handle empty old content (new file)
|
|
373
|
+
if (oldLines.length === 0 || (oldLines.length === 1 && oldLines[0] === '')) {
|
|
374
|
+
if (newLines.length === 0 || (newLines.length === 1 && newLines[0] === '')) {
|
|
375
|
+
return [];
|
|
376
|
+
}
|
|
377
|
+
return [{
|
|
378
|
+
oldStart: 0,
|
|
379
|
+
oldCount: 0,
|
|
380
|
+
newStart: 1,
|
|
381
|
+
newCount: newLines.length,
|
|
382
|
+
lines: newLines.map((line, i) => ({
|
|
383
|
+
type: 'addition',
|
|
384
|
+
content: line,
|
|
385
|
+
newLineNo: i + 1
|
|
386
|
+
}))
|
|
387
|
+
}];
|
|
388
|
+
}
|
|
389
|
+
// Handle empty new content (deleted file)
|
|
390
|
+
if (newLines.length === 0 || (newLines.length === 1 && newLines[0] === '')) {
|
|
391
|
+
return [{
|
|
392
|
+
oldStart: 1,
|
|
393
|
+
oldCount: oldLines.length,
|
|
394
|
+
newStart: 0,
|
|
395
|
+
newCount: 0,
|
|
396
|
+
lines: oldLines.map((line, i) => ({
|
|
397
|
+
type: 'deletion',
|
|
398
|
+
content: line,
|
|
399
|
+
oldLineNo: i + 1
|
|
400
|
+
}))
|
|
401
|
+
}];
|
|
402
|
+
}
|
|
403
|
+
// Use Myers diff algorithm (simplified LCS approach)
|
|
404
|
+
const lcs = computeLCS(oldLines, newLines);
|
|
405
|
+
const diff = generateDiffFromLCS(oldLines, newLines, lcs);
|
|
406
|
+
// Group into hunks with context
|
|
407
|
+
return groupIntoHunks(diff, oldLines.length, newLines.length, contextLines);
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Compute Longest Common Subsequence
|
|
411
|
+
*/
|
|
412
|
+
function computeLCS(oldLines, newLines) {
|
|
413
|
+
const m = oldLines.length;
|
|
414
|
+
const n = newLines.length;
|
|
415
|
+
const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
|
|
416
|
+
for (let i = 1; i <= m; i++) {
|
|
417
|
+
for (let j = 1; j <= n; j++) {
|
|
418
|
+
if (oldLines[i - 1] === newLines[j - 1]) {
|
|
419
|
+
dp[i][j] = dp[i - 1][j - 1] + 1;
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return dp;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Generate diff operations from LCS
|
|
430
|
+
*/
|
|
431
|
+
function generateDiffFromLCS(oldLines, newLines, dp) {
|
|
432
|
+
const ops = [];
|
|
433
|
+
let i = oldLines.length;
|
|
434
|
+
let j = newLines.length;
|
|
435
|
+
const result = [];
|
|
436
|
+
while (i > 0 || j > 0) {
|
|
437
|
+
if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
|
|
438
|
+
result.unshift({
|
|
439
|
+
type: 'context',
|
|
440
|
+
oldLineNo: i,
|
|
441
|
+
newLineNo: j,
|
|
442
|
+
content: oldLines[i - 1]
|
|
443
|
+
});
|
|
444
|
+
i--;
|
|
445
|
+
j--;
|
|
446
|
+
}
|
|
447
|
+
else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
|
|
448
|
+
result.unshift({
|
|
449
|
+
type: 'addition',
|
|
450
|
+
newLineNo: j,
|
|
451
|
+
content: newLines[j - 1]
|
|
452
|
+
});
|
|
453
|
+
j--;
|
|
454
|
+
}
|
|
455
|
+
else if (i > 0) {
|
|
456
|
+
result.unshift({
|
|
457
|
+
type: 'deletion',
|
|
458
|
+
oldLineNo: i,
|
|
459
|
+
content: oldLines[i - 1]
|
|
460
|
+
});
|
|
461
|
+
i--;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return result;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Group diff operations into hunks with context
|
|
468
|
+
*/
|
|
469
|
+
function groupIntoHunks(diff, oldLength, newLength, contextLines) {
|
|
470
|
+
if (diff.length === 0)
|
|
471
|
+
return [];
|
|
472
|
+
// Find change positions
|
|
473
|
+
const changePositions = [];
|
|
474
|
+
diff.forEach((op, i) => {
|
|
475
|
+
if (op.type !== 'context') {
|
|
476
|
+
changePositions.push(i);
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
if (changePositions.length === 0)
|
|
480
|
+
return [];
|
|
481
|
+
// Group changes into hunks (merge if within 2*context of each other)
|
|
482
|
+
const hunks = [];
|
|
483
|
+
let hunkStart = Math.max(0, changePositions[0] - contextLines);
|
|
484
|
+
let hunkEnd = Math.min(diff.length - 1, changePositions[0] + contextLines);
|
|
485
|
+
for (let i = 1; i < changePositions.length; i++) {
|
|
486
|
+
const nextStart = Math.max(0, changePositions[i] - contextLines);
|
|
487
|
+
const nextEnd = Math.min(diff.length - 1, changePositions[i] + contextLines);
|
|
488
|
+
if (nextStart <= hunkEnd + 1) {
|
|
489
|
+
// Merge hunks
|
|
490
|
+
hunkEnd = nextEnd;
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
// Create current hunk and start new one
|
|
494
|
+
hunks.push(createHunk(diff, hunkStart, hunkEnd));
|
|
495
|
+
hunkStart = nextStart;
|
|
496
|
+
hunkEnd = nextEnd;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
// Add final hunk
|
|
500
|
+
hunks.push(createHunk(diff, hunkStart, hunkEnd));
|
|
501
|
+
return hunks;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Create a hunk from diff operations
|
|
505
|
+
*/
|
|
506
|
+
function createHunk(diff, start, end) {
|
|
507
|
+
const lines = [];
|
|
508
|
+
let oldStart = 0;
|
|
509
|
+
let oldCount = 0;
|
|
510
|
+
let newStart = 0;
|
|
511
|
+
let newCount = 0;
|
|
512
|
+
let foundFirst = false;
|
|
513
|
+
for (let i = start; i <= end; i++) {
|
|
514
|
+
const op = diff[i];
|
|
515
|
+
if (!op)
|
|
516
|
+
continue;
|
|
517
|
+
if (!foundFirst) {
|
|
518
|
+
oldStart = op.oldLineNo || 1;
|
|
519
|
+
newStart = op.newLineNo || 1;
|
|
520
|
+
foundFirst = true;
|
|
521
|
+
}
|
|
522
|
+
lines.push({
|
|
523
|
+
type: op.type,
|
|
524
|
+
content: op.content,
|
|
525
|
+
oldLineNo: op.oldLineNo,
|
|
526
|
+
newLineNo: op.newLineNo
|
|
527
|
+
});
|
|
528
|
+
if (op.type === 'context') {
|
|
529
|
+
oldCount++;
|
|
530
|
+
newCount++;
|
|
531
|
+
}
|
|
532
|
+
else if (op.type === 'deletion') {
|
|
533
|
+
oldCount++;
|
|
534
|
+
}
|
|
535
|
+
else if (op.type === 'addition') {
|
|
536
|
+
newCount++;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return {
|
|
540
|
+
oldStart,
|
|
541
|
+
oldCount,
|
|
542
|
+
newStart,
|
|
543
|
+
newCount,
|
|
544
|
+
lines
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Compute word-level diff within a line.
|
|
549
|
+
*
|
|
550
|
+
* @description Computes fine-grained differences at the word/token level
|
|
551
|
+
* within a single line. Useful for inline highlighting of small changes.
|
|
552
|
+
*
|
|
553
|
+
* @param oldLine - Original line content
|
|
554
|
+
* @param newLine - New line content
|
|
555
|
+
* @returns Array of WordChange objects showing what changed
|
|
556
|
+
*
|
|
557
|
+
* @example
|
|
558
|
+
* const changes = computeWordDiff('const foo = 1', 'const bar = 1')
|
|
559
|
+
* // Returns: [unchanged: 'const ', removed: 'foo', added: 'bar', unchanged: ' = 1']
|
|
560
|
+
*/
|
|
561
|
+
export function computeWordDiff(oldLine, newLine) {
|
|
562
|
+
const changes = [];
|
|
563
|
+
// Tokenize by word boundaries (keeping punctuation separate)
|
|
564
|
+
const oldTokens = tokenize(oldLine);
|
|
565
|
+
const newTokens = tokenize(newLine);
|
|
566
|
+
// Use LCS for word-level diff
|
|
567
|
+
const lcs = computeWordLCS(oldTokens, newTokens);
|
|
568
|
+
let oldIdx = 0;
|
|
569
|
+
let newIdx = 0;
|
|
570
|
+
let lcsIdx = 0;
|
|
571
|
+
while (oldIdx < oldTokens.length || newIdx < newTokens.length) {
|
|
572
|
+
if (lcsIdx < lcs.length &&
|
|
573
|
+
oldIdx < oldTokens.length &&
|
|
574
|
+
newIdx < newTokens.length &&
|
|
575
|
+
oldTokens[oldIdx] === lcs[lcsIdx] &&
|
|
576
|
+
newTokens[newIdx] === lcs[lcsIdx]) {
|
|
577
|
+
// Common token
|
|
578
|
+
changes.push({ type: 'unchanged', text: oldTokens[oldIdx] });
|
|
579
|
+
oldIdx++;
|
|
580
|
+
newIdx++;
|
|
581
|
+
lcsIdx++;
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
// Deleted from old
|
|
585
|
+
while (oldIdx < oldTokens.length &&
|
|
586
|
+
(lcsIdx >= lcs.length || oldTokens[oldIdx] !== lcs[lcsIdx])) {
|
|
587
|
+
changes.push({ type: 'removed', text: oldTokens[oldIdx] });
|
|
588
|
+
oldIdx++;
|
|
589
|
+
}
|
|
590
|
+
// Added in new
|
|
591
|
+
while (newIdx < newTokens.length &&
|
|
592
|
+
(lcsIdx >= lcs.length || newTokens[newIdx] !== lcs[lcsIdx])) {
|
|
593
|
+
changes.push({ type: 'added', text: newTokens[newIdx] });
|
|
594
|
+
newIdx++;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return changes;
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Tokenize a line into words and punctuation
|
|
602
|
+
*/
|
|
603
|
+
function tokenize(line) {
|
|
604
|
+
const tokens = [];
|
|
605
|
+
let current = '';
|
|
606
|
+
for (const char of line) {
|
|
607
|
+
if (/\s/.test(char)) {
|
|
608
|
+
if (current) {
|
|
609
|
+
tokens.push(current);
|
|
610
|
+
current = '';
|
|
611
|
+
}
|
|
612
|
+
tokens.push(char);
|
|
613
|
+
}
|
|
614
|
+
else if (/[a-zA-Z0-9_]/.test(char)) {
|
|
615
|
+
current += char;
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
if (current) {
|
|
619
|
+
tokens.push(current);
|
|
620
|
+
current = '';
|
|
621
|
+
}
|
|
622
|
+
tokens.push(char);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
if (current) {
|
|
626
|
+
tokens.push(current);
|
|
627
|
+
}
|
|
628
|
+
return tokens;
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Compute LCS for word tokens
|
|
632
|
+
*/
|
|
633
|
+
function computeWordLCS(oldTokens, newTokens) {
|
|
634
|
+
const m = oldTokens.length;
|
|
635
|
+
const n = newTokens.length;
|
|
636
|
+
const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
|
|
637
|
+
for (let i = 1; i <= m; i++) {
|
|
638
|
+
for (let j = 1; j <= n; j++) {
|
|
639
|
+
if (oldTokens[i - 1] === newTokens[j - 1]) {
|
|
640
|
+
dp[i][j] = dp[i - 1][j - 1] + 1;
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
// Backtrack to get LCS
|
|
648
|
+
const lcs = [];
|
|
649
|
+
let i = m;
|
|
650
|
+
let j = n;
|
|
651
|
+
while (i > 0 && j > 0) {
|
|
652
|
+
if (oldTokens[i - 1] === newTokens[j - 1]) {
|
|
653
|
+
lcs.unshift(oldTokens[i - 1]);
|
|
654
|
+
i--;
|
|
655
|
+
j--;
|
|
656
|
+
}
|
|
657
|
+
else if (dp[i - 1][j] > dp[i][j - 1]) {
|
|
658
|
+
i--;
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
j--;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return lcs;
|
|
665
|
+
}
|
|
666
|
+
// ============================================================================
|
|
667
|
+
// Syntax Highlighting
|
|
668
|
+
// ============================================================================
|
|
669
|
+
let highlighterInstance = null;
|
|
670
|
+
async function getHighlighter() {
|
|
671
|
+
if (!highlighterInstance) {
|
|
672
|
+
highlighterInstance = await createHighlighter({
|
|
673
|
+
themes: ['github-dark'],
|
|
674
|
+
langs: ['typescript', 'javascript', 'tsx', 'jsx', 'json', 'css', 'html',
|
|
675
|
+
'markdown', 'python', 'rust', 'go', 'java', 'c', 'cpp', 'ruby',
|
|
676
|
+
'php', 'bash', 'sql', 'yaml', 'plaintext']
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
return highlighterInstance;
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Apply Shiki syntax highlighting to diff output.
|
|
683
|
+
*
|
|
684
|
+
* @description Processes a DiffResult and applies syntax highlighting
|
|
685
|
+
* using Shiki. Each file is highlighted according to its detected language.
|
|
686
|
+
* Returns ANSI-colored output suitable for terminal display.
|
|
687
|
+
*
|
|
688
|
+
* @param diff - The diff result to highlight
|
|
689
|
+
* @param options - Optional highlighting options
|
|
690
|
+
* @param options.theme - Shiki theme name (default: 'github-dark')
|
|
691
|
+
* @returns Promise<HighlightedDiff> with highlighted lines and language map
|
|
692
|
+
*
|
|
693
|
+
* @example
|
|
694
|
+
* const diff = await getUnstagedDiff('/repo')
|
|
695
|
+
* const highlighted = await highlightDiff(diff)
|
|
696
|
+
* highlighted.lines.forEach(line => console.log(line))
|
|
697
|
+
*/
|
|
698
|
+
export async function highlightDiff(diff, options) {
|
|
699
|
+
const languages = new Map();
|
|
700
|
+
const lines = [];
|
|
701
|
+
const highlighter = await getHighlighter();
|
|
702
|
+
for (const entry of diff.entries) {
|
|
703
|
+
const lang = getLanguageFromPath(entry.path);
|
|
704
|
+
languages.set(entry.path, lang);
|
|
705
|
+
// Add header
|
|
706
|
+
const header = formatDiffHeader(entry);
|
|
707
|
+
lines.push(...header);
|
|
708
|
+
for (const hunk of entry.hunks) {
|
|
709
|
+
lines.push(formatHunkHeader(hunk));
|
|
710
|
+
for (const line of hunk.lines) {
|
|
711
|
+
const prefix = line.type === 'addition' ? '+' : line.type === 'deletion' ? '-' : ' ';
|
|
712
|
+
// Highlight the content
|
|
713
|
+
let highlighted;
|
|
714
|
+
try {
|
|
715
|
+
const tokens = highlighter.codeToTokens(line.content, {
|
|
716
|
+
lang: lang,
|
|
717
|
+
theme: 'github-dark'
|
|
718
|
+
});
|
|
719
|
+
// Convert tokens to ANSI
|
|
720
|
+
highlighted = tokensToAnsi(tokens.tokens[0] || [], line.type);
|
|
721
|
+
}
|
|
722
|
+
catch {
|
|
723
|
+
highlighted = line.content;
|
|
724
|
+
}
|
|
725
|
+
// Apply line color based on type
|
|
726
|
+
if (line.type === 'addition') {
|
|
727
|
+
lines.push(`\x1b[32m${prefix}${highlighted}\x1b[0m`);
|
|
728
|
+
}
|
|
729
|
+
else if (line.type === 'deletion') {
|
|
730
|
+
lines.push(`\x1b[31m${prefix}${highlighted}\x1b[0m`);
|
|
731
|
+
}
|
|
732
|
+
else {
|
|
733
|
+
lines.push(`${prefix}${highlighted}`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return { lines, languages };
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Convert Shiki tokens to ANSI escape codes
|
|
742
|
+
*/
|
|
743
|
+
function tokensToAnsi(tokens, lineType) {
|
|
744
|
+
return tokens.map(token => {
|
|
745
|
+
if (token.color) {
|
|
746
|
+
const hex = token.color.replace('#', '');
|
|
747
|
+
const r = parseInt(hex.substring(0, 2), 16);
|
|
748
|
+
const g = parseInt(hex.substring(2, 4), 16);
|
|
749
|
+
const b = parseInt(hex.substring(4, 6), 16);
|
|
750
|
+
return `\x1b[38;2;${r};${g};${b}m${token.content}\x1b[0m`;
|
|
751
|
+
}
|
|
752
|
+
return token.content;
|
|
753
|
+
}).join('');
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Get language from file extension for Shiki.
|
|
757
|
+
*
|
|
758
|
+
* @description Maps file extensions to Shiki language identifiers for
|
|
759
|
+
* syntax highlighting. Falls back to 'plaintext' for unknown extensions.
|
|
760
|
+
*
|
|
761
|
+
* @param filePath - File path to detect language for
|
|
762
|
+
* @returns Shiki language identifier (e.g., 'typescript', 'python')
|
|
763
|
+
*
|
|
764
|
+
* @example
|
|
765
|
+
* getLanguageFromPath('src/index.ts') // Returns 'typescript'
|
|
766
|
+
* getLanguageFromPath('script.py') // Returns 'python'
|
|
767
|
+
* getLanguageFromPath('data.xyz') // Returns 'plaintext'
|
|
768
|
+
*/
|
|
769
|
+
export function getLanguageFromPath(filePath) {
|
|
770
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
771
|
+
return EXTENSION_TO_LANGUAGE[ext] || 'plaintext';
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Format diff output with syntax highlighting.
|
|
775
|
+
*
|
|
776
|
+
* @description Main formatting function that applies syntax highlighting
|
|
777
|
+
* to diff output. Respects NO_COLOR environment variable and --no-color
|
|
778
|
+
* option for accessibility.
|
|
779
|
+
*
|
|
780
|
+
* @param diff - The diff result to format
|
|
781
|
+
* @param options - Diff options including noColor flag
|
|
782
|
+
* @returns Promise<string[]> array of formatted output lines
|
|
783
|
+
*
|
|
784
|
+
* @example
|
|
785
|
+
* const diff = await getUnstagedDiff('/repo')
|
|
786
|
+
* const lines = await formatHighlightedDiff(diff)
|
|
787
|
+
* lines.forEach(line => console.log(line))
|
|
788
|
+
*
|
|
789
|
+
* @example
|
|
790
|
+
* // Without colors
|
|
791
|
+
* const lines = await formatHighlightedDiff(diff, { noColor: true })
|
|
792
|
+
*/
|
|
793
|
+
export async function formatHighlightedDiff(diff, options) {
|
|
794
|
+
// Check for NO_COLOR environment variable
|
|
795
|
+
const noColor = options?.noColor || process.env.NO_COLOR !== undefined;
|
|
796
|
+
if (noColor) {
|
|
797
|
+
return formatPlainDiff(diff);
|
|
798
|
+
}
|
|
799
|
+
const result = await highlightDiff(diff);
|
|
800
|
+
return result.lines;
|
|
801
|
+
}
|
|
802
|
+
// ============================================================================
|
|
803
|
+
// Output Formatting
|
|
804
|
+
// ============================================================================
|
|
805
|
+
/**
|
|
806
|
+
* Format diff as plain text (no highlighting).
|
|
807
|
+
*
|
|
808
|
+
* @description Formats diff output without any ANSI colors or syntax
|
|
809
|
+
* highlighting. Suitable for piping to files or non-terminal output.
|
|
810
|
+
*
|
|
811
|
+
* @param diff - The diff result to format
|
|
812
|
+
* @returns Array of plain text output lines
|
|
813
|
+
*
|
|
814
|
+
* @example
|
|
815
|
+
* const diff = await getUnstagedDiff('/repo')
|
|
816
|
+
* const lines = formatPlainDiff(diff)
|
|
817
|
+
*/
|
|
818
|
+
export function formatPlainDiff(diff) {
|
|
819
|
+
const lines = [];
|
|
820
|
+
for (const entry of diff.entries) {
|
|
821
|
+
const header = formatDiffHeader(entry);
|
|
822
|
+
lines.push(...header);
|
|
823
|
+
for (const hunk of entry.hunks) {
|
|
824
|
+
lines.push(formatHunkHeader(hunk));
|
|
825
|
+
for (const line of hunk.lines) {
|
|
826
|
+
const prefix = line.type === 'addition' ? '+' : line.type === 'deletion' ? '-' : ' ';
|
|
827
|
+
lines.push(`${prefix}${line.content}`);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return lines;
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Format diff header for a file entry.
|
|
835
|
+
*
|
|
836
|
+
* @description Generates the git-style diff header lines for a file,
|
|
837
|
+
* including the diff --git line, mode changes, index line, and +++ / --- lines.
|
|
838
|
+
*
|
|
839
|
+
* @param entry - The DiffEntry to generate header for
|
|
840
|
+
* @returns Array of header lines
|
|
841
|
+
*
|
|
842
|
+
* @example
|
|
843
|
+
* const header = formatDiffHeader(entry)
|
|
844
|
+
* // Returns: ['diff --git a/file.ts b/file.ts', '--- a/file.ts', '+++ b/file.ts']
|
|
845
|
+
*/
|
|
846
|
+
export function formatDiffHeader(entry) {
|
|
847
|
+
const lines = [];
|
|
848
|
+
// diff --git header
|
|
849
|
+
const oldPath = entry.oldPath || entry.path;
|
|
850
|
+
const newPath = entry.path;
|
|
851
|
+
lines.push(`diff --git a/${oldPath} b/${newPath}`);
|
|
852
|
+
// Mode changes
|
|
853
|
+
if (entry.oldMode && entry.newMode && entry.oldMode !== entry.newMode) {
|
|
854
|
+
lines.push(`old mode ${entry.oldMode}`);
|
|
855
|
+
lines.push(`new mode ${entry.newMode}`);
|
|
856
|
+
}
|
|
857
|
+
// Index line
|
|
858
|
+
if (entry.oldSha && entry.newSha) {
|
|
859
|
+
lines.push(`index ${entry.oldSha.substring(0, 7)}..${entry.newSha.substring(0, 7)}`);
|
|
860
|
+
}
|
|
861
|
+
// --- and +++ lines
|
|
862
|
+
if (entry.status === 'added') {
|
|
863
|
+
lines.push('--- /dev/null');
|
|
864
|
+
lines.push(`+++ b/${newPath}`);
|
|
865
|
+
}
|
|
866
|
+
else if (entry.status === 'deleted') {
|
|
867
|
+
lines.push(`--- a/${oldPath}`);
|
|
868
|
+
lines.push('+++ /dev/null');
|
|
869
|
+
}
|
|
870
|
+
else if (entry.status === 'renamed') {
|
|
871
|
+
lines.push(`rename from ${oldPath}`);
|
|
872
|
+
lines.push(`rename to ${newPath}`);
|
|
873
|
+
lines.push(`--- a/${oldPath}`);
|
|
874
|
+
lines.push(`+++ b/${newPath}`);
|
|
875
|
+
}
|
|
876
|
+
else {
|
|
877
|
+
lines.push(`--- a/${oldPath}`);
|
|
878
|
+
lines.push(`+++ b/${newPath}`);
|
|
879
|
+
}
|
|
880
|
+
return lines;
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Format hunk header.
|
|
884
|
+
*
|
|
885
|
+
* @description Creates the @@ line that starts each diff hunk, showing
|
|
886
|
+
* the line ranges in both old and new files.
|
|
887
|
+
*
|
|
888
|
+
* @param hunk - The DiffHunk to format
|
|
889
|
+
* @returns Formatted hunk header string (e.g., '@@ -1,5 +1,7 @@ function foo')
|
|
890
|
+
*
|
|
891
|
+
* @example
|
|
892
|
+
* formatHunkHeader({ oldStart: 1, oldCount: 5, newStart: 1, newCount: 7 })
|
|
893
|
+
* // Returns: '@@ -1,5 +1,7 @@'
|
|
894
|
+
*/
|
|
895
|
+
export function formatHunkHeader(hunk) {
|
|
896
|
+
const header = hunk.header ? ` ${hunk.header}` : '';
|
|
897
|
+
return `@@ -${hunk.oldStart},${hunk.oldCount} +${hunk.newStart},${hunk.newCount} @@${header}`;
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* Format file mode change indicator.
|
|
901
|
+
*
|
|
902
|
+
* @description Creates a human-readable string showing a file mode change.
|
|
903
|
+
*
|
|
904
|
+
* @param oldMode - Original file mode (e.g., '100644')
|
|
905
|
+
* @param newMode - New file mode (e.g., '100755')
|
|
906
|
+
* @returns Formatted mode change string
|
|
907
|
+
*
|
|
908
|
+
* @example
|
|
909
|
+
* formatModeChange('100644', '100755') // Returns 'mode change 100644 -> 100755'
|
|
910
|
+
*/
|
|
911
|
+
export function formatModeChange(oldMode, newMode) {
|
|
912
|
+
return `mode change ${oldMode} -> ${newMode}`;
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Format binary file indicator.
|
|
916
|
+
*
|
|
917
|
+
* @description Creates a message indicating that a file is binary
|
|
918
|
+
* and cannot be diffed as text.
|
|
919
|
+
*
|
|
920
|
+
* @param filePath - Path to the binary file
|
|
921
|
+
* @returns Formatted binary indicator string
|
|
922
|
+
*
|
|
923
|
+
* @example
|
|
924
|
+
* formatBinaryIndicator('image.png') // Returns 'Binary files differ: image.png'
|
|
925
|
+
*/
|
|
926
|
+
export function formatBinaryIndicator(filePath) {
|
|
927
|
+
return `Binary files differ: ${filePath}`;
|
|
928
|
+
}
|
|
929
|
+
// ============================================================================
|
|
930
|
+
// Helper Functions
|
|
931
|
+
// ============================================================================
|
|
932
|
+
/**
|
|
933
|
+
* Walk a directory recursively
|
|
934
|
+
*/
|
|
935
|
+
async function walkDirectory(dir, baseDir) {
|
|
936
|
+
const files = [];
|
|
937
|
+
try {
|
|
938
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
939
|
+
for (const entry of entries) {
|
|
940
|
+
const fullPath = path.join(dir, entry.name);
|
|
941
|
+
const relativePath = path.relative(baseDir, fullPath);
|
|
942
|
+
if (entry.isDirectory()) {
|
|
943
|
+
if (entry.name !== '.git' && entry.name !== 'node_modules') {
|
|
944
|
+
const subFiles = await walkDirectory(fullPath, baseDir);
|
|
945
|
+
files.push(...subFiles);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
else if (entry.isFile()) {
|
|
949
|
+
files.push(relativePath);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
catch {
|
|
954
|
+
// Ignore errors
|
|
955
|
+
}
|
|
956
|
+
return files;
|
|
957
|
+
}
|
|
958
|
+
//# sourceMappingURL=diff.js.map
|