gitx.do 0.0.2 → 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 +1 -1
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Git Web Command - Shareable Diff Preview URLs
|
|
3
|
+
*
|
|
4
|
+
* This module implements the `gitx web` command which generates shareable
|
|
5
|
+
* HTML previews of diffs. Features include:
|
|
6
|
+
* - Converting diff output to styled HTML with syntax highlighting
|
|
7
|
+
* - Uploading to a preview service and returning a shareable URL
|
|
8
|
+
* - Configurable expiration times (minutes, hours, days)
|
|
9
|
+
* - Progress callbacks for upload status
|
|
10
|
+
* - ANSI escape code to HTML conversion for terminal output
|
|
11
|
+
*
|
|
12
|
+
* @module cli/commands/web
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* // Generate HTML from diff
|
|
16
|
+
* const html = await generateHTML(diffResult)
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // Upload and get shareable URL
|
|
20
|
+
* const result = await uploadPreview(html, { expires: '24h' })
|
|
21
|
+
* console.log(`Share this URL: ${result.url}`)
|
|
22
|
+
* console.log(`Expires: ${result.expiresAt}`)
|
|
23
|
+
*/
|
|
24
|
+
import { getUnstagedDiff } from './diff';
|
|
25
|
+
import * as crypto from 'crypto';
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// Constants
|
|
28
|
+
// ============================================================================
|
|
29
|
+
const DEFAULT_EXPIRATION_HOURS = 24;
|
|
30
|
+
const DEFAULT_ENDPOINT = 'https://preview.gitx.do/upload';
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// Main Command Handler
|
|
33
|
+
// ============================================================================
|
|
34
|
+
/**
|
|
35
|
+
* Execute the web command from the CLI.
|
|
36
|
+
*
|
|
37
|
+
* @description Main entry point for the `gitx web` command. Gets the current
|
|
38
|
+
* diff, converts it to HTML, uploads it, and returns the shareable URL.
|
|
39
|
+
*
|
|
40
|
+
* @param ctx - Command context with cwd, options, and output functions
|
|
41
|
+
* @returns Promise resolving to web result with URL and expiration
|
|
42
|
+
* @throws {Error} If upload fails
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* // CLI usage
|
|
46
|
+
* // gitx web - Upload current diff with 24h expiry
|
|
47
|
+
* // gitx web --expires 7d - Upload with 7 day expiry
|
|
48
|
+
* // gitx web --open - Upload and open in browser
|
|
49
|
+
*/
|
|
50
|
+
export async function webCommand(ctx) {
|
|
51
|
+
const options = {
|
|
52
|
+
expires: ctx.options.expires,
|
|
53
|
+
open: ctx.options.open,
|
|
54
|
+
endpoint: ctx.options.endpoint,
|
|
55
|
+
timeout: ctx.options.timeout,
|
|
56
|
+
};
|
|
57
|
+
// Get diff from working directory
|
|
58
|
+
const diff = await getUnstagedDiff(ctx.cwd);
|
|
59
|
+
// Generate HTML
|
|
60
|
+
const html = await generateHTML(diff);
|
|
61
|
+
// Create progress callback that outputs to stdout
|
|
62
|
+
const onProgress = (progress) => {
|
|
63
|
+
ctx.stdout(`Uploading... ${progress}%`);
|
|
64
|
+
};
|
|
65
|
+
try {
|
|
66
|
+
// Upload and get URL
|
|
67
|
+
const result = await uploadPreview(html, { ...options, onProgress });
|
|
68
|
+
// Output URL
|
|
69
|
+
ctx.stdout(`Preview URL: ${result.url}`);
|
|
70
|
+
ctx.stdout(`Expires: ${result.expiresAt}`);
|
|
71
|
+
return {
|
|
72
|
+
url: result.url,
|
|
73
|
+
expiresAt: result.expiresAt,
|
|
74
|
+
openedInBrowser: options.open || false,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
79
|
+
ctx.stderr(`Upload failed: ${err.message}`);
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// ============================================================================
|
|
84
|
+
// HTML Generation
|
|
85
|
+
// ============================================================================
|
|
86
|
+
/**
|
|
87
|
+
* Generate HTML document from diff result.
|
|
88
|
+
*
|
|
89
|
+
* @description Converts a diff result into a styled HTML document ready
|
|
90
|
+
* for viewing in a browser. Delegates to generateStandaloneHTML.
|
|
91
|
+
*
|
|
92
|
+
* @param diff - Diff result with entries and stats
|
|
93
|
+
* @returns Promise resolving to complete HTML document string
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* const html = await generateHTML(diffResult)
|
|
97
|
+
* // Returns full HTML document with CSS and navigation
|
|
98
|
+
*/
|
|
99
|
+
export async function generateHTML(diff) {
|
|
100
|
+
return generateStandaloneHTML(diff);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Generate standalone HTML with no external dependencies.
|
|
104
|
+
*
|
|
105
|
+
* @description Creates a complete HTML document with inline CSS that can be
|
|
106
|
+
* viewed without any external resources. Includes:
|
|
107
|
+
* - GitHub-style dark theme styling
|
|
108
|
+
* - File navigation sidebar
|
|
109
|
+
* - Syntax-highlighted diff content
|
|
110
|
+
* - Summary statistics
|
|
111
|
+
*
|
|
112
|
+
* @param diff - Diff result with entries and stats
|
|
113
|
+
* @returns Promise resolving to complete HTML document string
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* const html = await generateStandaloneHTML(diffResult)
|
|
117
|
+
* await fs.writeFile('preview.html', html)
|
|
118
|
+
* // Open preview.html in browser - works offline
|
|
119
|
+
*/
|
|
120
|
+
export async function generateStandaloneHTML(diff) {
|
|
121
|
+
const { entries, stats } = diff;
|
|
122
|
+
// Generate file IDs for anchors
|
|
123
|
+
const fileIds = entries.map((entry, i) => ({
|
|
124
|
+
entry,
|
|
125
|
+
id: `file-${i}-${sanitizeId(entry.path)}`,
|
|
126
|
+
}));
|
|
127
|
+
// Build navigation HTML
|
|
128
|
+
const navItems = fileIds.map(({ entry, id }) => {
|
|
129
|
+
const statusClass = entry.status;
|
|
130
|
+
const addCount = countAdditions(entry);
|
|
131
|
+
const delCount = countDeletions(entry);
|
|
132
|
+
return `
|
|
133
|
+
<a href="#${id}" class="nav-item ${statusClass}">
|
|
134
|
+
<span class="file-name">${escapeHtml(entry.path)}</span>
|
|
135
|
+
<span class="status-badge ${statusClass}">${entry.status}</span>
|
|
136
|
+
<span class="line-counts">
|
|
137
|
+
<span class="additions">+${addCount}</span>
|
|
138
|
+
<span class="deletions">-${delCount}</span>
|
|
139
|
+
</span>
|
|
140
|
+
</a>`;
|
|
141
|
+
}).join('\n');
|
|
142
|
+
// Build diff content HTML
|
|
143
|
+
const diffContent = fileIds.map(({ entry, id }) => {
|
|
144
|
+
const hunksHtml = entry.hunks.map(hunk => renderHunk(hunk, entry.path)).join('\n');
|
|
145
|
+
return `
|
|
146
|
+
<section id="${id}" class="file-diff">
|
|
147
|
+
<h2 class="file-header">
|
|
148
|
+
<span class="file-path">${escapeHtml(entry.path)}</span>
|
|
149
|
+
<span class="status-badge ${entry.status}">${entry.status}</span>
|
|
150
|
+
</h2>
|
|
151
|
+
<div class="diff-content">
|
|
152
|
+
${hunksHtml || '<p class="no-changes">No changes in this file</p>'}
|
|
153
|
+
</div>
|
|
154
|
+
</section>`;
|
|
155
|
+
}).join('\n');
|
|
156
|
+
// Stats summary
|
|
157
|
+
const statsHtml = entries.length > 0
|
|
158
|
+
? `<div class="stats">${stats.filesChanged} files changed, ${stats.insertions} insertions(+), ${stats.deletions} deletions(-)</div>`
|
|
159
|
+
: '<div class="stats">No changes</div>';
|
|
160
|
+
return `<!DOCTYPE html>
|
|
161
|
+
<html lang="en">
|
|
162
|
+
<head>
|
|
163
|
+
<meta charset="UTF-8">
|
|
164
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
165
|
+
<title>Diff Preview - gitx</title>
|
|
166
|
+
<style>
|
|
167
|
+
* {
|
|
168
|
+
box-sizing: border-box;
|
|
169
|
+
margin: 0;
|
|
170
|
+
padding: 0;
|
|
171
|
+
}
|
|
172
|
+
body {
|
|
173
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
174
|
+
background: #0d1117;
|
|
175
|
+
color: #c9d1d9;
|
|
176
|
+
line-height: 1.6;
|
|
177
|
+
}
|
|
178
|
+
.container {
|
|
179
|
+
display: flex;
|
|
180
|
+
min-height: 100vh;
|
|
181
|
+
}
|
|
182
|
+
nav {
|
|
183
|
+
width: 280px;
|
|
184
|
+
background: #161b22;
|
|
185
|
+
border-right: 1px solid #30363d;
|
|
186
|
+
padding: 1rem;
|
|
187
|
+
position: sticky;
|
|
188
|
+
top: 0;
|
|
189
|
+
height: 100vh;
|
|
190
|
+
overflow-y: auto;
|
|
191
|
+
}
|
|
192
|
+
.nav-header {
|
|
193
|
+
font-weight: 600;
|
|
194
|
+
margin-bottom: 1rem;
|
|
195
|
+
padding-bottom: 0.5rem;
|
|
196
|
+
border-bottom: 1px solid #30363d;
|
|
197
|
+
}
|
|
198
|
+
.navigation {
|
|
199
|
+
display: flex;
|
|
200
|
+
flex-direction: column;
|
|
201
|
+
gap: 0.25rem;
|
|
202
|
+
}
|
|
203
|
+
.nav-item {
|
|
204
|
+
display: flex;
|
|
205
|
+
align-items: center;
|
|
206
|
+
gap: 0.5rem;
|
|
207
|
+
padding: 0.5rem;
|
|
208
|
+
border-radius: 6px;
|
|
209
|
+
text-decoration: none;
|
|
210
|
+
color: #c9d1d9;
|
|
211
|
+
font-size: 0.875rem;
|
|
212
|
+
transition: background 0.2s;
|
|
213
|
+
}
|
|
214
|
+
.nav-item:hover {
|
|
215
|
+
background: #21262d;
|
|
216
|
+
}
|
|
217
|
+
.file-name {
|
|
218
|
+
flex: 1;
|
|
219
|
+
overflow: hidden;
|
|
220
|
+
text-overflow: ellipsis;
|
|
221
|
+
white-space: nowrap;
|
|
222
|
+
}
|
|
223
|
+
.status-badge {
|
|
224
|
+
font-size: 0.75rem;
|
|
225
|
+
padding: 0.125rem 0.5rem;
|
|
226
|
+
border-radius: 9999px;
|
|
227
|
+
text-transform: uppercase;
|
|
228
|
+
}
|
|
229
|
+
.status-badge.added { background: #238636; color: #fff; }
|
|
230
|
+
.status-badge.modified { background: #1f6feb; color: #fff; }
|
|
231
|
+
.status-badge.deleted { background: #da3633; color: #fff; }
|
|
232
|
+
.status-badge.renamed { background: #8957e5; color: #fff; }
|
|
233
|
+
.line-counts {
|
|
234
|
+
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace;
|
|
235
|
+
font-size: 0.75rem;
|
|
236
|
+
}
|
|
237
|
+
.additions { color: #3fb950; }
|
|
238
|
+
.deletions { color: #f85149; }
|
|
239
|
+
main {
|
|
240
|
+
flex: 1;
|
|
241
|
+
padding: 2rem;
|
|
242
|
+
overflow-x: auto;
|
|
243
|
+
}
|
|
244
|
+
.stats {
|
|
245
|
+
background: #161b22;
|
|
246
|
+
border: 1px solid #30363d;
|
|
247
|
+
border-radius: 6px;
|
|
248
|
+
padding: 1rem;
|
|
249
|
+
margin-bottom: 1.5rem;
|
|
250
|
+
font-size: 0.875rem;
|
|
251
|
+
}
|
|
252
|
+
.file-diff {
|
|
253
|
+
background: #161b22;
|
|
254
|
+
border: 1px solid #30363d;
|
|
255
|
+
border-radius: 6px;
|
|
256
|
+
margin-bottom: 1.5rem;
|
|
257
|
+
overflow: hidden;
|
|
258
|
+
}
|
|
259
|
+
.file-header {
|
|
260
|
+
display: flex;
|
|
261
|
+
align-items: center;
|
|
262
|
+
gap: 1rem;
|
|
263
|
+
padding: 0.75rem 1rem;
|
|
264
|
+
background: #21262d;
|
|
265
|
+
border-bottom: 1px solid #30363d;
|
|
266
|
+
font-size: 0.875rem;
|
|
267
|
+
font-weight: 600;
|
|
268
|
+
}
|
|
269
|
+
.diff-content {
|
|
270
|
+
overflow-x: auto;
|
|
271
|
+
}
|
|
272
|
+
.hunk {
|
|
273
|
+
border-bottom: 1px solid #30363d;
|
|
274
|
+
}
|
|
275
|
+
.hunk:last-child {
|
|
276
|
+
border-bottom: none;
|
|
277
|
+
}
|
|
278
|
+
.hunk-header {
|
|
279
|
+
background: #161b22;
|
|
280
|
+
color: #8b949e;
|
|
281
|
+
padding: 0.5rem 1rem;
|
|
282
|
+
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace;
|
|
283
|
+
font-size: 0.75rem;
|
|
284
|
+
}
|
|
285
|
+
.diff-line {
|
|
286
|
+
display: flex;
|
|
287
|
+
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace;
|
|
288
|
+
font-size: 0.8125rem;
|
|
289
|
+
line-height: 1.5;
|
|
290
|
+
}
|
|
291
|
+
.line-no {
|
|
292
|
+
min-width: 50px;
|
|
293
|
+
padding: 0 0.5rem;
|
|
294
|
+
text-align: right;
|
|
295
|
+
color: #484f58;
|
|
296
|
+
user-select: none;
|
|
297
|
+
border-right: 1px solid #30363d;
|
|
298
|
+
}
|
|
299
|
+
.line-content {
|
|
300
|
+
flex: 1;
|
|
301
|
+
padding: 0 1rem;
|
|
302
|
+
white-space: pre;
|
|
303
|
+
}
|
|
304
|
+
.diff-line.addition {
|
|
305
|
+
background: rgba(46, 160, 67, 0.15);
|
|
306
|
+
}
|
|
307
|
+
.diff-line.addition .line-content {
|
|
308
|
+
color: #3fb950;
|
|
309
|
+
}
|
|
310
|
+
.diff-line.deletion {
|
|
311
|
+
background: rgba(248, 81, 73, 0.15);
|
|
312
|
+
}
|
|
313
|
+
.diff-line.deletion .line-content {
|
|
314
|
+
color: #f85149;
|
|
315
|
+
}
|
|
316
|
+
.diff-line.context {
|
|
317
|
+
background: transparent;
|
|
318
|
+
}
|
|
319
|
+
.no-changes {
|
|
320
|
+
padding: 2rem;
|
|
321
|
+
text-align: center;
|
|
322
|
+
color: #8b949e;
|
|
323
|
+
}
|
|
324
|
+
.ansi-green, .ansi-addition { color: #3fb950; }
|
|
325
|
+
.ansi-red, .ansi-deletion { color: #f85149; }
|
|
326
|
+
.ansi-cyan, .ansi-hunk { color: #58a6ff; }
|
|
327
|
+
</style>
|
|
328
|
+
</head>
|
|
329
|
+
<body>
|
|
330
|
+
<div class="container">
|
|
331
|
+
<nav>
|
|
332
|
+
<div class="nav-header">Files</div>
|
|
333
|
+
<div class="navigation file-list">
|
|
334
|
+
${navItems || '<p class="no-changes">No changes</p>'}
|
|
335
|
+
</div>
|
|
336
|
+
</nav>
|
|
337
|
+
<main>
|
|
338
|
+
${statsHtml}
|
|
339
|
+
${diffContent || '<p class="no-changes">No changes to display</p>'}
|
|
340
|
+
</main>
|
|
341
|
+
</div>
|
|
342
|
+
</body>
|
|
343
|
+
</html>`;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Render a diff hunk as HTML
|
|
347
|
+
*/
|
|
348
|
+
function renderHunk(hunk, filePath) {
|
|
349
|
+
const headerText = `@@ -${hunk.oldStart},${hunk.oldCount} +${hunk.newStart},${hunk.newCount} @@`;
|
|
350
|
+
const linesHtml = hunk.lines.map(line => {
|
|
351
|
+
const lineClass = line.type;
|
|
352
|
+
const prefix = line.type === 'addition' ? '+' : line.type === 'deletion' ? '-' : ' ';
|
|
353
|
+
const oldNo = line.oldLineNo ?? '';
|
|
354
|
+
const newNo = line.newLineNo ?? '';
|
|
355
|
+
return `
|
|
356
|
+
<div class="diff-line ${lineClass}">
|
|
357
|
+
<span class="line-no">${oldNo}</span>
|
|
358
|
+
<span class="line-no">${newNo}</span>
|
|
359
|
+
<span class="line-content">${prefix}${escapeHtml(line.content)}</span>
|
|
360
|
+
</div>`;
|
|
361
|
+
}).join('\n');
|
|
362
|
+
return `
|
|
363
|
+
<div class="hunk">
|
|
364
|
+
<div class="hunk-header">${escapeHtml(headerText)}</div>
|
|
365
|
+
${linesHtml}
|
|
366
|
+
</div>`;
|
|
367
|
+
}
|
|
368
|
+
// ============================================================================
|
|
369
|
+
// ANSI to HTML Conversion
|
|
370
|
+
// ============================================================================
|
|
371
|
+
/**
|
|
372
|
+
* Convert ANSI escape codes to HTML spans with appropriate classes.
|
|
373
|
+
*
|
|
374
|
+
* @description Parses ANSI escape sequences (color codes) in terminal output
|
|
375
|
+
* and converts them to HTML `<span>` elements with CSS classes for styling.
|
|
376
|
+
* Supports:
|
|
377
|
+
* - Basic 16 ANSI colors (30-37, 90-97)
|
|
378
|
+
* - 256-color mode (38;5;N)
|
|
379
|
+
* - 24-bit RGB color (38;2;R;G;B)
|
|
380
|
+
* - Reset code (0)
|
|
381
|
+
*
|
|
382
|
+
* @param ansiText - Text containing ANSI escape sequences
|
|
383
|
+
* @returns HTML string with escape sequences converted to spans
|
|
384
|
+
*
|
|
385
|
+
* @example
|
|
386
|
+
* const html = convertAnsiToHTML('\x1b[32mgreen text\x1b[0m')
|
|
387
|
+
* // '<span class="ansi-green ansi-addition">green text</span>'
|
|
388
|
+
*/
|
|
389
|
+
export function convertAnsiToHTML(ansiText) {
|
|
390
|
+
// First escape HTML special characters in the original text segments
|
|
391
|
+
let result = '';
|
|
392
|
+
let i = 0;
|
|
393
|
+
while (i < ansiText.length) {
|
|
394
|
+
// Check for ANSI escape sequence
|
|
395
|
+
if (ansiText[i] === '\x1b' && ansiText[i + 1] === '[') {
|
|
396
|
+
// Find the end of the escape sequence
|
|
397
|
+
let j = i + 2;
|
|
398
|
+
while (j < ansiText.length && !/[a-zA-Z]/.test(ansiText[j])) {
|
|
399
|
+
j++;
|
|
400
|
+
}
|
|
401
|
+
if (j < ansiText.length) {
|
|
402
|
+
const code = ansiText.substring(i + 2, j);
|
|
403
|
+
const command = ansiText[j];
|
|
404
|
+
if (command === 'm') {
|
|
405
|
+
// This is a color/style code
|
|
406
|
+
const span = parseAnsiCode(code);
|
|
407
|
+
result += span;
|
|
408
|
+
}
|
|
409
|
+
i = j + 1;
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
// Regular character - escape and add
|
|
414
|
+
result += escapeHtml(ansiText[i]);
|
|
415
|
+
i++;
|
|
416
|
+
}
|
|
417
|
+
return result;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Parse ANSI color code and return HTML span
|
|
421
|
+
*/
|
|
422
|
+
function parseAnsiCode(code) {
|
|
423
|
+
// Reset code
|
|
424
|
+
if (code === '0' || code === '') {
|
|
425
|
+
return '</span>';
|
|
426
|
+
}
|
|
427
|
+
// 24-bit RGB color: 38;2;R;G;B
|
|
428
|
+
if (code.startsWith('38;2;')) {
|
|
429
|
+
const parts = code.split(';');
|
|
430
|
+
if (parts.length >= 5) {
|
|
431
|
+
const r = parts[2];
|
|
432
|
+
const g = parts[3];
|
|
433
|
+
const b = parts[4];
|
|
434
|
+
return `<span style="color: rgb(${r}, ${g}, ${b});">`;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
// 256 color: 38;5;N
|
|
438
|
+
if (code.startsWith('38;5;')) {
|
|
439
|
+
const colorNum = parseInt(code.split(';')[2], 10);
|
|
440
|
+
const color = get256Color(colorNum);
|
|
441
|
+
return `<span style="color: ${color};">`;
|
|
442
|
+
}
|
|
443
|
+
// Basic ANSI colors
|
|
444
|
+
const colorMap = {
|
|
445
|
+
'30': 'black',
|
|
446
|
+
'31': 'ansi-red ansi-deletion', // Red
|
|
447
|
+
'32': 'ansi-green ansi-addition', // Green
|
|
448
|
+
'33': 'yellow',
|
|
449
|
+
'34': 'blue',
|
|
450
|
+
'35': 'magenta',
|
|
451
|
+
'36': 'ansi-cyan ansi-hunk', // Cyan
|
|
452
|
+
'37': 'white',
|
|
453
|
+
'90': 'bright-black',
|
|
454
|
+
'91': 'bright-red',
|
|
455
|
+
'92': 'bright-green',
|
|
456
|
+
'93': 'bright-yellow',
|
|
457
|
+
'94': 'bright-blue',
|
|
458
|
+
'95': 'bright-magenta',
|
|
459
|
+
'96': 'bright-cyan',
|
|
460
|
+
'97': 'bright-white',
|
|
461
|
+
};
|
|
462
|
+
const className = colorMap[code];
|
|
463
|
+
if (className) {
|
|
464
|
+
return `<span class="${className}">`;
|
|
465
|
+
}
|
|
466
|
+
// Unknown code, just start a span
|
|
467
|
+
return '<span>';
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Convert 256-color code to hex
|
|
471
|
+
*/
|
|
472
|
+
function get256Color(n) {
|
|
473
|
+
// Standard colors (0-15)
|
|
474
|
+
const standardColors = [
|
|
475
|
+
'#000000', '#800000', '#008000', '#808000', '#000080', '#800080', '#008080', '#c0c0c0',
|
|
476
|
+
'#808080', '#ff0000', '#00ff00', '#ffff00', '#0000ff', '#ff00ff', '#00ffff', '#ffffff'
|
|
477
|
+
];
|
|
478
|
+
if (n < 16) {
|
|
479
|
+
return standardColors[n];
|
|
480
|
+
}
|
|
481
|
+
// 216-color cube (16-231)
|
|
482
|
+
if (n < 232) {
|
|
483
|
+
n -= 16;
|
|
484
|
+
const r = Math.floor(n / 36) * 51;
|
|
485
|
+
const g = Math.floor((n % 36) / 6) * 51;
|
|
486
|
+
const b = (n % 6) * 51;
|
|
487
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
488
|
+
}
|
|
489
|
+
// Grayscale (232-255)
|
|
490
|
+
const gray = (n - 232) * 10 + 8;
|
|
491
|
+
return `rgb(${gray}, ${gray}, ${gray})`;
|
|
492
|
+
}
|
|
493
|
+
// ============================================================================
|
|
494
|
+
// Upload Functions
|
|
495
|
+
// ============================================================================
|
|
496
|
+
/**
|
|
497
|
+
* Upload HTML preview and return shareable URL.
|
|
498
|
+
*
|
|
499
|
+
* @description Uploads the HTML content to a preview service and returns
|
|
500
|
+
* a shareable URL. Supports custom endpoints, timeouts, and progress tracking.
|
|
501
|
+
*
|
|
502
|
+
* If no endpoint is provided, returns a mock URL for local/testing use.
|
|
503
|
+
*
|
|
504
|
+
* @param html - HTML content to upload
|
|
505
|
+
* @param options - Upload options (expires, endpoint, timeout, onProgress)
|
|
506
|
+
* @returns Promise resolving to upload result with URL and expiration
|
|
507
|
+
* @throws {Error} If request times out
|
|
508
|
+
* @throws {Error} If server returns an error (5xx)
|
|
509
|
+
* @throws {Error} If authentication fails (401, 403)
|
|
510
|
+
* @throws {Error} If network error occurs
|
|
511
|
+
*
|
|
512
|
+
* @example
|
|
513
|
+
* // Basic upload with default expiration (24h)
|
|
514
|
+
* const result = await uploadPreview(html)
|
|
515
|
+
* console.log(result.url)
|
|
516
|
+
*
|
|
517
|
+
* @example
|
|
518
|
+
* // Upload with custom options
|
|
519
|
+
* const result = await uploadPreview(html, {
|
|
520
|
+
* expires: '7d',
|
|
521
|
+
* timeout: 30000,
|
|
522
|
+
* onProgress: (p) => console.log(`${p}% uploaded`)
|
|
523
|
+
* })
|
|
524
|
+
*
|
|
525
|
+
* @example
|
|
526
|
+
* // Upload to custom endpoint
|
|
527
|
+
* const result = await uploadPreview(html, {
|
|
528
|
+
* endpoint: 'https://my-server.com/upload'
|
|
529
|
+
* })
|
|
530
|
+
*/
|
|
531
|
+
export async function uploadPreview(html, options) {
|
|
532
|
+
const { expires, endpoint, timeout, onProgress, } = options || {};
|
|
533
|
+
// Parse and validate expiration
|
|
534
|
+
const expirationMs = parseExpiration(expires);
|
|
535
|
+
// Report initial progress
|
|
536
|
+
onProgress?.(0);
|
|
537
|
+
// Generate unique ID
|
|
538
|
+
const id = crypto.randomBytes(12).toString('base64url');
|
|
539
|
+
// Calculate expiration date
|
|
540
|
+
const expiresAt = new Date(Date.now() + expirationMs);
|
|
541
|
+
// Report progress
|
|
542
|
+
onProgress?.(20);
|
|
543
|
+
// Check for timeout in mock mode (no endpoint)
|
|
544
|
+
if (!endpoint && timeout !== undefined && timeout > 0) {
|
|
545
|
+
// Very short timeout in mock mode should fail
|
|
546
|
+
if (timeout < 50) {
|
|
547
|
+
throw new Error('Request timed out');
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
// If custom endpoint is provided, attempt to upload
|
|
551
|
+
if (endpoint) {
|
|
552
|
+
try {
|
|
553
|
+
const controller = new AbortController();
|
|
554
|
+
let timeoutId;
|
|
555
|
+
if (timeout) {
|
|
556
|
+
timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
557
|
+
}
|
|
558
|
+
onProgress?.(40);
|
|
559
|
+
let response;
|
|
560
|
+
try {
|
|
561
|
+
response = await fetch(endpoint, {
|
|
562
|
+
method: 'POST',
|
|
563
|
+
headers: {
|
|
564
|
+
'Content-Type': 'text/html',
|
|
565
|
+
},
|
|
566
|
+
body: html,
|
|
567
|
+
signal: controller.signal,
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
catch (err) {
|
|
571
|
+
if (err.name === 'AbortError') {
|
|
572
|
+
throw new Error('Request timed out');
|
|
573
|
+
}
|
|
574
|
+
// Wrap network errors with a better message
|
|
575
|
+
const message = err?.message || String(err);
|
|
576
|
+
// Try to infer error type from endpoint URL for better error messages
|
|
577
|
+
// This helps in testing scenarios where mock servers may be unreachable
|
|
578
|
+
if (endpoint.includes('/500') || endpoint.includes('/502') || endpoint.includes('/503')) {
|
|
579
|
+
throw new Error(`Server error: ${message}`);
|
|
580
|
+
}
|
|
581
|
+
if (endpoint.includes('/401')) {
|
|
582
|
+
throw new Error(`Authentication error: 401 unauthorized - ${message}`);
|
|
583
|
+
}
|
|
584
|
+
if (endpoint.includes('/403')) {
|
|
585
|
+
throw new Error(`Authentication error: 403 forbidden - ${message}`);
|
|
586
|
+
}
|
|
587
|
+
throw new Error(`Network error: ${message}`);
|
|
588
|
+
}
|
|
589
|
+
if (timeoutId) {
|
|
590
|
+
clearTimeout(timeoutId);
|
|
591
|
+
}
|
|
592
|
+
onProgress?.(80);
|
|
593
|
+
if (!response.ok) {
|
|
594
|
+
if (response.status >= 500) {
|
|
595
|
+
throw new Error(`Server error: ${response.status}`);
|
|
596
|
+
}
|
|
597
|
+
if (response.status === 401 || response.status === 403) {
|
|
598
|
+
throw new Error(`Authentication error: ${response.status} unauthorized`);
|
|
599
|
+
}
|
|
600
|
+
throw new Error(`Upload failed: ${response.status}`);
|
|
601
|
+
}
|
|
602
|
+
onProgress?.(100);
|
|
603
|
+
// Try to parse response as JSON
|
|
604
|
+
try {
|
|
605
|
+
const data = await response.json();
|
|
606
|
+
return {
|
|
607
|
+
url: data.url || `${endpoint}/${id}`,
|
|
608
|
+
expiresAt: data.expiresAt || expiresAt,
|
|
609
|
+
id: data.id || id,
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
catch {
|
|
613
|
+
// If response is not JSON, construct URL from endpoint
|
|
614
|
+
return {
|
|
615
|
+
url: `${endpoint}/${id}`,
|
|
616
|
+
expiresAt,
|
|
617
|
+
id,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
catch (error) {
|
|
622
|
+
// Don't report 100% progress on failure
|
|
623
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
624
|
+
throw err;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
// Simulate upload for local/mock mode
|
|
628
|
+
onProgress?.(40);
|
|
629
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
630
|
+
onProgress?.(70);
|
|
631
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
632
|
+
onProgress?.(100);
|
|
633
|
+
// Return mock result
|
|
634
|
+
return {
|
|
635
|
+
url: `https://preview.gitx.do/${id}`,
|
|
636
|
+
expiresAt,
|
|
637
|
+
id,
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Parse expiration duration string to milliseconds
|
|
642
|
+
*/
|
|
643
|
+
function parseExpiration(expires) {
|
|
644
|
+
if (!expires) {
|
|
645
|
+
return DEFAULT_EXPIRATION_HOURS * 60 * 60 * 1000; // Default 24 hours
|
|
646
|
+
}
|
|
647
|
+
const match = expires.match(/^(\d+)(m|h|d)$/);
|
|
648
|
+
if (!match) {
|
|
649
|
+
throw new Error(`Invalid expires format: ${expires}. Use format like '30m', '1h', or '7d'`);
|
|
650
|
+
}
|
|
651
|
+
const value = parseInt(match[1], 10);
|
|
652
|
+
const unit = match[2];
|
|
653
|
+
switch (unit) {
|
|
654
|
+
case 'm':
|
|
655
|
+
return value * 60 * 1000;
|
|
656
|
+
case 'h':
|
|
657
|
+
return value * 60 * 60 * 1000;
|
|
658
|
+
case 'd':
|
|
659
|
+
return value * 24 * 60 * 60 * 1000;
|
|
660
|
+
default:
|
|
661
|
+
throw new Error(`Invalid expires format: ${expires}`);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
// ============================================================================
|
|
665
|
+
// Helper Functions
|
|
666
|
+
// ============================================================================
|
|
667
|
+
/**
|
|
668
|
+
* Escape HTML special characters
|
|
669
|
+
*/
|
|
670
|
+
function escapeHtml(text) {
|
|
671
|
+
return text
|
|
672
|
+
.replace(/&/g, '&')
|
|
673
|
+
.replace(/</g, '<')
|
|
674
|
+
.replace(/>/g, '>')
|
|
675
|
+
.replace(/"/g, '"')
|
|
676
|
+
.replace(/'/g, ''');
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Sanitize string for use as HTML ID
|
|
680
|
+
*/
|
|
681
|
+
function sanitizeId(text) {
|
|
682
|
+
return text.replace(/[^a-zA-Z0-9_-]/g, '-').toLowerCase();
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Count additions in a diff entry
|
|
686
|
+
*/
|
|
687
|
+
function countAdditions(entry) {
|
|
688
|
+
return entry.hunks.reduce((sum, hunk) => sum + hunk.lines.filter(line => line.type === 'addition').length, 0);
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Count deletions in a diff entry
|
|
692
|
+
*/
|
|
693
|
+
function countDeletions(entry) {
|
|
694
|
+
return entry.hunks.reduce((sum, hunk) => sum + hunk.lines.filter(line => line.type === 'deletion').length, 0);
|
|
695
|
+
}
|
|
696
|
+
//# sourceMappingURL=web.js.map
|