reslop 0.1.7 → 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.
Files changed (96) hide show
  1. package/CHANGELOG.md +19 -1
  2. package/README.md +1 -1
  3. package/bin/reslop.js +3 -1
  4. package/lib/capabilities.js +106 -0
  5. package/lib/cli.js +18 -14
  6. package/lib/deps/changes.js +331 -0
  7. package/lib/deps/items.js +131 -0
  8. package/lib/deps/manifest.js +145 -0
  9. package/lib/deps/merge.js +120 -0
  10. package/lib/deps/proposals.js +164 -0
  11. package/lib/deps/reports.js +180 -0
  12. package/lib/deps/rows.js +177 -0
  13. package/lib/deps/scan.js +138 -0
  14. package/lib/deps/usage.js +68 -0
  15. package/lib/deps/versions.js +119 -0
  16. package/lib/deps.js +65 -1348
  17. package/lib/diff/align.js +226 -0
  18. package/lib/diff/display.js +101 -0
  19. package/lib/diff/edit.js +47 -0
  20. package/lib/diff/inline.js +300 -0
  21. package/lib/diff/items.js +101 -0
  22. package/lib/diff/parse.js +156 -0
  23. package/lib/diff/patch.js +103 -0
  24. package/lib/diff.js +13 -932
  25. package/lib/editor.js +37 -0
  26. package/lib/files.js +88 -3
  27. package/lib/git/branches.js +291 -0
  28. package/lib/git/changes.js +164 -0
  29. package/lib/git/dependencies.js +489 -0
  30. package/lib/git/proc.js +162 -0
  31. package/lib/git/read.js +253 -0
  32. package/lib/git/run.js +100 -0
  33. package/lib/git.js +82 -1193
  34. package/lib/github.js +92 -222
  35. package/lib/gitlab.js +72 -216
  36. package/lib/highlight/bash.js +5 -9
  37. package/lib/highlight/core.js +1 -1
  38. package/lib/highlight/css.js +1 -3
  39. package/lib/highlight/csv.js +1 -3
  40. package/lib/highlight/dart.js +6 -20
  41. package/lib/highlight/dot.js +13 -13
  42. package/lib/highlight/html.js +1 -5
  43. package/lib/highlight/js.js +151 -115
  44. package/lib/highlight/json.js +1 -3
  45. package/lib/highlight/log.js +1 -3
  46. package/lib/highlight/txt.js +5 -10
  47. package/lib/highlight.js +4 -4
  48. package/lib/keys.js +59 -167
  49. package/lib/npm/commands.js +100 -0
  50. package/lib/remote/changes.js +37 -0
  51. package/lib/remote/headers.js +36 -0
  52. package/lib/remote/http.js +122 -0
  53. package/lib/remote/notes.js +94 -0
  54. package/lib/render/branches.js +218 -0
  55. package/lib/render/chrome.js +575 -0
  56. package/lib/render/diff.js +307 -0
  57. package/lib/render/files.js +166 -0
  58. package/lib/render/frame.js +197 -0
  59. package/lib/render/notes.js +201 -0
  60. package/lib/render/present.js +25 -0
  61. package/lib/render/primitives.js +200 -0
  62. package/lib/render.js +13 -1687
  63. package/lib/review/format.js +434 -0
  64. package/lib/review/storage.js +150 -0
  65. package/lib/review/store.js +178 -0
  66. package/lib/review/templates.js +49 -0
  67. package/lib/review.js +14 -747
  68. package/lib/select.js +3 -5
  69. package/lib/session/accessors.js +25 -0
  70. package/lib/session/actions.js +275 -0
  71. package/lib/session/branches.js +304 -0
  72. package/lib/session/change-actions.js +320 -0
  73. package/lib/session/commit.js +124 -0
  74. package/lib/session/compose.js +540 -0
  75. package/lib/session/dispatch.js +105 -0
  76. package/lib/session/files-pane.js +234 -0
  77. package/lib/session/items.js +177 -0
  78. package/lib/session/lifecycle.js +476 -0
  79. package/lib/session/load.js +85 -0
  80. package/lib/session/navigation.js +93 -0
  81. package/lib/session/ops.js +110 -0
  82. package/lib/session/origins.js +7 -0
  83. package/lib/session/pointer.js +154 -0
  84. package/lib/session/progress.js +29 -0
  85. package/lib/session/review.js +88 -0
  86. package/lib/session/templates.js +64 -0
  87. package/lib/session/terminal.js +169 -0
  88. package/lib/session/todos.js +269 -0
  89. package/lib/session/update.js +125 -0
  90. package/lib/session/view.js +129 -0
  91. package/lib/session/watch.js +219 -0
  92. package/lib/session.js +574 -2354
  93. package/lib/source.js +11 -11
  94. package/lib/sys.js +25 -4
  95. package/lib/wrap.js +4 -2
  96. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
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
+
5
22
  ## [0.1.7][] - 2026-09-16
6
23
 
7
24
  - Make TODOs repo-wide, not per-file todos
@@ -66,7 +83,8 @@
66
83
  - Feedback and todos that produce a repair plan for an agent
67
84
  - Intra-line highlighting in unified, mixed, and side-by-side layouts
68
85
 
69
- [unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.1.7...HEAD
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
70
88
  [0.1.7]: https://github.com/tshemsedinov/reslop/compare/v0.1.6...v0.1.7
71
89
  [0.1.6]: https://github.com/tshemsedinov/reslop/compare/v0.1.5...v0.1.6
72
90
  [0.1.5]: https://github.com/tshemsedinov/reslop/compare/v0.1.4...v0.1.5
package/README.md CHANGED
@@ -19,6 +19,7 @@ Review → Plan → Repair → Verify
19
19
  - Leave feedback, issues/todos, and in-place code proposals as a repair plan.
20
20
  - Review uncommitted git diffs, a given commit, a GitHub PR, or a GitLab MR.
21
21
  - Stage, unstage, or revert each contiguous block of diff lines.
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.
23
24
  - Commit, amend, or fixup staged changes from the file list.
24
25
  - Branches: checkout, create, rebase, drop, pull and push.
@@ -59,7 +60,6 @@ Reviews go in `.review/YYYY-MM-DD-NN.md` with frontmatter `status`:
59
60
  | `b` | List branches; `n` new; `r` rebase; `d` drop |
60
61
  | `p` / `s` | git pull/push |
61
62
  | `f` / `t` / `e` | Feedback / repo todo / edit added lines |
62
- | `r` | Reload diffs from disk |
63
63
  | `m` | Mode: unified / mixed / side-by-side |
64
64
  | `⏎` | Open the selected file |
65
65
  | `→` / `j` | Next remaining diff / file |
package/bin/reslop.js CHANGED
@@ -7,7 +7,9 @@ const { LEAVE_TERM } = require('../lib/session.js');
7
7
  const fail = (reason) => {
8
8
  try {
9
9
  process.stdout.write(LEAVE_TERM);
10
- } catch {}
10
+ } catch {
11
+ // ignore
12
+ }
11
13
  process.stderr.write(failLine(reason));
12
14
  process.exit(1);
13
15
  };
@@ -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 repo = deps.repo ?? createGitRepo();
127
+ const rawRepo = deps.repo ?? createGitRepo();
133
128
  const statSync = deps.statSync ?? fs.statSync;
134
- const scope = resolveScope(cwd, selected.paths, repo, statSync);
135
- const session = startSession(
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
- const repo = createLoadedSource({ items: [] });
162
- repo.loadAsync = loadRemote;
163
- return repo;
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
- return startSession(
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
+ };
@@ -0,0 +1,131 @@
1
+ 'use strict';
2
+
3
+ const { itemPath } = require('../files.js');
4
+
5
+ const { depFileMeta } = require('./manifest.js');
6
+ const { orderedChanges } = require('./changes.js');
7
+ const { buildRows, makeHunk } = require('./rows.js');
8
+
9
+ const uniquePaths = (items) => {
10
+ const files = [];
11
+ const seen = new Set();
12
+ for (const item of items) {
13
+ const rel = itemPath(item);
14
+ if (!rel || seen.has(rel)) continue;
15
+ seen.add(rel);
16
+ files.push(rel);
17
+ }
18
+ return files;
19
+ };
20
+
21
+ const filesForChange = (change, files) => {
22
+ const manifest = [];
23
+ const lockfile = [];
24
+ for (const rel of files) {
25
+ const meta = depFileMeta(rel);
26
+ if (!meta) continue;
27
+ if (meta.kind === 'manifest') manifest.push(rel);
28
+ else lockfile.push(rel);
29
+ }
30
+ if (change.section === 'field') {
31
+ if (manifest.length) return manifest;
32
+ return files;
33
+ }
34
+ if (change.section === 'resolved') {
35
+ if (lockfile.length) return lockfile;
36
+ return files;
37
+ }
38
+ if (change.propose) {
39
+ if (manifest.length) return manifest;
40
+ return files;
41
+ }
42
+ const both = [...manifest, ...lockfile];
43
+ if (both.length) return both;
44
+ return files;
45
+ };
46
+
47
+ const pickPrimary = (items) => {
48
+ for (const item of items) {
49
+ const rel = itemPath(item);
50
+ const meta = depFileMeta(rel);
51
+ if (meta && meta.kind === 'manifest') return item;
52
+ }
53
+ return items[0];
54
+ };
55
+
56
+ const makeDepItem = (origin, fileItems, hidden, change, extra, blockId) => {
57
+ const allFiles = uniquePaths(fileItems);
58
+ const files = change ? filesForChange(change, allFiles) : allFiles;
59
+ const primary = pickPrimary(fileItems);
60
+ const oldPath = itemPath(primary);
61
+ const newPath = oldPath;
62
+ const isNew = fileItems.some((item) => item.file && item.file.isNew);
63
+ const isDeleted = fileItems.every((item) => item.file && item.file.isDeleted);
64
+ let oldCount = 0;
65
+ let newCount = 0;
66
+ if (!change) {
67
+ oldCount = extra.oldCount ?? 0;
68
+ newCount = extra.newCount ?? 0;
69
+ }
70
+ const rows = buildRows(change, oldCount, newCount);
71
+ const hunk = makeHunk(rows, blockId);
72
+ const isBinary = false;
73
+ const preamble = [];
74
+ const hunks = [hunk];
75
+ const file = {
76
+ oldPath,
77
+ newPath,
78
+ isNew,
79
+ isDeleted,
80
+ isBinary,
81
+ preamble,
82
+ hunks,
83
+ };
84
+ const patchAdd = '';
85
+ const patchRevert = '';
86
+ const items = hidden;
87
+ const dep = { files, items, change };
88
+ const reload = !!(change && change.propose);
89
+ return { origin, file, hunk, blockId, patchAdd, patchRevert, dep, reload };
90
+ };
91
+
92
+ const makeDepItems = (origin, fileItems, hidden, summary) => {
93
+ const ordered = orderedChanges(summary.changes);
94
+ if (!ordered.length) {
95
+ return [makeDepItem(origin, fileItems, hidden, null, summary, 0)];
96
+ }
97
+ const items = [];
98
+ let blockId = 0;
99
+ for (const change of ordered) {
100
+ const made = makeDepItem(
101
+ origin,
102
+ fileItems,
103
+ hidden,
104
+ change,
105
+ summary,
106
+ blockId,
107
+ );
108
+ items.push(made);
109
+ blockId += 1;
110
+ }
111
+ return items;
112
+ };
113
+
114
+ const stubFileItem = (rel, origin) => {
115
+ const file = {
116
+ oldPath: rel,
117
+ newPath: rel,
118
+ isNew: false,
119
+ isDeleted: false,
120
+ isBinary: false,
121
+ preamble: [],
122
+ hunks: [],
123
+ };
124
+ const hunk = null;
125
+ const blockId = 0;
126
+ const patchAdd = '';
127
+ const patchRevert = '';
128
+ return { origin, file, hunk, blockId, patchAdd, patchRevert };
129
+ };
130
+
131
+ module.exports = { makeDepItems, stubFileItem };