import { execSync } from "child_process" ; import { existsSync } from "fs" ; import * as micromatch from "micromatch" ; import { resolve } from "path" ; import { Options , Path } from "./types" ; import { buildDebugger , withDuration } from "../utils" ; type ParsedLine = { relativePath : string ; commitCount : string ; } ; const internal = { debug : buildDebugger ( "churn" ) } ; const PER_LINE = "\n" ; export default { compute : ( ... args ) : Promise < Map < Path , number > > => withDuration ( compute , args , internal . debug ) } ; async function compute ( options ) { assertGitIsInstalled ( ) ; assertIsGitRootDirectory ( options . directory ) ; const isWindows = process . platform === "win32" ; const gitLogCommand = buildGitLogCommand ( options , isWindows ) ; internal . debug ( `command to measure churns: ${ gitLogCommand }` ) ; const gitLogStdout = execSync ( gitLogCommand , { encoding : "utf8" } ) ; const parsedLines = computeNumberOfTimesFilesChanged ( gitLogStdout ) . filter ( ( parsedLine ) => { return existsSync ( resolve ( options . directory , parsedLine . relativePath ) ) ; } ) ; const allChurns = parsedLines . reduce ( ( map , { relativePath , commitCount } ) => { const path = relativePath ; const churn = parseInt ( commitCount , 10 ) ; map . set ( path , churn ) ; return map ; } , new Map ( ) ) ; internal . debug ( `${ allChurns . size } files to compute churn on (before filters)` ) ; const filteredChurns = new Map ( ) ; allChurns . forEach ( ( churn , path ) => { const patchIsAMatch = options . filter && options . filter . length > 0 ? options . filter . every ( ( f ) => micromatch . isMatch ( path , f ) ) : true ; if ( patchIsAMatch ) filteredChurns . set ( path , churn ) ; } ) ; internal . debug ( `${ filteredChurns . size } files to compute churn on (after filters)` ) ; return filteredChurns ; } function assertGitIsInstalled ( ) { try { execSync ( "which git" ) ; } catch ( error ) { throw new Error ( "Program 'git' must be installed" ) ; } } function assertIsGitRootDirectory ( directory ) { if ( ! existsSync ( `${ directory }/.git` ) ) { throw new Error ( `Argument 'dir' must be the git root directory.` ) ; } } function buildGitLogCommand ( options , isWindows ) { return [ "git" , `-C ${ options . directory }` , `log` , `--follow` , `--format=${ isWindows ? "" : "''" }` , `--name-only` , options . since ? `--since="${ options . since }"` : "" , options . until ? `--until="${ options . until }"` : "" , isWindows ? "*" : "'*'" , ] . filter ( ( s ) => s . length > 0 ) . join ( " " ) ; } function computeNumberOfTimesFilesChanged ( gitLogOutput ) { const changedFiles = gitLogOutput . split ( PER_LINE ) . filter ( ( line ) => line !== "" ) . sort ( ) ; const changedFilesCount = changedFiles . reduce ( ( fileAndTimeChanged , fileName ) => { fileAndTimeChanged [ fileName ] = ( fileAndTimeChanged [ fileName ] || 0 ) + 1 ; return fileAndTimeChanged ; } , { } ) ; return Object . keys ( changedFilesCount ) . map ( ( changedFileName ) => ( { relativePath : changedFileName , commitCount : changedFilesCount [ changedFileName ] . toString ( ) } as ParsedLine ) ) ; }