reslop 0.1.8 → 0.1.9
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 +14 -1
- package/README.md +26 -38
- package/lib/ansi.js +2 -0
- package/lib/capabilities.js +3 -0
- package/lib/deps/changes.js +92 -1
- package/lib/deps/items.js +5 -2
- package/lib/deps/manifest.js +7 -0
- package/lib/deps/rows.js +0 -1
- package/lib/deps.js +65 -10
- package/lib/files.js +30 -1
- package/lib/git/branches.js +97 -5
- package/lib/git.js +6 -0
- package/lib/keys.js +9 -2
- package/lib/render/branches.js +39 -17
- package/lib/render/chrome.js +35 -15
- package/lib/render/commits.js +138 -0
- package/lib/render/files.js +30 -10
- package/lib/render/frame.js +6 -1
- package/lib/render/primitives.js +1 -1
- package/lib/render.js +1 -2
- package/lib/session/actions.js +65 -24
- package/lib/session/branches.js +16 -18
- package/lib/session/change-actions.js +8 -5
- package/lib/session/commit.js +191 -29
- package/lib/session/dispatch.js +12 -2
- package/lib/session/files-pane.js +10 -1
- package/lib/session/items.js +112 -7
- package/lib/session/lifecycle.js +44 -4
- package/lib/session/navigation.js +3 -2
- package/lib/session/pointer.js +7 -0
- package/lib/session/view.js +4 -2
- package/lib/session.js +37 -9
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased][unreleased]
|
|
4
4
|
|
|
5
|
+
- Remove `r` reload hotkey and hint; auto-reload covers disk changes
|
|
6
|
+
- Improve branch list TUI/TUX
|
|
7
|
+
- Right-align the time column in the branch list
|
|
8
|
+
- Refresh the branch list after push, pull, and rebase
|
|
9
|
+
- Keep diff viewer order when staging or unstaging a block
|
|
10
|
+
- Add a commits list screen with commit operation
|
|
11
|
+
- Show staged/total rather than staged/unstaged in files list
|
|
12
|
+
- Show file ages in the file list right-aligned
|
|
13
|
+
- Sort the file list by path from the repo root
|
|
14
|
+
- Update file-list hotkeys and hints
|
|
15
|
+
- Fix dependency updates, locks and audit
|
|
16
|
+
|
|
5
17
|
## [0.1.8][] - 2026-09-17
|
|
6
18
|
|
|
7
19
|
- Simplify session controllers, input dispatch, and loading orchestration
|
|
@@ -83,7 +95,8 @@
|
|
|
83
95
|
- Feedback and todos that produce a repair plan for an agent
|
|
84
96
|
- Intra-line highlighting in unified, mixed, and side-by-side layouts
|
|
85
97
|
|
|
86
|
-
[unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.1.
|
|
98
|
+
[unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.1.9...HEAD
|
|
99
|
+
[0.1.9]: https://github.com/tshemsedinov/reslop/compare/v0.1.8...v0.1.9
|
|
87
100
|
[0.1.8]: https://github.com/tshemsedinov/reslop/compare/v0.1.7...v0.1.8
|
|
88
101
|
[0.1.7]: https://github.com/tshemsedinov/reslop/compare/v0.1.6...v0.1.7
|
|
89
102
|
[0.1.6]: https://github.com/tshemsedinov/reslop/compare/v0.1.5...v0.1.6
|
package/README.md
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
# 👁️ reslop
|
|
1
|
+
# 👁️ reslop
|
|
2
|
+
|
|
3
|
+
## Review AI generated code and plan changes
|
|
2
4
|
|
|
3
5
|
[](https://github.com/tshemsedinov/reslop/actions?query=workflow%3A%22Testing+CI%22+branch%3Amain)
|
|
4
6
|
[](https://snyk.io/test/github/tshemsedinov/reslop)
|
|
@@ -16,16 +18,18 @@ for the agent.
|
|
|
16
18
|
Review → Plan → Repair → Verify
|
|
17
19
|
```
|
|
18
20
|
|
|
19
|
-
- Leave feedback, issues/todos, and in-place code proposals as a repair plan
|
|
20
|
-
- Review uncommitted git diffs, a given commit, a GitHub PR, or a GitLab MR
|
|
21
|
-
- Stage, unstage, or revert each contiguous block of diff lines
|
|
22
|
-
- Auto-reload local diffs when files change, keeping the current screen
|
|
23
|
-
- Import GitHub PR and GitLab MR review comments into the local plan for AI
|
|
24
|
-
-
|
|
25
|
-
- Branches: checkout, create, rebase, drop, pull and push
|
|
26
|
-
- Review each npm dependency once across `package.json` and the lockfile
|
|
27
|
-
- Propose unused removals, npm audit fixes, and outdated updates as diffs
|
|
28
|
-
- Code highlighting
|
|
21
|
+
- Leave feedback, issues/todos, and in-place code proposals as a repair plan
|
|
22
|
+
- Review uncommitted git diffs, a given commit, a GitHub PR, or a GitLab MR
|
|
23
|
+
- Stage, unstage, or revert each contiguous block of diff lines
|
|
24
|
+
- Auto-reload local diffs when files change, keeping the current screen
|
|
25
|
+
- Import GitHub PR and GitLab MR review comments into the local plan for AI
|
|
26
|
+
- Commits list: commit, amend, fixup, and drop
|
|
27
|
+
- Branches: checkout, create, rebase, drop, pull, and push
|
|
28
|
+
- Review each npm dependency once across `package.json` and the lockfile
|
|
29
|
+
- Propose unused removals, npm audit fixes, and outdated updates as diffs
|
|
30
|
+
- Code highlighting and intra-line diffs: unified, mixed, and side-by-side
|
|
31
|
+
- Auto-update patch and minor releases; confirm a new major
|
|
32
|
+
- Work with git, npm and fetch in background
|
|
29
33
|
|
|
30
34
|
## Install
|
|
31
35
|
|
|
@@ -51,35 +55,19 @@ Reviews go in `.review/YYYY-MM-DD-NN.md` with frontmatter `status`:
|
|
|
51
55
|
- `partial`: AI started; some items remain
|
|
52
56
|
- `done`: all items marked `[x]`
|
|
53
57
|
|
|
54
|
-
## Hotkeys
|
|
55
|
-
|
|
56
|
-
| Key | Action |
|
|
57
|
-
| ------------------- | --------------------------------------------- |
|
|
58
|
-
| `a` / `u` / `d` | Stage / unstage / drop this diff |
|
|
59
|
-
| `c` | File list: `c` commit / `a` amend / `f` fixup |
|
|
60
|
-
| `b` | List branches; `n` new; `r` rebase; `d` drop |
|
|
61
|
-
| `p` / `s` | git pull/push |
|
|
62
|
-
| `f` / `t` / `e` | Feedback / repo todo / edit added lines |
|
|
63
|
-
| `m` | Mode: unified / mixed / side-by-side |
|
|
64
|
-
| `⏎` | Open the selected file |
|
|
65
|
-
| `→` / `j` | Next remaining diff / file |
|
|
66
|
-
| `←` / `k` | Previous remaining diff / file |
|
|
67
|
-
| `Ctrl-e` / `Ctrl-y` | Scroll one line down / up |
|
|
68
|
-
| `Ctrl-f` / `Ctrl-b` | Scroll page down / up |
|
|
69
|
-
| `Ctrl-d` / `Ctrl-u` | Scroll half-page down / up |
|
|
70
|
-
| `⌫` / `Del` | Remove the selected todo |
|
|
71
|
-
| `Esc` / `q` | Quit |
|
|
72
|
-
|
|
73
58
|
## Future
|
|
74
59
|
|
|
75
|
-
- Import issues from GitHub and GitLab
|
|
76
|
-
-
|
|
77
|
-
-
|
|
78
|
-
- Send
|
|
79
|
-
-
|
|
80
|
-
-
|
|
81
|
-
-
|
|
82
|
-
-
|
|
60
|
+
- Import issues from GitHub and GitLab
|
|
61
|
+
- Search, blame, and file history in the review
|
|
62
|
+
- Security and code-quality audit; propose a repair plan
|
|
63
|
+
- Send anonymized code blocks for expert review
|
|
64
|
+
- Send questions and the repair plan to experts for approval
|
|
65
|
+
- Ask experts
|
|
66
|
+
- Verify the codebase
|
|
67
|
+
- Apply refactoring skills
|
|
68
|
+
- Call agents, harnesses, and IDEs to execute prepared plans
|
|
69
|
+
- Squash, cherry-pick, and interactive rebase
|
|
70
|
+
- Merge, rename, and conflict resolution
|
|
83
71
|
|
|
84
72
|
## License
|
|
85
73
|
|
package/lib/ansi.js
CHANGED
|
@@ -67,6 +67,7 @@ const THEME = {
|
|
|
67
67
|
buttonFg: [0x6a, 0x6a, 0x7e],
|
|
68
68
|
buttonBg: [0x2a, 0x2a, 0x40],
|
|
69
69
|
buttonHotFg: [0xff, 0xff, 0xff],
|
|
70
|
+
currentBg: [0xa8, 0xcc, 0xb4],
|
|
70
71
|
checkFg: [0xf4, 0xf4, 0xfc],
|
|
71
72
|
checkBg: [0x52, 0x52, 0x74],
|
|
72
73
|
checkDoneFg: [0xf0, 0xff, 0xf4],
|
|
@@ -75,6 +76,7 @@ const THEME = {
|
|
|
75
76
|
warnFg: [0xe8, 0xc8, 0x4a],
|
|
76
77
|
mutedFg: [0x7a, 0x7a, 0x8a],
|
|
77
78
|
shaFg: [0x56, 0x9c, 0xd6],
|
|
79
|
+
shaDarkFg: [0x15, 0x4a, 0x8c],
|
|
78
80
|
stagedDelLineFg: [0xa0, 0xa0, 0xa8],
|
|
79
81
|
stagedDelLineBg: [0x20, 0x20, 0x26],
|
|
80
82
|
stagedDelCharFg: [0xf0, 0xf0, 0xf4],
|
package/lib/capabilities.js
CHANGED
|
@@ -25,6 +25,7 @@ const BRANCH_METHODS = [
|
|
|
25
25
|
'hasStaged',
|
|
26
26
|
'lastMessage',
|
|
27
27
|
'listBranches',
|
|
28
|
+
'listCommits',
|
|
28
29
|
'currentBranch',
|
|
29
30
|
'checkout',
|
|
30
31
|
'checkoutAsync',
|
|
@@ -34,6 +35,8 @@ const BRANCH_METHODS = [
|
|
|
34
35
|
'rebaseAsync',
|
|
35
36
|
'drop',
|
|
36
37
|
'dropAsync',
|
|
38
|
+
'dropCommit',
|
|
39
|
+
'dropCommitAsync',
|
|
37
40
|
'pull',
|
|
38
41
|
'pullAsync',
|
|
39
42
|
'push',
|
package/lib/deps/changes.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { itemPath } = require('../files.js');
|
|
4
|
+
const {
|
|
5
|
+
countSides,
|
|
6
|
+
formatHunkHeader,
|
|
7
|
+
formatPatch,
|
|
8
|
+
} = require('../diff/patch.js');
|
|
4
9
|
|
|
5
10
|
const manifest = require('./manifest.js');
|
|
6
11
|
const { MANIFEST, LOCKFILE, SECTION_KEYS, depFileMeta, relOf } = manifest;
|
|
@@ -73,6 +78,87 @@ const diffOtherFields = (oldPkg, newPkg) => {
|
|
|
73
78
|
return changes;
|
|
74
79
|
};
|
|
75
80
|
|
|
81
|
+
const foldedDepNames = (changes) => {
|
|
82
|
+
const names = new Set();
|
|
83
|
+
for (const change of changes) {
|
|
84
|
+
if (change.section === 'field') continue;
|
|
85
|
+
names.add(change.name);
|
|
86
|
+
}
|
|
87
|
+
return names;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const lineEntryName = (text) => {
|
|
91
|
+
const entry = DEP_ENTRY.exec(text);
|
|
92
|
+
return entry ? entry[1] : '';
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const lineInBlock = (item, line) => {
|
|
96
|
+
const blockId = item.blockId;
|
|
97
|
+
if (typeof blockId !== 'number') return true;
|
|
98
|
+
return line.blockId === blockId;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const isSectionFence = (text) =>
|
|
102
|
+
SECTION_OPEN.test(text) || SECTION_END.test(text);
|
|
103
|
+
|
|
104
|
+
const hunkOnlyFoldedDeps = (item, names) => {
|
|
105
|
+
if (!item.hunk || !names.size) return false;
|
|
106
|
+
let folded = 0;
|
|
107
|
+
for (const line of item.hunk.lines) {
|
|
108
|
+
if (line.type === 'ctx') continue;
|
|
109
|
+
if (!lineInBlock(item, line)) continue;
|
|
110
|
+
const name = lineEntryName(line.text);
|
|
111
|
+
if (name && names.has(name)) {
|
|
112
|
+
folded += 1;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (isSectionFence(line.text)) continue;
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
return folded > 0;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const stripFoldedDepItem = (item, names) => {
|
|
122
|
+
if (!item.hunk || !names.size) return item;
|
|
123
|
+
const lines = [];
|
|
124
|
+
let removed = false;
|
|
125
|
+
for (const line of item.hunk.lines) {
|
|
126
|
+
if (line.type !== 'ctx') {
|
|
127
|
+
const name = lineEntryName(line.text);
|
|
128
|
+
if (name && names.has(name)) {
|
|
129
|
+
removed = true;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
lines.push(line);
|
|
134
|
+
}
|
|
135
|
+
if (!removed) return item;
|
|
136
|
+
let hasEdit = false;
|
|
137
|
+
for (const line of lines) {
|
|
138
|
+
if (line.type === 'ctx') continue;
|
|
139
|
+
if (!lineInBlock(item, line)) continue;
|
|
140
|
+
hasEdit = true;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
if (!hasEdit) return null;
|
|
144
|
+
const counts = countSides(lines);
|
|
145
|
+
const hunk = {
|
|
146
|
+
...item.hunk,
|
|
147
|
+
oldCount: counts.oldCount,
|
|
148
|
+
newCount: counts.newCount,
|
|
149
|
+
header: formatHunkHeader(
|
|
150
|
+
item.hunk.oldStart,
|
|
151
|
+
counts.oldCount,
|
|
152
|
+
item.hunk.newStart,
|
|
153
|
+
counts.newCount,
|
|
154
|
+
),
|
|
155
|
+
lines,
|
|
156
|
+
};
|
|
157
|
+
const patchAdd = formatPatch(item.file, hunk, item.blockId, 'old');
|
|
158
|
+
const patchRevert = formatPatch(item.file, hunk, item.blockId, 'new');
|
|
159
|
+
return { ...item, hunk, patchAdd, patchRevert };
|
|
160
|
+
};
|
|
161
|
+
|
|
76
162
|
const onlyDepEdits = (items) => {
|
|
77
163
|
if (!items.length) return false;
|
|
78
164
|
for (const item of items) {
|
|
@@ -314,9 +400,12 @@ const collectGroups = (items) => {
|
|
|
314
400
|
const takeManifestItems = (summary) => {
|
|
315
401
|
if (!summary.foldManifest) return [];
|
|
316
402
|
if (summary.takeAllManifest) return [...summary.manifestItems];
|
|
403
|
+
const names = foldedDepNames(summary.changes);
|
|
317
404
|
const taken = [];
|
|
318
405
|
for (const item of summary.manifestItems) {
|
|
319
|
-
if (onlyDepEdits([item])
|
|
406
|
+
if (onlyDepEdits([item]) || hunkOnlyFoldedDeps(item, names)) {
|
|
407
|
+
taken.push(item);
|
|
408
|
+
}
|
|
320
409
|
}
|
|
321
410
|
return taken;
|
|
322
411
|
};
|
|
@@ -328,4 +417,6 @@ module.exports = {
|
|
|
328
417
|
summarizeGroup,
|
|
329
418
|
collectGroups,
|
|
330
419
|
takeManifestItems,
|
|
420
|
+
foldedDepNames,
|
|
421
|
+
stripFoldedDepItem,
|
|
331
422
|
};
|
package/lib/deps/items.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { itemPath } = require('../files.js');
|
|
4
4
|
|
|
5
|
-
const { depFileMeta } = require('./manifest.js');
|
|
5
|
+
const { depFileMeta, listFilePath } = require('./manifest.js');
|
|
6
6
|
const { orderedChanges } = require('./changes.js');
|
|
7
7
|
const { buildRows, makeHunk } = require('./rows.js');
|
|
8
8
|
|
|
@@ -57,7 +57,7 @@ const makeDepItem = (origin, fileItems, hidden, change, extra, blockId) => {
|
|
|
57
57
|
const allFiles = uniquePaths(fileItems);
|
|
58
58
|
const files = change ? filesForChange(change, allFiles) : allFiles;
|
|
59
59
|
const primary = pickPrimary(fileItems);
|
|
60
|
-
const oldPath = itemPath(primary);
|
|
60
|
+
const oldPath = listFilePath(itemPath(primary));
|
|
61
61
|
const newPath = oldPath;
|
|
62
62
|
const isNew = fileItems.some((item) => item.file && item.file.isNew);
|
|
63
63
|
const isDeleted = fileItems.every((item) => item.file && item.file.isDeleted);
|
|
@@ -92,6 +92,9 @@ const makeDepItem = (origin, fileItems, hidden, change, extra, blockId) => {
|
|
|
92
92
|
const makeDepItems = (origin, fileItems, hidden, summary) => {
|
|
93
93
|
const ordered = orderedChanges(summary.changes);
|
|
94
94
|
if (!ordered.length) {
|
|
95
|
+
const oldCount = summary.oldCount ?? 0;
|
|
96
|
+
const newCount = summary.newCount ?? 0;
|
|
97
|
+
if (oldCount === newCount) return [];
|
|
95
98
|
return [makeDepItem(origin, fileItems, hidden, null, summary, 0)];
|
|
96
99
|
}
|
|
97
100
|
const items = [];
|
package/lib/deps/manifest.js
CHANGED
|
@@ -29,6 +29,12 @@ const depFileMeta = (rel) => {
|
|
|
29
29
|
|
|
30
30
|
const relOf = (dir, name) => (dir === '.' ? name : `${dir}/${name}`);
|
|
31
31
|
|
|
32
|
+
const listFilePath = (rel) => {
|
|
33
|
+
const meta = depFileMeta(rel);
|
|
34
|
+
if (meta && meta.kind === 'lockfile') return relOf(meta.dir, MANIFEST);
|
|
35
|
+
return posixRel(rel);
|
|
36
|
+
};
|
|
37
|
+
|
|
32
38
|
const parseJson = (text) => {
|
|
33
39
|
if (!text || !`${text}`.trim()) return null;
|
|
34
40
|
try {
|
|
@@ -134,6 +140,7 @@ module.exports = {
|
|
|
134
140
|
posixRel,
|
|
135
141
|
depFileMeta,
|
|
136
142
|
relOf,
|
|
143
|
+
listFilePath,
|
|
137
144
|
parseJson,
|
|
138
145
|
emptySectionMaps,
|
|
139
146
|
readSections,
|
package/lib/deps/rows.js
CHANGED
package/lib/deps.js
CHANGED
|
@@ -4,6 +4,7 @@ const { itemPath } = require('./files.js');
|
|
|
4
4
|
|
|
5
5
|
const manifest = require('./deps/manifest.js');
|
|
6
6
|
const { MANIFEST, LOCKFILE, depFileMeta, parseJson } = manifest;
|
|
7
|
+
const { relOf, listFilePath } = manifest;
|
|
7
8
|
const { readSections, lockEntries, lockPackageCount } = manifest;
|
|
8
9
|
|
|
9
10
|
const { mergeDepFile } = require('./deps/merge.js');
|
|
@@ -11,6 +12,7 @@ const { mergeDepFile } = require('./deps/merge.js');
|
|
|
11
12
|
const changes = require('./deps/changes.js');
|
|
12
13
|
const { diffSections, mergeResolved, summarizeGroup } = changes;
|
|
13
14
|
const { collectGroups, takeManifestItems } = changes;
|
|
15
|
+
const { foldedDepNames, stripFoldedDepItem } = changes;
|
|
14
16
|
|
|
15
17
|
const { makeDepItems, stubFileItem } = require('./deps/items.js');
|
|
16
18
|
|
|
@@ -46,6 +48,47 @@ const loadExportEntries = (names, extra = {}, pkgDir) => {
|
|
|
46
48
|
return merged;
|
|
47
49
|
};
|
|
48
50
|
|
|
51
|
+
const groupDir = (key) => key.slice(key.indexOf('\0') + 1);
|
|
52
|
+
|
|
53
|
+
const groupedSynthetics = (synthetic) => {
|
|
54
|
+
const byPath = new Map();
|
|
55
|
+
for (const key of synthetic.keys()) {
|
|
56
|
+
const rel = relOf(groupDir(key), MANIFEST);
|
|
57
|
+
const list = byPath.get(rel) ?? [];
|
|
58
|
+
for (const item of synthetic.get(key)) list.push(item);
|
|
59
|
+
byPath.set(rel, list);
|
|
60
|
+
}
|
|
61
|
+
return byPath;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const spliceAtSlots = (out, synthetic, slot) => {
|
|
65
|
+
const byPath = groupedSynthetics(synthetic);
|
|
66
|
+
const inserts = [];
|
|
67
|
+
for (const rel of byPath.keys()) {
|
|
68
|
+
const at = slot.has(rel) ? slot.get(rel) : out.length;
|
|
69
|
+
inserts.push({ at, items: byPath.get(rel) });
|
|
70
|
+
}
|
|
71
|
+
inserts.sort((left, right) => right.at - left.at);
|
|
72
|
+
for (const insert of inserts) {
|
|
73
|
+
out.splice(insert.at, 0, ...insert.items);
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const lastPathIndex = (items, rel) => {
|
|
79
|
+
let at = -1;
|
|
80
|
+
for (let i = 0; i < items.length; i++) {
|
|
81
|
+
if (listFilePath(itemPath(items[i])) === rel) at = i;
|
|
82
|
+
}
|
|
83
|
+
return at;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const insertAfterPath = (items, added, rel) => {
|
|
87
|
+
const at = lastPathIndex(items, rel);
|
|
88
|
+
if (at < 0) return [...items, ...added];
|
|
89
|
+
return [...items.slice(0, at + 1), ...added, ...items.slice(at + 1)];
|
|
90
|
+
};
|
|
91
|
+
|
|
49
92
|
const foldDepItems = (
|
|
50
93
|
items,
|
|
51
94
|
readSides = noSides,
|
|
@@ -58,6 +101,7 @@ const foldDepItems = (
|
|
|
58
101
|
if (!groups.size) return items;
|
|
59
102
|
const folded = new Set();
|
|
60
103
|
const synthetic = new Map();
|
|
104
|
+
const leftoverNames = new Map();
|
|
61
105
|
for (const key of groups.keys()) {
|
|
62
106
|
const group = groups.get(key);
|
|
63
107
|
const summary = summarizeGroup(group, readSides);
|
|
@@ -72,32 +116,42 @@ const foldDepItems = (
|
|
|
72
116
|
const audited = markAuditChanges(unused, audit);
|
|
73
117
|
const changes = markOutdatedChanges(audited, outdated);
|
|
74
118
|
const next = { ...summary, changes };
|
|
119
|
+
leftoverNames.set(key, foldedDepNames(next.changes));
|
|
75
120
|
const taken = takeManifestItems(next);
|
|
76
121
|
if (next.foldLock) {
|
|
77
122
|
for (const item of next.lockItems) taken.push(item);
|
|
78
123
|
}
|
|
79
124
|
if (!taken.length && !next.changes.length) continue;
|
|
80
125
|
const made = makeDepItems(group.origin, group.items, taken, next);
|
|
81
|
-
synthetic.set(key, made);
|
|
126
|
+
if (made.length) synthetic.set(key, made);
|
|
82
127
|
for (const item of taken) folded.add(item);
|
|
83
128
|
}
|
|
84
|
-
if (!synthetic.size) return items;
|
|
129
|
+
if (!synthetic.size && !folded.size) return items;
|
|
85
130
|
const out = [];
|
|
86
|
-
const
|
|
131
|
+
const slot = new Map();
|
|
87
132
|
for (const item of items) {
|
|
88
133
|
const rel = itemPath(item);
|
|
89
134
|
const meta = depFileMeta(rel);
|
|
90
135
|
if (meta) {
|
|
136
|
+
const pkgRel = relOf(meta.dir, MANIFEST);
|
|
91
137
|
const key = `${item.origin}\0${meta.dir}`;
|
|
92
|
-
if (synthetic.has(key) && !
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
138
|
+
if (synthetic.has(key) && !slot.has(pkgRel)) {
|
|
139
|
+
slot.set(pkgRel, out.length);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (folded.has(item)) continue;
|
|
143
|
+
if (meta && meta.kind === 'manifest') {
|
|
144
|
+
const names = leftoverNames.get(`${item.origin}\0${meta.dir}`);
|
|
145
|
+
const kept = names ? stripFoldedDepItem(item, names) : item;
|
|
146
|
+
if (kept) {
|
|
147
|
+
out.push(kept);
|
|
148
|
+
slot.set(relOf(meta.dir, MANIFEST), out.length);
|
|
96
149
|
}
|
|
150
|
+
continue;
|
|
97
151
|
}
|
|
98
|
-
|
|
152
|
+
out.push(item);
|
|
99
153
|
}
|
|
100
|
-
return out;
|
|
154
|
+
return spliceAtSlots(out, synthetic, slot);
|
|
101
155
|
};
|
|
102
156
|
|
|
103
157
|
const proposeDepItems = (pkgText, outdated, audit, extra = {}) => {
|
|
@@ -133,7 +187,8 @@ const mergeProposedItems = (items, pkgText, outdated, audit, extra = {}) => {
|
|
|
133
187
|
added.push(item);
|
|
134
188
|
}
|
|
135
189
|
if (!added.length) return items;
|
|
136
|
-
|
|
190
|
+
const rel = relOf(extra.dir ?? '.', MANIFEST);
|
|
191
|
+
return insertAfterPath(items, added, rel);
|
|
137
192
|
};
|
|
138
193
|
|
|
139
194
|
module.exports = {
|
package/lib/files.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { listFilePath } = require('./deps/manifest.js');
|
|
4
|
+
|
|
3
5
|
const REVIEW_DIR = '.review';
|
|
4
6
|
const ORIGIN_ORDER = ['staged', 'unstaged', 'untracked', 'commit', 'pr'];
|
|
5
7
|
const TODO_FILE = 'TODOs';
|
|
@@ -24,6 +26,8 @@ const itemPath = (item) => {
|
|
|
24
26
|
return item.file.newPath || item.file.oldPath || '';
|
|
25
27
|
};
|
|
26
28
|
|
|
29
|
+
const listPath = (item) => listFilePath(itemPath(item));
|
|
30
|
+
|
|
27
31
|
const isTodoItem = (item) => !!(item && item.origin === 'todo');
|
|
28
32
|
|
|
29
33
|
const isTodosEntry = (entry) => !!(entry && entry.kind === 'todos');
|
|
@@ -72,6 +76,25 @@ const lineDelta = ({ hunk, blockId }) => {
|
|
|
72
76
|
return delta;
|
|
73
77
|
};
|
|
74
78
|
|
|
79
|
+
const countAgo = (n, unit) => {
|
|
80
|
+
const label = n === 1 ? unit : `${unit}s`;
|
|
81
|
+
return `${n} ${label} ago`;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const relativeAge = (thenMs, nowMs = Date.now()) => {
|
|
85
|
+
const diff = Math.max(0, Math.round((nowMs - thenMs) / 1000));
|
|
86
|
+
if (diff < 90) return countAgo(diff, 'second');
|
|
87
|
+
const minutes = Math.round(diff / 60);
|
|
88
|
+
if (minutes < 90) return countAgo(minutes, 'minute');
|
|
89
|
+
const hours = Math.round(minutes / 60);
|
|
90
|
+
if (hours < 36) return countAgo(hours, 'hour');
|
|
91
|
+
const days = Math.round(hours / 24);
|
|
92
|
+
if (days < 14) return countAgo(days, 'day');
|
|
93
|
+
if (days < 70) return countAgo(Math.round(days / 7), 'week');
|
|
94
|
+
if (days < 365) return countAgo(Math.round(days / 30), 'month');
|
|
95
|
+
return countAgo(Math.round(days / 365), 'year');
|
|
96
|
+
};
|
|
97
|
+
|
|
75
98
|
const addLineDelta = (group, origin, delta) => {
|
|
76
99
|
group.added += delta.added;
|
|
77
100
|
group.removed += delta.removed;
|
|
@@ -116,7 +139,8 @@ const fileEntries = (items) => {
|
|
|
116
139
|
for (let i = 0; i < items.length; i++) {
|
|
117
140
|
const item = items[i];
|
|
118
141
|
if (isTodoItem(item)) continue;
|
|
119
|
-
const rel =
|
|
142
|
+
const rel = listPath(item);
|
|
143
|
+
if (!rel) continue;
|
|
120
144
|
if (!groups.has(rel)) {
|
|
121
145
|
groups.set(rel, {
|
|
122
146
|
path: rel,
|
|
@@ -132,10 +156,12 @@ const fileEntries = (items) => {
|
|
|
132
156
|
unstagedRemoved: 0,
|
|
133
157
|
staged: 0,
|
|
134
158
|
unstaged: 0,
|
|
159
|
+
date: '',
|
|
135
160
|
});
|
|
136
161
|
}
|
|
137
162
|
const group = groups.get(rel);
|
|
138
163
|
const { origin } = item;
|
|
164
|
+
if (item.date && !group.date) group.date = item.date;
|
|
139
165
|
group.origins.push(origin);
|
|
140
166
|
group.remaining += 1;
|
|
141
167
|
addLineDelta(group, origin, lineDelta(item));
|
|
@@ -149,6 +175,7 @@ const fileEntries = (items) => {
|
|
|
149
175
|
for (const group of entries) {
|
|
150
176
|
group.status = fileStatus(group.origins);
|
|
151
177
|
}
|
|
178
|
+
entries.sort((left, right) => left.path.localeCompare(right.path, 'en'));
|
|
152
179
|
return entries;
|
|
153
180
|
};
|
|
154
181
|
|
|
@@ -197,6 +224,7 @@ module.exports = {
|
|
|
197
224
|
TOTAL_LABEL,
|
|
198
225
|
isPathInScope,
|
|
199
226
|
itemPath,
|
|
227
|
+
listPath,
|
|
200
228
|
isTodoItem,
|
|
201
229
|
isTodosEntry,
|
|
202
230
|
isTotalEntry,
|
|
@@ -205,4 +233,5 @@ module.exports = {
|
|
|
205
233
|
fileStatus,
|
|
206
234
|
fileEntries,
|
|
207
235
|
fileTotals,
|
|
236
|
+
relativeAge,
|
|
208
237
|
};
|
package/lib/git/branches.js
CHANGED
|
@@ -26,6 +26,100 @@ const lastMessage = (top) => {
|
|
|
26
26
|
return (result.stdout ?? '').replace(/\s+$/, '');
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
+
const COMMIT_FORMAT = ['%H', '%h', '%an', '%ar', '%s'].join('%x1f');
|
|
30
|
+
const COMMIT_LIMIT = 256;
|
|
31
|
+
const UNIT = '\x1f';
|
|
32
|
+
|
|
33
|
+
const parseCommitLine = (line, index) => {
|
|
34
|
+
const parts = line.split(UNIT);
|
|
35
|
+
if (parts.length < 5) return null;
|
|
36
|
+
const sha = (parts[0] ?? '').trim();
|
|
37
|
+
if (!sha) return null;
|
|
38
|
+
return {
|
|
39
|
+
sha,
|
|
40
|
+
shortSha: (parts[1] ?? '').trim() || sha.slice(0, 7),
|
|
41
|
+
author: (parts[2] ?? '').trim(),
|
|
42
|
+
date: (parts[3] ?? '').trim(),
|
|
43
|
+
subject: parts.slice(4).join(UNIT).trim(),
|
|
44
|
+
head: index === 0,
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const listCommits = (top) => {
|
|
49
|
+
const args = [
|
|
50
|
+
'log',
|
|
51
|
+
`--format=${COMMIT_FORMAT}`,
|
|
52
|
+
`--max-count=${COMMIT_LIMIT}`,
|
|
53
|
+
];
|
|
54
|
+
const result = requireOk(runGit(args, top), 'log failed');
|
|
55
|
+
const commits = [];
|
|
56
|
+
for (const line of result.stdout.split('\n')) {
|
|
57
|
+
if (!line) continue;
|
|
58
|
+
const entry = parseCommitLine(line, commits.length);
|
|
59
|
+
if (entry) commits.push(entry);
|
|
60
|
+
}
|
|
61
|
+
return commits;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const namedCommit = (sha) => {
|
|
65
|
+
const commit = `${sha ?? ''}`.trim();
|
|
66
|
+
if (!commit) throw new Error('empty commit');
|
|
67
|
+
return commit;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const resolveCommit = (top, sha) => {
|
|
71
|
+
const result = requireOk(runGit(['rev-parse', sha], top), 'rev-parse failed');
|
|
72
|
+
return (result.stdout ?? '').trim();
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const dropHeadCommit = (top) => {
|
|
76
|
+
requireOk(runGit(['reset', '--soft', 'HEAD~1'], top), 'drop failed');
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const rebaseFailed = (result) => {
|
|
80
|
+
if (result.status === 0) return '';
|
|
81
|
+
return result.stderr || result.stdout || 'rebase failed';
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const dropOlderCommit = (top, sha) => {
|
|
85
|
+
const onto = `${sha}^`;
|
|
86
|
+
const result = runGit(['rebase', '--onto', onto, sha], top);
|
|
87
|
+
const failed = rebaseFailed(result);
|
|
88
|
+
if (!failed) return;
|
|
89
|
+
runGit(['rebase', '--abort'], top);
|
|
90
|
+
throw new Error(oneLine(failed));
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const dropOlderCommitAsync = async (top, sha) => {
|
|
94
|
+
const onto = `${sha}^`;
|
|
95
|
+
const result = await runGitAsync(['rebase', '--onto', onto, sha], top);
|
|
96
|
+
const failed = rebaseFailed(result);
|
|
97
|
+
if (!failed) return;
|
|
98
|
+
await runGitAsync(['rebase', '--abort'], top);
|
|
99
|
+
throw new Error(oneLine(failed));
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const dropCommit = (top, sha) => {
|
|
103
|
+
const spec = namedCommit(sha);
|
|
104
|
+
const target = resolveCommit(top, spec);
|
|
105
|
+
const parent = runGit(['rev-parse', '--verify', `${target}^`], top);
|
|
106
|
+
if (parent.status !== 0) throw new Error('cannot drop root commit');
|
|
107
|
+
const head = resolveCommit(top, 'HEAD');
|
|
108
|
+
if (target === head) return void dropHeadCommit(top);
|
|
109
|
+
dropOlderCommit(top, target);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const dropCommitAsync = async (top, sha) => {
|
|
113
|
+
const spec = namedCommit(sha);
|
|
114
|
+
const target = resolveCommit(top, spec);
|
|
115
|
+
const parentArgs = ['rev-parse', '--verify', `${target}^`];
|
|
116
|
+
const parent = await runGitAsync(parentArgs, top);
|
|
117
|
+
if (parent.status !== 0) throw new Error('cannot drop root commit');
|
|
118
|
+
const head = resolveCommit(top, 'HEAD');
|
|
119
|
+
if (target === head) return void dropHeadCommit(top);
|
|
120
|
+
await dropOlderCommitAsync(top, target);
|
|
121
|
+
};
|
|
122
|
+
|
|
29
123
|
const parseTrack = (text) => {
|
|
30
124
|
const raw = `${text ?? ''}`.trim().replace(/^\[|\]$/g, '');
|
|
31
125
|
if (!raw) return { ahead: 0, behind: 0, gone: false };
|
|
@@ -125,11 +219,6 @@ const createBranchAsync = async (top, name) => {
|
|
|
125
219
|
requireOk(await runGitAsync(args, top), 'checkout failed');
|
|
126
220
|
};
|
|
127
221
|
|
|
128
|
-
const rebaseFailed = (result) => {
|
|
129
|
-
if (result.status === 0) return '';
|
|
130
|
-
return result.stderr || result.stdout || 'rebase failed';
|
|
131
|
-
};
|
|
132
|
-
|
|
133
222
|
const rebaseBranch = (top, onto) => {
|
|
134
223
|
const branch = namedBranch(onto);
|
|
135
224
|
const result = runGit(['rebase', branch], top);
|
|
@@ -272,6 +361,7 @@ const commitChangesAsync = async (top, kind, message) => {
|
|
|
272
361
|
|
|
273
362
|
module.exports = {
|
|
274
363
|
lastMessage,
|
|
364
|
+
listCommits,
|
|
275
365
|
listBranches,
|
|
276
366
|
checkoutBranch,
|
|
277
367
|
checkoutBranchAsync,
|
|
@@ -279,6 +369,8 @@ module.exports = {
|
|
|
279
369
|
createBranchAsync,
|
|
280
370
|
rebaseBranch,
|
|
281
371
|
rebaseBranchAsync,
|
|
372
|
+
dropCommit,
|
|
373
|
+
dropCommitAsync,
|
|
282
374
|
dropBranch,
|
|
283
375
|
dropBranchAsync,
|
|
284
376
|
pullChanges,
|