@feng0531/dsh-vcs 0.3.51 → 0.3.54

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/git-history.mjs CHANGED
@@ -1,3 +1,19 @@
1
+ // Search values are literal, bounded and never revision/options or pathspec expressions.
2
+ export function historySearchArgs(search){
3
+ if(search===undefined)return {args:[],paths:[]};
4
+ if(!search||typeof search!=='object'||Array.isArray(search)||Object.keys(search).some(k=>!['message','author','path'].includes(k)))throw new Error('Invalid history search');
5
+ const args=['--fixed-strings','--regexp-ignore-case'],paths=[];
6
+ for(const key of ['message','author','path']){
7
+ const value=search[key];if(value===undefined)continue;
8
+ if(typeof value!=='string'||value.length>1024||/[\0\r\n]/.test(value))throw new Error('Invalid history search value');
9
+ if(!value)continue;
10
+ if(key==='path'){
11
+ if(value.startsWith('/')||value.includes('\\')||value.split('/').some(p=>!p||p==='.'||p==='..'))throw new Error('Invalid history search path');
12
+ paths.push(value);
13
+ }else args.push((key==='message'?'--grep=':'--author=')+value);
14
+ }
15
+ return {args,paths};
16
+ }
1
17
  // Each line-porcelain record carries its own metadata; never interpret filename as an access path.
2
18
  export function parseBlame(text,maxLines=500){
3
19
  if(typeof text!=='string'||Buffer.byteLength(text,'utf8')>2*1024*1024)throw new Error('Invalid blame output size');
package/index.mjs CHANGED
@@ -22,9 +22,10 @@ export async function resolveSessionCwd(ctx, sessionId, signal) {
22
22
 
23
23
  const rediscover = () => Object.assign(new Error('Repository authorization is missing, expired, or changed. Rediscover repositories for this Session and retry.'), { code: 'vcs/rediscover-required' });
24
24
 
25
+ import {historySearchArgs} from './git-history.mjs';
25
26
  function validatePayload(endpoint, payload) {
26
27
  if (!payload || typeof payload !== 'object' || Array.isArray(payload)) throw invalid('An object payload is required.');
27
- 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'] : ['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'] : 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'] : ['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
29
  if (Object.keys(payload).some(key => !fields.includes(key))) throw invalid('Only Session-addressed repository requests are supported; unknown payload field.');
29
30
  if (endpoint === 'vcs/repositories') {
30
31
  if (payload.subdirectory !== undefined) {
@@ -44,6 +45,7 @@ function validatePayload(endpoint, payload) {
44
45
  if(typeof payload.commit!=='string'||!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(payload.commit))throw invalid('Invalid commit id.');
45
46
  if(payload.parentIndex!==undefined&&(!Number.isInteger(payload.parentIndex)||payload.parentIndex<0||payload.parentIndex>100))throw invalid('Invalid parent index.');
46
47
  }
48
+ if(endpoint==='vcs/history'){try{historySearchArgs(payload.search);}catch{throw invalid('Invalid history search.');}}
47
49
  if(['vcs/history','vcs/file-history'].includes(endpoint)){
48
50
  if(payload.offset===null||payload.limit===null)throw invalid('Invalid history pagination.');
49
51
  if(payload.snapshot!==undefined&&(typeof payload.snapshot!=='string'||!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(payload.snapshot)))throw invalid('Invalid history snapshot.');
@@ -142,7 +144,7 @@ export function createHandler(ctx, api = adapter, maxActive = 4, { now = Date.no
142
144
  : endpoint === 'vcs/commit'
143
145
  ? await api.getCommitDetails({...repository},{commit:payload.commit,parentIndex:payload.parentIndex??0,signal})
144
146
  : endpoint === 'vcs/history'
145
- ? await api.listHistory({...repository},{snapshot:payload.snapshot,offset:payload.offset??0,limit:payload.limit??50,signal})
147
+ ? await api.listHistory({...repository},{snapshot:payload.snapshot,search:payload.search,offset:payload.offset??0,limit:payload.limit??50,signal})
146
148
  : endpoint === 'vcs/status'
147
149
  ? { cwd, repository: { ...repository }, changes: await api.listChanges({ ...repository }, mode), mode }
148
150
  : await api.getComparison({ ...repository }, { mode, id: payload.id });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feng0531/dsh-vcs",
3
- "version": "0.3.51",
3
+ "version": "0.3.54",
4
4
  "private": false,
5
5
  "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
6
6
  "type": "module",
package/vcs.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import {preparePng} from './image-preview.mjs';
2
2
  import { spawn } from 'node:child_process';
3
- import {parseHistory,HISTORY_FORMAT,historyPage,parseBlame,parseReferences,REFS_FORMAT} from './git-history.mjs';
3
+ import {parseHistory,HISTORY_FORMAT,historyPage,historySearchArgs,parseBlame,parseReferences,REFS_FORMAT} from './git-history.mjs';
4
4
  import * as fs from 'node:fs/promises';
5
5
  import path from 'node:path';
6
6
  import { createHash } from 'node:crypto';
@@ -221,7 +221,8 @@ async function checkedRepo(repo, signal) {
221
221
  if (!found || found.type !== repo.type || path.resolve(found.root) !== path.resolve(repo.root)) throw new Error('Repository root changed or is invalid');
222
222
  return found;
223
223
  }
224
- export async function listHistory(repo, {snapshot,offset=0,limit=50,signal} = {}) {
224
+ export async function listHistory(repo, {snapshot,offset=0,limit=50,search,signal} = {}) {
225
+ const query=historySearchArgs(search);
225
226
  if(!Number.isInteger(offset)||offset<0||offset>10000||!Number.isInteger(limit)||limit<1||limit>100) throw new Error('Invalid history pagination');
226
227
  if(snapshot!==undefined && (typeof snapshot!=='string'||!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(snapshot))) throw new Error('Invalid history snapshot');
227
228
  if(signal?.aborted) throw Object.assign(new Error('History cancelled'),{code:'ABORT_ERR'});
@@ -238,7 +239,7 @@ export async function listHistory(repo, {snapshot,offset=0,limit=50,signal} = {}
238
239
  throw e;
239
240
  }
240
241
  }
241
- const text=await git(repo.root,['log','-z','--no-show-signature','--encoding=UTF-8','--topo-order','--max-count='+String(limit+1),'--skip='+String(offset),'--format='+HISTORY_FORMAT,snapshot,'--'],MAX_TEXT,{signal});
242
+ const text=await git(repo.root,['--literal-pathspecs','log','-z','--no-show-signature','--encoding=UTF-8','--topo-order','--max-count='+String(limit+1),'--skip='+String(offset),'--format='+HISTORY_FORMAT,...query.args,snapshot,'--',...query.paths],MAX_TEXT,{signal});
242
243
  const rows=parseHistory(text.toString('utf8'),limit+1);
243
244
  return historyPage(rows,snapshot,offset,limit);
244
245
  }