@openvcs/git-plugin 0.1.0 → 0.2.0-edge.20260426.39
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/ARCHITECTURE.md +8 -0
- package/README.md +12 -1
- package/bin/git.js +64 -27
- package/bin/plugin-helpers.js +44 -0
- package/bin/plugin-request-handler.js +97 -11
- package/bin/plugin.js +1 -1
- package/bin/submodules.js +123 -40
- package/package.json +11 -3
- package/src/git.ts +79 -29
- package/src/plugin-helpers.ts +58 -0
- package/src/plugin-request-handler.ts +141 -10
- package/src/plugin.ts +1 -1
- package/src/submodules.ts +146 -42
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);
|
|
@@ -34,6 +34,12 @@ function buildSubmoduleRow(entry) {
|
|
|
34
34
|
description,
|
|
35
35
|
actions: [
|
|
36
36
|
{ type: 'button', id: 'submodules-update', content: 'Update', payload: { path: entry.path } },
|
|
37
|
+
{
|
|
38
|
+
type: 'button',
|
|
39
|
+
id: 'submodules-update-remote',
|
|
40
|
+
content: 'Update Remote',
|
|
41
|
+
payload: { path: entry.path },
|
|
42
|
+
},
|
|
37
43
|
{ type: 'button', id: 'submodules-sync', content: 'Sync', payload: { path: entry.path } },
|
|
38
44
|
{
|
|
39
45
|
type: 'button',
|
|
@@ -45,47 +51,96 @@ function buildSubmoduleRow(entry) {
|
|
|
45
51
|
],
|
|
46
52
|
};
|
|
47
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
|
+
}
|
|
48
70
|
/** Builds and opens the submodule manager modal. */
|
|
49
71
|
async function openSubmodulesModal() {
|
|
50
72
|
const git = createGitCommand();
|
|
51
|
-
|
|
73
|
+
let entries;
|
|
74
|
+
try {
|
|
75
|
+
entries = git.listSubmodules();
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
return handleSubmoduleModalError('failed to list submodules', error);
|
|
79
|
+
}
|
|
52
80
|
console.log('Git submodules: building modal', { count: entries.length });
|
|
53
|
-
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')
|
|
54
92
|
.text('Review, add, update, sync, and remove submodules without leaving Git.')
|
|
93
|
+
.text('Use Update Remote to follow each submodule branch configured in .gitmodules.')
|
|
55
94
|
.separator()
|
|
56
|
-
.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
95
|
+
.verticalBox([
|
|
96
|
+
{
|
|
97
|
+
type: 'input',
|
|
98
|
+
id: 'url',
|
|
99
|
+
label: 'Submodule URL',
|
|
100
|
+
kind: 'url',
|
|
101
|
+
placeholder: 'https://example.com/repo.git',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
type: 'grid',
|
|
105
|
+
columns: 'minmax(0, 1fr) minmax(0, 1fr)',
|
|
106
|
+
gap: '.75rem',
|
|
107
|
+
content: [
|
|
108
|
+
{
|
|
109
|
+
type: 'input',
|
|
110
|
+
id: 'path',
|
|
111
|
+
label: 'Submodule Path',
|
|
112
|
+
placeholder: 'libs/example',
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
type: 'input',
|
|
116
|
+
id: 'name',
|
|
117
|
+
label: 'Submodule Name',
|
|
118
|
+
placeholder: 'example',
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
type: 'input',
|
|
124
|
+
id: 'branch',
|
|
125
|
+
label: 'Branch (optional)',
|
|
126
|
+
placeholder: 'main',
|
|
127
|
+
},
|
|
128
|
+
], { gap: '1rem' })
|
|
129
|
+
.horizontalBox([
|
|
130
|
+
{ type: 'button', id: 'submodules-add', content: 'Add Submodule', variant: 'primary' },
|
|
131
|
+
{ type: 'button', id: 'repo-submodules', content: 'Refresh' },
|
|
132
|
+
], { gap: '.5rem', align: 'centered', wrap: true })
|
|
133
|
+
.horizontalBox([
|
|
134
|
+
{ type: 'button', id: 'submodules-update-all', content: 'Update All (Recursive)' },
|
|
135
|
+
{ type: 'button', id: 'submodules-update-all-remote', content: 'Update All From Branches' },
|
|
136
|
+
{ type: 'button', id: 'submodules-sync-all', content: 'Sync All' },
|
|
137
|
+
], { gap: '.5rem', align: 'centered', wrap: true })
|
|
82
138
|
.separator()
|
|
83
139
|
.list('submodules', {
|
|
84
140
|
label: 'Submodules',
|
|
85
141
|
emptyText: 'This repository has no submodules yet.',
|
|
86
142
|
items: entries.map((entry) => buildSubmoduleRow(entry)),
|
|
87
143
|
});
|
|
88
|
-
return modal.open();
|
|
89
144
|
}
|
|
90
145
|
/** Builds and opens the remove confirmation modal for one submodule. */
|
|
91
146
|
async function openRemoveConfirmationModal(path, name) {
|
|
@@ -95,15 +150,22 @@ async function openRemoveConfirmationModal(path, name) {
|
|
|
95
150
|
console.log('Git submodules: building remove confirmation', { path: submodulePath, name });
|
|
96
151
|
const modal = new ModalBuilder('Confirm Submodule Removal')
|
|
97
152
|
.text(`Remove the submodule at ${submodulePath}? This will deinitialize the submodule, remove it from the index, and delete its working tree entry.`)
|
|
98
|
-
.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
153
|
+
.horizontalBox([
|
|
154
|
+
{ type: 'button', id: 'repo-submodules', content: 'Back' },
|
|
155
|
+
{
|
|
156
|
+
type: 'button',
|
|
157
|
+
id: 'submodules-remove-confirm',
|
|
158
|
+
content: 'Remove Submodule',
|
|
159
|
+
variant: 'danger',
|
|
160
|
+
payload: { path: submodulePath, name: String(name || '').trim() || undefined },
|
|
161
|
+
},
|
|
162
|
+
], { gap: '.5rem', align: 'centered', wrap: true });
|
|
163
|
+
try {
|
|
164
|
+
return await modal.open();
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
return handleSubmoduleModalError('failed to open remove confirmation modal', error);
|
|
168
|
+
}
|
|
107
169
|
}
|
|
108
170
|
/** Removes one submodule and refreshes the toolkit modal. */
|
|
109
171
|
async function removeSubmodule(payload) {
|
|
@@ -136,6 +198,15 @@ async function updateSubmodule(payload) {
|
|
|
136
198
|
git.updateSubmodule(path);
|
|
137
199
|
await openSubmodulesModal();
|
|
138
200
|
}
|
|
201
|
+
/** Updates one submodule from its configured branch and refreshes the toolkit modal. */
|
|
202
|
+
async function updateSubmoduleRemote(payload) {
|
|
203
|
+
const path = payloadString(payload, 'path');
|
|
204
|
+
if (!path)
|
|
205
|
+
return;
|
|
206
|
+
const git = createGitCommand();
|
|
207
|
+
git.updateSubmoduleRemote(path);
|
|
208
|
+
await openSubmodulesModal();
|
|
209
|
+
}
|
|
139
210
|
/** Syncs one submodule and refreshes the toolkit modal. */
|
|
140
211
|
async function syncSubmodule(payload) {
|
|
141
212
|
const path = payloadString(payload, 'path');
|
|
@@ -151,6 +222,12 @@ async function updateAllSubmodules() {
|
|
|
151
222
|
git.updateAllSubmodules();
|
|
152
223
|
await openSubmodulesModal();
|
|
153
224
|
}
|
|
225
|
+
/** Updates all submodules from their configured branches and refreshes the toolkit modal. */
|
|
226
|
+
async function updateAllSubmodulesRemote() {
|
|
227
|
+
const git = createGitCommand();
|
|
228
|
+
git.updateAllSubmodulesRemote();
|
|
229
|
+
await openSubmodulesModal();
|
|
230
|
+
}
|
|
154
231
|
/** Syncs all submodules recursively and refreshes the toolkit modal. */
|
|
155
232
|
async function syncAllSubmodules() {
|
|
156
233
|
const git = createGitCommand();
|
|
@@ -159,7 +236,7 @@ async function syncAllSubmodules() {
|
|
|
159
236
|
}
|
|
160
237
|
/** Registers the Git submodule toolkit menu and action handlers. */
|
|
161
238
|
export function registerSubmoduleToolkit() {
|
|
162
|
-
const repoMenu = getOrCreateMenu('repository', 'Repository');
|
|
239
|
+
const repoMenu = getOrCreateMenu('repository', 'Repository', { surface: 'menubar' });
|
|
163
240
|
repoMenu?.addItem({ label: 'Submodules', action: 'repo-submodules' });
|
|
164
241
|
registerAction('repo-submodules', async () => {
|
|
165
242
|
console.log('Git submodules: repo-submodules action invoked');
|
|
@@ -171,12 +248,18 @@ export function registerSubmoduleToolkit() {
|
|
|
171
248
|
registerAction('submodules-update-all', async () => {
|
|
172
249
|
return updateAllSubmodules();
|
|
173
250
|
});
|
|
251
|
+
registerAction('submodules-update-all-remote', async () => {
|
|
252
|
+
return updateAllSubmodulesRemote();
|
|
253
|
+
});
|
|
174
254
|
registerAction('submodules-sync-all', async () => {
|
|
175
255
|
return syncAllSubmodules();
|
|
176
256
|
});
|
|
177
257
|
registerAction('submodules-update', async (payload) => {
|
|
178
258
|
return updateSubmodule(asPayload(payload));
|
|
179
259
|
});
|
|
260
|
+
registerAction('submodules-update-remote', async (payload) => {
|
|
261
|
+
return updateSubmoduleRemote(asPayload(payload));
|
|
262
|
+
});
|
|
180
263
|
registerAction('submodules-sync', async (payload) => {
|
|
181
264
|
return syncSubmodule(asPayload(payload));
|
|
182
265
|
});
|
package/package.json
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openvcs/git-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-edge.20260426.39",
|
|
4
4
|
"description": "OpenVCS Git plugin - Node.js runtime",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
|
+
"homepage": "https://github.com/Open-VCS/OpenVCS-Plugin-Git",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Open-VCS/OpenVCS-Plugin-Git.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/Open-VCS/OpenVCS-Plugin-Git/issues"
|
|
13
|
+
},
|
|
6
14
|
"type": "module",
|
|
7
15
|
"engines": {
|
|
8
16
|
"node": ">=18"
|
|
@@ -10,7 +18,7 @@
|
|
|
10
18
|
"openvcs": {
|
|
11
19
|
"id": "openvcs.git",
|
|
12
20
|
"name": "Git",
|
|
13
|
-
"version": "0.
|
|
21
|
+
"version": "0.2.0-edge.20260426.39",
|
|
14
22
|
"author": "OpenVCS Contributors",
|
|
15
23
|
"description": "Git VCS backend plugin for OpenVCS",
|
|
16
24
|
"default_enabled": true,
|
|
@@ -29,7 +37,7 @@
|
|
|
29
37
|
"build": "node ./node_modules/@openvcs/sdk/bin/openvcs.js build"
|
|
30
38
|
},
|
|
31
39
|
"dependencies": {
|
|
32
|
-
"@openvcs/sdk": "
|
|
40
|
+
"@openvcs/sdk": "edge"
|
|
33
41
|
},
|
|
34
42
|
"devDependencies": {
|
|
35
43
|
"@types/node": "^25.5.0",
|
package/src/git.ts
CHANGED
|
@@ -14,10 +14,12 @@ import type {
|
|
|
14
14
|
} from '@openvcs/sdk/types';
|
|
15
15
|
import type { GitCommandResult, RunGitOptions } from './plugin-types.js';
|
|
16
16
|
import {
|
|
17
|
+
applySubmoduleStatusHints,
|
|
17
18
|
asString,
|
|
18
19
|
buildFetchArgs,
|
|
19
20
|
buildPullFfOnlyArgs,
|
|
20
21
|
buildPushArgs,
|
|
22
|
+
buildSubmoduleUpdateArgs,
|
|
21
23
|
parseCommits,
|
|
22
24
|
parseStatusOutput,
|
|
23
25
|
} from './plugin-helpers.js';
|
|
@@ -148,7 +150,7 @@ export class GitCommand {
|
|
|
148
150
|
|
|
149
151
|
status(): StatusParseResult & { exitCode: number } {
|
|
150
152
|
const result = this.run(['status', '--porcelain=1', '--branch', '-z', '-uall']);
|
|
151
|
-
const parsed = parseStatusOutput(result.stdout);
|
|
153
|
+
const parsed = applySubmoduleStatusHints(parseStatusOutput(result.stdout), this.listSubmodulePaths());
|
|
152
154
|
return { ...parsed, exitCode: result.status };
|
|
153
155
|
}
|
|
154
156
|
|
|
@@ -272,31 +274,49 @@ export class GitCommand {
|
|
|
272
274
|
return this.runChecked(args, 'git-pull-failed');
|
|
273
275
|
}
|
|
274
276
|
|
|
275
|
-
commit
|
|
276
|
-
|
|
277
|
+
/** Returns the current HEAD commit id. */
|
|
278
|
+
currentHead(): string {
|
|
279
|
+
return this.runChecked(['rev-parse', 'HEAD'], 'git-head-failed').stdout.trim();
|
|
277
280
|
}
|
|
278
281
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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 {
|
|
288
298
|
const commitMessage = message || 'Stage changes';
|
|
289
|
-
|
|
299
|
+
|
|
290
300
|
const execArgs = [
|
|
291
301
|
...(name ? ['-c', `user.name=${name}`] : []),
|
|
292
302
|
...(email ? ['-c', `user.email=${email}`] : []),
|
|
293
|
-
|
|
294
|
-
'-m',
|
|
303
|
+
'commit',
|
|
304
|
+
'-m',
|
|
305
|
+
commitMessage,
|
|
295
306
|
];
|
|
296
|
-
|
|
307
|
+
|
|
297
308
|
return this.runChecked(execArgs, 'git-commit-failed');
|
|
298
309
|
}
|
|
299
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
|
+
|
|
300
320
|
listCommits(options: ListCommitsOptions = {}): { commits: CommitEntry[]; exitCode: number } {
|
|
301
321
|
const args = ['log', '--all'];
|
|
302
322
|
|
|
@@ -328,7 +348,7 @@ export class GitCommand {
|
|
|
328
348
|
args.push(`--until=${options.until_utc}`);
|
|
329
349
|
}
|
|
330
350
|
|
|
331
|
-
args.push('--pretty=format:%H%
|
|
351
|
+
args.push('--pretty=format:%H%x00%s%x00%aN%x00%aI%x00%P%x1e');
|
|
332
352
|
|
|
333
353
|
if (options.branch) {
|
|
334
354
|
args.push(options.branch);
|
|
@@ -343,9 +363,13 @@ export class GitCommand {
|
|
|
343
363
|
return { commits, exitCode: result.status };
|
|
344
364
|
}
|
|
345
365
|
|
|
346
|
-
|
|
366
|
+
/** Reads `.gitmodules` entries indexed by submodule name and path. */
|
|
367
|
+
private readSubmoduleConfig(): {
|
|
368
|
+
byName: Map<string, { name: string; path?: string; url?: string; branch?: string }>;
|
|
369
|
+
byPath: Map<string, { name: string; url?: string; branch?: string }>;
|
|
370
|
+
} {
|
|
347
371
|
const configResult = this.run(['config', '-f', '.gitmodules', '--null', '--list']);
|
|
348
|
-
const
|
|
372
|
+
const byName = new Map<string, { name: string; path?: string; url?: string; branch?: string }>();
|
|
349
373
|
|
|
350
374
|
if (configResult.status === 0) {
|
|
351
375
|
for (const entry of configResult.stdout.split('\0')) {
|
|
@@ -361,23 +385,34 @@ export class GitCommand {
|
|
|
361
385
|
if (!match) continue;
|
|
362
386
|
|
|
363
387
|
const [, name, field] = match;
|
|
364
|
-
const target =
|
|
388
|
+
const target = byName.get(name) || { name };
|
|
365
389
|
|
|
366
390
|
if (field === 'path') target.path = value;
|
|
367
391
|
if (field === 'url') target.url = value;
|
|
368
392
|
if (field === 'branch') target.branch = value;
|
|
369
393
|
|
|
370
|
-
|
|
394
|
+
byName.set(name, target);
|
|
371
395
|
}
|
|
372
396
|
}
|
|
373
397
|
|
|
374
|
-
const
|
|
375
|
-
for (const entry of
|
|
398
|
+
const byPath = new Map<string, { name: string; url?: string; branch?: string }>();
|
|
399
|
+
for (const entry of byName.values()) {
|
|
376
400
|
if (entry.path) {
|
|
377
|
-
|
|
401
|
+
byPath.set(entry.path, entry);
|
|
378
402
|
}
|
|
379
403
|
}
|
|
380
404
|
|
|
405
|
+
return { byName, byPath };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** Returns known submodule paths from `.gitmodules`. */
|
|
409
|
+
private listSubmodulePaths(): Set<string> {
|
|
410
|
+
return new Set(this.readSubmoduleConfig().byPath.keys());
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
listSubmodules(): SubmoduleEntry[] {
|
|
414
|
+
const { byPath: configByPath } = this.readSubmoduleConfig();
|
|
415
|
+
|
|
381
416
|
const statusResult = this.run(['submodule', 'status', '--recursive']);
|
|
382
417
|
if (statusResult.status !== 0) {
|
|
383
418
|
return [];
|
|
@@ -423,11 +458,21 @@ export class GitCommand {
|
|
|
423
458
|
}
|
|
424
459
|
|
|
425
460
|
updateSubmodule(path: string): GitCommandResult {
|
|
426
|
-
return this.runChecked(
|
|
461
|
+
return this.runChecked(buildSubmoduleUpdateArgs({ path }), 'git-submodule-update-failed');
|
|
427
462
|
}
|
|
428
463
|
|
|
429
464
|
updateAllSubmodules(): GitCommandResult {
|
|
430
|
-
return this.runChecked(
|
|
465
|
+
return this.runChecked(buildSubmoduleUpdateArgs({}), 'git-submodule-update-failed');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** Updates one submodule from its configured branch recursively. */
|
|
469
|
+
updateSubmoduleRemote(path: string): GitCommandResult {
|
|
470
|
+
return this.runChecked(buildSubmoduleUpdateArgs({ path, remote: true }), 'git-submodule-update-remote-failed');
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/** Updates all submodules from their configured branches recursively. */
|
|
474
|
+
updateAllSubmodulesRemote(): GitCommandResult {
|
|
475
|
+
return this.runChecked(buildSubmoduleUpdateArgs({ remote: true }), 'git-submodule-update-remote-failed');
|
|
431
476
|
}
|
|
432
477
|
|
|
433
478
|
syncSubmodule(path: string): GitCommandResult {
|
|
@@ -498,8 +543,11 @@ export class GitCommand {
|
|
|
498
543
|
return this.run(['hash-object', '-w', '--stdin'], { stdin: content }).stdout.trim();
|
|
499
544
|
}
|
|
500
545
|
|
|
546
|
+
/** Stages a textual patch into the index without requiring worktree/index parity. */
|
|
501
547
|
stagePatch(patch: string): void {
|
|
502
|
-
this.runChecked(['apply', '--
|
|
548
|
+
this.runChecked(['apply', '--cached', '--unidiff-zero'], 'git-stage-patch-failed', {
|
|
549
|
+
stdin: patch,
|
|
550
|
+
});
|
|
503
551
|
}
|
|
504
552
|
|
|
505
553
|
applyReversePatch(patch: string): void {
|
|
@@ -514,9 +562,10 @@ export class GitCommand {
|
|
|
514
562
|
this.runChecked(['reset', '--soft', ref], 'git-reset-soft-failed');
|
|
515
563
|
}
|
|
516
564
|
|
|
565
|
+
/** Reads the effective Git commit identity from config. */
|
|
517
566
|
getIdentity(): { name: string; email: string } | null {
|
|
518
|
-
const nameResult = this.run(['config', '--
|
|
519
|
-
const emailResult = this.run(['config', '--
|
|
567
|
+
const nameResult = this.run(['config', '--get', 'user.name']);
|
|
568
|
+
const emailResult = this.run(['config', '--get', 'user.email']);
|
|
520
569
|
|
|
521
570
|
if (nameResult.status !== 0 || emailResult.status !== 0) {
|
|
522
571
|
return null;
|
|
@@ -528,6 +577,7 @@ export class GitCommand {
|
|
|
528
577
|
};
|
|
529
578
|
}
|
|
530
579
|
|
|
580
|
+
/** Stores repository-local commit identity in Git config. */
|
|
531
581
|
setIdentityLocal(name: string, email: string): void {
|
|
532
582
|
this.runChecked(['config', '--local', 'user.name', name], 'git-identity-set-failed');
|
|
533
583
|
this.runChecked(['config', '--local', 'user.email', email], 'git-identity-set-failed');
|
package/src/plugin-helpers.ts
CHANGED
|
@@ -74,6 +74,14 @@ export function buildPushArgs(params: RequestParams): string[] {
|
|
|
74
74
|
return args;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/** Builds `git clone` arguments with recursive submodule initialization enabled. */
|
|
78
|
+
export function buildCloneArgs(params: RequestParams): string[] {
|
|
79
|
+
const args = ['clone', '--recurse-submodules'];
|
|
80
|
+
pushOptionalArg(args, params.url);
|
|
81
|
+
pushOptionalArg(args, params.dest);
|
|
82
|
+
return args;
|
|
83
|
+
}
|
|
84
|
+
|
|
77
85
|
/** Builds `git pull --ff-only` arguments while omitting empty optional values. */
|
|
78
86
|
export function buildPullFfOnlyArgs(params: RequestParams): string[] {
|
|
79
87
|
const args = ['pull', '--ff-only'];
|
|
@@ -82,6 +90,22 @@ export function buildPullFfOnlyArgs(params: RequestParams): string[] {
|
|
|
82
90
|
return args;
|
|
83
91
|
}
|
|
84
92
|
|
|
93
|
+
/** Builds `git submodule update` arguments for pinned or remote-tracking updates. */
|
|
94
|
+
export function buildSubmoduleUpdateArgs(params: RequestParams): string[] {
|
|
95
|
+
const args = ['submodule', 'update', '--init', '--recursive'];
|
|
96
|
+
|
|
97
|
+
if (params.remote === true) {
|
|
98
|
+
args.push('--remote');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const path = asTrimmedString(params.path);
|
|
102
|
+
if (path) {
|
|
103
|
+
args.push('--', path);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return args;
|
|
107
|
+
}
|
|
108
|
+
|
|
85
109
|
/** Parses `git status --porcelain=1 --branch -z -uall` output into OpenVCS payloads. */
|
|
86
110
|
export function parseStatusOutput(output: string): StatusParseResult {
|
|
87
111
|
const records = output.split('\0').filter(Boolean);
|
|
@@ -164,6 +188,40 @@ export function parseStatusOutput(output: string): StatusParseResult {
|
|
|
164
188
|
};
|
|
165
189
|
}
|
|
166
190
|
|
|
191
|
+
/** Applies submodule-specific status labels to known submodule paths. */
|
|
192
|
+
export function applySubmoduleStatusHints(
|
|
193
|
+
parsed: StatusParseResult,
|
|
194
|
+
submodulePaths: Iterable<string>,
|
|
195
|
+
): StatusParseResult {
|
|
196
|
+
const knownPaths = new Set(
|
|
197
|
+
Array.from(submodulePaths)
|
|
198
|
+
.map((entry) => asTrimmedString(entry))
|
|
199
|
+
.filter(Boolean),
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
if (knownPaths.size === 0) {
|
|
203
|
+
return parsed;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
...parsed,
|
|
208
|
+
payload: {
|
|
209
|
+
...parsed.payload,
|
|
210
|
+
files: parsed.payload.files.map((file) => {
|
|
211
|
+
const path = asTrimmedString(file.path);
|
|
212
|
+
if (!knownPaths.has(path)) {
|
|
213
|
+
return file;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
...file,
|
|
218
|
+
status: 'S',
|
|
219
|
+
};
|
|
220
|
+
}),
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
167
225
|
/** Parses `git log` output into commit entries expected by the host. */
|
|
168
226
|
export function parseCommits(raw: string): CommitEntry[] {
|
|
169
227
|
const records = raw
|