@@ -8,17 +8,20 @@ import { Repo } from '@sourcebot/db';
88import { createLogger } from '@sourcebot/shared' ;
99import path from 'path' ;
1010import { simpleGit } from 'simple-git' ;
11- import { FileTreeItem , FileTreeNode } from './types' ;
11+ import { FileTreeItem } from './types' ;
12+ import { buildFileTree , isPathValid , normalizePath } from './utils' ;
13+ import { compareFileTreeItems } from './utils' ;
1214
1315const logger = createLogger ( 'file-tree' ) ;
1416
1517/**
16- * Returns the tree of files (blobs) and directories (trees) for a given repository,
17- * at a given revision.
18+ * Returns a file tree spanning the union of all provided paths for the given
19+ * repo/revision, including intermediate directories needed to connect them
20+ * into a single tree.
1821 */
19- export const getTree = async ( params : { repoName : string , revisionName : string } ) => sew ( ( ) =>
22+ export const getTree = async ( params : { repoName : string , revisionName : string , paths : string [ ] } ) => sew ( ( ) =>
2023 withOptionalAuthV2 ( async ( { org, prisma } ) => {
21- const { repoName, revisionName } = params ;
24+ const { repoName, revisionName, paths } = params ;
2225 const repo = await prisma . repo . findFirst ( {
2326 where : {
2427 name : repoName ,
@@ -33,21 +36,30 @@ export const getTree = async (params: { repoName: string, revisionName: string }
3336 const { path : repoPath } = getRepoPath ( repo ) ;
3437
3538 const git = simpleGit ( ) . cwd ( repoPath ) ;
39+ if ( ! paths . every ( path => isPathValid ( path ) ) ) {
40+ return notFound ( ) ;
41+ }
3642
37- let result : string ;
43+ const normalizedPaths = paths . map ( path => normalizePath ( path ) ) ;
44+
45+ let result : string = '' ;
3846 try {
39- result = await git . raw ( [
47+
48+ const command = [
4049 // Disable quoting of non-ASCII characters in paths
4150 '-c' , 'core.quotePath=false' ,
4251 'ls-tree' ,
4352 revisionName ,
44- // recursive
45- '-r' ,
46- // include trees when recursing
47- '-t' ,
4853 // format as output as {type},{path}
4954 '--format=%(objecttype),%(path)' ,
50- ] ) ;
55+ // include tree nodes
56+ '-t' ,
57+ '--' ,
58+ '.' ,
59+ ...normalizedPaths ,
60+ ] ;
61+
62+ result = await git . raw ( command ) ;
5163 } catch ( error ) {
5264 logger . error ( 'git ls-tree failed.' , { error } ) ;
5365 return unexpectedError ( 'git ls-tree command failed.' ) ;
@@ -90,31 +102,12 @@ export const getFolderContents = async (params: { repoName: string, revisionName
90102 }
91103
92104 const { path : repoPath } = getRepoPath ( repo ) ;
105+ const git = simpleGit ( ) . cwd ( repoPath ) ;
93106
94- // @note : we don't allow directory traversal
95- // or null bytes in the path.
96- if ( path . includes ( '..' ) || path . includes ( '\0' ) ) {
107+ if ( ! isPathValid ( path ) ) {
97108 return notFound ( ) ;
98109 }
99-
100- // Normalize the path by...
101- let normalizedPath = path ;
102-
103- // ... adding a trailing slash if it doesn't have one.
104- // This is important since ls-tree won't return the contents
105- // of a directory if it doesn't have a trailing slash.
106- if ( ! normalizedPath . endsWith ( '/' ) ) {
107- normalizedPath = `${ normalizedPath } /` ;
108- }
109-
110- // ... removing any leading slashes. This is needed since
111- // the path is relative to the repository's root, so we
112- // need a relative path.
113- if ( normalizedPath . startsWith ( '/' ) ) {
114- normalizedPath = normalizedPath . slice ( 1 ) ;
115- }
116-
117- const git = simpleGit ( ) . cwd ( repoPath ) ;
110+ const normalizedPath = normalizePath ( path ) ;
118111
119112 let result : string ;
120113 try {
@@ -145,6 +138,9 @@ export const getFolderContents = async (params: { repoName: string, revisionName
145138 }
146139 } ) ;
147140
141+ // Sort the contents in place, first by type (trees before blobs), then by name.
142+ contents . sort ( compareFileTreeItems ) ;
143+
148144 return contents ;
149145 } ) ) ;
150146
@@ -199,60 +195,6 @@ export const getFiles = async (params: { repoName: string, revisionName: string
199195
200196 } ) ) ;
201197
202- const buildFileTree = ( flatList : { type : string , path : string } [ ] ) : FileTreeNode => {
203- const root : FileTreeNode = {
204- name : 'root' ,
205- path : '' ,
206- type : 'tree' ,
207- children : [ ] ,
208- } ;
209-
210- for ( const item of flatList ) {
211- const parts = item . path . split ( '/' ) ;
212- let current : FileTreeNode = root ;
213-
214- for ( let i = 0 ; i < parts . length ; i ++ ) {
215- const part = parts [ i ] ;
216- const isLeaf = i === parts . length - 1 ;
217- const nodeType = isLeaf ? item . type : 'tree' ;
218- let next = current . children . find ( ( child : FileTreeNode ) => child . name === part && child . type === nodeType ) ;
219-
220- if ( ! next ) {
221- next = {
222- name : part ,
223- path : item . path ,
224- type : nodeType ,
225- children : [ ] ,
226- } ;
227- current . children . push ( next ) ;
228- }
229- current = next ;
230- }
231- }
232-
233- const sortTree = ( node : FileTreeNode ) : FileTreeNode => {
234- if ( node . type === 'blob' ) {
235- return node ;
236- }
237-
238- const sortedChildren = node . children
239- . map ( sortTree )
240- . sort ( ( a : FileTreeNode , b : FileTreeNode ) => {
241- if ( a . type !== b . type ) {
242- return a . type === 'tree' ? - 1 : 1 ;
243- }
244- return a . name . localeCompare ( b . name , undefined , { sensitivity : 'base' } ) ;
245- } ) ;
246-
247- return {
248- ...node ,
249- children : sortedChildren ,
250- } ;
251- } ;
252-
253- return sortTree ( root ) ;
254- }
255-
256198// @todo : this is duplicated from the `getRepoPath` function in the
257199// backend's `utils.ts` file. Eventually we should move this to a shared
258200// package.
0 commit comments