git-off

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

commit fe414d996ec90cda4b4c6d7365c0c7d0b068fbce
parent 77a3fc05b7c4cef9f548cdf4b2132710b8472374
Author: Remy Noulin (Spartatek) <remy.noulin@spartatek.se>
Date:   Sun, 20 Nov 2016 21:28:13 +0100

add scp transport

add local config option thisrepo
add mode scp
add partial help
add ignoreStderr to exec function

src/gitoff.coffee | 324 +++++++++++++++++++++++++++++++++++++++++++++++-------
1 file changed, 285 insertions(+), 39 deletions(-)

Diffstat:
Msrc/gitoff.coffee | 324+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
1 file changed, 285 insertions(+), 39 deletions(-)

diff --git a/src/gitoff.coffee b/src/gitoff.coffee @@ -1,10 +1,12 @@ #! /usr/bin/env coffee # TODO -# detect when not setup -# default setup global -# option to setup locally -# create git off modes +# add help +# add rsync backend - change push and smudge +# add pem config +# handle wrong config like getLog +# add multiple config like our git fat +# add a command to copy stores # # parse params # use logger @@ -15,12 +17,26 @@ # # COMMANDS # -# git off install +# git off install [thisrepo] # setup git config (default global) +# thisrepo sets up config in current repo +# +# git off mode [thisrepo] [copy|scp] +# set/show git off mode +# +# git off scp [thisrepo] [host] +# setup scp config +# host has format host:path +# Example: localhost:/tmp/offStore +# +# git off scpuser [thisrepo] [username] +# setup scp username config # # git off track # setup gitattribute filters # example: git off track '*.bin' +# without parameter, list git off attributes +# calls git off install # # git off clean # internal filter @@ -45,6 +61,12 @@ # git off defaults # shows first time config # +# git off env +# shows config +# +# git off help +# shows git off help +# # config: # gitoff mode rsync, http, copy # store=/we @@ -80,7 +102,8 @@ # offHelpers: git-off helpers # # core -# globalConfig: handles global git config +# transport: transport functions for git off store +# gitConfig: handles global git config # offCommands: command line functions # # main @@ -121,6 +144,9 @@ externalHelpers = 'gitDiff': 'git diff-tree -r' 'gitCat': 'git cat-file -p' 'sha': 'git hash-object --no-filters' + 'listAttr': 'git check-attr -a' + 'ssh': 'ssh -C -o StrictHostKeyChecking=no -o ConnectTimeout=3' + 'scp': 'scp -C -o StrictHostKeyChecking=no -o ConnectTimeout=3' # config built by offHelpers # use offHelpers to access runtimeConfig @@ -128,9 +154,13 @@ runtimeConfig = 'currentRepoRoot': '' 'objectPath': '' 'offStore': '' + 'offMode': '' + 'offScp': '' + 'offScpUser': '' 'log': '' # default configuration for first time install +# objectPath is git off cache in repo offDEFAULTS = 'objectPath': '/.git/off/objects' 'mode': 'copy' @@ -154,7 +184,8 @@ expandHome = (path) -> offLog = (s) -> if runtimeConfig.log == '' # set runtimeConfig.log and get config in git - r = syncexec externalHelpers.gitConfig + ' off.log' + # use syncexec to avoid circular calls + r = syncexec externalHelpers.gitConfig + ' off.log' runtimeConfig.log = expandHome r.stdout.trim() # error handling if runtimeConfig.log == '' @@ -176,13 +207,14 @@ offLogRepo = (s) -> # paramsArray is an array of strings # errors are logged in off.log and in console # returns r (r.stdout, r.stderr) -exec = (cmdName, paramsArray=[]) -> - #console.log externalHelpers[cmdName] + ' ' + paramsArray.join(' ') +exec = (cmdName, paramsArray=[], ignoreStderr=false) -> + console.log externalHelpers[cmdName] + ' ' + paramsArray.join(' ') #offLog externalHelpers[cmdName] + ' ' + paramsArray.join(' ') r = syncexec externalHelpers[cmdName] + ' ' + paramsArray.join(' ') + #console.log r.stdout # offLog r # offLog r.stdout - if r.stderr != '' + if r.stderr != '' and ignoreStderr == false console.error r.stderr.red.bold offLog r.stderr.red.bold process.exit(1) @@ -211,9 +243,16 @@ copy = (src, dst) -> return # git-off helpers -# gitRepoRoot: sets and returns runtimeConfig.currentRepoRoot -# objectPath: sets and returns runtimeConfig.objectPath -# getLog: sets and returns runtimeConfig.log +# gitRepoRoot: sets and returns runtimeConfig.currentRepoRoot +# objectPath: sets and returns runtimeConfig.objectPath +# offStore: sets and returns runtimeConfig.offStore +# offMode: sets and returns runtimeConfig.offMode +# offScp: sets and returns runtimeConfig.offScp +# offScpUser: sets and returns runtimeConfig.offScpUser +# log: sets and returns runtimeConfig.log +# getLog: sets and returns runtimeConfig.log with error checking +# userAt: returns user@ or '' +# setTransport: set send and receive functions for transport offHelpers = 'gitRepoRoot': -> if runtimeConfig.currentRepoRoot == '' @@ -232,17 +271,69 @@ offHelpers = runtimeConfig.offStore = expandHome r.stdout.trim() runtimeConfig.offStore - 'getLog': -> + 'offMode': -> + if runtimeConfig.offMode == '' + r = exec 'gitConfig', ['off.mode'] + runtimeConfig.offMode = expandHome r.stdout.trim() + runtimeConfig.offMode + + 'offScp': -> + if runtimeConfig.offScp == '' + r = exec 'gitConfig', ['off.scphost'] + runtimeConfig.offScp = expandHome r.stdout.trim() + runtimeConfig.offScp + + 'offScpUser': -> + if runtimeConfig.offScpUser == '' + r = exec 'gitConfig', ['off.scpuser'] + runtimeConfig.offScpUser = expandHome r.stdout.trim() + runtimeConfig.offScpUser + + 'log': -> if runtimeConfig.log == '' - # get config in git + # use syncexec to avoid circular calls r = syncexec externalHelpers.gitConfig + ' off.log' runtimeConfig.log = expandHome r.stdout.trim() - # error handling - if runtimeConfig.log == '' - console.error 'Missing off.log config. Run "git config --global off.log ~/.git-off/log".'.red.bold - process.exit(1) runtimeConfig.log + 'getLog': -> + offHelpers.log() + # error handling + if runtimeConfig.log == '' + console.error 'Missing off.log config. Run "git config --global off.log ~/.git-off/log".'.red.bold + process.exit(1) + runtimeConfig.log + + 'userAt': -> + if offHelpers.offScpUser() != '' + user = offHelpers.offScpUser() + '@' + else + user = '' + user + + 'setTransport': -> + if offHelpers.offMode() == 'copy' + transport['send'] = (file) -> + copy offHelpers.objectPath() + '/' + file, offHelpers.offStore() + '/' + file + fs.chmodSync offHelpers.offStore() + '/' + file, '444' + return + transport['receive'] = (file) -> + readStream = fs.createReadStream(offHelpers.offStore() + '/' + file) + readStream.pipe(process.stdout) + return + else if offHelpers.offMode() == 'scp' + transport['send'] = (file) -> + user = offHelpers.userAt() + exec 'scp', [offHelpers.objectPath() + '/' + file, user + offHelpers.offScp() + '/' + file] + return + transport['receive'] = (file) -> + user = offHelpers.userAt() + exec 'scp', [user + offHelpers.offScp() + '/' + file, offHelpers.objectPath() + '/' + file] + readStream = fs.createReadStream(offHelpers.objectPath() + '/' + file) + readStream.pipe(process.stdout) + return + return + # list offHelpers, run 'git off' #console.log Object.keys(offHelpers) @@ -250,15 +341,34 @@ offHelpers = # core ### +transport = + 'send': (src) -> + # to be initialized by setTransport + return + 'receive': (src) -> + # send data to stdout + # to be initialized by setTransport + return + # handles global git config -globalConfig = +gitConfig = 'set': (key, value)-> exec 'gitConfigGlobal', [key, '"' + value + '"'] return + 'setLocal': (key, value) -> + exec('gitConfig', [key, '"' + value + '"']) + return + + 'get': (key) -> + exec('gitConfig', [key]).stdout.trim() + # command line functions # localSetup: setup current git # install: setup git config (default global) +# mode: set/show git off mode +# scp: setup scp config +# scpuser: setup scp username config # track: setup gitattribute filters # clean: replace files handled by git off with reference # prepush: read stdin and calls push @@ -267,6 +377,7 @@ globalConfig = # clearAll: delete store, cache in current git and log # clearCache: delete cache in current git # defaults: show first time config (offDEFAULTS) +# env: show config offCommands = 'localSetup': -> # setup current git @@ -284,28 +395,82 @@ offCommands = # process.exit(1) return - 'install': (setCfg = globalConfig.set) -> + 'install': (setCfg = gitConfig.set) -> # setup git config # create off.store + offCommands.localSetup() - setCfg('filter.off.clean','git off clean %f') - setCfg('filter.off.smudge','git off smudge %f') - setCfg('off.mode', offDEFAULTS.mode) - setCfg('off.store',offDEFAULTS.store) - setCfg('off.log',offDEFAULTS.log) - if offDEFAULTS.mode == 'copy' - mkdirParents offDEFAULTS.store + + # setup config only if not already set + if gitConfig.get('filter.off.clean') == '' or setCfg != gitConfig.set + setCfg('filter.off.clean','git off clean %f') + if gitConfig.get('filter.off.smudge') == '' or setCfg != gitConfig.set + setCfg('filter.off.smudge','git off smudge %f') + if offHelpers.log() == '' or setCfg != gitConfig.set + setCfg('off.log',offDEFAULTS.log) + + if offHelpers.offMode() == '' or setCfg != gitConfig.set + setCfg('off.mode', offDEFAULTS.mode) + if offHelpers.offStore() == '' or setCfg != gitConfig.set + setCfg('off.store',offDEFAULTS.store) + + # create off.store + + if runtimeConfig.offMode == 'copy' + mkdirParents runtimeConfig.offStore + + if runtimeConfig.offMode == 'scp' + user = offHelpers.userAt() + # scphost format is host:path + h_l = offHelpers.offScp().split(':') + # bug in coffeescript - cant have '"mkdir '+ h_l[1] +'"' + sshCmd = '"mkdir '+ h_l[1] + sshCmd += '"' + # ignore error from mkdir for already existing store + exec 'ssh', [user + h_l[0], sshCmd], true + return + + 'mode': (setCfg) -> + len = process.argv.length + mode = process.argv[len-1] + if mode != 'mode' and mode != 'thisrepo' + setCfg('off.mode', mode) + else + console.log 'off.mode '.blue.bold + offHelpers.offMode() + return + + 'scp': (setCfg) -> + len = process.argv.length + scpHost = process.argv[len-1] + if scpHost != 'scp' and scpHost != 'thisrepo' + setCfg('off.scphost', scpHost) + else + console.log 'off.scp '.blue.bold + offHelpers.offScp() + return + + 'scpUser': (setCfg) -> + len = process.argv.length + scpUser = process.argv[len-1] + if scpUser != 'scpuser' and scpUser != 'thisrepo' + setCfg('off.scpuser', scpUser) + else + console.log 'off.scpuser '.blue.bold + offHelpers.offScpUser() return 'track': -> # setup gitattribute filters in current folder + # list current git off attributes when there is no parameter if process.argv[3] != undefined offCommands.install() - #TODO git command to add stuff in attributes - fs.writeFileSync(offHelpers.gitRepoRoot() + '/.gitattributes', process.argv[3] + ' filter=off -text') + fs.appendFileSync(offHelpers.gitRepoRoot() + '/.gitattributes', process.argv[3] + ' filter=off -text\n') else - #TODO - console.log 'TODO: list current attributes'.red + # list current git off attributes + + r = exec 'listAttr', ['`cd ' + offHelpers.gitRepoRoot() + '; git ls-files`'] + # gff/b.bin: filter: off + for l in r.stdout.split('\n') + if l.indexOf(': filter: off') != -1 + console.log l return 'clean': -> @@ -349,6 +514,8 @@ offCommands = # read stdin and calls push # stdin (data from git) is a line with remoteName url localRef localSha remoteRef remoteSha + offHelpers.setTransport() + rl = readline.createInterface( input: process.stdin output: process.stdout @@ -414,8 +581,8 @@ offCommands = # ### git-off v1 sha:be3e02b60effe3eab232d5590a6a2e2c2c2f443b size:119 b.bin offFile = offRef.split(':')[1].split(' ')[0] - copy offHelpers.objectPath() + '/' + offFile, offHelpers.offStore() + '/' + offFile - fs.chmodSync offHelpers.offStore() + '/' + offFile, '444' + + transport.send offFile # test prevent push by issuing an error #process.exit(1) @@ -428,6 +595,8 @@ offCommands = # load header # for git off object, replace git off reference with data from cache or off.store # for normal object, copy data from repo to stdout + + offHelpers.setTransport() offCommands.localSetup() #not used file = process.argv[3] @@ -450,10 +619,15 @@ offCommands = if header.slice(0,offDEFAULTS.offSignature.length) == offDEFAULTS.offSignature # for git off object, replace git off reference with data from cache or off.store - # TODO detect if file is already in cache offFile = header.split(':')[1] - readStream = fs.createReadStream(offHelpers.offStore() + '/' + offFile) - readStream.pipe(process.stdout) + # detect if file is already in cache + if fs.existsSync(offHelpers.objectPath() + '/' + offFile) == true + # copy from cache + readStream = fs.createReadStream(offHelpers.objectPath() + '/' + offFile) + readStream.pipe(process.stdout) + else + # copy from off.store + transport.receive offFile else # for normal object, copy data from repo to stdout @@ -464,7 +638,17 @@ offCommands = 'clearAll': -> # delete store, cache in current git and log - rmAll offHelpers.offStore() + if offHelpers.offMode() == 'copy' + rmAll offHelpers.offStore() + if offHelpers.offMode() == 'scp' + user = offHelpers.userAt() + # scphost format is host:path + h_l = offHelpers.offScp().split(':') + # bug in coffeescript - cant have '"mkdir '+ h_l[1] +'"' + sshCmd = '"rm -rf '+ h_l[1] + sshCmd += '"' + # ignore error from rm + exec 'ssh', [user + h_l[0], sshCmd], true rmAll offHelpers.objectPath() rmAll offHelpers.getLog() return @@ -480,6 +664,14 @@ offCommands = console.log k.blue.bold + ' ' + offDEFAULTS[k] return + 'env': -> + console.log 'off.mode '.blue.bold + offHelpers.offMode() + console.log 'off.store '.blue.bold + offHelpers.offStore() + console.log 'off.scphost '.blue.bold + offHelpers.offScp() + console.log 'off.scpuser '.blue.bold + offHelpers.offScpUser() + console.log 'off.log '.blue.bold + offHelpers.getLog() + return + # list offCommands, run 'git off' #console.log Object.keys(offCommands) @@ -489,8 +681,25 @@ offCommands = # parse CLI arguments +thisrepo = (cmd) -> + if process.argv[3] != 'thisrepo' + cmd(gitConfig['set']) + else + # setup config in current repo + cmd(gitConfig['setLocal']) + return + if process.argv[2] == 'install' - offCommands.install() + thisrepo offCommands['install'] + +if process.argv[2] == 'mode' + thisrepo offCommands['mode'] + +if process.argv[2] == 'scp' + thisrepo offCommands['scp'] + +if process.argv[2] == 'scpuser' + thisrepo offCommands['scpUser'] if process.argv[2] == 'track' offCommands.track() @@ -512,3 +721,40 @@ if (process.argv[2] == 'clearCache') || (process.argv[2] == 'cc') if process.argv[2] == 'defaults' offCommands.defaults() + +if process.argv[2] == 'env' + offCommands.env() + +if process.argv[2] == 'help' or process.argv[2] == undefined + console.log 'git-off help\n'.green.bold + if process.argv[3] == undefined + console.log '# USAGE'.green.bold + console.log 'Setup:' + console.log "git off track '*.bin'" + console.log " git off now handles files with bin extensions".green + console.log 'git add .' + console.log ' use git normally'.green + console.log 'git commit' + console.log 'git push' + console.log 'git checkout master' + console.log ' checkout commit with bin files'.green + console.log '\n# Other\n'.green.bold + console.log 'git off install' + console.log ' setup git config (default global)'.green + console.log 'git off mode scp' + console.log ' store object in a remote sshhost, setup host and path with git off scp'.green + console.log 'git off scp localhost:/tmp/offStore' + console.log ' setup host and path'.green + console.log 'git off scpuser username' + console.log ' setup scp username config'.green + console.log 'git off cc' + console.log ' clear cache in current repo'.green + console.log 'git off ca' + console.log ' clear store and cache'.green + console.log 'git off env' + console.log ' shows current config'.green + console.log 'git off defaults' + console.log ' shows first time config'.green + else + console.log 'Command help TODO' + # TODO option: thisrepo for local config