git-history-ui 1.0.6 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/build/frontend/index.html +13 -10
  3. package/build/frontend/main-7AXSFTJM.js +11 -0
  4. package/build/frontend/styles-YQ73RJ2V.css +1 -0
  5. package/dist/backend/dev-server.d.ts +0 -1
  6. package/dist/backend/dev-server.js +8 -4
  7. package/dist/backend/dev-server.js.map +1 -1
  8. package/dist/backend/gitService.d.ts +24 -10
  9. package/dist/backend/gitService.js +340 -240
  10. package/dist/backend/gitService.js.map +1 -1
  11. package/dist/backend/server.d.ts +8 -2
  12. package/dist/backend/server.js +145 -112
  13. package/dist/backend/server.js.map +1 -1
  14. package/dist/cli.d.ts +0 -1
  15. package/dist/cli.js +44 -22
  16. package/dist/cli.js.map +1 -1
  17. package/package.json +43 -27
  18. package/build/frontend/main-44CFNHDH.js +0 -8
  19. package/build/frontend/main-5GQESVK5.js +0 -8
  20. package/build/frontend/main-KMFUNYSW.js +0 -8
  21. package/build/frontend/main-YHO6NCZZ.js +0 -8
  22. package/build/frontend/styles-26JPPBSI.css +0 -1
  23. package/build/frontend/styles-J5I4DBTU.css +0 -1
  24. package/dist/__tests__/gitService.test.d.ts +0 -2
  25. package/dist/__tests__/gitService.test.d.ts.map +0 -1
  26. package/dist/__tests__/gitService.test.js +0 -435
  27. package/dist/__tests__/gitService.test.js.map +0 -1
  28. package/dist/__tests__/setup.d.ts +0 -2
  29. package/dist/__tests__/setup.d.ts.map +0 -1
  30. package/dist/__tests__/setup.js +0 -24
  31. package/dist/__tests__/setup.js.map +0 -1
  32. package/dist/backend/dev-server.d.ts.map +0 -1
  33. package/dist/backend/gitService.d.ts.map +0 -1
  34. package/dist/backend/server.d.ts.map +0 -1
  35. package/dist/cli.d.ts.map +0 -1
  36. package/dist/config/paths.d.ts +0 -14
  37. package/dist/config/paths.d.ts.map +0 -1
  38. package/dist/config/paths.js +0 -35
  39. package/dist/config/paths.js.map +0 -1
@@ -1,283 +1,383 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.GitService = void 0;
7
- const simple_git_1 = __importDefault(require("simple-git"));
8
- class GitService {
3
+ exports.GitService = exports.NotARepositoryError = void 0;
4
+ const child_process_1 = require("child_process");
5
+ const util_1 = require("util");
6
+ const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
7
+ const FIELD_SEP = "\x1f"; // Unit Separator (NUL is rejected by Node argv)
8
+ const RECORD_SEP = '\x1e';
9
+ const LOG_FORMAT = ['%H', '%h', '%an', '%ae', '%aI', '%P', '%s', '%b'].join(FIELD_SEP);
10
+ const REF_INDEX_TTL_MS = 5_000;
11
+ const COUNT_CACHE_TTL_MS = 10_000;
12
+ class NotARepositoryError extends Error {
9
13
  constructor() {
10
- this.git = (0, simple_git_1.default)();
14
+ super('Not a git repository');
15
+ this.name = 'NotARepositoryError';
11
16
  }
12
- async getCommits(options = {}) {
17
+ }
18
+ exports.NotARepositoryError = NotARepositoryError;
19
+ class GitService {
20
+ repoPath;
21
+ refIndexCache = null;
22
+ countCache = new Map();
23
+ repoCheckResult = null;
24
+ constructor(repoPath = process.cwd()) {
25
+ this.repoPath = repoPath;
26
+ }
27
+ async verifyRepository() {
28
+ if (this.repoCheckResult !== null)
29
+ return this.repoCheckResult;
13
30
  try {
14
- const page = options.page || 1;
15
- const pageSize = options.pageSize || 25;
16
- const skip = (page - 1) * pageSize;
17
- // Get total count first (without limit)
18
- const countOptions = {
19
- maxCount: 0 // Get all commits for counting
20
- };
21
- let totalLog;
22
- if (options.file) {
23
- totalLog = await this.git.log({
24
- ...countOptions,
25
- file: options.file
26
- });
27
- }
28
- else if (options.since) {
29
- totalLog = await this.git.log({
30
- ...countOptions,
31
- from: options.since
32
- });
33
- }
34
- else if (options.author) {
35
- totalLog = await this.git.log({
36
- ...countOptions,
37
- author: options.author
38
- });
39
- }
40
- else {
41
- totalLog = await this.git.log(countOptions);
42
- }
43
- const total = totalLog.all.length;
44
- const totalPages = Math.ceil(total / pageSize);
45
- // Get paginated commits
46
- // For pagination, we need to get all commits and then slice them
47
- // since simple-git doesn't support skip parameter properly
48
- const allLogOptions = {
49
- maxCount: 0 // Get all commits
50
- };
51
- let allLog;
52
- if (options.file) {
53
- allLog = await this.git.log({
54
- ...allLogOptions,
55
- file: options.file
56
- });
57
- }
58
- else if (options.since) {
59
- allLog = await this.git.log({
60
- ...allLogOptions,
61
- from: options.since
62
- });
63
- }
64
- else if (options.author) {
65
- allLog = await this.git.log({
66
- ...allLogOptions,
67
- author: options.author
68
- });
31
+ await this.git(['rev-parse', '--is-inside-work-tree']);
32
+ this.repoCheckResult = true;
33
+ }
34
+ catch {
35
+ this.repoCheckResult = false;
36
+ }
37
+ return this.repoCheckResult;
38
+ }
39
+ async getRefIndex() {
40
+ const now = Date.now();
41
+ if (this.refIndexCache && now - this.refIndexCache.builtAt < REF_INDEX_TTL_MS) {
42
+ return this.refIndexCache;
43
+ }
44
+ const branchesByCommit = new Map();
45
+ const tagsByCommit = new Map();
46
+ const out = await this.git([
47
+ 'for-each-ref',
48
+ '--format=%(objectname)\t%(refname:short)\t%(refname)',
49
+ 'refs/heads',
50
+ 'refs/tags',
51
+ 'refs/remotes'
52
+ ]);
53
+ for (const line of out.split('\n')) {
54
+ if (!line)
55
+ continue;
56
+ const [hash, short, full] = line.split('\t');
57
+ if (!hash || !short)
58
+ continue;
59
+ if (full && full.startsWith('refs/tags/')) {
60
+ push(tagsByCommit, hash, short);
69
61
  }
70
62
  else {
71
- allLog = await this.git.log(allLogOptions);
63
+ push(branchesByCommit, hash, short);
72
64
  }
73
- // Apply pagination manually
74
- const paginatedCommits = allLog.all.slice(skip, skip + pageSize);
75
- const commits = await Promise.all(paginatedCommits.map(async (commit) => {
76
- const [branches, tags] = await Promise.all([
77
- this.getBranchesForCommit(commit.hash),
78
- this.getTagsForCommit(commit.hash)
79
- ]);
80
- return {
81
- hash: commit.hash,
82
- author: commit.author_name,
83
- date: commit.date,
84
- message: commit.message,
85
- files: await this.getFilesForCommit(commit.hash),
86
- parents: [], // We'll get this from git show if needed
87
- branches,
88
- tags
89
- };
90
- }));
91
- return {
92
- commits,
93
- total,
94
- page,
95
- pageSize,
96
- totalPages,
97
- hasNext: page < totalPages,
98
- hasPrevious: page > 1
99
- };
100
65
  }
101
- catch (error) {
102
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
103
- throw new Error(`Failed to get commits: ${errorMessage}`);
66
+ this.refIndexCache = { branchesByCommit, tagsByCommit, builtAt: now };
67
+ return this.refIndexCache;
68
+ function push(map, key, value) {
69
+ const list = map.get(key);
70
+ if (list)
71
+ list.push(value);
72
+ else
73
+ map.set(key, [value]);
104
74
  }
105
75
  }
106
- async getCommit(hash) {
76
+ async getCommits(options = {}) {
77
+ if (!(await this.verifyRepository())) {
78
+ throw new NotARepositoryError();
79
+ }
80
+ const page = Math.max(1, options.page || 1);
81
+ const pageSize = clamp(options.pageSize || 25, 1, 500);
82
+ const skip = (page - 1) * pageSize;
83
+ const filterArgs = this.buildFilterArgs(options);
84
+ const cacheKey = filterArgs.join(' ');
85
+ const total = await this.getTotalCount(cacheKey, filterArgs);
86
+ const totalPages = Math.max(1, Math.ceil(total / pageSize));
87
+ const args = [
88
+ 'log',
89
+ `--max-count=${pageSize}`,
90
+ `--skip=${skip}`,
91
+ `--pretty=format:${LOG_FORMAT}${RECORD_SEP}`,
92
+ ...filterArgs
93
+ ];
94
+ const out = await this.git(args, { maxBuffer: 64 * 1024 * 1024 });
95
+ const refs = await this.getRefIndex();
96
+ const commits = this.parseLog(out, refs);
97
+ return {
98
+ commits,
99
+ total,
100
+ page,
101
+ pageSize,
102
+ totalPages,
103
+ hasNext: page < totalPages,
104
+ hasPrevious: page > 1
105
+ };
106
+ }
107
+ async getTotalCount(cacheKey, filterArgs) {
108
+ const now = Date.now();
109
+ const cached = this.countCache.get(cacheKey);
110
+ if (cached && cached.expiresAt > now)
111
+ return cached.total;
112
+ // Insert HEAD before the pathspec separator so that --author/--since/--grep
113
+ // apply to the count. Otherwise rev-list would only honour pathspec filters.
114
+ const sepIdx = filterArgs.indexOf('--');
115
+ const revArgs = sepIdx >= 0
116
+ ? [...filterArgs.slice(0, sepIdx), 'HEAD', ...filterArgs.slice(sepIdx)]
117
+ : [...filterArgs, 'HEAD'];
118
+ let total = 0;
107
119
  try {
108
- const log = await this.git.log({
109
- from: hash,
110
- to: hash,
111
- maxCount: 1
112
- });
113
- if (log.all.length === 0) {
114
- throw new Error('Commit not found');
115
- }
116
- const commit = log.all[0];
117
- const [branches, tags] = await Promise.all([
118
- this.getBranchesForCommit(hash),
119
- this.getTagsForCommit(hash)
120
- ]);
121
- return {
122
- hash: commit.hash,
123
- author: commit.author_name,
124
- date: commit.date,
125
- message: commit.message,
126
- files: await this.getFilesForCommit(hash),
127
- parents: [], // We'll get this from git show if needed
120
+ const out = await this.git(['rev-list', '--count', ...revArgs]);
121
+ total = parseInt(out.trim(), 10) || 0;
122
+ }
123
+ catch {
124
+ const fallback = await this.git(['log', '--oneline', ...filterArgs]).catch(() => '');
125
+ total = fallback ? fallback.split('\n').filter(Boolean).length : 0;
126
+ }
127
+ this.countCache.set(cacheKey, { total, expiresAt: now + COUNT_CACHE_TTL_MS });
128
+ return total;
129
+ }
130
+ buildFilterArgs(options) {
131
+ const args = [];
132
+ if (options.author)
133
+ args.push(`--author=${options.author}`);
134
+ if (options.since)
135
+ args.push(`--since=${options.since}`);
136
+ if (options.until)
137
+ args.push(`--until=${options.until}`);
138
+ if (options.search)
139
+ args.push(`--grep=${options.search}`, '--regexp-ignore-case');
140
+ if (options.file)
141
+ args.push('--', options.file);
142
+ return args;
143
+ }
144
+ parseLog(raw, refs) {
145
+ if (!raw)
146
+ return [];
147
+ const commits = [];
148
+ for (const record of raw.split(RECORD_SEP)) {
149
+ const trimmed = record.trim();
150
+ if (!trimmed)
151
+ continue;
152
+ const fields = trimmed.split(FIELD_SEP);
153
+ if (fields.length < 8)
154
+ continue;
155
+ const [hash, shortHash, author, authorEmail, date, parentsStr, subject, ...rest] = fields;
156
+ const body = rest.join(FIELD_SEP);
157
+ const parents = parentsStr ? parentsStr.split(' ').filter(Boolean) : [];
158
+ const branches = refs.branchesByCommit.get(hash) ?? [];
159
+ const tags = refs.tagsByCommit.get(hash) ?? [];
160
+ commits.push({
161
+ hash,
162
+ shortHash,
163
+ author,
164
+ authorEmail,
165
+ date,
166
+ subject,
167
+ body,
168
+ message: body ? `${subject}\n\n${body}` : subject,
169
+ parents,
128
170
  branches,
129
- tags
130
- };
171
+ tags,
172
+ isMerge: parents.length > 1
173
+ });
131
174
  }
132
- catch (error) {
133
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
134
- throw new Error(`Failed to get commit: ${errorMessage}`);
175
+ return commits;
176
+ }
177
+ async getCommit(hash) {
178
+ if (!isPlausibleHash(hash))
179
+ throw new Error('Invalid commit hash');
180
+ const out = await this.git([
181
+ 'log',
182
+ '--max-count=1',
183
+ `--pretty=format:${LOG_FORMAT}${RECORD_SEP}`,
184
+ hash
185
+ ]);
186
+ const refs = await this.getRefIndex();
187
+ const commits = this.parseLog(out, refs);
188
+ if (commits.length === 0)
189
+ throw new Error('Commit not found');
190
+ return commits[0];
191
+ }
192
+ async getAuthors() {
193
+ const out = await this.git(['log', '--all', '--pretty=format:%an']);
194
+ const seen = new Set();
195
+ for (const line of out.split('\n')) {
196
+ const v = line.trim();
197
+ if (v)
198
+ seen.add(v);
135
199
  }
200
+ return Array.from(seen).sort((a, b) => a.localeCompare(b));
136
201
  }
137
202
  async getDiff(hash) {
138
- try {
139
- const diff = await this.git.diff([hash + '^', hash]);
140
- return this.parseDiff(diff);
203
+ if (!isPlausibleHash(hash))
204
+ throw new Error('Invalid commit hash');
205
+ const parentsOut = await this.git(['log', '-1', '--pretty=format:%P', hash]);
206
+ const parents = parentsOut.trim().split(/\s+/).filter(Boolean);
207
+ let raw;
208
+ if (parents.length === 0) {
209
+ raw = await this.git(['diff-tree', '--root', '-p', '-M', '--no-color', hash]);
141
210
  }
142
- catch (error) {
143
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
144
- throw new Error(`Failed to get diff: ${errorMessage}`);
211
+ else {
212
+ raw = await this.git(['diff', '-M', '--no-color', `${hash}^1`, hash]);
145
213
  }
214
+ return parseUnifiedDiff(raw);
146
215
  }
147
216
  async getBlame(filePath) {
148
- try {
149
- const blame = await this.git.raw(['blame', '--porcelain', filePath]);
150
- return this.parseBlame(blame);
151
- }
152
- catch (error) {
153
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
154
- throw new Error(`Failed to get blame: ${errorMessage}`);
155
- }
217
+ if (filePath.includes('\0'))
218
+ throw new Error('Invalid path');
219
+ const raw = await this.git(['blame', '--porcelain', '--', filePath]);
220
+ return parsePorcelainBlame(raw);
156
221
  }
157
222
  async getTags() {
158
- try {
159
- const tags = await this.git.tags();
160
- return tags.all;
161
- }
162
- catch (error) {
163
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
164
- throw new Error(`Failed to get tags: ${errorMessage}`);
165
- }
223
+ const out = await this.git(['tag', '--list']);
224
+ return out.split('\n').map((s) => s.trim()).filter(Boolean);
166
225
  }
167
226
  async getBranches() {
227
+ const out = await this.git([
228
+ 'for-each-ref',
229
+ '--format=%(refname:short)',
230
+ 'refs/heads',
231
+ 'refs/remotes'
232
+ ]);
233
+ return out.split('\n').map((s) => s.trim()).filter(Boolean);
234
+ }
235
+ async git(args, opts = {}) {
168
236
  try {
169
- const branches = await this.git.branch();
170
- return branches.all;
237
+ const { stdout } = await execFileAsync('git', args, {
238
+ cwd: this.repoPath,
239
+ maxBuffer: opts.maxBuffer ?? 16 * 1024 * 1024,
240
+ env: {
241
+ ...process.env,
242
+ GIT_PAGER: 'cat',
243
+ GIT_TERMINAL_PROMPT: '0',
244
+ LC_ALL: 'C'
245
+ },
246
+ encoding: 'utf8'
247
+ });
248
+ return stdout;
171
249
  }
172
- catch (error) {
173
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
174
- throw new Error(`Failed to get branches: ${errorMessage}`);
250
+ catch (err) {
251
+ const e = err;
252
+ const msg = e.stderr?.toString().trim() || e.message;
253
+ throw new Error(`git ${args[0]} failed: ${msg}`);
175
254
  }
176
255
  }
177
- async getFilesForCommit(hash) {
178
- try {
179
- const diff = await this.git.diff([hash + '^', hash, '--name-only']);
180
- return diff.split('\n').filter(Boolean);
256
+ }
257
+ exports.GitService = GitService;
258
+ function isPlausibleHash(hash) {
259
+ return typeof hash === 'string' && /^[0-9a-fA-F]{4,40}$/.test(hash);
260
+ }
261
+ function clamp(value, min, max) {
262
+ return Math.max(min, Math.min(max, value));
263
+ }
264
+ function parseUnifiedDiff(raw) {
265
+ const files = [];
266
+ if (!raw)
267
+ return files;
268
+ let current = null;
269
+ let currentLines = [];
270
+ const flush = () => {
271
+ if (!current)
272
+ return;
273
+ current.changes = currentLines.join('\n');
274
+ files.push(current);
275
+ current = null;
276
+ currentLines = [];
277
+ };
278
+ for (const line of raw.split('\n')) {
279
+ if (line.startsWith('diff --git ')) {
280
+ flush();
281
+ const match = line.match(/^diff --git a\/(.+?) b\/(.+?)$/);
282
+ const a = match?.[1];
283
+ const b = match?.[2];
284
+ current = {
285
+ file: b ?? a ?? '',
286
+ oldFile: a !== b ? a : undefined,
287
+ status: 'modified',
288
+ additions: 0,
289
+ deletions: 0,
290
+ changes: ''
291
+ };
292
+ currentLines = [line];
181
293
  }
182
- catch (error) {
183
- return [];
294
+ else if (!current) {
295
+ continue;
184
296
  }
185
- }
186
- async getBranchesForCommit(hash) {
187
- try {
188
- const branches = await this.git.branch(['--contains', hash]);
189
- return branches.all;
297
+ else if (line.startsWith('new file mode')) {
298
+ current.status = 'added';
299
+ currentLines.push(line);
190
300
  }
191
- catch (error) {
192
- return [];
301
+ else if (line.startsWith('deleted file mode')) {
302
+ current.status = 'deleted';
303
+ currentLines.push(line);
193
304
  }
194
- }
195
- async getTagsForCommit(hash) {
196
- try {
197
- const tags = await this.git.raw(['tag', '--contains', hash]);
198
- return tags.split('\n').filter(Boolean);
305
+ else if (line.startsWith('rename from ')) {
306
+ current.status = 'renamed';
307
+ current.oldFile = line.substring('rename from '.length);
308
+ currentLines.push(line);
199
309
  }
200
- catch (error) {
201
- return [];
310
+ else if (line.startsWith('rename to ')) {
311
+ current.file = line.substring('rename to '.length);
312
+ currentLines.push(line);
202
313
  }
203
- }
204
- parseDiff(diff) {
205
- const files = [];
206
- const lines = diff.split('\n');
207
- let currentFile = null;
208
- let currentFileLines = [];
209
- for (const line of lines) {
210
- if (line.startsWith('diff --git')) {
211
- if (currentFile) {
212
- currentFile.changes = currentFileLines.join('\n');
213
- files.push(currentFile);
214
- }
215
- const fileMatch = line.match(/b\/(.+)$/);
216
- currentFile = {
217
- file: fileMatch ? fileMatch[1] : '',
218
- additions: 0,
219
- deletions: 0,
220
- changes: ''
221
- };
222
- currentFileLines = [];
223
- }
224
- else if (line.startsWith('+') && !line.startsWith('+++')) {
225
- if (currentFile)
226
- currentFile.additions++;
227
- currentFileLines.push(line);
228
- }
229
- else if (line.startsWith('-') && !line.startsWith('---')) {
230
- if (currentFile)
231
- currentFile.deletions++;
232
- currentFileLines.push(line);
233
- }
234
- else if (line.startsWith('@@') || line.startsWith('---') || line.startsWith('+++') || line.trim() === '') {
235
- // Include git diff headers and context lines
236
- currentFileLines.push(line);
237
- }
238
- else if (line.startsWith(' ')) {
239
- // Context lines (unchanged)
240
- currentFileLines.push(line);
241
- }
314
+ else if (line.startsWith('copy from ')) {
315
+ current.status = 'copied';
316
+ current.oldFile = line.substring('copy from '.length);
317
+ currentLines.push(line);
242
318
  }
243
- if (currentFile) {
244
- currentFile.changes = currentFileLines.join('\n');
245
- files.push(currentFile);
319
+ else if (line.startsWith('Binary files')) {
320
+ current.status = 'binary';
321
+ currentLines.push(line);
322
+ }
323
+ else if (line.startsWith('+') && !line.startsWith('+++')) {
324
+ current.additions++;
325
+ currentLines.push(line);
326
+ }
327
+ else if (line.startsWith('-') && !line.startsWith('---')) {
328
+ current.deletions++;
329
+ currentLines.push(line);
330
+ }
331
+ else {
332
+ currentLines.push(line);
246
333
  }
247
- return files;
248
334
  }
249
- parseBlame(blame) {
250
- const lines = [];
251
- const blameLines = blame.split('\n');
252
- let currentLine = null;
253
- for (const line of blameLines) {
254
- if (line.startsWith('author ')) {
255
- if (currentLine) {
256
- lines.push(currentLine);
257
- }
258
- const hash = blameLines[blameLines.indexOf(line) - 1].split(' ')[0];
259
- const author = line.substring(7);
260
- const dateLine = blameLines[blameLines.indexOf(line) + 1];
261
- const date = dateLine.startsWith('author-time ')
262
- ? new Date(parseInt(dateLine.substring(12)) * 1000).toISOString()
263
- : '';
264
- currentLine = {
265
- line: lines.length + 1,
266
- hash,
267
- author,
268
- date,
269
- content: ''
270
- };
271
- }
272
- else if (line.startsWith('\t') && currentLine) {
273
- currentLine.content = line.substring(1);
274
- }
335
+ flush();
336
+ return files;
337
+ }
338
+ function parsePorcelainBlame(raw) {
339
+ if (!raw)
340
+ return [];
341
+ const lines = raw.split('\n');
342
+ const out = [];
343
+ const meta = new Map();
344
+ let i = 0;
345
+ let lineNumber = 0;
346
+ while (i < lines.length) {
347
+ const header = lines[i];
348
+ if (!header) {
349
+ i++;
350
+ continue;
275
351
  }
276
- if (currentLine) {
277
- lines.push(currentLine);
352
+ const m = header.match(/^([0-9a-f]{40}) \d+ (\d+)(?: \d+)?$/);
353
+ if (!m) {
354
+ i++;
355
+ continue;
278
356
  }
279
- return lines;
357
+ const hash = m[1];
358
+ lineNumber = parseInt(m[2], 10);
359
+ i++;
360
+ let author = meta.get(hash)?.author ?? '';
361
+ let epoch = meta.get(hash)?.epoch ?? 0;
362
+ while (i < lines.length && !lines[i].startsWith('\t')) {
363
+ const h = lines[i];
364
+ if (h.startsWith('author '))
365
+ author = h.substring(7);
366
+ else if (h.startsWith('author-time '))
367
+ epoch = parseInt(h.substring(12), 10);
368
+ i++;
369
+ }
370
+ meta.set(hash, { author, epoch });
371
+ const content = i < lines.length && lines[i].startsWith('\t') ? lines[i].substring(1) : '';
372
+ out.push({
373
+ line: lineNumber,
374
+ hash,
375
+ author,
376
+ date: epoch ? new Date(epoch * 1000).toISOString() : '',
377
+ content
378
+ });
379
+ i++;
280
380
  }
381
+ return out;
281
382
  }
282
- exports.GitService = GitService;
283
383
  //# sourceMappingURL=gitService.js.map