@jaydenfyi/diffx 0.0.1 → 0.0.3

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 CHANGED
@@ -38,13 +38,13 @@ diffx --name-status
38
38
 
39
39
  ## `diffx` vs `git diff`
40
40
 
41
- | Capability | `diffx` | `git diff` |
42
- | ---------- | ------- | ---------- |
43
- | Full working tree snapshot (tracked + untracked) | ✅ | ❌ |
44
- | Direct GitHub PR and GitLab MR diffing | ✅ | ❌ |
45
- | Cross-remote and fork comparisons | ✅ | ❌ |
46
- | Include/exclude glob filtering | ✅ | ❌ |
47
- | `git diff` compatibility | ✅ | ✅ |
41
+ | Capability | `diffx` | `git diff` |
42
+ | ------------------------------------------------ | ------- | ---------- |
43
+ | Full working tree snapshot (tracked + untracked) | ✅ | ❌ |
44
+ | Direct GitHub PR and GitLab MR diffing | ✅ | ❌ |
45
+ | Cross-remote and fork comparisons | ✅ | ❌ |
46
+ | Include/exclude glob filtering | ✅ | ❌ |
47
+ | `git diff` compatibility | ✅ | ✅ |
48
48
 
49
49
  ## Command
50
50
 
@@ -124,6 +124,13 @@ diffx gitlab:owner/repo@main..feature
124
124
  diffx gitlab:owner/repo!123
125
125
  ```
126
126
 
127
+ ### Two-dot vs three-dot
128
+
129
+ All range formats support both `..` and `...` separators:
130
+
131
+ - `A..B` compares the two tips directly (same as `git diff A B`)
132
+ - `A...B` compares from the merge-base of A and B to B (same as `git diff A...B`)
133
+
127
134
  ## Output Modes
128
135
 
129
136
  `diffx` defaults to `diff` mode.
package/dist/bin.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as handleError, t as diffxCommand } from "./command-5FPIcmTx.mjs";
2
+ import { n as handleError, t as diffxCommand } from "./command-CWAThFBF.mjs";
3
3
  import { cli } from "gunshi";
4
4
 
5
5
  //#region src/bin.ts
@@ -95,25 +95,31 @@ function parsePRRef(input) {
95
95
  };
96
96
  }
97
97
  function parseGithubRefRange(input) {
98
- const result = input.match(/^github:([^/]+)\/([^@]+)@(.+)\.\.(.+)$/i);
98
+ const result = input.match(/^github:([^/]+)\/([^@]+)@(.+)(\.\.\.?)(.+)$/i);
99
99
  if (!result) return null;
100
100
  const owner = result[1];
101
101
  const repo = result[2];
102
102
  const left = result[3].trim();
103
- const right = result[4].trim();
103
+ const separator = result[4];
104
+ const right = result[5].trim();
105
+ const rangeSyntax = separator.length === 3 ? "three-dot" : "two-dot";
104
106
  if (!owner || !repo || !left || !right) return null;
105
107
  return {
106
108
  ownerRepo: `${owner}/${repo}`,
107
109
  left,
108
- right
110
+ right,
111
+ rangeSyntax
109
112
  };
110
113
  }
111
114
  function parsePRRange(input) {
112
115
  if (!input.includes("..")) return null;
113
- const parts = input.split("..");
114
- if (parts.length !== 2) return null;
115
- const left = parseGitHubPRUrl(parts[0].trim()) ?? parsePRRef(parts[0].trim());
116
- const right = parseGitHubPRUrl(parts[1].trim()) ?? parsePRRef(parts[1].trim());
116
+ const separatorMatch = input.match(/\.\.\.|\.\./);
117
+ if (!separatorMatch || separatorMatch.index === void 0) return null;
118
+ const leftStr = input.slice(0, separatorMatch.index).trim();
119
+ const rightStr = input.slice(separatorMatch.index + separatorMatch[0].length).trim();
120
+ if (!leftStr || !rightStr) return null;
121
+ const left = parseGitHubPRUrl(leftStr) ?? parsePRRef(leftStr);
122
+ const right = parseGitHubPRUrl(rightStr) ?? parsePRRef(rightStr);
117
123
  if (!left || !right) return null;
118
124
  return {
119
125
  left,
@@ -125,10 +131,13 @@ function parsePRRange(input) {
125
131
  //#region src/parsers/range/git-url-parser.ts
126
132
  function parseGitUrlRange(input) {
127
133
  const isGitUrl = (s) => s.includes("://") || s.includes("@") && s.includes(":");
128
- const doubleDotIndex = input.indexOf("..");
129
- if (doubleDotIndex !== -1) {
130
- const leftPart = input.slice(0, doubleDotIndex);
131
- const rightPart = input.slice(doubleDotIndex + 2);
134
+ const separatorMatch = input.match(/\.\.\.|\.\./);
135
+ if (separatorMatch && separatorMatch.index !== void 0) {
136
+ const sepIdx = separatorMatch.index;
137
+ const sepLen = separatorMatch[0].length;
138
+ const rangeSyntax = sepLen === 3 ? "three-dot" : "two-dot";
139
+ const leftPart = input.slice(0, sepIdx);
140
+ const rightPart = input.slice(sepIdx + sepLen);
132
141
  const lastAtLeft = leftPart.lastIndexOf("@");
133
142
  const lastAtRight = rightPart.lastIndexOf("@");
134
143
  if (lastAtLeft !== -1 && lastAtRight !== -1) {
@@ -140,20 +149,42 @@ function parseGitUrlRange(input) {
140
149
  leftUrl,
141
150
  leftRef,
142
151
  rightUrl,
143
- rightRef
152
+ rightRef,
153
+ rangeSyntax
144
154
  };
145
155
  }
146
156
  }
147
- const atBeforeDoubleDot = input.lastIndexOf("@", input.indexOf(".."));
148
- if (atBeforeDoubleDot !== -1 && input.includes("..")) {
149
- const url = input.slice(0, atBeforeDoubleDot);
150
- const parts = input.slice(atBeforeDoubleDot + 1).split("..");
151
- if (parts.length === 2 && isGitUrl(url)) return {
152
- leftUrl: url,
153
- leftRef: parts[0],
154
- rightUrl: url,
155
- rightRef: parts[1]
156
- };
157
+ if (separatorMatch && separatorMatch.index !== void 0) {
158
+ const sepIdx = separatorMatch.index;
159
+ const atBeforeSep = input.lastIndexOf("@", sepIdx);
160
+ if (atBeforeSep !== -1) {
161
+ const url = input.slice(0, atBeforeSep);
162
+ const refPart = input.slice(atBeforeSep + 1);
163
+ const rangeSyntax = separatorMatch[0].length === 3 ? "three-dot" : "two-dot";
164
+ if (rangeSyntax === "three-dot") {
165
+ const dotIdx = refPart.indexOf("...");
166
+ if (dotIdx !== -1) {
167
+ const leftRef = refPart.slice(0, dotIdx);
168
+ const rightRef = refPart.slice(dotIdx + 3);
169
+ if (leftRef && rightRef && isGitUrl(url)) return {
170
+ leftUrl: url,
171
+ leftRef,
172
+ rightUrl: url,
173
+ rightRef,
174
+ rangeSyntax
175
+ };
176
+ }
177
+ } else {
178
+ const parts = refPart.split("..");
179
+ if (parts.length === 2 && parts[0] && parts[1] && isGitUrl(url)) return {
180
+ leftUrl: url,
181
+ leftRef: parts[0],
182
+ rightUrl: url,
183
+ rightRef: parts[1],
184
+ rangeSyntax
185
+ };
186
+ }
187
+ }
157
188
  }
158
189
  return null;
159
190
  }
@@ -170,27 +201,32 @@ function parseMRRef(input) {
170
201
  };
171
202
  }
172
203
  function parseGitlabRefRange(input) {
173
- const result = input.match(/^gitlab:([^/]+)\/([^@]+)@(.+)\.\.(.+)$/i);
204
+ const result = input.match(/^gitlab:([^/]+)\/([^@]+)@(.+)(\.\.\.?)(.+)$/i);
174
205
  if (!result) return null;
175
206
  const owner = result[1];
176
207
  const repo = result[2];
177
208
  const left = result[3].trim();
178
- const right = result[4].trim();
209
+ const separator = result[4];
210
+ const right = result[5].trim();
211
+ const rangeSyntax = separator.length === 3 ? "three-dot" : "two-dot";
179
212
  if (!owner || !repo || !left || !right) return null;
180
213
  return {
181
214
  ownerRepo: `${owner}/${repo}`,
182
215
  left,
183
- right
216
+ right,
217
+ rangeSyntax
184
218
  };
185
219
  }
186
220
 
187
221
  //#endregion
188
222
  //#region src/parsers/range/ref-range-parser.ts
189
223
  function parseRemoteRefRange(input) {
190
- const separatorIndex = input.indexOf("..");
191
- if (separatorIndex === -1) return null;
224
+ const separatorMatch = input.match(/\.\.\.|\.\./);
225
+ if (!separatorMatch || separatorMatch.index === void 0) return null;
226
+ const separatorIndex = separatorMatch.index;
227
+ const rangeSyntax = separatorMatch[0].length === 3 ? "three-dot" : "two-dot";
192
228
  const leftPart = input.slice(0, separatorIndex).trim();
193
- const rightPart = input.slice(separatorIndex + 2).trim();
229
+ const rightPart = input.slice(separatorIndex + separatorMatch[0].length).trim();
194
230
  if (!leftPart || !rightPart) return null;
195
231
  const parseRemoteSide = (value) => {
196
232
  const match = value.match(/^([^/]+)\/([^@]+)@(.+)$/);
@@ -213,29 +249,34 @@ function parseRemoteRefRange(input) {
213
249
  return {
214
250
  left: `${left.owner}/${left.repo}@${left.ref}`,
215
251
  right: `${rightFull.owner}/${rightFull.repo}@${rightFull.ref}`,
216
- ownerRepo: `${left.owner}/${left.repo}`
252
+ ownerRepo: `${left.owner}/${left.repo}`,
253
+ rangeSyntax
217
254
  };
218
255
  }
219
256
  return {
220
257
  left: `${left.owner}/${left.repo}@${left.ref}`,
221
258
  right: `${left.owner}/${left.repo}@${rightPart}`,
222
- ownerRepo: `${left.owner}/${left.repo}`
259
+ ownerRepo: `${left.owner}/${left.repo}`,
260
+ rangeSyntax
223
261
  };
224
262
  }
225
263
  function parseLocalRefRange(input) {
226
- const parts = input.split("..");
227
- if (parts.length !== 2 || !parts[0] || !parts[1]) return null;
264
+ const separatorMatch = input.match(/\.\.(?:\.?)/);
265
+ if (!separatorMatch || separatorMatch.index === void 0) return null;
266
+ const separatorIndex = separatorMatch.index;
267
+ const rangeSyntax = separatorMatch[0].length === 3 ? "three-dot" : "two-dot";
268
+ const left = input.slice(0, separatorIndex).trim();
269
+ const right = input.slice(separatorIndex + separatorMatch[0].length).trim();
270
+ if (!left || !right) return null;
228
271
  return {
229
- left: parts[0].trim(),
230
- right: parts[1].trim()
272
+ left,
273
+ right,
274
+ rangeSyntax
231
275
  };
232
276
  }
233
277
 
234
278
  //#endregion
235
279
  //#region src/parsers/range-parser.ts
236
- /**
237
- * Parse a range input string into a RefRange
238
- */
239
280
  function parseRangeInput(input) {
240
281
  const prRange = parsePRRange(input);
241
282
  if (prRange) return {
@@ -243,7 +284,8 @@ function parseRangeInput(input) {
243
284
  left: "",
244
285
  right: "",
245
286
  leftPr: prRange.left,
246
- rightPr: prRange.right
287
+ rightPr: prRange.right,
288
+ rangeSyntax: void 0
247
289
  };
248
290
  const gitUrlRange = parseGitUrlRange(input);
249
291
  if (gitUrlRange) return {
@@ -251,7 +293,8 @@ function parseRangeInput(input) {
251
293
  left: gitUrlRange.leftRef,
252
294
  right: gitUrlRange.rightRef,
253
295
  leftGitUrl: gitUrlRange.leftUrl,
254
- rightGitUrl: gitUrlRange.rightUrl
296
+ rightGitUrl: gitUrlRange.rightUrl,
297
+ rangeSyntax: gitUrlRange.rangeSyntax
255
298
  };
256
299
  const githubCompare = parseGitHubCompareUrl(input);
257
300
  if (githubCompare) return {
@@ -262,7 +305,8 @@ function parseRangeInput(input) {
262
305
  leftRef: githubCompare.leftRef,
263
306
  rightRef: githubCompare.rightRef,
264
307
  rightOwner: githubCompare.rightOwner,
265
- rightRepo: githubCompare.rightRepo
308
+ rightRepo: githubCompare.rightRepo,
309
+ rangeSyntax: void 0
266
310
  };
267
311
  const githubPrChanges = parseGitHubPRChangesUrl(input);
268
312
  if (githubPrChanges) return {
@@ -272,7 +316,8 @@ function parseRangeInput(input) {
272
316
  ownerRepo: `${githubPrChanges.owner}/${githubPrChanges.repo}`,
273
317
  prNumber: githubPrChanges.prNumber,
274
318
  leftCommitSha: githubPrChanges.leftCommitSha,
275
- rightCommitSha: githubPrChanges.rightCommitSha
319
+ rightCommitSha: githubPrChanges.rightCommitSha,
320
+ rangeSyntax: void 0
276
321
  };
277
322
  const githubPr = parseGitHubPRUrl(input);
278
323
  if (githubPr) return {
@@ -280,7 +325,8 @@ function parseRangeInput(input) {
280
325
  left: "",
281
326
  right: "",
282
327
  ownerRepo: `${githubPr.owner}/${githubPr.repo}`,
283
- prNumber: githubPr.prNumber
328
+ prNumber: githubPr.prNumber,
329
+ rangeSyntax: void 0
284
330
  };
285
331
  const githubCommit = parseGitHubCommitUrl(input);
286
332
  if (githubCommit) return {
@@ -288,7 +334,8 @@ function parseRangeInput(input) {
288
334
  left: "",
289
335
  right: "",
290
336
  ownerRepo: `${githubCommit.owner}/${githubCommit.repo}`,
291
- commitSha: githubCommit.commitSha
337
+ commitSha: githubCommit.commitSha,
338
+ rangeSyntax: void 0
292
339
  };
293
340
  const githubRefRange = parseGithubRefRange(input);
294
341
  if (githubRefRange) {
@@ -298,7 +345,8 @@ function parseRangeInput(input) {
298
345
  left: githubRefRange.left,
299
346
  right: githubRefRange.right,
300
347
  leftGitUrl: gitUrl,
301
- rightGitUrl: gitUrl
348
+ rightGitUrl: gitUrl,
349
+ rangeSyntax: githubRefRange.rangeSyntax
302
350
  };
303
351
  }
304
352
  const gitlabRefRange = parseGitlabRefRange(input);
@@ -309,7 +357,8 @@ function parseRangeInput(input) {
309
357
  left: gitlabRefRange.left,
310
358
  right: gitlabRefRange.right,
311
359
  leftGitUrl: gitUrl,
312
- rightGitUrl: gitUrl
360
+ rightGitUrl: gitUrl,
361
+ rangeSyntax: gitlabRefRange.rangeSyntax
313
362
  };
314
363
  }
315
364
  const prRef = parsePRRef(input);
@@ -318,7 +367,8 @@ function parseRangeInput(input) {
318
367
  left: "",
319
368
  right: "",
320
369
  ownerRepo: `${prRef.owner}/${prRef.repo}`,
321
- prNumber: prRef.prNumber
370
+ prNumber: prRef.prNumber,
371
+ rangeSyntax: void 0
322
372
  };
323
373
  const mrRef = parseMRRef(input);
324
374
  if (mrRef) return {
@@ -326,22 +376,25 @@ function parseRangeInput(input) {
326
376
  left: "",
327
377
  right: "",
328
378
  ownerRepo: `${mrRef.owner}/${mrRef.repo}`,
329
- prNumber: mrRef.mrNumber
379
+ prNumber: mrRef.mrNumber,
380
+ rangeSyntax: void 0
330
381
  };
331
382
  const remoteRange = parseRemoteRefRange(input);
332
383
  if (remoteRange) return {
333
384
  type: "remote-range",
334
385
  left: remoteRange.left,
335
386
  right: remoteRange.right,
336
- ownerRepo: remoteRange.ownerRepo
387
+ ownerRepo: remoteRange.ownerRepo,
388
+ rangeSyntax: remoteRange.rangeSyntax
337
389
  };
338
390
  const localRange = parseLocalRefRange(input);
339
391
  if (localRange) return {
340
392
  type: "local-range",
341
393
  left: localRange.left,
342
- right: localRange.right
394
+ right: localRange.right,
395
+ rangeSyntax: localRange.rangeSyntax
343
396
  };
344
- throw new DiffxError(`Invalid range or URL: ${input}\n\nSupported formats:\n - Local refs: main..feature, abc123..def456\n - Remote refs: owner/repo@main..owner/repo@feature\n - Git URL: git@github.com:owner/repo.git@main..feature\n - Git URL (HTTPS): https://github.com/owner/repo.git@main..feature\n - GitHub refs: github:owner/repo@main..feature\n - GitHub PR ref: github:owner/repo#123\n - GitHub PR range: github:owner/repo#123..github:owner/repo#456\n - GitHub PR URL: https://github.com/owner/repo/pull/123\n - PR URL range: https://github.com/owner/repo/pull/123..https://github.com/owner/repo/pull/456\n - GitHub commit URL: https://github.com/owner/repo/commit/abc123\n - GitHub PR changes URL: https://github.com/owner/repo/pull/123/changes/abc123..def456\n - GitHub compare URL: https://github.com/owner/repo/compare/main...feature\n - Cross-fork compare: https://github.com/owner/repo/compare/main...other:repo:feature\n - GitLab refs: gitlab:owner/repo@main..feature\n - GitLab MR ref: gitlab:owner/repo!123`, ExitCode.INVALID_INPUT);
397
+ throw new DiffxError(`Invalid range or URL: ${input}\n\nSupported formats:\n - Local refs: main..feature, main...feature, abc123..def456\n - Remote refs: owner/repo@main..owner/repo@feature\n - Git URL: git@github.com:owner/repo.git@main..feature\n - Git URL (HTTPS): https://github.com/owner/repo.git@main..feature\n - GitHub refs: github:owner/repo@main..feature\n - GitHub PR ref: github:owner/repo#123\n - GitHub PR range: github:owner/repo#123..github:owner/repo#456\n - GitHub PR URL: https://github.com/owner/repo/pull/123\n - PR URL range: https://github.com/owner/repo/pull/123..https://github.com/owner/repo/pull/456\n - GitHub commit URL: https://github.com/owner/repo/commit/abc123\n - GitHub PR changes URL: https://github.com/owner/repo/pull/123/changes/abc123..def456\n - GitHub compare URL: https://github.com/owner/repo/compare/main...feature\n - Cross-fork compare: https://github.com/owner/repo/compare/main...other:repo:feature\n - GitLab refs: gitlab:owner/repo@main..feature\n - GitLab MR ref: gitlab:owner/repo!123`, ExitCode.INVALID_INPUT);
345
398
  }
346
399
 
347
400
  //#endregion
@@ -894,6 +947,10 @@ async function resolveLocalRefs(range) {
894
947
  const rightExists = await gitClient.refExistsAny(right);
895
948
  if (!leftExists) throw new DiffxError(`Left ref does not exist: ${range.left}`, ExitCode.INVALID_INPUT);
896
949
  if (!rightExists) throw new DiffxError(`Right ref does not exist: ${range.right}`, ExitCode.INVALID_INPUT);
950
+ if (range.rangeSyntax === "three-dot") return {
951
+ left: (await gitClient.mergeBase(left, right)).trim(),
952
+ right
953
+ };
897
954
  return {
898
955
  left,
899
956
  right
@@ -920,6 +977,13 @@ async function resolveRemoteRefs(range) {
920
977
  const rightDestRef = `${tempPrefix}/right`;
921
978
  try {
922
979
  await gitClient.fetchFromUrl(remoteUrl, [`${leftRemoteRef}:${leftDestRef}`, `${rightRemoteRef}:${rightDestRef}`], 1);
980
+ if (range.rangeSyntax === "three-dot") return {
981
+ left: (await gitClient.mergeBase(leftDestRef, rightDestRef)).trim(),
982
+ right: rightDestRef,
983
+ cleanup: async () => {
984
+ await gitClient.deleteRefs([leftDestRef, rightDestRef]);
985
+ }
986
+ };
923
987
  return {
924
988
  left: leftDestRef,
925
989
  right: rightDestRef,
@@ -1116,6 +1180,13 @@ async function resolveGitUrlRefs(range) {
1116
1180
  await gitClient.fetchFromUrl(leftUrl, [`${leftRef}:${leftDestRef}`], 1);
1117
1181
  await gitClient.fetchFromUrl(rightUrl, [`${rightRef}:${rightDestRef}`], 1);
1118
1182
  }
1183
+ if (range.rangeSyntax === "three-dot") return {
1184
+ left: (await gitClient.mergeBase(leftDestRef, rightDestRef)).trim(),
1185
+ right: rightDestRef,
1186
+ cleanup: async () => {
1187
+ await gitClient.deleteRefs([leftDestRef, rightDestRef]);
1188
+ }
1189
+ };
1119
1190
  return {
1120
1191
  left: leftDestRef,
1121
1192
  right: rightDestRef,
@@ -2431,4 +2502,4 @@ const diffxCommand = define({
2431
2502
 
2432
2503
  //#endregion
2433
2504
  export { DiffxError as a, parseRangeInput as i, handleError as n, ExitCode as o, resolveRefs as r, diffxCommand as t };
2434
- //# sourceMappingURL=command-5FPIcmTx.mjs.map
2505
+ //# sourceMappingURL=command-CWAThFBF.mjs.map