💾 Archived View for gmi.noulin.net › gitRepositories › git-off › file › src › gitoff.coffee.gmi captured on 2023-01-29 at 11:40:04. Gemini links have been rewritten to link to archived content

View Raw

More Information

-=-=-=-=-=-=-

git-off

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