@feng0531/dsh-vcs 0.3.68 → 0.3.69
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 +3 -2
- package/dist/client.js +3 -3
- package/dist/editor.js +87 -87
- package/index.mjs +6 -5
- package/package.json +1 -1
- package/text-content.mjs +24 -0
- package/vcs.mjs +23 -10
package/index.mjs
CHANGED
|
@@ -34,11 +34,11 @@ const REQUEST_FIELDS = {
|
|
|
34
34
|
'vcs/compare': ['sessionId', 'repositoryId', 'mode', 'id'],
|
|
35
35
|
'vcs/history': ['sessionId', 'repositoryId', 'snapshot', 'offset', 'limit', 'search'],
|
|
36
36
|
'vcs/commit': ['sessionId', 'repositoryId', 'commit', 'parentIndex'],
|
|
37
|
-
'vcs/commit-compare': ['sessionId', 'repositoryId', 'commit', 'parentIndex', 'id'],
|
|
37
|
+
'vcs/commit-compare': ['sessionId', 'repositoryId', 'commit', 'parentIndex', 'id', 'large'],
|
|
38
38
|
'vcs/file-history': ['sessionId', 'repositoryId', 'commit', 'parentIndex', 'id', 'offset', 'limit', 'follow'],
|
|
39
39
|
'vcs/blame': ['sessionId', 'repositoryId', 'commit', 'parentIndex', 'id', 'startLine', 'lineLimit'],
|
|
40
40
|
'vcs/revision-changes': ['sessionId', 'repositoryId', 'base', 'target'],
|
|
41
|
-
'vcs/revision-compare': ['sessionId', 'repositoryId', 'base', 'target', 'id'],
|
|
41
|
+
'vcs/revision-compare': ['sessionId', 'repositoryId', 'base', 'target', 'id', 'large'],
|
|
42
42
|
'vcs/revision-image': ['sessionId', 'repositoryId', 'base', 'target', 'id', 'side'],
|
|
43
43
|
'vcs/tree': ['sessionId', 'repositoryId', 'commit'],
|
|
44
44
|
'vcs/tree-file': ['sessionId', 'repositoryId', 'commit', 'path'],
|
|
@@ -68,6 +68,7 @@ function validatePayload(endpoint, payload) {
|
|
|
68
68
|
if(typeof payload.commit!=='string'||!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(payload.commit))throw invalid('Invalid commit id.');
|
|
69
69
|
if(payload.parentIndex!==undefined&&(!Number.isInteger(payload.parentIndex)||payload.parentIndex<0||payload.parentIndex>100))throw invalid('Invalid parent index.');
|
|
70
70
|
}
|
|
71
|
+
if(['vcs/commit-compare','vcs/revision-compare'].includes(endpoint)&&payload.large!==undefined&&typeof payload.large!=='boolean')throw invalid('Invalid large comparison mode.');
|
|
71
72
|
if(endpoint==='vcs/tree-segment'&&payload.offset!==undefined&&(!Number.isInteger(payload.offset)||payload.offset<0||payload.offset>16777216))throw invalid('Invalid segment offset.');
|
|
72
73
|
if(endpoint==='vcs/blame'&&((payload.startLine!==undefined&&(!Number.isInteger(payload.startLine)||payload.startLine<1||payload.startLine>100001))||(payload.lineLimit!==undefined&&(!Number.isInteger(payload.lineLimit)||payload.lineLimit<1||payload.lineLimit>500))))throw invalid('Invalid blame line window.');
|
|
73
74
|
if(endpoint==='vcs/file-history'&&payload.follow!==undefined&&typeof payload.follow!=='boolean')throw invalid('Invalid rename follow mode.');
|
|
@@ -111,9 +112,9 @@ async function readRepository(api, endpoint, repository, payload, {cwd, mode, si
|
|
|
111
112
|
case 'vcs/revision-changes':
|
|
112
113
|
return api.getRevisionChanges({...repository}, {base: payload.base, target: payload.target, signal});
|
|
113
114
|
case 'vcs/revision-compare':
|
|
114
|
-
return api.getRevisionComparison({...repository}, {base: payload.base, target: payload.target, id: payload.id, signal});
|
|
115
|
+
return api.getRevisionComparison({...repository}, {base: payload.base, target: payload.target, id: payload.id, ...(payload.large !== undefined ? {large: payload.large} : {}), signal});
|
|
115
116
|
case 'vcs/commit-compare':
|
|
116
|
-
return api.getCommitComparison({...repository}, {commit: payload.commit, parentIndex: payload.parentIndex ?? 0, id: payload.id, signal});
|
|
117
|
+
return api.getCommitComparison({...repository}, {commit: payload.commit, parentIndex: payload.parentIndex ?? 0, id: payload.id, ...(payload.large !== undefined ? {large: payload.large} : {}), signal});
|
|
117
118
|
case 'vcs/commit':
|
|
118
119
|
return api.getCommitDetails({...repository}, {commit: payload.commit, parentIndex: payload.parentIndex ?? 0, signal});
|
|
119
120
|
case 'vcs/history':
|
|
@@ -194,7 +195,7 @@ export function createHandler(ctx, api = adapter, maxActive = 4, { now = Date.no
|
|
|
194
195
|
// Both adapter operations revalidate the canonical root; comparison also validates
|
|
195
196
|
// the change ID against fresh status, so do not duplicate a full status scan here.
|
|
196
197
|
if(['vcs/history','vcs/commit','vcs/commit-compare','vcs/revision-changes','vcs/revision-compare','vcs/file-history','vcs/blame','vcs/tree','vcs/tree-file','vcs/tree-segment','vcs/commit-image','vcs/revision-image','vcs/workspace-image','vcs/references'].includes(endpoint)&&repository.type!=='git')throw invalid('History currently supports Git only.');
|
|
197
|
-
if(endpoint==='vcs/tree-segment'){if(activeSegments>=1)return failure('vcs/busy','A text segment is already loading. Please retry.');activeSegments++;segmentSlot=true;}
|
|
198
|
+
if(endpoint==='vcs/tree-segment'||(['vcs/commit-compare','vcs/revision-compare'].includes(endpoint)&&payload.large===true)){if(activeSegments>=1)return failure('vcs/busy','A text segment is already loading. Please retry.');activeSegments++;segmentSlot=true;}
|
|
198
199
|
if(['vcs/commit-image','vcs/revision-image','vcs/workspace-image'].includes(endpoint)){
|
|
199
200
|
if(activeImages>=2)return failure('vcs/busy','Too many image requests. Please retry.');
|
|
200
201
|
activeImages++;imageSlot=true;
|
package/package.json
CHANGED
package/text-content.mjs
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
/** Pure bounded byte decoding shared by working and committed previews. */
|
|
2
2
|
export const MAX_TEXT = 2 * 1024 * 1024;
|
|
3
|
+
export const MAX_LARGE_TEXT = 8 * 1024 * 1024;
|
|
4
|
+
|
|
5
|
+
/** Explicit historical mode only: complete UTF-8, never a truncated Diff input. */
|
|
6
|
+
export function decodeLargeText(buffer, signal) {
|
|
7
|
+
signal?.throwIfAborted();
|
|
8
|
+
if (buffer.length > MAX_LARGE_TEXT) throw new Error('Large comparison exceeds 8 MiB per side.');
|
|
9
|
+
if (buffer.some(byte => byte < 32 && ![9, 10, 12, 13].includes(byte))) {
|
|
10
|
+
throw new Error('Large comparison requires UTF-8 text without binary control bytes.');
|
|
11
|
+
}
|
|
12
|
+
let text;
|
|
13
|
+
try { text = new TextDecoder('utf-8', {fatal: true}).decode(buffer); }
|
|
14
|
+
catch { throw new Error('Large comparison requires valid UTF-8 text.'); }
|
|
15
|
+
let lines = 1, length = 0;
|
|
16
|
+
for (let index = 0; index < text.length; index++) {
|
|
17
|
+
if (index % 65536 === 0) signal?.throwIfAborted();
|
|
18
|
+
if (text[index] === '\n' || text[index] === '\r') {
|
|
19
|
+
if (text[index] === '\r' && text[index + 1] === '\n') index++;
|
|
20
|
+
if (++lines > 100000) throw new Error('Large comparison exceeds 100000 lines per side.');
|
|
21
|
+
length = 0;
|
|
22
|
+
} else if (++length > 65536) throw new Error('Large comparison exceeds 65536 characters per line.');
|
|
23
|
+
}
|
|
24
|
+
signal?.throwIfAborted();
|
|
25
|
+
return {text, encoding: 'UTF-8', totalBytes: buffer.length, lineCount: lines};
|
|
26
|
+
}
|
|
3
27
|
|
|
4
28
|
export function decode(buffer) {
|
|
5
29
|
if (buffer.length > MAX_TEXT) {
|
package/vcs.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import path from 'node:path';
|
|
|
8
8
|
import { createHash } from 'node:crypto';
|
|
9
9
|
import { XMLParser, XMLValidator } from 'fast-xml-parser';
|
|
10
10
|
|
|
11
|
-
import {decode, MAX_TEXT} from './text-content.mjs';
|
|
11
|
+
import {decode, MAX_TEXT, MAX_LARGE_TEXT, decodeLargeText} from './text-content.mjs';
|
|
12
12
|
import {inside, confined} from './repository-path.mjs';
|
|
13
13
|
const xml = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '', parseTagValue: false, parseAttributeValue: false, trimValues: false, processEntities: true, isArray: name => ['entry', 'target', 'property'].includes(name) });
|
|
14
14
|
|
|
@@ -455,35 +455,48 @@ async function working(root, relative, svnLink = false) {
|
|
|
455
455
|
try { const info = await handle.stat(); if (!info.isFile()) throw new Error('Not a regular file'); const buffer = Buffer.alloc(MAX_TEXT + 1); const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0); await confined(root, relative); return decode(buffer.subarray(0, bytesRead)); } finally { await handle.close(); }
|
|
456
456
|
}
|
|
457
457
|
async function gitBlob(root, revision, file) { return content(() => git(root, ['show', '--no-ext-diff', '--no-textconv', revision + ':' + file], MAX_TEXT)); }
|
|
458
|
-
async function historicalBlob(root,revision,file,signal){
|
|
458
|
+
async function historicalBlob(root,revision,file,signal,large=false){
|
|
459
459
|
const listing=(await git(root,['ls-tree','-z',revision,'--',file],MAX_TEXT,{signal})).toString('utf8');
|
|
460
460
|
const records=listing.split(String.fromCharCode(0)).filter(Boolean);
|
|
461
461
|
const record=records.find(row=>row.slice(row.indexOf(' ')+1)===file);
|
|
462
462
|
if(!record)throw new Error('Historical path missing');
|
|
463
463
|
const [mode,type,oid]=record.slice(0,record.indexOf(' ')).split(' ');
|
|
464
|
+
if(large&&(!['100644','100755'].includes(mode)||type!=='blob'))throw new Error('Large comparison requires a committed regular file.');
|
|
464
465
|
if(mode==='160000')return {text:oid,notice:'Submodule commit reference; repository contents are not loaded.'};
|
|
465
466
|
if(type!=='blob'||!['100644','100755','120000'].includes(mode))return {text:'',notice:'Unsupported historical object type.'};
|
|
467
|
+
if (large) {
|
|
468
|
+
const rawSize = (await git(root, ['cat-file', '-s', oid], MAX_TEXT, {signal})).toString('utf8').trim();
|
|
469
|
+
if (!/^\d+$/.test(rawSize)) throw new Error('Invalid historical object size.');
|
|
470
|
+
const size = Number(rawSize);
|
|
471
|
+
if (size > MAX_LARGE_TEXT) throw new Error('Large comparison exceeds 8 MiB per side.');
|
|
472
|
+
const buffer = await git(root, ['cat-file', 'blob', oid], MAX_LARGE_TEXT, {signal});
|
|
473
|
+
if (buffer.length !== size) throw new Error('Historical object size changed.');
|
|
474
|
+
return decodeLargeText(buffer, signal);
|
|
475
|
+
}
|
|
476
|
+
|
|
466
477
|
const result=await content(()=>git(root,['cat-file','blob',oid],MAX_TEXT,{signal}));
|
|
467
478
|
if(mode==='120000')result.notice=[result.notice,'Symbolic link target text; not followed.'].filter(Boolean).join(' ');
|
|
468
479
|
return result;
|
|
469
480
|
}
|
|
470
|
-
export async function getCommitComparison(repo,{commit,parentIndex=0,id,signal}={}){
|
|
481
|
+
export async function getCommitComparison(repo,{commit,parentIndex=0,id,signal,large=false}={}){
|
|
482
|
+
if(typeof large!=='boolean')throw new Error('Invalid large comparison mode');
|
|
471
483
|
if(typeof id!=='string'||!/^[a-f0-9]{64}$/.test(id))throw new Error('Invalid historical change id');
|
|
472
484
|
const details=await commitDetails(repo,{commit,parentIndex,signal});
|
|
473
485
|
const entry=details.changes.find(row=>row.id===id);
|
|
474
486
|
if(!entry)throw new Error('Historical change is not part of selected commit and parent');
|
|
475
|
-
const left=details.parent&&entry.status!=='added'?await historicalBlob(repo.root,details.parent,entry.oldPath||entry.path,signal):{text:''};
|
|
476
|
-
const right=entry.status!=='deleted'?await historicalBlob(repo.root,commit,entry.path,signal):{text:''};
|
|
477
|
-
return {...entry,left:{...left,label:details.parent||'Empty tree'},right:{...right,label:commit},binary:!!(left.binary||right.binary),notice:[left.notice,right.notice].filter(Boolean).join(' ')};
|
|
487
|
+
const left=details.parent&&entry.status!=='added'?await historicalBlob(repo.root,details.parent,entry.oldPath||entry.path,signal,large):{text:''};
|
|
488
|
+
const right=entry.status!=='deleted'?await historicalBlob(repo.root,commit,entry.path,signal,large):{text:''};
|
|
489
|
+
return {...entry,large,left:{...left,label:details.parent||'Empty tree'},right:{...right,label:commit},binary:!!(left.binary||right.binary),notice:[left.notice,right.notice].filter(Boolean).join(' ')};
|
|
478
490
|
}
|
|
479
|
-
export async function getRevisionComparison(repo,{base,target,id,signal}={}){
|
|
491
|
+
export async function getRevisionComparison(repo,{base,target,id,signal,large=false}={}){
|
|
492
|
+
if(typeof large!=='boolean')throw new Error('Invalid large comparison mode');
|
|
480
493
|
if(typeof id!=='string'||!/^[a-f0-9]{64}$/.test(id))throw new Error('Invalid revision change id');
|
|
481
494
|
const details=await getRevisionChanges(repo,{base,target,signal});
|
|
482
495
|
const entry=details.changes.find(row=>row.id===id);
|
|
483
496
|
if(!entry)throw new Error('Change is not part of selected revision pair');
|
|
484
|
-
const left=entry.status!=='added'?await historicalBlob(repo.root,base,entry.oldPath||entry.path,signal):{text:''};
|
|
485
|
-
const right=entry.status!=='deleted'?await historicalBlob(repo.root,target,entry.path,signal):{text:''};
|
|
486
|
-
return {...entry,base,target,left:{...left,label:base},right:{...right,label:target},binary:!!(left.binary||right.binary),notice:[left.notice,right.notice].filter(Boolean).join(' ')};
|
|
497
|
+
const left=entry.status!=='added'?await historicalBlob(repo.root,base,entry.oldPath||entry.path,signal,large):{text:''};
|
|
498
|
+
const right=entry.status!=='deleted'?await historicalBlob(repo.root,target,entry.path,signal,large):{text:''};
|
|
499
|
+
return {...entry,base,target,large,left:{...left,label:base},right:{...right,label:target},binary:!!(left.binary||right.binary),notice:[left.notice,right.notice].filter(Boolean).join(' ')};
|
|
487
500
|
}
|
|
488
501
|
async function properties(root, file, base) {
|
|
489
502
|
const result = parseXML(await svn(root, ['proplist', '--xml', '--verbose', ...(base ? ['--revision', 'BASE'] : []), '--', file + '@']));
|