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.
- package/CHANGELOG.md +19 -1
- package/README.md +1 -1
- package/bin/reslop.js +3 -1
- 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 +88 -3
- 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 -1193
- 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 +59 -167
- 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 +13 -1687
- 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 -747
- 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 +574 -2354
- package/lib/source.js +11 -11
- package/lib/sys.js +25 -4
- package/lib/wrap.js +4 -2
- 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
|
-
|
|
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
|
+
};
|