withub-cli 0.1.0
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/LICENSE +201 -0
- package/README.md +41 -0
- package/dist/commands/account.js +109 -0
- package/dist/commands/checkout.js +213 -0
- package/dist/commands/clone.js +263 -0
- package/dist/commands/commit.js +150 -0
- package/dist/commands/diff.js +159 -0
- package/dist/commands/fetch.js +169 -0
- package/dist/commands/init.js +125 -0
- package/dist/commands/invite.js +72 -0
- package/dist/commands/list.js +214 -0
- package/dist/commands/plumbing.js +98 -0
- package/dist/commands/pull.js +160 -0
- package/dist/commands/push.js +371 -0
- package/dist/commands/registerCommands.js +183 -0
- package/dist/commands/removeUser.js +63 -0
- package/dist/commands/stub.js +11 -0
- package/dist/commands/transfer.js +46 -0
- package/dist/commands/walrusBlob.js +50 -0
- package/dist/commands/walrusQuilt.js +282 -0
- package/dist/commands/workspace.js +260 -0
- package/dist/index.js +46 -0
- package/dist/lib/config.js +49 -0
- package/dist/lib/constants.js +6 -0
- package/dist/lib/fs.js +154 -0
- package/dist/lib/keys.js +224 -0
- package/dist/lib/manifest.js +37 -0
- package/dist/lib/quilt.js +38 -0
- package/dist/lib/repo.js +70 -0
- package/dist/lib/schema.js +53 -0
- package/dist/lib/seal.js +157 -0
- package/dist/lib/serialize.js +30 -0
- package/dist/lib/state.js +57 -0
- package/dist/lib/suiRepo.js +220 -0
- package/dist/lib/ui.js +51 -0
- package/dist/lib/validate.js +13 -0
- package/dist/lib/walrus.js +237 -0
- package/package.json +57 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.cloneAction = cloneAction;
|
|
7
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const client_1 = require("@mysten/sui/client");
|
|
10
|
+
const ui_1 = require("../lib/ui");
|
|
11
|
+
const walrus_1 = require("../lib/walrus");
|
|
12
|
+
const suiRepo_1 = require("../lib/suiRepo");
|
|
13
|
+
const schema_1 = require("../lib/schema");
|
|
14
|
+
const manifest_1 = require("../lib/manifest");
|
|
15
|
+
const serialize_1 = require("../lib/serialize");
|
|
16
|
+
const fs_1 = require("../lib/fs");
|
|
17
|
+
const state_1 = require("../lib/state");
|
|
18
|
+
const repo_1 = require("../lib/repo");
|
|
19
|
+
const seal_1 = require("../lib/seal");
|
|
20
|
+
const keys_1 = require("../lib/keys");
|
|
21
|
+
const DEFAULT_RELAYS = ['https://upload-relay.testnet.walrus.space'];
|
|
22
|
+
const DEFAULT_NETWORK = 'testnet';
|
|
23
|
+
async function cloneAction(repoId) {
|
|
24
|
+
if (!repoId) {
|
|
25
|
+
throw new Error('Usage: wit clone <repo_id>');
|
|
26
|
+
}
|
|
27
|
+
// eslint-disable-next-line no-console
|
|
28
|
+
console.log(ui_1.colors.header('Starting clone...'));
|
|
29
|
+
// Prepare .wit layout and config
|
|
30
|
+
const witPath = await ensureLayout(process.cwd(), repoId);
|
|
31
|
+
const resolved = await (0, walrus_1.resolveWalrusConfig)(process.cwd());
|
|
32
|
+
const suiClient = new client_1.SuiClient({ url: resolved.suiRpcUrl });
|
|
33
|
+
const walrusSvc = await walrus_1.WalrusService.fromRepo();
|
|
34
|
+
const signerInfo = await (0, keys_1.loadSigner)();
|
|
35
|
+
// Fetch on-chain head
|
|
36
|
+
const onchain = await (0, suiRepo_1.fetchRepositoryStateWithRetry)(suiClient, repoId);
|
|
37
|
+
if (!onchain.headCommit || !onchain.headManifest || !onchain.headQuilt) {
|
|
38
|
+
// eslint-disable-next-line no-console
|
|
39
|
+
console.log(ui_1.colors.yellow('Remote repository has no head. Nothing to clone.'));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
// Download manifest
|
|
43
|
+
// eslint-disable-next-line no-console
|
|
44
|
+
console.log(ui_1.colors.cyan('Downloading manifest...'));
|
|
45
|
+
const manifestBuf = Buffer.from(await walrusSvc.readBlob(onchain.headManifest));
|
|
46
|
+
const manifest = schema_1.ManifestSchema.parse(JSON.parse(manifestBuf.toString('utf8')));
|
|
47
|
+
const computedRoot = (0, manifest_1.computeRootHash)(Object.fromEntries(Object.entries(manifest.files).map(([rel, meta]) => [
|
|
48
|
+
rel,
|
|
49
|
+
{ hash: meta.hash, size: meta.size, mode: meta.mode, mtime: meta.mtime },
|
|
50
|
+
])));
|
|
51
|
+
if (computedRoot !== manifest.root_hash) {
|
|
52
|
+
throw new Error('Manifest root_hash mismatch; aborting clone.');
|
|
53
|
+
}
|
|
54
|
+
await cacheJson(path_1.default.join(witPath, 'objects', 'manifests', `${(0, state_1.idToFileName)(onchain.headManifest)}.json`), (0, serialize_1.canonicalStringify)(manifest));
|
|
55
|
+
// Persist repo config seal policy if present
|
|
56
|
+
if (onchain.sealPolicyId) {
|
|
57
|
+
try {
|
|
58
|
+
const cfgRaw = await promises_1.default.readFile(path_1.default.join(witPath, 'config.json'), 'utf8');
|
|
59
|
+
const cfg = JSON.parse(cfgRaw);
|
|
60
|
+
cfg.seal_policy_id = onchain.sealPolicyId;
|
|
61
|
+
await (0, repo_1.writeRepoConfig)(witPath, cfg);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// best-effort
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const sealPolicyId = onchain.sealPolicyId || null;
|
|
68
|
+
const hasEncrypted = Object.values(manifest.files).some((meta) => meta.enc);
|
|
69
|
+
if (hasEncrypted && !sealPolicyId) {
|
|
70
|
+
throw new Error('Encrypted repository detected but no seal policy id on chain.');
|
|
71
|
+
}
|
|
72
|
+
// Download remote commit
|
|
73
|
+
// eslint-disable-next-line no-console
|
|
74
|
+
console.log(ui_1.colors.cyan('Downloading commit...'));
|
|
75
|
+
const commitBuf = Buffer.from(await walrusSvc.readBlob(onchain.headCommit));
|
|
76
|
+
const commit = parseRemoteCommit(commitBuf);
|
|
77
|
+
if (commit.tree.root_hash !== manifest.root_hash) {
|
|
78
|
+
throw new Error('Commit root_hash does not match manifest; aborting clone.');
|
|
79
|
+
}
|
|
80
|
+
await cacheJson(path_1.default.join(witPath, 'objects', 'commits', `${(0, state_1.idToFileName)(onchain.headCommit)}.json`), commitBuf.toString('utf8'));
|
|
81
|
+
// Fetch files by id
|
|
82
|
+
const entries = Object.entries(manifest.files);
|
|
83
|
+
// eslint-disable-next-line no-console
|
|
84
|
+
console.log(ui_1.colors.cyan(`Downloading ${entries.length} files from Walrus...`));
|
|
85
|
+
const index = {};
|
|
86
|
+
for (let i = 0; i < entries.length; i += 1) {
|
|
87
|
+
const [rel, meta] = entries[i];
|
|
88
|
+
const data = await fetchFileBytes(walrusSvc, manifest, rel, meta);
|
|
89
|
+
let plain;
|
|
90
|
+
try {
|
|
91
|
+
if (meta.enc) {
|
|
92
|
+
// Map metadata to EncryptionMeta
|
|
93
|
+
const encAny = meta.enc;
|
|
94
|
+
const encMeta = {
|
|
95
|
+
alg: encAny.alg || encAny.enc_alg,
|
|
96
|
+
policy_id: encAny.policy_id || encAny.enc_policy,
|
|
97
|
+
package_id: encAny.package_id || encAny.enc_package || '0x0', // Fallback or read from meta
|
|
98
|
+
sealed_session_key: encAny.sealed_session_key || encAny.enc_sealed_key,
|
|
99
|
+
iv: encAny.iv || encAny.enc_iv,
|
|
100
|
+
tag: encAny.tag || encAny.enc_tag,
|
|
101
|
+
};
|
|
102
|
+
plain = await (0, seal_1.decryptWithSeal)(data, encMeta, signerInfo.signer, suiClient);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
plain = data;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
// Handle decryption errors gracefully
|
|
110
|
+
if (err.message?.includes('NoAccess')) {
|
|
111
|
+
// eslint-disable-next-line no-console
|
|
112
|
+
console.log(ui_1.colors.red('❌ Access denied: You are not whitelisted for this private repository.'));
|
|
113
|
+
// eslint-disable-next-line no-console
|
|
114
|
+
console.log(ui_1.colors.yellow(` Policy ID: ${meta.enc?.policy_id || sealPolicyId}`));
|
|
115
|
+
// eslint-disable-next-line no-console
|
|
116
|
+
console.log(ui_1.colors.yellow(' Please contact the repository owner to be added to the whitelist.'));
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
if (err.message?.includes('Timeout')) {
|
|
120
|
+
// eslint-disable-next-line no-console
|
|
121
|
+
console.log(ui_1.colors.red('❌ Connection timeout: Unable to reach Seal decryption servers.'));
|
|
122
|
+
// eslint-disable-next-line no-console
|
|
123
|
+
console.log(ui_1.colors.yellow(' Please check your network connection and try again.'));
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
// For other errors, show the file that failed
|
|
127
|
+
// eslint-disable-next-line no-console
|
|
128
|
+
console.log(ui_1.colors.red(`❌ Failed to decrypt ${rel}: ${err.message}`));
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
const hash = (0, serialize_1.sha256Base64)(plain);
|
|
132
|
+
if (hash !== meta.hash || plain.length !== meta.size) {
|
|
133
|
+
throw new Error(`Hash/size mismatch for ${rel}`);
|
|
134
|
+
}
|
|
135
|
+
const abs = path_1.default.join(process.cwd(), rel);
|
|
136
|
+
await (0, fs_1.ensureDirForFile)(abs);
|
|
137
|
+
await promises_1.default.writeFile(abs, plain);
|
|
138
|
+
const mode = parseInt(meta.mode, 10) & 0o777;
|
|
139
|
+
await promises_1.default.chmod(abs, mode);
|
|
140
|
+
index[rel] = { hash: meta.hash, size: meta.size, mode: meta.mode, mtime: meta.mtime };
|
|
141
|
+
}
|
|
142
|
+
// Write index and refs/state
|
|
143
|
+
await (0, fs_1.writeIndex)(path_1.default.join(witPath, 'index'), index);
|
|
144
|
+
await ensureHeadFiles(witPath, onchain.headCommit);
|
|
145
|
+
await (0, repo_1.writeRemoteRef)(witPath, onchain.headCommit);
|
|
146
|
+
await (0, repo_1.writeRemoteState)(witPath, {
|
|
147
|
+
repo_id: repoId,
|
|
148
|
+
head_commit: onchain.headCommit,
|
|
149
|
+
head_manifest: onchain.headManifest,
|
|
150
|
+
head_quilt: onchain.headQuilt,
|
|
151
|
+
version: onchain.version,
|
|
152
|
+
});
|
|
153
|
+
const commitMap = await readCommitIdMapSafe(witPath);
|
|
154
|
+
await downloadCommitChain(walrusSvc, onchain.headCommit, witPath, commitMap);
|
|
155
|
+
await (0, state_1.writeCommitIdMap)(witPath, commitMap);
|
|
156
|
+
// eslint-disable-next-line no-console
|
|
157
|
+
console.log(ui_1.colors.green('Clone complete.'));
|
|
158
|
+
// eslint-disable-next-line no-console
|
|
159
|
+
console.log(`Head: ${ui_1.colors.hash(onchain.headCommit)}`);
|
|
160
|
+
// eslint-disable-next-line no-console
|
|
161
|
+
console.log(`Manifest: ${ui_1.colors.hash(onchain.headManifest)}`);
|
|
162
|
+
// eslint-disable-next-line no-console
|
|
163
|
+
console.log(`Quilt: ${ui_1.colors.hash(onchain.headQuilt)}`);
|
|
164
|
+
}
|
|
165
|
+
async function fetchFileBytes(walrusSvc, manifest, rel, meta) {
|
|
166
|
+
// Prefer quilt fetch when quilt_id is present; fallback to direct file id
|
|
167
|
+
if (manifest.quilt_id) {
|
|
168
|
+
try {
|
|
169
|
+
const bytes = await walrusSvc.readQuiltFile(manifest.quilt_id, rel);
|
|
170
|
+
return Buffer.from(bytes);
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
// fallback to direct id path below
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (meta.id) {
|
|
177
|
+
const files = await walrusSvc.getClient().getFiles({ ids: [meta.id] });
|
|
178
|
+
const file = files[0];
|
|
179
|
+
const data = Buffer.from(await file.bytes());
|
|
180
|
+
const tags = await file.getTags();
|
|
181
|
+
if (tags?.hash && tags.hash !== meta.hash) {
|
|
182
|
+
throw new Error(`Tag hash mismatch for ${rel}`);
|
|
183
|
+
}
|
|
184
|
+
return data;
|
|
185
|
+
}
|
|
186
|
+
throw new Error(`Manifest entry missing file id for ${rel}`);
|
|
187
|
+
}
|
|
188
|
+
async function ensureLayout(cwd, repoId) {
|
|
189
|
+
const witPath = path_1.default.join(cwd, '.wit');
|
|
190
|
+
await promises_1.default.mkdir(witPath, { recursive: true });
|
|
191
|
+
const subdirs = [
|
|
192
|
+
'refs/heads',
|
|
193
|
+
'refs/remotes',
|
|
194
|
+
'objects/blobs',
|
|
195
|
+
'objects/commits',
|
|
196
|
+
'objects/manifests',
|
|
197
|
+
'objects/quilts',
|
|
198
|
+
'objects/maps',
|
|
199
|
+
'state',
|
|
200
|
+
];
|
|
201
|
+
await Promise.all(subdirs.map((d) => promises_1.default.mkdir(path_1.default.join(witPath, d), { recursive: true })));
|
|
202
|
+
const cfgPath = path_1.default.join(witPath, 'config.json');
|
|
203
|
+
try {
|
|
204
|
+
await promises_1.default.access(cfgPath);
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
if (err?.code === 'ENOENT') {
|
|
208
|
+
const cfg = {
|
|
209
|
+
repo_name: repoId,
|
|
210
|
+
repo_id: repoId,
|
|
211
|
+
network: DEFAULT_NETWORK,
|
|
212
|
+
relays: DEFAULT_RELAYS,
|
|
213
|
+
author: 'unknown',
|
|
214
|
+
key_alias: 'default',
|
|
215
|
+
seal_policy_id: null,
|
|
216
|
+
created_at: new Date().toISOString(),
|
|
217
|
+
};
|
|
218
|
+
await (0, repo_1.writeRepoConfig)(witPath, cfg);
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
throw err;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
await promises_1.default.writeFile(path_1.default.join(witPath, 'HEAD'), 'refs/heads/main\n', 'utf8');
|
|
225
|
+
return witPath;
|
|
226
|
+
}
|
|
227
|
+
async function ensureHeadFiles(witPath, headCommit) {
|
|
228
|
+
const headRefPath = path_1.default.join(witPath, 'refs', 'heads', 'main');
|
|
229
|
+
await promises_1.default.writeFile(headRefPath, `${headCommit}\n`, 'utf8');
|
|
230
|
+
}
|
|
231
|
+
function parseRemoteCommit(buf) {
|
|
232
|
+
const parsed = JSON.parse(buf.toString('utf8'));
|
|
233
|
+
if (!parsed?.tree?.root_hash || !parsed?.tree?.manifest_id) {
|
|
234
|
+
throw new Error('Invalid remote commit object');
|
|
235
|
+
}
|
|
236
|
+
return parsed;
|
|
237
|
+
}
|
|
238
|
+
async function cacheJson(filePath, content) {
|
|
239
|
+
await (0, fs_1.ensureDirForFile)(filePath);
|
|
240
|
+
await promises_1.default.writeFile(filePath, content, 'utf8');
|
|
241
|
+
}
|
|
242
|
+
async function readCommitIdMapSafe(witPath) {
|
|
243
|
+
try {
|
|
244
|
+
return await (0, state_1.readCommitIdMap)(witPath);
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return {};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
async function downloadCommitChain(walrusSvc, startId, witPath, map) {
|
|
251
|
+
const seen = new Set();
|
|
252
|
+
let current = startId;
|
|
253
|
+
while (current && !seen.has(current)) {
|
|
254
|
+
seen.add(current);
|
|
255
|
+
const buf = Buffer.from(await walrusSvc.readBlob(current));
|
|
256
|
+
const commit = parseRemoteCommit(buf);
|
|
257
|
+
await cacheJson(path_1.default.join(witPath, 'objects', 'commits', `${(0, state_1.idToFileName)(current)}.json`), buf.toString('utf8'));
|
|
258
|
+
if (!map[current]) {
|
|
259
|
+
map[current] = current;
|
|
260
|
+
}
|
|
261
|
+
current = commit.parent;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.commitAction = commitAction;
|
|
7
|
+
exports.logAction = logAction;
|
|
8
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const serialize_1 = require("../lib/serialize");
|
|
11
|
+
const fs_1 = require("../lib/fs");
|
|
12
|
+
const state_1 = require("../lib/state");
|
|
13
|
+
const repo_1 = require("../lib/repo");
|
|
14
|
+
const manifest_1 = require("../lib/manifest");
|
|
15
|
+
const ui_1 = require("../lib/ui");
|
|
16
|
+
const WIT_DIR = '.wit';
|
|
17
|
+
async function commitAction(opts) {
|
|
18
|
+
const message = opts.message;
|
|
19
|
+
if (!message) {
|
|
20
|
+
throw new Error('Commit message is required (use -m/--message).');
|
|
21
|
+
}
|
|
22
|
+
const witPath = await requireWitDir();
|
|
23
|
+
const indexPath = path_1.default.join(witPath, 'index');
|
|
24
|
+
const index = await (0, fs_1.readIndex)(indexPath);
|
|
25
|
+
if (Object.keys(index).length === 0) {
|
|
26
|
+
// eslint-disable-next-line no-console
|
|
27
|
+
console.warn('Index is empty. Nothing to commit.');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const config = await readConfig(witPath);
|
|
31
|
+
if (!config.author || config.author === 'unknown') {
|
|
32
|
+
// eslint-disable-next-line no-console
|
|
33
|
+
console.warn(ui_1.colors.yellow('Warning: author is unknown. Set author in .wit/config.json or ~/.witconfig.'));
|
|
34
|
+
}
|
|
35
|
+
const headRefPath = await (0, state_1.readHeadRefPath)(witPath);
|
|
36
|
+
const parent = await (0, state_1.readRef)(headRefPath);
|
|
37
|
+
const rootHash = (0, manifest_1.computeRootHash)(index);
|
|
38
|
+
const commit = {
|
|
39
|
+
tree: {
|
|
40
|
+
root_hash: rootHash,
|
|
41
|
+
manifest_id: null,
|
|
42
|
+
quilt_id: null,
|
|
43
|
+
files: index,
|
|
44
|
+
},
|
|
45
|
+
parent,
|
|
46
|
+
author: config.author || 'unknown',
|
|
47
|
+
message,
|
|
48
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
49
|
+
extras: { patch_id: null, tags: {} },
|
|
50
|
+
};
|
|
51
|
+
const serialized = (0, serialize_1.canonicalStringify)(commit);
|
|
52
|
+
const commitId = (0, serialize_1.sha256Base64)(serialized);
|
|
53
|
+
await writeCommitObject(witPath, commitId, serialized);
|
|
54
|
+
await promises_1.default.writeFile(headRefPath, `${commitId}\n`, 'utf8');
|
|
55
|
+
await updateCommitMap(witPath, commitId);
|
|
56
|
+
// eslint-disable-next-line no-console
|
|
57
|
+
console.log(ui_1.colors.green(`Committed ${commitId}`));
|
|
58
|
+
}
|
|
59
|
+
async function logAction() {
|
|
60
|
+
const witPath = await requireWitDir();
|
|
61
|
+
const headRefPath = await (0, state_1.readHeadRefPath)(witPath);
|
|
62
|
+
const head = await (0, state_1.readRef)(headRefPath);
|
|
63
|
+
const remoteHead = await (0, repo_1.readRemoteRef)(witPath);
|
|
64
|
+
const commitMap = await (0, state_1.readCommitIdMap)(witPath);
|
|
65
|
+
// If the local HEAD has already been pushed and maps to remoteHead, treat it as the same commit.
|
|
66
|
+
const headRemoteMapped = head ? commitMap[head] : null;
|
|
67
|
+
const remoteAligned = head && remoteHead && headRemoteMapped === remoteHead;
|
|
68
|
+
const seen = new Set();
|
|
69
|
+
if (head) {
|
|
70
|
+
// eslint-disable-next-line no-console
|
|
71
|
+
console.log(ui_1.colors.header('Local (HEAD):'));
|
|
72
|
+
let currentId = head;
|
|
73
|
+
while (currentId) {
|
|
74
|
+
const commit = await readCommit(witPath, currentId);
|
|
75
|
+
printCommit(currentId, commit);
|
|
76
|
+
seen.add(currentId);
|
|
77
|
+
currentId = commit.parent;
|
|
78
|
+
}
|
|
79
|
+
if (remoteAligned) {
|
|
80
|
+
// eslint-disable-next-line no-console
|
|
81
|
+
console.log(ui_1.colors.gray(`(remote id: ${ui_1.colors.hash(remoteHead)})`));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// eslint-disable-next-line no-console
|
|
86
|
+
console.log('No local commits yet.');
|
|
87
|
+
}
|
|
88
|
+
if (remoteHead && (!head || remoteHead !== head) && !remoteAligned) {
|
|
89
|
+
// eslint-disable-next-line no-console
|
|
90
|
+
console.log(ui_1.colors.header('Remote (remotes/main):'));
|
|
91
|
+
let currentId = remoteHead;
|
|
92
|
+
while (currentId && !seen.has(currentId)) {
|
|
93
|
+
const commit = await readCommit(witPath, currentId);
|
|
94
|
+
printCommit(currentId, commit);
|
|
95
|
+
seen.add(currentId);
|
|
96
|
+
currentId = commit.parent;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function requireWitDir() {
|
|
101
|
+
const dir = path_1.default.join(process.cwd(), WIT_DIR);
|
|
102
|
+
try {
|
|
103
|
+
await promises_1.default.access(dir);
|
|
104
|
+
return dir;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
throw new Error('Not a wit repository (missing .wit). Run `wit init` first.');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function readConfig(witPath) {
|
|
111
|
+
const file = path_1.default.join(witPath, 'config.json');
|
|
112
|
+
try {
|
|
113
|
+
const raw = await promises_1.default.readFile(file, 'utf8');
|
|
114
|
+
return JSON.parse(raw);
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
if (err?.code === 'ENOENT') {
|
|
118
|
+
return {};
|
|
119
|
+
}
|
|
120
|
+
throw err;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async function writeCommitObject(witPath, commitId, serialized) {
|
|
124
|
+
const file = path_1.default.join(witPath, 'objects', 'commits', `${(0, state_1.idToFileName)(commitId)}.json`);
|
|
125
|
+
await promises_1.default.writeFile(file, serialized, 'utf8');
|
|
126
|
+
}
|
|
127
|
+
async function readCommit(witPath, commitId) {
|
|
128
|
+
return (0, state_1.readCommitById)(witPath, commitId);
|
|
129
|
+
}
|
|
130
|
+
function printCommit(id, commit) {
|
|
131
|
+
// eslint-disable-next-line no-console
|
|
132
|
+
console.log(ui_1.colors.header(`commit ${ui_1.colors.hash(id)}`));
|
|
133
|
+
// eslint-disable-next-line no-console
|
|
134
|
+
console.log(`Author: ${ui_1.colors.author(commit.author)}`);
|
|
135
|
+
// eslint-disable-next-line no-console
|
|
136
|
+
console.log(`Date: ${ui_1.colors.date(new Date(commit.timestamp * 1000).toISOString())}`);
|
|
137
|
+
// eslint-disable-next-line no-console
|
|
138
|
+
console.log();
|
|
139
|
+
// eslint-disable-next-line no-console
|
|
140
|
+
console.log(` ${commit.message}`);
|
|
141
|
+
// eslint-disable-next-line no-console
|
|
142
|
+
console.log();
|
|
143
|
+
}
|
|
144
|
+
async function updateCommitMap(witPath, commitId) {
|
|
145
|
+
const map = await (0, state_1.readCommitIdMap)(witPath);
|
|
146
|
+
if (!map[commitId]) {
|
|
147
|
+
map[commitId] = null;
|
|
148
|
+
await (0, state_1.writeCommitIdMap)(witPath, map);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.diffAction = diffAction;
|
|
7
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const diff_1 = require("diff");
|
|
10
|
+
const isbinaryfile_1 = require("isbinaryfile");
|
|
11
|
+
const fs_1 = require("../lib/fs");
|
|
12
|
+
const state_1 = require("../lib/state");
|
|
13
|
+
const ui_1 = require("../lib/ui");
|
|
14
|
+
const WIT_DIR = '.wit';
|
|
15
|
+
async function diffAction(opts) {
|
|
16
|
+
const cwd = process.cwd();
|
|
17
|
+
const witPath = await requireWitDir();
|
|
18
|
+
const indexPath = path_1.default.join(witPath, 'index');
|
|
19
|
+
const index = await (0, fs_1.readIndex)(indexPath);
|
|
20
|
+
if (opts.cached) {
|
|
21
|
+
const headRefPath = await (0, state_1.readHeadRefPath)(witPath);
|
|
22
|
+
const headId = await (0, state_1.readRef)(headRefPath);
|
|
23
|
+
if (!headId) {
|
|
24
|
+
// eslint-disable-next-line no-console
|
|
25
|
+
console.warn('No commits to diff against.');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const commit = await (0, state_1.readCommitById)(witPath, headId);
|
|
29
|
+
const changes = await diffIndex(commit.tree.files, index, {
|
|
30
|
+
loadBase: (rel, meta) => (0, fs_1.readBlob)(witPath, meta.hash),
|
|
31
|
+
loadTarget: (rel, meta) => (0, fs_1.readBlob)(witPath, meta.hash),
|
|
32
|
+
baseLabel: 'HEAD',
|
|
33
|
+
targetLabel: 'index',
|
|
34
|
+
cwd,
|
|
35
|
+
});
|
|
36
|
+
printChanges('index vs HEAD', changes);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
const tracked = new Set(Object.keys(index));
|
|
40
|
+
const ig = await (0, fs_1.buildIgnore)(cwd);
|
|
41
|
+
const workspaceFiles = await (0, fs_1.walkFiles)(cwd, ig, cwd, tracked);
|
|
42
|
+
const workspaceMeta = {};
|
|
43
|
+
for (const file of workspaceFiles) {
|
|
44
|
+
const rel = (0, fs_1.pathToPosix)(path_1.default.relative(cwd, file));
|
|
45
|
+
workspaceMeta[rel] = await (0, fs_1.computeFileMeta)(file);
|
|
46
|
+
}
|
|
47
|
+
const changes = await diffIndex(index, workspaceMeta, {
|
|
48
|
+
loadBase: (rel, meta) => (0, fs_1.readBlob)(witPath, meta.hash),
|
|
49
|
+
loadTarget: async (rel) => {
|
|
50
|
+
try {
|
|
51
|
+
return await promises_1.default.readFile(path_1.default.join(cwd, rel));
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
if (err?.code === 'ENOENT')
|
|
55
|
+
return null;
|
|
56
|
+
throw err;
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
baseLabel: 'index',
|
|
60
|
+
targetLabel: 'worktree',
|
|
61
|
+
cwd,
|
|
62
|
+
});
|
|
63
|
+
printChanges('worktree vs index', changes);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function diffIndex(base, target, ctx) {
|
|
67
|
+
const changes = [];
|
|
68
|
+
const paths = new Set([...Object.keys(base), ...Object.keys(target)]);
|
|
69
|
+
for (const rel of Array.from(paths).sort()) {
|
|
70
|
+
const a = base[rel];
|
|
71
|
+
const b = target[rel];
|
|
72
|
+
if (!a && b) {
|
|
73
|
+
const targetBuf = await ctx.loadTarget(rel, b);
|
|
74
|
+
const binary = await isBinaryBuffers(undefined, targetBuf);
|
|
75
|
+
const patch = binary ? undefined : createPatch(rel, '', targetBuf?.toString('utf8') ?? '', ctx.baseLabel, ctx.targetLabel);
|
|
76
|
+
changes.push({ path: rel, kind: 'A', binary, patch });
|
|
77
|
+
}
|
|
78
|
+
else if (a && !b) {
|
|
79
|
+
const baseBuf = await ctx.loadBase(rel, a);
|
|
80
|
+
const binary = await isBinaryBuffers(baseBuf, undefined);
|
|
81
|
+
const patch = binary ? undefined : createPatch(rel, baseBuf?.toString('utf8') ?? '', '', ctx.baseLabel, ctx.targetLabel);
|
|
82
|
+
changes.push({ path: rel, kind: 'D', binary, patch });
|
|
83
|
+
}
|
|
84
|
+
else if (a && b && !sameMeta(a, b)) {
|
|
85
|
+
const baseBuf = await ctx.loadBase(rel, a);
|
|
86
|
+
const targetBuf = await ctx.loadTarget(rel, b);
|
|
87
|
+
const binary = await isBinaryBuffers(baseBuf, targetBuf);
|
|
88
|
+
const patch = binary
|
|
89
|
+
? undefined
|
|
90
|
+
: createPatch(rel, baseBuf?.toString('utf8') ?? '', targetBuf?.toString('utf8') ?? '', ctx.baseLabel, ctx.targetLabel);
|
|
91
|
+
changes.push({ path: rel, kind: 'M', binary, patch });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return changes;
|
|
95
|
+
}
|
|
96
|
+
function sameMeta(a, b) {
|
|
97
|
+
return a.hash === b.hash && a.size === b.size && a.mode === b.mode;
|
|
98
|
+
}
|
|
99
|
+
async function isBinaryBuffers(a, b) {
|
|
100
|
+
const checks = [a, b];
|
|
101
|
+
for (const buf of checks) {
|
|
102
|
+
if (!buf)
|
|
103
|
+
continue;
|
|
104
|
+
try {
|
|
105
|
+
if (await (0, isbinaryfile_1.isBinaryFile)(buf))
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// fallback silent; continue to next buffer
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
function printChanges(title, changes) {
|
|
115
|
+
if (!changes.length) {
|
|
116
|
+
// eslint-disable-next-line no-console
|
|
117
|
+
console.log('No differences.');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
// eslint-disable-next-line no-console
|
|
121
|
+
console.log(ui_1.colors.header(`# Diff (${title})`));
|
|
122
|
+
changes.forEach((c) => {
|
|
123
|
+
const kindLabel = c.kind === 'A' ? 'Added' : c.kind === 'D' ? 'Deleted' : 'Modified';
|
|
124
|
+
const binaryLabel = c.binary ? 'binary' : 'text';
|
|
125
|
+
const color = c.kind === 'A' ? ui_1.colors.green : c.kind === 'D' ? ui_1.colors.red : ui_1.colors.yellow;
|
|
126
|
+
// eslint-disable-next-line no-console
|
|
127
|
+
console.log(color(`${c.kind}\t[${binaryLabel}]\t${c.path}\t${kindLabel}`));
|
|
128
|
+
if (!c.binary && c.patch) {
|
|
129
|
+
console.log(formatPatch(c.patch));
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function formatPatch(patch) {
|
|
134
|
+
return patch
|
|
135
|
+
.split('\n')
|
|
136
|
+
.map((line) => {
|
|
137
|
+
if (line.startsWith('+'))
|
|
138
|
+
return ui_1.colors.green(line);
|
|
139
|
+
if (line.startsWith('-'))
|
|
140
|
+
return ui_1.colors.red(line);
|
|
141
|
+
if (line.startsWith('@@'))
|
|
142
|
+
return ui_1.colors.cyan(line);
|
|
143
|
+
return line;
|
|
144
|
+
})
|
|
145
|
+
.join('\n');
|
|
146
|
+
}
|
|
147
|
+
function createPatch(rel, a, b, aLabel, bLabel) {
|
|
148
|
+
return (0, diff_1.createTwoFilesPatch)(`${aLabel}:${rel}`, `${bLabel}:${rel}`, a, b, undefined, undefined, { context: 3 });
|
|
149
|
+
}
|
|
150
|
+
async function requireWitDir() {
|
|
151
|
+
const dir = path_1.default.join(process.cwd(), WIT_DIR);
|
|
152
|
+
try {
|
|
153
|
+
await promises_1.default.access(dir);
|
|
154
|
+
return dir;
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
throw new Error('Not a wit repository (missing .wit). Run `wit init` first.');
|
|
158
|
+
}
|
|
159
|
+
}
|