import fs from 'fs-extra' ; import path from 'path' ; import tmp from 'tmp' ; import MetadataRef from '../../wrapper-classes/metadata_ref' ; import { repoConfig , userConfig } from '../config' ; import { getBranchToRefMapping } from '../git-refs/branch_ref' ; import { getRevListGitTree } from '../git-refs/branch_relations' ; import { currentBranchPrecondition } from '../preconditions' ; import { gpExecSync , logInfo , logWarn } from '../utils' ; type stateT = { refTree : Record < string , string [ ] > ; branchToRefMapping : Record < string , string > ; userConfig : string ; repoConfig : string ; metadata : Record < string , string > ; currentBranchName : string ; } ; export function captureState ( ) { const refTree = getRevListGitTree ( { useMemoizedResults : false , direction : 'parents' , } ) ; const branchToRefMapping = getBranchToRefMapping ( ) ; const metadata = { } ; MetadataRef . allMetadataRefs ( ) . forEach ( ( ref ) => { metadata [ ref . _branchName ] = JSON . stringify ( ref . read ( ) ) ; } ) ; const currentBranchName = currentBranchPrecondition ( ) . name ; const state = { refTree , branchToRefMapping , userConfig : JSON . stringify ( userConfig . _data ) , repoConfig : JSON . stringify ( repoConfig . _data ) , metadata , currentBranchName , } ; return JSON . stringify ( state , null , 2 ) ; } export function recreateState ( stateJson ) { const state = JSON . parse ( stateJson ) as stateT ; const refMappingsOldToNew = { } ; const tmpTrunk = `initial-debug-context-head-${ Date . now ( ) }` ; const tmpDir = createTmpGitDir ( { trunkName : tmpTrunk , } ) ; process . chdir ( tmpDir ) ; logInfo ( `Creating ${ Object . keys ( state . refTree ) . length } commits` ) ; recreateCommits ( { refTree : state . refTree , refMappingsOldToNew } ) ; logInfo ( `Creating ${ Object . keys ( state . branchToRefMapping ) . length } branches` ) ; createBranches ( { branchToRefMapping : state . branchToRefMapping , refMappingsOldToNew , } ) ; logInfo ( `Creating the repo config` ) ; fs . writeFileSync ( path . join ( tmpDir , '/.git/.graphite_repo_config' ) , state . repoConfig ) ; logInfo ( `Creating the metadata` ) ; createMetadata ( { metadata : state . metadata , tmpDir } ) ; gpExecSync ( { command : `git checkout "${ state . currentBranchName }"` } ) ; gpExecSync ( { command : `git branch -D "${ tmpTrunk }"` } ) ; return tmpDir ; } function createMetadata ( opts ) { fs . mkdirSync ( `${ opts . tmpDir }/.git/refs/branch-metadata` ) ; Object . keys ( opts . metadata ) . forEach ( ( branchName ) => { const metaSha = gpExecSync ( { command : `git hash-object -w --stdin` , options : { input : opts . metadata [ branchName ] , } , } ) . toString ( ) ; fs . writeFileSync ( `${ opts . tmpDir }/.git/refs/branch-metadata/${ branchName }` , metaSha ) ; } ) ; } function createBranches ( opts ) { const curBranch = currentBranchPrecondition ( ) ; Object . keys ( opts . branchToRefMapping ) . forEach ( ( branch ) => { const originalRef = opts . refMappingsOldToNew [ opts . branchToRefMapping [ branch ] ] ; if ( branch != curBranch . name ) { gpExecSync ( { command : `git branch -f ${ branch } ${ originalRef }` } ) ; } else { logWarn ( `Skipping creating ${ branch } which matches the name of the current branch` ) ; } } ) ; } function recreateCommits ( opts ) { const treeObjectId = getTreeObjectId ( ) ; const commitsToCreate = commitRefsWithNoParents ( opts . refTree ) ; const firstCommitRef = gpExecSync ( { command : `git rev-parse HEAD` } ) ; const totalOldCommits = Object . keys ( opts . refTree ) . length ; while ( commitsToCreate . length > 0 ) { const originalCommitRef = commitsToCreate . shift ( ) ! ; if ( originalCommitRef in opts . refMappingsOldToNew ) { continue ; } const originalParents = opts . refTree [ originalCommitRef ] || [ ] ; const missingParent = originalParents . find ( ( p ) => opts . refMappingsOldToNew [ p ] === undefined ) ; if ( missingParent ) { commitsToCreate . push ( originalCommitRef ) ; continue ; } const newCommitRef = gpExecSync ( { command : `git commit-tree ${ treeObjectId } -m "${ originalCommitRef }" ${ originalParents . length === 0 ? `-p ${ firstCommitRef }` : originalParents . map ( ( p ) => opts . refMappingsOldToNew [ p ] ) . map ( ( newParentRef ) => `-p ${ newParentRef }` ) . join ( ' ' ) }` , } ) . toString ( ) . trim ( ) ; opts . refMappingsOldToNew [ originalCommitRef ] = newCommitRef ; const totalNewCommits = Object . keys ( opts . refMappingsOldToNew ) . length ; if ( totalNewCommits % 100 === 0 ) { console . log ( `Progress: ${ totalNewCommits } / ${ totalOldCommits } created` ) ; } Object . keys ( opts . refTree ) . forEach ( ( potentialChildRef ) => { const parents = opts . refTree [ potentialChildRef ] ; if ( parents . includes ( originalCommitRef ) ) { commitsToCreate . push ( potentialChildRef ) ; } } ) ; } } function createTmpGitDir ( opts ? ) { const tmpDir = tmp . dirSync ( ) . name ; logInfo ( `Creating tmp repo` ) ; gpExecSync ( { command : `git -C ${ tmpDir } init -b "${ opts ?. trunkName ?? 'main' }"` , } ) ; gpExecSync ( { command : `cd ${ tmpDir } && echo "first" > first.txt && git add first.txt && git commit -m "first"` , } ) ; return tmpDir ; } function commitRefsWithNoParents ( refTree ) { const allRefs = [ ... new Set ( Object . keys ( refTree ) . concat . apply ( [ ] , Object . values ( refTree ) ) ) , ] ; return allRefs . filter ( ( ref ) => refTree [ ref ] === undefined || refTree [ ref ] . length === 0 ) ; } function getTreeObjectId ( ) { return gpExecSync ( { command : `git cat-file -p HEAD | grep tree | awk '{print $2}'` , } ) . toString ( ) . trim ( ) ; }