import fs from 'fs' ; import path from 'path' ; import http , { ServerOptions } from 'http' ; import https from 'https' ; import url from 'url' ; import qs from 'querystring' ; import { HttpDuplex } from './http-duplex' ; import { spawn } from 'child_process' ; import { EventEmitter } from 'events' ; import { parseGitName , createAction , infoResponse , basicAuth , noCache , } from './util' ; import { ServiceString } from './types' ; const services = [ 'upload-pack' , 'receive-pack' ] ; interface GitServerOptions extends ServerOptions { type : 'http' | 'https' ; } export interface GitOptions { autoCreate ? : boolean ; authenticate ? : ( options : GitAuthenticateOptions , callback : ( error ? : Error ) => void | undefined ) => void | Promise < Error | undefined | void > | undefined ; checkout ? : boolean ; } export interface GitAuthenticateOptions { type : string ; repo : string ; user : ( ( ) => Promise < [ string | undefined , string | undefined ] > ) & ( ( callback : ( username ? : string | undefined , password ? : string | undefined ) => void ) => void ) ; headers : http . IncomingHttpHeaders ; } /**
 * An http duplex object (see below) with these extra properties:
 */ export interface TagData extends HttpDuplex { repo : string ; commit : string ; version : string ; } /**
 * Is a http duplex object (see below) with these extra properties
 */ export interface PushData extends HttpDuplex { repo : string ; commit : string ; branch : string ; } /**
 * an http duplex object (see below) with these extra properties
 */ export interface FetchData extends HttpDuplex { repo : string ; commit : string ; } /**
 * an http duplex object (see below) with these extra properties
 */ export interface InfoData extends HttpDuplex { repo : string ; } /**
 * an http duplex object (see below) with these extra properties
 */ export interface HeadData extends HttpDuplex { repo : string ; } export interface GitEvents { example on ( event , listener ) : this ; example on ( event , listener ) : this ; example on ( event , listener ) : this ; example on ( event , listener ) : this ; example on ( event , listener ) : this ; } export class Git extends EventEmitter implements GitEvents { dirMap : ( dir ? : string ) => string ; authenticate : ( ( options : GitAuthenticateOptions , callback : ( error ? : Error ) => void | undefined ) => void | Promise < Error | undefined | void > | undefined ) | undefined ; autoCreate : boolean ; checkout : boolean | undefined ; server : https . Server | http . Server | undefined ; param repoDir param options param options . autoCreate param options . authenticate param options . checkout constructor ( repoDir , options = { } ) { super ( ) ; if ( typeof repoDir === 'function' ) { this . dirMap = repoDir ; } else { this . dirMap = ( dir ? ) : string => { return path . normalize ( ( dir ? path . join ( repoDir , dir ) : repoDir ) as string ) ; } ; } if ( options . authenticate ) { this . authenticate = options . authenticate ; } this . autoCreate = options . autoCreate === false ? false : true ; this . checkout = options . checkout ; } param { Function } callback list ( callback ) ; list ( ) ; list ( callback ? ) { const execf = ( res , rej ) => fs . readdir ( this . dirMap ( ) , ( error , results ) => { if ( error ) return rej ( error ) ; const repos = results . filter ( ( r ) => { return r . substring ( r . length - 3 , r . length ) == 'git' ; } , [ ] ) ; res ( repos ) ; } ) ; if ( callback ) return execf ( ( repos ) => callback ( void 0 , repos ) , ( err ) => callback ( err , void 0 ) ) ; else return new Promise < string [ ] > ( ( res , rej ) => execf ( res , rej ) ) ; } param repo param callback exists ( repo ) { return fs . existsSync ( this . dirMap ( repo ) ) ; } param dir param callback mkdir ( dir ) { fs . mkdirSync ( path . dirname ( dir ) , { recursive : true } ) ; } param repo param callback create ( repo , callback ) { function next ( self ) { let ps ; let _error = '' ; const dir = self . dirMap ( repo ) ; if ( self . checkout ) { ps = spawn ( 'git' , [ 'init' , dir ] ) ; } else { ps = spawn ( 'git' , [ 'init' , '--bare' , dir ] ) ; } ps . stderr . on ( 'data' , function ( chunk ) { _error += chunk ; } ) ; ps . on ( 'exit' , ( code ) => { if ( ! callback ) { return ; } else if ( code ) { callback ( new Error ( _error ) ) ; } else { callback ( ) ; } } ) ; } if ( typeof callback !== 'function' ) callback = ( ) => { return ; } ; if ( ! /\.git$/ . test ( repo ) ) repo += '.git' ; const exists = this . exists ( repo ) ; if ( ! exists ) { this . mkdir ( repo ) ; } next ( this ) ; } param service getType ( service ) { switch ( service ) { case 'upload-pack' : return 'fetch' ; case 'receive-pack' : return 'push' ; default : return 'unknown' ; } } param http param http handle ( req , res ) { const self = this ; const handlers = [ ( req , res ) => { if ( req . method !== 'GET' ) return false ; const u = url . parse ( req ?. url || '' ) ; const m = u . pathname ?. match ( /\/(.+)\/info\/refs$/ ) ; if ( ! m ) return false ; if ( /\.\./ . test ( m [ 1 ] ) ) return false ; const repo = m [ 1 ] ; const params = qs . parse ( u ?. query || '' ) ; if ( ! params . service || typeof params . service !== 'string' ) { res . statusCode = 400 ; res . end ( 'service parameter required' ) ; return ; } const service = params . service . replace ( /^git-/ , '' ) ; if ( services . indexOf ( service ) < 0 ) { res . statusCode = 405 ; res . end ( 'service not available' ) ; return ; } const repoName = parseGitName ( m [ 1 ] ) ; const next = ( error ? ) => { if ( error ) { res . setHeader ( 'Content-Type' , 'text/plain' ) ; res . setHeader ( 'WWW-Authenticate' , 'Basic realm="authorization needed"' ) ; res . writeHead ( 401 ) ; res . end ( typeof error === 'string' ? error : error . toString ( ) ) ; return ; } else { return infoResponse ( this , repo , service as ServiceString , req , res ) ; } } ; if ( this . authenticate ) { const type = this . getType ( service ) ; const headers = req . headers ; const user = ( callback ? ) => callback ? basicAuth ( req , res , callback ) : new Promise < [ string | undefined , string | undefined ] > ( ( resolve ) => basicAuth ( req , res , ( u , p ) => resolve ( [ u , p ] ) ) ) ; const promise = this . authenticate ( { type , repo : repoName , user : user as unknown as GitAuthenticateOptions [ 'user' ] , headers , } , ( error ? ) => { return next ( error ) ; } ) ; if ( promise instanceof Promise ) { return promise . then ( next ) . catch ( next ) ; } } else { return next ( ) ; } } , ( req , res ) => { if ( req . method !== 'GET' ) return false ; const u = url . parse ( req . url || '' ) ; const m = u . pathname ?. match ( /^\/(.+)\/HEAD$/ ) ; if ( ! m ) return false ; if ( /\.\./ . test ( m [ 1 ] ) ) return false ; const repo = m [ 1 ] ; const next = ( ) => { const file = this . dirMap ( path . join ( m [ 1 ] , 'HEAD' ) ) ; const exists = this . exists ( file ) ; if ( exists ) { fs . createReadStream ( file ) . pipe ( res ) ; } else { res . statusCode = 404 ; res . end ( 'not found' ) ; } } ; const exists = this . exists ( repo ) ; const anyListeners = self . listeners ( 'head' ) . length > 0 ; const dup = new HttpDuplex ( req , res ) ; dup . exists = exists ; dup . repo = repo ; dup . cwd = this . dirMap ( repo ) ; dup . accept = dup . emit . bind ( dup , 'accept' ) ; dup . reject = dup . emit . bind ( dup , 'reject' ) ; dup . once ( 'reject' , ( code ) => { dup . statusCode = code || 500 ; dup . end ( ) ; } ) ; if ( ! exists && self . autoCreate ) { dup . once ( 'accept' , ( dir ) => { self . create ( dir || repo , next ) ; } ) ; self . emit ( 'head' , dup ) ; if ( ! anyListeners ) dup . accept ( ) ; } else if ( ! exists ) { res . statusCode = 404 ; res . setHeader ( 'content-type' , 'text/plain' ) ; res . end ( 'repository not found' ) ; } else { dup . once ( 'accept' , next ) ; self . emit ( 'head' , dup ) ; if ( ! anyListeners ) dup . accept ( ) ; } } , ( req , res ) => { if ( req . method !== 'POST' ) return false ; const m = req . url ?. match ( /\/(.+)\/git-(.+)/ ) ; if ( ! m ) return false ; if ( /\.\./ . test ( m [ 1 ] ) ) return false ; const repo = m [ 1 ] , service = m [ 2 ] ; if ( services . indexOf ( service ) < 0 ) { res . statusCode = 405 ; res . end ( 'service not available' ) ; return ; } res . setHeader ( 'content-type' , 'application/x-git-' + service + '-result' ) ; noCache ( res ) ; const action = createAction ( { repo : repo , service : service as ServiceString , cwd : self . dirMap ( repo ) , } , req , res ) ; action . on ( 'header' , ( ) => { const evName = action . evName ; if ( evName ) { const anyListeners = self . listeners ( evName ) . length > 0 ; self . emit ( evName , action ) ; if ( ! anyListeners ) action . accept ( ) ; } } ) ; } , ( req , res ) => { if ( req . method !== 'GET' && req . method !== 'POST' ) { res . statusCode = 405 ; res . end ( 'method not supported' ) ; } else { return false ; } } , ( req , res ) => { res . statusCode = 404 ; res . end ( 'not found' ) ; } , ] ; res . setHeader ( 'connection' , 'close' ) ; ( function next ( ix ) { const x = handlers [ ix ] . call ( self , req , res ) ; if ( x === false ) next ( ix + 1 ) ; } ) ( 0 ) ; } param port param options param options . type param options . key param options . cert param callback listen ( port , options ? , callback ? ) { if ( ! options ) { options = { type : 'http' } ; } const createServer = options . type == 'http' ? http . createServer : https . createServer . bind ( this , options ) ; this . server = createServer ( ( req , res ) => { this . handle ( req , res ) ; } ) ; this . server . listen ( port , callback ) ; return this ; } param will close ( ) { return new Promise < string > ( ( resolve , reject ) => { this . server ?. close ( ( err ) => { err ? reject ( err ) : resolve ( 'Success' ) ; } ) ; } ) ; } }