@shivanshshrivas/gwit 0.2.2 → 0.2.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 +18 -18
- package/dist/index.cjs +146 -53
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
# gwit
|
|
2
|
-
|
|
3
|
-
[](https://www.npmjs.com/package/@shivanshshrivas/gwit)
|
|
4
|
-
[](https://www.npmjs.com/package/@shivanshshrivas/gwit)
|
|
5
|
-
[](https://github.com/shivanshshrivas/gwit/actions/workflows/ci.yml)
|
|
6
|
-
[](https://nodejs.org/)
|
|
7
|
-
[](./LICENSE)
|
|
8
|
-
|
|
9
|
-
Fully isolated git worktrees, in one command.
|
|
1
|
+
# gwit
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@shivanshshrivas/gwit)
|
|
4
|
+
[](https://www.npmjs.com/package/@shivanshshrivas/gwit)
|
|
5
|
+
[](https://github.com/shivanshshrivas/gwit/actions/workflows/ci.yml)
|
|
6
|
+
[](https://nodejs.org/)
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
|
|
9
|
+
Fully isolated git worktrees, in one command.
|
|
10
10
|
|
|
11
11
|
gwit wraps `git worktree` to turn a fresh checkout into a fully working environment - gitignored files copied, unique port assigned, per-worktree env vars injected, and setup scripts run automatically.
|
|
12
12
|
|
|
@@ -125,18 +125,18 @@ gwit sync --back feature/auth # three-way merge worktree -> main
|
|
|
125
125
|
gwit sync --back # auto-detect branch, then merge back
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
-
`--back` compares three versions of each snapshot-tracked file:
|
|
128
|
+
`--back` compares three versions of each snapshot-tracked file:
|
|
129
129
|
|
|
130
130
|
1. **base** - file content captured when the worktree was created
|
|
131
131
|
2. **main** - current file in the main worktree
|
|
132
|
-
3. **worktree** - current file in the linked worktree
|
|
133
|
-
|
|
134
|
-
If both sides changed different text regions, gwit applies a clean merge.
|
|
135
|
-
If both sides changed the same region, gwit writes git-style conflict markers
|
|
136
|
-
(`<<<<<<<`, `=======`, `>>>>>>>`). Binary conflicts are skipped with a warning.
|
|
137
|
-
Files currently matched by `.gwitinclude` but not present in the snapshot
|
|
138
|
-
(for example, new files created later inside an included ignored directory)
|
|
139
|
-
are reverse-copied directly from worktree to main.
|
|
132
|
+
3. **worktree** - current file in the linked worktree
|
|
133
|
+
|
|
134
|
+
If both sides changed different text regions, gwit applies a clean merge.
|
|
135
|
+
If both sides changed the same region, gwit writes git-style conflict markers
|
|
136
|
+
(`<<<<<<<`, `=======`, `>>>>>>>`). Binary conflicts are skipped with a warning.
|
|
137
|
+
Files currently matched by `.gwitinclude` but not present in the snapshot
|
|
138
|
+
(for example, new files created later inside an included ignored directory)
|
|
139
|
+
are reverse-copied directly from worktree to main.
|
|
140
140
|
|
|
141
141
|
### `gwit open <branch>`
|
|
142
142
|
|
package/dist/index.cjs
CHANGED
|
@@ -437,11 +437,67 @@ function expandGlob(pattern, cwd) {
|
|
|
437
437
|
const files = result.stdout.split("\n").filter((f) => f.length > 0);
|
|
438
438
|
return files.filter((f) => (0, import_minimatch.minimatch)(f, pattern, { dot: true }));
|
|
439
439
|
}
|
|
440
|
+
function normalizeRelPath(relPath) {
|
|
441
|
+
return relPath.replace(/\\/g, "/");
|
|
442
|
+
}
|
|
443
|
+
function resolveLiteralEntryFiles(entry, sourcePath) {
|
|
444
|
+
const entryPath = entry.replace(/\/$/, "");
|
|
445
|
+
if (entryPath.length === 0) return [];
|
|
446
|
+
if (path2.isAbsolute(entryPath)) return [];
|
|
447
|
+
const sourceResolved = path2.resolve(sourcePath);
|
|
448
|
+
const resolvedEntry = path2.resolve(sourcePath, entryPath);
|
|
449
|
+
const relToSource = path2.relative(sourceResolved, resolvedEntry);
|
|
450
|
+
if (relToSource.startsWith("..") || path2.isAbsolute(relToSource)) return [];
|
|
451
|
+
if (!fs2.existsSync(resolvedEntry)) return [];
|
|
452
|
+
const stat = fs2.statSync(resolvedEntry);
|
|
453
|
+
if (stat.isFile()) {
|
|
454
|
+
return [normalizeRelPath(relToSource)];
|
|
455
|
+
}
|
|
456
|
+
if (!stat.isDirectory()) return [];
|
|
457
|
+
const files = [];
|
|
458
|
+
const stack = [resolvedEntry];
|
|
459
|
+
while (stack.length > 0) {
|
|
460
|
+
const current = stack.pop();
|
|
461
|
+
if (!current) continue;
|
|
462
|
+
const entries = fs2.readdirSync(current, { withFileTypes: true });
|
|
463
|
+
for (const dirent of entries) {
|
|
464
|
+
const childPath = path2.join(current, dirent.name);
|
|
465
|
+
if (dirent.isDirectory()) {
|
|
466
|
+
stack.push(childPath);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
if (dirent.isFile()) {
|
|
470
|
+
const rel = path2.relative(sourceResolved, childPath);
|
|
471
|
+
if (!rel.startsWith("..") && !path2.isAbsolute(rel)) {
|
|
472
|
+
files.push(normalizeRelPath(rel));
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return files;
|
|
478
|
+
}
|
|
440
479
|
function parseGwitInclude(mainPath) {
|
|
441
480
|
const includePath = path2.join(mainPath, GWITINCLUDE_FILE);
|
|
442
481
|
if (!fs2.existsSync(includePath)) return [];
|
|
443
482
|
return _parseLines(fs2.readFileSync(includePath, "utf-8"));
|
|
444
483
|
}
|
|
484
|
+
function resolveIncludedFilePaths(mainPath, sourcePath) {
|
|
485
|
+
const rawEntries = parseGwitInclude(mainPath);
|
|
486
|
+
if (rawEntries.length === 0) return [];
|
|
487
|
+
const files = /* @__PURE__ */ new Set();
|
|
488
|
+
for (const entry of rawEntries) {
|
|
489
|
+
if (isGlobPattern(entry)) {
|
|
490
|
+
for (const match of expandGlob(entry, sourcePath)) {
|
|
491
|
+
files.add(normalizeRelPath(match));
|
|
492
|
+
}
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
for (const relPath of resolveLiteralEntryFiles(entry, sourcePath)) {
|
|
496
|
+
files.add(relPath);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return [...files].sort();
|
|
500
|
+
}
|
|
445
501
|
function copyIncludedFiles(mainPath, worktreePath) {
|
|
446
502
|
const rawEntries = parseGwitInclude(mainPath);
|
|
447
503
|
if (rawEntries.length === 0) return [];
|
|
@@ -537,7 +593,7 @@ var path3 = __toESM(require("path"));
|
|
|
537
593
|
var import_crypto = require("crypto");
|
|
538
594
|
var FILES_DIR_NAME = "files";
|
|
539
595
|
var MANIFEST_FILE_NAME = "manifest.json";
|
|
540
|
-
function
|
|
596
|
+
function normalizeRelPath2(relPath) {
|
|
541
597
|
return relPath.replace(/\\/g, "/");
|
|
542
598
|
}
|
|
543
599
|
function tryChmod(filePath, mode) {
|
|
@@ -565,7 +621,7 @@ function collectEntryFiles(entryPath, mainPath) {
|
|
|
565
621
|
}
|
|
566
622
|
const stat = fs3.statSync(resolvedEntry);
|
|
567
623
|
if (stat.isFile()) {
|
|
568
|
-
return [
|
|
624
|
+
return [normalizeRelPath2(relToMain)];
|
|
569
625
|
}
|
|
570
626
|
const files = [];
|
|
571
627
|
const stack = [resolvedEntry];
|
|
@@ -582,7 +638,7 @@ function collectEntryFiles(entryPath, mainPath) {
|
|
|
582
638
|
if (entry.isFile()) {
|
|
583
639
|
const rel = path3.relative(resolvedMain, childPath);
|
|
584
640
|
if (!rel.startsWith("..") && !path3.isAbsolute(rel)) {
|
|
585
|
-
files.push(
|
|
641
|
+
files.push(normalizeRelPath2(rel));
|
|
586
642
|
}
|
|
587
643
|
}
|
|
588
644
|
}
|
|
@@ -1421,7 +1477,7 @@ var fs9 = __toESM(require("fs"));
|
|
|
1421
1477
|
// src/core/merge.ts
|
|
1422
1478
|
var fs8 = __toESM(require("fs"));
|
|
1423
1479
|
var path7 = __toESM(require("path"));
|
|
1424
|
-
function
|
|
1480
|
+
function normalizeRelPath3(relPath) {
|
|
1425
1481
|
return relPath.replace(/\\/g, "/");
|
|
1426
1482
|
}
|
|
1427
1483
|
function copyFileToMain(worktreeFile, mainFile) {
|
|
@@ -1459,6 +1515,73 @@ function runGitMergeFile(mainFile, snapshotFile, worktreeFile, branch) {
|
|
|
1459
1515
|
]);
|
|
1460
1516
|
return result.exitCode > 0;
|
|
1461
1517
|
}
|
|
1518
|
+
function isInMergeScope(relPath, mainPath) {
|
|
1519
|
+
return isGitIgnored(relPath, mainPath) && !isGitTracked(relPath, mainPath);
|
|
1520
|
+
}
|
|
1521
|
+
function processSnapshotTrackedFile(result, relPath, worktreePath, mainPath, slug, branch, baseHash) {
|
|
1522
|
+
if (!isInMergeScope(relPath, mainPath)) {
|
|
1523
|
+
result.skipped.push(relPath);
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
const mainFile = path7.resolve(mainPath, relPath);
|
|
1527
|
+
const worktreeFile = path7.resolve(worktreePath, relPath);
|
|
1528
|
+
const snapshotFile = getSnapshotFilePath(slug, relPath);
|
|
1529
|
+
if (!isExistingFile(mainFile)) {
|
|
1530
|
+
result.skipped.push(relPath);
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
if (!isExistingFile(worktreeFile)) {
|
|
1534
|
+
result.skipped.push(relPath);
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
if (!snapshotFile || !isExistingFile(snapshotFile)) {
|
|
1538
|
+
result.skipped.push(relPath);
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
const mainHash = hashFile(mainFile);
|
|
1542
|
+
const worktreeHash = hashFile(worktreeFile);
|
|
1543
|
+
if (baseHash === mainHash && baseHash === worktreeHash) {
|
|
1544
|
+
result.skipped.push(relPath);
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
if (baseHash === mainHash) {
|
|
1548
|
+
copyFileToMain(worktreeFile, mainFile);
|
|
1549
|
+
result.copied.push(relPath);
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
if (baseHash === worktreeHash || mainHash === worktreeHash) {
|
|
1553
|
+
result.skipped.push(relPath);
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
if (isBinaryFile(mainFile) || isBinaryFile(worktreeFile) || isBinaryFile(snapshotFile)) {
|
|
1557
|
+
result.binarySkipped.push(relPath);
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
const hasConflicts = runGitMergeFile(mainFile, snapshotFile, worktreeFile, branch);
|
|
1561
|
+
if (hasConflicts) {
|
|
1562
|
+
result.conflicts.push(relPath);
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
result.merged.push(relPath);
|
|
1566
|
+
}
|
|
1567
|
+
function processNonSnapshotIncludeFile(result, relPath, worktreePath, mainPath) {
|
|
1568
|
+
if (!isInMergeScope(relPath, mainPath)) {
|
|
1569
|
+
result.skipped.push(relPath);
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
const worktreeFile = path7.resolve(worktreePath, relPath);
|
|
1573
|
+
if (!isExistingFile(worktreeFile)) {
|
|
1574
|
+
result.skipped.push(relPath);
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
const mainFile = path7.resolve(mainPath, relPath);
|
|
1578
|
+
if (isExistingFile(mainFile) && hashFile(mainFile) === hashFile(worktreeFile)) {
|
|
1579
|
+
result.skipped.push(relPath);
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
copyFileToMain(worktreeFile, mainFile);
|
|
1583
|
+
result.copied.push(relPath);
|
|
1584
|
+
}
|
|
1462
1585
|
function mergeBackIncludedFiles(worktreePath, mainPath, slug) {
|
|
1463
1586
|
const result = {
|
|
1464
1587
|
copied: [],
|
|
@@ -1469,59 +1592,29 @@ function mergeBackIncludedFiles(worktreePath, mainPath, slug) {
|
|
|
1469
1592
|
};
|
|
1470
1593
|
const manifest = readSnapshot(slug);
|
|
1471
1594
|
if (!manifest) return result;
|
|
1472
|
-
const
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
const
|
|
1595
|
+
const snapshotEntries = Object.keys(manifest.files).sort();
|
|
1596
|
+
const snapshotRelPathSet = new Set(snapshotEntries.map((relPath) => normalizeRelPath3(relPath)));
|
|
1597
|
+
for (const rawRelPath of snapshotEntries) {
|
|
1598
|
+
const relPath = normalizeRelPath3(rawRelPath);
|
|
1599
|
+
const base = manifest.files[rawRelPath];
|
|
1476
1600
|
if (!base) {
|
|
1477
1601
|
result.skipped.push(relPath);
|
|
1478
1602
|
continue;
|
|
1479
1603
|
}
|
|
1480
|
-
|
|
1481
|
-
result
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
}
|
|
1495
|
-
if (!snapshotFile || !isExistingFile(snapshotFile)) {
|
|
1496
|
-
result.skipped.push(relPath);
|
|
1497
|
-
continue;
|
|
1498
|
-
}
|
|
1499
|
-
const baseHash = base.hash;
|
|
1500
|
-
const mainHash = hashFile(mainFile);
|
|
1501
|
-
const worktreeHash = hashFile(worktreeFile);
|
|
1502
|
-
if (baseHash === mainHash && baseHash === worktreeHash) {
|
|
1503
|
-
result.skipped.push(relPath);
|
|
1504
|
-
continue;
|
|
1505
|
-
}
|
|
1506
|
-
if (baseHash === mainHash) {
|
|
1507
|
-
copyFileToMain(worktreeFile, mainFile);
|
|
1508
|
-
result.copied.push(relPath);
|
|
1509
|
-
continue;
|
|
1510
|
-
}
|
|
1511
|
-
if (baseHash === worktreeHash || mainHash === worktreeHash) {
|
|
1512
|
-
result.skipped.push(relPath);
|
|
1513
|
-
continue;
|
|
1514
|
-
}
|
|
1515
|
-
if (isBinaryFile(mainFile) || isBinaryFile(worktreeFile) || isBinaryFile(snapshotFile)) {
|
|
1516
|
-
result.binarySkipped.push(relPath);
|
|
1517
|
-
continue;
|
|
1518
|
-
}
|
|
1519
|
-
const hasConflicts = runGitMergeFile(mainFile, snapshotFile, worktreeFile, manifest.branch);
|
|
1520
|
-
if (hasConflicts) {
|
|
1521
|
-
result.conflicts.push(relPath);
|
|
1522
|
-
} else {
|
|
1523
|
-
result.merged.push(relPath);
|
|
1524
|
-
}
|
|
1604
|
+
processSnapshotTrackedFile(
|
|
1605
|
+
result,
|
|
1606
|
+
relPath,
|
|
1607
|
+
worktreePath,
|
|
1608
|
+
mainPath,
|
|
1609
|
+
slug,
|
|
1610
|
+
manifest.branch,
|
|
1611
|
+
base.hash
|
|
1612
|
+
);
|
|
1613
|
+
}
|
|
1614
|
+
const includeFiles = resolveIncludedFilePaths(mainPath, worktreePath);
|
|
1615
|
+
for (const relPath of includeFiles) {
|
|
1616
|
+
if (snapshotRelPathSet.has(relPath)) continue;
|
|
1617
|
+
processNonSnapshotIncludeFile(result, relPath, worktreePath, mainPath);
|
|
1525
1618
|
}
|
|
1526
1619
|
return result;
|
|
1527
1620
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivanshshrivas/gwit",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Fully isolated git worktrees for parallel development",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"git",
|
|
@@ -24,9 +24,10 @@
|
|
|
24
24
|
"files": [
|
|
25
25
|
"dist"
|
|
26
26
|
],
|
|
27
|
-
"scripts": {
|
|
28
|
-
"build": "tsup",
|
|
29
|
-
"
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsup",
|
|
29
|
+
"prepack": "npm run build",
|
|
30
|
+
"dev": "tsx src/index.ts",
|
|
30
31
|
"typecheck": "tsc --noEmit",
|
|
31
32
|
"test": "vitest run",
|
|
32
33
|
"test:watch": "vitest",
|