@feng0531/dsh-vcs 0.3.65 → 0.3.67
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 +6 -4
- package/dist/client.js +3 -3
- package/dist/svn-host.mjs +1 -1
- package/docs/RELEASING.md +3 -3
- package/git-history.mjs +27 -14
- package/image-preview.mjs +47 -16
- package/index.mjs +66 -32
- package/local-command.mjs +47 -0
- package/package.json +4 -1
- package/repository-path.mjs +32 -0
- package/text-content.mjs +42 -0
- package/vcs.mjs +7 -66
package/vcs.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {parseSvnIdentity} from './src/svn-identity.mjs';
|
|
2
2
|
import {prepareRaster} from './image-preview.mjs';
|
|
3
|
-
import {
|
|
3
|
+
import {git, svn, TIMEOUT} from './local-command.mjs';
|
|
4
4
|
import {parseHistory,parseFollowHistory,HISTORY_FORMAT,historyPage,historySearchArgs,parseBlame,parseReferences,REFS_FORMAT} from './git-history.mjs';
|
|
5
5
|
import * as fs from 'node:fs/promises';
|
|
6
6
|
import {constants as fileFlags} from 'node:fs';
|
|
@@ -8,60 +8,17 @@ import path from 'node:path';
|
|
|
8
8
|
import { createHash } from 'node:crypto';
|
|
9
9
|
import { XMLParser, XMLValidator } from 'fast-xml-parser';
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const TIMEOUT = 15000;
|
|
11
|
+
import {decode, MAX_TEXT} from './text-content.mjs';
|
|
12
|
+
import {inside, confined} from './repository-path.mjs';
|
|
14
13
|
const xml = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '', parseTagValue: false, parseAttributeValue: false, trimValues: false, processEntities: true, isArray: name => ['entry', 'target', 'property'].includes(name) });
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
return new Promise((resolve, reject) => {
|
|
18
|
-
if (signal?.aborted) { reject(Object.assign(new Error('Discovery cancelled'), { code: 'ABORT_ERR' })); return; }
|
|
19
|
-
// Inherited Git overrides must not redirect reads to a different repository.
|
|
20
|
-
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.toUpperCase().startsWith('GIT_')));
|
|
21
|
-
Object.assign(env, { GIT_OPTIONAL_LOCKS: '0', GIT_TERMINAL_PROMPT: '0', GIT_NO_REPLACE_OBJECTS: '1', GIT_NO_LAZY_FETCH: '1', GIT_ALLOW_PROTOCOL: '', LC_ALL: process.platform === 'linux' ? 'C.UTF-8' : 'en_US.UTF-8' });
|
|
22
|
-
const child = spawn(command, args, { cwd, shell: false, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'], env });
|
|
23
|
-
const out = [], err = []; let size = 0, error, done = false;
|
|
24
|
-
const stop = e => { error ??= e; child.kill(); };
|
|
25
|
-
const abort = () => stop(Object.assign(new Error('Discovery cancelled'), { code: 'ABORT_ERR' }));
|
|
26
|
-
signal?.addEventListener('abort', abort, { once: true });
|
|
27
|
-
const timer = setTimeout(() => stop(Object.assign(new Error(command + ' timed out'), { code: 'TIMEOUT' })), timeoutMs);
|
|
28
|
-
child.stdout.on('data', b => { size += b.length; if (size > max) stop(Object.assign(new Error('Output exceeds size limit'), { code: 'TOO_LARGE' })); else out.push(b); });
|
|
29
|
-
child.stderr.on('data', b => { size += b.length; if (size > max) stop(Object.assign(new Error('Output exceeds size limit'), { code: 'TOO_LARGE' })); else err.push(b); });
|
|
30
|
-
const finish = e => { if (done) return; done = true; clearTimeout(timer); signal?.removeEventListener('abort', abort); if (e) reject(e); else resolve(Buffer.concat(out)); };
|
|
31
|
-
child.on('error', finish);
|
|
32
|
-
child.on('close', code => finish(error || (code !== 0 ? Object.assign(new Error(command + ': ' + Buffer.concat(err).toString('utf8').trim()), { code: 'VCS_COMMAND', exitCode: code }) : null)));
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
const git = (root, args, max, options) => run('git', ['--no-pager', '--literal-pathspecs', '-c', 'core.fsmonitor=false', ...args], root, max, options);
|
|
36
|
-
const svn = (root, args, max, options) => run('svn', ['--non-interactive', ...args], root, max, options);
|
|
15
|
+
|
|
37
16
|
function parseXML(buffer) {
|
|
38
17
|
const text = buffer.toString('utf8');
|
|
39
18
|
if (/<!DOCTYPE|<!ENTITY/i.test(text) || XMLValidator.validate(text) !== true) throw new Error('Invalid VCS XML');
|
|
40
19
|
return xml.parse(text);
|
|
41
20
|
}
|
|
42
|
-
|
|
43
|
-
async function confined(root, relative) {
|
|
44
|
-
if (typeof relative !== 'string' || relative.includes('\0') || path.isAbsolute(relative) || /^[a-z]:/i.test(relative)) throw new Error('Invalid repository path');
|
|
45
|
-
const target = path.resolve(root, relative);
|
|
46
|
-
if (!inside(root, target)) throw new Error('Path escapes repository');
|
|
47
|
-
// Inspect each existing component, including dangling links whose realpath fails.
|
|
48
|
-
let component = root;
|
|
49
|
-
for (const part of path.relative(root, target).split(path.sep).filter(Boolean)) {
|
|
50
|
-
component = path.join(component, part);
|
|
51
|
-
try {
|
|
52
|
-
if ((await fs.lstat(component)).isSymbolicLink()) {
|
|
53
|
-
const destination = path.resolve(path.dirname(component), await fs.readlink(component));
|
|
54
|
-
if (!inside(root, destination)) throw new Error('Symlink escapes repository');
|
|
55
|
-
}
|
|
56
|
-
} catch (e) { if (!['ENOENT', 'ENOTDIR'].includes(e.code)) throw e; }
|
|
57
|
-
}
|
|
58
|
-
let current = target;
|
|
59
|
-
for (;;) {
|
|
60
|
-
try { const real = await fs.realpath(current); if (!inside(root, real)) throw new Error('Symlink escapes repository'); break; }
|
|
61
|
-
catch (e) { if (!['ENOENT', 'ENOTDIR'].includes(e.code)) throw e; const parent = path.dirname(current); if (parent === current) throw e; current = parent; }
|
|
62
|
-
}
|
|
63
|
-
return target;
|
|
64
|
-
}
|
|
21
|
+
|
|
65
22
|
|
|
66
23
|
/** Detect the nearest enclosing Git or SVN working copy; never initializes one. */
|
|
67
24
|
export async function detectRepository(cwd) {
|
|
@@ -485,24 +442,8 @@ async function changes(repo, mode) {
|
|
|
485
442
|
}
|
|
486
443
|
/** Changes have opaque IDs, status words, and repo-relative paths. */
|
|
487
444
|
export async function listChanges(repo, mode = 'all') { repo = await checkedRepo(repo); modeFor(repo, mode); return changes(repo, mode); }
|
|
488
|
-
export
|
|
489
|
-
|
|
490
|
-
let text, encoding = 'UTF-8';
|
|
491
|
-
if (buffer[0] === 0xff && buffer[1] === 0xfe) { if (buffer.length % 2) return { text: '', binary: true, notice: 'Invalid UTF-16 file.' }; text = buffer.subarray(2).toString('utf16le'); encoding = 'UTF-16LE'; }
|
|
492
|
-
else if (buffer[0] === 0xfe && buffer[1] === 0xff) { const b = Buffer.from(buffer.subarray(2)); if (b.length % 2) return { text: '', binary: true, notice: 'Invalid UTF-16 file.' }; b.swap16(); text = b.toString('utf16le'); encoding = 'UTF-16BE'; }
|
|
493
|
-
else {
|
|
494
|
-
if (buffer.includes(0)) return { text: '', binary: true, notice: 'Binary file; text preview unavailable.' };
|
|
495
|
-
if (buffer.some(byte => byte < 32 && ![9,10,12,13].includes(byte))) return { text: '', binary: true, notice: 'Binary control bytes; text preview unavailable.' };
|
|
496
|
-
try { text = new TextDecoder('utf-8', { fatal: true }).decode(buffer); }
|
|
497
|
-
catch {
|
|
498
|
-
// A UTF-8 BOM is authoritative; do not reinterpret malformed UTF-8 as GBK.
|
|
499
|
-
if (buffer.subarray(0,3).equals(Buffer.from([0xef,0xbb,0xbf]))) return {text:'',binary:true,notice:'Invalid UTF-8 file.'};
|
|
500
|
-
try { text = new TextDecoder('gbk', {fatal:true}).decode(buffer); encoding = 'GBK'; }
|
|
501
|
-
catch { return {text:'',binary:true,notice:'Unsupported encoding or binary file; preview unavailable.'}; }
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
return { text, encoding };
|
|
505
|
-
}
|
|
445
|
+
// Keep the public adapter decoding export compatible with existing consumers.
|
|
446
|
+
export {decode} from './text-content.mjs';
|
|
506
447
|
async function content(fn) { try { return decode(await fn()); } catch (e) { if (e.code === 'TOO_LARGE') return { text: '', notice: 'File exceeds the 2 MiB preview limit.' }; throw e; } }
|
|
507
448
|
async function working(root, relative, svnLink = false) {
|
|
508
449
|
const target = await confined(root, relative);
|