git-off

git off handles large files in git repos
git clone https://noulin.net/git/git-off.git
Log | Files | Refs | README

gitoff.coffee (54141B)


      1 #! /usr/bin/env coffee
      2 
      3 # TODO
      4 # use the path function
      5 # handle wrong config like getLog
      6 # check parameters from CLI
      7 #
      8 # parse params
      9 # use logger
     10 #
     11 # How to add a new config
     12 #   add variable in runtimeConfig
     13 #   add default settings in offDEFAULTS
     14 #   add a helper in offHelpers
     15 #   add setting of default in offCommands.install
     16 #   add a command for setting value in offCommands
     17 #   print value in offCommands.env
     18 #   add command in CLI parser
     19 #   update help
     20 #
     21 # How to add a new transport
     22 #   add transport functions in offHelpers.setTransport
     23 #   add transport in offHelpers.mkdirStore and offHelpers.rmAllStore
     24 #   add transport in offCommands.clearAll
     25 #   update git off mode help
     26 #   if needed, add store creation in offCommands.install
     27 #
     28 # How to add a new command
     29 #   add command in offCommands
     30 #   add command in CLI parser
     31 #   update help
     32 
     33 ###
     34 # CODE
     35 #
     36 # modules
     37 # defaults
     38 # helpers
     39 # core
     40 # main
     41 #
     42 #
     43 # modules
     44 #   all dependencies
     45 #
     46 # defaults
     47 #   objInfo fields:  for indexing cat-diff response in push command
     48 #   externalHelpers: shell commands to be used with the exec function
     49 #   runtimeConfig:   config built by offHelpers
     50 #   offDEFAULTS:     default configuration for first time install
     51 #
     52 # helpers
     53 #   expandHome:      expands ~/
     54 #   gitConfig:       handles global git config
     55 #   offLog:          appends log to git config off.log
     56 #   offLogRepo:      log for commands run in git repo
     57 #   exec:            runs an externalHelpers with parameters
     58 #   mkdirParents:    recursive mkdir
     59 #   rmAll:           delete recursively files and directories
     60 #   copy:            copies files
     61 #   offHelpers:      git-off helpers
     62 #
     63 # core
     64 #   transport:       transport functions for git off store
     65 #   offCommands:     command line functions
     66 #
     67 # main
     68 #   parse CLI arguments
     69 ###
     70 
     71 
     72 ###
     73 # modules
     74 ###
     75 
     76 require('colors')
     77 fs            = require('fs')
     78 path          = require('path')
     79 fssync        = require('fs-sync')
     80 syncexec      = require('sync-exec')
     81 mkdirp        = require('mkdirp')
     82 rimraf        = require('rimraf')
     83 readline      = require('readline')
     84 StringDecoder = require('string_decoder').StringDecoder
     85 
     86 ###
     87 # defaults
     88 ###
     89 
     90 # objInfo fields for indexing cat-diff response in push command
     91 oiPREVIOUSPERMISSIONS = 0
     92 oiPERMISSIONS         = 1
     93 oiPREVIOUSOID         = 2
     94 oiOID                 = 3
     95 oiNAME                = 4
     96 
     97 # shell commands to be used with the exec function
     98 # example: exec 'gitRepoRoot'
     99 externalHelpers =
    100   'gitConfigGlobal': 'git config --global'
    101   'gitConfig': 'git config'
    102   'gitRepoRoot': 'git rev-parse --show-toplevel'
    103   'gitList': 'git rev-list'
    104   'gitListEmptyRemote': 'git rev-list --max-parents=0'
    105   'gitDiff': 'git show --raw --format="" --no-abbrev'
    106   'gitCat': 'git cat-file -p'
    107   'sha': 'git hash-object --no-filters'
    108   'listAttr': 'git check-attr -a'
    109   'ssh': 'ssh'
    110   'scp': 'scp'
    111   'curl': 'curl'
    112   'mv': 'mv'
    113   'rsync': 'rsync'
    114 
    115 # config built by offHelpers
    116 # use offHelpers to access runtimeConfig
    117 runtimeConfig =
    118   'currentRepoRoot': ''
    119   'objectPath': ''
    120   'offStore': ''
    121   'offHttp': ''
    122   'offMode': ''
    123   'offIntegrity': ''
    124   'offScp': ''
    125   'offScpUser': ''
    126   'offPem': ''
    127   'offSshOptions': ''
    128   'offScpOptions': ''
    129   'offRsyncOptions': ''
    130   'offCurlOptions': ''
    131   'log': ''
    132   'offConfigAlways': ''
    133   's3Region': ''
    134   's3Bucket': ''
    135   'transform': ''
    136   'transformTo': ''
    137   'transformFrom': ''
    138 
    139 # default configuration for first time install
    140 # objectPath is git off cache in repo
    141 offDEFAULTS =
    142   'objectPath': '/.git/off/objects'
    143   'mode': 'copy'
    144   'integrity': 'disable'
    145   'pem': 'offNoValue'
    146   'scpHost': 'offNoValue'
    147   'scpUser': ''
    148   'sshOptions': '-C -o StrictHostKeyChecking=no -o ConnectTimeout=3'
    149   'scpOptions': '-C -o StrictHostKeyChecking=no -o ConnectTimeout=3 -p'
    150   'rsyncOptions': '-az -e \'ssh -i _i\''
    151   'store': '~/.git-off/offStore'
    152   'http': 'offNoValue'
    153   'curlOptions': '-o'
    154   'log': '~/.git-off/log'
    155   'configAlways': 'offNoValue'
    156   's3Region': 'offNoValue'
    157   's3Bucket': 'offNoValue'
    158   'transform': 'disable'
    159   'transformTo': 'pbzip2 -9 -c _1 > _2'
    160   'transformFrom': 'pbzip2 -d -c _1 > _2'
    161   'prePush': '#!/bin/sh\ncommand -v git-off >/dev/null 2>&1 || { echo >&2 "\\nThis repository is configured for Git off but \'git-off\' was not found on your path. If you no longer wish to use git off, remove this hook by deleting .git/hooks/pre-push.\\n"; exit 2; }\ngit off pre-push "$@"'
    162   'offSignature': '### git-off v1 sha:'
    163   'shaLength': 40
    164 
    165 ###
    166 # helpers
    167 ###
    168 
    169 # expands ~/
    170 expandHome = (p) ->
    171   if p.slice(0,2) == '~/'
    172     p = process.env.HOME + '/' + p.slice(2)
    173   p
    174 
    175 # handles global git config
    176 gitConfig =
    177   'set': (key, value)->
    178     exec 'gitConfig', ['--global', key, '"' + value + '"']
    179     return
    180 
    181   'setLocal': (key, value) ->
    182     exec('gitConfig', [key, '"' + value + '"'])
    183     return
    184 
    185   'setThisRepo': (key, value) ->
    186     exec('gitConfig', ['--file ' + offHelpers.gitRepoRoot() + '/.git-off', key, '"' + value + '"'])
    187     return
    188 
    189   'get': (key) ->
    190     # use configAlways setting or
    191     # return value from
    192     # GIT_OFF_CONFIG if found
    193     # repo config if found
    194     # global config
    195     if offHelpers.offConfigAlways() == 'GIT_OFF_CONFIG'
    196       r = ''
    197       if process.env.GIT_OFF_CONFIG != undefined and fs.existsSync(process.env.GIT_OFF_CONFIG) == true
    198         r = exec('gitConfig', ['--file ' + process.env.GIT_OFF_CONFIG, key]).stdout.trim()
    199       return r
    200     if offHelpers.offConfigAlways() == 'repo'
    201       r = ''
    202       if fs.existsSync(offHelpers.gitRepoRoot(true) + '/.git-off') == true
    203         r = exec('gitConfig', ['--file ' + offHelpers.gitRepoRoot() + '/.git-off', key]).stdout.trim()
    204       return r
    205     if offHelpers.offConfigAlways() == 'global'
    206       return exec('gitConfig', [key]).stdout.trim()
    207     if process.env.GIT_OFF_CONFIG != undefined and fs.existsSync(process.env.GIT_OFF_CONFIG) == true
    208       r = exec('gitConfig', ['--file ' + process.env.GIT_OFF_CONFIG, key]).stdout.trim()
    209       if r != ''
    210         return r
    211     if fs.existsSync(offHelpers.gitRepoRoot(true) + '/.git-off') == true
    212       r = exec('gitConfig', ['--file ' + offHelpers.gitRepoRoot() + '/.git-off', key]).stdout.trim()
    213       if r != ''
    214         return r
    215     exec('gitConfig', [key]).stdout.trim()
    216 
    217 
    218   'getSyncexec': (key) ->
    219     # always global git config
    220     syncexec externalHelpers.gitConfig + ' ' + key
    221 
    222 # recursive mkdir
    223 mkdirParents = (p) ->
    224   p = expandHome p
    225   mkdirp.sync(p, (err) ->
    226     console.error err
    227     return)
    228   return
    229 
    230 # delete recursively files and directories
    231 rmAll = (p) ->
    232   p = expandHome p
    233   rimraf.sync p
    234   return
    235 
    236 # appends log to git config off.log
    237 offLog = (s) ->
    238   if runtimeConfig.log == ''
    239     # set runtimeConfig.log and get config in git
    240     # use syncexec to avoid circular calls
    241     r                 = gitConfig.getSyncexec 'off.log'
    242     runtimeConfig.log = expandHome r.stdout.trim()
    243     # error handling
    244     if runtimeConfig.log == ''
    245       console.error 'Missing off.log config. Run "git config --global off.log ~/.git-off/log".'.red.bold
    246       process.exit(1)
    247   # append log (s)
    248   if fs.existsSync(path.dirname(runtimeConfig.log)) == false
    249     mkdirParents path.dirname(runtimeConfig.log)
    250   fs.appendFileSync(runtimeConfig.log, s + '\n')
    251   return
    252 
    253 # log for commands run in git repo
    254 # adds current repo path to string s in log
    255 offLogRepo = (s) ->
    256   # depends on offHelpers
    257   offLog 'REPO path: ' + offHelpers.gitRepoRoot() + ' ' + s
    258   return
    259 
    260 # runs an externalHelpers with parameters
    261 # cmdName is string
    262 # paramsArray is an array of strings
    263 # errors are logged in off.log and in console
    264 # returns r (r.stdout, r.stderr)
    265 exec = (cmdName, paramsArray=[], ignoreStderr=false) ->
    266   #console.log  externalHelpers[cmdName] + ' ' + paramsArray.join(' ')
    267   #offLog       externalHelpers[cmdName] + ' ' + paramsArray.join(' ')
    268   r = syncexec externalHelpers[cmdName] + ' ' + paramsArray.join(' ')
    269   #console.log r.stdout
    270   # offLog r
    271   # offLog r.stdout
    272   if r.stderr != '' and ignoreStderr == false
    273      console.error r.stderr.red.bold
    274      offLog        r.stderr.red.bold
    275      process.exit(1)
    276   r
    277 
    278 # copies files
    279 # file modes are not kept
    280 copy = (src, dst) ->
    281   fssync.copy(src, dst)
    282   return
    283 
    284 # List all files in a directory recursively in a synchronous fashion
    285 walkSync = (dir, filelist = []) ->
    286   files = fs.readdirSync(dir)
    287   files.forEach (file) ->
    288     if fs.statSync(dir + '/' + file).isDirectory()
    289       filelist = walkSync(dir + '/' + file, filelist)
    290     else
    291       filelist.push dir+ '/' + file
    292     return
    293   filelist
    294 
    295 # git-off helpers
    296 # gitRepoRoot:    sets and returns runtimeConfig.currentRepoRoot
    297 # objectPath:     sets and returns runtimeConfig.objectPath
    298 # offStore:       sets and returns runtimeConfig.offStore
    299 # offMode:        sets and returns runtimeConfig.offMode
    300 # offIntegrity    sets and returns runtimeConfig.offIntegrity
    301 # offScp:         sets and returns runtimeConfig.offScp
    302 # offScpUser:     sets and returns runtimeConfig.offScpUser
    303 # log:            sets and returns runtimeConfig.log
    304 # getLog:         sets and returns runtimeConfig.log with error checking
    305 # userAt:         returns user@ or ''
    306 # getSSHConfig    extracts host, port and path from off.sshhost
    307 # mkdirStore      mkdir in remote store
    308 # rmAllStore      rm in remote store
    309 # checkIntegrity  check integrity of files coming from the store
    310 # setTransport:   set send and receive functions for transport
    311 # getOffFilePath: creates path for offFile
    312 offHelpers =
    313   'gitRepoRoot': (ignoreStderr=false) ->
    314     if runtimeConfig.currentRepoRoot == ''
    315       r                             = exec 'gitRepoRoot', [], ignoreStderr
    316       runtimeConfig.currentRepoRoot = r.stdout.trim()
    317     runtimeConfig.currentRepoRoot
    318 
    319   'objectPath': ->
    320     if runtimeConfig.objectPath == ''
    321       runtimeConfig.objectPath = this.gitRepoRoot() + offDEFAULTS.objectPath
    322     runtimeConfig.objectPath
    323 
    324   'offStore': ->
    325     if runtimeConfig.offStore == ''
    326       r                      = gitConfig.get 'off.store'
    327       runtimeConfig.offStore = expandHome r
    328     runtimeConfig.offStore
    329 
    330   'offHttp': ->
    331     if runtimeConfig.offHttp == ''
    332       runtimeConfig.offHttp = gitConfig.get 'off.http'
    333     runtimeConfig.offHttp
    334 
    335   'offCurlOptions': ->
    336     if runtimeConfig.offCurlOptions == ''
    337       runtimeConfig.offCurlOptions = gitConfig.get 'off.curloptions'
    338     runtimeConfig.offCurlOptions
    339 
    340   'offMode': ->
    341     if runtimeConfig.offMode == ''
    342       runtimeConfig.offMode = gitConfig.get 'off.mode'
    343     runtimeConfig.offMode
    344 
    345   'offIntegrity': ->
    346     if runtimeConfig.offIntegrity == ''
    347       runtimeConfig.offIntegrity = gitConfig.get 'off.integrity'
    348     runtimeConfig.offIntegrity
    349 
    350   'offPem': ->
    351     if runtimeConfig.offPem == ''
    352       r                          = gitConfig.get 'off.pem'
    353       runtimeConfig.offPem = expandHome r
    354     runtimeConfig.offPem
    355 
    356   'offSshOptions': ->
    357     if runtimeConfig.offSshOptions == ''
    358       runtimeConfig.offSshOptions = gitConfig.get 'off.sshoptions'
    359     runtimeConfig.offSshOptions
    360 
    361   'offScpOptions': ->
    362     if runtimeConfig.offScpOptions == ''
    363       runtimeConfig.offScpOptions = gitConfig.get 'off.scpoptions'
    364     runtimeConfig.offScpOptions
    365 
    366   'offRsyncOptions': ->
    367     if runtimeConfig.offRsyncOptions == ''
    368       runtimeConfig.offRsyncOptions = gitConfig.get 'off.rsyncoptions'
    369     runtimeConfig.offRsyncOptions
    370 
    371   'offScp': ->
    372     if runtimeConfig.offScp == ''
    373       runtimeConfig.offScp = gitConfig.get 'off.scphost'
    374     runtimeConfig.offScp
    375 
    376   'offScpUser': ->
    377     if runtimeConfig.offScpUser == ''
    378       runtimeConfig.offScpUser = gitConfig.get 'off.scpuser'
    379     runtimeConfig.offScpUser
    380 
    381   'log': ->
    382     if runtimeConfig.log == ''
    383       # use syncexec to avoid circular calls
    384       r = gitConfig.getSyncexec 'off.log'
    385       runtimeConfig.log = expandHome r.stdout.trim()
    386     runtimeConfig.log
    387 
    388   'getLog': ->
    389     offHelpers.log()
    390     # error handling
    391     if runtimeConfig.log == ''
    392       console.error 'Missing off.log config. Run "git config --global off.log ~/.git-off/log".'.red.bold
    393       process.exit(1)
    394     runtimeConfig.log
    395 
    396   'offConfigAlways': ->
    397     if runtimeConfig.offConfigAlways == ''
    398       r                             = gitConfig.getSyncexec 'off.configAlways'
    399       runtimeConfig.offConfigAlways = r.stdout.trim()
    400     runtimeConfig.offConfigAlways
    401 
    402   's3Region': ->
    403     if runtimeConfig.s3Region == ''
    404       runtimeConfig.s3Region = gitConfig.get 'off.s3region'
    405     runtimeConfig.s3Region
    406 
    407   's3Bucket': ->
    408     if runtimeConfig.s3Bucket == ''
    409       runtimeConfig.s3Bucket = gitConfig.get 'off.s3bucket'
    410     runtimeConfig.s3Bucket
    411 
    412   'transform': ->
    413     if runtimeConfig.transform == ''
    414       runtimeConfig.transform = gitConfig.get 'off.transform'
    415     runtimeConfig.transform
    416 
    417   'transformTo': ->
    418     if runtimeConfig.transformTo == ''
    419       runtimeConfig.transformTo = gitConfig.get 'off.transformTo'
    420     runtimeConfig.transformTo
    421 
    422   'transformFrom': ->
    423     if runtimeConfig.transformFrom == ''
    424       runtimeConfig.transformFrom = gitConfig.get 'off.transformFrom'
    425     runtimeConfig.transformFrom
    426 
    427   'userAt': ->
    428     if offHelpers.offScpUser() != ''
    429       user = offHelpers.offScpUser() + '@'
    430     else
    431       user = ''
    432     user
    433 
    434   'getSSHConfig': ->
    435     # extract host, port and path from off.sshhost
    436 
    437     user     = offHelpers.userAt()
    438 
    439     h_l      = offHelpers.offScp().split(':')
    440     if h_l[1] == undefined
    441       port = NaN
    442     else
    443       portAndPath_l = h_l[1].split('/')
    444       port     = parseInt portAndPath_l[0]
    445     if isNaN port
    446       # use default port
    447       host      = h_l[0]
    448       storePath = h_l[1]
    449     else
    450       host      = h_l[0]
    451       storePath = '/' + portAndPath_l.slice(1).join('/')
    452     [user + host, storePath, port]
    453 
    454 
    455   'mkdirStore': (p) ->
    456     # mkdir in remote store
    457 
    458     # TODO handle multiple transports
    459     h_l      = offHelpers.getSSHConfig()
    460     # bug in coffeescript - cant have '"mkdir '+ h_l[1] +'"'
    461     sshCmd   = '"mkdir -p '+ h_l[1] + '/' + p
    462     sshCmd   += '"'
    463 
    464     # setup ssh/scp private key
    465     if offHelpers.offPem() == '' or offHelpers.offPem() == 'offNoValue'
    466       pem = ''
    467     else
    468       pem = '-i ' + offHelpers.offPem()
    469 
    470     # ignore error from mkdir for already existing store
    471     if isNaN h_l[2]
    472       exec 'ssh', [offHelpers.offSshOptions(), pem, h_l[0], sshCmd], true
    473     else
    474       exec 'ssh', [offHelpers.offSshOptions(), pem, '-p ' + h_l[2], h_l[0], sshCmd], true
    475     return
    476 
    477   'rmAllStore': (p) ->
    478     # rm in remote store
    479 
    480     # setup ssh/scp private key
    481     if offHelpers.offPem() == '' or offHelpers.offPem() == 'offNoValue'
    482       pem = ''
    483     else
    484       pem = '-i ' + offHelpers.offPem()
    485 
    486     # scphost format is host:path
    487     h_l      = offHelpers.getSSHConfig()
    488     # bug in coffeescript - cant have '"mkdir '+ h_l[1] +'"'
    489     sshCmd   = '"rm -rf '+ h_l[1] + '/' + p
    490     sshCmd   += '"'
    491     # ignore error from rm
    492     if isNaN h_l[2]
    493       exec 'ssh', [offHelpers.offSshOptions(), pem, h_l[0], sshCmd], true
    494     else
    495       exec 'ssh', [offHelpers.offSshOptions(), pem, '-p ' + h_l[2], h_l[0], sshCmd], true
    496     return
    497 
    498   'copyTo': ->
    499     # list file in cache
    500     # copy to store
    501 
    502     # list file in cache
    503 
    504     tfiles = walkSync(offHelpers.objectPath())
    505     files = []
    506     for f in tfiles
    507       files.push f.replace(offHelpers.objectPath() + '/' , '')
    508 
    509     # copy to store
    510 
    511     for f in files
    512       transport.send f
    513     return
    514 
    515   'checkIntegrity': (p) ->
    516     # check integrity of files coming from the store
    517 
    518     result = true
    519 
    520     if offHelpers.offIntegrity() == 'disable'
    521       return true
    522 
    523     r           = exec 'sha', [p]
    524     # trim because git hash-object adds \n
    525     receivedSha = r.stdout.split(' ')[0].trim()
    526 
    527     if path.basename(p) != receivedSha
    528       # the file received is different from the one that was sent.
    529       console.log 'git-off: The file ' + p + ' differs from the one that was pushed.'
    530       result = false
    531 
    532     result
    533 
    534   'setTransport': (mode = 'config') ->
    535     # set send and receive functions for transport
    536     # copy, scp
    537 
    538     # use mode from config or from parameter
    539     if mode == 'config'
    540       mode = offHelpers.offMode()
    541 
    542     # copy mode
    543     if mode == 'copy'
    544       transport['send'] = (file) ->
    545         # create file directories in store
    546         f_l = file.split '/'
    547         if fs.existsSync(offHelpers.offStore() + '/' + f_l[0] + '/' + f_l[1]) == false
    548           mkdirParents offHelpers.offStore() + '/' + f_l[0] + '/' + f_l[1]
    549 
    550         copy offHelpers.objectPath() + '/' + file, offHelpers.offStore() + '/' + file
    551         fs.chmodSync offHelpers.offStore() + '/' + file, '444'
    552         return
    553       transport['receive'] = (file) ->
    554         # transfer and transform
    555         if offHelpers.transform() == 'enable' and offHelpers.transformFrom() != ''
    556           # create file directories in objectPath
    557           f_l = file.split '/'
    558           if fs.existsSync(offHelpers.objectPath() + '/' + f_l[0] + '/' + f_l[1]) == false
    559             mkdirParents offHelpers.objectPath() + '/' + f_l[0] + '/' + f_l[1]
    560 
    561           copy offHelpers.offStore() + '/' + file, offHelpers.objectPath() + '/' + file
    562           transport['transformFrom'](file)
    563           if offHelpers.checkIntegrity offHelpers.objectPath() + '/../tmp/' + file
    564             readStream = fs.createReadStream(offHelpers.objectPath() + '/../tmp/' + file)
    565             readStream.pipe(process.stdout)
    566         else
    567           if offHelpers.checkIntegrity offHelpers.offStore() + '/' + file
    568             readStream = fs.createReadStream(offHelpers.offStore() + '/' + file)
    569             readStream.pipe(process.stdout)
    570         return
    571 
    572     # rsync mode
    573     else if mode == 'rsync'
    574       transport['send'] = (file) ->
    575 
    576         # create file directories in store
    577         f_l = file.split '/'
    578         offHelpers.mkdirStore f_l[0] + '/' + f_l[1]
    579 
    580         # set pem or not? check rsyncoptions
    581         options = offHelpers.offRsyncOptions()
    582         if options.indexOf('_i') != -1
    583           # there is an identity setting in options
    584           # setup ssh/scp private key
    585           if offHelpers.offPem() != '' and offHelpers.offPem() != 'offNoValue'
    586             options = options.replace '_i', offHelpers.offPem()
    587 
    588         exec 'rsync', [options, offHelpers.objectPath() + '/' + file, offHelpers.offScp() + '/' + file]
    589         return
    590       transport['receive'] = (file) ->
    591         # create file directories in cache
    592         f_l = file.split '/'
    593         if fs.existsSync(offHelpers.objectPath() + '/' + f_l[0] + '/' + f_l[1]) == false
    594           mkdirParents offHelpers.objectPath() + '/' + f_l[0] + '/' + f_l[1]
    595 
    596         # set pem or not? check rsyncoptions
    597         options = offHelpers.offRsyncOptions()
    598         if options.indexOf('_i') != -1
    599           # there is an identity setting in options
    600           # setup ssh/scp private key
    601           if offHelpers.offPem() != '' and offHelpers.offPem() != 'offNoValue'
    602             options = options.replace '_i', offHelpers.offPem()
    603 
    604         exec 'rsync', [options, offHelpers.offScp() + '/' + file, offHelpers.objectPath() + '/' + file]
    605 
    606         # transform
    607         if offHelpers.transform() == 'enable' and offHelpers.transformFrom() != ''
    608           transport['transformFrom'](file)
    609           if offHelpers.checkIntegrity offHelpers.objectPath() + '/../tmp/' + file
    610             readStream = fs.createReadStream(offHelpers.objectPath() + '/../tmp/' + file)
    611             readStream.pipe(process.stdout)
    612         else
    613           if offHelpers.checkIntegrity offHelpers.objectPath() + '/' + file
    614             readStream = fs.createReadStream(offHelpers.objectPath() + '/' + file)
    615             readStream.pipe(process.stdout)
    616         return
    617 
    618     # scp mode
    619     else if mode == 'scp'
    620       transport['send'] = (file) ->
    621 
    622         # create file directories in store
    623         f_l = file.split '/'
    624         offHelpers.mkdirStore f_l[0] + '/' + f_l[1]
    625 
    626         # setup ssh/scp private key
    627         if offHelpers.offPem() == '' or offHelpers.offPem() == 'offNoValue'
    628           pem = ''
    629         else
    630           pem = '-i ' + offHelpers.offPem()
    631 
    632         h_l = offHelpers.getSSHConfig()
    633         if isNaN h_l[2]
    634           exec 'scp', [offHelpers.offScpOptions(), pem, offHelpers.objectPath() + '/' + file, h_l[0] + ':' + h_l[1] + '/' + file]
    635         else
    636           exec 'scp', [offHelpers.offScpOptions(), pem, '-P ' + h_l[2], offHelpers.objectPath() + '/' + file, h_l[0] + ':' + h_l[1] + '/' + file]
    637         return
    638       transport['receive'] = (file) ->
    639         # create file directories in cache
    640         f_l = file.split '/'
    641         if fs.existsSync(offHelpers.objectPath() + '/' + f_l[0] + '/' + f_l[1]) == false
    642           mkdirParents offHelpers.objectPath() + '/' + f_l[0] + '/' + f_l[1]
    643 
    644         # setup ssh/scp private key
    645         if offHelpers.offPem() == '' or offHelpers.offPem() == 'offNoValue'
    646           pem = ''
    647         else
    648           pem = '-i ' + offHelpers.offPem()
    649 
    650         h_l = offHelpers.getSSHConfig()
    651         if isNaN h_l[2]
    652           exec 'scp', [offHelpers.offScpOptions(), pem, h_l[0] + ':' + h_l[1] + '/' + file, offHelpers.objectPath() + '/' + file]
    653         else
    654           exec 'scp', [offHelpers.offScpOptions(), pem, '-P ' + h_l[2], h_l[0] + ':' + h_l[1] + '/' + file, offHelpers.objectPath() + '/' + file]
    655 
    656         # transform
    657         if offHelpers.transform() == 'enable' and offHelpers.transformFrom() != ''
    658           transport['transformFrom'](file)
    659           if offHelpers.checkIntegrity offHelpers.objectPath() + '/../tmp/' + file
    660             readStream = fs.createReadStream(offHelpers.objectPath() + '/../tmp/' + file)
    661             readStream.pipe(process.stdout)
    662         else
    663           if offHelpers.checkIntegrity offHelpers.objectPath() + '/' + file
    664             readStream = fs.createReadStream(offHelpers.objectPath() + '/' + file)
    665             readStream.pipe(process.stdout)
    666         return
    667 
    668     # http mode
    669     else if mode == 'http'
    670       transport['send'] = (file) ->
    671         offLogRepo('Http mode is read-only: ' + file + ' is stored in cache only')
    672         return
    673       transport['receive'] = (file) ->
    674         # create file directories in cache
    675         f_l = file.split '/'
    676         if fs.existsSync(offHelpers.objectPath() + '/' + f_l[0] + '/' + f_l[1]) == false
    677           mkdirParents offHelpers.objectPath() + '/' + f_l[0] + '/' + f_l[1]
    678 
    679         exec 'curl', [offHelpers.offCurlOptions(), offHelpers.objectPath() + '/' + file, offHelpers.offHttp() + '/' + file]
    680 
    681         # transform
    682         if offHelpers.transform() == 'enable' and offHelpers.transformFrom() != ''
    683           transport['transformFrom'](file)
    684           if offHelpers.checkIntegrity offHelpers.objectPath() + '/../tmp/' + file
    685             readStream = fs.createReadStream(offHelpers.objectPath() + '/../tmp/' + file)
    686             readStream.pipe(process.stdout)
    687         else
    688           if offHelpers.checkIntegrity offHelpers.objectPath() + '/' + file
    689             readStream = fs.createReadStream(offHelpers.objectPath() + '/' + file)
    690             readStream.pipe(process.stdout)
    691         return
    692 
    693     # s3 mode
    694     else if mode == 's3'
    695       transport['send'] = (file) ->
    696         AWS = require 'aws-sdk'
    697         s3 = new AWS.S3({region: offHelpers.s3Region(), signatureVersion: 'v4'})
    698 
    699         # upload to s3
    700         upParams = {Bucket: offHelpers.s3Bucket(), Key: file, Body: ''}
    701         fileStream = fs.createReadStream(offHelpers.objectPath() + '/' + file)
    702         upParams.Body = fileStream
    703         s3.upload(upParams, (err, data) ->
    704           # TODO log eventual error
    705           return
    706         )
    707 
    708         return
    709       transport['receive'] = (file) ->
    710         AWS = require 'aws-sdk'
    711         s3 = new AWS.S3({region: offHelpers.s3Region(), signatureVersion: 'v4'})
    712 
    713         # create file directories in cache
    714         f_l = file.split '/'
    715         if fs.existsSync(offHelpers.objectPath() + '/' + f_l[0] + '/' + f_l[1]) == false
    716           mkdirParents offHelpers.objectPath() + '/' + f_l[0] + '/' + f_l[1]
    717 
    718         # download from s3
    719         dlParams = {Bucket: offHelpers.s3Bucket(), Key: file}
    720         fileStream = fs.createWriteStream(offHelpers.objectPath() + '/' + file)
    721         fileStream.on('finish', ->
    722           # transfer is finished, the complete file is in the cache
    723           # transform
    724           if offHelpers.transform() == 'enable' and offHelpers.transformFrom() != ''
    725             transport['transformFrom'](file)
    726             if offHelpers.checkIntegrity offHelpers.objectPath() + '/../tmp/' + file
    727               readStream = fs.createReadStream(offHelpers.objectPath() + '/../tmp/' + file)
    728               readStream.pipe(process.stdout)
    729           else
    730             if offHelpers.checkIntegrity offHelpers.objectPath() + '/' + file
    731               readStream = fs.createReadStream(offHelpers.objectPath() + '/' + file)
    732               readStream.pipe(process.stdout)
    733           return
    734         )
    735         s3.getObject(dlParams, (err, data) ->
    736           # TODO log eventual error
    737           return
    738         ).on('httpData', (chunk) ->
    739           fileStream.write chunk
    740           return
    741         ).on('httpDone', ->
    742           fileStream.end()
    743           return
    744         )
    745         return
    746     return
    747 
    748   'getOffFilePath': (offFile) ->
    749     [offFile.slice(0,2) + '/' + offFile.slice(2,4) + '/' + offFile, offFile.slice(0,2) + '/' + offFile.slice(2,4)]
    750 
    751 # list offHelpers, run 'git off'
    752 #console.log Object.keys(offHelpers)
    753 
    754 ###
    755 # core
    756 ###
    757 
    758 transport =
    759   'send': (src) ->
    760     # to be initialized by setTransport
    761     return
    762   'receive': (src) ->
    763     # send data to stdout
    764     # to be initialized by setTransport
    765     return
    766   'transformFrom': (file) ->
    767     # transform file in objectPath to objectPath/../tmp/file
    768     # create directories in tmp
    769     offFile     = path.basename file
    770     offFilePath = offHelpers.getOffFilePath(offFile)
    771     if fs.existsSync(offHelpers.objectPath() + '/../tmp/' + offFilePath[1]) == false
    772       # create the file directory
    773       mkdirParents offHelpers.objectPath() + '/../tmp/' + offFilePath[1]
    774 
    775     cmd = offHelpers.transformFrom()
    776     cmd = cmd.replace('_1', offHelpers.objectPath() + '/' + file).replace('_2', offHelpers.objectPath() + '/../tmp/' + file)
    777     r = syncexec cmd
    778     return
    779 
    780 
    781 # command line functions
    782 # localSetup: setup current git
    783 # install:    setup git config (default global)
    784 # mode:       set/show git off mode
    785 # scp:        setup scp config
    786 # scpuser:    setup scp username config
    787 # track:      setup gitattribute filters
    788 # clean:      replace files handled by git off with reference
    789 # prepush:    read stdin and calls push
    790 # push:       copy objects to off.store
    791 # smudge:     copy objects from off.store for files handled by git off
    792 # clearAll:   delete store, cache in current git and log
    793 # clearCache: delete cache in current git
    794 # clearStore: delete store
    795 # defaults:   show first time config (offDEFAULTS)
    796 # env:        show config
    797 offCommands =
    798   'localSetup': ->
    799     # setup current git
    800     # create off directories in current .git
    801     # install pre-push hooks
    802     mkdirParents offHelpers.objectPath()
    803 
    804     hook = offHelpers.gitRepoRoot() + '/.git/hooks/pre-push'
    805     if fs.existsSync(hook) == false
    806       fs.writeFileSync hook, offDEFAULTS.prePush
    807       fs.chmodSync hook, '755'
    808     # error disabled because localSetup is run many times (called from clean and smudge)
    809     # else
    810     #   console.error hook.red.bold + ' already exists. Skipping pre-push hook setup.'.red.bold
    811     #   process.exit(1)
    812     return
    813 
    814   'install': (setCfg = gitConfig.set) ->
    815     # setup git config
    816     # create off.store
    817 
    818     offCommands.localSetup()
    819 
    820     # setup config only if not already set
    821     if gitConfig.get('filter.off.clean') == '' or setCfg != gitConfig.set
    822       if setCfg == gitConfig.setThisRepo
    823        gitConfig.setLocal('filter.off.clean','git off clean %f')
    824       else
    825        setCfg('filter.off.clean','git off clean %f')
    826     if gitConfig.get('filter.off.smudge') == '' or setCfg != gitConfig.set
    827       if setCfg == gitConfig.setThisRepo
    828         gitConfig.setLocal('filter.off.smudge','git off smudge %f')
    829       else
    830         setCfg('filter.off.smudge','git off smudge %f')
    831     if offHelpers.log() == '' or setCfg != gitConfig.set
    832         # log always in global config
    833         gitConfig.set('off.log',offDEFAULTS.log)
    834 
    835     if offHelpers.offMode() == '' or setCfg != gitConfig.set
    836       setCfg('off.mode', offDEFAULTS.mode)
    837     if offHelpers.offIntegrity() == '' or setCfg != gitConfig.set
    838       setCfg('off.integrity', offDEFAULTS.integrity)
    839     if offHelpers.offPem() == '' or setCfg != gitConfig.set
    840       setCfg('off.pem', offDEFAULTS.pem)
    841     if offHelpers.offScp() == '' or setCfg != gitConfig.set
    842       setCfg('off.scphost', offDEFAULTS.scpHost)
    843     if offHelpers.offScpUser() == '' or setCfg != gitConfig.set
    844       setCfg('off.scpuser', offDEFAULTS.scpUser)
    845     if offHelpers.offSshOptions() == '' or setCfg != gitConfig.set
    846       setCfg('off.sshoptions', offDEFAULTS.sshOptions)
    847     if offHelpers.offScpOptions() == '' or setCfg != gitConfig.set
    848       setCfg('off.scpoptions', offDEFAULTS.scpOptions)
    849     if offHelpers.offRsyncOptions() == '' or setCfg != gitConfig.set
    850       setCfg('off.rsyncoptions', offDEFAULTS.rsyncOptions)
    851     if offHelpers.offStore() == '' or setCfg != gitConfig.set
    852       setCfg('off.store', offDEFAULTS.store)
    853     if offHelpers.offHttp() == '' or setCfg != gitConfig.set
    854       setCfg('off.http', offDEFAULTS.http)
    855     if offHelpers.offCurlOptions() == '' or setCfg != gitConfig.set
    856       setCfg('off.curloptions', offDEFAULTS.curlOptions)
    857     if offHelpers.offConfigAlways() == '' or setCfg != gitConfig.set
    858       setCfg('off.configAlways', offDEFAULTS.configAlways)
    859     if offHelpers.s3Region() == '' or setCfg != gitConfig.set
    860       setCfg('off.s3Region', offDEFAULTS.s3Region)
    861     if offHelpers.s3Bucket() == '' or setCfg != gitConfig.set
    862       setCfg('off.s3Bucket', offDEFAULTS.s3Bucket)
    863     if offHelpers.transform() == '' or setCfg != gitConfig.set
    864       setCfg('off.transform', offDEFAULTS.transform)
    865     if offHelpers.transformTo() == '' or setCfg != gitConfig.set
    866       setCfg('off.transformTo', offDEFAULTS.transformTo)
    867     if offHelpers.transformFrom() == '' or setCfg != gitConfig.set
    868       setCfg('off.transformFrom', offDEFAULTS.transformFrom)
    869 
    870     # create off.store
    871 
    872     if runtimeConfig.offMode == 'copy'
    873       mkdirParents runtimeConfig.offStore
    874 
    875     if runtimeConfig.offMode == 'scp' or runtimeConfig.offMode == 'rsync'
    876       offHelpers.mkdirStore ''
    877     return
    878 
    879   'mode': (setCfg) ->
    880     len  = process.argv.length
    881     mode = process.argv[len-1]
    882     if mode != 'mode' and mode != 'thisrepo'
    883       setCfg('off.mode', mode)
    884     else
    885       console.log 'off.mode '.blue.bold + offHelpers.offMode()
    886     return
    887 
    888   'store': (setCfg) ->
    889     len  = process.argv.length
    890     store = process.argv[len-1]
    891     if store != 'store' and store != 'thisrepo'
    892       setCfg('off.store', store)
    893     else
    894       console.log 'off.store '.blue.bold + offHelpers.offStore()
    895     return
    896 
    897   'scp': (setCfg) ->
    898     len     = process.argv.length
    899     scpHost = process.argv[len-1]
    900     if scpHost != 'scp' and scpHost != 'thisrepo'
    901       setCfg('off.scphost', scpHost)
    902     else
    903       console.log 'off.scp '.blue.bold + offHelpers.offScp()
    904     return
    905 
    906   'http': (setCfg) ->
    907     len  = process.argv.length
    908     http = process.argv[len-1]
    909     if http != 'http' and http != 'thisrepo'
    910       setCfg('off.http', http)
    911     else
    912       console.log 'off.http '.blue.bold + offHelpers.offHttp()
    913     return
    914 
    915   'curl': (setCfg) ->
    916     len  = process.argv.length
    917     curl = process.argv[len-1]
    918     if curl != 'curl' and curl != 'thisrepo'
    919       setCfg('off.curloptions', curl)
    920     else
    921       console.log 'off.curloptions '.blue.bold + offHelpers.offCurlOptions()
    922     return
    923 
    924   'integrity': (setCfg) ->
    925     len       = process.argv.length
    926     integrity = process.argv[len-1]
    927     if integrity != 'integrity' and integrity != 'thisrepo'
    928       setCfg('off.integrity', integrity)
    929     else
    930       console.log 'off.integrity '.blue.bold + offHelpers.offIntegrity()
    931     return
    932 
    933   'pem': (setCfg) ->
    934     len = process.argv.length
    935     pem = process.argv[len-1]
    936     if pem != 'pem' and pem != 'thisrepo'
    937       setCfg('off.pem', pem)
    938     else
    939       console.log 'off.pem '.blue.bold + offHelpers.offPem()
    940     return
    941 
    942   'sshoptions': (setCfg) ->
    943     len        = process.argv.length
    944     sshoptions = process.argv[len-1]
    945     if sshoptions != 'sshoptions' and sshoptions != 'thisrepo'
    946       setCfg('off.sshoptions', sshoptions)
    947     else
    948       console.log 'off.sshoptions '.blue.bold + offHelpers.offSshOptions()
    949     return
    950 
    951   'scpoptions': (setCfg) ->
    952     len        = process.argv.length
    953     scpoptions = process.argv[len-1]
    954     if scpoptions != 'scpoptions' and scpoptions != 'thisrepo'
    955       setCfg('off.scpoptions', scpoptions)
    956     else
    957       console.log 'off.scpoptions '.blue.bold + offHelpers.offScpOptions()
    958     return
    959 
    960   'rsyncoptions': (setCfg) ->
    961     len        = process.argv.length
    962     rsyncoptions = process.argv[len-1]
    963     if rsyncoptions != 'rsyncoptions' and rsyncoptions != 'thisrepo'
    964       setCfg('off.rsyncoptions', rsyncoptions)
    965     else
    966       console.log 'off.rsyncoptions '.blue.bold + offHelpers.offRsyncOptions()
    967     return
    968 
    969   'scpUser': (setCfg) ->
    970     len     = process.argv.length
    971     scpUser = process.argv[len-1]
    972     if scpUser != 'scpuser' and scpUser != 'thisrepo'
    973       setCfg('off.scpuser', scpUser)
    974     else
    975       console.log 'off.scpuser '.blue.bold + offHelpers.offScpUser()
    976     return
    977 
    978   'track': ->
    979     # setup gitattribute filters in current folder
    980     # list current git off attributes when there is no parameter
    981     if process.argv[3] != undefined
    982       offCommands.install()
    983       fs.appendFileSync(offHelpers.gitRepoRoot() + '/.gitattributes', process.argv[3] + ' filter=off -text\n')
    984     else
    985       # list current git off attributes
    986 
    987       r = exec 'listAttr', ['`cd ' + offHelpers.gitRepoRoot()  + '; git ls-files`']
    988       # gff/b.bin: filter: off
    989       for l in r.stdout.split('\n')
    990         if l.indexOf(': filter: off') != -1
    991           console.log l
    992     return
    993 
    994   'configAlways': ->
    995     len          = process.argv.length
    996     configAlways = process.argv[len-1]
    997     if configAlways != 'configAlways'
    998       gitConfig.set 'off.configAlways', configAlways
    999     else
   1000       console.log 'off.configAlways '.blue.bold + offHelpers.offConfigAlways()
   1001     return
   1002 
   1003   's3region': (setCfg) ->
   1004     len  = process.argv.length
   1005     region = process.argv[len-1]
   1006     if region != 's3region' and region != 'thisrepo'
   1007       setCfg('off.s3region', region)
   1008     else
   1009       console.log 'off.s3region '.blue.bold + offHelpers.s3Region()
   1010     return
   1011 
   1012   's3bucket': (setCfg) ->
   1013     len  = process.argv.length
   1014     bucket = process.argv[len-1]
   1015     if bucket != 's3bucket' and bucket != 'thisrepo'
   1016       setCfg('off.s3bucket', bucket)
   1017     else
   1018       console.log 'off.s3bucket '.blue.bold + offHelpers.s3Bucket()
   1019     return
   1020 
   1021   'transform': (setCfg) ->
   1022     len  = process.argv.length
   1023     setting = process.argv[len-1]
   1024     if setting != 'transform' and setting != 'thisrepo'
   1025       setCfg('off.transform', setting)
   1026     else
   1027       console.log 'off.transform '.blue.bold + offHelpers.transform()
   1028     return
   1029 
   1030   'transformTo': (setCfg) ->
   1031     len  = process.argv.length
   1032     setting = process.argv[len-1]
   1033     if setting != 'transformTo' and setting != 'thisrepo'
   1034       setCfg('off.transformTo', setting)
   1035     else
   1036       console.log 'off.transformTo '.blue.bold + offHelpers.transformTo()
   1037     return
   1038 
   1039   'transformFrom': (setCfg) ->
   1040     len  = process.argv.length
   1041     setting = process.argv[len-1]
   1042     if setting != 'transformFrom' and setting != 'thisrepo'
   1043       setCfg('off.transformFrom', setting)
   1044     else
   1045       console.log 'off.transformFrom '.blue.bold + offHelpers.transformFrom()
   1046     return
   1047 
   1048   'clean': ->
   1049     # replace files handled by git off with reference
   1050     # stdin is data from the working directory file
   1051     #
   1052     # create local setup in case the repo is freshly cloned
   1053     # create file information (size, sha = git off filename)
   1054     # copy stdin to git off cache in current git
   1055     #   transform input file
   1056     # print git off ref to stdout for git
   1057 
   1058     # create local setup in case the repo is freshly cloned
   1059     offCommands.localSetup()
   1060 
   1061     # create file information (size, sha)
   1062     file        = process.argv[3]
   1063     size        = fs.statSync(file).size
   1064     quotedFile  = '"'+file+'"'
   1065     r           = exec 'sha', [quotedFile]
   1066     # trim because git hash-object adds \n
   1067     offFile     = r.stdout.split(' ')[0].trim()
   1068     offFilePath = offHelpers.getOffFilePath(offFile)
   1069     if fs.existsSync(offHelpers.objectPath() + '/' + offFilePath[1]) == false
   1070       # create the file directory
   1071       mkdirParents offHelpers.objectPath() + '/' + offFilePath[1]
   1072     offFilePath = offFilePath[0]
   1073 
   1074     # copy stdin to git off cache in current git
   1075     # clean runs during git add and git commit, do work only once
   1076     if fs.existsSync(offHelpers.objectPath() + '/' + offFilePath) == false
   1077       # stream stdin to sha in git off cache
   1078       writeStream = fs.createWriteStream(offHelpers.objectPath() + '/' + offFilePath)
   1079       pipe = process.stdin.pipe(writeStream)
   1080       pipe.on('finish', ->
   1081         # all objects in cache are read-only
   1082         fs.chmodSync offHelpers.objectPath() + '/' + offFilePath, '444'
   1083 
   1084         # transform input file
   1085         if offHelpers.transform() == 'enable' and offHelpers.transformTo() != ''
   1086           if fs.existsSync offHelpers.objectPath() + '/../tmp' == false
   1087             mkdirParents offHelpers.objectPath() + '/../tmp'
   1088 
   1089           # create directories in tmp
   1090           offFilePath = offHelpers.getOffFilePath(offFile)
   1091           if fs.existsSync(offHelpers.objectPath() + '/../tmp/' + offFilePath[1]) == false
   1092             # create the file directory
   1093             mkdirParents offHelpers.objectPath() + '/../tmp/' + offFilePath[1]
   1094           offFilePath = offFilePath[0]
   1095 
   1096           # original file is moved to tmp
   1097           # transformed file is stored in objectPath under the same name
   1098           exec 'mv', [offHelpers.objectPath() + '/' + offFilePath, offHelpers.objectPath() + '/../tmp/' + offFilePath]
   1099           cmd = offHelpers.transformTo()
   1100           cmd = cmd.replace('_1', offHelpers.objectPath() + '/../tmp/' + offFilePath).replace('_2', offHelpers.objectPath() + '/' + offFilePath)
   1101           r = syncexec cmd
   1102         return
   1103       )
   1104     else
   1105       # file already exists, discard data
   1106       writeStream = fs.createWriteStream('/dev/null')
   1107       process.stdin.pipe(writeStream)
   1108 
   1109     # print git off ref to stdout for git
   1110     # ### git-off v1 sha:be3e02b60effe3eab232d5590a6a2e2c2c2f443b size:119 b.bin
   1111     console.log offDEFAULTS.offSignature + offFile + ' size:' + size
   1112     return
   1113 
   1114   'prepush': ->
   1115     # read stdin and calls push
   1116     # stdin (data from git) is a line with remoteName url localRef localSha remoteRef remoteSha
   1117 
   1118     offHelpers.setTransport()
   1119 
   1120     rl = readline.createInterface(
   1121       input: process.stdin
   1122       output: process.stdout
   1123       terminal: false)
   1124     rl.on 'line', (line) ->
   1125       offCommands.push(line)
   1126       return
   1127     return
   1128 
   1129   'push': (line) ->
   1130     # copy objects to off.store
   1131     #
   1132     # set remoteName url localRef localSha remoteRef remoteSha
   1133     # list missing commits in remote
   1134     # list changed objects in missing commits
   1135     # check header with git cat
   1136     # copy git off objects to off.store
   1137 
   1138     len        = process.argv.length
   1139 
   1140     # set remoteName url localRef localSha remoteRef remoteSha
   1141     remoteName = process.argv[len-2]
   1142     url        = process.argv[len-1]
   1143 
   1144     line_l     = line.split(' ')
   1145     localRef   = line_l[0]
   1146     localSha   = line_l[1]
   1147     remoteRef  = line_l[2]
   1148     remoteSha  = line_l[3]
   1149 
   1150     # list missing commits in remote
   1151     r = 0
   1152     # handle empty cloned repo
   1153     if remoteSha == '0000000000000000000000000000000000000000'
   1154       r = exec 'gitListEmptyRemote', [localSha]
   1155     else
   1156       r = exec 'gitList', [localSha, '^' + remoteSha]
   1157     commitListToPush = r.stdout.split('\n')
   1158     # remove last empty line
   1159     commitListToPush = commitListToPush.slice(0, commitListToPush.length-1)
   1160 
   1161 
   1162     # list changed objects in missing commits
   1163     for sha in commitListToPush
   1164       # list changes for commit sha
   1165       r = exec 'gitDiff', [sha]
   1166       # :000000 100644 0000000000000000000000000000000000000000 23636079ef005f53e81066aaf092521d8f2346df A      dsd
   1167       # TODO filter according to .gitattributes, check files in filter only
   1168       objInfoList = r.stdout.split('\n')
   1169       # remove first line (commit) and last empty line
   1170       objInfoList = objInfoList.slice(1, objInfoList.length-1)
   1171 
   1172       for oi in objInfoList
   1173         objInfo = oi.split(' ')
   1174 
   1175         # dont consider deleted files
   1176         if objInfo[oiNAME].slice(0,1) != 'D'
   1177 
   1178           # check header with git cat
   1179 
   1180           r = exec 'gitCat', [objInfo[oiOID]]
   1181           offRef = r.stdout.split('\n')[0]
   1182 
   1183           if offRef.indexOf(offDEFAULTS.offSignature) != -1
   1184 
   1185             # copy git off objects to off.store
   1186             # found an off reference
   1187             # ### git-off v1 sha:be3e02b60effe3eab232d5590a6a2e2c2c2f443b size:119 b.bin
   1188 
   1189             offFile = offRef.split(':')[1].split(' ')[0]
   1190             offFilePath = offHelpers.getOffFilePath(offFile)[0]
   1191 
   1192             transport.send offFilePath
   1193 
   1194     # test prevent push by issuing an error
   1195     #process.exit(1)
   1196     return
   1197 
   1198   'smudge': ->
   1199     # copy objects from off.store for files handled by git off
   1200     # stdin is the data from the git repo
   1201     #
   1202     # load header
   1203     # for git off object, replace git off reference with data from cache or off.store
   1204     # for normal object, copy data from repo to stdout
   1205 
   1206     offHelpers.setTransport()
   1207     offCommands.localSetup()
   1208     #not used file = process.argv[3]
   1209 
   1210     status = 'header'
   1211     process.stdin.on('readable', ->
   1212       if status == 'header'
   1213         # change status before streaming to stdout
   1214         status = 'stream'
   1215 
   1216         # load header
   1217 
   1218         data = process.stdin.read(offDEFAULTS.offSignature.length + offDEFAULTS.shaLength)
   1219         decoder = new StringDecoder('utf8')
   1220         if data == null
   1221           # error no data
   1222           offLogRepo 'smudge error: cant get data from stdin.'
   1223           process.exit(1)
   1224         header = decoder.write(data)
   1225 
   1226         if header.slice(0,offDEFAULTS.offSignature.length) == offDEFAULTS.offSignature
   1227           # for git off object, replace git off reference with data from cache or off.store
   1228 
   1229           offFile     = header.split(':')[1]
   1230           offFilePath = offHelpers.getOffFilePath(offFile)[0]
   1231           # detect if file is already in cache
   1232           if fs.existsSync(offHelpers.objectPath() + '/' + offFilePath) == true
   1233             # copy from cache
   1234             # transform file
   1235             if offHelpers.transform() == 'enable' and offHelpers.transformFrom() != ''
   1236               if fs.existsSync offHelpers.objectPath() + '/../tmp' == false
   1237                 mkdirParents offHelpers.objectPath() + '/../tmp'
   1238 
   1239               # create directories in tmp
   1240               offFilePath = offHelpers.getOffFilePath(offFile)
   1241               if fs.existsSync(offHelpers.objectPath() + '/../tmp/' + offFilePath[1]) == false
   1242                 # create the file directory
   1243                 mkdirParents offHelpers.objectPath() + '/../tmp/' + offFilePath[1]
   1244               offFilePath = offFilePath[0]
   1245 
   1246               cmd = offHelpers.transformFrom()
   1247               cmd = cmd.replace('_1', offHelpers.objectPath() + '/' + offFilePath).replace('_2', offHelpers.objectPath() + '/../tmp/' + offFilePath)
   1248               r = syncexec cmd
   1249               readStream = fs.createReadStream(offHelpers.objectPath() + '/../tmp/' + offFilePath)
   1250             else
   1251               readStream = fs.createReadStream(offHelpers.objectPath() + '/' + offFilePath)
   1252             readStream.pipe(process.stdout)
   1253           else
   1254             # copy from off.store
   1255             transport.receive offFilePath
   1256         else
   1257           # for normal object, copy data from repo to stdout
   1258 
   1259           process.stdout.write(data)
   1260           process.stdin.pipe(process.stdout)
   1261       return)
   1262     return
   1263 
   1264   'copyTo': ->
   1265     # copy cache to store for process.argv[3] mode
   1266     if process.argv[3] == undefined
   1267       console.log 'Choose a mode where to copy the cache'.red.bold
   1268       return
   1269 
   1270     offHelpers.setTransport(process.argv[3])
   1271     offHelpers.copyTo()
   1272     return
   1273 
   1274   'pushTo': ->
   1275     if offHelpers.offMode() != undefined
   1276       offHelpers.setTransport offHelpers.offMode()
   1277       offHelpers.copyTo()
   1278     return
   1279 
   1280   'clearAll': ->
   1281     # delete store, cache in current git and log
   1282     offCommands.clearStore()
   1283     offCommands.clearCache()
   1284     rmAll offHelpers.getLog()
   1285     return
   1286 
   1287   'clearCache': ->
   1288     # delete cache in current git
   1289     rmAll offHelpers.gitRepoRoot()+'/.git/off'
   1290     return
   1291 
   1292   'clearStore': ->
   1293     # delete store
   1294     if offHelpers.offMode() == 'copy'
   1295       rmAll offHelpers.offStore()
   1296     if offHelpers.offMode() == 'scp' or offHelpers.offMode() == 'rsync'
   1297       offHelpers.rmAllStore ''
   1298     return
   1299 
   1300   'clearTmp': ->
   1301     # delete cache in current git
   1302     rmAll offHelpers.gitRepoRoot()+'/.git/off/tmp'
   1303     return
   1304 
   1305   'defaults': ->
   1306     # show first time config (offDEFAULTS)
   1307     for k in Object.keys(offDEFAULTS)
   1308       console.log k.blue.bold + ' ' + offDEFAULTS[k]
   1309     return
   1310 
   1311   'env': ->
   1312     console.log 'off.mode '.blue.bold + offHelpers.offMode()
   1313     console.log 'off.integrity '.blue.bold + offHelpers.offIntegrity()
   1314     console.log 'off.pem '.blue.bold + offHelpers.offPem()
   1315     console.log 'off.sshoptions '.blue.bold + offHelpers.offSshOptions()
   1316     console.log 'off.scpoptions '.blue.bold + offHelpers.offScpOptions()
   1317     console.log 'off.rsyncoptions '.blue.bold + offHelpers.offRsyncOptions()
   1318     console.log 'off.store '.blue.bold + offHelpers.offStore()
   1319     console.log 'off.http '.blue.bold + offHelpers.offHttp()
   1320     console.log 'off.curloptions '.blue.bold + offHelpers.offCurlOptions()
   1321     console.log 'off.scphost '.blue.bold + offHelpers.offScp()
   1322     console.log 'off.scpuser '.blue.bold + offHelpers.offScpUser()
   1323     console.log 'off.log '.blue.bold + offHelpers.getLog()
   1324     console.log 'off.configAlways '.blue.bold + offHelpers.offConfigAlways()
   1325     console.log 'off.s3region '.blue.bold + offHelpers.s3Region()
   1326     console.log 'off.s3bucket '.blue.bold + offHelpers.s3Bucket()
   1327     console.log 'off.transform '.blue.bold + offHelpers.transform()
   1328     console.log 'off.transformTo '.blue.bold + offHelpers.transformTo()
   1329     console.log 'off.transformFrom '.blue.bold + offHelpers.transformFrom()
   1330     return
   1331 
   1332   'help': ->
   1333     console.log 'git-off help\n'.green.bold
   1334     if process.argv[3] == undefined
   1335       console.log '# Quick Start'.green.bold
   1336       console.log 'Setup:'
   1337       console.log "git off track '*.bin'"
   1338       console.log 'git add .'
   1339       console.log 'git commit'
   1340       console.log 'git push'
   1341       console.log 'git checkout master'
   1342       console.log '\n# Other'.green.bold
   1343       console.log 'git off install'
   1344       console.log 'git off mode scp'
   1345       console.log 'git off scp localhost:/tmp/offStore'
   1346       console.log 'git off scpuser username'
   1347       console.log 'git off cc'
   1348       console.log 'git off ca'
   1349       console.log 'git off env'
   1350       console.log 'git off defaults\n'
   1351 
   1352       for c in Object.keys(COMMAND_MAP)
   1353         showCommandHelp(c)
   1354 
   1355       console.log '\nGo to https://noulin.net/git-off/file/README.md.html for more information'.green.bold
   1356     else
   1357       if COMMAND_MAP[process.argv[3]] == undefined
   1358         console.log COMMAND_MAP.help.h.green
   1359       else
   1360         showCommandHelp(process.argv[3])
   1361     return
   1362 
   1363 # list offCommands, run 'git off'
   1364 #console.log Object.keys(offCommands)
   1365 
   1366 ###
   1367 # main
   1368 ###
   1369 
   1370 # parse CLI arguments
   1371 
   1372 thisrepo = (cmd) ->
   1373   if process.argv[3] != 'thisrepo'
   1374     cmd(gitConfig['set'])
   1375   else
   1376     # setup config in current repo
   1377     cmd(gitConfig['setThisRepo'])
   1378   return
   1379 
   1380 showCommandHelp = (cmd) ->
   1381   l_l = COMMAND_MAP[cmd].h.split('\n')
   1382   console.log l_l[0]
   1383   l_l = l_l.slice(1)
   1384   console.log l_l.join('\n').green
   1385   return
   1386 
   1387 COMMAND_MAP =
   1388   'install':
   1389     f: ->
   1390       thisrepo offCommands['install']
   1391       return
   1392     h: 'git off install [thisrepo]\n  setup git config (default global)\n  thisrepo sets up config in current repo'
   1393 
   1394   'mode':
   1395     f: ->
   1396       thisrepo offCommands['mode']
   1397       return
   1398     h: 'git off mode [thisrepo] [copy|rsync|scp|http|s3]\n  set/show git off mode'
   1399 
   1400   'store':
   1401     f: ->
   1402       thisrepo offCommands['store']
   1403       return
   1404     h: 'git off store [thisrepo] [path]\n  set/show git off store path for copy mode'
   1405 
   1406   'scp':
   1407     f: ->
   1408       thisrepo offCommands['scp']
   1409       return
   1410     h: 'git off scp [thisrepo] [host]\n  setup scp config\n  host has format host:path, user@host:path, user@host:port/path\n  Example: localhost:/tmp/offStore'
   1411 
   1412   'http':
   1413     f: ->
   1414       thisrepo offCommands['http']
   1415       return
   1416     h: 'git off http [thisrepo] [host]\n  setup http config\n  host has format http://host/path\n  Example: http://localhost/offStore'
   1417 
   1418   'curl':
   1419     f: ->
   1420       thisrepo offCommands['curl']
   1421       return
   1422     h: 'git off curl [thisrepo] [options]\n  setup curl config'
   1423 
   1424   'integrity':
   1425     f: ->
   1426       thisrepo offCommands['integrity']
   1427       return
   1428     h: 'git off integrity [thisrepo] [enable|disable]\n  set/show git off integrity.\n  when enabled, the SHA of the file received from the store is\n  checked again the SHA of the original file'
   1429 
   1430   'pem':
   1431     f: ->
   1432       thisrepo offCommands['pem']
   1433       return
   1434     h: "git off pem [thisrepo] [pathToPrivateKey]\n  set/show git off pem.\n  off.pem is the private key for ssh, rsync and scp\n  set 'offNoValue' to set an empty value (useful when there are multiple configs)"
   1435 
   1436   'sshoptions':
   1437     f: ->
   1438       thisrepo offCommands['sshoptions']
   1439       return
   1440     h: 'git off sshoptions [thisrepo] [options]\n  set/show git off sshoptions'
   1441 
   1442   'scpoptions':
   1443     f: ->
   1444       thisrepo offCommands['scpoptions']
   1445       return
   1446     h: 'git off scpoptions [thisrepo] [options]\n  set/show git off scpoptions'
   1447 
   1448   'rsyncoptions':
   1449     f: ->
   1450       thisrepo offCommands['rsyncoptions']
   1451       return
   1452     h: 'git off rsyncoptions [thisrepo] [options]\n  set/show git off rsyncoptions'
   1453 
   1454   'scpuser':
   1455     f: ->
   1456       thisrepo offCommands['scpUser']
   1457       return
   1458     h: 'git off scpuser [thisrepo] [username]\n  setup scp username config'
   1459 
   1460   'track':
   1461     f: ->
   1462       offCommands.track()
   1463       return
   1464     h: "git off track\n  setup gitattribute filters\n  example: git off track '*.bin'\n  without parameter, list git off attributes\n  calls git off install"
   1465 
   1466   'configAlways':
   1467     f: ->
   1468       offCommands.configAlways()
   1469       return
   1470     h: "git off configAlways [''|GIT_OFF_CONFIG|repo|global]\n  '' disable configAlways\n  GIT_OFF_CONFIG load all configurations from $GIT_OFF_CONFIG\n  repo load all configurations from current git repo\n  global load all configurations from global git config\n  set 'offNoValue' to set an empty value"
   1471 
   1472   's3region':
   1473     f: ->
   1474       thisrepo offCommands['s3region']
   1475       return
   1476     h: 'git off s3region [thisrepo] [region]\n  setup amazon s3 region for the bucket'
   1477 
   1478   's3bucket':
   1479     f: ->
   1480       thisrepo offCommands['s3bucket']
   1481       return
   1482     h: 'git off s3bucket [thisrepo] [bucket]\n  setup amazon s3 bucket'
   1483 
   1484   'transform':
   1485     f: ->
   1486       thisrepo offCommands['transform']
   1487       return
   1488     h: 'git off transform [thisrepo] [enable|disable]\n  enable transform in clean and smudge filters'
   1489 
   1490   'transformTo':
   1491     f: ->
   1492       thisrepo offCommands['transformTo']
   1493       return
   1494     h: "git off transformTo [thisrepo] ['cmd _1 _2']\n  setup transform command for clear filter\n  When the command is empty the regular transport is performed"
   1495 
   1496   'transformFrom':
   1497     f: ->
   1498       thisrepo offCommands['transformFrom']
   1499       return
   1500     h: "git off transformFrom [thisrepo] ['cmd _1 _2']\n  setup transform command for smudge filter\n  When the command is empty the regular transport is performed"
   1501 
   1502   'clean':
   1503     f: ->
   1504       offCommands.clean()
   1505       return
   1506     h: 'git off clean\n  internal filter\n  dont use directly'
   1507 
   1508   'pre-push':
   1509     f: ->
   1510       offCommands.prepush()
   1511       return
   1512     h: 'git off pre-push\n  internal filter\n  dont use directly'
   1513 
   1514   'smudge':
   1515     f: ->
   1516       offCommands.smudge()
   1517       return
   1518     h: 'git off smudge\n  internal filter\n  dont use directly'
   1519 
   1520   'copyTo':
   1521     f: ->
   1522       offCommands.copyTo()
   1523       return
   1524     h: 'git off copyTo [copy|rsync|scp|s3]\n  copy cache to store for specified mode'
   1525 
   1526   'push':
   1527     f: ->
   1528       offCommands.pushTo()
   1529       return
   1530     h: 'git off push\n  copy cache to store for selected mode'
   1531 
   1532   'clearAll':
   1533     f: ->
   1534       offCommands.clearAll()
   1535       return
   1536     h: 'git off clearAll\n  delete store, cache and log'
   1537 
   1538   'ca':
   1539     f: ->
   1540       offCommands.clearAll()
   1541       return
   1542     h: 'git off ca\n  delete store, cache and log'
   1543 
   1544   'clearCache':
   1545     f: ->
   1546       offCommands.clearCache()
   1547       return
   1548     h: 'git off clearCache\n  delete cache in current git'
   1549 
   1550   'cc':
   1551     f: ->
   1552       offCommands.clearCache()
   1553       return
   1554     h: 'git off cc\n  delete cache in current git'
   1555 
   1556   'clearStore':
   1557     f: ->
   1558       offCommands.clearStore()
   1559       return
   1560     h: 'git off clearStore\n  delete store'
   1561 
   1562   'cs':
   1563     f: ->
   1564       offCommands.clearStore()
   1565       return
   1566     h: 'git off ct\n  delete store'
   1567 
   1568   'clearTmp':
   1569     f: ->
   1570       offCommands.clearTmp()
   1571       return
   1572     h: 'git off clearTmp\n  delete tmp in git off cache\n  Useful when transform is enabled'
   1573 
   1574   'ct':
   1575     f: ->
   1576       offCommands.clearTmp()
   1577       return
   1578     h: 'git off cs\n  delete tmp in git off cache\n  Useful when transform is enabled'
   1579 
   1580   'defaults':
   1581     f: ->
   1582       offCommands.defaults()
   1583       return
   1584     h: 'git off defaults\n  shows first time config'
   1585 
   1586   'env':
   1587     f: ->
   1588       offCommands.env()
   1589       return
   1590     h: 'git off env\n  shows config'
   1591 
   1592   'help':
   1593     f: ->
   1594       offCommands.help()
   1595       return
   1596     h: 'git off help [cmd]\n  git off help. Run git off help command to get help for a specific command.'
   1597 
   1598 if process.argv[2] == undefined
   1599   COMMAND_MAP.help.f()
   1600 else
   1601   if COMMAND_MAP[process.argv[2]] == undefined
   1602     COMMAND_MAP['help'].f()
   1603   else
   1604     COMMAND_MAP[process.argv[2]].f()
   1605 
   1606 ###
   1607 # exports for unit tests
   1608 # export everything
   1609 ###
   1610 module.exports =
   1611   runtimeConfig: runtimeConfig
   1612   offDEFAULTS: offDEFAULTS
   1613   expandHome: expandHome
   1614   gitConfig: gitConfig
   1615   offLog: offLog
   1616   offLogRepo: offLogRepo
   1617   exec: exec
   1618   mkdirParents: mkdirParents
   1619   rmAll: rmAll
   1620   copy: copy
   1621   walkSync: walkSync
   1622   offHelpers: offHelpers
   1623   transport: transport
   1624   offCommands: offCommands
   1625   thisrepo: thisrepo
   1626   showCommandHelp: showCommandHelp
   1627   COMMAND_MAP: COMMAND_MAP