git-history-ui 1.0.6 → 2.0.1
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/CHANGELOG.md +60 -0
- package/build/frontend/index.html +13 -10
- package/build/frontend/main-YTFHD36T.js +11 -0
- package/build/frontend/styles-YQ73RJ2V.css +1 -0
- package/dist/backend/dev-server.d.ts +0 -1
- package/dist/backend/dev-server.js +8 -4
- package/dist/backend/dev-server.js.map +1 -1
- package/dist/backend/gitService.d.ts +24 -10
- package/dist/backend/gitService.js +340 -240
- package/dist/backend/gitService.js.map +1 -1
- package/dist/backend/server.d.ts +8 -2
- package/dist/backend/server.js +145 -112
- package/dist/backend/server.js.map +1 -1
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +44 -22
- package/dist/cli.js.map +1 -1
- package/package.json +43 -27
- package/build/frontend/main-44CFNHDH.js +0 -8
- package/build/frontend/main-5GQESVK5.js +0 -8
- package/build/frontend/main-KMFUNYSW.js +0 -8
- package/build/frontend/main-YHO6NCZZ.js +0 -8
- package/build/frontend/styles-26JPPBSI.css +0 -1
- package/build/frontend/styles-J5I4DBTU.css +0 -1
- package/dist/__tests__/gitService.test.d.ts +0 -2
- package/dist/__tests__/gitService.test.d.ts.map +0 -1
- package/dist/__tests__/gitService.test.js +0 -435
- package/dist/__tests__/gitService.test.js.map +0 -1
- package/dist/__tests__/setup.d.ts +0 -2
- package/dist/__tests__/setup.d.ts.map +0 -1
- package/dist/__tests__/setup.js +0 -24
- package/dist/__tests__/setup.js.map +0 -1
- package/dist/backend/dev-server.d.ts.map +0 -1
- package/dist/backend/gitService.d.ts.map +0 -1
- package/dist/backend/server.d.ts.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/config/paths.d.ts +0 -14
- package/dist/config/paths.d.ts.map +0 -1
- package/dist/config/paths.js +0 -35
- 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
|
|
8
|
-
|
|
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
|
-
|
|
14
|
+
super('Not a git repository');
|
|
15
|
+
this.name = 'NotARepositoryError';
|
|
11
16
|
}
|
|
12
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
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
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
143
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
159
|
-
|
|
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
|
|
170
|
-
|
|
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 (
|
|
173
|
-
const
|
|
174
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
183
|
-
|
|
294
|
+
else if (!current) {
|
|
295
|
+
continue;
|
|
184
296
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
192
|
-
|
|
301
|
+
else if (line.startsWith('deleted file mode')) {
|
|
302
|
+
current.status = 'deleted';
|
|
303
|
+
currentLines.push(line);
|
|
193
304
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
201
|
-
|
|
310
|
+
else if (line.startsWith('rename to ')) {
|
|
311
|
+
current.file = line.substring('rename to '.length);
|
|
312
|
+
currentLines.push(line);
|
|
202
313
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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 (
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
277
|
-
|
|
352
|
+
const m = header.match(/^([0-9a-f]{40}) \d+ (\d+)(?: \d+)?$/);
|
|
353
|
+
if (!m) {
|
|
354
|
+
i++;
|
|
355
|
+
continue;
|
|
278
356
|
}
|
|
279
|
-
|
|
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
|