reslop 0.2.4 → 0.2.6
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 +25 -1
- package/LICENSE +1 -1
- package/README.md +12 -13
- package/bin/reslop.js +3 -8
- package/lib/ansi.js +32 -19
- package/lib/capabilities.js +8 -12
- package/lib/cli.js +40 -74
- package/lib/clipboard.js +48 -18
- package/lib/dashboard/git.js +128 -0
- package/lib/dashboard/model.js +332 -0
- package/lib/dashboard/npm.js +193 -0
- package/lib/dashboard/tree.js +278 -0
- package/lib/deps-diff.js +196 -404
- package/lib/deps.js +240 -399
- package/lib/detect.js +8 -18
- package/lib/diff/align.js +41 -113
- package/lib/diff/diff.js +12 -43
- package/lib/diff/display.js +25 -36
- package/lib/diff/edit.js +6 -20
- package/lib/diff/inline.js +138 -141
- package/lib/diff/items.js +33 -47
- package/lib/diff/parse.js +28 -56
- package/lib/diff/patch.js +12 -33
- package/lib/diff/unit.js +90 -206
- package/lib/editor.js +98 -55
- package/lib/files.js +86 -103
- package/lib/git-branches.js +254 -416
- package/lib/git-deps.js +182 -344
- package/lib/git-reword-todo.js +5 -13
- package/lib/git-worktree.js +141 -212
- package/lib/git.js +21 -49
- package/lib/github.js +67 -181
- package/lib/gitlab.js +40 -90
- package/lib/highlight/bash.js +55 -177
- package/lib/highlight/core.js +157 -132
- package/lib/highlight/css.js +25 -58
- package/lib/highlight/csv.js +19 -37
- package/lib/highlight/dart.js +72 -213
- package/lib/highlight/dot.js +16 -26
- package/lib/highlight/html.js +42 -63
- package/lib/highlight/js.js +177 -506
- package/lib/highlight/json.js +17 -37
- package/lib/highlight/log.js +22 -35
- package/lib/highlight/txt.js +9 -22
- package/lib/highlight.js +17 -46
- package/lib/keys.js +62 -85
- package/lib/manifest.js +67 -130
- package/lib/npm-commands.js +119 -189
- package/lib/remote.js +141 -197
- package/lib/render/branches.js +66 -73
- package/lib/render/chrome.js +264 -323
- package/lib/render/commits.js +187 -500
- package/lib/render/dash-activity.js +212 -0
- package/lib/render/dash-blocks.js +245 -0
- package/lib/render/dash-table.js +290 -0
- package/lib/render/dashboard.js +62 -0
- package/lib/render/diff.js +123 -228
- package/lib/render/files.js +45 -99
- package/lib/render/frame.js +129 -183
- package/lib/render/notes.js +144 -160
- package/lib/render/npm.js +234 -112
- package/lib/render/present.js +4 -11
- package/lib/render/primitives.js +131 -170
- package/lib/render/render.js +5 -14
- package/lib/render/report.js +43 -0
- package/lib/render/tiles.js +260 -0
- package/lib/render/unit.js +15 -28
- package/lib/report-model.js +110 -33
- package/lib/report-parse.js +56 -38
- package/lib/report-render.js +102 -59
- package/lib/report-run.js +87 -78
- package/lib/report-test.js +256 -141
- package/lib/report-tools.js +96 -161
- package/lib/review-merge.js +114 -152
- package/lib/review.js +115 -189
- package/lib/runs.js +225 -0
- package/lib/select.js +89 -75
- package/lib/session/accessors.js +14 -21
- package/lib/session/actions.js +82 -63
- package/lib/session/branches.js +193 -247
- package/lib/session/change-actions.js +171 -214
- package/lib/session/commit.js +280 -342
- package/lib/session/compose.js +559 -674
- package/lib/session/dashboard.js +385 -0
- package/lib/session/dispatch.js +164 -157
- package/lib/session/edit-click.js +57 -0
- package/lib/session/files-pane.js +85 -53
- package/lib/session/items.js +73 -104
- package/lib/session/lifecycle.js +403 -541
- package/lib/session/load.js +48 -68
- package/lib/session/navigation.js +96 -87
- package/lib/session/npm.js +409 -345
- package/lib/session/ops.js +196 -63
- package/lib/session/pointer.js +23 -15
- package/lib/session/progress.js +33 -21
- package/lib/session/review.js +33 -54
- package/lib/session/tasks.js +333 -0
- package/lib/session/templates.js +51 -59
- package/lib/session/terminal.js +85 -136
- package/lib/session/unit.js +3 -4
- package/lib/session/update.js +76 -97
- package/lib/session/view.js +52 -83
- package/lib/session/watch.js +208 -204
- package/lib/session.js +112 -237
- package/lib/update.js +45 -116
- package/lib/utilities.js +67 -89
- package/lib/wrap.js +33 -44
- package/package.json +1 -1
- package/lib/session/todos.js +0 -293
package/lib/git-branches.js
CHANGED
|
@@ -4,10 +4,12 @@ const fs = require('node:fs');
|
|
|
4
4
|
const os = require('node:os');
|
|
5
5
|
const path = require('node:path');
|
|
6
6
|
|
|
7
|
+
const utilities = require('./utilities.js');
|
|
8
|
+
const { oneLine } = utilities;
|
|
7
9
|
const files = require('./files.js');
|
|
8
|
-
const gitWorktree = require('./git-worktree.js');
|
|
9
10
|
const { shortAge } = files;
|
|
10
|
-
const
|
|
11
|
+
const worktree = require('./git-worktree.js');
|
|
12
|
+
const { requireOk, runGit, command, gitOp } = worktree;
|
|
11
13
|
|
|
12
14
|
const BRANCH_FORMAT = [
|
|
13
15
|
'%(HEAD)',
|
|
@@ -17,25 +19,14 @@ const BRANCH_FORMAT = [
|
|
|
17
19
|
'%(upstream:track)',
|
|
18
20
|
'%(subject)',
|
|
19
21
|
].join('%09');
|
|
20
|
-
const TRACK_AHEAD = /ahead (\d+)/;
|
|
21
|
-
const TRACK_BEHIND = /behind (\d+)/;
|
|
22
22
|
const ORIGIN_HEAD = 'refs/remotes/origin/HEAD';
|
|
23
|
+
const ORIGIN_PREFIX = 'origin/';
|
|
23
24
|
const DEFAULT_BRANCHES = ['main', 'master'];
|
|
24
25
|
const PUSH_REJECTED = [
|
|
25
26
|
'non-fast-forward',
|
|
26
27
|
'failed to push some refs',
|
|
27
28
|
'updates were rejected',
|
|
28
29
|
];
|
|
29
|
-
|
|
30
|
-
const commitMessage = (top, sha) => {
|
|
31
|
-
const spec = `${sha ?? ''}`.trim() || 'HEAD';
|
|
32
|
-
const result = runGit(['log', '-1', '--format=%B', spec], top);
|
|
33
|
-
if (result.status !== 0) return '';
|
|
34
|
-
return (result.stdout ?? '').replace(/\s+$/, '');
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const lastMessage = (top) => commitMessage(top, 'HEAD');
|
|
38
|
-
|
|
39
30
|
const COMMIT_FIELDS = ['%H', '%h', '%an', '%ae', '%ar', '%ci', '%D', '%s'];
|
|
40
31
|
const COMMIT_FORMAT = `${COMMIT_FIELDS.join('%x1f')}%x1e%B`;
|
|
41
32
|
const COMMIT_LIMIT = 256;
|
|
@@ -43,42 +34,72 @@ const UNIT = '\x1f';
|
|
|
43
34
|
const RECORD = '\x1e';
|
|
44
35
|
const TAG_REF = /^tag: /;
|
|
45
36
|
const REMOTE_HEAD = /\/HEAD(?: -> |$)/;
|
|
37
|
+
const STAT_LEAD = /^\n* *\d+ files? changed[^\n]*\n?/;
|
|
38
|
+
const FIXUP_MARK = 'fixup!';
|
|
39
|
+
const AUTOSQUASH_ARGS = [
|
|
40
|
+
'-c',
|
|
41
|
+
'sequence.editor=:',
|
|
42
|
+
'rebase',
|
|
43
|
+
'--interactive',
|
|
44
|
+
'--autosquash',
|
|
45
|
+
];
|
|
46
|
+
const REWORD_TODO = path.join(__dirname, 'git-reword-todo.js');
|
|
47
|
+
|
|
48
|
+
const trimText = (value) => `${value ?? ''}`.trim();
|
|
49
|
+
|
|
50
|
+
const required = (value, message) => {
|
|
51
|
+
const text = trimText(value);
|
|
52
|
+
if (!text) throw new Error(message);
|
|
53
|
+
return text;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const branchName = (name) => required(name, 'empty branch name');
|
|
57
|
+
|
|
58
|
+
const messageBody = (message) => {
|
|
59
|
+
const text = `${message ?? ''}`;
|
|
60
|
+
return text.endsWith('\n') ? text : `${text}\n`;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const commitMessage = (top, sha) => {
|
|
64
|
+
const spec = trimText(sha) || 'HEAD';
|
|
65
|
+
const result = runGit(['log', '-1', '--format=%B', spec], top);
|
|
66
|
+
return result.status === 0 ? result.stdout.trimEnd() : '';
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const lastMessage = (top) => commitMessage(top, 'HEAD');
|
|
70
|
+
|
|
71
|
+
const isShownRef = (name) => {
|
|
72
|
+
if (!name || TAG_REF.test(name)) return false;
|
|
73
|
+
return name === 'HEAD' || !REMOTE_HEAD.test(name);
|
|
74
|
+
};
|
|
46
75
|
|
|
47
76
|
const formatCommitRefs = (raw) => {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (!name || TAG_REF.test(name)) continue;
|
|
54
|
-
if (name !== 'HEAD' && REMOTE_HEAD.test(name)) continue;
|
|
55
|
-
names.push(name);
|
|
56
|
-
}
|
|
57
|
-
return names.join(', ');
|
|
77
|
+
const names = trimText(raw).split(', ');
|
|
78
|
+
return names
|
|
79
|
+
.map((name) => name.trim())
|
|
80
|
+
.filter(isShownRef)
|
|
81
|
+
.join(', ');
|
|
58
82
|
};
|
|
59
83
|
|
|
60
84
|
const parseCommitRecord = (record, index) => {
|
|
61
85
|
const cut = record.indexOf(RECORD);
|
|
62
86
|
if (cut < 0) return null;
|
|
63
|
-
const parts = record
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
return (parts[at] ?? '').trim();
|
|
71
|
-
};
|
|
72
|
-
const subject = read('%s') || body.split('\n')[0].trim();
|
|
87
|
+
const parts = record
|
|
88
|
+
.slice(0, cut)
|
|
89
|
+
.split(UNIT)
|
|
90
|
+
.map((part) => part.trim());
|
|
91
|
+
if (parts.length < COMMIT_FIELDS.length || !parts[0]) return null;
|
|
92
|
+
const sha = parts[0];
|
|
93
|
+
const body = record.slice(cut + 1).trimEnd();
|
|
73
94
|
return {
|
|
74
95
|
sha,
|
|
75
|
-
shortSha:
|
|
76
|
-
author:
|
|
77
|
-
email:
|
|
78
|
-
date: shortAge(
|
|
79
|
-
when:
|
|
80
|
-
refs: formatCommitRefs(
|
|
81
|
-
subject,
|
|
96
|
+
shortSha: parts[1] || sha.slice(0, 7),
|
|
97
|
+
author: parts[2],
|
|
98
|
+
email: parts[3],
|
|
99
|
+
date: shortAge(parts[4]),
|
|
100
|
+
when: parts[5],
|
|
101
|
+
refs: formatCommitRefs(parts[6]),
|
|
102
|
+
subject: parts[7] || body.split('\n')[0].trim(),
|
|
82
103
|
body,
|
|
83
104
|
added: 0,
|
|
84
105
|
removed: 0,
|
|
@@ -86,25 +107,11 @@ const parseCommitRecord = (record, index) => {
|
|
|
86
107
|
};
|
|
87
108
|
};
|
|
88
109
|
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const match = STAT_LEAD.exec(chunk);
|
|
93
|
-
if (!match) return { stat: '', rest: chunk };
|
|
94
|
-
return { stat: match[0], rest: chunk.slice(match[0].length) };
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
const countIn = (line, word) => {
|
|
98
|
-
const match = new RegExp(`(\\d+) ${word}`).exec(line);
|
|
99
|
-
if (!match) return 0;
|
|
100
|
-
return parseInt(match[1], 10);
|
|
110
|
+
const statCount = (stat, word) => {
|
|
111
|
+
const match = new RegExp(`(\\d+) ${word}`).exec(stat);
|
|
112
|
+
return match ? parseInt(match[1], 10) : 0;
|
|
101
113
|
};
|
|
102
114
|
|
|
103
|
-
const parseShortstat = (line) => ({
|
|
104
|
-
added: countIn(line, 'insertion'),
|
|
105
|
-
removed: countIn(line, 'deletion'),
|
|
106
|
-
});
|
|
107
|
-
|
|
108
115
|
const listCommits = (top) => {
|
|
109
116
|
const args = [
|
|
110
117
|
'log',
|
|
@@ -116,257 +123,62 @@ const listCommits = (top) => {
|
|
|
116
123
|
const result = requireOk(runGit(args, top), 'log failed');
|
|
117
124
|
const commits = [];
|
|
118
125
|
for (const chunk of result.stdout.split('\0')) {
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
last.
|
|
124
|
-
last.removed = stat.removed;
|
|
126
|
+
const stat = STAT_LEAD.exec(chunk);
|
|
127
|
+
const last = commits.at(-1);
|
|
128
|
+
if (stat && last) {
|
|
129
|
+
last.added = statCount(stat[0], 'insertion');
|
|
130
|
+
last.removed = statCount(stat[0], 'deletion');
|
|
125
131
|
}
|
|
126
|
-
|
|
127
|
-
|
|
132
|
+
const rest = stat ? chunk.slice(stat[0].length) : chunk;
|
|
133
|
+
if (!rest) continue;
|
|
134
|
+
const entry = parseCommitRecord(rest, commits.length);
|
|
128
135
|
if (entry) commits.push(entry);
|
|
129
136
|
}
|
|
130
137
|
return commits;
|
|
131
138
|
};
|
|
132
139
|
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
const resolveCommit = (top, sha) => {
|
|
140
|
-
const result = requireOk(runGit(['rev-parse', sha], top), 'rev-parse failed');
|
|
141
|
-
return (result.stdout ?? '').trim();
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
const dropHeadCommit = (top) => {
|
|
145
|
-
requireOk(runGit(['reset', '--soft', 'HEAD~1'], top), 'drop failed');
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
const runRebase = (top, args, extra = {}) => {
|
|
149
|
-
const result = runGit(args, top, undefined, extra);
|
|
150
|
-
if (result.status === 0) return;
|
|
151
|
-
runGit(['rebase', '--abort'], top);
|
|
152
|
-
requireOk(result, 'rebase failed');
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
const runRebaseAsync = async (top, args, extra = {}) => {
|
|
156
|
-
const result = await runGitAsync(args, top, extra);
|
|
157
|
-
if (result.status === 0) return;
|
|
158
|
-
await runGitAsync(['rebase', '--abort'], top);
|
|
159
|
-
requireOk(result, 'rebase failed');
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
const dropTarget = (top, sha) => {
|
|
163
|
-
const target = resolveCommit(top, namedCommit(sha));
|
|
164
|
-
const parent = runGit(['rev-parse', '--verify', `${target}^`], top);
|
|
165
|
-
if (parent.status !== 0) throw new Error('cannot drop root commit');
|
|
166
|
-
return target;
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
const dropCommit = (top, sha) => {
|
|
170
|
-
const target = dropTarget(top, sha);
|
|
171
|
-
if (target === resolveCommit(top, 'HEAD')) return void dropHeadCommit(top);
|
|
172
|
-
runRebase(top, ['rebase', '--onto', `${target}^`, target]);
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
const dropCommitAsync = async (top, sha) => {
|
|
176
|
-
const target = dropTarget(top, sha);
|
|
177
|
-
if (target === resolveCommit(top, 'HEAD')) {
|
|
178
|
-
requireOk(await runGitAsync(['reset', '--soft', 'HEAD~1'], top));
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
await runRebaseAsync(top, ['rebase', '--onto', `${target}^`, target]);
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
const FIXUP_MARK = 'fixup!';
|
|
185
|
-
const AUTOSQUASH_ARGS = [
|
|
186
|
-
'-c',
|
|
187
|
-
'sequence.editor=:',
|
|
188
|
-
'rebase',
|
|
189
|
-
'--interactive',
|
|
190
|
-
'--autosquash',
|
|
191
|
-
];
|
|
192
|
-
|
|
193
|
-
const isFixupSubject = (subject) =>
|
|
194
|
-
`${subject ?? ''}`.trim().startsWith(FIXUP_MARK);
|
|
195
|
-
|
|
196
|
-
const fixupRest = (subject) => {
|
|
197
|
-
let text = `${subject ?? ''}`.trim();
|
|
198
|
-
while (text.startsWith(FIXUP_MARK)) {
|
|
199
|
-
text = text.slice(FIXUP_MARK.length).trim();
|
|
200
|
-
}
|
|
201
|
-
return text;
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
const commitSubject = (top, sha) => {
|
|
205
|
-
const args = ['log', '-1', '--format=%s', sha];
|
|
206
|
-
const result = requireOk(runGit(args, top), 'log failed');
|
|
207
|
-
return (result.stdout ?? '').trim();
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
const ancestorWithSubject = (top, sha, want) => {
|
|
211
|
-
const args = ['log', '--format=%H%x1f%s', `${sha}^`];
|
|
212
|
-
const result = runGit(args, top);
|
|
213
|
-
if (result.status !== 0) return '';
|
|
214
|
-
for (const line of result.stdout.split('\n')) {
|
|
215
|
-
const cut = line.indexOf(UNIT);
|
|
216
|
-
if (cut < 0) continue;
|
|
217
|
-
const subject = line.slice(cut + 1).trim();
|
|
218
|
-
if (subject !== want) continue;
|
|
219
|
-
return line.slice(0, cut).trim();
|
|
220
|
-
}
|
|
221
|
-
return '';
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
const rebaseRange = (top, sha) => {
|
|
225
|
-
const parent = runGit(['rev-parse', '--verify', `${sha}^`], top);
|
|
226
|
-
return parent.status === 0 ? [parent.stdout.trim()] : ['--root'];
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
const fixupArgs = (top, sha) => {
|
|
230
|
-
const commit = resolveCommit(top, namedCommit(sha));
|
|
231
|
-
const subject = commitSubject(top, commit);
|
|
232
|
-
if (!isFixupSubject(subject)) throw new Error('not a fixup commit');
|
|
233
|
-
const want = fixupRest(subject);
|
|
234
|
-
if (!want) throw new Error('no fixup target');
|
|
235
|
-
const original = ancestorWithSubject(top, commit, want);
|
|
236
|
-
if (!original) throw new Error('no fixup target');
|
|
237
|
-
return [...AUTOSQUASH_ARGS, ...rebaseRange(top, original)];
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
const applyFixup = (top, sha) => runRebase(top, fixupArgs(top, sha));
|
|
241
|
-
|
|
242
|
-
const applyFixupAsync = async (top, sha) =>
|
|
243
|
-
runRebaseAsync(top, fixupArgs(top, sha));
|
|
244
|
-
|
|
245
|
-
const messageBody = (message) => {
|
|
246
|
-
const text = `${message ?? ''}`;
|
|
247
|
-
return text.endsWith('\n') ? text : `${text}\n`;
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
const REWORD_HEAD_ARGS = ['commit', '--amend', '--only', '-F', '-'];
|
|
251
|
-
|
|
252
|
-
const REWORD_TODO = path.join(__dirname, 'git-reword-todo.js');
|
|
253
|
-
|
|
254
|
-
const prepareRewordEditor = (message) => {
|
|
255
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'reslop-reword-'));
|
|
256
|
-
const msgFile = path.join(dir, 'MSG');
|
|
257
|
-
fs.writeFileSync(msgFile, messageBody(message));
|
|
258
|
-
const node = JSON.stringify(process.execPath);
|
|
259
|
-
const script = JSON.stringify(REWORD_TODO);
|
|
140
|
+
const parseTrack = (text) => {
|
|
141
|
+
const raw = trimText(text).replace(/^\[|\]$/g, '');
|
|
142
|
+
const ahead = /ahead (\d+)/.exec(raw);
|
|
143
|
+
const behind = /behind (\d+)/.exec(raw);
|
|
260
144
|
return {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
RESLOP_REWORD_FILE: msgFile,
|
|
265
|
-
},
|
|
145
|
+
ahead: ahead ? Number(ahead[1]) : 0,
|
|
146
|
+
behind: behind ? Number(behind[1]) : 0,
|
|
147
|
+
gone: raw === 'gone',
|
|
266
148
|
};
|
|
267
149
|
};
|
|
268
150
|
|
|
269
|
-
const rewordOlder = (top, sha, message) => {
|
|
270
|
-
const prepared = prepareRewordEditor(message);
|
|
271
|
-
try {
|
|
272
|
-
const args = ['rebase', '--interactive', ...rebaseRange(top, sha)];
|
|
273
|
-
runRebase(top, args, { env: prepared.env });
|
|
274
|
-
} finally {
|
|
275
|
-
fs.rmSync(prepared.dir, { recursive: true, force: true });
|
|
276
|
-
}
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
const rewordOlderAsync = async (top, sha, message) => {
|
|
280
|
-
const prepared = prepareRewordEditor(message);
|
|
281
|
-
try {
|
|
282
|
-
const args = ['rebase', '--interactive', ...rebaseRange(top, sha)];
|
|
283
|
-
await runRebaseAsync(top, args, { env: prepared.env });
|
|
284
|
-
} finally {
|
|
285
|
-
fs.rmSync(prepared.dir, { recursive: true, force: true });
|
|
286
|
-
}
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
const rewordCommit = (top, sha, message) => {
|
|
290
|
-
const commit = resolveCommit(top, namedCommit(sha));
|
|
291
|
-
if (commit === resolveCommit(top, 'HEAD')) {
|
|
292
|
-
requireOk(runGit(REWORD_HEAD_ARGS, top, messageBody(message)));
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
rewordOlder(top, commit, message);
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
const rewordCommitAsync = async (top, sha, message) => {
|
|
299
|
-
const commit = resolveCommit(top, namedCommit(sha));
|
|
300
|
-
if (commit === resolveCommit(top, 'HEAD')) {
|
|
301
|
-
const input = messageBody(message);
|
|
302
|
-
requireOk(await runGitAsync(REWORD_HEAD_ARGS, top, { input }));
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
await rewordOlderAsync(top, commit, message);
|
|
306
|
-
};
|
|
307
|
-
|
|
308
|
-
const parseTrack = (text) => {
|
|
309
|
-
const raw = `${text ?? ''}`.trim().replace(/^\[|\]$/g, '');
|
|
310
|
-
if (!raw) return { ahead: 0, behind: 0, gone: false };
|
|
311
|
-
if (raw === 'gone') return { ahead: 0, behind: 0, gone: true };
|
|
312
|
-
const aheadMatch = TRACK_AHEAD.exec(raw);
|
|
313
|
-
const behindMatch = TRACK_BEHIND.exec(raw);
|
|
314
|
-
const ahead = aheadMatch ? Number(aheadMatch[1]) : 0;
|
|
315
|
-
const behind = behindMatch ? Number(behindMatch[1]) : 0;
|
|
316
|
-
return { ahead, behind, gone: false };
|
|
317
|
-
};
|
|
318
|
-
|
|
319
151
|
const parseBranchLine = (line) => {
|
|
320
152
|
const parts = line.split('\t');
|
|
321
|
-
|
|
322
|
-
const name = (parts[1] ?? '').trim();
|
|
153
|
+
const name = trimText(parts[1]);
|
|
323
154
|
if (!name) return null;
|
|
324
|
-
const mark = parts[0] ?? '';
|
|
325
|
-
const sha = (parts[2] ?? '').trim();
|
|
326
|
-
const date = shortAge(parts[3] ?? '');
|
|
327
|
-
const track = parseTrack(parts[4]);
|
|
328
|
-
const subject = parts.slice(5).join('\t').trim();
|
|
329
155
|
return {
|
|
330
156
|
name,
|
|
331
|
-
current:
|
|
157
|
+
current: parts[0].includes('*'),
|
|
332
158
|
isDefault: false,
|
|
333
|
-
sha,
|
|
334
|
-
date,
|
|
335
|
-
subject,
|
|
336
|
-
...
|
|
159
|
+
sha: trimText(parts[2]),
|
|
160
|
+
date: shortAge(parts[3]),
|
|
161
|
+
subject: parts.slice(5).join('\t').trim(),
|
|
162
|
+
...parseTrack(parts[4]),
|
|
337
163
|
};
|
|
338
164
|
};
|
|
339
165
|
|
|
340
|
-
const originHead = (top) => {
|
|
341
|
-
const args = ['symbolic-ref', '--short', ORIGIN_HEAD];
|
|
342
|
-
const result = runGit(args, top);
|
|
343
|
-
if (result.status !== 0) return '';
|
|
344
|
-
const ref = result.stdout.trim();
|
|
345
|
-
const prefix = 'origin/';
|
|
346
|
-
if (ref.startsWith(prefix)) return ref.slice(prefix.length);
|
|
347
|
-
return ref;
|
|
348
|
-
};
|
|
349
|
-
|
|
350
166
|
const defaultBranch = (top, names) => {
|
|
351
|
-
const
|
|
167
|
+
const result = runGit(['symbolic-ref', '--short', ORIGIN_HEAD], top);
|
|
168
|
+
const ref = result.status === 0 ? result.stdout.trim() : '';
|
|
169
|
+
const remote = ref.startsWith(ORIGIN_PREFIX)
|
|
170
|
+
? ref.slice(ORIGIN_PREFIX.length)
|
|
171
|
+
: ref;
|
|
352
172
|
if (names.has(remote)) return remote;
|
|
353
|
-
|
|
354
|
-
if (names.has(name)) return name;
|
|
355
|
-
}
|
|
356
|
-
return '';
|
|
173
|
+
return DEFAULT_BRANCHES.find((name) => names.has(name)) ?? '';
|
|
357
174
|
};
|
|
358
175
|
|
|
359
176
|
const listBranches = (top) => {
|
|
360
177
|
const args = ['for-each-ref', `--format=${BRANCH_FORMAT}`, 'refs/heads'];
|
|
361
178
|
const result = requireOk(runGit(args, top), 'branch failed');
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
const entry = parseBranchLine(line);
|
|
366
|
-
if (entry) branches.push(entry);
|
|
367
|
-
}
|
|
368
|
-
const names = new Set();
|
|
369
|
-
for (const entry of branches) names.add(entry.name);
|
|
179
|
+
const lines = result.stdout.split('\n').filter(Boolean);
|
|
180
|
+
const branches = lines.map((line) => parseBranchLine(line)).filter(Boolean);
|
|
181
|
+
const names = new Set(branches.map((entry) => entry.name));
|
|
370
182
|
const primary = defaultBranch(top, names);
|
|
371
183
|
return branches.map((entry) => ({
|
|
372
184
|
...entry,
|
|
@@ -374,175 +186,201 @@ const listBranches = (top) => {
|
|
|
374
186
|
}));
|
|
375
187
|
};
|
|
376
188
|
|
|
377
|
-
const
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
return branch;
|
|
189
|
+
const hasStaged = (top) => {
|
|
190
|
+
const result = runGit(['diff', '--cached', '--quiet'], top);
|
|
191
|
+
return result.status === 1;
|
|
381
192
|
};
|
|
382
193
|
|
|
383
|
-
|
|
384
|
-
const
|
|
385
|
-
requireOk(
|
|
386
|
-
}
|
|
194
|
+
function* resolveCommit(sha) {
|
|
195
|
+
const result = yield command(['rev-parse', sha]);
|
|
196
|
+
return requireOk(result, 'rev-parse failed').stdout.trim();
|
|
197
|
+
}
|
|
387
198
|
|
|
388
|
-
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
}
|
|
199
|
+
function* rebaseRange(sha) {
|
|
200
|
+
const parent = yield command(['rev-parse', '--verify', `${sha}^`]);
|
|
201
|
+
return parent.status === 0 ? [parent.stdout.trim()] : ['--root'];
|
|
202
|
+
}
|
|
392
203
|
|
|
393
|
-
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
204
|
+
function* rebase(args, extra) {
|
|
205
|
+
const result = yield command(args, extra);
|
|
206
|
+
if (result.status === 0) return;
|
|
207
|
+
yield command(['rebase', '--abort']);
|
|
208
|
+
requireOk(result, 'rebase failed');
|
|
209
|
+
}
|
|
398
210
|
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
};
|
|
211
|
+
const simpleOp = (toArgs, fallback) =>
|
|
212
|
+
gitOp(function* (...args) {
|
|
213
|
+
const result = yield command(toArgs(...args));
|
|
214
|
+
requireOk(result, fallback);
|
|
215
|
+
});
|
|
404
216
|
|
|
405
|
-
const
|
|
406
|
-
|
|
217
|
+
const checkoutOp = simpleOp(
|
|
218
|
+
(name) => ['checkout', branchName(name)],
|
|
219
|
+
'checkout failed',
|
|
220
|
+
);
|
|
407
221
|
|
|
408
|
-
const
|
|
409
|
-
|
|
222
|
+
const createOp = simpleOp(
|
|
223
|
+
(name) => ['checkout', '-b', branchName(name)],
|
|
224
|
+
'checkout failed',
|
|
225
|
+
);
|
|
410
226
|
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
227
|
+
const dropBranchOp = simpleOp(
|
|
228
|
+
(name) => ['branch', '-D', branchName(name)],
|
|
229
|
+
'drop failed',
|
|
230
|
+
);
|
|
415
231
|
|
|
416
|
-
const
|
|
417
|
-
const branch = namedBranch(name);
|
|
418
|
-
requireOk(await runGitAsync(['branch', '-D', branch], top), 'drop failed');
|
|
419
|
-
};
|
|
232
|
+
const pullOp = simpleOp(() => ['pull'], 'pull failed');
|
|
420
233
|
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
};
|
|
234
|
+
const rebaseOp = gitOp(function* (onto) {
|
|
235
|
+
yield* rebase(['rebase', branchName(onto)]);
|
|
236
|
+
});
|
|
424
237
|
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
238
|
+
const commitOp = gitOp(function* (kind, message) {
|
|
239
|
+
const args = ['commit'];
|
|
240
|
+
if (kind === 'amend') args.push('--amend');
|
|
241
|
+
args.push('-F', '-');
|
|
242
|
+
const result = yield command(args, { input: messageBody(message) });
|
|
243
|
+
requireOk(result);
|
|
244
|
+
});
|
|
428
245
|
|
|
429
|
-
const
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
246
|
+
const dropCommitOp = gitOp(function* (sha) {
|
|
247
|
+
const target = yield* resolveCommit(required(sha, 'empty commit'));
|
|
248
|
+
const parent = yield command(['rev-parse', '--verify', `${target}^`]);
|
|
249
|
+
if (parent.status !== 0) throw new Error('cannot drop root commit');
|
|
250
|
+
if (target === (yield* resolveCommit('HEAD'))) {
|
|
251
|
+
const result = yield command(['reset', '--soft', 'HEAD~1']);
|
|
252
|
+
return void requireOk(result, 'drop failed');
|
|
433
253
|
}
|
|
434
|
-
|
|
435
|
-
};
|
|
254
|
+
yield* rebase(['rebase', '--onto', `${target}^`, target]);
|
|
255
|
+
});
|
|
436
256
|
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
return
|
|
257
|
+
const fixupRest = (subject) => {
|
|
258
|
+
let text = subject;
|
|
259
|
+
while (text.startsWith(FIXUP_MARK)) {
|
|
260
|
+
text = text.slice(FIXUP_MARK.length).trim();
|
|
261
|
+
}
|
|
262
|
+
return text;
|
|
443
263
|
};
|
|
444
264
|
|
|
445
|
-
const
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
265
|
+
const shaWithSubject = (log, subject) => {
|
|
266
|
+
for (const line of log.split('\n')) {
|
|
267
|
+
const cut = line.indexOf(UNIT);
|
|
268
|
+
if (cut < 0 || line.slice(cut + 1).trim() !== subject) continue;
|
|
269
|
+
return line.slice(0, cut).trim();
|
|
270
|
+
}
|
|
271
|
+
return '';
|
|
450
272
|
};
|
|
451
273
|
|
|
452
|
-
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
];
|
|
459
|
-
const
|
|
460
|
-
|
|
274
|
+
function* fixupTarget(commit) {
|
|
275
|
+
const shown = yield command(['log', '-1', '--format=%s', commit]);
|
|
276
|
+
const subject = requireOk(shown, 'log failed').stdout.trim();
|
|
277
|
+
if (!subject.startsWith(FIXUP_MARK)) throw new Error('not a fixup commit');
|
|
278
|
+
const want = fixupRest(subject);
|
|
279
|
+
if (!want) throw new Error('no fixup target');
|
|
280
|
+
const log = yield command(['log', '--format=%H%x1f%s', `${commit}^`]);
|
|
281
|
+
const original = log.status === 0 ? shaWithSubject(log.stdout, want) : '';
|
|
282
|
+
if (!original) throw new Error('no fixup target');
|
|
283
|
+
return original;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const fixupOp = gitOp(function* (sha) {
|
|
287
|
+
const commit = yield* resolveCommit(required(sha, 'empty commit'));
|
|
288
|
+
const original = yield* fixupTarget(commit);
|
|
289
|
+
const range = yield* rebaseRange(original);
|
|
290
|
+
yield* rebase([...AUTOSQUASH_ARGS, ...range]);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const rewordEnv = (msgFile) => {
|
|
294
|
+
const node = JSON.stringify(process.execPath);
|
|
295
|
+
const script = JSON.stringify(REWORD_TODO);
|
|
296
|
+
return {
|
|
297
|
+
GIT_SEQUENCE_EDITOR: `${node} ${script}`,
|
|
298
|
+
RESLOP_REWORD_FILE: msgFile,
|
|
299
|
+
};
|
|
461
300
|
};
|
|
462
301
|
|
|
463
|
-
const
|
|
302
|
+
const rewordOp = gitOp(function* (sha, message) {
|
|
303
|
+
const commit = yield* resolveCommit(required(sha, 'empty commit'));
|
|
304
|
+
const input = messageBody(message);
|
|
305
|
+
if (commit === (yield* resolveCommit('HEAD'))) {
|
|
306
|
+
const args = ['commit', '--amend', '--only', '-F', '-'];
|
|
307
|
+
const result = yield command(args, { input });
|
|
308
|
+
return void requireOk(result);
|
|
309
|
+
}
|
|
310
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'reslop-reword-'));
|
|
311
|
+
try {
|
|
312
|
+
const msgFile = path.join(dir, 'MSG');
|
|
313
|
+
fs.writeFileSync(msgFile, input);
|
|
314
|
+
const range = yield* rebaseRange(commit);
|
|
315
|
+
const env = rewordEnv(msgFile);
|
|
316
|
+
yield* rebase(['rebase', '--interactive', ...range], { env });
|
|
317
|
+
} finally {
|
|
318
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const pushArgs = (force, branch) => {
|
|
464
323
|
const args = ['push'];
|
|
465
324
|
if (force) args.push('--force-with-lease');
|
|
466
325
|
if (branch) args.push('--set-upstream', 'origin', branch);
|
|
467
326
|
return args;
|
|
468
327
|
};
|
|
469
328
|
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
329
|
+
const pushError = (text) => {
|
|
330
|
+
const lower = text.toLowerCase();
|
|
331
|
+
const rejected = PUSH_REJECTED.some((token) => lower.includes(token));
|
|
332
|
+
const message = rejected ? 'push rejected' : oneLine(text, 'git failed');
|
|
333
|
+
const error = new Error(message);
|
|
334
|
+
error.rejected = rejected;
|
|
335
|
+
return error;
|
|
476
336
|
};
|
|
477
337
|
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
result = runGit(pushCmd(useForce, branch), top);
|
|
485
|
-
}
|
|
486
|
-
requirePushOk(result);
|
|
487
|
-
};
|
|
338
|
+
const UPSTREAM_ARGS = [
|
|
339
|
+
'rev-parse',
|
|
340
|
+
'--abbrev-ref',
|
|
341
|
+
'--symbolic-full-name',
|
|
342
|
+
'@{upstream}',
|
|
343
|
+
];
|
|
488
344
|
|
|
489
|
-
const
|
|
345
|
+
const pushOp = gitOp(function* (force) {
|
|
490
346
|
const useForce = force === true;
|
|
491
|
-
const
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
347
|
+
const head = yield command(['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
348
|
+
const branch = head.stdout.trim() || 'HEAD';
|
|
349
|
+
const upstream = yield command(UPSTREAM_ARGS);
|
|
350
|
+
const tracked = upstream.status === 0 ? '' : branch;
|
|
351
|
+
let result = yield command(pushArgs(useForce, tracked));
|
|
352
|
+
const output = (result.stderr || result.stdout).toLowerCase();
|
|
353
|
+
if (result.status !== 0 && output.includes('no upstream')) {
|
|
354
|
+
result = yield command(pushArgs(useForce, branch));
|
|
496
355
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
const hasStaged = (top) => {
|
|
501
|
-
const result = runGit(['diff', '--cached', '--quiet'], top);
|
|
502
|
-
return result.status === 1;
|
|
503
|
-
};
|
|
504
|
-
|
|
505
|
-
const commitPlan = (kind, message) => {
|
|
506
|
-
const args = ['commit'];
|
|
507
|
-
if (kind === 'amend') args.push('--amend');
|
|
508
|
-
args.push('-F', '-');
|
|
509
|
-
return { args, body: messageBody(message) };
|
|
510
|
-
};
|
|
511
|
-
|
|
512
|
-
const commitChanges = (top, kind, message) => {
|
|
513
|
-
const plan = commitPlan(kind, message);
|
|
514
|
-
requireOk(runGit(plan.args, top, plan.body));
|
|
515
|
-
};
|
|
516
|
-
|
|
517
|
-
const commitChangesAsync = async (top, kind, message) => {
|
|
518
|
-
const plan = commitPlan(kind, message);
|
|
519
|
-
requireOk(await runGitAsync(plan.args, top, { input: plan.body }));
|
|
520
|
-
};
|
|
356
|
+
if (result.status === 0) return;
|
|
357
|
+
throw pushError(result.stderr || result.stdout || 'push failed');
|
|
358
|
+
});
|
|
521
359
|
|
|
522
360
|
module.exports = {
|
|
523
361
|
lastMessage,
|
|
524
362
|
commitMessage,
|
|
525
363
|
listCommits,
|
|
526
364
|
listBranches,
|
|
527
|
-
checkoutBranch,
|
|
528
|
-
checkoutBranchAsync,
|
|
529
|
-
createBranch,
|
|
530
|
-
createBranchAsync,
|
|
531
|
-
rebaseBranch,
|
|
532
|
-
rebaseBranchAsync,
|
|
533
|
-
dropCommit,
|
|
534
|
-
dropCommitAsync,
|
|
535
|
-
applyFixup,
|
|
536
|
-
applyFixupAsync,
|
|
537
|
-
rewordCommit,
|
|
538
|
-
rewordCommitAsync,
|
|
539
|
-
dropBranch,
|
|
540
|
-
dropBranchAsync,
|
|
541
|
-
pullChanges,
|
|
542
|
-
pullChangesAsync,
|
|
543
|
-
pushChanges,
|
|
544
|
-
pushChangesAsync,
|
|
545
365
|
hasStaged,
|
|
546
|
-
|
|
547
|
-
|
|
366
|
+
checkoutBranch: checkoutOp.sync,
|
|
367
|
+
checkoutBranchAsync: checkoutOp.async,
|
|
368
|
+
createBranch: createOp.sync,
|
|
369
|
+
createBranchAsync: createOp.async,
|
|
370
|
+
rebaseBranch: rebaseOp.sync,
|
|
371
|
+
rebaseBranchAsync: rebaseOp.async,
|
|
372
|
+
dropBranch: dropBranchOp.sync,
|
|
373
|
+
dropBranchAsync: dropBranchOp.async,
|
|
374
|
+
dropCommit: dropCommitOp.sync,
|
|
375
|
+
dropCommitAsync: dropCommitOp.async,
|
|
376
|
+
applyFixup: fixupOp.sync,
|
|
377
|
+
applyFixupAsync: fixupOp.async,
|
|
378
|
+
rewordCommit: rewordOp.sync,
|
|
379
|
+
rewordCommitAsync: rewordOp.async,
|
|
380
|
+
pullChanges: pullOp.sync,
|
|
381
|
+
pullChangesAsync: pullOp.async,
|
|
382
|
+
pushChanges: pushOp.sync,
|
|
383
|
+
pushChangesAsync: pushOp.async,
|
|
384
|
+
commitChanges: commitOp.sync,
|
|
385
|
+
commitChangesAsync: commitOp.async,
|
|
548
386
|
};
|