├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── config.js ├── git-remote-gittorrent ├── git.js ├── gittorrentd └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # GitTorrent is an OPEN Open Source Project 2 | 3 | Individuals making significant and valuable contributions are given commit-access to the project to contribute as they see fit. This project is more like an open wiki than a standard guarded open source project. 4 | 5 | ## Rules 6 | 7 | There are a few basic ground-rules for contributors: 8 | 9 | 1. **No `--force` pushes** or modifying the Git history in any way. 10 | 1. **External API changes and significant modifications** should be subject to a **pull request** to solicit feedback from other contributors. 11 | 1. Pull requests to solicit feedback are *encouraged* for any other non-trivial contribution but left to the discretion of the contributor. 12 | 1. Use a non-`master` branch for ongoing work. 13 | 1. Contributors should attempt to adhere to the prevailing code style. 14 | 1. Run `npm test` locally before submitting your PR to catch easy-to-miss style & testing issues 15 | 16 | ## Releases 17 | 18 | Declaring formal releases remains the prerogative of the project maintainer. 19 | 20 | ## Changes to this arrangement 21 | 22 | This is an experiment and feedback is welcome! This document is subject to pull requests or changes by contributors where you believe you have something valuable to add or change. 23 | 24 | *Thanks to [Rod Vagg](https://github.com/rvagg) and the [LevelUP](https://github.com/rvagg/node-levelup) project for coming up with this model of open source contribution, and to [Feross Aboukhadijeh](https://github.com/feross) for writing this document.* 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Chris Ball 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [GitTorrent](http://gittorrent.org) 2 | 3 | ### The Decentralization of GitHub 4 | 5 | **GitTorrent** is a peer-to-peer network of Git repositories being shared over BitTorrent. You can read more about the project at [this blog post](http://blog.printf.net/articles/2015/05/29/announcing-gittorrent-a-decentralized-github/). 6 | 7 | To get started: 8 | ``` 9 | sudo npm install --global gittorrent 10 | ``` 11 | (You can avoid `sudo` if you place the gittorrent binaries in your `$PATH`.) 12 | 13 | After that, you can clone a repo with: 14 | ``` 15 | git clone gittorrent://github.com/someuser/somerepo 16 | ``` 17 | Or serve your own repos with: 18 | ``` 19 | touch somerepo/.git/git-daemon-export-ok 20 | gittorrentd 21 | ``` 22 | 23 | Note that GitTorrent is not affiliated with the [git project](http://git-scm.com/). 24 | 25 | # Design 26 | 27 | The design of GitTorrent has five components: 28 | 29 | 1. A "git transport helper" that knows how to download and unpack git objects, and can be used by Git itself to perform a fetch/clone/push. 30 | 1. A distributed hash table that advertises which git commits a node is willing to serve. 31 | 1. A BitTorrent protocol extension that negotiates sending a packfile with needed objects to a peer 32 | 1. A key/value store on the distributed hash table, used as a "user profile" describing a user's repositories and their latest git hashes. 33 | 1. A method for registering friendly usernames on Bitcoin's blockchain, so that a written username can be used to find a user instead of an ugly hex string. 34 | 35 | ## 1. Git Transport Helper 36 | 37 | When Git is asked to perform a network operation with a URL that starts with e.g. `someprotocol://`, it calls `git-remote-someprotocol` and passes the URL as an argument. The remote helper binary is responsible for telling Git what capabilities it has, receiving commands from Git, and downloading objects into the `.git/` directory. 38 | 39 | In GitTorrent's case, we could be asked for three styles of URL: 40 | * `gittorrent://some.git.hosting.site/somerepo` -- we connect over `git://` to find out what the latest commit is, then perform the download using that commit's sha1. This is kind of like a [CDN](CDN) for a git server; the actual download of objects happens via peers, but the lookup of which objects to download happens in the normal Git way. 41 | * `gittorrent:///reponame` -- the sha1 corresponds to a gittorrent user's "mutable key" (hash of their public key) on our DHT -- we look up the key, receive JSON describing the user's repositories, and then perform the download using that commit's sha1. This doesn't use any resources outside of GitTorrent's network. 42 | * `gittorrent://` -- the username is converted into a mutable key sha1 as above. The mapping from usernames to sha1s happens on Bitcoin's blockchain in an OP_RETURN transaction. 43 | 44 | ## 2. Distributed hash table 45 | 46 | The bootstrap server for this DHT runs at `core.gittorrent.org:6881`. It is a bittorrent mainline DHT. Git SHA1s are announced by nodes who can create packfiles for them. The clients on this DHT support dht-store (BEP 44) and use it to store mutable keys. 47 | 48 | ## 3. Protocol extension 49 | 50 | Once a client has connected to another node, it sends a request for the SHA1 it's looking for as bencoded JSON: 51 | ``` 52 | {gittorrent: ask: "sha1"} 53 | ``` 54 | The node providing the packfile returns: 55 | ``` 56 | {gittorrent: sendTorrent: "infoHash"} 57 | ``` 58 | 59 | ## 4. Key/value store 60 | BEP 44 adds support for *mutable* and *immutable* keys. Immutable keys are addressed by the hash of their content, but mutable keys are addressed by the hash of a crypto keypair's public key. The owner of that keypair publishes signed updates to their public key's hash, with a sequence number to ensure the latest value is always propagated by peers. The hash of the public key here is a GitTorrent user ID, and the value associated with that key is a JSON object describing the user's repositories in a User Profile. 61 | 62 | ### User Profile JSON format 63 | * name (string) 64 | * email (string) 65 | * repositories (array) 66 | * name (string) 67 | * refs (array) 68 | * name (string) 69 | * sha1 (string) 70 | 71 | ### Mutable key file JSON format 72 | * pub (string) 73 | * priv (string) 74 | 75 | ## Bitcoin username registration 76 | 77 | *This feature is not going to work on the live Bitcoin network until the OP_RETURN length is increased from 40 to 80 bytes, which will happen in Bitcoin Core v0.11, currently scheduled for release on July 1 2015. Until then, we'll use the Bitcoin testnet, but username registrations will be discarded when the move to the live network happens.* 78 | 79 | Our DHT can't resolve arguments over which mutable key owns a given username -- we need something capable of distributed consensus (like a blockchain) for that. 80 | 81 | The idea of using OP_RETURN comes from telehash's blockname project, but while blockname registers domain names on the blockchain, we're registering username<->key mappings instead. The format is: 82 | ``` 83 | @service!username!key 84 | ``` 85 | e.g. 86 | ``` 87 | @gittorrent!cjb!81e24205d4bac8496d3e13282c90ead5045f09ea 88 | ``` 89 | 90 | Note that OP_RETURN transactions are limited to 80 bytes, which limits usernames in this scheme to 27 bytes. 91 | 92 | As a convenience, this repository will include a database of registered usernames that is updated regularly. This doesn't make GitTorrent any more centralized -- you can run the same scripts yourself on a downloaded blockchain to make sure that this repository does not lie. This is just to save everyone from downloading tens of gigabytes of blockchain to process. 93 | 94 | By the way, storing full Bitcoin history is not necessary. We just need to scan every transaction once, and can discard each transaction after we've scanned it once and determined whether it contained a valid username registration that we record. We just need to scan through all unprocessed blockchain transactions once, and record where we got up to so that we don't have to look at them again after that. 95 | 96 | ## Contributing 97 | 98 | Please send pull requests! Even changes to the design of GitTorrent are welcome and encouraged; nothing is set in stone. 99 | 100 | #### JavaScript Standard Style 101 | 102 | GitTorrent uses [JavaScript Standard Style](https://github.com/feross/standard). 103 | 104 | [![js-standard-style](https://raw.githubusercontent.com/feross/standard/master/badge.png)](https://github.com/feross/standard) 105 | 106 | #### Enable debug logs 107 | 108 | In **node**, enable debug logs by setting the `DEBUG` environment variable to the name of the 109 | module you want to debug (e.g. `bittorrent-protocol`, or `*` to print **all logs**). 110 | 111 | ### License 112 | 113 | MIT. Copyright (c) [Chris Ball](http://printf.net). 114 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('rc')('gittorrent', { 2 | dht: { 3 | bootstrap: [ 4 | 'dht.gittorrent.org:6881', 5 | 'core.gittorrent.org:6881' 6 | ], 7 | listen: 6881, 8 | announce: 30000 9 | }, 10 | key: 'ed25519.key' 11 | }) 12 | -------------------------------------------------------------------------------- /git-remote-gittorrent: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var Chalk = require('chalk') 4 | var DHT = require('bittorrent-dht') 5 | var exec = require('child_process').exec 6 | var hat = require('hat') 7 | var magnet = require('magnet-uri') 8 | var prettyjson = require('prettyjson') 9 | var spawn = require('child_process').spawn 10 | var Swarm = require('bittorrent-swarm') 11 | var ut_gittorrent = require('ut_gittorrent') 12 | var WebTorrent = require('webtorrent') 13 | var zeroFill = require('zero-fill') 14 | var config = require('./config') 15 | var git = require('./git') 16 | 17 | // BitTorrent client version string (used in peer ID). 18 | // Generated from package.json major and minor version. For example: 19 | // '0.16.1' -> '0016' 20 | // '1.2.5' -> '0102' 21 | // 22 | var VERSION = require('./package.json').version 23 | .match(/([0-9]+)/g).slice(0, 2).map(zeroFill(2)).join('') 24 | 25 | function die (error) { 26 | console.error(error) 27 | process.exit(1) 28 | } 29 | 30 | // Gotta enable color manually because stdout isn't a tty. 31 | var chalk = new Chalk.constructor({enabled: true}); 32 | 33 | var dht = new DHT({ 34 | bootstrap: config.dht.bootstrap 35 | }) 36 | 37 | // After building a dictionary of references (sha's to branch names), responds 38 | // to git's "list" and "fetch" commands. 39 | function talk_to_git (refs) { 40 | process.stdin.setEncoding('utf8') 41 | var didFetch = false 42 | process.stdin.on('readable', function () { 43 | var chunk = process.stdin.read() 44 | if (chunk === 'capabilities\n') { 45 | process.stdout.write('fetch\n\n') 46 | } else if (chunk === 'list\n') { 47 | Object.keys(refs).forEach(function (branch, i) { 48 | process.stdout.write(refs[branch] + ' ' + branch + '\n') 49 | }) 50 | process.stdout.write('\n') 51 | } else if (chunk && chunk.search(/^fetch/) !== -1) { 52 | didFetch = true 53 | chunk.split(/\n/).forEach(function (line) { 54 | if (line === '') { 55 | return 56 | } 57 | // Format: "fetch sha branch" 58 | line = line.split(/\s/) 59 | get_infohash(line[1], line[2]) 60 | }) 61 | } else if (chunk && chunk !== '' && chunk !== '\n') { 62 | console.warn('unhandled command: "' + chunk + '"') 63 | } 64 | if (chunk === '\n') { 65 | process.stdout.write('\n') 66 | if (!didFetch) { 67 | // If git already has all the refs it needs, we should exit now. 68 | process.exit() 69 | } 70 | return 71 | } 72 | }) 73 | process.stdout.on('error', function () { 74 | // stdout was closed 75 | }) 76 | } 77 | 78 | var remotename = process.argv[2] 79 | var url = process.argv[3] 80 | var matches = url.match(/gittorrent:\/\/([a-f0-9]{40})\/(.*)/) 81 | var refs = {} // Maps branch names to sha's. 82 | if (matches) { 83 | var key = matches[1] 84 | var reponame = matches[2] 85 | if (remotename.search(/^gittorrent:\/\//) !== -1) { 86 | remotename = key 87 | } 88 | dht.on('ready', function () { 89 | var val = new Buffer(key, 'hex') 90 | dht.get(val, function (err, res) { 91 | if (err) { 92 | return console.error(err) 93 | } 94 | var json = res.v.toString() 95 | var repos = JSON.parse(json) 96 | console.warn('\nMutable key ' + chalk.green(key) + ' returned:\n' + 97 | prettyjson.render(repos, {keysColor: 'yellow', valuesColor: 'green'}) + '\n') 98 | talk_to_git(repos.repositories[reponame]) 99 | }) 100 | }) 101 | } else { 102 | url = url.replace(/^gittorrent:/i, 'git:') 103 | var ls = git.ls(url, function (sha, branch) { 104 | refs[branch] = sha 105 | }) 106 | ls.on('exit', function (err) { 107 | if (err) { 108 | die(err) 109 | } 110 | dht.on('ready', function () { 111 | talk_to_git(refs) 112 | }) 113 | }) 114 | } 115 | 116 | var fetching = {} // Maps shas -> {got: , swarm, branches: [...]} 117 | var todo = 0 // The number of sha's we have yet to fetch. We will not exit 118 | // until this equals zero. 119 | dht.on('peer', function (addr, hash, from) { 120 | var goal = fetching[hash] 121 | if (!goal.peer) { 122 | todo++ 123 | goal.peer = true 124 | } 125 | goal.swarm.addPeer(addr) 126 | }) 127 | 128 | function get_infohash (sha, branch) { 129 | branch = branch.replace(/^refs\/(heads\/)?/, '') 130 | branch = branch.replace(/\/head$/, '') 131 | 132 | // We use console.warn (stderr) because git ignores our writes to stdout. 133 | console.warn('Okay, we want to get ' + chalk.yellow(branch) + ': ' + 134 | chalk.green(sha)) 135 | 136 | if (sha in fetching) { 137 | fetching[sha].branches.push(branch) 138 | // Prevent starting a redundant lookup 139 | return 140 | } 141 | 142 | var info = {got: false, peer: false, swarm: null, branches: [branch]} 143 | fetching[sha] = info 144 | 145 | var magnetUri = 'magnet:?xt=urn:btih:' + sha 146 | var parsed = magnet(magnetUri) 147 | dht.lookup(parsed.infoHash) 148 | 149 | var peerId = new Buffer('-WW' + VERSION + '-' + hat(48), 'utf8') 150 | info.swarm = new Swarm(parsed.infoHash, peerId) 151 | info.swarm.on('wire', function (wire, addr) { 152 | console.warn('\nAdding swarm peer: ' + chalk.green(addr) + ' for ' + 153 | chalk.green(parsed.infoHash)) 154 | wire.use(ut_gittorrent()) 155 | wire.ut_gittorrent.on('handshake', function () { 156 | wire.ut_gittorrent.ask(parsed.infoHash) 157 | }) 158 | wire.ut_gittorrent.on('receivedTorrent', function (infoHash) { 159 | var client = new WebTorrent({ 160 | dht: { 161 | bootstrap: config.dht.bootstrap 162 | }, 163 | tracker: false 164 | }) 165 | client.download(infoHash, function (torrent) { 166 | console.warn('Downloading ' + chalk.green(torrent.files[0].path) + 167 | ' with infohash: ' + chalk.green(infoHash) + '\n') 168 | torrent.on('done', function (done) { 169 | console.warn('done downloading: ' + chalk.green(torrent.files[0].path)) 170 | fetching[sha].got = true 171 | 172 | var stream = torrent.files[0].createReadStream() 173 | var unpack = spawn('git', ['index-pack', '--stdin', '-v', '--fix-thin']) 174 | stream.pipe(unpack.stdin) 175 | unpack.stderr.pipe(process.stderr) 176 | unpack.on('exit', function (code) { 177 | todo-- 178 | if (todo <= 0) { 179 | // These writes are actually necessary for git to finish 180 | // checkout. 181 | process.stdout.write('\n\n') 182 | process.exit() 183 | } 184 | }) 185 | }) 186 | }) 187 | }) 188 | }) 189 | } 190 | -------------------------------------------------------------------------------- /git.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var spawn = require('child_process').spawn 4 | 5 | // Returns a process running `git ls-remote ` that calls `with_ref` on 6 | // each parsed reference. The url may point to a local repository. 7 | function ls (url, with_ref) { 8 | var ls = spawn('git', ['ls-remote', url]) 9 | ls.stdout.on('data', function (lines) { 10 | lines.toString().split('\n').forEach(function (line) { 11 | if (!line || line === '') { 12 | return 13 | } 14 | line = line.split('\t') 15 | var sha = line[0] 16 | var branch = line[1] 17 | if (sha.length !== 40) { 18 | console.warn('[git ls-remote] expected a 40-byte sha: ' + sha + '\n') 19 | console.warn('[git ls-remote] on line: ' + line.join('\t')) 20 | } 21 | with_ref(sha, branch) 22 | }) 23 | }) 24 | return ls 25 | } 26 | 27 | function pad4 (num) { 28 | num = num.toString(16) 29 | while (num.length < 4) { 30 | num = '0' + num 31 | } 32 | return num 33 | } 34 | 35 | // Invokes `$ git-upload-pack --strict `, communicates haves and wants and 36 | // emits 'ready' when stdout becomes a pack file stream. 37 | function upload_pack (dir, want, have) { 38 | // reference: 39 | // https://github.com/git/git/blob/b594c975c7e865be23477989d7f36157ad437dc7/Documentation/technical/pack-protocol.txt#L346-L393 40 | var upload = spawn('git-upload-pack', ['--strict', dir]) 41 | writeln('want ' + want) 42 | writeln() 43 | if (have) { 44 | writeln('have ' + have) 45 | writeln() 46 | } 47 | writeln('done') 48 | 49 | // We want to read git's output one line at a time, and not read any more 50 | // than we have to. That way, when we finish discussing wants and haves, we 51 | // can pipe the rest of the output to a stream. 52 | // 53 | // We use `mode` to keep track of state and formulate responses. It returns 54 | // `false` when we should stop reading. 55 | var mode = list 56 | upload.stdout.on('readable', function () { 57 | while (true) { 58 | var line = getline() 59 | if (line === null) { 60 | return // to wait for more output 61 | } 62 | if (!mode(line)) { 63 | upload.stdout.removeAllListeners('readable') 64 | upload.emit('ready') 65 | return 66 | } 67 | } 68 | }) 69 | 70 | var getline_len = null 71 | // Extracts exactly one line from the stream. Uses `getline_len` in case the 72 | // whole line could not be read. 73 | function getline () { 74 | // Format: '####line' where '####' represents the length of 'line' in hex. 75 | if (!getline_len) { 76 | getline_len = upload.stdout.read(4) 77 | if (getline_len === null) { 78 | return null 79 | } 80 | getline_len = parseInt(getline_len, 16) 81 | } 82 | 83 | if (getline_len === 0) { 84 | return '' 85 | } 86 | 87 | // Subtract by the four we just read, and the terminating newline. 88 | var line = upload.stdout.read(getline_len - 4 - 1) 89 | if (!line) { 90 | return null 91 | } 92 | getline_len = null 93 | upload.stdout.read(1) // And discard the newline. 94 | return line.toString() 95 | } 96 | 97 | // First, the server lists the refs it has, but we already know from 98 | // `git ls-remote`, so wait for it to signal the end. 99 | function list (line) { 100 | if (line === '') { 101 | mode = have ? ack_objects_continue : wait_for_nak 102 | } 103 | return true 104 | } 105 | 106 | // If we only gave wants, git should respond with 'NAK', then the pack file. 107 | function wait_for_nak (line) { 108 | return line !== 'NAK' 109 | } 110 | 111 | // With haves, we wait for 'ACK', but only if not ending in 'continue'. 112 | function ack_objects_continue (line) { 113 | return !(line.search(/^ACK/) !== -1 && line.search(/continue$/) === -1) 114 | } 115 | 116 | // Writes one line to stdin so git-upload-pack can understand. 117 | function writeln (line) { 118 | if (line) { 119 | var len = pad4(line.length + 4 + 1) // Add one for the newline. 120 | upload.stdin.write(len + line + '\n') 121 | } else { 122 | upload.stdin.write('0000') 123 | } 124 | } 125 | 126 | return upload 127 | } 128 | 129 | module.exports = {ls: ls, upload_pack: upload_pack} 130 | -------------------------------------------------------------------------------- /gittorrentd: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var DHT = require('bittorrent-dht') 4 | var EC = require('elliptic').ec 5 | var ed25519 = new EC('ed25519') 6 | var exec = require('child_process').exec 7 | var glob = require('glob') 8 | var fs = require('fs') 9 | var hat = require('hat') 10 | var net = require('net') 11 | var Protocol = require('bittorrent-protocol') 12 | var spawn = require('child_process').spawn 13 | var ut_gittorrent = require('ut_gittorrent') 14 | var ut_metadata = require('ut_metadata') 15 | var WebTorrent = require('webtorrent') 16 | var zeroFill = require('zero-fill') 17 | var config = require('./config') 18 | var git = require('./git') 19 | 20 | // BitTorrent client version string (used in peer ID). 21 | // Generated from package.json major and minor version. For example: 22 | // '0.16.1' -> '0016' 23 | // '1.2.5' -> '0102' 24 | // 25 | var VERSION = require('./package.json').version 26 | .match(/([0-9]+)/g).slice(0, 2).map(zeroFill(2)).join('') 27 | 28 | function die (error) { 29 | console.error(error) 30 | process.exit(1) 31 | } 32 | 33 | var dht = new DHT({ 34 | bootstrap: config.dht.bootstrap 35 | }) 36 | dht.listen(config.dht.listen) 37 | 38 | var announcedRefs = { 39 | } 40 | var userProfile = { 41 | repositories: {} 42 | } 43 | 44 | var key = create_or_read_keyfile() 45 | 46 | function create_or_read_keyfile () { 47 | if (!fs.existsSync(config.key)) { 48 | var keypair = new EC('ed25519').genKeyPair() 49 | fs.writeFileSync(config.key, JSON.stringify({ 50 | pub: keypair.getPublic('hex'), 51 | priv: keypair.getPrivate('hex') 52 | })) 53 | } 54 | 55 | // Okay, now the file exists, whether created here or not. 56 | var key = JSON.parse(fs.readFileSync(config.key).toString()) 57 | return ed25519.keyPair({ 58 | priv: key.priv, 59 | privEnc: 'hex', 60 | pub: key.pub, 61 | pubEnc: 'hex' 62 | }) 63 | } 64 | 65 | function bpad (n, buf) { 66 | if (buf.length === n) return buf 67 | if (buf.length < n) { 68 | var b = new Buffer(n) 69 | buf.copy(b, n - buf.length) 70 | for (var i = 0; i < n - buf.length; i++) b[i] = 0 71 | return b 72 | } 73 | } 74 | 75 | var head = '' 76 | 77 | dht.on('ready', function () { 78 | // Spider all */.git dirs and announce all refs. 79 | var repos = glob.sync('*/{,.git/}git-daemon-export-ok', {strict: false}) 80 | var count = repos.length 81 | repos.forEach(function (repo) { 82 | console.log('in repo ' + repo) 83 | repo = repo.replace(/git-daemon-export-ok$/, '') 84 | console.log(repo) 85 | 86 | var reponame = repo.replace(/\/.git\/$/, '') 87 | userProfile.repositories[reponame] = {} 88 | 89 | var ls = git.ls(repo, function (sha, ref) { 90 | // FIXME: Can't pull in too many branches, so only do heads for now. 91 | if (ref !== 'HEAD' && !ref.match(/^refs\/heads\//)) { 92 | return 93 | } 94 | if (ref === 'refs/heads/master') { 95 | head = sha 96 | } 97 | userProfile.repositories[reponame][ref] = sha 98 | if (!announcedRefs[sha]) { 99 | console.log('Announcing ' + sha + ' for ' + ref + ' on repo ' + repo) 100 | announcedRefs[sha] = repo 101 | dht.announce(sha, config.dht.announce, function (err) { 102 | if (err !== null) { 103 | console.log('Announced ' + sha) 104 | } 105 | }) 106 | } 107 | }) 108 | ls.stdout.on('end', function () { 109 | count-- 110 | if (count <= 0) { 111 | publish_mutable_key() 112 | } 113 | }) 114 | ls.on('exit', function (err) { 115 | if (err) { 116 | die(err) 117 | } 118 | }) 119 | }) 120 | 121 | function publish_mutable_key () { 122 | var json = JSON.stringify(userProfile) 123 | if (json.length > 950) { 124 | console.error("Can't publish mutable key: doesn't fit in 950 bytes.") 125 | return false 126 | } 127 | var value = new Buffer(json.length) 128 | value.write(json) 129 | var sig = key.sign(value) 130 | var opts = { 131 | k: bpad(32, Buffer(key.getPublic().x.toArray())), 132 | seq: 0, 133 | v: value, 134 | sig: Buffer.concat([ 135 | bpad(32, Buffer(sig.r.toArray())), 136 | bpad(32, Buffer(sig.s.toArray())) 137 | ])} 138 | console.log(json) 139 | dht.put(opts, function (errors, hash) { 140 | console.error('errors=', errors) 141 | console.log('hash=', hash.toString('hex')) 142 | }) 143 | } 144 | 145 | net.createServer(function (socket) { 146 | var wire = new Protocol() 147 | wire.use(ut_gittorrent()) 148 | wire.use(ut_metadata()) 149 | socket.pipe(wire).pipe(socket) 150 | wire.on('handshake', function (infoHash, peerId) { 151 | console.log('Received handshake for ' + infoHash.toString('hex')) 152 | var myPeerId = new Buffer('-WW' + VERSION + '-' + hat(48), 'utf8') 153 | wire.handshake(new Buffer(infoHash), new Buffer(myPeerId)) 154 | }) 155 | wire.ut_gittorrent.on('generatePack', function (sha) { 156 | console.error('calling git pack-objects for ' + sha) 157 | if (!announcedRefs[sha]) { 158 | console.error('Asked for an unknown sha: ' + sha) 159 | return 160 | } 161 | var directory = announcedRefs[sha] 162 | var have = null 163 | if (sha !== head) { 164 | have = head 165 | } 166 | var pack = git.upload_pack(directory, sha, have) 167 | pack.stderr.pipe(process.stderr) 168 | pack.on('ready', function () { 169 | var filename = sha + '.pack' 170 | var stream = fs.createWriteStream(filename) 171 | pack.stdout.pipe(stream) 172 | stream.on('close', function () { 173 | console.error('Finished writing ' + filename) 174 | var webtorrent = new WebTorrent({ 175 | dht: {bootstrap: config.dht.bootstrap}, 176 | tracker: false 177 | }) 178 | webtorrent.seed(filename, function onTorrent (torrent) { 179 | console.error(torrent.infoHash) 180 | wire.ut_gittorrent.sendTorrent(torrent.infoHash) 181 | }) 182 | }) 183 | }) 184 | pack.on('exit', function (code) { 185 | if (code !== 0) { 186 | console.error('git-upload-pack process exited with code ' + code) 187 | } 188 | }) 189 | }) 190 | }).listen(config.dht.announce) 191 | }) 192 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gittorrent", 3 | "description": "Using BitTorrent to share git repositories", 4 | "version": "0.1.9", 5 | "author": { 6 | "name": "Chris Ball", 7 | "email": "chris@printf.net", 8 | "url": "http://printf.net/" 9 | }, 10 | "bin": { 11 | "git-remote-gittorrent": "./git-remote-gittorrent", 12 | "gittorrentd": "./gittorrentd" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/cjb/gittorrent/issues" 16 | }, 17 | "dependencies": { 18 | "bittorrent-dht": "git://github.com/cjb/bittorrent-dht#dht-store", 19 | "bittorrent-protocol": "^1.5.7", 20 | "bittorrent-swarm": "^5.0.2", 21 | "chalk": "^1.0.0", 22 | "elliptic": "^3.0.3", 23 | "glob": "^5.0.6", 24 | "hat": "^0.0.3", 25 | "inherits": "^2.0.1", 26 | "magnet-uri": "^4.0.0", 27 | "prettyjson": "^1.1.2", 28 | "rc": "^1.0.3", 29 | "ut_gittorrent": "^0.1.0", 30 | "ut_metadata": "^2.7.3", 31 | "webtorrent": "^0.48.0", 32 | "zero-fill": "^2.2.1" 33 | }, 34 | "devDependencies": { 35 | "standard": "^3.1.1" 36 | }, 37 | "homepage": "http://gittorrent.org", 38 | "keywords": [ 39 | "torrent", 40 | "bittorrent", 41 | "bittorrent client", 42 | "git", 43 | "gittorrent", 44 | "mad science" 45 | ], 46 | "license": "MIT", 47 | "main": "git-remote-gittorrent", 48 | "repository": { 49 | "type": "git", 50 | "url": "git://github.com/cjb/gittorrent.git" 51 | } 52 | } 53 | --------------------------------------------------------------------------------