import { assert } from "@fluidframework/common-utils" ; import * as resources from "@fluidframework/gitresources" ; import { buildHierarchy } from "@fluidframework/protocol-base" ; import * as api from "@fluidframework/protocol-definitions" ; import { debug } from "./debug" ; import { ICreateRefParamsExternal , IPatchRefParamsExternal , IGitManager , IHistorian } from "./storage" ; import { IWholeFlatSummary , IWholeSummaryPayload , IWriteSummaryResponse } from "./storageContracts" ; export class GitManager implements IGitManager { private readonly blobCache = new Map < string , resources . IBlob > ( ) ; private readonly commitCache = new Map < string , resources . ICommit > ( ) ; private readonly treeCache = new Map < string , resources . ITree > ( ) ; private readonly refCache = new Map < string , string > ( ) ; constructor ( private readonly historian ) { } public async getHeader ( id , sha ) { const header = await this . historian . getHeader ( sha ) ; for ( const blob of header . blobs ) { this . blobCache . set ( blob . sha , blob ) ; } return buildHierarchy ( header . tree ) ; } public async getFullTree ( sha ) { return this . historian . getFullTree ( sha ) ; } public async getCommit ( sha ) { if ( this . commitCache . has ( sha ) ) { debug ( `Cache hit on ${ sha }` ) ; return this . commitCache . get ( sha ) ; } return this . historian . getCommit ( sha ) ; } /**
     * Reads the object with the given ID. We defer to the client implementation to do the actual read.
     */ public async getCommits ( shaOrRef , count ) { let sha = shaOrRef ; if ( this . refCache . has ( shaOrRef ) ) { debug ( `Commit cache hit on ${ shaOrRef }` ) ; sha = this . refCache . get ( shaOrRef ) ; this . refCache . delete ( shaOrRef ) ; if ( ! sha ) { return [ ] ; } } if ( this . commitCache . has ( sha ) ) { const commit = this . commitCache . get ( sha ) ; return [ { commit : { author : commit . author , committer : commit . committer , message : commit . message , tree : commit . tree , url : commit . url } , parents : commit . parents , sha : commit . sha , url : commit . url } ] ; } return this . historian . getCommits ( sha , count ) ; } /**
     * Reads the object with the given ID. We defer to the client implementation to do the actual read.
     */ public async getTree ( root , recursive = true ) { if ( this . treeCache . has ( root ) ) { debug ( `Tree cache hit on ${ root }` ) ; return this . treeCache . get ( root ) ; } return this . historian . getTree ( root , recursive ) ; } public async getBlob ( sha ) { if ( this . blobCache . has ( sha ) ) { debug ( `Blob cache hit on ${ sha }` ) ; return this . blobCache . get ( sha ) ; } return this . historian . getBlob ( sha ) ; } public getRawUrl ( sha ) { return `${ this . historian . endpoint }/git/blobs/raw/${ sha }` ; } /**
     * Retrieves the object at the given revision number
     */ public getContent ( commit , path ) { return this . historian . getContent ( path , commit ) ; } public createBlob ( content , encoding ) { const blob = { content , encoding } ; return this . historian . createBlob ( blob ) ; } public createGitTree ( params ) { const treeP = this . historian . createTree ( params ) ; return treeP ; } public createTree ( files ) { return this . createTreeCore ( files , 0 ) ; } public async createCommit ( commit ) { return this . historian . createCommit ( commit ) ; } public async createSummary ( summary ) { return this . historian . createSummary ( summary ) ; } public async deleteSummary ( softDelete ) { return this . historian . deleteSummary ( softDelete ) ; } public async getSummary ( sha ) { return this . historian . getSummary ( sha ) ; } public async getRef ( ref ) { return this . historian . getRef ( `heads/${ ref }` ) . catch ( ( error ) => { if ( error === 400 || error === 404 ) { return null ; } else { return Promise . reject ( error ) ; } } ) ; } public async createRef ( branch , sha ) { const createRefParams = { ref : `refs/heads/${ branch }` , sha , config : { enabled : true } } ; return this . historian . createRef ( createRefParams ) ; } public async upsertRef ( branch , commitSha ) { const ref = { force : true , sha : commitSha , config : { enabled : true } } ; return this . historian . updateRef ( `heads/${ branch }` , ref ) ; } public addRef ( ref , sha ) { this . refCache . set ( ref , sha ) ; } public addCommit ( commit ) { this . commitCache . set ( commit . sha , commit ) ; } public addTree ( tree ) { this . treeCache . set ( tree . sha , tree ) ; } public addBlob ( blob ) { this . blobCache . set ( blob . sha , blob ) ; } /**
     * Writes to the object with the given ID
     */ public async write ( branch , inputTree , parents , message ) { const tree = await this . createTree ( inputTree ) ; const commitParams = { author : { date : new Date ( ) . toISOString ( ) , email : "kurtb@microsoft.com" , name : "Kurt Berglund" } , message , parents , tree : tree . sha } ; const commit = await this . historian . createCommit ( commitParams ) ; const existingRef = await this . getRef ( branch ) ; if ( existingRef ) { await this . upsertRef ( branch , commit . sha ) ; } else { await this . createRef ( branch , commit . sha ) ; } return commit ; } private async createTreeCore ( files , depth ) { if ( files . id ) { return this . getTree ( files . id ) ; } const entriesP = [ ] ; for ( const entry of files . entries ) { switch ( api . TreeEntry [ entry . type ] ) { case api . TreeEntry . Blob : const entryAsBlob = entry . value as api . IBlob ; if ( entry . mode === api . FileMode . Symlink ) { entryAsBlob . contents = this . translateSymlink ( entryAsBlob . contents , depth ) ; } const blobP = this . createBlob ( entryAsBlob . contents , entryAsBlob . encoding ) ; entriesP . push ( blobP ) ; break ; case api . TreeEntry . Tree : const entryAsTree = entry . value as api . ITree ; const treeBlobP = this . createTreeCore ( entryAsTree , depth + 1 ) ; entriesP . push ( treeBlobP ) ; break ; default : return Promise . reject ( new Error ( "Unknown entry type" ) ) ; } } const entries = await Promise . all ( entriesP ) ; const tree = [ ] ; assert ( entries . length === files . entries . length , "File entries length is not correct" ) ; for ( let i = 0 ; i < files . entries . length ; i ++ ) { const type = files . entries [ i ] . type === api . TreeEntry . Tree ? "tree" : ( files . entries [ i ] . type === api . TreeEntry . Blob ? "blob" : "commit" ) ; tree . push ( { mode : files . entries [ i ] . mode , path : files . entries [ i ] . path , sha : entries [ i ] . sha , type } ) ; } const requestBody = { tree } ; const treeP = this . historian . createTree ( requestBody ) ; return treeP ; } private translateSymlink ( link , depth ) { let prefix = "" ; for ( let i = 0 ; i <= depth ; i ++ ) { prefix += "../" ; } return `${ prefix }${ link }` ; } }