@openvcs/git-plugin 0.2.0 → 0.3.0-edge.20260511.66
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 +3 -3
- package/bin/git.js +16 -5
- package/bin/plugin-helpers.js +13 -8
- package/bin/plugin-request-handler.js +16 -3
- package/package.json +13 -4
- package/src/git.ts +16 -5
- package/src/plugin-helpers.ts +14 -10
- package/src/plugin-request-handler.ts +17 -3
package/ARCHITECTURE.md
CHANGED
|
@@ -29,6 +29,14 @@ through `@openvcs/sdk/runtime` delegates and exposes a single VCS backend id:
|
|
|
29
29
|
- For rename and copy records, the porcelain format includes two NUL-terminated
|
|
30
30
|
paths: the original/source path first, then the new/destination path. The
|
|
31
31
|
plugin assigns `path` to the new path and `old_path` to the original path.
|
|
32
|
+
- Unmerged porcelain states such as `UU`, `AA`, and `DD` are normalized to `U`
|
|
33
|
+
in status payloads so the client opens merge-conflict UI instead of a normal
|
|
34
|
+
diff view.
|
|
35
|
+
- File diffs first read worktree changes and fall back to `git diff --cached`
|
|
36
|
+
for staged-only files so selected staged changes still render textual hunks.
|
|
37
|
+
- Commit history uses the current `HEAD` or requested revision instead of
|
|
38
|
+
`git log --all`, so internal refs such as `refs/stash` are not shown as normal
|
|
39
|
+
history entries.
|
|
32
40
|
- Network commands (`fetch`, `push`, `pull`) omit optional arguments (remote,
|
|
33
41
|
refspec, branch) when not provided, allowing Git to use its defaults instead
|
|
34
42
|
of receiving empty string arguments.
|
package/README.md
CHANGED
|
@@ -61,12 +61,12 @@ npm pack
|
|
|
61
61
|
|
|
62
62
|
## Release Channels
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
CI publishes prereleases with these dist-tags:
|
|
65
65
|
|
|
66
|
-
- `latest`: stable releases
|
|
66
|
+
- `latest`: stable releases from `Stable`
|
|
67
67
|
- `beta`: builds from the `Beta` branch
|
|
68
|
+
- `nightly`: scheduled builds from `Dev` when changes exist since the last nightly
|
|
68
69
|
- `edge`: working builds from `Dev` push commits
|
|
69
|
-
- `nightly`: scheduled builds from `Dev` when there are changes since the last nightly
|
|
70
70
|
|
|
71
71
|
Examples:
|
|
72
72
|
|
package/bin/git.js
CHANGED
|
@@ -4,7 +4,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
4
4
|
import { rmSync } from 'node:fs';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { pluginError } from '@openvcs/sdk/runtime';
|
|
7
|
-
import { applySubmoduleStatusHints, asString, buildFetchArgs,
|
|
7
|
+
import { applySubmoduleStatusHints, asString, buildFetchArgs, buildPullArgs, buildPushArgs, buildSubmoduleUpdateArgs, parseCommits, parseStatusOutput, } from './plugin-helpers.js';
|
|
8
8
|
export class GitCommand {
|
|
9
9
|
cwd;
|
|
10
10
|
constructor(cwd) {
|
|
@@ -160,7 +160,7 @@ export class GitCommand {
|
|
|
160
160
|
return this.runChecked(args, 'git-push-failed');
|
|
161
161
|
}
|
|
162
162
|
pull(options = {}) {
|
|
163
|
-
const args =
|
|
163
|
+
const args = buildPullArgs(options);
|
|
164
164
|
return this.runChecked(args, 'git-pull-failed');
|
|
165
165
|
}
|
|
166
166
|
/** Returns the current HEAD commit id. */
|
|
@@ -198,12 +198,18 @@ export class GitCommand {
|
|
|
198
198
|
}
|
|
199
199
|
this.runChecked(['add', '-A', '--', ...paths], 'git-stage-paths-failed');
|
|
200
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* Lists commits from Git with an optional cap.
|
|
203
|
+
*
|
|
204
|
+
* A non-positive limit skips the `-n` flag so callers can request the full
|
|
205
|
+
* history without hard-capping the result set.
|
|
206
|
+
*/
|
|
201
207
|
listCommits(options = {}) {
|
|
202
|
-
const args = ['log'
|
|
208
|
+
const args = ['log'];
|
|
203
209
|
if (options.topo_order) {
|
|
204
210
|
args.push('--topo-order');
|
|
205
211
|
}
|
|
206
|
-
if (options.limit !== undefined) {
|
|
212
|
+
if (options.limit !== undefined && options.limit > 0) {
|
|
207
213
|
args.push(`-${options.limit}`);
|
|
208
214
|
}
|
|
209
215
|
if (options.skip !== undefined) {
|
|
@@ -350,7 +356,12 @@ export class GitCommand {
|
|
|
350
356
|
}
|
|
351
357
|
}
|
|
352
358
|
diffFile(path) {
|
|
353
|
-
|
|
359
|
+
const worktreeDiff = this.runChecked(['diff', '--no-ext-diff', '--', path], 'git-diff-failed')
|
|
360
|
+
.stdout;
|
|
361
|
+
if (worktreeDiff.trim().length > 0)
|
|
362
|
+
return worktreeDiff;
|
|
363
|
+
return this.runChecked(['diff', '--cached', '--no-ext-diff', '--', path], 'git-diff-failed')
|
|
364
|
+
.stdout;
|
|
354
365
|
}
|
|
355
366
|
diffCommit(commit) {
|
|
356
367
|
return this.runChecked(['diff', `${commit}^`, commit], 'git-diff-failed').stdout;
|
package/bin/plugin-helpers.js
CHANGED
|
@@ -59,9 +59,9 @@ export function buildCloneArgs(params) {
|
|
|
59
59
|
pushOptionalArg(args, params.dest);
|
|
60
60
|
return args;
|
|
61
61
|
}
|
|
62
|
-
/** Builds `git pull --
|
|
63
|
-
export function
|
|
64
|
-
const args = ['pull', '--
|
|
62
|
+
/** Builds `git pull --no-rebase --no-edit` arguments while omitting empty optional values. */
|
|
63
|
+
export function buildPullArgs(params) {
|
|
64
|
+
const args = ['pull', '--no-rebase', '--no-edit'];
|
|
65
65
|
pushOptionalArg(args, params.remote);
|
|
66
66
|
pushOptionalArg(args, params.branch);
|
|
67
67
|
return args;
|
|
@@ -83,6 +83,7 @@ export function parseStatusOutput(output) {
|
|
|
83
83
|
const records = output.split('\0').filter(Boolean);
|
|
84
84
|
let ahead = 0;
|
|
85
85
|
let behind = 0;
|
|
86
|
+
let branchOnRemote = false;
|
|
86
87
|
const files = [];
|
|
87
88
|
const summary = {
|
|
88
89
|
untracked: 0,
|
|
@@ -97,6 +98,8 @@ export function parseStatusOutput(output) {
|
|
|
97
98
|
const behindMatch = record.match(/behind\s+(\d+)/);
|
|
98
99
|
ahead = aheadMatch ? Number(aheadMatch[1]) : 0;
|
|
99
100
|
behind = behindMatch ? Number(behindMatch[1]) : 0;
|
|
101
|
+
const trackingMatch = record.match(/^## [^ ]+\.\.\.\S+/);
|
|
102
|
+
branchOnRemote = !!trackingMatch;
|
|
100
103
|
continue;
|
|
101
104
|
}
|
|
102
105
|
if (record.length < 4) {
|
|
@@ -113,14 +116,15 @@ export function parseStatusOutput(output) {
|
|
|
113
116
|
oldPath = payloadPath;
|
|
114
117
|
index += 1;
|
|
115
118
|
}
|
|
119
|
+
const conflicted = x === 'U' ||
|
|
120
|
+
y === 'U' ||
|
|
121
|
+
(x === 'A' && y === 'A') ||
|
|
122
|
+
(x === 'D' && y === 'D');
|
|
116
123
|
const staged = x !== ' ' && x !== '?';
|
|
117
124
|
if (x === '?' || y === '?') {
|
|
118
125
|
summary.untracked += 1;
|
|
119
126
|
}
|
|
120
|
-
else if (
|
|
121
|
-
y === 'U' ||
|
|
122
|
-
(x === 'A' && y === 'A') ||
|
|
123
|
-
(x === 'D' && y === 'D')) {
|
|
127
|
+
else if (conflicted) {
|
|
124
128
|
summary.conflicted += 1;
|
|
125
129
|
}
|
|
126
130
|
else {
|
|
@@ -134,7 +138,7 @@ export function parseStatusOutput(output) {
|
|
|
134
138
|
files.push({
|
|
135
139
|
path,
|
|
136
140
|
old_path: oldPath,
|
|
137
|
-
status: `${x}${y}`.trim() || 'M',
|
|
141
|
+
status: conflicted ? 'U' : `${x}${y}`.trim() || 'M',
|
|
138
142
|
staged,
|
|
139
143
|
resolved_conflict: false,
|
|
140
144
|
hunks: [],
|
|
@@ -146,6 +150,7 @@ export function parseStatusOutput(output) {
|
|
|
146
150
|
files,
|
|
147
151
|
ahead,
|
|
148
152
|
behind,
|
|
153
|
+
branch_on_remote: branchOnRemote,
|
|
149
154
|
},
|
|
150
155
|
};
|
|
151
156
|
}
|
|
@@ -7,6 +7,11 @@ import { GitCommand } from './git.js';
|
|
|
7
7
|
function asOptionalBoolean(value) {
|
|
8
8
|
return typeof value === 'boolean' ? value : undefined;
|
|
9
9
|
}
|
|
10
|
+
/** Splits Git diff stdout into lines without manufacturing a blank entry for empty output. */
|
|
11
|
+
function splitDiffLines(output) {
|
|
12
|
+
const normalized = output.trimEnd();
|
|
13
|
+
return normalized.length > 0 ? normalized.split('\n') : [];
|
|
14
|
+
}
|
|
10
15
|
/** Reduces a file status string to the primary status code needed for discard routing. */
|
|
11
16
|
function getPrimaryDiscardStatus(status) {
|
|
12
17
|
const normalized = asTrimmedString(status);
|
|
@@ -159,7 +164,15 @@ export class GitVcsDelegates extends VcsDelegateBase {
|
|
|
159
164
|
}
|
|
160
165
|
createBranch(params, _context) {
|
|
161
166
|
const git = this.requireGit(params.session_id);
|
|
162
|
-
|
|
167
|
+
const name = asTrimmedString(params.name);
|
|
168
|
+
const checkout = params.checkout === true;
|
|
169
|
+
if (checkout) {
|
|
170
|
+
git.createBranch(name);
|
|
171
|
+
git.checkoutBranch(name);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
git.createBranch(name);
|
|
175
|
+
}
|
|
163
176
|
return null;
|
|
164
177
|
}
|
|
165
178
|
checkoutBranch(params, _context) {
|
|
@@ -252,11 +265,11 @@ export class GitVcsDelegates extends VcsDelegateBase {
|
|
|
252
265
|
}
|
|
253
266
|
diffFile(params, _context) {
|
|
254
267
|
const git = this.requireGit(params.session_id);
|
|
255
|
-
return git.diffFile(asTrimmedString(params.path))
|
|
268
|
+
return splitDiffLines(git.diffFile(asTrimmedString(params.path)));
|
|
256
269
|
}
|
|
257
270
|
diffCommit(params, _context) {
|
|
258
271
|
const git = this.requireGit(params.session_id);
|
|
259
|
-
return git.diffCommit(asTrimmedString(params.rev))
|
|
272
|
+
return splitDiffLines(git.diffCommit(asTrimmedString(params.rev)));
|
|
260
273
|
}
|
|
261
274
|
getConflictDetails(params, _context) {
|
|
262
275
|
const git = this.requireGit(params.session_id);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openvcs/git-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0-edge.20260511.66",
|
|
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",
|
|
@@ -18,14 +18,23 @@
|
|
|
18
18
|
"openvcs": {
|
|
19
19
|
"id": "openvcs.git",
|
|
20
20
|
"name": "Git",
|
|
21
|
-
"version": "0.
|
|
21
|
+
"version": "0.3.0-edge.20260511.66",
|
|
22
22
|
"author": "OpenVCS Contributors",
|
|
23
23
|
"description": "Git VCS backend plugin for OpenVCS",
|
|
24
24
|
"default_enabled": true,
|
|
25
25
|
"module": {
|
|
26
26
|
"exec": "openvcs-git-plugin.js",
|
|
27
27
|
"vcs_backends": [
|
|
28
|
-
|
|
28
|
+
{
|
|
29
|
+
"id": "git",
|
|
30
|
+
"name": "Git",
|
|
31
|
+
"action_labels": {
|
|
32
|
+
"VCS.Commit": "Commit",
|
|
33
|
+
"VCS.Fetch": "Fetch",
|
|
34
|
+
"VCS.Pull": "Pull",
|
|
35
|
+
"VCS.Push": "Push"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
29
38
|
]
|
|
30
39
|
}
|
|
31
40
|
},
|
|
@@ -37,7 +46,7 @@
|
|
|
37
46
|
"build": "node ./node_modules/@openvcs/sdk/bin/openvcs.js build"
|
|
38
47
|
},
|
|
39
48
|
"dependencies": {
|
|
40
|
-
"@openvcs/sdk": "edge"
|
|
49
|
+
"@openvcs/sdk": "^0.3.0-edge.20260506.51"
|
|
41
50
|
},
|
|
42
51
|
"devDependencies": {
|
|
43
52
|
"@types/node": "^25.5.0",
|
package/src/git.ts
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
applySubmoduleStatusHints,
|
|
18
18
|
asString,
|
|
19
19
|
buildFetchArgs,
|
|
20
|
-
|
|
20
|
+
buildPullArgs,
|
|
21
21
|
buildPushArgs,
|
|
22
22
|
buildSubmoduleUpdateArgs,
|
|
23
23
|
parseCommits,
|
|
@@ -270,7 +270,7 @@ export class GitCommand {
|
|
|
270
270
|
}
|
|
271
271
|
|
|
272
272
|
pull(options: PullOptions = {}): GitCommandResult {
|
|
273
|
-
const args =
|
|
273
|
+
const args = buildPullArgs(options as unknown as Record<string, unknown>);
|
|
274
274
|
return this.runChecked(args, 'git-pull-failed');
|
|
275
275
|
}
|
|
276
276
|
|
|
@@ -317,14 +317,20 @@ export class GitCommand {
|
|
|
317
317
|
this.runChecked(['add', '-A', '--', ...paths], 'git-stage-paths-failed');
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Lists commits from Git with an optional cap.
|
|
322
|
+
*
|
|
323
|
+
* A non-positive limit skips the `-n` flag so callers can request the full
|
|
324
|
+
* history without hard-capping the result set.
|
|
325
|
+
*/
|
|
320
326
|
listCommits(options: ListCommitsOptions = {}): { commits: CommitEntry[]; exitCode: number } {
|
|
321
|
-
const args = ['log'
|
|
327
|
+
const args = ['log'];
|
|
322
328
|
|
|
323
329
|
if (options.topo_order) {
|
|
324
330
|
args.push('--topo-order');
|
|
325
331
|
}
|
|
326
332
|
|
|
327
|
-
if (options.limit !== undefined) {
|
|
333
|
+
if (options.limit !== undefined && options.limit > 0) {
|
|
328
334
|
args.push(`-${options.limit}`);
|
|
329
335
|
}
|
|
330
336
|
|
|
@@ -496,7 +502,12 @@ export class GitCommand {
|
|
|
496
502
|
}
|
|
497
503
|
|
|
498
504
|
diffFile(path: string): string {
|
|
499
|
-
|
|
505
|
+
const worktreeDiff = this.runChecked(['diff', '--no-ext-diff', '--', path], 'git-diff-failed')
|
|
506
|
+
.stdout;
|
|
507
|
+
if (worktreeDiff.trim().length > 0) return worktreeDiff;
|
|
508
|
+
|
|
509
|
+
return this.runChecked(['diff', '--cached', '--no-ext-diff', '--', path], 'git-diff-failed')
|
|
510
|
+
.stdout;
|
|
500
511
|
}
|
|
501
512
|
|
|
502
513
|
diffCommit(commit: string): string {
|
package/src/plugin-helpers.ts
CHANGED
|
@@ -82,9 +82,9 @@ export function buildCloneArgs(params: RequestParams): string[] {
|
|
|
82
82
|
return args;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
/** Builds `git pull --
|
|
86
|
-
export function
|
|
87
|
-
const args = ['pull', '--
|
|
85
|
+
/** Builds `git pull --no-rebase --no-edit` arguments while omitting empty optional values. */
|
|
86
|
+
export function buildPullArgs(params: RequestParams): string[] {
|
|
87
|
+
const args = ['pull', '--no-rebase', '--no-edit'];
|
|
88
88
|
pushOptionalArg(args, params.remote);
|
|
89
89
|
pushOptionalArg(args, params.branch);
|
|
90
90
|
return args;
|
|
@@ -111,6 +111,7 @@ export function parseStatusOutput(output: string): StatusParseResult {
|
|
|
111
111
|
const records = output.split('\0').filter(Boolean);
|
|
112
112
|
let ahead = 0;
|
|
113
113
|
let behind = 0;
|
|
114
|
+
let branchOnRemote = false;
|
|
114
115
|
const files: StatusFileEntry[] = [];
|
|
115
116
|
const summary: StatusSummary = {
|
|
116
117
|
untracked: 0,
|
|
@@ -127,6 +128,8 @@ export function parseStatusOutput(output: string): StatusParseResult {
|
|
|
127
128
|
const behindMatch = record.match(/behind\s+(\d+)/);
|
|
128
129
|
ahead = aheadMatch ? Number(aheadMatch[1]) : 0;
|
|
129
130
|
behind = behindMatch ? Number(behindMatch[1]) : 0;
|
|
131
|
+
const trackingMatch = record.match(/^## [^ ]+\.\.\.\S+/);
|
|
132
|
+
branchOnRemote = !!trackingMatch;
|
|
130
133
|
continue;
|
|
131
134
|
}
|
|
132
135
|
|
|
@@ -147,16 +150,16 @@ export function parseStatusOutput(output: string): StatusParseResult {
|
|
|
147
150
|
index += 1;
|
|
148
151
|
}
|
|
149
152
|
|
|
153
|
+
const conflicted =
|
|
154
|
+
x === 'U' ||
|
|
155
|
+
y === 'U' ||
|
|
156
|
+
(x === 'A' && y === 'A') ||
|
|
157
|
+
(x === 'D' && y === 'D');
|
|
150
158
|
const staged = x !== ' ' && x !== '?';
|
|
151
159
|
|
|
152
160
|
if (x === '?' || y === '?') {
|
|
153
161
|
summary.untracked += 1;
|
|
154
|
-
} else if (
|
|
155
|
-
x === 'U' ||
|
|
156
|
-
y === 'U' ||
|
|
157
|
-
(x === 'A' && y === 'A') ||
|
|
158
|
-
(x === 'D' && y === 'D')
|
|
159
|
-
) {
|
|
162
|
+
} else if (conflicted) {
|
|
160
163
|
summary.conflicted += 1;
|
|
161
164
|
} else {
|
|
162
165
|
if (staged) {
|
|
@@ -171,7 +174,7 @@ export function parseStatusOutput(output: string): StatusParseResult {
|
|
|
171
174
|
files.push({
|
|
172
175
|
path,
|
|
173
176
|
old_path: oldPath,
|
|
174
|
-
status: `${x}${y}`.trim() || 'M',
|
|
177
|
+
status: conflicted ? 'U' : `${x}${y}`.trim() || 'M',
|
|
175
178
|
staged,
|
|
176
179
|
resolved_conflict: false,
|
|
177
180
|
hunks: [],
|
|
@@ -184,6 +187,7 @@ export function parseStatusOutput(output: string): StatusParseResult {
|
|
|
184
187
|
files,
|
|
185
188
|
ahead,
|
|
186
189
|
behind,
|
|
190
|
+
branch_on_remote: branchOnRemote,
|
|
187
191
|
},
|
|
188
192
|
};
|
|
189
193
|
}
|
|
@@ -35,6 +35,12 @@ function asOptionalBoolean(value: unknown): boolean | undefined {
|
|
|
35
35
|
return typeof value === 'boolean' ? value : undefined;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/** Splits Git diff stdout into lines without manufacturing a blank entry for empty output. */
|
|
39
|
+
function splitDiffLines(output: string): string[] {
|
|
40
|
+
const normalized = output.trimEnd();
|
|
41
|
+
return normalized.length > 0 ? normalized.split('\n') : [];
|
|
42
|
+
}
|
|
43
|
+
|
|
38
44
|
/** Reduces a file status string to the primary status code needed for discard routing. */
|
|
39
45
|
function getPrimaryDiscardStatus(status: string): string {
|
|
40
46
|
const normalized = asTrimmedString(status);
|
|
@@ -261,7 +267,15 @@ export class GitVcsDelegates extends VcsDelegateBase<GitRuntimeDependencies> {
|
|
|
261
267
|
_context: PluginRuntimeContext,
|
|
262
268
|
): null {
|
|
263
269
|
const git = this.requireGit(params.session_id);
|
|
264
|
-
|
|
270
|
+
const name = asTrimmedString(params.name);
|
|
271
|
+
const checkout = params.checkout === true;
|
|
272
|
+
|
|
273
|
+
if (checkout) {
|
|
274
|
+
git.createBranch(name);
|
|
275
|
+
git.checkoutBranch(name);
|
|
276
|
+
} else {
|
|
277
|
+
git.createBranch(name);
|
|
278
|
+
}
|
|
265
279
|
return null;
|
|
266
280
|
}
|
|
267
281
|
|
|
@@ -416,7 +430,7 @@ export class GitVcsDelegates extends VcsDelegateBase<GitRuntimeDependencies> {
|
|
|
416
430
|
_context: PluginRuntimeContext,
|
|
417
431
|
): string[] {
|
|
418
432
|
const git = this.requireGit(params.session_id);
|
|
419
|
-
return git.diffFile(asTrimmedString(params.path))
|
|
433
|
+
return splitDiffLines(git.diffFile(asTrimmedString(params.path)));
|
|
420
434
|
}
|
|
421
435
|
|
|
422
436
|
override diffCommit(
|
|
@@ -424,7 +438,7 @@ export class GitVcsDelegates extends VcsDelegateBase<GitRuntimeDependencies> {
|
|
|
424
438
|
_context: PluginRuntimeContext,
|
|
425
439
|
): string[] {
|
|
426
440
|
const git = this.requireGit(params.session_id);
|
|
427
|
-
return git.diffCommit(asTrimmedString(params.rev))
|
|
441
|
+
return splitDiffLines(git.diffCommit(asTrimmedString(params.rev)));
|
|
428
442
|
}
|
|
429
443
|
|
|
430
444
|
override getConflictDetails(
|