├── .npmrc ├── .gitignore ├── index.js ├── web.js ├── bin ├── usage.txt └── cli.js ├── src ├── commands │ ├── create.js │ ├── web.js │ ├── seed.js │ ├── id.js │ └── fork.js ├── unused.js └── utils.js ├── package.json └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | exports.create = require('./src/commands/create') 2 | exports.fork = require('./src/commands/fork') 3 | exports.id = require('./src/commands/id') 4 | exports.seed = require('./src/commands/seed') 5 | exports.web = require('./src/commands/web') 6 | -------------------------------------------------------------------------------- /web.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | var web = require('gitverse') 3 | 4 | module.exports = function (dbs) { 5 | var server = http.createServer(web(dbs)) 6 | server.listen(9111, function () { 7 | console.log('gitverse live on http://localhost:9111') 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /bin/usage.txt: -------------------------------------------------------------------------------- 1 | USAGE: hypergit [--version] [command] [--help] 2 | 3 | Commands: 4 | create Create a hypergit repo. 5 | 6 | seed Actively share all local repos with peers. 7 | 8 | fork Create a new remote 'fork' of the current repo. 9 | 10 | web Host local web frontend. 11 | -------------------------------------------------------------------------------- /src/commands/create.js: -------------------------------------------------------------------------------- 1 | var u = require('../utils') 2 | 3 | var createRemote = u.createRemote 4 | var getHyperdb = u.getHyperdb 5 | 6 | module.exports = function create () { 7 | getHyperdb (null, function(err, db) { 8 | var name = 'swarm' 9 | var key = db.key.toString('hex') 10 | createRemote(name, 'hypergit://' + key) 11 | console.log('hypergit://' + key) 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /src/commands/web.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | var web = require('gitverse') 3 | var u = require('../utils') 4 | 5 | var getAllHyperdbs = u.getAllHyperdbs 6 | 7 | module.exports = function webCommand () { 8 | getAllHyperdbs(function (err, dbs) { 9 | var server = http.createServer(web(dbs)) 10 | server.listen(9111, function () { 11 | console.log('gitverse live on http://localhost:9111') 12 | }) 13 | }) 14 | } -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var path = require('path') 4 | var args = require('minimist')(process.argv) 5 | var create = require('../src/commands/create') 6 | var seed = require('../src/commands/seed') 7 | var web = require('../src/commands/web') 8 | var id = require('../src/commands/id') 9 | var fork = require('../src/commands/fork') 10 | 11 | if (args._.length === 2) { 12 | printUsage() 13 | return 14 | } 15 | 16 | switch (args._[2]) { 17 | case 'create': 18 | create() 19 | break 20 | case 'seed': 21 | seed() 22 | break 23 | case 'web': 24 | web() 25 | break 26 | case 'id': 27 | id() 28 | break 29 | case 'fork': 30 | fork() 31 | break 32 | default: 33 | printUsage() 34 | break 35 | } 36 | 37 | function printUsage () { 38 | require('fs') 39 | .createReadStream(path.join(__dirname, '/usage.txt')) 40 | .pipe(process.stdout) 41 | } 42 | -------------------------------------------------------------------------------- /src/commands/seed.js: -------------------------------------------------------------------------------- 1 | var discovery = require('discovery-swarm') 2 | var swarmDefaults = require('dat-swarm-defaults') 3 | var u = require('../utils') 4 | 5 | var getAllHyperdbs = u.getAllHyperdbs 6 | 7 | function swarmReplicate (swarm, db) { 8 | var key = db.key.toString('hex') 9 | console.log('[' + key + '] seeding') 10 | swarm.join(key) 11 | swarm.on('connection', function (conn, info) { 12 | console.log('['+key+'] found peer', info.id.toString('hex')) 13 | var r = db.replicate({live:false}) 14 | r.pipe(conn).pipe(r) 15 | r.once('end', function () { 16 | console.error('[' + key + '] done replicating', info.id.toString('hex')) 17 | }) 18 | r.once('error', function (err) { 19 | console.error('[' + key + '] timeout with', info.id.toString('hex')) 20 | }) 21 | }) 22 | } 23 | 24 | module.exports = function seed () { 25 | // seed ALL repos 26 | getAllHyperdbs(function (err, dbs) { 27 | dbs.forEach(function (db, n) { 28 | dbs.push(db) 29 | var swarm = discovery(swarmDefaults()) 30 | swarm.listen(2342 + n) 31 | swarmReplicate(swarm, db) 32 | }) 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /src/commands/id.js: -------------------------------------------------------------------------------- 1 | var u = require('../utils') 2 | 3 | var getHypergitRemotes = u.getHypergitRemotes 4 | var getHyperdb = u.getHyperdb 5 | 6 | module.exports = function idCommand () { 7 | getHypergitRemotes(function (err, remotes) { 8 | if (!remotes.length) { 9 | console.log('No hypergit remotes on this git repo.') 10 | return process.exit(1) 11 | } else if (remotes.length === 1) { 12 | var key = remotes[0].url.replace('hypergit://', '') 13 | getHyperdb(key, function (err, db) { 14 | if (err) throw err 15 | console.log(db.local.key.toString('hex')) 16 | }) 17 | } else if (!process.argv[3]) { 18 | console.log('Multiple hypergit remotes present. Specify which one you\'d like the id of.') 19 | return process.exit(1) 20 | } else { 21 | var remote = config.remote[process.argv[3]] 22 | if (!remote) { 23 | console.log('No hypergit remote by that name.') 24 | return process.exit(1) 25 | } 26 | var key = remote.url.replace('hypergit://', '') 27 | getHyperdb(key, function (err, db) { 28 | if (err) throw err 29 | console.log(db.local.key.toString('hex')) 30 | }) 31 | } 32 | }) 33 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hypergit", 3 | "description": "command line tool for hypergit", 4 | "author": "Stephen Whitmore ", 5 | "version": "3.2.0", 6 | "repository": { 7 | "url": "git://github.com/noffle/hypergit.git" 8 | }, 9 | "bin": { 10 | "hypergit": "bin/cli.js", 11 | "git-remote-hypergit": "node_modules/.bin/git-remote-hypergit" 12 | }, 13 | "homepage": "https://github.com/noffle/hypergit", 14 | "bugs": "https://github.com/noffle/hypergit/issues", 15 | "main": "index.js", 16 | "scripts": { 17 | "test": "tape test/*.js", 18 | "lint": "standard" 19 | }, 20 | "keywords": [], 21 | "dependencies": { 22 | "dat-swarm-defaults": "^1.0.1", 23 | "discovery-swarm": "^5.1.1", 24 | "env-paths": "^1.0.0", 25 | "git-remote-hypergit": "^2.0.1", 26 | "gitconfiglocal": "^2.0.1", 27 | "gitverse": "^1.0.0", 28 | "hypercore": "^6.15.0", 29 | "hypercore-crypto": "^1.0.0", 30 | "hyperdb": "^3.1.2", 31 | "hyperdb-git-repo": "^1.0.2", 32 | "minimist": "^1.2.0", 33 | "mkdirp": "^0.5.1", 34 | "through2": "^2.0.3" 35 | }, 36 | "devDependencies": { 37 | "tape": "~4.6.2", 38 | "standard": "~10.0.0" 39 | }, 40 | "license": "non-commercial" 41 | } 42 | -------------------------------------------------------------------------------- /src/unused.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var path = require('path') 3 | var envpaths = require('env-paths')('hypergit') 4 | var gitconfig = require('gitconfiglocal') 5 | var u = require('./utils') 6 | 7 | var getHyperdb = u.getHyperdb 8 | 9 | function hyperdir (key) { 10 | return path.join(envpaths.config, key) 11 | } 12 | 13 | exports.getCurrentHyperdb = function getCurrentHyperdb (cb) { 14 | gitconfig('.', function (err, config) { 15 | if (err) throw err 16 | var remotes = Object.keys(config.remote) 17 | .map(function (key) { 18 | var remote = config.remote[key] 19 | remote.name = key 20 | return remote 21 | }) 22 | .filter(function (remote) { 23 | return remote.url.startsWith('hypergit://') 24 | }) 25 | 26 | if (!remotes.length) { 27 | cb(new Error('no hypergit remotes here')) 28 | } else if (remotes.length === 1) { 29 | var key = remotes[0].url.replace('hypergit://', '') 30 | getHyperdb(key, cb) 31 | } else { 32 | cb(new Error('multiple hypergit remotes here')) 33 | } 34 | }) 35 | } 36 | 37 | exports.ensureNoHypergit = function ensureNoHypergit (key) { 38 | if (fs.existsSync(hyperdir(key))) { 39 | console.log('A hypergit repo already exists in this directory.') 40 | return process.exit(1) 41 | } 42 | if (!fs.existsSync(path.join('.git', 'config'))) { 43 | console.log('There is no git repository here.') 44 | return process.exit(1) 45 | } 46 | } 47 | 48 | exports.ensureValidHypergit = function ensureValidHypergit () { 49 | if (!fs.existsSync(hyperdir(key))) { 50 | console.log('No hypergit repo exists in this directory.') 51 | return process.exit(1) 52 | } 53 | if (!fs.existsSync(path.join('.git', 'config'))) { 54 | console.log('There is no git repository here.') 55 | return process.exit(1) 56 | } 57 | } -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var path = require('path') 3 | var hyperdb = require('hyperdb') 4 | var crypto = require('hypercore-crypto') 5 | var spawn = require('child_process').spawnSync; 6 | var envpaths = require('env-paths')('hypergit') 7 | var gitconfig = require('gitconfiglocal') 8 | 9 | exports.createRemote = function createRemote (name, url) { 10 | spawn('git', ['remote', 'add', name, url]); 11 | } 12 | 13 | exports.getHyperdb = function getHyperdb (key, cb) { 14 | var db 15 | if (key === null) { 16 | // create 17 | var keypair = crypto.keyPair() 18 | key = keypair.publicKey.toString('hex') 19 | db = hyperdb(path.join(envpaths.config, key), key, {secretKey: keypair.secretKey}) 20 | } else { 21 | // existing 22 | db = hyperdb(path.join(envpaths.config, key), key) 23 | } 24 | db.ready(function () { 25 | cb(null, db) 26 | }) 27 | } 28 | 29 | exports.getAllHyperdbs = function getAllHyperdbs (cb) { 30 | fs.readdir(envpaths.config, function (err, keys) { 31 | if (err) throw err 32 | var dbs = [] 33 | var pending = 0 34 | keys.forEach(function (key, n) { 35 | pending++ 36 | exports.getHyperdb(key, function (err, db) { 37 | if (err) return done() 38 | dbs.push(db) 39 | done() 40 | }) 41 | }) 42 | function done () { 43 | if (--pending) return 44 | cb(null, dbs) 45 | } 46 | }) 47 | } 48 | 49 | exports.getHypergitRemotes = function getHypergitRemotes (cb) { 50 | gitconfig('.', function (err, config) { 51 | if (err) return cb(err) 52 | var remotes = Object.keys(config.remote) 53 | .map(function (key) { 54 | var remote = config.remote[key] 55 | remote.name = key 56 | return remote 57 | }) 58 | .filter(function (remote) { 59 | return remote.url.startsWith('hypergit://') 60 | }) 61 | cb(null, remotes) 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /src/commands/fork.js: -------------------------------------------------------------------------------- 1 | var through = require('through2') 2 | var u = require('../utils') 3 | 4 | var getHypergitRemotes = u.getHypergitRemotes 5 | var getHyperdb = u.getHyperdb 6 | var createRemote = u.createRemote 7 | 8 | function createFork (db, cb) { 9 | getHyperdb(null, function (err, newDb) { 10 | var t = through.obj(write, flush) 11 | db.createHistoryStream().pipe(t) 12 | 13 | function write (node, _, next) { 14 | console.log('copying', node.key) 15 | newDb.put(node.key, node.value, function (err) { 16 | if (err) throw err 17 | next() 18 | }) 19 | } 20 | 21 | function flush (done) { 22 | console.log('flushing') 23 | done() 24 | cb(null, newDb) 25 | } 26 | }) 27 | } 28 | 29 | module.exports = function fork () { 30 | getHypergitRemotes(function (err, remotes) { 31 | if (!remotes.length) { 32 | console.log('No hypergit remotes on this git repo.') 33 | return process.exit(1) 34 | } else if (remotes.length === 1) { 35 | var key = remotes[0].url.replace('hypergit://', '') 36 | getHyperdb(key, function (err, db) { 37 | if (err) throw err 38 | createFork(db, function (err, newDb) { 39 | var key = newDb.key.toString('hex') 40 | createRemote('fork', 'hypergit://' + key) 41 | console.log('Created new remote "fork": hypergit://' + key) 42 | }) 43 | }) 44 | } else if (!process.argv[3]) { 45 | console.log('Multiple hypergit remotes present. Specify which one you\'d like the id of.') 46 | return process.exit(1) 47 | } else { 48 | var remote = config.remote[process.argv[3]] 49 | if (!remote) { 50 | console.log('No hypergit remote by that name.') 51 | return process.exit(1) 52 | } 53 | var key = remote.url.replace('hypergit://', '') 54 | getHyperdb(key, function (err, db) { 55 | if (err) throw err 56 | createFork(db, function (err, newDb) { 57 | var key = newDb.key.toString('hex') 58 | createRemote('fork', 'hypergit://' + key) 59 | console.log('Created new remote "fork": hypergit://' + key) 60 | }) 61 | }) 62 | } 63 | }) 64 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hypergit 2 | 3 | > CLI for managing peer-to-peer git repositories 4 | 5 | ## Install 6 | 7 | ``` 8 | npm i hypergit -g 9 | ``` 10 | 11 | ## Usage 12 | 13 | ``` 14 | USAGE: hypergit [command] 15 | 16 | Commands: 17 | create Create a hypergit repo. 18 | 19 | seed Actively share all local repos with peers. 20 | 21 | fork Create a new remote 'fork' of the current repo. 22 | 23 | web Host local web frontend. 24 | ``` 25 | 26 | The `hypergit` cli lets you 27 | 28 | 1. manage hypergit remotes, and 29 | 2. lets you `git clone hypergit://...` URLs 30 | 31 | ## What? 32 | 33 | ### Just another git remote 34 | 35 | hypergit wants to be a special git remote, like `https://...` or `ssh://...`, 36 | except instead of pointing to a specific (centralized) server somewhere on the 37 | internet, it points to a peer-to-peer-friendly database on your own computer. 38 | When you 'push' to a hypergit remote, you're writing your changes to a local 39 | hyperdb, which mirrors how git would lay itself out on the filesystem. 40 | 41 | ## Why? 42 | 43 | ### P2P sync & works offline 44 | 45 | hyperdb's special power is the ability to sync itself with other peers over the 46 | internet and local network. 47 | 48 | Creating a hypergit repo (hypergit create) adds a remote called swarm to your 49 | .git/config, and creates a new unique id (it's actually a public key) that 50 | identifies that new remote. Something like 51 | `hypergit://ccc0940b5b13937e5b32ed48b412803d2d70caa18c3ec7ba385c77c204d70c94`. 52 | When someone runs `git clone hypergit://...` or `hypergit seed`, the program 53 | will connect to a distributed hash table and find other peers who are interested 54 | in that identifier. The ID is hashed before you look for it, so users couldn't 55 | discover the repo without knowing the original key. Connections are opened to 56 | those peers and you exchange hyperdb state so that you both end up with the same 57 | resulting state. 58 | 59 | From here you could do a git fetch swarm to pull down those latest changes. The 60 | cool thing is that since the remote lives on your local filesystem, you can do 61 | push and pull and fetch even while offline, and your changes will sync to the 62 | rest of the peers involved in this repo once you're online again. You can even 63 | peer with just other users on the same local network as you and collaborate in 64 | offline environments. 65 | 66 | ### Data lives on the peers 67 | 68 | This approach differs from federation, where users pick from a set of servers to 69 | host their git repositories. With hypergit all of the data lives on peers 70 | directly, so everyone with a laptop is a first-class citizen, and 71 | doesn't have to choose between hosted services that could go down at some point. 72 | By using hypergit seed you can, not unlike bittorrent, re-host git repos you 73 | like on servers and provide them greater availability. 74 | 75 | ## What hypergit isn't 76 | 77 | This isn't a github replacement. hypergit wants to be a powerful primitive that 78 | *enables* social peer-to-peer coding. There are many ways to do coding with 79 | other humans, and heavy opinions on that don't feel like they ought to be tied 80 | in with the lower level pieces. 81 | 82 | ## Contributors 83 | 84 | With hypergit, each project participant has their own hypergit repo. There is no 85 | "origin" central authority. 86 | 87 | - @noffle: `hypergit://207dbaef657d688ad528573ae66b5a0bede40fcb82e6b91afc44ddd43c84874f` 88 | - @wmhilton: `hypergit://86b0753499e01564be180c4f5b8c7f53449bfc27753eb12fb97adf81838167ce` 89 | 90 | If you'd like your hypergit added, open a PR. 91 | 92 | ## Other hypergits 93 | 94 | - [@isomorphic-git](https://github.com/isomorphic-git): `hypergit://5701a1c08ae15dba17e181b1a9a28bdfb8b95200d77a25be6051bb018e25439a` 95 | 96 | If you'd like your hypergit added, open a PR. 97 | --------------------------------------------------------------------------------