@feng0531/dsh-vcs 0.3.56 → 0.3.57
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/dist/client.js +3 -2
- package/dist/editor.js +1 -1
- package/git-history.mjs +12 -1
- package/index.mjs +3 -2
- package/package.json +1 -1
- package/vcs.mjs +8 -5
package/git-history.mjs
CHANGED
|
@@ -15,6 +15,17 @@ export function historySearchArgs(search){
|
|
|
15
15
|
return {args,paths};
|
|
16
16
|
}
|
|
17
17
|
// Each line-porcelain record carries its own metadata; never interpret filename as an access path.
|
|
18
|
+
export function decodeBlamePath(value){
|
|
19
|
+
if(!value.startsWith('"'))return value;
|
|
20
|
+
if(!value.endsWith('"'))throw Error('Invalid quoted blame path');
|
|
21
|
+
const bytes=[];const escapes={a:7,b:8,t:9,n:10,v:11,f:12,r:13,'"':34,'\\':92};
|
|
22
|
+
for(let i=1;i<value.length-1;i++){
|
|
23
|
+
if(value[i]!=='\\'){bytes.push(...Buffer.from(value[i],'utf8'));continue;}
|
|
24
|
+
const c=value[++i];if(Object.hasOwn(escapes,c)){bytes.push(escapes[c]);continue;}
|
|
25
|
+
const octal=value.slice(i,i+3);if(!/^[0-3][0-7]{2}$/.test(octal))throw Error('Invalid blame path escape');bytes.push(parseInt(octal,8));i+=2;
|
|
26
|
+
}
|
|
27
|
+
return new TextDecoder('utf-8',{fatal:true}).decode(Uint8Array.from(bytes));
|
|
28
|
+
}
|
|
18
29
|
export function parseBlame(text,maxLines=500){
|
|
19
30
|
if(typeof text!=='string'||Buffer.byteLength(text,'utf8')>2*1024*1024)throw new Error('Invalid blame output size');
|
|
20
31
|
if(!Number.isInteger(maxLines)||maxLines<1||maxLines>500)throw new Error('Invalid blame line limit');
|
|
@@ -35,7 +46,7 @@ export function parseBlame(text,maxLines=500){
|
|
|
35
46
|
}
|
|
36
47
|
if(i===fields.length||!['author','author-time','author-tz','summary','filename'].every(k=>Object.hasOwn(metadata,k)))throw new Error('Incomplete blame record');
|
|
37
48
|
if(!/^-?[0-9]+$/.test(metadata['author-time'])||!Number.isSafeInteger(Number(metadata['author-time']))||! /^[+-][0-9]{4}$/.test(metadata['author-tz']))throw new Error('Invalid blame timestamp');
|
|
38
|
-
rows.push({commit:header[1],originalLine,line,author:metadata.author,authorTime:Number(metadata['author-time']),authorTimezone:metadata['author-tz'],summary:metadata.summary,text:fields[i++].slice(1)});
|
|
49
|
+
rows.push({commit:header[1],originalLine,line,author:metadata.author,authorTime:Number(metadata['author-time']),authorTimezone:metadata['author-tz'],summary:metadata.summary,path:decodeBlamePath(metadata.filename),text:fields[i++].slice(1)});
|
|
39
50
|
if(rows.length>maxLines)throw new Error('Blame line limit exceeded');
|
|
40
51
|
}
|
|
41
52
|
return rows;
|
package/index.mjs
CHANGED
|
@@ -25,7 +25,7 @@ const rediscover = () => Object.assign(new Error('Repository authorization is mi
|
|
|
25
25
|
import {historySearchArgs} from './git-history.mjs';
|
|
26
26
|
function validatePayload(endpoint, payload) {
|
|
27
27
|
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) throw invalid('An object payload is required.');
|
|
28
|
-
const fields = endpoint==='vcs/tree-file'?['sessionId','repositoryId','commit','path'] : endpoint==='vcs/references'?['sessionId','repositoryId'] : endpoint==='vcs/commit-image'?['sessionId','repositoryId','commit','parentIndex','id','side'] : endpoint==='vcs/tree'?['sessionId','repositoryId','commit'] : endpoint==='vcs/file-history'?['sessionId','repositoryId','commit','parentIndex','id','offset','limit','follow'] : ['vcs/revision-changes','vcs/revision-compare'].includes(endpoint) ? ['sessionId','repositoryId','base','target',...(endpoint==='vcs/revision-compare'?['id']:[])] : ['vcs/commit-compare','vcs/blame'].includes(endpoint) ? ['sessionId','repositoryId','commit','parentIndex','id'] : endpoint === 'vcs/commit' ? ['sessionId','repositoryId','commit','parentIndex'] : endpoint === 'vcs/history' ? ['sessionId','repositoryId','snapshot','offset','limit','search'] : endpoint === 'vcs/repositories' ? ['sessionId', 'subdirectory'] : ['sessionId', 'repositoryId', 'mode', ...(endpoint === 'vcs/compare' ? ['id'] : [])];
|
|
28
|
+
const fields = endpoint==='vcs/tree-file'?['sessionId','repositoryId','commit','path'] : endpoint==='vcs/references'?['sessionId','repositoryId'] : endpoint==='vcs/commit-image'?['sessionId','repositoryId','commit','parentIndex','id','side'] : endpoint==='vcs/tree'?['sessionId','repositoryId','commit'] : endpoint==='vcs/file-history'?['sessionId','repositoryId','commit','parentIndex','id','offset','limit','follow'] : ['vcs/revision-changes','vcs/revision-compare'].includes(endpoint) ? ['sessionId','repositoryId','base','target',...(endpoint==='vcs/revision-compare'?['id']:[])] : ['vcs/commit-compare','vcs/blame'].includes(endpoint) ? ['sessionId','repositoryId','commit','parentIndex','id',...(endpoint==='vcs/blame'?['startLine','lineLimit']:[])] : endpoint === 'vcs/commit' ? ['sessionId','repositoryId','commit','parentIndex'] : endpoint === 'vcs/history' ? ['sessionId','repositoryId','snapshot','offset','limit','search'] : endpoint === 'vcs/repositories' ? ['sessionId', 'subdirectory'] : ['sessionId', 'repositoryId', 'mode', ...(endpoint === 'vcs/compare' ? ['id'] : [])];
|
|
29
29
|
if (Object.keys(payload).some(key => !fields.includes(key))) throw invalid('Only Session-addressed repository requests are supported; unknown payload field.');
|
|
30
30
|
if (endpoint === 'vcs/repositories') {
|
|
31
31
|
if (payload.subdirectory !== undefined) {
|
|
@@ -45,6 +45,7 @@ function validatePayload(endpoint, payload) {
|
|
|
45
45
|
if(typeof payload.commit!=='string'||!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(payload.commit))throw invalid('Invalid commit id.');
|
|
46
46
|
if(payload.parentIndex!==undefined&&(!Number.isInteger(payload.parentIndex)||payload.parentIndex<0||payload.parentIndex>100))throw invalid('Invalid parent index.');
|
|
47
47
|
}
|
|
48
|
+
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.');
|
|
48
49
|
if(endpoint==='vcs/file-history'&&payload.follow!==undefined&&typeof payload.follow!=='boolean')throw invalid('Invalid rename follow mode.');
|
|
49
50
|
if(endpoint==='vcs/history'){try{historySearchArgs(payload.search);}catch{throw invalid('Invalid history search.');}}
|
|
50
51
|
if(['vcs/history','vcs/file-history'].includes(endpoint)){
|
|
@@ -133,7 +134,7 @@ export function createHandler(ctx, api = adapter, maxActive = 4, { now = Date.no
|
|
|
133
134
|
: endpoint === 'vcs/tree'
|
|
134
135
|
? await api.getHistoricalTree({...repository},{commit:payload.commit,signal})
|
|
135
136
|
: endpoint === 'vcs/blame'
|
|
136
|
-
? await api.getFileBlame({...repository},{commit:payload.commit,parentIndex:payload.parentIndex??0,id:payload.id,signal})
|
|
137
|
+
? await api.getFileBlame({...repository},{commit:payload.commit,parentIndex:payload.parentIndex??0,id:payload.id,...(payload.startLine!==undefined?{startLine:payload.startLine}:{}),...(payload.lineLimit!==undefined?{lineLimit:payload.lineLimit}:{}),signal})
|
|
137
138
|
: endpoint === 'vcs/file-history'
|
|
138
139
|
? await api.listFileHistory({...repository},{commit:payload.commit,parentIndex:payload.parentIndex??0,id:payload.id,offset:payload.offset??0,limit:payload.limit??50,...(payload.follow!==undefined?{follow:payload.follow}:{}),signal})
|
|
139
140
|
: endpoint === 'vcs/revision-changes'
|
package/package.json
CHANGED
package/vcs.mjs
CHANGED
|
@@ -331,21 +331,24 @@ export async function getCommitImage(repo,{commit,parentIndex=0,id,side='right',
|
|
|
331
331
|
const {data,...metadata}=await preparePng(raw,{signal});
|
|
332
332
|
return {commit:revision,path:file,oid,...metadata,base64:data.toString('base64')};
|
|
333
333
|
}
|
|
334
|
-
export async function getFileBlame(repo,{commit,parentIndex=0,id,signal}={}){
|
|
334
|
+
export async function getFileBlame(repo,{commit,parentIndex=0,id,startLine=1,lineLimit=500,signal}={}){
|
|
335
|
+
if(!Number.isInteger(startLine)||startLine<1||startLine>100001||!Number.isInteger(lineLimit)||lineLimit<1||lineLimit>500)throw Error('Invalid blame line window');
|
|
335
336
|
if(typeof id!=='string'||!/^[a-f0-9]{64}$/.test(id))throw new Error('Invalid historical change id');
|
|
336
337
|
const details=await commitDetails(repo,{commit,parentIndex,signal});
|
|
337
338
|
const entry=details.changes.find(row=>row.id===id);if(!entry)throw new Error('Change is not part of selected commit');
|
|
338
|
-
const result={commit,path:entry.path,lines:[],truncated:false};
|
|
339
|
+
const result={commit,path:entry.path,lines:[],truncated:false,startLine,nextLine:null};
|
|
339
340
|
if(entry.status==='deleted')return {...result,notice:'File is deleted at this commit; blame unavailable.'};
|
|
340
341
|
const blob=await historicalBlob(repo.root,commit,entry.path,signal);
|
|
341
342
|
if(blob.notice||blob.binary||blob.encoding!=='UTF-8')return {...result,notice:blob.notice||'Blame supports UTF-8 regular text only.'};
|
|
342
343
|
if(!blob.text)return result;
|
|
343
|
-
const count=blob.text.split('\n').length-(blob.text.endsWith('\n')?1:0)
|
|
344
|
+
const count=blob.text.split('\n').length-(blob.text.endsWith('\n')?1:0);
|
|
345
|
+
if(startLine>count)return {...result,totalLines:count};
|
|
346
|
+
const endLine=Math.min(count,startLine+lineLimit-1),limit=endLine-startLine+1;
|
|
344
347
|
let ignored;
|
|
345
348
|
try{ignored=(await git(repo.root,['config','--null','--get-all','blame.ignoreRevsFile'],MAX_TEXT,{signal})).toString('utf8');}catch(e){if(e.code!=='VCS_COMMAND'||e.exitCode!==1)throw e;}
|
|
346
349
|
if(ignored?.split(String.fromCharCode(0)).some(value=>value.length>0))return {...result,notice:'Blame unavailable while blame.ignoreRevsFile is configured; external revision files are not read.'};
|
|
347
|
-
const output=await git(repo.root,['--literal-pathspecs','blame','--ignore-revs-file=','--line-porcelain','--no-textconv','--encoding=UTF-8','-L','
|
|
348
|
-
return {...result,lines:parseBlame(output.toString('utf8'),limit),truncated:count
|
|
350
|
+
const output=await git(repo.root,['--literal-pathspecs','blame','--ignore-revs-file=','--line-porcelain','--no-textconv','--encoding=UTF-8','-L',startLine+','+endLine,commit,'--',entry.path],MAX_TEXT,{signal});
|
|
351
|
+
return {...result,lines:parseBlame(output.toString('utf8'),limit),totalLines:count,nextLine:endLine<count&&endLine<100001?endLine+1:null,truncated:endLine<count};
|
|
349
352
|
}
|
|
350
353
|
export async function listFileHistory(repo,{commit,parentIndex=0,id,offset=0,limit=50,follow=false,signal}={}){
|
|
351
354
|
if(!Number.isInteger(offset)||offset<0||offset>10000||!Number.isInteger(limit)||limit<1||limit>100)throw new Error('Invalid history pagination');
|