@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 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
- The npm package can be consumed from prerelease channels published by CI:
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, buildPullFfOnlyArgs, buildPushArgs, buildSubmoduleUpdateArgs, parseCommits, parseStatusOutput, } from './plugin-helpers.js';
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 = buildPullFfOnlyArgs(options);
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', '--all'];
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
- return this.runChecked(['diff', '--no-ext-diff', '--', path], 'git-diff-failed').stdout;
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;
@@ -59,9 +59,9 @@ export function buildCloneArgs(params) {
59
59
  pushOptionalArg(args, params.dest);
60
60
  return args;
61
61
  }
62
- /** Builds `git pull --ff-only` arguments while omitting empty optional values. */
63
- export function buildPullFfOnlyArgs(params) {
64
- const args = ['pull', '--ff-only'];
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 (x === 'U' ||
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
- git.createBranch(asTrimmedString(params.name));
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)).split('\n');
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)).split('\n');
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.2.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.2.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
- "git"
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
- buildPullFfOnlyArgs,
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 = buildPullFfOnlyArgs(options as unknown as Record<string, unknown>);
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', '--all'];
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
- return this.runChecked(['diff', '--no-ext-diff', '--', path], 'git-diff-failed').stdout;
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 {
@@ -82,9 +82,9 @@ export function buildCloneArgs(params: RequestParams): string[] {
82
82
  return args;
83
83
  }
84
84
 
85
- /** Builds `git pull --ff-only` arguments while omitting empty optional values. */
86
- export function buildPullFfOnlyArgs(params: RequestParams): string[] {
87
- const args = ['pull', '--ff-only'];
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
- git.createBranch(asTrimmedString(params.name));
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)).split('\n');
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)).split('\n');
441
+ return splitDiffLines(git.diffCommit(asTrimmedString(params.rev)));
428
442
  }
429
443
 
430
444
  override getConflictDetails(