reslop 0.1.6 → 0.1.8
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 +29 -1
- package/README.md +33 -19
- package/bin/reslop.js +3 -1
- package/lib/ansi.js +1 -0
- package/lib/capabilities.js +106 -0
- package/lib/cli.js +18 -14
- package/lib/deps/changes.js +331 -0
- package/lib/deps/items.js +131 -0
- package/lib/deps/manifest.js +145 -0
- package/lib/deps/merge.js +120 -0
- package/lib/deps/proposals.js +164 -0
- package/lib/deps/reports.js +180 -0
- package/lib/deps/rows.js +177 -0
- package/lib/deps/scan.js +138 -0
- package/lib/deps/usage.js +68 -0
- package/lib/deps/versions.js +119 -0
- package/lib/deps.js +65 -1348
- package/lib/diff/align.js +226 -0
- package/lib/diff/display.js +101 -0
- package/lib/diff/edit.js +47 -0
- package/lib/diff/inline.js +300 -0
- package/lib/diff/items.js +101 -0
- package/lib/diff/parse.js +156 -0
- package/lib/diff/patch.js +103 -0
- package/lib/diff.js +13 -932
- package/lib/editor.js +37 -0
- package/lib/files.js +121 -7
- package/lib/git/branches.js +291 -0
- package/lib/git/changes.js +164 -0
- package/lib/git/dependencies.js +489 -0
- package/lib/git/proc.js +162 -0
- package/lib/git/read.js +253 -0
- package/lib/git/run.js +100 -0
- package/lib/git.js +82 -1152
- package/lib/github.js +92 -222
- package/lib/gitlab.js +72 -216
- package/lib/highlight/bash.js +5 -9
- package/lib/highlight/core.js +1 -1
- package/lib/highlight/css.js +1 -3
- package/lib/highlight/csv.js +1 -3
- package/lib/highlight/dart.js +6 -20
- package/lib/highlight/dot.js +13 -13
- package/lib/highlight/html.js +1 -5
- package/lib/highlight/js.js +151 -115
- package/lib/highlight/json.js +1 -3
- package/lib/highlight/log.js +1 -3
- package/lib/highlight/txt.js +5 -10
- package/lib/highlight.js +4 -4
- package/lib/keys.js +63 -142
- package/lib/npm/commands.js +100 -0
- package/lib/remote/changes.js +37 -0
- package/lib/remote/headers.js +36 -0
- package/lib/remote/http.js +122 -0
- package/lib/remote/notes.js +94 -0
- package/lib/render/branches.js +218 -0
- package/lib/render/chrome.js +575 -0
- package/lib/render/diff.js +307 -0
- package/lib/render/files.js +166 -0
- package/lib/render/frame.js +197 -0
- package/lib/render/notes.js +201 -0
- package/lib/render/present.js +25 -0
- package/lib/render/primitives.js +200 -0
- package/lib/render.js +14 -1565
- package/lib/review/format.js +434 -0
- package/lib/review/storage.js +150 -0
- package/lib/review/store.js +178 -0
- package/lib/review/templates.js +49 -0
- package/lib/review.js +14 -804
- package/lib/select.js +3 -5
- package/lib/session/accessors.js +25 -0
- package/lib/session/actions.js +275 -0
- package/lib/session/branches.js +304 -0
- package/lib/session/change-actions.js +320 -0
- package/lib/session/commit.js +124 -0
- package/lib/session/compose.js +540 -0
- package/lib/session/dispatch.js +105 -0
- package/lib/session/files-pane.js +234 -0
- package/lib/session/items.js +177 -0
- package/lib/session/lifecycle.js +476 -0
- package/lib/session/load.js +85 -0
- package/lib/session/navigation.js +93 -0
- package/lib/session/ops.js +110 -0
- package/lib/session/origins.js +7 -0
- package/lib/session/pointer.js +154 -0
- package/lib/session/progress.js +29 -0
- package/lib/session/review.js +88 -0
- package/lib/session/templates.js +64 -0
- package/lib/session/terminal.js +169 -0
- package/lib/session/todos.js +269 -0
- package/lib/session/update.js +125 -0
- package/lib/session/view.js +129 -0
- package/lib/session/watch.js +219 -0
- package/lib/session.js +579 -2213
- package/lib/source.js +11 -11
- package/lib/sys.js +25 -4
- package/lib/wrap.js +4 -2
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased][unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.1.8][] - 2026-09-17
|
|
6
|
+
|
|
7
|
+
- Simplify session controllers, input dispatch, and loading orchestration
|
|
8
|
+
- Enforce structural limits and session dependency direction on library files
|
|
9
|
+
- Show staged/unstaged +/- and staged/remaining totals in status line
|
|
10
|
+
- Jump by word with Ctrl+Left/Right in all text editors
|
|
11
|
+
- Support PgUp, PgDn, Home, and End in lists and multiline editors
|
|
12
|
+
- Support mouse click in submenu and confirmation choices on the hint line
|
|
13
|
+
- Fix TODO wrapped text; keep up/down navigation inside a multiline todos
|
|
14
|
+
- Fix identifier diffs as whole words compare, not just letters
|
|
15
|
+
- Keep reslop open when a local repo has no unstaged changes
|
|
16
|
+
- Fix invisible cursor movement in scrolling diffs
|
|
17
|
+
- Auto-reload worktree on disk changes, keeping the current UI state
|
|
18
|
+
- Fix disk watch paths on Windows
|
|
19
|
+
- Set git push upstream to `origin <branch>` when missing
|
|
20
|
+
- Save in-place diff edits to the reviewed file
|
|
21
|
+
|
|
22
|
+
## [0.1.7][] - 2026-09-16
|
|
23
|
+
|
|
24
|
+
- Make TODOs repo-wide, not per-file todos
|
|
25
|
+
- Improve TODO screen and files screen TUI/TUX including hotkeys
|
|
26
|
+
- Implement branch rebase and drop
|
|
27
|
+
- Improve branch screen TUI/TUX
|
|
28
|
+
- Fix TODOs autosave bug with todo duplication
|
|
29
|
+
|
|
5
30
|
## [0.1.6][] - 2026-09-15
|
|
6
31
|
|
|
7
32
|
- Auto updates for patch/minor, y/n per new major
|
|
@@ -58,7 +83,10 @@
|
|
|
58
83
|
- Feedback and todos that produce a repair plan for an agent
|
|
59
84
|
- Intra-line highlighting in unified, mixed, and side-by-side layouts
|
|
60
85
|
|
|
61
|
-
[unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.1.
|
|
86
|
+
[unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.1.8...HEAD
|
|
87
|
+
[0.1.8]: https://github.com/tshemsedinov/reslop/compare/v0.1.7...v0.1.8
|
|
88
|
+
[0.1.7]: https://github.com/tshemsedinov/reslop/compare/v0.1.6...v0.1.7
|
|
89
|
+
[0.1.6]: https://github.com/tshemsedinov/reslop/compare/v0.1.5...v0.1.6
|
|
62
90
|
[0.1.5]: https://github.com/tshemsedinov/reslop/compare/v0.1.4...v0.1.5
|
|
63
91
|
[0.1.4]: https://github.com/tshemsedinov/reslop/compare/v0.1.3...v0.1.4
|
|
64
92
|
[0.1.3]: https://github.com/tshemsedinov/reslop/compare/v0.1.2...v0.1.3
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# 👁️ reslop — Review
|
|
1
|
+
# 👁️ reslop — Review AI generated code and plan changes
|
|
2
2
|
|
|
3
3
|
[](https://github.com/tshemsedinov/reslop/actions?query=workflow%3A%22Testing+CI%22+branch%3Amain)
|
|
4
4
|
[](https://snyk.io/test/github/tshemsedinov/reslop)
|
|
@@ -16,12 +16,16 @@ for the agent.
|
|
|
16
16
|
Review → Plan → Repair → Verify
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
- Leave feedback, issues/todos, and in-place code proposals as a repair plan.
|
|
19
20
|
- Review uncommitted git diffs, a given commit, a GitHub PR, or a GitLab MR.
|
|
20
21
|
- Stage, unstage, or revert each contiguous block of diff lines.
|
|
21
|
-
-
|
|
22
|
+
- Auto-reload local diffs when files change, keeping the current screen.
|
|
22
23
|
- Import GitHub PR and GitLab MR review comments into the local plan for AI.
|
|
24
|
+
- Commit, amend, or fixup staged changes from the file list.
|
|
25
|
+
- Branches: checkout, create, rebase, drop, pull and push.
|
|
23
26
|
- Review each npm dependency once across `package.json` and the lockfile.
|
|
24
27
|
- Propose unused removals, npm audit fixes, and outdated updates as diffs.
|
|
28
|
+
- Code highlighting, diff compare: unified, mixed, and side-by-side layouts.
|
|
25
29
|
|
|
26
30
|
## Install
|
|
27
31
|
|
|
@@ -49,23 +53,33 @@ Reviews go in `.review/YYYY-MM-DD-NN.md` with frontmatter `status`:
|
|
|
49
53
|
|
|
50
54
|
## Hotkeys
|
|
51
55
|
|
|
52
|
-
| Key | Action
|
|
53
|
-
| ------------------- |
|
|
54
|
-
| `a` / `u` / `d` | Stage / unstage / drop this diff
|
|
55
|
-
| `c` |
|
|
56
|
-
| `b` | List branches; `
|
|
57
|
-
| `p` / `s` | git pull/push
|
|
58
|
-
| `f` / `t` / `e` |
|
|
59
|
-
| `
|
|
60
|
-
|
|
|
61
|
-
|
|
|
62
|
-
|
|
|
63
|
-
|
|
|
64
|
-
| `Ctrl-
|
|
65
|
-
| `Ctrl-
|
|
66
|
-
|
|
|
67
|
-
|
|
|
68
|
-
|
|
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
|
+
## Future
|
|
74
|
+
|
|
75
|
+
- Import issues from GitHub and GitLab.
|
|
76
|
+
- Security and code-quality audit; propose a repair plan.
|
|
77
|
+
- Send anonymized code blocks for expert review.
|
|
78
|
+
- Send questions and the repair plan to experts for approval.
|
|
79
|
+
- Ask experts.
|
|
80
|
+
- Verify the codebase.
|
|
81
|
+
- Apply refactoring skills.
|
|
82
|
+
- Call agents, harnesses, and IDEs to execute prepared plans.
|
|
69
83
|
|
|
70
84
|
## License
|
|
71
85
|
|
package/bin/reslop.js
CHANGED
package/lib/ansi.js
CHANGED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
|
|
5
|
+
const READ_METHODS = [
|
|
6
|
+
'load',
|
|
7
|
+
'loadAsync',
|
|
8
|
+
'loadExtras',
|
|
9
|
+
'toplevel',
|
|
10
|
+
'resolveRev',
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const CHANGE_METHODS = [
|
|
14
|
+
'add',
|
|
15
|
+
'addAsync',
|
|
16
|
+
'unstage',
|
|
17
|
+
'revert',
|
|
18
|
+
'revertFile',
|
|
19
|
+
'edit',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const BRANCH_METHODS = [
|
|
23
|
+
'commit',
|
|
24
|
+
'commitAsync',
|
|
25
|
+
'hasStaged',
|
|
26
|
+
'lastMessage',
|
|
27
|
+
'listBranches',
|
|
28
|
+
'currentBranch',
|
|
29
|
+
'checkout',
|
|
30
|
+
'checkoutAsync',
|
|
31
|
+
'createBranch',
|
|
32
|
+
'createBranchAsync',
|
|
33
|
+
'rebase',
|
|
34
|
+
'rebaseAsync',
|
|
35
|
+
'drop',
|
|
36
|
+
'dropAsync',
|
|
37
|
+
'pull',
|
|
38
|
+
'pullAsync',
|
|
39
|
+
'push',
|
|
40
|
+
'pushAsync',
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const SOURCE_CAPS = {
|
|
44
|
+
local: { read: true, changes: true, branches: true, review: true },
|
|
45
|
+
commit: { read: true, changes: false, branches: false, review: true },
|
|
46
|
+
'github-pr': { read: true, changes: false, branches: false, review: true },
|
|
47
|
+
'gitlab-mr': { read: true, changes: false, branches: false, review: true },
|
|
48
|
+
'read-only': { read: true, changes: false, branches: false, review: true },
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const withReadOnly = (caps, readOnly) => {
|
|
52
|
+
if (!readOnly) return { ...caps };
|
|
53
|
+
return { ...caps, changes: false, branches: false };
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const capabilitiesFor = (kind, extra = {}) => {
|
|
57
|
+
const base = SOURCE_CAPS[kind] ?? SOURCE_CAPS.local;
|
|
58
|
+
return withReadOnly(base, extra.readOnly === true);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const copyMethods = (target, source, names) => {
|
|
62
|
+
for (const name of names) {
|
|
63
|
+
const method = source[name];
|
|
64
|
+
if (typeof method === 'function') target[name] = method;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const attachCapabilities = (impl, caps) => {
|
|
69
|
+
const source = { capabilities: { ...caps } };
|
|
70
|
+
if (caps.read) copyMethods(source, impl, READ_METHODS);
|
|
71
|
+
if (caps.changes) copyMethods(source, impl, CHANGE_METHODS);
|
|
72
|
+
if (caps.branches) copyMethods(source, impl, BRANCH_METHODS);
|
|
73
|
+
return source;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const sessionCapabilities = (options) => {
|
|
77
|
+
const readOnly = options.readOnly === true;
|
|
78
|
+
if (options.capabilities) return withReadOnly(options.capabilities, readOnly);
|
|
79
|
+
if (options.rev) return capabilitiesFor('commit', { readOnly });
|
|
80
|
+
const source = options.change && options.change.source;
|
|
81
|
+
if (source && SOURCE_CAPS[source]) {
|
|
82
|
+
return capabilitiesFor(source, { readOnly });
|
|
83
|
+
}
|
|
84
|
+
const repoCaps = options.repo && options.repo.capabilities;
|
|
85
|
+
if (repoCaps) return withReadOnly(repoCaps, readOnly);
|
|
86
|
+
return capabilitiesFor(readOnly ? 'read-only' : 'local');
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const createReviewStorage = (io = {}) => ({
|
|
90
|
+
readdirSync: io.readdirSync ?? fs.readdirSync,
|
|
91
|
+
readFileSync: io.readFileSync ?? fs.readFileSync,
|
|
92
|
+
writeFileSync: io.writeFileSync ?? fs.writeFileSync,
|
|
93
|
+
mkdirSync: io.mkdirSync ?? fs.mkdirSync,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
module.exports = {
|
|
97
|
+
READ_METHODS,
|
|
98
|
+
CHANGE_METHODS,
|
|
99
|
+
BRANCH_METHODS,
|
|
100
|
+
SOURCE_CAPS,
|
|
101
|
+
capabilitiesFor,
|
|
102
|
+
withReadOnly,
|
|
103
|
+
attachCapabilities,
|
|
104
|
+
sessionCapabilities,
|
|
105
|
+
createReviewStorage,
|
|
106
|
+
};
|
package/lib/cli.js
CHANGED
|
@@ -8,6 +8,7 @@ const { githubToken, loadPullRequest } = require('./github.js');
|
|
|
8
8
|
const { gitlabToken, loadMergeRequest } = require('./gitlab.js');
|
|
9
9
|
const { selectChangeSource, createLoadedSource } = require('./source.js');
|
|
10
10
|
const { Session } = require('./session.js');
|
|
11
|
+
const { capabilitiesFor, attachCapabilities } = require('./capabilities.js');
|
|
11
12
|
|
|
12
13
|
const HEX_REV = /^[0-9a-f]{4,40}$/i;
|
|
13
14
|
const HEAD_REV = /^HEAD([~^].*)?$/i;
|
|
@@ -121,22 +122,20 @@ const sessionOptions = (proc, deps, args, env, extra) => ({
|
|
|
121
122
|
...extra,
|
|
122
123
|
});
|
|
123
124
|
|
|
124
|
-
const startSession = (options, extra = {}) => {
|
|
125
|
-
const session = new Session(options);
|
|
126
|
-
if (extra.load) session.load();
|
|
127
|
-
return session;
|
|
128
|
-
};
|
|
129
|
-
|
|
130
125
|
const openLocalSession = (proc, deps, args, selected, env) => {
|
|
131
126
|
const cwd = deps.cwd ?? proc.cwd();
|
|
132
|
-
const
|
|
127
|
+
const rawRepo = deps.repo ?? createGitRepo();
|
|
133
128
|
const statSync = deps.statSync ?? fs.statSync;
|
|
134
|
-
const scope = resolveScope(cwd, selected.paths,
|
|
135
|
-
const
|
|
129
|
+
const scope = resolveScope(cwd, selected.paths, rawRepo, statSync);
|
|
130
|
+
const kind = scope.rev ? 'commit' : 'local';
|
|
131
|
+
const capabilities = capabilitiesFor(kind, { readOnly: args.readOnly });
|
|
132
|
+
const repo = attachCapabilities(rawRepo, capabilities);
|
|
133
|
+
const session = new Session(
|
|
136
134
|
sessionOptions(proc, deps, args, env, {
|
|
137
135
|
repo,
|
|
138
136
|
paths: scope.paths,
|
|
139
137
|
rev: scope.rev,
|
|
138
|
+
capabilities,
|
|
140
139
|
}),
|
|
141
140
|
);
|
|
142
141
|
session.ensureRepo();
|
|
@@ -148,27 +147,32 @@ const remoteRepo = (deps, selected, env, cwd) => {
|
|
|
148
147
|
const remote = REMOTE_CHANGE[selected.kind];
|
|
149
148
|
const loader = deps[remote.load] ?? remote.fallback;
|
|
150
149
|
const token = remote.token(env);
|
|
151
|
-
const loadRemote = async () => {
|
|
150
|
+
const loadRemote = async (_cwd, _paths, options = {}) => {
|
|
152
151
|
const loaded = await loader(remote.target(selected), {
|
|
153
152
|
cwd,
|
|
154
153
|
paths: selected.paths,
|
|
155
154
|
token,
|
|
156
155
|
fetch: deps.fetch,
|
|
156
|
+
signal: options.signal,
|
|
157
157
|
});
|
|
158
158
|
const items = loaded.items ?? [];
|
|
159
159
|
return { ...loaded, items: [...items] };
|
|
160
160
|
};
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
161
|
+
return createLoadedSource({ items: [] }, selected.kind, {
|
|
162
|
+
loadAsync: loadRemote,
|
|
163
|
+
});
|
|
164
164
|
};
|
|
165
165
|
|
|
166
166
|
const openRemoteSession = (proc, deps, args, selected, env) => {
|
|
167
167
|
const cwd = deps.cwd ?? proc.cwd();
|
|
168
|
-
|
|
168
|
+
const capabilities = capabilitiesFor(selected.kind, {
|
|
169
|
+
readOnly: args.readOnly,
|
|
170
|
+
});
|
|
171
|
+
return new Session(
|
|
169
172
|
sessionOptions(proc, deps, args, env, {
|
|
170
173
|
repo: remoteRepo(deps, selected, env, cwd),
|
|
171
174
|
paths: selected.paths,
|
|
175
|
+
capabilities,
|
|
172
176
|
}),
|
|
173
177
|
);
|
|
174
178
|
};
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { itemPath } = require('../files.js');
|
|
4
|
+
|
|
5
|
+
const manifest = require('./manifest.js');
|
|
6
|
+
const { MANIFEST, LOCKFILE, SECTION_KEYS, depFileMeta, relOf } = manifest;
|
|
7
|
+
const { parseJson, readSections, emptySectionMaps } = manifest;
|
|
8
|
+
const { lockRootSections, lockEntries, lockPackageCount } = manifest;
|
|
9
|
+
|
|
10
|
+
const SECTION_ORDER = [...SECTION_KEYS, 'resolved', 'field'];
|
|
11
|
+
const ACTION_ORDER = ['added', 'removed', 'changed', 'vulnerable'];
|
|
12
|
+
const SECTION_NAMES = SECTION_KEYS.join('|');
|
|
13
|
+
const SECTION_OPEN = new RegExp(`^\\s*"(${SECTION_NAMES})"\\s*:\\s*\\{\\s*$`);
|
|
14
|
+
const SECTION_END = /^\s*\},?\s*$/;
|
|
15
|
+
const DEP_ENTRY = /^\s*"([^"]+)"\s*:\s*"([^"]*)"\s*,?\s*$/;
|
|
16
|
+
|
|
17
|
+
const diffMaps = (oldMap, newMap, section) => {
|
|
18
|
+
const keys = [...oldMap.keys(), ...newMap.keys()];
|
|
19
|
+
const names = [...new Set(keys)];
|
|
20
|
+
names.sort();
|
|
21
|
+
const changes = [];
|
|
22
|
+
for (const name of names) {
|
|
23
|
+
const from = oldMap.has(name) ? oldMap.get(name) : '';
|
|
24
|
+
const to = newMap.has(name) ? newMap.get(name) : '';
|
|
25
|
+
if (from === to) continue;
|
|
26
|
+
let action = 'changed';
|
|
27
|
+
if (!oldMap.has(name)) action = 'added';
|
|
28
|
+
else if (!newMap.has(name)) action = 'removed';
|
|
29
|
+
changes.push({ name, section, action, from, to });
|
|
30
|
+
}
|
|
31
|
+
return changes;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const diffSections = (oldMaps, newMaps) => {
|
|
35
|
+
const changes = [];
|
|
36
|
+
for (const key of SECTION_KEYS) {
|
|
37
|
+
const found = diffMaps(oldMaps[key], newMaps[key], key);
|
|
38
|
+
for (const change of found) changes.push(change);
|
|
39
|
+
}
|
|
40
|
+
return changes;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const fieldText = (value) => {
|
|
44
|
+
if (value === undefined) return '';
|
|
45
|
+
if (typeof value === 'string') return value;
|
|
46
|
+
return JSON.stringify(value);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const sameJson = (left, right) =>
|
|
50
|
+
JSON.stringify(left) === JSON.stringify(right);
|
|
51
|
+
|
|
52
|
+
const diffOtherFields = (oldPkg, newPkg) => {
|
|
53
|
+
if (!oldPkg && !newPkg) return [];
|
|
54
|
+
const oldObj = oldPkg ?? {};
|
|
55
|
+
const newObj = newPkg ?? {};
|
|
56
|
+
const keys = [...Object.keys(oldObj), ...Object.keys(newObj)];
|
|
57
|
+
const names = [...new Set(keys)];
|
|
58
|
+
names.sort();
|
|
59
|
+
const changes = [];
|
|
60
|
+
for (const name of names) {
|
|
61
|
+
if (SECTION_KEYS.includes(name)) continue;
|
|
62
|
+
const prev = oldObj[name];
|
|
63
|
+
const next = newObj[name];
|
|
64
|
+
if (sameJson(prev, next)) continue;
|
|
65
|
+
let action = 'changed';
|
|
66
|
+
if (!Object.hasOwn(oldObj, name)) action = 'added';
|
|
67
|
+
else if (!Object.hasOwn(newObj, name)) action = 'removed';
|
|
68
|
+
const from = fieldText(prev);
|
|
69
|
+
const to = fieldText(next);
|
|
70
|
+
const section = 'field';
|
|
71
|
+
changes.push({ name, section, action, from, to });
|
|
72
|
+
}
|
|
73
|
+
return changes;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const onlyDepEdits = (items) => {
|
|
77
|
+
if (!items.length) return false;
|
|
78
|
+
for (const item of items) {
|
|
79
|
+
if (!item.hunk) return false;
|
|
80
|
+
const blockId = item.blockId;
|
|
81
|
+
const scoped = typeof blockId === 'number';
|
|
82
|
+
let section = '';
|
|
83
|
+
for (const line of item.hunk.lines) {
|
|
84
|
+
const open = SECTION_OPEN.exec(line.text);
|
|
85
|
+
let nextSection = section;
|
|
86
|
+
if (open) nextSection = open[1];
|
|
87
|
+
else if (section && SECTION_END.test(line.text)) nextSection = '';
|
|
88
|
+
const inBlock = !scoped || line.blockId === blockId;
|
|
89
|
+
if (inBlock && line.type !== 'ctx') {
|
|
90
|
+
const inDep = !!(open || section);
|
|
91
|
+
if (!inDep) return false;
|
|
92
|
+
const isEntry = DEP_ENTRY.test(line.text);
|
|
93
|
+
const isClose = SECTION_END.test(line.text);
|
|
94
|
+
if (!open && !isEntry && !isClose) return false;
|
|
95
|
+
}
|
|
96
|
+
section = nextSection;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const mapsFromHunks = (items) => {
|
|
103
|
+
const oldMaps = emptySectionMaps();
|
|
104
|
+
const newMaps = emptySectionMaps();
|
|
105
|
+
let section = '';
|
|
106
|
+
for (const item of items) {
|
|
107
|
+
if (!item.hunk) continue;
|
|
108
|
+
for (const line of item.hunk.lines) {
|
|
109
|
+
const open = SECTION_OPEN.exec(line.text);
|
|
110
|
+
if (open) {
|
|
111
|
+
section = open[1];
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (section && SECTION_END.test(line.text)) {
|
|
115
|
+
section = '';
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (!section) continue;
|
|
119
|
+
const entry = DEP_ENTRY.exec(line.text);
|
|
120
|
+
if (!entry) continue;
|
|
121
|
+
const name = entry[1];
|
|
122
|
+
const version = entry[2];
|
|
123
|
+
if (line.type === 'del' || line.type === 'ctx') {
|
|
124
|
+
oldMaps[section].set(name, version);
|
|
125
|
+
}
|
|
126
|
+
if (line.type === 'add' || line.type === 'ctx') {
|
|
127
|
+
newMaps[section].set(name, version);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return { oldMaps, newMaps };
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const directNames = (oldMaps, newMaps) => {
|
|
135
|
+
const names = new Set();
|
|
136
|
+
for (const key of SECTION_KEYS) {
|
|
137
|
+
for (const name of oldMaps[key].keys()) names.add(name);
|
|
138
|
+
for (const name of newMaps[key].keys()) names.add(name);
|
|
139
|
+
}
|
|
140
|
+
return names;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const mergeResolved = (changes, oldResolved, newResolved, oldMaps, newMaps) => {
|
|
144
|
+
const seen = new Set();
|
|
145
|
+
const merged = [];
|
|
146
|
+
for (const change of changes) {
|
|
147
|
+
seen.add(change.name);
|
|
148
|
+
const resolvedFrom = oldResolved.get(change.name) ?? '';
|
|
149
|
+
const resolvedTo = newResolved.get(change.name) ?? '';
|
|
150
|
+
merged.push({ ...change, resolvedFrom, resolvedTo });
|
|
151
|
+
}
|
|
152
|
+
const names = [...directNames(oldMaps, newMaps)];
|
|
153
|
+
names.sort();
|
|
154
|
+
for (const name of names) {
|
|
155
|
+
if (seen.has(name)) continue;
|
|
156
|
+
const from = oldResolved.get(name) ?? '';
|
|
157
|
+
const to = newResolved.get(name) ?? '';
|
|
158
|
+
if (from === to) continue;
|
|
159
|
+
if (!from && !to) continue;
|
|
160
|
+
let action = 'changed';
|
|
161
|
+
if (!from) action = 'added';
|
|
162
|
+
else if (!to) action = 'removed';
|
|
163
|
+
const section = 'resolved';
|
|
164
|
+
const resolvedFrom = from;
|
|
165
|
+
const resolvedTo = to;
|
|
166
|
+
merged.push({ name, section, action, from, to, resolvedFrom, resolvedTo });
|
|
167
|
+
}
|
|
168
|
+
return merged;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const compareChangeOrder = (left, right) => {
|
|
172
|
+
const section = SECTION_ORDER.indexOf(left.section);
|
|
173
|
+
const other = SECTION_ORDER.indexOf(right.section);
|
|
174
|
+
if (section !== other) return section - other;
|
|
175
|
+
const leftAction = ACTION_ORDER.indexOf(left.action);
|
|
176
|
+
const rightAction = ACTION_ORDER.indexOf(right.action);
|
|
177
|
+
if (leftAction !== rightAction) return leftAction - rightAction;
|
|
178
|
+
return left.name.localeCompare(right.name);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const orderedChanges = (changes) => [...changes].sort(compareChangeOrder);
|
|
182
|
+
|
|
183
|
+
const kindItems = (items, kind) => {
|
|
184
|
+
const matched = [];
|
|
185
|
+
for (const item of items) {
|
|
186
|
+
const rel = itemPath(item);
|
|
187
|
+
const meta = depFileMeta(rel);
|
|
188
|
+
if (meta && meta.kind === kind) matched.push(item);
|
|
189
|
+
}
|
|
190
|
+
return matched;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const readGroupSnapshot = (group, readSides) => {
|
|
194
|
+
const origin = group.origin;
|
|
195
|
+
const dir = group.dir;
|
|
196
|
+
const groupItems = group.items;
|
|
197
|
+
const manifestItems = kindItems(groupItems, 'manifest');
|
|
198
|
+
const lockItems = kindItems(groupItems, 'lockfile');
|
|
199
|
+
const manifestRel = relOf(dir, MANIFEST);
|
|
200
|
+
const lockRel = relOf(dir, LOCKFILE);
|
|
201
|
+
const manifestSides = readSides(origin, manifestRel);
|
|
202
|
+
const lockSides = readSides(origin, lockRel);
|
|
203
|
+
const oldPkg = parseJson(manifestSides.oldText);
|
|
204
|
+
const newPkg = parseJson(manifestSides.newText);
|
|
205
|
+
const oldLock = parseJson(lockSides.oldText);
|
|
206
|
+
const newLock = parseJson(lockSides.newText);
|
|
207
|
+
const parsedManifest = !!(oldPkg || newPkg);
|
|
208
|
+
return {
|
|
209
|
+
origin,
|
|
210
|
+
dir,
|
|
211
|
+
groupItems,
|
|
212
|
+
manifestItems,
|
|
213
|
+
lockItems,
|
|
214
|
+
oldPkg,
|
|
215
|
+
newPkg,
|
|
216
|
+
oldLock,
|
|
217
|
+
newLock,
|
|
218
|
+
parsedManifest,
|
|
219
|
+
};
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const sectionMapsOf = (snapshot) => {
|
|
223
|
+
if (snapshot.parsedManifest) {
|
|
224
|
+
const oldMaps = readSections(snapshot.oldPkg);
|
|
225
|
+
const newMaps = readSections(snapshot.newPkg);
|
|
226
|
+
return { oldMaps, newMaps };
|
|
227
|
+
}
|
|
228
|
+
if (snapshot.manifestItems.length) {
|
|
229
|
+
return mapsFromHunks(snapshot.manifestItems);
|
|
230
|
+
}
|
|
231
|
+
const oldMaps = lockRootSections(snapshot.oldLock);
|
|
232
|
+
const newMaps = lockRootSections(snapshot.newLock);
|
|
233
|
+
return { oldMaps, newMaps };
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const comparedChanges = (snapshot) => {
|
|
237
|
+
const maps = sectionMapsOf(snapshot);
|
|
238
|
+
const oldMaps = maps.oldMaps;
|
|
239
|
+
const newMaps = maps.newMaps;
|
|
240
|
+
const manifestChanges = diffSections(oldMaps, newMaps);
|
|
241
|
+
const parsed = snapshot.parsedManifest;
|
|
242
|
+
let otherChanges = [];
|
|
243
|
+
if (parsed) {
|
|
244
|
+
otherChanges = diffOtherFields(snapshot.oldPkg, snapshot.newPkg);
|
|
245
|
+
}
|
|
246
|
+
const oldResolved = lockEntries(snapshot.oldLock);
|
|
247
|
+
const newResolved = lockEntries(snapshot.newLock);
|
|
248
|
+
const changes = mergeResolved(
|
|
249
|
+
manifestChanges,
|
|
250
|
+
oldResolved,
|
|
251
|
+
newResolved,
|
|
252
|
+
oldMaps,
|
|
253
|
+
newMaps,
|
|
254
|
+
);
|
|
255
|
+
const oldCount = lockPackageCount(snapshot.oldLock);
|
|
256
|
+
const newCount = lockPackageCount(snapshot.newLock);
|
|
257
|
+
return { changes, otherChanges, oldCount, newCount };
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const foldingPolicy = (snapshot, compared) => {
|
|
261
|
+
const foldLock = snapshot.lockItems.length > 0;
|
|
262
|
+
const hasDepChanges = compared.changes.length > 0;
|
|
263
|
+
if (!snapshot.manifestItems.length) {
|
|
264
|
+
const foldManifest = false;
|
|
265
|
+
const takeAllManifest = false;
|
|
266
|
+
return { foldLock, foldManifest, takeAllManifest };
|
|
267
|
+
}
|
|
268
|
+
if (snapshot.parsedManifest) {
|
|
269
|
+
const foldManifest = hasDepChanges;
|
|
270
|
+
const takeAllManifest = hasDepChanges && compared.otherChanges.length === 0;
|
|
271
|
+
return { foldLock, foldManifest, takeAllManifest };
|
|
272
|
+
}
|
|
273
|
+
const foldManifest = hasDepChanges && onlyDepEdits(snapshot.manifestItems);
|
|
274
|
+
const takeAllManifest = foldManifest;
|
|
275
|
+
return { foldLock, foldManifest, takeAllManifest };
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const summarizeGroup = (group, readSides) => {
|
|
279
|
+
const snapshot = readGroupSnapshot(group, readSides);
|
|
280
|
+
const compared = comparedChanges(snapshot);
|
|
281
|
+
const policy = foldingPolicy(snapshot, compared);
|
|
282
|
+
return {
|
|
283
|
+
changes: compared.changes,
|
|
284
|
+
oldCount: compared.oldCount,
|
|
285
|
+
newCount: compared.newCount,
|
|
286
|
+
foldLock: policy.foldLock,
|
|
287
|
+
foldManifest: policy.foldManifest,
|
|
288
|
+
takeAllManifest: policy.takeAllManifest,
|
|
289
|
+
manifestItems: snapshot.manifestItems,
|
|
290
|
+
lockItems: snapshot.lockItems,
|
|
291
|
+
pkg: snapshot.newPkg || snapshot.oldPkg,
|
|
292
|
+
};
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const collectGroups = (items) => {
|
|
296
|
+
const groups = new Map();
|
|
297
|
+
for (const item of items) {
|
|
298
|
+
const rel = itemPath(item);
|
|
299
|
+
const meta = depFileMeta(rel);
|
|
300
|
+
if (!meta) continue;
|
|
301
|
+
const key = `${item.origin}\0${meta.dir}`;
|
|
302
|
+
let group = groups.get(key);
|
|
303
|
+
if (!group) {
|
|
304
|
+
const dir = meta.dir;
|
|
305
|
+
const origin = item.origin;
|
|
306
|
+
group = { dir, origin, items: [] };
|
|
307
|
+
groups.set(key, group);
|
|
308
|
+
}
|
|
309
|
+
group.items.push(item);
|
|
310
|
+
}
|
|
311
|
+
return groups;
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const takeManifestItems = (summary) => {
|
|
315
|
+
if (!summary.foldManifest) return [];
|
|
316
|
+
if (summary.takeAllManifest) return [...summary.manifestItems];
|
|
317
|
+
const taken = [];
|
|
318
|
+
for (const item of summary.manifestItems) {
|
|
319
|
+
if (onlyDepEdits([item])) taken.push(item);
|
|
320
|
+
}
|
|
321
|
+
return taken;
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
module.exports = {
|
|
325
|
+
diffSections,
|
|
326
|
+
mergeResolved,
|
|
327
|
+
orderedChanges,
|
|
328
|
+
summarizeGroup,
|
|
329
|
+
collectGroups,
|
|
330
|
+
takeManifestItems,
|
|
331
|
+
};
|