import { spawnSync } from "child_process" ; import { FileSystemAdapter } from "obsidian" ; import * as path from "path" ; import simpleGit , * as simple from "simple-git" ; import { GitManager } from "./gitManager" ; import ObsidianGit from "./main" ; import { BranchInfo , FileStatusResult , PluginState } from "./types" ; export class SimpleGit extends GitManager { git : simple . SimpleGit ; constructor ( plugin ) { super ( plugin ) ; this . setGitInstance ( ) ; } private setGitInstance ( ) { if ( this . isGitInstalled ( ) ) { const adapter = this . app . vault . adapter as FileSystemAdapter ; const path = adapter . getBasePath ( ) ; this . git = simpleGit ( { baseDir : path , binary : this . plugin . settings . gitPath || undefined , config : [ "core.quotepath=off" ] } ) ; } } async status ( ) { this . plugin . setState ( PluginState . status ) ; const status = await this . git . status ( ) ; this . plugin . setState ( PluginState . idle ) ; return { changed : status . files . filter ( ( e ) => e . working_dir !== " " ) . map ( ( e ) => { const res = this . formatPath ( e . path ) ; e . path = res . path ; e . from = res . from ; e . working_dir = e . working_dir === "?" ? "U" : e . working_dir ; return e ; } ) , staged : status . files . filter ( ( e ) => e . index !== " " && e . index != "?" ) . map ( ( e ) => { const res = this . formatPath ( e . path , e . index === "R" ) ; e . path = res . path ; e . from = res . from ; return e ; } ) , conflicted : status . conflicted . map ( ( e ) => this . formatPath ( e ) . path ) , } ; } formatPath ( path , renamed = false ) { function format ( path ) { if ( path . startsWith ( '"' ) && path . endsWith ( '"' ) ) { return path . substring ( 1 , path . length - 1 ) ; } else { return path ; } } if ( renamed ) { const paths = path . split ( " -> " ) . map ( ( e ) => format ( e ) ) ; return { from : paths [ 0 ] , path : paths [ 1 ] , } ; } else { return { path : format ( path ) } ; } } async commitAll ( message ? ) { if ( this . plugin . settings . updateSubmodules ) { this . plugin . setState ( PluginState . commit ) ; await this . git . subModule ( [ "foreach" , "--recursive" , `git add -A && if [ ! -z "$(git status --porcelain)" ]; then git commit -m "${ message ?? await this . formatCommitMessage ( ) }"; fi` ] , ( err ) => this . onError ( err ) ) ; } this . plugin . setState ( PluginState . add ) ; await this . git . add ( "./*" , ( err ) => this . onError ( err ) ) ; this . plugin . setState ( PluginState . commit ) ; return ( await this . git . commit ( await this . formatCommitMessage ( message ) ) ) . summary . changes ; } async commit ( message ? ) { this . plugin . setState ( PluginState . commit ) ; const res = ( await this . git . commit ( await this . formatCommitMessage ( message ) ) ) . summary . changes ; this . plugin . setState ( PluginState . idle ) ; return res ; } async stage ( filepath ) { this . plugin . setState ( PluginState . add ) ; await this . git . add ( filepath , ( err ) => this . onError ( err ) ) ; this . plugin . setState ( PluginState . idle ) ; } async stageAll ( ) { this . plugin . setState ( PluginState . add ) ; await this . git . add ( "./*" , ( err ) => this . onError ( err ) ) ; this . plugin . setState ( PluginState . idle ) ; } async unstageAll ( ) { this . plugin . setState ( PluginState . add ) ; await this . git . reset ( [ "--" , "./*" ] , ( err ) => this . onError ( err ) ) ; this . plugin . setState ( PluginState . idle ) ; } async unstage ( filepath ) { this . plugin . setState ( PluginState . add ) ; await this . git . reset ( [ "--" , filepath ] , ( err ) => this . onError ( err ) ) ; this . plugin . setState ( PluginState . idle ) ; } async discard ( filepath ) { this . plugin . setState ( PluginState . add ) ; await this . git . checkout ( [ "--" , filepath ] , ( err ) => this . onError ( err ) ) ; this . plugin . setState ( PluginState . idle ) ; } async pull ( ) { this . plugin . setState ( PluginState . pull ) ; if ( this . plugin . settings . updateSubmodules ) await this . git . subModule ( [ "update" , "--remote" , "--merge" , "--recursive" ] , ( err ) => this . onError ( err ) ) ; let lastRemoteCommitBefore ; if ( ! this . plugin . settings . mergeOnPull ) { lastRemoteCommitBefore = await this . getNewestRemoteCommit ( ) ; } const pullResult = await this . git . pull ( [ this . plugin . settings . mergeOnPull ? '--no-rebase' : '--rebase' ] , async ( err ) => { if ( err ) { this . plugin . displayError ( `Pull failed ${ err . message }` ) ; const status = await this . git . status ( ) ; if ( status . conflicted . length > 0 ) { this . plugin . handleConflict ( status . conflicted ) ; } } } ) ; if ( ! this . plugin . settings . mergeOnPull ) { const lastRemoteCommitAfter = await this . getNewestRemoteCommit ( ) ; if ( lastRemoteCommitAfter != lastRemoteCommitBefore ) { return 1 ; } else { return 0 ; } } else { return pullResult . files . length ; } } private async getNewestRemoteCommit ( ) { const branchInfo = await this . branchInfo ( ) ; const newestRemoteCommit = ( await this . git . log ( [ `-n 1 ${ branchInfo . tracking }` ] ) ) . all [ 0 ] . hash ; return newestRemoteCommit ; } async push ( ) { this . plugin . setState ( PluginState . status ) ; const status = await this . git . status ( ) ; const trackingBranch = status . tracking ; const currentBranch = status . current ; const remoteChangedFiles = ( await this . git . diffSummary ( [ currentBranch , trackingBranch ] ) ) . changed ; this . plugin . setState ( PluginState . push ) ; if ( this . plugin . settings . updateSubmodules ) { await this . git . env ( { ... process . env , "OBSIDIAN_GIT" : 1 } ) . subModule ( [ "foreach" , "--recursive" , `tracking=$(git for-each-ref --format='%(upstream:short)' "$(git symbolic-ref -q HEAD)"); echo $tracking; if [ ! -z "$(git diff --shortstat $tracking)" ]; then git push; fi` ] , ( err ) => this . onError ( err ) ) ; } await this . git . env ( { ... process . env , "OBSIDIAN_GIT" : 1 } ) . push ( ( err ) => this . onError ( err ) ) ; return remoteChangedFiles ; } async canPush ( ) { if ( this . plugin . settings . updateSubmodules === true ) { return true ; } const status = await this . git . status ( ( err ) => this . onError ( err ) ) ; const trackingBranch = status . tracking ; const currentBranch = status . current ; const remoteChangedFiles = ( await this . git . diffSummary ( [ currentBranch , trackingBranch ] ) ) . changed ; return remoteChangedFiles !== 0 ; } async checkRequirements ( ) { if ( ! this . isGitInstalled ( ) ) { return "missing-git" ; } if ( ! ( await this . git . checkIsRepo ( ) ) ) { return "missing-repo" ; } return "valid" ; } async branchInfo ( ) { const status = await this . git . status ( ( err ) => this . onError ( err ) ) ; const branches = await this . git . branch ( [ "--no-color" ] , ( err ) => this . onError ( err ) ) ; return { current : status . current , tracking : status . tracking , branches : branches . all , } ; } async checkout ( branch ) { await this . git . checkout ( branch , ( err ) => this . onError ( err ) ) ; } async init ( ) { await this . git . init ( false , ( err ) => this . onError ( err ) ) ; } async clone ( url , dir ) { await this . git . clone ( url , path . join ( ( this . app . vault . adapter as FileSystemAdapter ) . getBasePath ( ) , dir ) , [ ] , ( err ) => this . onError ( err ) ) ; } async setConfig ( path , value ) { await this . git . addConfig ( path , value , ( err ) => this . onError ( err ) ) ; } async getConfig ( path ) { const config = await this . git . listConfig ( ( err ) => this . onError ( err ) ) ; return config . all [ path ] ; } async fetch ( remote ? ) { await this . git . fetch ( remote != undefined ? [ remote ] : [ ] , ( err ) => this . onError ( err ) ) ; } async setRemote ( name , url ) { if ( ( await this . getRemotes ( ) ) . includes ( name ) ) await this . git . remote ( [ "set-url" , name , url ] , ( err ) => this . onError ( err ) ) ; else { await this . git . remote ( [ "add" , name , url ] , ( err ) => this . onError ( err ) ) ; } } async getRemoteBranches ( remote ) { const res = await this . git . branch ( [ "-r" , "--list" , `${ remote }*` ] , ( err ) => this . onError ( err ) ) ; const list = [ ] ; for ( var item in res . branches ) { list . push ( res . branches [ item ] . name ) ; } return list ; } async getRemotes ( ) { const res = await this . git . remote ( [ ] , ( err ) => this . onError ( err ) ) ; if ( res ) { return res . trim ( ) . split ( "\n" ) ; } else { return [ ] ; } } async removeRemote ( remoteName ) { await this . git . removeRemote ( remoteName ) ; } async updateUpstreamBranch ( remoteBranch ) { await this . git . push ( [ "--set-upstream" , ... remoteBranch . split ( "/" ) ] , ( err ) => this . onError ( err ) ) ; } updateGitPath ( gitPath ) { this . setGitInstance ( ) ; } async getDiffString ( filePath ) { return ( await this . git . diff ( [ filePath ] ) ) ; } private isGitInstalled ( ) { const command = spawnSync ( this . plugin . settings . gitPath || 'git' , [ '--version' ] , { stdio : 'ignore' } ) ; if ( command . error ) { console . error ( command . error ) ; return false ; } return true ; } private onError ( error ) { if ( error ) { this . plugin . displayError ( error . message ) ; this . plugin . setState ( PluginState . idle ) ; } } }