@openvcs/git-plugin 0.1.0-nightly.20260421.19 → 0.2.0-beta.40
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/README.md +2 -0
- package/bin/git.js +36 -16
- package/bin/plugin-request-handler.js +33 -12
- package/bin/plugin.js +1 -1
- package/bin/submodules.js +42 -6
- package/package.json +3 -3
- package/src/git.ts +42 -19
- package/src/plugin-request-handler.ts +49 -18
- package/src/plugin.ts +1 -1
- package/src/submodules.ts +49 -7
package/README.md
CHANGED
|
@@ -65,11 +65,13 @@ The npm package can be consumed from prerelease channels published by CI:
|
|
|
65
65
|
|
|
66
66
|
- `latest`: stable releases
|
|
67
67
|
- `beta`: builds from the `Beta` branch
|
|
68
|
+
- `edge`: working builds from `Dev` push commits
|
|
68
69
|
- `nightly`: scheduled builds from `Dev` when there are changes since the last nightly
|
|
69
70
|
|
|
70
71
|
Examples:
|
|
71
72
|
|
|
72
73
|
```bash
|
|
74
|
+
npm install @openvcs/git-plugin@edge
|
|
73
75
|
npm install @openvcs/git-plugin@beta
|
|
74
76
|
npm install @openvcs/git-plugin@nightly
|
|
75
77
|
```
|
package/bin/git.js
CHANGED
|
@@ -163,26 +163,41 @@ export class GitCommand {
|
|
|
163
163
|
const args = buildPullFfOnlyArgs(options);
|
|
164
164
|
return this.runChecked(args, 'git-pull-failed');
|
|
165
165
|
}
|
|
166
|
-
commit
|
|
167
|
-
|
|
166
|
+
/** Returns the current HEAD commit id. */
|
|
167
|
+
currentHead() {
|
|
168
|
+
return this.runChecked(['rev-parse', 'HEAD'], 'git-head-failed').stdout.trim();
|
|
168
169
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
170
|
+
/** Creates a commit, optionally limited to the provided paths. */
|
|
171
|
+
commit(message, name, email, paths) {
|
|
172
|
+
const execArgs = [
|
|
173
|
+
...(name ? ['-c', `user.name=${name}`] : []),
|
|
174
|
+
...(email ? ['-c', `user.email=${email}`] : []),
|
|
175
|
+
'commit',
|
|
176
|
+
'-m',
|
|
177
|
+
message,
|
|
178
|
+
...(paths && paths.length > 0 ? ['--', ...paths] : []),
|
|
179
|
+
];
|
|
180
|
+
return this.runChecked(execArgs, 'git-commit-failed');
|
|
181
|
+
}
|
|
182
|
+
/** Creates a commit from the current index only. */
|
|
183
|
+
commitIndex(message, name, email) {
|
|
177
184
|
const commitMessage = message || 'Stage changes';
|
|
178
185
|
const execArgs = [
|
|
179
186
|
...(name ? ['-c', `user.name=${name}`] : []),
|
|
180
187
|
...(email ? ['-c', `user.email=${email}`] : []),
|
|
181
|
-
|
|
182
|
-
'-m',
|
|
188
|
+
'commit',
|
|
189
|
+
'-m',
|
|
190
|
+
commitMessage,
|
|
183
191
|
];
|
|
184
192
|
return this.runChecked(execArgs, 'git-commit-failed');
|
|
185
193
|
}
|
|
194
|
+
/** Stages the provided repository-relative paths into the index. */
|
|
195
|
+
stagePaths(paths) {
|
|
196
|
+
if (paths.length === 0) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
this.runChecked(['add', '-A', '--', ...paths], 'git-stage-paths-failed');
|
|
200
|
+
}
|
|
186
201
|
listCommits(options = {}) {
|
|
187
202
|
const args = ['log', '--all'];
|
|
188
203
|
if (options.topo_order) {
|
|
@@ -206,7 +221,7 @@ export class GitCommand {
|
|
|
206
221
|
if (options.until_utc) {
|
|
207
222
|
args.push(`--until=${options.until_utc}`);
|
|
208
223
|
}
|
|
209
|
-
args.push('--pretty=format:%H%
|
|
224
|
+
args.push('--pretty=format:%H%x00%s%x00%aN%x00%aI%x00%P%x1e');
|
|
210
225
|
if (options.branch) {
|
|
211
226
|
args.push(options.branch);
|
|
212
227
|
}
|
|
@@ -371,8 +386,11 @@ export class GitCommand {
|
|
|
371
386
|
hashObject(content) {
|
|
372
387
|
return this.run(['hash-object', '-w', '--stdin'], { stdin: content }).stdout.trim();
|
|
373
388
|
}
|
|
389
|
+
/** Stages a textual patch into the index without requiring worktree/index parity. */
|
|
374
390
|
stagePatch(patch) {
|
|
375
|
-
this.runChecked(['apply', '--
|
|
391
|
+
this.runChecked(['apply', '--cached', '--unidiff-zero'], 'git-stage-patch-failed', {
|
|
392
|
+
stdin: patch,
|
|
393
|
+
});
|
|
376
394
|
}
|
|
377
395
|
applyReversePatch(patch) {
|
|
378
396
|
this.runChecked(['apply', '-R', patch], 'git-apply-reverse-failed');
|
|
@@ -383,9 +401,10 @@ export class GitCommand {
|
|
|
383
401
|
resetSoftTo(ref) {
|
|
384
402
|
this.runChecked(['reset', '--soft', ref], 'git-reset-soft-failed');
|
|
385
403
|
}
|
|
404
|
+
/** Reads the effective Git commit identity from config. */
|
|
386
405
|
getIdentity() {
|
|
387
|
-
const nameResult = this.run(['config', '--
|
|
388
|
-
const emailResult = this.run(['config', '--
|
|
406
|
+
const nameResult = this.run(['config', '--get', 'user.name']);
|
|
407
|
+
const emailResult = this.run(['config', '--get', 'user.email']);
|
|
389
408
|
if (nameResult.status !== 0 || emailResult.status !== 0) {
|
|
390
409
|
return null;
|
|
391
410
|
}
|
|
@@ -394,6 +413,7 @@ export class GitCommand {
|
|
|
394
413
|
email: emailResult.stdout.trim(),
|
|
395
414
|
};
|
|
396
415
|
}
|
|
416
|
+
/** Stores repository-local commit identity in Git config. */
|
|
397
417
|
setIdentityLocal(name, email) {
|
|
398
418
|
this.runChecked(['config', '--local', 'user.name', name], 'git-identity-set-failed');
|
|
399
419
|
this.runChecked(['config', '--local', 'user.email', email], 'git-identity-set-failed');
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
import { VcsDelegateBase, pluginError, } from '@openvcs/sdk/runtime';
|
|
4
4
|
import { asNumber, asRecord, asString, asStringArray, asTrimmedString, buildCloneArgs, parseStatusOutput, } from './plugin-helpers.js';
|
|
5
5
|
import { GitCommand } from './git.js';
|
|
6
|
+
/** Returns an optional boolean only when the input is already a boolean. */
|
|
7
|
+
function asOptionalBoolean(value) {
|
|
8
|
+
return typeof value === 'boolean' ? value : undefined;
|
|
9
|
+
}
|
|
6
10
|
/** Reduces a file status string to the primary status code needed for discard routing. */
|
|
7
11
|
function getPrimaryDiscardStatus(status) {
|
|
8
12
|
const normalized = asTrimmedString(status);
|
|
@@ -214,15 +218,13 @@ export class GitVcsDelegates extends VcsDelegateBase {
|
|
|
214
218
|
}
|
|
215
219
|
commit(params, _context) {
|
|
216
220
|
const git = this.requireGit(params.session_id);
|
|
217
|
-
|
|
218
|
-
git.
|
|
219
|
-
return result.stdout.trim();
|
|
221
|
+
git.commit(asTrimmedString(params.message), asTrimmedString(params.name), asTrimmedString(params.email), asStringArray(params.paths));
|
|
222
|
+
return git.currentHead();
|
|
220
223
|
}
|
|
221
224
|
commitIndex(params, _context) {
|
|
222
225
|
const git = this.requireGit(params.session_id);
|
|
223
|
-
|
|
224
|
-
git.
|
|
225
|
-
return result.stdout.trim();
|
|
226
|
+
git.commitIndex(asTrimmedString(params.message), asTrimmedString(params.name), asTrimmedString(params.email));
|
|
227
|
+
return git.currentHead();
|
|
226
228
|
}
|
|
227
229
|
getStatusSummary(params, _context) {
|
|
228
230
|
const git = this.requireGit(params.session_id);
|
|
@@ -239,8 +241,8 @@ export class GitVcsDelegates extends VcsDelegateBase {
|
|
|
239
241
|
branch: asTrimmedString(query.rev) || undefined,
|
|
240
242
|
skip: asNumber(query.skip, 0) || undefined,
|
|
241
243
|
limit: asNumber(query.limit, 0),
|
|
242
|
-
topo_order: query.topo_order
|
|
243
|
-
include_merges: query.include_merges
|
|
244
|
+
topo_order: asOptionalBoolean(query.topo_order),
|
|
245
|
+
include_merges: asOptionalBoolean(query.include_merges),
|
|
244
246
|
author_contains: asTrimmedString(query.author_contains) || undefined,
|
|
245
247
|
since_utc: asTrimmedString(query.since_utc) || undefined,
|
|
246
248
|
until_utc: asTrimmedString(query.until_utc) || undefined,
|
|
@@ -280,6 +282,11 @@ export class GitVcsDelegates extends VcsDelegateBase {
|
|
|
280
282
|
git.stagePatch(asString(params.patch));
|
|
281
283
|
return null;
|
|
282
284
|
}
|
|
285
|
+
stagePaths(params, _context) {
|
|
286
|
+
const git = this.requireGit(params.session_id);
|
|
287
|
+
git.stagePaths(asStringArray(params.paths));
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
283
290
|
discardPaths(params, _context) {
|
|
284
291
|
const git = this.requireGit(params.session_id);
|
|
285
292
|
const paths = asStringArray(params.paths);
|
|
@@ -288,15 +295,29 @@ export class GitVcsDelegates extends VcsDelegateBase {
|
|
|
288
295
|
}
|
|
289
296
|
const status = git.runChecked(['status', '--porcelain=1', '-z', '-uall', '--', ...paths], 'git-discard-paths-failed');
|
|
290
297
|
const discardPlan = planDiscardPaths(status.stdout);
|
|
298
|
+
let failure;
|
|
291
299
|
if (discardPlan.unstageThenRemove.length > 0) {
|
|
292
|
-
|
|
300
|
+
try {
|
|
301
|
+
git.runChecked(['rm', '-f', '--cached', '--', ...discardPlan.unstageThenRemove], 'git-discard-paths-failed');
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
failure = error;
|
|
305
|
+
}
|
|
293
306
|
}
|
|
294
|
-
if (discardPlan.restore.length > 0) {
|
|
295
|
-
|
|
307
|
+
if (!failure && discardPlan.restore.length > 0) {
|
|
308
|
+
try {
|
|
309
|
+
git.runChecked(['restore', '--source=HEAD', '--staged', '--worktree', '--', ...discardPlan.restore], 'git-discard-paths-failed');
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
failure = error;
|
|
313
|
+
}
|
|
296
314
|
}
|
|
297
|
-
if (discardPlan.clean.length > 0) {
|
|
315
|
+
if (!failure && discardPlan.clean.length > 0) {
|
|
298
316
|
git.runChecked(['clean', '-f', '--', ...discardPlan.clean], 'git-discard-paths-failed');
|
|
299
317
|
}
|
|
318
|
+
if (failure) {
|
|
319
|
+
throw failure;
|
|
320
|
+
}
|
|
300
321
|
return null;
|
|
301
322
|
}
|
|
302
323
|
applyReversePatch(params, _context) {
|
package/bin/plugin.js
CHANGED
|
@@ -38,7 +38,7 @@ export function OnPluginStart() {
|
|
|
38
38
|
}
|
|
39
39
|
const delegates = new GitVcsDelegates(createGitRuntimeDependencies());
|
|
40
40
|
PluginDefinition.vcs = delegates.toDelegates();
|
|
41
|
-
const repoMenu = getOrCreateMenu('repository', 'Repository');
|
|
41
|
+
const repoMenu = getOrCreateMenu('repository', 'Repository', { surface: 'menubar' });
|
|
42
42
|
if (repoMenu) {
|
|
43
43
|
repoMenu.addItem({ label: 'Edit .gitignore', action: 'repo-edit-gitignore' });
|
|
44
44
|
repoMenu.addItem({ label: 'Edit .gitattributes', action: 'repo-edit-gitattributes' });
|
package/bin/submodules.js
CHANGED
|
@@ -18,7 +18,7 @@ function payloadString(payload, key) {
|
|
|
18
18
|
return String(payload[key] ?? '').trim();
|
|
19
19
|
}
|
|
20
20
|
/** Builds one modal row for a submodule entry. */
|
|
21
|
-
function buildSubmoduleRow(entry) {
|
|
21
|
+
export function buildSubmoduleRow(entry) {
|
|
22
22
|
const metaBits = [entry.commit ? `commit ${entry.commit}` : '', entry.branch ? `branch ${entry.branch}` : '']
|
|
23
23
|
.map((part) => String(part || '').trim())
|
|
24
24
|
.filter(Boolean);
|
|
@@ -51,12 +51,44 @@ function buildSubmoduleRow(entry) {
|
|
|
51
51
|
],
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
|
+
/** Builds an error fallback modal with a descriptive message. */
|
|
55
|
+
function buildErrorModal(message) {
|
|
56
|
+
return new ModalBuilder('Error').text(message).text('Please try again or check the Git repository state.');
|
|
57
|
+
}
|
|
58
|
+
/** Opens a fallback modal for a submodule UI failure and preserves the original error on fallback failure. */
|
|
59
|
+
export async function handleSubmoduleModalError(prefix, error, openFallback = (message) => buildErrorModal(message).open()) {
|
|
60
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
61
|
+
const fullMessage = `${prefix}: ${message}`;
|
|
62
|
+
console.error(`Git submodules: ${fullMessage}`);
|
|
63
|
+
try {
|
|
64
|
+
return await openFallback(fullMessage);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
54
70
|
/** Builds and opens the submodule manager modal. */
|
|
55
71
|
async function openSubmodulesModal() {
|
|
56
72
|
const git = createGitCommand();
|
|
57
|
-
|
|
73
|
+
let entries;
|
|
74
|
+
try {
|
|
75
|
+
entries = git.listSubmodules();
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
return handleSubmoduleModalError('failed to list submodules', error);
|
|
79
|
+
}
|
|
58
80
|
console.log('Git submodules: building modal', { count: entries.length });
|
|
59
|
-
const modal =
|
|
81
|
+
const modal = buildSubmodulesModal(entries);
|
|
82
|
+
try {
|
|
83
|
+
return await modal.open();
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
return handleSubmoduleModalError('failed to open modal', error);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Builds the submodule manager modal with given entries. */
|
|
90
|
+
function buildSubmodulesModal(entries) {
|
|
91
|
+
return new ModalBuilder('Manage Submodules')
|
|
60
92
|
.text('Review, add, update, sync, and remove submodules without leaving Git.')
|
|
61
93
|
.text('Use Update Remote to follow each submodule branch configured in .gitmodules.')
|
|
62
94
|
.separator()
|
|
@@ -109,7 +141,6 @@ async function openSubmodulesModal() {
|
|
|
109
141
|
emptyText: 'This repository has no submodules yet.',
|
|
110
142
|
items: entries.map((entry) => buildSubmoduleRow(entry)),
|
|
111
143
|
});
|
|
112
|
-
return modal.open();
|
|
113
144
|
}
|
|
114
145
|
/** Builds and opens the remove confirmation modal for one submodule. */
|
|
115
146
|
async function openRemoveConfirmationModal(path, name) {
|
|
@@ -129,7 +160,12 @@ async function openRemoveConfirmationModal(path, name) {
|
|
|
129
160
|
payload: { path: submodulePath, name: String(name || '').trim() || undefined },
|
|
130
161
|
},
|
|
131
162
|
], { gap: '.5rem', align: 'centered', wrap: true });
|
|
132
|
-
|
|
163
|
+
try {
|
|
164
|
+
return await modal.open();
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
return handleSubmoduleModalError('failed to open remove confirmation modal', error);
|
|
168
|
+
}
|
|
133
169
|
}
|
|
134
170
|
/** Removes one submodule and refreshes the toolkit modal. */
|
|
135
171
|
async function removeSubmodule(payload) {
|
|
@@ -200,7 +236,7 @@ async function syncAllSubmodules() {
|
|
|
200
236
|
}
|
|
201
237
|
/** Registers the Git submodule toolkit menu and action handlers. */
|
|
202
238
|
export function registerSubmoduleToolkit() {
|
|
203
|
-
const repoMenu = getOrCreateMenu('repository', 'Repository');
|
|
239
|
+
const repoMenu = getOrCreateMenu('repository', 'Repository', { surface: 'menubar' });
|
|
204
240
|
repoMenu?.addItem({ label: 'Submodules', action: 'repo-submodules' });
|
|
205
241
|
registerAction('repo-submodules', async () => {
|
|
206
242
|
console.log('Git submodules: repo-submodules action invoked');
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openvcs/git-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-beta.40",
|
|
4
4
|
"description": "OpenVCS Git plugin - Node.js runtime",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"homepage": "https://github.com/Open-VCS/OpenVCS-Plugin-Git",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/Open-VCS/OpenVCS-Plugin-Git"
|
|
9
|
+
"url": "git+https://github.com/Open-VCS/OpenVCS-Plugin-Git.git"
|
|
10
10
|
},
|
|
11
11
|
"bugs": {
|
|
12
12
|
"url": "https://github.com/Open-VCS/OpenVCS-Plugin-Git/issues"
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"openvcs": {
|
|
19
19
|
"id": "openvcs.git",
|
|
20
20
|
"name": "Git",
|
|
21
|
-
"version": "0.
|
|
21
|
+
"version": "0.2.0-beta.40",
|
|
22
22
|
"author": "OpenVCS Contributors",
|
|
23
23
|
"description": "Git VCS backend plugin for OpenVCS",
|
|
24
24
|
"default_enabled": true,
|
package/src/git.ts
CHANGED
|
@@ -274,31 +274,49 @@ export class GitCommand {
|
|
|
274
274
|
return this.runChecked(args, 'git-pull-failed');
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
-
commit
|
|
278
|
-
|
|
277
|
+
/** Returns the current HEAD commit id. */
|
|
278
|
+
currentHead(): string {
|
|
279
|
+
return this.runChecked(['rev-parse', 'HEAD'], 'git-head-failed').stdout.trim();
|
|
279
280
|
}
|
|
280
281
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
282
|
+
/** Creates a commit, optionally limited to the provided paths. */
|
|
283
|
+
commit(message: string, name?: string, email?: string, paths?: string[]): GitCommandResult {
|
|
284
|
+
const execArgs = [
|
|
285
|
+
...(name ? ['-c', `user.name=${name}`] : []),
|
|
286
|
+
...(email ? ['-c', `user.email=${email}`] : []),
|
|
287
|
+
'commit',
|
|
288
|
+
'-m',
|
|
289
|
+
message,
|
|
290
|
+
...(paths && paths.length > 0 ? ['--', ...paths] : []),
|
|
291
|
+
];
|
|
292
|
+
|
|
293
|
+
return this.runChecked(execArgs, 'git-commit-failed');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Creates a commit from the current index only. */
|
|
297
|
+
commitIndex(message?: string, name?: string, email?: string): GitCommandResult {
|
|
290
298
|
const commitMessage = message || 'Stage changes';
|
|
291
|
-
|
|
299
|
+
|
|
292
300
|
const execArgs = [
|
|
293
301
|
...(name ? ['-c', `user.name=${name}`] : []),
|
|
294
302
|
...(email ? ['-c', `user.email=${email}`] : []),
|
|
295
|
-
|
|
296
|
-
'-m',
|
|
303
|
+
'commit',
|
|
304
|
+
'-m',
|
|
305
|
+
commitMessage,
|
|
297
306
|
];
|
|
298
|
-
|
|
307
|
+
|
|
299
308
|
return this.runChecked(execArgs, 'git-commit-failed');
|
|
300
309
|
}
|
|
301
310
|
|
|
311
|
+
/** Stages the provided repository-relative paths into the index. */
|
|
312
|
+
stagePaths(paths: string[]): void {
|
|
313
|
+
if (paths.length === 0) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
this.runChecked(['add', '-A', '--', ...paths], 'git-stage-paths-failed');
|
|
318
|
+
}
|
|
319
|
+
|
|
302
320
|
listCommits(options: ListCommitsOptions = {}): { commits: CommitEntry[]; exitCode: number } {
|
|
303
321
|
const args = ['log', '--all'];
|
|
304
322
|
|
|
@@ -330,7 +348,7 @@ export class GitCommand {
|
|
|
330
348
|
args.push(`--until=${options.until_utc}`);
|
|
331
349
|
}
|
|
332
350
|
|
|
333
|
-
args.push('--pretty=format:%H%
|
|
351
|
+
args.push('--pretty=format:%H%x00%s%x00%aN%x00%aI%x00%P%x1e');
|
|
334
352
|
|
|
335
353
|
if (options.branch) {
|
|
336
354
|
args.push(options.branch);
|
|
@@ -525,8 +543,11 @@ export class GitCommand {
|
|
|
525
543
|
return this.run(['hash-object', '-w', '--stdin'], { stdin: content }).stdout.trim();
|
|
526
544
|
}
|
|
527
545
|
|
|
546
|
+
/** Stages a textual patch into the index without requiring worktree/index parity. */
|
|
528
547
|
stagePatch(patch: string): void {
|
|
529
|
-
this.runChecked(['apply', '--
|
|
548
|
+
this.runChecked(['apply', '--cached', '--unidiff-zero'], 'git-stage-patch-failed', {
|
|
549
|
+
stdin: patch,
|
|
550
|
+
});
|
|
530
551
|
}
|
|
531
552
|
|
|
532
553
|
applyReversePatch(patch: string): void {
|
|
@@ -541,9 +562,10 @@ export class GitCommand {
|
|
|
541
562
|
this.runChecked(['reset', '--soft', ref], 'git-reset-soft-failed');
|
|
542
563
|
}
|
|
543
564
|
|
|
565
|
+
/** Reads the effective Git commit identity from config. */
|
|
544
566
|
getIdentity(): { name: string; email: string } | null {
|
|
545
|
-
const nameResult = this.run(['config', '--
|
|
546
|
-
const emailResult = this.run(['config', '--
|
|
567
|
+
const nameResult = this.run(['config', '--get', 'user.name']);
|
|
568
|
+
const emailResult = this.run(['config', '--get', 'user.email']);
|
|
547
569
|
|
|
548
570
|
if (nameResult.status !== 0 || emailResult.status !== 0) {
|
|
549
571
|
return null;
|
|
@@ -555,6 +577,7 @@ export class GitCommand {
|
|
|
555
577
|
};
|
|
556
578
|
}
|
|
557
579
|
|
|
580
|
+
/** Stores repository-local commit identity in Git config. */
|
|
558
581
|
setIdentityLocal(name: string, email: string): void {
|
|
559
582
|
this.runChecked(['config', '--local', 'user.name', name], 'git-identity-set-failed');
|
|
560
583
|
this.runChecked(['config', '--local', 'user.email', email], 'git-identity-set-failed');
|
|
@@ -30,6 +30,11 @@ export interface DiscardPathPlan {
|
|
|
30
30
|
clean: string[];
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/** Returns an optional boolean only when the input is already a boolean. */
|
|
34
|
+
function asOptionalBoolean(value: unknown): boolean | undefined {
|
|
35
|
+
return typeof value === 'boolean' ? value : undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
33
38
|
/** Reduces a file status string to the primary status code needed for discard routing. */
|
|
34
39
|
function getPrimaryDiscardStatus(status: string): string {
|
|
35
40
|
const normalized = asTrimmedString(status);
|
|
@@ -348,9 +353,13 @@ export class GitVcsDelegates extends VcsDelegateBase<GitRuntimeDependencies> {
|
|
|
348
353
|
_context: PluginRuntimeContext,
|
|
349
354
|
): string {
|
|
350
355
|
const git = this.requireGit(params.session_id);
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
356
|
+
git.commit(
|
|
357
|
+
asTrimmedString(params.message),
|
|
358
|
+
asTrimmedString(params.name),
|
|
359
|
+
asTrimmedString(params.email),
|
|
360
|
+
asStringArray(params.paths),
|
|
361
|
+
);
|
|
362
|
+
return git.currentHead();
|
|
354
363
|
}
|
|
355
364
|
|
|
356
365
|
override commitIndex(
|
|
@@ -358,14 +367,12 @@ export class GitVcsDelegates extends VcsDelegateBase<GitRuntimeDependencies> {
|
|
|
358
367
|
_context: PluginRuntimeContext,
|
|
359
368
|
): string {
|
|
360
369
|
const git = this.requireGit(params.session_id);
|
|
361
|
-
const result = git.runChecked(['rev-parse', 'HEAD'], 'git-commit-failed');
|
|
362
370
|
git.commitIndex(
|
|
363
371
|
asTrimmedString(params.message),
|
|
364
372
|
asTrimmedString(params.name),
|
|
365
373
|
asTrimmedString(params.email),
|
|
366
|
-
asStringArray(params.paths),
|
|
367
374
|
);
|
|
368
|
-
return
|
|
375
|
+
return git.currentHead();
|
|
369
376
|
}
|
|
370
377
|
|
|
371
378
|
override getStatusSummary(
|
|
@@ -394,8 +401,8 @@ export class GitVcsDelegates extends VcsDelegateBase<GitRuntimeDependencies> {
|
|
|
394
401
|
branch: asTrimmedString(query.rev) || undefined,
|
|
395
402
|
skip: asNumber(query.skip, 0) || undefined,
|
|
396
403
|
limit: asNumber(query.limit, 0),
|
|
397
|
-
topo_order: query.topo_order
|
|
398
|
-
include_merges: query.include_merges
|
|
404
|
+
topo_order: asOptionalBoolean(query.topo_order),
|
|
405
|
+
include_merges: asOptionalBoolean(query.include_merges),
|
|
399
406
|
author_contains: asTrimmedString(query.author_contains) || undefined,
|
|
400
407
|
since_utc: asTrimmedString(query.since_utc) || undefined,
|
|
401
408
|
until_utc: asTrimmedString(query.until_utc) || undefined,
|
|
@@ -463,6 +470,15 @@ export class GitVcsDelegates extends VcsDelegateBase<GitRuntimeDependencies> {
|
|
|
463
470
|
return null;
|
|
464
471
|
}
|
|
465
472
|
|
|
473
|
+
override stagePaths(
|
|
474
|
+
params: OpenVcs.VcsStagePathsParams,
|
|
475
|
+
_context: PluginRuntimeContext,
|
|
476
|
+
): null {
|
|
477
|
+
const git = this.requireGit(params.session_id);
|
|
478
|
+
git.stagePaths(asStringArray(params.paths));
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
|
|
466
482
|
override discardPaths(
|
|
467
483
|
params: OpenVcs.VcsDiscardPathsParams,
|
|
468
484
|
_context: PluginRuntimeContext,
|
|
@@ -479,23 +495,38 @@ export class GitVcsDelegates extends VcsDelegateBase<GitRuntimeDependencies> {
|
|
|
479
495
|
);
|
|
480
496
|
const discardPlan = planDiscardPaths(status.stdout);
|
|
481
497
|
|
|
498
|
+
let failure: unknown;
|
|
499
|
+
|
|
482
500
|
if (discardPlan.unstageThenRemove.length > 0) {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
501
|
+
try {
|
|
502
|
+
git.runChecked(
|
|
503
|
+
['rm', '-f', '--cached', '--', ...discardPlan.unstageThenRemove],
|
|
504
|
+
'git-discard-paths-failed',
|
|
505
|
+
);
|
|
506
|
+
} catch (error) {
|
|
507
|
+
failure = error;
|
|
508
|
+
}
|
|
487
509
|
}
|
|
488
510
|
|
|
489
|
-
if (discardPlan.restore.length > 0) {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
511
|
+
if (!failure && discardPlan.restore.length > 0) {
|
|
512
|
+
try {
|
|
513
|
+
git.runChecked(
|
|
514
|
+
['restore', '--source=HEAD', '--staged', '--worktree', '--', ...discardPlan.restore],
|
|
515
|
+
'git-discard-paths-failed',
|
|
516
|
+
);
|
|
517
|
+
} catch (error) {
|
|
518
|
+
failure = error;
|
|
519
|
+
}
|
|
494
520
|
}
|
|
495
521
|
|
|
496
|
-
if (discardPlan.clean.length > 0) {
|
|
522
|
+
if (!failure && discardPlan.clean.length > 0) {
|
|
497
523
|
git.runChecked(['clean', '-f', '--', ...discardPlan.clean], 'git-discard-paths-failed');
|
|
498
524
|
}
|
|
525
|
+
|
|
526
|
+
if (failure) {
|
|
527
|
+
throw failure;
|
|
528
|
+
}
|
|
529
|
+
|
|
499
530
|
return null;
|
|
500
531
|
}
|
|
501
532
|
|
package/src/plugin.ts
CHANGED
|
@@ -58,7 +58,7 @@ export function OnPluginStart(): void {
|
|
|
58
58
|
const delegates = new GitVcsDelegates(createGitRuntimeDependencies());
|
|
59
59
|
PluginDefinition.vcs = delegates.toDelegates();
|
|
60
60
|
|
|
61
|
-
const repoMenu = getOrCreateMenu('repository', 'Repository');
|
|
61
|
+
const repoMenu = getOrCreateMenu('repository', 'Repository', { surface: 'menubar' });
|
|
62
62
|
if (repoMenu) {
|
|
63
63
|
repoMenu.addItem({ label: 'Edit .gitignore', action: 'repo-edit-gitignore' });
|
|
64
64
|
repoMenu.addItem({ label: 'Edit .gitattributes', action: 'repo-edit-gitattributes' });
|
package/src/submodules.ts
CHANGED
|
@@ -26,7 +26,7 @@ function payloadString(payload: ModalActionPayload, key: string): string {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/** Builds one modal row for a submodule entry. */
|
|
29
|
-
function buildSubmoduleRow(entry: SubmoduleEntry) {
|
|
29
|
+
export function buildSubmoduleRow(entry: SubmoduleEntry) {
|
|
30
30
|
const metaBits = [entry.commit ? `commit ${entry.commit}` : '', entry.branch ? `branch ${entry.branch}` : '']
|
|
31
31
|
.map((part) => String(part || '').trim())
|
|
32
32
|
.filter(Boolean);
|
|
@@ -62,13 +62,52 @@ function buildSubmoduleRow(entry: SubmoduleEntry) {
|
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/** Builds an error fallback modal with a descriptive message. */
|
|
66
|
+
function buildErrorModal(message: string): ModalBuilder {
|
|
67
|
+
return new ModalBuilder('Error').text(message).text('Please try again or check the Git repository state.');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Opens a fallback modal for a submodule UI failure and preserves the original error on fallback failure. */
|
|
71
|
+
export async function handleSubmoduleModalError(
|
|
72
|
+
prefix: string,
|
|
73
|
+
error: unknown,
|
|
74
|
+
openFallback: (message: string) => Promise<unknown> = (message) => buildErrorModal(message).open(),
|
|
75
|
+
): Promise<unknown> {
|
|
76
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
77
|
+
const fullMessage = `${prefix}: ${message}`;
|
|
78
|
+
console.error(`Git submodules: ${fullMessage}`);
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
return await openFallback(fullMessage);
|
|
82
|
+
} catch {
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
65
87
|
/** Builds and opens the submodule manager modal. */
|
|
66
88
|
async function openSubmodulesModal(): Promise<unknown> {
|
|
67
89
|
const git = createGitCommand();
|
|
68
|
-
|
|
90
|
+
|
|
91
|
+
let entries: SubmoduleEntry[];
|
|
92
|
+
try {
|
|
93
|
+
entries = git.listSubmodules();
|
|
94
|
+
} catch (error) {
|
|
95
|
+
return handleSubmoduleModalError('failed to list submodules', error);
|
|
96
|
+
}
|
|
97
|
+
|
|
69
98
|
console.log('Git submodules: building modal', { count: entries.length });
|
|
70
99
|
|
|
71
|
-
const modal =
|
|
100
|
+
const modal = buildSubmodulesModal(entries);
|
|
101
|
+
try {
|
|
102
|
+
return await modal.open();
|
|
103
|
+
} catch (error) {
|
|
104
|
+
return handleSubmoduleModalError('failed to open modal', error);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Builds the submodule manager modal with given entries. */
|
|
109
|
+
function buildSubmodulesModal(entries: SubmoduleEntry[]): ModalBuilder {
|
|
110
|
+
return new ModalBuilder('Manage Submodules')
|
|
72
111
|
.text('Review, add, update, sync, and remove submodules without leaving Git.')
|
|
73
112
|
.text('Use Update Remote to follow each submodule branch configured in .gitmodules.')
|
|
74
113
|
.separator()
|
|
@@ -130,14 +169,13 @@ async function openSubmodulesModal(): Promise<unknown> {
|
|
|
130
169
|
emptyText: 'This repository has no submodules yet.',
|
|
131
170
|
items: entries.map((entry) => buildSubmoduleRow(entry)),
|
|
132
171
|
});
|
|
133
|
-
|
|
134
|
-
return modal.open();
|
|
135
172
|
}
|
|
136
173
|
|
|
137
174
|
/** Builds and opens the remove confirmation modal for one submodule. */
|
|
138
175
|
async function openRemoveConfirmationModal(path: string, name?: string): Promise<unknown> {
|
|
139
176
|
const submodulePath = String(path || '').trim();
|
|
140
177
|
if (!submodulePath) return;
|
|
178
|
+
|
|
141
179
|
console.log('Git submodules: building remove confirmation', { path: submodulePath, name });
|
|
142
180
|
|
|
143
181
|
const modal = new ModalBuilder('Confirm Submodule Removal')
|
|
@@ -156,7 +194,11 @@ async function openRemoveConfirmationModal(path: string, name?: string): Promise
|
|
|
156
194
|
{ gap: '.5rem', align: 'centered', wrap: true },
|
|
157
195
|
);
|
|
158
196
|
|
|
159
|
-
|
|
197
|
+
try {
|
|
198
|
+
return await modal.open();
|
|
199
|
+
} catch (error) {
|
|
200
|
+
return handleSubmoduleModalError('failed to open remove confirmation modal', error);
|
|
201
|
+
}
|
|
160
202
|
}
|
|
161
203
|
|
|
162
204
|
/** Removes one submodule and refreshes the toolkit modal. */
|
|
@@ -234,7 +276,7 @@ async function syncAllSubmodules(): Promise<void> {
|
|
|
234
276
|
|
|
235
277
|
/** Registers the Git submodule toolkit menu and action handlers. */
|
|
236
278
|
export function registerSubmoduleToolkit(): void {
|
|
237
|
-
const repoMenu = getOrCreateMenu('repository', 'Repository');
|
|
279
|
+
const repoMenu = getOrCreateMenu('repository', 'Repository', { surface: 'menubar' });
|
|
238
280
|
repoMenu?.addItem({ label: 'Submodules', action: 'repo-submodules' });
|
|
239
281
|
|
|
240
282
|
registerAction('repo-submodules', async () => {
|