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/lib/editor.js CHANGED
@@ -22,6 +22,36 @@ const offsetAt = (parts, count) => {
22
22
  return pos;
23
23
  };
24
24
 
25
+ const WORD_RE = /[\p{L}\p{N}_]/u;
26
+
27
+ const isWordChar = (ch) => WORD_RE.test(ch);
28
+
29
+ const skipWhile = (parts, at, dir, pred) => {
30
+ if (dir > 0) {
31
+ while (at < parts.length && pred(parts[at])) at += 1;
32
+ return at;
33
+ }
34
+ while (at > 0 && pred(parts[at - 1])) at -= 1;
35
+ return at;
36
+ };
37
+
38
+ const wordIndex = (parts, at, dir) => {
39
+ const word = (ch) => isWordChar(ch);
40
+ const sep = (ch) => !isWordChar(ch);
41
+ if (dir > 0) {
42
+ if (at < parts.length && word(parts[at])) {
43
+ return skipWhile(parts, at, 1, word);
44
+ }
45
+ at = skipWhile(parts, at, 1, sep);
46
+ return skipWhile(parts, at, 1, word);
47
+ }
48
+ if (at > 0 && word(parts[at - 1])) {
49
+ return skipWhile(parts, at, -1, word);
50
+ }
51
+ at = skipWhile(parts, at, -1, sep);
52
+ return skipWhile(parts, at, -1, word);
53
+ };
54
+
25
55
  class Editor {
26
56
  constructor(text = '') {
27
57
  this.text = text;
@@ -87,6 +117,13 @@ class Editor {
87
117
  this.cursor = offsetAt(parts, next);
88
118
  }
89
119
 
120
+ moveWord(delta) {
121
+ this.wantCol = null;
122
+ const dir = delta < 0 ? -1 : 1;
123
+ const { parts, i } = graphemeIndex(this.text, this.cursor);
124
+ this.cursor = offsetAt(parts, wordIndex(parts, i, dir));
125
+ }
126
+
90
127
  linePos() {
91
128
  const before = this.text.slice(0, this.cursor);
92
129
  const at = before.lastIndexOf('\n');
package/lib/files.js CHANGED
@@ -4,6 +4,7 @@ const REVIEW_DIR = '.review';
4
4
  const ORIGIN_ORDER = ['staged', 'unstaged', 'untracked', 'commit', 'pr'];
5
5
  const TODO_FILE = 'TODOs';
6
6
  const REPO_TODOS_LABEL = 'Repository TODOs and Issues';
7
+ const TOTAL_LABEL = 'total';
7
8
 
8
9
  const normalizePathSpec = (spec) =>
9
10
  `${spec ?? ''}`.replaceAll('\\', '/').replace(/\/+$/, '');
@@ -27,6 +28,8 @@ const isTodoItem = (item) => !!(item && item.origin === 'todo');
27
28
 
28
29
  const isTodosEntry = (entry) => !!(entry && entry.kind === 'todos');
29
30
 
31
+ const isTotalEntry = (entry) => !!(entry && entry.kind === 'total');
32
+
30
33
  const repoTodosLabel = () => REPO_TODOS_LABEL;
31
34
 
32
35
  const makeTodoItem = () => ({
@@ -69,6 +72,45 @@ const lineDelta = ({ hunk, blockId }) => {
69
72
  return delta;
70
73
  };
71
74
 
75
+ const addLineDelta = (group, origin, delta) => {
76
+ group.added += delta.added;
77
+ group.removed += delta.removed;
78
+ if (origin === 'staged') {
79
+ group.stagedAdded += delta.added;
80
+ group.stagedRemoved += delta.removed;
81
+ return;
82
+ }
83
+ group.unstagedAdded += delta.added;
84
+ group.unstagedRemoved += delta.removed;
85
+ };
86
+
87
+ const originLineDelta = (entry) => {
88
+ if (entry.stagedAdded !== undefined || entry.unstagedAdded !== undefined) {
89
+ return {
90
+ stagedAdded: entry.stagedAdded ?? 0,
91
+ stagedRemoved: entry.stagedRemoved ?? 0,
92
+ unstagedAdded: entry.unstagedAdded ?? 0,
93
+ unstagedRemoved: entry.unstagedRemoved ?? 0,
94
+ };
95
+ }
96
+ const added = entry.added ?? 0;
97
+ const removed = entry.removed ?? 0;
98
+ if ((entry.status ?? '') === 'staged') {
99
+ return {
100
+ stagedAdded: added,
101
+ stagedRemoved: removed,
102
+ unstagedAdded: 0,
103
+ unstagedRemoved: 0,
104
+ };
105
+ }
106
+ return {
107
+ stagedAdded: 0,
108
+ stagedRemoved: 0,
109
+ unstagedAdded: added,
110
+ unstagedRemoved: removed,
111
+ };
112
+ };
113
+
72
114
  const fileEntries = (items) => {
73
115
  const groups = new Map();
74
116
  for (let i = 0; i < items.length; i++) {
@@ -84,6 +126,10 @@ const fileEntries = (items) => {
84
126
  remaining: 0,
85
127
  added: 0,
86
128
  removed: 0,
129
+ stagedAdded: 0,
130
+ stagedRemoved: 0,
131
+ unstagedAdded: 0,
132
+ unstagedRemoved: 0,
87
133
  staged: 0,
88
134
  unstaged: 0,
89
135
  });
@@ -92,9 +138,7 @@ const fileEntries = (items) => {
92
138
  const { origin } = item;
93
139
  group.origins.push(origin);
94
140
  group.remaining += 1;
95
- const delta = lineDelta(item);
96
- group.added += delta.added;
97
- group.removed += delta.removed;
141
+ addLineDelta(group, origin, lineDelta(item));
98
142
  if (origin === 'staged') group.staged += 1;
99
143
  if (origin === 'unstaged') {
100
144
  if (group.unstaged === 0) group.openIndex = i;
@@ -108,16 +152,57 @@ const fileEntries = (items) => {
108
152
  return entries;
109
153
  };
110
154
 
155
+ const fileTotals = (files) => {
156
+ const total = {
157
+ path: TOTAL_LABEL,
158
+ kind: 'total',
159
+ status: '',
160
+ remaining: 0,
161
+ added: 0,
162
+ removed: 0,
163
+ stagedAdded: 0,
164
+ stagedRemoved: 0,
165
+ unstagedAdded: 0,
166
+ unstagedRemoved: 0,
167
+ staged: 0,
168
+ unstaged: 0,
169
+ };
170
+ for (const entry of files) {
171
+ if (isTodosEntry(entry) || isTotalEntry(entry)) continue;
172
+ const remaining = entry.remaining ?? 0;
173
+ const staged = entry.staged ?? 0;
174
+ const added = entry.added ?? 0;
175
+ const removed = entry.removed ?? 0;
176
+ const lines = originLineDelta(entry);
177
+ const listed = entry.unstaged ?? Math.max(0, remaining - staged);
178
+ const untracked = (entry.status ?? '') === 'untracked';
179
+ const unstaged = untracked && listed === 0 ? remaining : listed;
180
+ total.remaining += remaining;
181
+ total.added += added;
182
+ total.removed += removed;
183
+ total.stagedAdded += lines.stagedAdded;
184
+ total.stagedRemoved += lines.stagedRemoved;
185
+ total.unstagedAdded += lines.unstagedAdded;
186
+ total.unstagedRemoved += lines.unstagedRemoved;
187
+ total.staged += staged;
188
+ total.unstaged += unstaged;
189
+ }
190
+ return total;
191
+ };
192
+
111
193
  module.exports = {
112
194
  REVIEW_DIR,
113
195
  TODO_FILE,
114
196
  REPO_TODOS_LABEL,
197
+ TOTAL_LABEL,
115
198
  isPathInScope,
116
199
  itemPath,
117
200
  isTodoItem,
118
201
  isTodosEntry,
202
+ isTotalEntry,
119
203
  repoTodosLabel,
120
204
  makeTodoItem,
121
205
  fileStatus,
122
206
  fileEntries,
207
+ fileTotals,
123
208
  };
@@ -0,0 +1,291 @@
1
+ 'use strict';
2
+
3
+ const { oneLine, requireOk, runGit, runGitAsync } = require('./run.js');
4
+
5
+ const BRANCH_FORMAT = [
6
+ '%(HEAD)',
7
+ '%(refname:short)',
8
+ '%(objectname:short)',
9
+ '%(committerdate:relative)',
10
+ '%(upstream:track)',
11
+ '%(subject)',
12
+ ].join('%09');
13
+ const TRACK_AHEAD = /ahead (\d+)/;
14
+ const TRACK_BEHIND = /behind (\d+)/;
15
+ const ORIGIN_HEAD = 'refs/remotes/origin/HEAD';
16
+ const DEFAULT_BRANCHES = ['main', 'master'];
17
+ const PUSH_REJECTED = [
18
+ 'non-fast-forward',
19
+ 'failed to push some refs',
20
+ 'updates were rejected',
21
+ ];
22
+
23
+ const lastMessage = (top) => {
24
+ const result = runGit(['log', '-1', '--format=%B'], top);
25
+ if (result.status !== 0) return '';
26
+ return (result.stdout ?? '').replace(/\s+$/, '');
27
+ };
28
+
29
+ const parseTrack = (text) => {
30
+ const raw = `${text ?? ''}`.trim().replace(/^\[|\]$/g, '');
31
+ if (!raw) return { ahead: 0, behind: 0, gone: false };
32
+ if (raw === 'gone') return { ahead: 0, behind: 0, gone: true };
33
+ const aheadMatch = TRACK_AHEAD.exec(raw);
34
+ const behindMatch = TRACK_BEHIND.exec(raw);
35
+ const ahead = aheadMatch ? Number(aheadMatch[1]) : 0;
36
+ const behind = behindMatch ? Number(behindMatch[1]) : 0;
37
+ return { ahead, behind, gone: false };
38
+ };
39
+
40
+ const parseBranchLine = (line) => {
41
+ const parts = line.split('\t');
42
+ if (parts.length < 2) return null;
43
+ const name = (parts[1] ?? '').trim();
44
+ if (!name) return null;
45
+ const mark = parts[0] ?? '';
46
+ const sha = (parts[2] ?? '').trim();
47
+ const date = (parts[3] ?? '').trim();
48
+ const track = parseTrack(parts[4]);
49
+ const subject = parts.slice(5).join('\t').trim();
50
+ return {
51
+ name,
52
+ current: mark.includes('*'),
53
+ isDefault: false,
54
+ sha,
55
+ date,
56
+ subject,
57
+ ...track,
58
+ };
59
+ };
60
+
61
+ const originHead = (top) => {
62
+ const args = ['symbolic-ref', '--short', ORIGIN_HEAD];
63
+ const result = runGit(args, top);
64
+ if (result.status !== 0) return '';
65
+ const ref = result.stdout.trim();
66
+ const prefix = 'origin/';
67
+ if (ref.startsWith(prefix)) return ref.slice(prefix.length);
68
+ return ref;
69
+ };
70
+
71
+ const defaultBranch = (top, names) => {
72
+ const remote = originHead(top);
73
+ if (names.has(remote)) return remote;
74
+ for (const name of DEFAULT_BRANCHES) {
75
+ if (names.has(name)) return name;
76
+ }
77
+ return '';
78
+ };
79
+
80
+ const listBranches = (top) => {
81
+ const args = ['for-each-ref', `--format=${BRANCH_FORMAT}`, 'refs/heads'];
82
+ const result = requireOk(runGit(args, top), 'branch failed');
83
+ const branches = [];
84
+ for (const line of result.stdout.split('\n')) {
85
+ if (!line) continue;
86
+ const entry = parseBranchLine(line);
87
+ if (entry) branches.push(entry);
88
+ }
89
+ const names = new Set();
90
+ for (const entry of branches) names.add(entry.name);
91
+ const primary = defaultBranch(top, names);
92
+ const listed = [];
93
+ for (const entry of branches) {
94
+ const isDefault = entry.name === primary;
95
+ listed.push({ ...entry, isDefault });
96
+ }
97
+ return listed;
98
+ };
99
+
100
+ const namedBranch = (name) => {
101
+ const branch = `${name ?? ''}`.trim();
102
+ if (!branch) throw new Error('empty branch name');
103
+ return branch;
104
+ };
105
+
106
+ const checkoutBranch = (top, name) => {
107
+ const branch = namedBranch(name);
108
+ requireOk(runGit(['checkout', branch], top), 'checkout failed');
109
+ };
110
+
111
+ const checkoutBranchAsync = async (top, name) => {
112
+ const branch = namedBranch(name);
113
+ requireOk(await runGitAsync(['checkout', branch], top), 'checkout failed');
114
+ };
115
+
116
+ const createBranch = (top, name) => {
117
+ const branch = namedBranch(name);
118
+ const args = ['checkout', '-b', branch];
119
+ requireOk(runGit(args, top), 'checkout failed');
120
+ };
121
+
122
+ const createBranchAsync = async (top, name) => {
123
+ const branch = namedBranch(name);
124
+ const args = ['checkout', '-b', branch];
125
+ requireOk(await runGitAsync(args, top), 'checkout failed');
126
+ };
127
+
128
+ const rebaseFailed = (result) => {
129
+ if (result.status === 0) return '';
130
+ return result.stderr || result.stdout || 'rebase failed';
131
+ };
132
+
133
+ const rebaseBranch = (top, onto) => {
134
+ const branch = namedBranch(onto);
135
+ const result = runGit(['rebase', branch], top);
136
+ const failed = rebaseFailed(result);
137
+ if (!failed) return;
138
+ runGit(['rebase', '--abort'], top);
139
+ throw new Error(oneLine(failed));
140
+ };
141
+
142
+ const rebaseBranchAsync = async (top, onto) => {
143
+ const branch = namedBranch(onto);
144
+ const result = await runGitAsync(['rebase', branch], top);
145
+ const failed = rebaseFailed(result);
146
+ if (!failed) return;
147
+ await runGitAsync(['rebase', '--abort'], top);
148
+ throw new Error(oneLine(failed));
149
+ };
150
+
151
+ const dropBranch = (top, name) => {
152
+ const branch = namedBranch(name);
153
+ requireOk(runGit(['branch', '-D', branch], top), 'drop failed');
154
+ };
155
+
156
+ const dropBranchAsync = async (top, name) => {
157
+ const branch = namedBranch(name);
158
+ requireOk(await runGitAsync(['branch', '-D', branch], top), 'drop failed');
159
+ };
160
+
161
+ const pullChanges = (top) => {
162
+ requireOk(runGit(['pull'], top), 'pull failed');
163
+ };
164
+
165
+ const pullChangesAsync = async (top) => {
166
+ requireOk(await runGitAsync(['pull'], top), 'pull failed');
167
+ };
168
+
169
+ const isRejectedPush = (msg) => {
170
+ const text = `${msg ?? ''}`.toLowerCase();
171
+ for (const token of PUSH_REJECTED) {
172
+ if (text.includes(token)) return true;
173
+ }
174
+ return false;
175
+ };
176
+
177
+ const pushError = (msg) => {
178
+ const text = `${msg ?? ''}`;
179
+ const rejected = isRejectedPush(text);
180
+ const error = new Error(rejected ? 'push rejected' : oneLine(text));
181
+ error.rejected = rejected;
182
+ return error;
183
+ };
184
+
185
+ const pushBranch = (top) => {
186
+ const result = runGit(['rev-parse', '--abbrev-ref', 'HEAD'], top);
187
+ const name = (result.stdout ?? '').trim();
188
+ if (!name || name === 'HEAD') return 'HEAD';
189
+ return name;
190
+ };
191
+
192
+ const hasUpstream = (top) => {
193
+ const args = [
194
+ 'rev-parse',
195
+ '--abbrev-ref',
196
+ '--symbolic-full-name',
197
+ '@{upstream}',
198
+ ];
199
+ const result = runGit(args, top);
200
+ return result.status === 0;
201
+ };
202
+
203
+ const pushCmd = (force, branch) => {
204
+ const args = ['push'];
205
+ if (force) args.push('--force-with-lease');
206
+ if (branch) args.push('--set-upstream', 'origin', branch);
207
+ return args;
208
+ };
209
+
210
+ const noUpstream = (msg) =>
211
+ `${msg ?? ''}`.toLowerCase().includes('no upstream');
212
+
213
+ const finishPush = (result, force, branch, run) => {
214
+ if (result.status === 0) return;
215
+ const msg = result.stderr || result.stdout || 'push failed';
216
+ if (!noUpstream(msg)) throw pushError(msg);
217
+ const next = run(pushCmd(force, branch));
218
+ if (next.status !== 0) {
219
+ throw pushError(next.stderr || next.stdout || 'push failed');
220
+ }
221
+ };
222
+
223
+ const pushChanges = (top, force) => {
224
+ const useForce = force === true;
225
+ const branch = pushBranch(top);
226
+ const tracked = hasUpstream(top) ? '' : branch;
227
+ const result = runGit(pushCmd(useForce, tracked), top);
228
+ finishPush(result, useForce, branch, (args) => runGit(args, top));
229
+ };
230
+
231
+ const pushChangesAsync = async (top, force) => {
232
+ const useForce = force === true;
233
+ const branch = pushBranch(top);
234
+ const tracked = hasUpstream(top) ? '' : branch;
235
+ const result = await runGitAsync(pushCmd(useForce, tracked), top);
236
+ if (result.status === 0) return;
237
+ const msg = result.stderr || result.stdout || 'push failed';
238
+ if (!noUpstream(msg)) throw pushError(msg);
239
+ const next = await runGitAsync(pushCmd(useForce, branch), top);
240
+ if (next.status !== 0) {
241
+ throw pushError(next.stderr || next.stdout || 'push failed');
242
+ }
243
+ };
244
+
245
+ const hasStaged = (top) => {
246
+ const result = runGit(['diff', '--cached', '--quiet'], top);
247
+ return result.status === 1;
248
+ };
249
+
250
+ const commitPlan = (kind, message) => {
251
+ if (kind === 'fixup') {
252
+ const target = (message ?? '').trim() || 'HEAD';
253
+ return { args: ['commit', `--fixup=${target}`], body: undefined };
254
+ }
255
+ const args = ['commit'];
256
+ if (kind === 'amend') args.push('--amend');
257
+ args.push('-F', '-');
258
+ const text = message ?? '';
259
+ const body = text.endsWith('\n') ? text : `${text}\n`;
260
+ return { args, body };
261
+ };
262
+
263
+ const commitChanges = (top, kind, message) => {
264
+ const plan = commitPlan(kind, message);
265
+ requireOk(runGit(plan.args, top, plan.body));
266
+ };
267
+
268
+ const commitChangesAsync = async (top, kind, message) => {
269
+ const plan = commitPlan(kind, message);
270
+ requireOk(await runGitAsync(plan.args, top, { input: plan.body }));
271
+ };
272
+
273
+ module.exports = {
274
+ lastMessage,
275
+ listBranches,
276
+ checkoutBranch,
277
+ checkoutBranchAsync,
278
+ createBranch,
279
+ createBranchAsync,
280
+ rebaseBranch,
281
+ rebaseBranchAsync,
282
+ dropBranch,
283
+ dropBranchAsync,
284
+ pullChanges,
285
+ pullChangesAsync,
286
+ pushChanges,
287
+ pushChangesAsync,
288
+ hasStaged,
289
+ commitChanges,
290
+ commitChangesAsync,
291
+ };
@@ -0,0 +1,164 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const { itemPath } = require('../files.js');
7
+ const { replaceBlockAdds } = require('../diff/edit.js');
8
+ const { requireOk, runGit } = require('./run.js');
9
+
10
+ const applyPatch = (cwd, patch, flags) => {
11
+ const args = ['apply', ...flags, '--whitespace=nowarn', '-'];
12
+ const result = runGit(args, cwd, patch);
13
+ requireOk(result, 'apply failed');
14
+ };
15
+
16
+ const addItem = (top, item) => {
17
+ if (item.origin === 'staged' || item.origin === 'commit') return;
18
+ if (item.origin === 'untracked' || item.file.isBinary) {
19
+ requireOk(runGit(['add', '--', item.file.newPath], top));
20
+ return;
21
+ }
22
+ applyPatch(top, item.patchAdd, ['--cached']);
23
+ };
24
+
25
+ const unstageItem = (top, item) => {
26
+ if (item.origin !== 'staged') return;
27
+ const rel = itemPath(item);
28
+ if (item.file.isBinary) {
29
+ requireOk(runGit(['restore', '--staged', '--', rel], top));
30
+ return;
31
+ }
32
+ applyPatch(top, item.patchAdd, ['--reverse', '--cached']);
33
+ };
34
+
35
+ const revertOnePath = (top, rel, items) => {
36
+ const origins = new Set();
37
+ let isNew = false;
38
+ for (const item of items) {
39
+ origins.add(item.origin);
40
+ if (item.file && item.file.isNew) isNew = true;
41
+ }
42
+ if (isNew) {
43
+ if (origins.has('staged')) {
44
+ requireOk(runGit(['rm', '-f', '--', rel], top));
45
+ return;
46
+ }
47
+ fs.unlinkSync(path.join(top, rel));
48
+ return;
49
+ }
50
+ if (origins.has('untracked') && origins.size === 1) {
51
+ fs.unlinkSync(path.join(top, rel));
52
+ return;
53
+ }
54
+ const args = ['restore', '-s', 'HEAD', '--worktree', '--staged', '--', rel];
55
+ requireOk(runGit(args, top));
56
+ };
57
+
58
+ const revertItem = (top, item) => {
59
+ if (item.origin === 'commit') return;
60
+ const rel = itemPath(item);
61
+ if (item.origin === 'untracked') {
62
+ fs.unlinkSync(path.join(top, rel));
63
+ return;
64
+ }
65
+ if (item.file.isBinary) {
66
+ const args = ['restore', '-s', 'HEAD', '--worktree'];
67
+ if (item.origin === 'staged') args.push('--staged');
68
+ args.push('--', rel);
69
+ requireOk(runGit(args, top));
70
+ return;
71
+ }
72
+ if (item.origin === 'unstaged') {
73
+ applyPatch(top, item.patchRevert, ['--reverse']);
74
+ return;
75
+ }
76
+ applyPatch(top, item.patchRevert, ['--reverse', '--cached']);
77
+ try {
78
+ applyPatch(top, item.patchRevert, ['--reverse']);
79
+ } catch (error) {
80
+ if (!error.message.includes('does not apply')) throw error;
81
+ }
82
+ };
83
+
84
+ const revertFile = (top, rel, items) => {
85
+ const group = [];
86
+ for (const item of items) {
87
+ if (itemPath(item) === rel) group.push(item);
88
+ }
89
+ const targets = group.length ? group : items;
90
+ revertOnePath(top, rel, targets);
91
+ };
92
+
93
+ const indexText = (top, rel) => {
94
+ const result = runGit(['show', `:${rel}`], top);
95
+ requireOk(result, 'show failed');
96
+ return result.stdout ?? '';
97
+ };
98
+
99
+ const indexMode = (top, rel) => {
100
+ const result = runGit(['ls-files', '--stage', '--', rel], top);
101
+ if (result.status !== 0) return '100644';
102
+ const line = (result.stdout ?? '').trim();
103
+ if (!line) return '100644';
104
+ const mode = line.split(' ')[0];
105
+ return mode || '100644';
106
+ };
107
+
108
+ const writeIndex = (top, rel, text) => {
109
+ const hashed = runGit(['hash-object', '-w', '--stdin'], top, text);
110
+ requireOk(hashed, 'hash-object failed');
111
+ const sha = (hashed.stdout ?? '').trim();
112
+ const mode = indexMode(top, rel);
113
+ const args = ['update-index', '--cacheinfo', mode, sha, rel];
114
+ requireOk(runGit(args, top), 'update-index failed');
115
+ };
116
+
117
+ const writeWorktree = (abs, text) => {
118
+ fs.writeFileSync(abs, text);
119
+ };
120
+
121
+ const editStaged = (top, rel, abs, hunk, blockId, text) => {
122
+ const source = indexText(top, rel);
123
+ const next = replaceBlockAdds(source, hunk, blockId, text);
124
+ writeIndex(top, rel, next);
125
+ let work = null;
126
+ try {
127
+ work = fs.readFileSync(abs, 'utf8');
128
+ } catch (error) {
129
+ if (error.code !== 'ENOENT') throw error;
130
+ }
131
+ if (work === null) {
132
+ writeWorktree(abs, next);
133
+ return;
134
+ }
135
+ if (work === source) writeWorktree(abs, next);
136
+ };
137
+
138
+ const editWorktree = (abs, hunk, blockId, text) => {
139
+ const source = fs.readFileSync(abs, 'utf8');
140
+ const next = replaceBlockAdds(source, hunk, blockId, text);
141
+ if (next === source) return;
142
+ writeWorktree(abs, next);
143
+ };
144
+
145
+ const editItem = (top, item, text) => {
146
+ if (item.origin === 'commit' || item.origin === 'pr') return;
147
+ const rel = itemPath(item);
148
+ if (!rel || !item.hunk) return;
149
+ const abs = path.join(top, rel);
150
+ if (item.origin === 'staged') {
151
+ editStaged(top, rel, abs, item.hunk, item.blockId, text);
152
+ return;
153
+ }
154
+ editWorktree(abs, item.hunk, item.blockId, text);
155
+ };
156
+
157
+ module.exports = {
158
+ addItem,
159
+ unstageItem,
160
+ revertOnePath,
161
+ revertItem,
162
+ revertFile,
163
+ editItem,
164
+ };