git-off

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

commit e5108dc86261ca9c7902d7740c04c3648a4cb39a
parent b126e1dcbf5895404a894d63565d775583ef932d
Author: Remy Noulin (Spartatek) <remy.noulin@spartatek.se>
Date:   Tue, 22 Nov 2016 15:38:16 +0100

Fix npm package

and simplify ssh configuration

README.md         |   1 +
bin/git-off       |   1 -
gen.sh            |   6 +
package.json      |   4 +-
src/git-off       | 748 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/gitoff.coffee | 103 +++++---
6 files changed, 831 insertions(+), 32 deletions(-)

Diffstat:
MREADME.md | 1+
Dbin/git-off | 2--
Agen.sh | 6++++++
Mpackage.json | 4++--
Asrc/git-off | 748+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/gitoff.coffee | 103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
6 files changed, 831 insertions(+), 33 deletions(-)

diff --git a/README.md b/README.md @@ -106,5 +106,6 @@ git off help # Dependencies +- nodejs - git - ssh and scp for scp transport diff --git a/bin/git-off b/bin/git-off @@ -1 +0,0 @@ -../src/gitoff.coffee- \ No newline at end of file diff --git a/gen.sh b/gen.sh @@ -0,0 +1,6 @@ +#! /usr/bin/env bash + +NAME=git-off +echo "#! /usr/bin/env node" > src/$NAME +coffee --compile --print --bare --no-header src/gitoff.coffee >> src/$NAME +chmod 755 src/$NAME diff --git a/package.json b/package.json @@ -1,8 +1,8 @@ { "name": "git-off", - "version": "0.0.1", + "version": "0.0.2", "description": "large file handler for git", - "bin": "./bin/git-off", + "bin": "./src/git-off", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, diff --git a/src/git-off b/src/git-off @@ -0,0 +1,748 @@ +#! /usr/bin/env node +/* + * USAGE + * + * COMMANDS + * + * 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 + * + * git off smudge + * internal filter + * + * git off pre-push + * internal filter + * + * git off smudge + * internal filter + * + * git off clearAll + * git off ca + * delete store, cache and log + * + * git off clearCache + * git off cc + * deletes cache in current git + * + * 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 + */ + +/* + * CODE + * + * modules + * defaults + * helpers + * core + * main + * + * + * modules + * all dependencies + * + * defaults + * objInfo fields: for indexing cat-diff response in push command + * externalHelpers: shell commands to be used with the exec function + * runtimeConfig: config built by offHelpers + * offDEFAULTS: default configuration for first time install + * + * helpers + * expandHome: expands ~/ + * gitConfig: handles global git config + * offLog: appends log to git config off.log + * offLogRepo: log for commands run in git repo + * exec: runs an externalHelpers with parameters + * mkdirParents: recursive mkdir + * rmAll: delete recursively files and directories + * copy: copies files + * offHelpers: git-off helpers + * + * core + * transport: transport functions for git off store + * offCommands: command line functions + * + * main + * parse CLI arguments + */ + +/* + * modules + */ +var StringDecoder, copy, exec, expandHome, externalHelpers, fs, gitConfig, mkdirParents, mkdirp, offCommands, offDEFAULTS, offHelpers, offLog, offLogRepo, oiNAME, oiOID, oiPERMISSIONS, oiPREVIOUSOID, oiPREVIOUSPERMISSIONS, readline, rimraf, rmAll, runtimeConfig, syncexec, thisrepo, transport; + +require('colors'); + +fs = require('fs'); + +syncexec = require('sync-exec'); + +mkdirp = require('mkdirp'); + +rimraf = require('rimraf'); + +readline = require('readline'); + +StringDecoder = require('string_decoder').StringDecoder; + + +/* + * defaults + */ + +oiPREVIOUSPERMISSIONS = 0; + +oiPERMISSIONS = 1; + +oiPREVIOUSOID = 2; + +oiOID = 3; + +oiNAME = 4; + +externalHelpers = { + 'gitConfigGlobal': 'git config --global', + 'gitConfig': 'git config', + 'gitRepoRoot': 'git rev-parse --show-toplevel', + 'gitList': 'git rev-list', + '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 -p' +}; + +runtimeConfig = { + 'currentRepoRoot': '', + 'objectPath': '', + 'offStore': '', + 'offMode': '', + 'offScp': '', + 'offScpUser': '', + 'log': '' +}; + +offDEFAULTS = { + 'objectPath': '/.git/off/objects', + 'mode': 'copy', + 'store': '~/.git-off/offStore', + 'log': '~/.git-off/log', + '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 "$@"', + 'offSignature': '### git-off v1 sha:', + 'shaLength': 40 +}; + + +/* + * helpers + */ + +expandHome = function(path) { + if (path.slice(0, 2) === '~/') { + path = process.env.HOME + '/' + path.slice(2); + } + return path; +}; + +gitConfig = { + 'set': function(key, value) { + exec('gitConfig', ['--global', key, '"' + value + '"']); + }, + 'setLocal': function(key, value) { + exec('gitConfig', [key, '"' + value + '"']); + }, + 'setThisRepo': function(key, value) { + exec('gitConfig', ['--file ' + offHelpers.gitRepoRoot() + '/.git-off', key, '"' + value + '"']); + }, + 'get': function(key) { + if (fs.existsSync(offHelpers.gitRepoRoot(true) + '/.git-off') === false) { + return exec('gitConfig', [key]).stdout.trim(); + } else { + return exec('gitConfig', ['--file ' + offHelpers.gitRepoRoot() + '/.git-off', key]).stdout.trim(); + } + }, + 'getSyncexec': function(key) { + return syncexec(externalHelpers.gitConfig + ' ' + key); + } +}; + +offLog = function(s) { + var r; + if (runtimeConfig.log === '') { + r = gitConfig.getSyncexec('off.log'); + runtimeConfig.log = expandHome(r.stdout.trim()); + if (runtimeConfig.log === '') { + console.error('Missing off.log config. Run "git config --global off.log ~/.git-off/log".'.red.bold); + process.exit(1); + } + } + fs.appendFileSync(runtimeConfig.log, s + '\n'); +}; + +offLogRepo = function(s) { + offLog('REPO path: ' + offHelpers.gitRepoRoot() + ' ' + s); +}; + +exec = function(cmdName, paramsArray, ignoreStderr) { + var r; + if (paramsArray == null) { + paramsArray = []; + } + if (ignoreStderr == null) { + ignoreStderr = false; + } + r = syncexec(externalHelpers[cmdName] + ' ' + paramsArray.join(' ')); + if (r.stderr !== '' && ignoreStderr === false) { + console.error(r.stderr.red.bold); + offLog(r.stderr.red.bold); + process.exit(1); + } + return r; +}; + +mkdirParents = function(path) { + path = expandHome(path); + mkdirp.sync(path, function(err) { + console.error(err); + }); +}; + +rmAll = function(path) { + path = expandHome(path); + rimraf(path, function(err) {}); +}; + +copy = function(src, dst) { + src = expandHome(src); + dst = expandHome(dst); + fs.createReadStream(src).pipe(fs.createWriteStream(dst)); +}; + +offHelpers = { + 'gitRepoRoot': function(ignoreStderr) { + var r; + if (ignoreStderr == null) { + ignoreStderr = false; + } + if (runtimeConfig.currentRepoRoot === '') { + r = exec('gitRepoRoot', [], ignoreStderr); + runtimeConfig.currentRepoRoot = r.stdout.trim(); + } + return runtimeConfig.currentRepoRoot; + }, + 'objectPath': function() { + if (runtimeConfig.objectPath === '') { + runtimeConfig.objectPath = this.gitRepoRoot() + offDEFAULTS.objectPath; + } + return runtimeConfig.objectPath; + }, + 'offStore': function() { + var r; + if (runtimeConfig.offStore === '') { + r = gitConfig.get('off.store'); + runtimeConfig.offStore = expandHome(r); + } + return runtimeConfig.offStore; + }, + 'offMode': function() { + var r; + if (runtimeConfig.offMode === '') { + r = gitConfig.get('off.mode'); + runtimeConfig.offMode = expandHome(r); + } + return runtimeConfig.offMode; + }, + 'offScp': function() { + if (runtimeConfig.offScp === '') { + runtimeConfig.offScp = gitConfig.get('off.scphost'); + } + return runtimeConfig.offScp; + }, + 'offScpUser': function() { + var r; + if (runtimeConfig.offScpUser === '') { + r = gitConfig.get('off.scpuser'); + runtimeConfig.offScpUser = expandHome(r); + } + return runtimeConfig.offScpUser; + }, + 'log': function() { + var r; + if (runtimeConfig.log === '') { + r = gitConfig.getSyncexec('off.log'); + runtimeConfig.log = expandHome(r.stdout.trim()); + } + return runtimeConfig.log; + }, + 'getLog': function() { + offHelpers.log(); + if (runtimeConfig.log === '') { + console.error('Missing off.log config. Run "git config --global off.log ~/.git-off/log".'.red.bold); + process.exit(1); + } + return runtimeConfig.log; + }, + 'userAt': function() { + var user; + if (offHelpers.offScpUser() !== '') { + user = offHelpers.offScpUser() + '@'; + } else { + user = ''; + } + return user; + }, + 'getSSHConfig': function() { + var h_l, host, port, portAndPath_l, storePath, user; + user = offHelpers.userAt(); + h_l = offHelpers.offScp().split(':'); + portAndPath_l = h_l[1].split('/'); + port = parseInt(portAndPath_l[0]); + if (port === NaN) { + host = h_l[0]; + storePath = h_l[1]; + } else { + host = h_l[0]; + storePath = '/' + portAndPath_l.slice(1).join('/'); + } + return [user + host, storePath, port]; + }, + 'mkdirStore': function(path) { + var h_l, sshCmd; + h_l = offHelpers.getSSHConfig(); + sshCmd = '"mkdir -p ' + h_l[1] + '/' + path; + sshCmd += '"'; + if (h_l[2] === NaN) { + exec('ssh', [h_l[0], sshCmd], true); + } else { + exec('ssh', ['-p ' + h_l[2], h_l[0], sshCmd], true); + } + }, + 'rmAllStore': function(path) { + var h_l, sshCmd; + h_l = offHelpers.getSSHConfig(); + sshCmd = '"rm -rf ' + h_l[1] + '/' + path; + sshCmd += '"'; + if (h_l[2] === NaN) { + exec('ssh', [h_l[0], sshCmd], true); + } else { + exec('ssh', ['-p ' + h_l[2], h_l[0], sshCmd], true); + } + }, + 'setTransport': function() { + if (offHelpers.offMode() === 'copy') { + transport['send'] = function(file) { + var f_l; + f_l = file.split('/'); + if (fs.existsSync(offHelpers.offStore() + '/' + f_l[0] + '/' + f_l[1]) === false) { + mkdirParents(offHelpers.offStore() + '/' + f_l[0] + '/' + f_l[1]); + } + copy(offHelpers.objectPath() + '/' + file, offHelpers.offStore() + '/' + file); + fs.chmodSync(offHelpers.offStore() + '/' + file, '444'); + }; + transport['receive'] = function(file) { + var readStream; + readStream = fs.createReadStream(offHelpers.offStore() + '/' + file); + readStream.pipe(process.stdout); + }; + } else if (offHelpers.offMode() === 'scp') { + transport['send'] = function(file) { + var f_l, h_l, user; + user = offHelpers.userAt(); + f_l = file.split('/'); + offHelpers.mkdirStore(f_l[0] + '/' + f_l[1]); + h_l = offHelpers.getSSHConfig(); + if (h_l[2] === NaN) { + exec('scp', [offHelpers.objectPath() + '/' + file, user + offHelpers.offScp() + '/' + file]); + } else { + exec('scp', ['-P ' + h_l[2], offHelpers.objectPath() + '/' + file, h_l[0] + ':' + h_l[1] + '/' + file]); + } + }; + transport['receive'] = function(file) { + var f_l, h_l, readStream, user; + f_l = file.split('/'); + if (fs.existsSync(offHelpers.objectPath() + '/' + f_l[0] + '/' + f_l[1]) === false) { + mkdirParents(offHelpers.objectPath() + '/' + f_l[0] + '/' + f_l[1]); + } + user = offHelpers.userAt(); + h_l = offHelpers.getSSHConfig(); + if (h_l[2] === NaN) { + exec('scp', [user + offHelpers.offScp() + '/' + file, offHelpers.objectPath() + '/' + file]); + } else { + exec('scp', ['-P ' + h_l[2], h_l[0] + ':' + h_l[1] + '/' + file, offHelpers.objectPath() + '/' + file]); + } + readStream = fs.createReadStream(offHelpers.objectPath() + '/' + file); + readStream.pipe(process.stdout); + }; + } + }, + 'getOffFilePath': function(offFile) { + return [offFile.slice(0, 2) + '/' + offFile.slice(2, 4) + '/' + offFile, offFile.slice(0, 2) + '/' + offFile.slice(2, 4)]; + } +}; + + +/* + * core + */ + +transport = { + 'send': function(src) {}, + 'receive': function(src) {} +}; + +offCommands = { + 'localSetup': function() { + var hook; + mkdirParents(offHelpers.objectPath()); + hook = offHelpers.gitRepoRoot() + '/.git/hooks/pre-push'; + if (fs.existsSync(hook) === false) { + fs.writeFileSync(hook, offDEFAULTS.prePush); + fs.chmodSync(hook, '755'); + } + }, + 'install': function(setCfg) { + if (setCfg == null) { + setCfg = gitConfig.set; + } + offCommands.localSetup(); + if (gitConfig.get('filter.off.clean') === '' || setCfg !== gitConfig.set) { + if (setCfg === gitConfig.setThisRepo) { + gitConfig.setLocal('filter.off.clean', 'git off clean %f'); + } else { + setCfg('filter.off.clean', 'git off clean %f'); + } + } + if (gitConfig.get('filter.off.smudge') === '' || setCfg !== gitConfig.set) { + if (setCfg === gitConfig.setThisRepo) { + gitConfig.setLocal('filter.off.smudge', 'git off smudge %f'); + } else { + setCfg('filter.off.smudge', 'git off smudge %f'); + } + } + if (offHelpers.log() === '' || setCfg !== gitConfig.set) { + if (setCfg === gitConfig.setThisRepo) { + gitConfig.setLocal('off.log', offDEFAULTS.log); + } else { + setCfg('off.log', offDEFAULTS.log); + } + } + if (offHelpers.offMode() === '' || setCfg !== gitConfig.set) { + setCfg('off.mode', offDEFAULTS.mode); + } + if (offHelpers.offStore() === '' || setCfg !== gitConfig.set) { + setCfg('off.store', offDEFAULTS.store); + } + if (runtimeConfig.offMode === 'copy') { + mkdirParents(runtimeConfig.offStore); + } + if (runtimeConfig.offMode === 'scp') { + offHelpers.mkdirStore(''); + } + }, + 'mode': function(setCfg) { + var len, mode; + len = process.argv.length; + mode = process.argv[len - 1]; + if (mode !== 'mode' && mode !== 'thisrepo') { + setCfg('off.mode', mode); + } else { + console.log('off.mode '.blue.bold + offHelpers.offMode()); + } + }, + 'scp': function(setCfg) { + var len, scpHost; + len = process.argv.length; + scpHost = process.argv[len - 1]; + if (scpHost !== 'scp' && scpHost !== 'thisrepo') { + setCfg('off.scphost', scpHost); + } else { + console.log('off.scp '.blue.bold + offHelpers.offScp()); + } + }, + 'scpUser': function(setCfg) { + var len, scpUser; + len = process.argv.length; + scpUser = process.argv[len - 1]; + if (scpUser !== 'scpuser' && scpUser !== 'thisrepo') { + setCfg('off.scpuser', scpUser); + } else { + console.log('off.scpuser '.blue.bold + offHelpers.offScpUser()); + } + }, + 'track': function() { + var i, l, len1, r, ref; + if (process.argv[3] !== void 0) { + offCommands.install(); + fs.appendFileSync(offHelpers.gitRepoRoot() + '/.gitattributes', process.argv[3] + ' filter=off -text\n'); + } else { + r = exec('listAttr', ['`cd ' + offHelpers.gitRepoRoot() + '; git ls-files`']); + ref = r.stdout.split('\n'); + for (i = 0, len1 = ref.length; i < len1; i++) { + l = ref[i]; + if (l.indexOf(': filter: off') !== -1) { + console.log(l); + } + } + } + }, + 'clean': function() { + var file, offFile, offFilePath, r, size, writeStream; + offCommands.localSetup(); + file = process.argv[3]; + size = fs.statSync(file).size; + r = exec('sha', [file]); + offFile = r.stdout.split(' ')[0].trim(); + offFilePath = offHelpers.getOffFilePath(offFile); + if (fs.existsSync(offHelpers.objectPath() + '/' + offFilePath[1]) === false) { + mkdirParents(offHelpers.objectPath() + '/' + offFilePath[1]); + } + offFilePath = offFilePath[0]; + if (fs.existsSync(offHelpers.objectPath() + '/' + offFilePath) === false) { + writeStream = fs.createWriteStream(offHelpers.objectPath() + '/' + offFilePath); + process.stdin.pipe(writeStream); + fs.chmodSync(offHelpers.objectPath() + '/' + offFilePath, '444'); + } else { + writeStream = fs.createWriteStream('/dev/null'); + process.stdin.pipe(writeStream); + } + console.log(offDEFAULTS.offSignature + offFile + ' size:' + size); + }, + 'prepush': function() { + var rl; + offHelpers.setTransport(); + rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + terminal: false + }); + rl.on('line', function(line) { + offCommands.push(line); + }); + }, + 'push': function(line) { + var commitListToPush, i, j, len, len1, len2, line_l, localRef, localSha, objInfo, objInfoList, offFile, offFilePath, offRef, oi, r, remoteName, remoteRef, remoteSha, sha, url; + len = process.argv.length; + remoteName = process.argv[len - 2]; + url = process.argv[len - 1]; + line_l = line.split(' '); + localRef = line_l[0]; + localSha = line_l[1]; + remoteRef = line_l[2]; + remoteSha = line_l[3]; + r = exec('gitList', [localSha, '^' + remoteSha]); + commitListToPush = r.stdout.split('\n'); + commitListToPush = commitListToPush.slice(0, commitListToPush.length - 1); + for (i = 0, len1 = commitListToPush.length; i < len1; i++) { + sha = commitListToPush[i]; + r = exec('gitDiff', [sha]); + objInfoList = r.stdout.split('\n'); + objInfoList = objInfoList.slice(1, objInfoList.length - 1); + for (j = 0, len2 = objInfoList.length; j < len2; j++) { + oi = objInfoList[j]; + objInfo = oi.split(' '); + if (objInfo[oiNAME].slice(0, 1) !== 'D') { + r = exec('gitCat', [objInfo[oiOID]]); + offRef = r.stdout.split('\n')[0]; + if (offRef.indexOf(offDEFAULTS.offSignature) !== -1) { + offFile = offRef.split(':')[1].split(' ')[0]; + offFilePath = offHelpers.getOffFilePath(offFile)[0]; + transport.send(offFilePath); + } + } + } + } + }, + 'smudge': function() { + var status; + offHelpers.setTransport(); + offCommands.localSetup(); + status = 'header'; + process.stdin.on('readable', function() { + var data, decoder, header, offFile, offFilePath, readStream; + if (status === 'header') { + status = 'stream'; + data = process.stdin.read(offDEFAULTS.offSignature.length + offDEFAULTS.shaLength); + decoder = new StringDecoder('utf8'); + if (data === null) { + offLogRepo('smudge error: cant get data from stdin.'); + process.exit(1); + } + header = decoder.write(data); + if (header.slice(0, offDEFAULTS.offSignature.length) === offDEFAULTS.offSignature) { + offFile = header.split(':')[1]; + offFilePath = offHelpers.getOffFilePath(offFile)[0]; + if (fs.existsSync(offHelpers.objectPath() + '/' + offFilePath) === true) { + readStream = fs.createReadStream(offHelpers.objectPath() + '/' + offFilePath); + readStream.pipe(process.stdout); + } else { + transport.receive(offFilePath); + } + } else { + process.stdout.write(data); + process.stdin.pipe(process.stdout); + } + } + }); + }, + 'clearAll': function() { + if (offHelpers.offMode() === 'copy') { + rmAll(offHelpers.offStore()); + } + if (offHelpers.offMode() === 'scp') { + offHelpers.rmAllStore(''); + } + rmAll(offHelpers.objectPath()); + rmAll(offHelpers.getLog()); + }, + 'clearCache': function() { + rmAll(offHelpers.objectPath()); + }, + 'defaults': function() { + var i, k, len1, ref; + ref = Object.keys(offDEFAULTS); + for (i = 0, len1 = ref.length; i < len1; i++) { + k = ref[i]; + console.log(k.blue.bold + ' ' + offDEFAULTS[k]); + } + }, + 'env': function() { + 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()); + } +}; + + +/* + * main + */ + +thisrepo = function(cmd) { + if (process.argv[3] !== 'thisrepo') { + cmd(gitConfig['set']); + } else { + cmd(gitConfig['setThisRepo']); + } +}; + +if (process.argv[2] === '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(); +} + +if (process.argv[2] === 'clean') { + offCommands.clean(); +} + +if (process.argv[2] === 'pre-push') { + offCommands.prepush(); +} + +if (process.argv[2] === 'smudge') { + offCommands.smudge(); +} + +if ((process.argv[2] === 'clearAll') || (process.argv[2] === 'ca')) { + offCommands.clearAll(); +} + +if ((process.argv[2] === 'clearCache') || (process.argv[2] === 'cc')) { + offCommands.clearCache(); +} + +if (process.argv[2] === 'defaults') { + offCommands.defaults(); +} + +if (process.argv[2] === 'env') { + offCommands.env(); +} + +if (process.argv[2] === 'help' || process.argv[2] === void 0) { + console.log('git-off help\n'.green.bold); + if (process.argv[3] === void 0) { + 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 off caches bin files'.green); + console.log('git commit'); + console.log(' git off caches new bin files'.green); + console.log('git push'); + console.log(' git off sends bin files to store'.green); + console.log('git checkout master'); + console.log(' checkout commit with bin files'.green); + console.log(' git off downloads bin files from store'.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, port and path'.green); + console.log(' user@localhost:port/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'); + } +} diff --git a/src/gitoff.coffee b/src/gitoff.coffee @@ -146,7 +146,7 @@ externalHelpers = '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' + 'scp': 'scp -C -o StrictHostKeyChecking=no -o ConnectTimeout=3 -p' # config built by offHelpers # use offHelpers to access runtimeConfig @@ -275,6 +275,9 @@ copy = (src, dst) -> # log: sets and returns runtimeConfig.log # getLog: sets and returns runtimeConfig.log with error checking # userAt: returns user@ or '' +# getSSHConfig extracts host, port and path from off.sshhost +# mkdirStore mkdir in remote store +# rmAllStore rm in remote store # setTransport: set send and receive functions for transport # getOffFilePath: creates path for offFile offHelpers = @@ -303,8 +306,7 @@ offHelpers = 'offScp': -> if runtimeConfig.offScp == '' - r = gitConfig.get 'off.scphost' - runtimeConfig.offScp = expandHome r + runtimeConfig.offScp = gitConfig.get 'off.scphost' runtimeConfig.offScp 'offScpUser': -> @@ -335,9 +337,59 @@ offHelpers = user = '' user + 'getSSHConfig': -> + # extract host, port and path from off.sshhost + + user = offHelpers.userAt() + + h_l = offHelpers.offScp().split(':') + portAndPath_l = h_l[1].split('/') + port = parseInt portAndPath_l[0] + if port == NaN + # use default port + host = h_l[0] + storePath = h_l[1] + else + host = h_l[0] + storePath = '/' + portAndPath_l.slice(1).join('/') + [user + host, storePath, port] + + + 'mkdirStore': (path) -> + # mkdir in remote store + + # TODO handle multiple transports + h_l = offHelpers.getSSHConfig() + # bug in coffeescript - cant have '"mkdir '+ h_l[1] +'"' + sshCmd = '"mkdir -p '+ h_l[1] + '/' + path + sshCmd += '"' + + # ignore error from mkdir for already existing store + if h_l[2] == NaN + exec 'ssh', [h_l[0], sshCmd], true + else + exec 'ssh', ['-p ' + h_l[2], h_l[0], sshCmd], true + return + + 'rmAllStore': (path) -> + # rm in remote store + + # scphost format is host:path + h_l = offHelpers.getSSHConfig() + # bug in coffeescript - cant have '"mkdir '+ h_l[1] +'"' + sshCmd = '"rm -rf '+ h_l[1] + '/' + path + sshCmd += '"' + # ignore error from rm + if h_l[2] == NaN + exec 'ssh', [h_l[0], sshCmd], true + else + exec 'ssh', ['-p ' + h_l[2], h_l[0], sshCmd], true + return + 'setTransport': -> # set send and receive functions for transport # copy, scp + if offHelpers.offMode() == 'copy' transport['send'] = (file) -> # create file directories in store @@ -358,16 +410,15 @@ offHelpers = user = offHelpers.userAt() # create file directories in store - h_l = offHelpers.offScp().split(':') f_l = file.split '/' - # bug in coffeescript - cant have '"mkdir '+ h_l[1] +'"' - sshCmd = '"mkdir -p '+ h_l[1] + '/' + f_l[0] + '/' + f_l[1] - sshCmd += '"' - # ignore error from mkdir for already existing store - exec 'ssh', [user + h_l[0], sshCmd], true - + offHelpers.mkdirStore f_l[0] + '/' + f_l[1] - exec 'scp', [offHelpers.objectPath() + '/' + file, user + offHelpers.offScp() + '/' + file] + #TODO remove user + h_l = offHelpers.getSSHConfig() + if h_l[2] == NaN + exec 'scp', [offHelpers.objectPath() + '/' + file, user + offHelpers.offScp() + '/' + file] + else + exec 'scp', ['-P ' + h_l[2], offHelpers.objectPath() + '/' + file, h_l[0] + ':' + h_l[1] + '/' + file] return transport['receive'] = (file) -> # create file directories in cache @@ -376,7 +427,14 @@ offHelpers = mkdirParents offHelpers.objectPath() + '/' + f_l[0] + '/' + f_l[1] user = offHelpers.userAt() - exec 'scp', [user + offHelpers.offScp() + '/' + file, offHelpers.objectPath() + '/' + file] + + #TODO remove user + h_l = offHelpers.getSSHConfig() + if h_l[2] == NaN + exec 'scp', [user + offHelpers.offScp() + '/' + file, offHelpers.objectPath() + '/' + file] + else + exec 'scp', ['-P ' + h_l[2], h_l[0] + ':' + h_l[1] + '/' + file, offHelpers.objectPath() + '/' + file] + readStream = fs.createReadStream(offHelpers.objectPath() + '/' + file) readStream.pipe(process.stdout) return @@ -467,14 +525,7 @@ offCommands = 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 + offHelpers.mkdirStore '' return 'mode': (setCfg) -> @@ -695,14 +746,7 @@ offCommands = 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 + offHelpers.rmAllStore '' rmAll offHelpers.objectPath() rmAll offHelpers.getLog() return @@ -802,7 +846,8 @@ if process.argv[2] == 'help' or process.argv[2] == undefined 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 ' setup host, port and path'.green + console.log ' user@localhost:port/path'.green console.log 'git off scpuser username' console.log ' setup scp username config'.green console.log 'git off cc'