git-reviewer 2.6.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/lib/changes.js +129 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://github.com/dheerajjha/reviewer/actions/workflows/ci.yml)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](package.json)
|
|
6
|
-
[](test/)
|
|
7
7
|
[](package.json)
|
|
8
8
|
|
|
9
9
|
When an agent writes the code, reading it becomes the bottleneck — and the tool
|
|
@@ -294,7 +294,7 @@ rather than a public issue.
|
|
|
294
294
|
|
|
295
295
|
```bash
|
|
296
296
|
npm install # 3 dependencies, no build step, ~6MB
|
|
297
|
-
npm test #
|
|
297
|
+
npm test # 281 tests
|
|
298
298
|
npm run test:watch
|
|
299
299
|
npm run test:coverage
|
|
300
300
|
```
|
package/lib/changes.js
CHANGED
|
@@ -14,6 +14,119 @@
|
|
|
14
14
|
* @property {'A'|'M'|'D'|'R'|'B'} status
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Unquotes git's octal-escaped, C-quoted filename strings (e.g. "\"caf\\303\\251.txt\"" -> "café.txt").
|
|
19
|
+
*
|
|
20
|
+
* When core.quotePath is enabled (default in git), paths containing non-ASCII
|
|
21
|
+
* or control characters are enclosed in double quotes and byte-escaped with octal
|
|
22
|
+
* sequences.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} str
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
function unquoteGitPath(str) {
|
|
28
|
+
if (typeof str !== 'string') return str;
|
|
29
|
+
if (str.length >= 2 && str.startsWith('"') && str.endsWith('"')) {
|
|
30
|
+
const inner = str.slice(1, -1);
|
|
31
|
+
const bytes = [];
|
|
32
|
+
for (let i = 0; i < inner.length; i++) {
|
|
33
|
+
if (inner[i] === '\\' && i + 1 < inner.length) {
|
|
34
|
+
i++;
|
|
35
|
+
const next = inner[i];
|
|
36
|
+
if (next >= '0' && next <= '7') {
|
|
37
|
+
let octal = next;
|
|
38
|
+
if (i + 1 < inner.length && inner[i + 1] >= '0' && inner[i + 1] <= '7') {
|
|
39
|
+
i++;
|
|
40
|
+
octal += inner[i];
|
|
41
|
+
if (i + 1 < inner.length && inner[i + 1] >= '0' && inner[i + 1] <= '7') {
|
|
42
|
+
i++;
|
|
43
|
+
octal += inner[i];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
bytes.push(parseInt(octal, 8));
|
|
47
|
+
} else if (next === 'a') {
|
|
48
|
+
bytes.push(0x07);
|
|
49
|
+
} else if (next === 'b') {
|
|
50
|
+
bytes.push(0x08);
|
|
51
|
+
} else if (next === 't') {
|
|
52
|
+
bytes.push(0x09);
|
|
53
|
+
} else if (next === 'n') {
|
|
54
|
+
bytes.push(0x0a);
|
|
55
|
+
} else if (next === 'v') {
|
|
56
|
+
bytes.push(0x0b);
|
|
57
|
+
} else if (next === 'f') {
|
|
58
|
+
bytes.push(0x0c);
|
|
59
|
+
} else if (next === 'r') {
|
|
60
|
+
bytes.push(0x0d);
|
|
61
|
+
} else if (next === '"') {
|
|
62
|
+
bytes.push(0x22);
|
|
63
|
+
} else if (next === '\\') {
|
|
64
|
+
bytes.push(0x5c);
|
|
65
|
+
} else {
|
|
66
|
+
const code = next.charCodeAt(0);
|
|
67
|
+
if (code >= 0xd800 && code <= 0xdbff && i + 1 < inner.length) {
|
|
68
|
+
const nextCode = inner.charCodeAt(i + 1);
|
|
69
|
+
if (nextCode >= 0xdc00 && nextCode <= 0xdfff) {
|
|
70
|
+
const pair = inner.slice(i, i + 2);
|
|
71
|
+
i++;
|
|
72
|
+
const buf = Buffer.from(pair, 'utf8');
|
|
73
|
+
for (const b of buf) bytes.push(b);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const buf = Buffer.from(next, 'utf8');
|
|
78
|
+
for (const b of buf) bytes.push(b);
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
const code = inner.charCodeAt(i);
|
|
82
|
+
if (code >= 0xd800 && code <= 0xdbff && i + 1 < inner.length) {
|
|
83
|
+
const nextCode = inner.charCodeAt(i + 1);
|
|
84
|
+
if (nextCode >= 0xdc00 && nextCode <= 0xdfff) {
|
|
85
|
+
const pair = inner.slice(i, i + 2);
|
|
86
|
+
i++;
|
|
87
|
+
const buf = Buffer.from(pair, 'utf8');
|
|
88
|
+
for (const b of buf) bytes.push(b);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const buf = Buffer.from(inner[i], 'utf8');
|
|
93
|
+
for (const b of buf) bytes.push(b);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return Buffer.from(bytes).toString('utf8');
|
|
97
|
+
}
|
|
98
|
+
return str;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Extracts the target path and whether the file entry represents a rename
|
|
103
|
+
* from git diff --stat display format (e.g. "old => new", "prefix/{old => new}.ext").
|
|
104
|
+
*
|
|
105
|
+
* @param {string} raw
|
|
106
|
+
* @returns {{ isRename: boolean, path: string }}
|
|
107
|
+
*/
|
|
108
|
+
function parseCommitFilePath(raw) {
|
|
109
|
+
if (typeof raw !== 'string') return { isRename: false, path: raw ?? '' };
|
|
110
|
+
|
|
111
|
+
// 1. Brace rename: prefix{old => new}suffix or {old => new}
|
|
112
|
+
const braceMatch = raw.match(/^(.*?)\{(?:.*?) => (.*?)\}(.*)$/);
|
|
113
|
+
if (braceMatch) {
|
|
114
|
+
const rawTo = braceMatch[1] + braceMatch[2] + braceMatch[3];
|
|
115
|
+
const normalized = unquoteGitPath(rawTo).replace(/\/{2,}/g, '/').replace(/^\/+/, '');
|
|
116
|
+
return { isRename: true, path: normalized };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 2. Arrow rename: old => new (or "old" => "new")
|
|
120
|
+
const arrowMatch = raw.match(/^(?:.*?) => (.*)$/);
|
|
121
|
+
if (arrowMatch) {
|
|
122
|
+
const rawTo = arrowMatch[1].trim();
|
|
123
|
+
const normalized = unquoteGitPath(rawTo).replace(/\/{2,}/g, '/').replace(/^\/+/, '');
|
|
124
|
+
return { isRename: true, path: normalized };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { isRename: false, path: unquoteGitPath(raw).replace(/^\/+/, '') };
|
|
128
|
+
}
|
|
129
|
+
|
|
17
130
|
/**
|
|
18
131
|
* Collect working-directory changes from a `simple-git` status result.
|
|
19
132
|
*
|
|
@@ -39,7 +152,8 @@ function collectWorkingChanges(status) {
|
|
|
39
152
|
for (const [files, letter] of buckets) {
|
|
40
153
|
for (const entry of files) {
|
|
41
154
|
// `renamed` holds `{ from, to }`; every other bucket holds plain strings.
|
|
42
|
-
const
|
|
155
|
+
const rawPath = typeof entry === 'string' ? entry : entry?.to;
|
|
156
|
+
const filePath = unquoteGitPath(rawPath);
|
|
43
157
|
if (!filePath || seen.has(filePath)) continue;
|
|
44
158
|
seen.set(filePath, { path: filePath, status: letter });
|
|
45
159
|
}
|
|
@@ -56,9 +170,10 @@ function collectWorkingChanges(status) {
|
|
|
56
170
|
* it deletes none.
|
|
57
171
|
*
|
|
58
172
|
* @param {object} file entry from `git.diffSummary()`
|
|
59
|
-
* @returns {'A'|'M'|'D'|'B'}
|
|
173
|
+
* @returns {'A'|'M'|'D'|'R'|'B'}
|
|
60
174
|
*/
|
|
61
175
|
function classifyCommitFile(file) {
|
|
176
|
+
if (file?.file && parseCommitFilePath(file.file).isRename) return 'R';
|
|
62
177
|
if (file?.binary) return 'B';
|
|
63
178
|
|
|
64
179
|
const insertions = file?.insertions ?? 0;
|
|
@@ -78,7 +193,17 @@ function classifyCommitFile(file) {
|
|
|
78
193
|
*/
|
|
79
194
|
function collectCommitChanges(diffSummary) {
|
|
80
195
|
const files = diffSummary?.files ?? [];
|
|
81
|
-
return files.map(file =>
|
|
196
|
+
return files.map(file => {
|
|
197
|
+
const { path, isRename } = parseCommitFilePath(file?.file);
|
|
198
|
+
const status = isRename ? 'R' : classifyCommitFile(file);
|
|
199
|
+
return { path, status };
|
|
200
|
+
});
|
|
82
201
|
}
|
|
83
202
|
|
|
84
|
-
module.exports = {
|
|
203
|
+
module.exports = {
|
|
204
|
+
collectWorkingChanges,
|
|
205
|
+
collectCommitChanges,
|
|
206
|
+
classifyCommitFile,
|
|
207
|
+
unquoteGitPath,
|
|
208
|
+
parseCommitFilePath
|
|
209
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git-reviewer",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"description": "Review your coding agent's changes like a pull request — locally, with inline comments and threaded follow-ups, then hand the marked-up review back to the agent",
|
|
5
5
|
"author": "Dheeraj Jha",
|
|
6
6
|
"license": "MIT",
|