├── .gitignore ├── .travis.yml ├── Makefile ├── README.md ├── cli-screenshot.png ├── cli.js ├── documentation ├── README.md.html ├── cli.coffee.html ├── cli.js.html ├── cli.sh.html ├── doc-filelist.js ├── doc-script.js ├── doc-style.css ├── example.coffee.html ├── example.js.html ├── host.coffee.html └── host.js.html ├── example.js ├── header.png ├── header.psd ├── lib ├── api.js └── host.js ├── main.js ├── package.json └── test ├── helper.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | coverage.html -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '6' 4 | - '4' 5 | after_script: make test-cov 6 | env: 7 | matrix: 8 | - COVERALLS_SERVICE_NAME=travis-ci 9 | global: 10 | secure: sa3dK+Avi/dcFaUZ/8bquBCAe0nUQ/yVusvzw5joUpJpzrIJKlIxV86bLa58cJAZSNGNyl/ZcjUaejR4xKyjJuYuLsOqN9p3tAY24AgnSf1Tr8EH3ML0k8e0HdOtmbEo9Rn742XSxN0VcPoXgeJgM0vigTJ66iqav6sJ2DMPKDQ= 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TESTS = test 2 | REPORTER = spec 3 | XML_FILE = reports/TEST-all.xml 4 | HTML_FILE = reports/coverage.html 5 | ssl = GIT_SSL_NO_VERIFY=true 6 | 7 | test: test-mocha 8 | 9 | test-ci: 10 | $(MAKE) test-mocha REPORTER=xUnit > $(XML_FILE) 11 | 12 | test-all: clean test-ci test-cov 13 | 14 | test-mocha: 15 | @echo ${ssl} 16 | @NODE_ENV=test mocha \ 17 | --timeout 10000 \ 18 | --reporter ${REPORTER} \ 19 | $(TESTS) 20 | 21 | test-cov: 22 | @echo TRAVIS_JOB_ID=$(TRAVIS_JOB_ID) 23 | @echo ${ssl} 24 | @NODE_ENV=test mocha \ 25 | --timeout 10000 \ 26 | --require blanket \ 27 | --reporter mocha-lcov-reporter | ./node_modules/coveralls/bin/coveralls.js \ 28 | $(TESTS) 29 | 30 | clean: 31 | rm -f reports/* 32 | rm -fr lib-cov -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NodeJS Git Server 2 | 3 | ![image](https://raw.github.com/qrpike/NodeJS-Git-Server/master/header.png) 4 | [![Build Status](https://travis-ci.org/stackdot/NodeJS-Git-Server.svg?branch=master)](https://travis-ci.org/stackdot/NodeJS-Git-Server) [![Coverage Status](https://coveralls.io/repos/github/qrpike/NodeJS-Git-Server/badge.svg?branch=master)](https://coveralls.io/github/qrpike/NodeJS-Git-Server?branch=master) 5 | 6 | A multi-tenant git server using NodeJS. 7 | 8 | Read the [Documented Source Code Here](http://qrpike.github.io/NodeJS-Git-Server/host.coffee.html) 9 | 10 | Made to be able to support many git repo's and users with Read/Write customizable permissions. 11 | 12 | 13 | # Install Git Server 14 | 15 | To install the git server run: 16 | 17 | npm install git-server 18 | 19 | To run tests 20 | 21 | git clone https://github.com/qrpike/NodeJS-Git-Server.git 22 | cd ./NodeJS-Git-Server 23 | make test 24 | 25 | 26 | # Example Usage 27 | 28 | The GitServer is a very easy to get up and running git server. It uses the [Pushover](https://github.com/substack/pushover) and [git-emit](https://github.com/substack/node-git-emit) modules for listening to git events, and its own layer to do the security for each repo & user. 29 | 30 | ```javascript 31 | var GitServer = require('git-server'); 32 | var newUser = { 33 | username:'demo', 34 | password:'demo' 35 | } 36 | var newRepo = { 37 | name:'myrepo', 38 | anonRead:false, 39 | users: [ 40 | { user:newUser, permissions:['R','W'] } 41 | ] 42 | } 43 | server = new GitServer({repos: [ newRepo ]}); 44 | ``` 45 | 46 | # Events: 47 | 48 | Server object emits these events: 49 | 50 | ##passive events 51 | 52 | These events are informational only. They can not be aborted. 53 | 54 | * post-applypatch 55 | * post-commit 56 | * post-checkout 57 | * post-merge 58 | * post-receive 59 | * post-update 60 | * post-rewrite 61 | 62 | ##abortable events 63 | 64 | These events can be aborted or accepted. If there will be no listeners for any of these events, they will be automatically accepted. If object can be aborted, it will have canAbort property in update argument. 65 | 66 | * fetch 67 | * commit 68 | * applypatch-msg 69 | * pre-applypatch 70 | * pre-commit 71 | * prepare-commit-msg 72 | * commit-msg 73 | * pre-rebase 74 | * pre-receive 75 | * update 76 | * pre-auto-gc 77 | 78 | ```javascript 79 | var GitServer = require('git-server'); 80 | var newUser = { 81 | username:'demo', 82 | password:'demo' 83 | } 84 | var newRepo = { 85 | name:'myrepo', 86 | anonRead:false, 87 | users: [ 88 | { user:newUser, permissions:['R','W'] } 89 | ] 90 | } 91 | server = new GitServer({repos: [ newRepo ]}); 92 | server.on('commit', function(update, repo) { 93 | // do some logging or other stuff 94 | update.accept() //accept the update. 95 | }); 96 | server.on('post-update', function(update, repo) { 97 | //do some deploy stuff 98 | }); 99 | ``` 100 | 101 | When we start the git server, it will default to port 7000. We can test this using git on this (or another ) machine. 102 | 103 | ``` 104 | git clone http://localhost:7000/myrepo.git 105 | ``` 106 | 107 | Since this repo does *NOT* allow anonymous reading, it will prompt us for a user/pass 108 | 109 | To make this faster, we can use the basic auth structure: 110 | 111 | git clone http://demo:demo@localhost:7000/myrepo.git 112 | 113 | This should not prompt you for any user/pass. Also in the future when you push changes, or pull, it will not ask you for this info again. 114 | 115 | ## Repo object 116 | 117 | Repo object is the object passed to start the server plus some additional methods and properties. 118 | 119 | ```javascript 120 | { 121 | name: 'stackdot', 122 | anonRead: false, 123 | users: [ { user: {username: "demo", password: "demo"}, permissions: ["R", "W"] } ], 124 | path: '/tmp/repos/stackdot.git', 125 | last_commit: { 126 | status: 'pending', 127 | repo: 'anon.git', 128 | service: 'receive-pack', 129 | cwd: '/tmp/repos/stackdot.git', 130 | last: '00960000000000000000000000000000000000000000', 131 | commit: '67359bb4a6cddd97b59507413542e0b08ef078b0', 132 | evName: 'push', 133 | branch: 'master' 134 | } 135 | } 136 | ``` 137 | 138 | ## Update object 139 | 140 | `update` is an http duplex object (see below) with these extra properties: 141 | 142 | ```javascript 143 | { 144 | cwd: '/tmp/repos/stackdot.git', // Current repo dir 145 | repo: 'stackdot.git', // Repo name 146 | accept: [Function], // Method to accept request (if aplicable) 147 | reject: [Function], // Method to reject request (if aplicable) 148 | exists: true, // Does the repo exist 149 | name: 'fetch', // Event name 150 | canAbort: true // If event can be abbortable 151 | } 152 | ``` 153 | 154 | # HTTPS 155 | 156 | The ability to use HTTPS is now implemented for the module and the cli. This is important so that your username & password is encrypted when being sent over the wire. If you are not using username/password then you may want to disregard this section. 157 | 158 | To enable HTTPS in the module, use the 'cert' param: 159 | 160 | ```javascript 161 | var fs = require('fs'); 162 | var certs = { 163 | key : fs.readFileSync('../certs/privatekey.pem') 164 | cert : fs.readFileSync('../certs/certificate.pem') 165 | }; 166 | _g = new GitServer({repos: [ newRepo ]}, undefined, undefined, undefined, certs); 167 | ``` 168 | 169 | To enable HTTPS in the cli, use the '--ssl' option along with '--key' and '--cert' options: 170 | 171 | git-server[|gitserver] --ssl --key ../certs/privatekey.pem --cert ../certs/certificate.pem 172 | 173 | To create these certs you can run: 174 | 175 | openssl genrsa -out privatekey.pem 1024 176 | openssl req -new -key privatekey.pem -out certrequest.csr 177 | openssl x509 -req -in certrequest.csr -signkey privatekey.pem -out certificate.pem 178 | 179 | Also, be aware that when using HTTPS for the git server, when you try to clone,etc. It will give you an SSL error because git (which uses CURL) cannot verify the SSL Cert. To correct this, install a actual, verified SSL Cert ( Free ones here: [StartCom](http://www.startssl.com/?app=1) ) 180 | 181 | If you want to keep using the self signed cert like we created above ^ just tell git to not verify the cert. ( Other ways to do it [here](http://www.f15ijp.com/2012/08/git-ssl-certificate-problem-how-to-turn-off-ssl-validation-for-a-repo/) ) 182 | 183 | export GIT_SSL_NO_VERIFY=true 184 | 185 | And you are good to go! 186 | 187 | # CLI Usage 188 | 189 | 190 | When you install this package globally using 191 | 192 | ``` 193 | sudo npm install -g git-server 194 | ``` 195 | 196 | You will now have a CLI interface you can run and interact with. 197 | 198 | Get started by typing `git-server` or `gitserver` into your terminal. 199 | 200 | You should see something similar to this: 201 | ![image](https://raw.github.com/qrpike/NodeJS-Git-Server/master/cli-screenshot.png) 202 | 203 | With this interface you can type the following to see the available commands: 204 | 205 | git-server> help 206 | 207 | You will see a list of possible commands, just enter a command and the prompt will ask you for any additional details needed. 208 | 209 | # TODO Items 210 | 211 | - Make YouTube demo of the app 212 | 213 | ### This is a work in progress - please feel free to contribute! 214 | please contribute 215 | #License 216 | 217 | (The MIT License) 218 | 219 | Copyright (c) 2016 [Quinton Pike](https://twitter.com/QuintonPike) 220 | 221 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 222 | 223 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 224 | 225 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 226 | -------------------------------------------------------------------------------- /cli-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackdot/NodeJS-Git-Server/3be1aa65d428568dcd105d7c9062ab90260b10e1/cli-screenshot.png -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // Generated by CoffeeScript 1.6.1 3 | /* 4 | This is the CLI interface for using git-server. 5 | */ 6 | 7 | var CLI, EventEmitter, GITCLI, GitServer, Table, async, commander, certs, fs, getUserHomeDir, logging, mkdirp, path, repoDB, repoLocation, repoPort, repos, _c, _g, 8 | _this = this, 9 | __hasProp = {}.hasOwnProperty, 10 | __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; 11 | 12 | EventEmitter = require('events').EventEmitter; 13 | 14 | CLI = require('cli-listener'); 15 | 16 | GitServer = require('./lib/host.js'); 17 | 18 | mkdirp = require('mkdirp'); 19 | 20 | fs = require('fs'); 21 | 22 | async = require('async'); 23 | 24 | path = require('path'); 25 | 26 | Table = require('cli-table'); 27 | 28 | commander = require('commander'); 29 | 30 | commander.version('0.2.2') 31 | .option('-p, --port [value]', 'Port to run Git on', parseInt) 32 | .option('-d, --directory [value]', 'Directory of the repos') 33 | .option('-l, --logging', 'Verbose logging on or off') 34 | .option('-s, --ssl', 'Enable SSL support; requires key and cert options') 35 | .option('-k, --key [value]', 'SSL key path (required for ssl)') 36 | .option('-c, --cert [value]', 'SSL cert path (required for ssl)') 37 | .option('-a, --certificate-authority [value]', 'SSL certificate authority path') 38 | .parse(process.argv); 39 | 40 | repoPort = commander.port || 7000; 41 | 42 | logging = commander.logging || false; 43 | 44 | if (commander.ssl && commander.key && commander.cert) { 45 | var readFileSync = fs.readFileSync, 46 | resolve = require('path').resolve, 47 | readFile = function(path) { 48 | path = resolve(path); 49 | return readFileSync(path, {encoding:'utf8'}).toString(); 50 | }; 51 | 52 | certs = { 53 | key: readFile(commander.key), 54 | cert: readFile(commander.cert) 55 | }; 56 | 57 | if(commander.certificateAuthority) { 58 | // Properly concatinate the ca chain for node https 59 | var caChain = function caChain(cc) { 60 | var ca = [], 61 | cert = [], 62 | chain = cc.split('\n'), 63 | _i, _len, line; 64 | 65 | for (_i = 0, _len = chain.length; _i < _len; _i++) { 66 | line = chain[_i]; 67 | if (!(line.length !== 0)) { 68 | continue; 69 | } 70 | cert.push(line); 71 | if (line.match(/-END CERTIFICATE-/)) { 72 | ca.push(cert.join("\n")); 73 | cert = []; 74 | } 75 | } 76 | 77 | return ca; 78 | }; 79 | certs.ca = caChain(readFile(commander.certificateAuthority)); 80 | } 81 | } 82 | 83 | getUserHomeDir = function() { 84 | var dir; 85 | if (process.platform === 'win32') { 86 | dir = 'USERPROFILE'; 87 | } else { 88 | dir = 'HOME'; 89 | } 90 | return process.env[dir]; 91 | }; 92 | 93 | repoLocation = commander.directory || path.join(getUserHomeDir(), './git-server/repos'); 94 | 95 | if (commander.directory !== void 0) { 96 | repoDB = commander.directory + '.db'; 97 | } else { 98 | repoDB = path.join(getUserHomeDir(), './git-server/repos.db'); 99 | } 100 | 101 | mkdirp.sync(repoLocation); 102 | 103 | if (fs.existsSync(repoDB)) { 104 | repos = JSON.parse(fs.readFileSync(repoDB)); 105 | } else { 106 | repos = { 107 | repos: [], 108 | users: [] 109 | }; 110 | } 111 | 112 | GITCLI = (function(_super) { 113 | 114 | __extends(GITCLI, _super); 115 | 116 | /* 117 | Constructor for the CLI interface 118 | @param {Object} gitServer Git-Server object instance 119 | @param {Array} users Users we are managing 120 | */ 121 | 122 | 123 | function GITCLI(gitServer, users) { 124 | var availableCalls, welcomeMessage, 125 | _this = this; 126 | this.gitServer = gitServer; 127 | this.users = users != null ? users : []; 128 | this.saveConfig = function() { 129 | return GITCLI.prototype.saveConfig.apply(_this, arguments); 130 | }; 131 | this.listRepos = function(callback) { 132 | return GITCLI.prototype.listRepos.apply(_this, arguments); 133 | }; 134 | this.listUsers = function(callback) { 135 | return GITCLI.prototype.listUsers.apply(_this, arguments); 136 | }; 137 | this.columnPercentage = function(percentage) { 138 | return GITCLI.prototype.columnPercentage.apply(_this, arguments); 139 | }; 140 | this.getUser = function(username) { 141 | return GITCLI.prototype.getUser.apply(_this, arguments); 142 | }; 143 | this.addUserToRepo = function(callback) { 144 | return GITCLI.prototype.addUserToRepo.apply(_this, arguments); 145 | }; 146 | this.createUser = function(callback) { 147 | return GITCLI.prototype.createUser.apply(_this, arguments); 148 | }; 149 | this.createRepo = function(callback) { 150 | return GITCLI.prototype.createRepo.apply(_this, arguments); 151 | }; 152 | availableCalls = { 153 | 'create repo': this.createRepo, 154 | 'create user': this.createUser, 155 | 'list repos': this.listRepos, 156 | 'list users': this.listUsers, 157 | 'add user to repo': this.addUserToRepo 158 | }; 159 | welcomeMessage = "Welcome to Git Server - Powered by NodeJS\n - Repo Location: " + repoLocation + "\n - Listening Port: " + repoPort + "\n - Repo Count: " + this.gitServer.repos.length + "\n - User Count: " + this.users.length; 160 | this.cli = new CLI('git-server', welcomeMessage, availableCalls); 161 | this.on('changedData', this.saveConfig); 162 | setTimeout(this.cli.resetInput, 100); 163 | } 164 | 165 | GITCLI.prototype.createRepo = function(callback) { 166 | var _this = this; 167 | return this.cli.ask({ 168 | name: 'Repo Name: ', 169 | anonRead: 'Anonymous Access? [y,N] :: ' 170 | }, function(err, results) { 171 | var anon, name; 172 | if (err) { 173 | throw err; 174 | } 175 | name = results.name.toLowerCase(); 176 | anon = results.anonRead.toLowerCase(); 177 | if (anon === 'y') { 178 | anon = true; 179 | } else { 180 | anon = false; 181 | } 182 | _this.gitServer.createRepo({ 183 | name: name, 184 | anonRead: anon, 185 | users: [] 186 | }); 187 | _this.emit('changedData'); 188 | return callback(); 189 | }); 190 | }; 191 | 192 | GITCLI.prototype.createUser = function(callback) { 193 | var _this = this; 194 | return this.cli.ask({ 195 | username: 'Users username: ', 196 | password: 'Users password: ' 197 | }, function(err, answers) { 198 | var user, username; 199 | if (err) { 200 | throw err; 201 | } 202 | username = answers.username.toLowerCase(); 203 | user = _this.getUser(username); 204 | if (user !== false) { 205 | console.log('This username already exists'); 206 | return callback(); 207 | } else { 208 | user = { 209 | username: username, 210 | password: answers.password 211 | }; 212 | _this.users.push(user); 213 | _this.emit('changedData'); 214 | return callback(); 215 | } 216 | }); 217 | }; 218 | 219 | GITCLI.prototype.addUserToRepo = function(callback) { 220 | var _this = this; 221 | return this.cli.ask({ 222 | repoName: 'Repo Name: ', 223 | username: 'Users username: ', 224 | permissions: 'Permissions (comma seperated: R,W ): ' 225 | }, function(err, answers) { 226 | var permissions, repo, repoName, user, username; 227 | repoName = answers.repoName.toLowerCase(); 228 | username = answers.username.toLowerCase(); 229 | repo = _this.gitServer.getRepo(repoName + '.git'); 230 | user = _this.getUser(username); 231 | permissions = answers.permissions.split(','); 232 | if (permissions.length === 0) { 233 | permissions = ['R']; 234 | } 235 | if (repo === false) { 236 | return console.log('Repo doesnt exist.'); 237 | } else if (user === false) { 238 | return console.log('User doesnt exist.'); 239 | } else { 240 | repo.users.push({ 241 | user: user, 242 | permissions: permissions 243 | }); 244 | _this.emit('changedData'); 245 | return callback(); 246 | } 247 | }); 248 | }; 249 | 250 | /* 251 | Loop through and find this user 252 | @param {String} username Username of the user we are looking for 253 | */ 254 | 255 | 256 | GITCLI.prototype.getUser = function(username) { 257 | var user, _i, _len, _ref; 258 | _ref = this.users; 259 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 260 | user = _ref[_i]; 261 | if (user.username === username) { 262 | return user; 263 | } 264 | } 265 | return false; 266 | }; 267 | 268 | /* 269 | Get the number of columns needed from a % width 270 | @param {Int} percentage Percentage of the console width 271 | */ 272 | 273 | 274 | GITCLI.prototype.columnPercentage = function(percentage) { 275 | return Math.floor(process.stdout.columns * (percentage / 100)); 276 | }; 277 | 278 | GITCLI.prototype.listUsers = function(callback) { 279 | var repo, repoUser, table, user, users, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1, _ref2, _ref3; 280 | users = this.users; 281 | _ref = this.users; 282 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 283 | user = _ref[_i]; 284 | user.repos = []; 285 | _ref1 = this.gitServer.repos; 286 | for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { 287 | repo = _ref1[_j]; 288 | _ref2 = repo.users; 289 | for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) { 290 | repoUser = _ref2[_k]; 291 | if (repoUser.user.username === user.username) { 292 | user.repos.push({ 293 | name: repo.name, 294 | permissions: repoUser.permissions 295 | }); 296 | } 297 | } 298 | } 299 | } 300 | table = new Table({ 301 | head: ['Username', 'Password', 'Repos'], 302 | colWidths: [this.columnPercentage(40) - 1, this.columnPercentage(20) - 1, this.columnPercentage(40) - 1] 303 | }); 304 | _ref3 = this.users; 305 | for (_l = 0, _len3 = _ref3.length; _l < _len3; _l++) { 306 | user = _ref3[_l]; 307 | repos = (function() { 308 | var _len4, _m, _ref4, _results; 309 | _ref4 = user.repos; 310 | _results = []; 311 | for (_m = 0, _len4 = _ref4.length; _m < _len4; _m++) { 312 | repo = _ref4[_m]; 313 | _results.push("" + repo.name + " (" + (repo.permissions.join(',')) + ")"); 314 | } 315 | return _results; 316 | })(); 317 | table.push([user.username, user.password, repos.join('\n')]); 318 | } 319 | console.log(table.toString()); 320 | return callback(); 321 | }; 322 | 323 | GITCLI.prototype.listRepos = function(callback) { 324 | var repo, table, user, users, _i, _len; 325 | repos = this.gitServer.repos; 326 | table = new Table({ 327 | head: ['Repo Name', 'Anonymous Reads', 'Users'], 328 | colWidths: [this.columnPercentage(40) - 1, this.columnPercentage(20) - 1, this.columnPercentage(40) - 1] 329 | }); 330 | for (_i = 0, _len = repos.length; _i < _len; _i++) { 331 | repo = repos[_i]; 332 | users = (function() { 333 | var _j, _len1, _ref, _results; 334 | _ref = repo.users; 335 | _results = []; 336 | for (_j = 0, _len1 = _ref.length; _j < _len1; _j++) { 337 | user = _ref[_j]; 338 | _results.push("" + user.user.username + " (" + (user.permissions.join(',')) + ")"); 339 | } 340 | return _results; 341 | })(); 342 | table.push([repo.name, repo.anonRead, users.join('\n')]); 343 | } 344 | console.log(table.toString()); 345 | return callback(); 346 | }; 347 | 348 | GITCLI.prototype.saveConfig = function() { 349 | var config; 350 | config = JSON.stringify({ 351 | repos: this.gitServer.repos, 352 | users: this.users 353 | }); 354 | return fs.writeFileSync(repoDB, config); 355 | }; 356 | 357 | return GITCLI; 358 | 359 | })(EventEmitter); 360 | 361 | if (!certs) { 362 | _g = new GitServer(repos.repos, logging, repoLocation, repoPort); 363 | } else { 364 | _g = new GitServer(repos.repos, logging, repoLocation, repoPort, certs); 365 | } 366 | 367 | _c = new GITCLI(_g, repos.users); 368 | -------------------------------------------------------------------------------- /documentation/README.md.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | README.md 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 50 | 51 |
52 |
53 |

54 | 55 | #NodeJS Git Server 56 |

57 |
58 | 59 | 60 |

image

61 | 62 |

A multi-tenant git server using NodeJS.

63 | 64 |

Read the Documented Source Code Here

65 | 66 |

Made to be able to support many git repo's and users with Read/Write customizable permissions.

67 | 68 | 69 |
70 |

71 | 72 | ## Install Git Server 73 |

74 |
75 | 76 | 77 |

To install the git server run:

78 | 79 | 80 |
npm install git-server
 81 | 
82 | 83 | 84 | 85 | 86 |
87 |

88 | 89 | ## Example Usage 90 |

91 |
92 | 93 | 94 |

The GitServer is a very easy to get up and running git server. It uses the Pushover module for listening to git events, and its own layer to do the security for each repo & user.

95 | 96 | 97 |
var GitServer = require('git-server');
 98 | var newUser = {
 99 |     username:'demo',
100 |     password:'demo'
101 | }
102 | var newRepo = {
103 |     name:'myrepo',
104 |     anonRead:false,
105 |     users: [
106 |         { user:newUser, permissions:['R','W'] }
107 |     ]
108 | }
109 | _g = new GitServer([ newRepo ]);
110 | 
111 | 112 | 113 | 114 | 115 |
116 |
117 | 118 | Event Triggers: 119 |
120 |
121 | 122 | 123 |

If you want onSuccessful triggers, you can add them to each repo like so:

124 | 125 | 126 |
var newRepo = {
127 |     name:'myrepo',
128 |     anonRead:false,
129 |     users: [
130 |         { user:newUser, permissions:['R','W'] }
131 |     ],
132 |     onSuccessful : {
133 |         fetch : function( repo, method ){
134 |             console.log('Successful fetch/pull/clone on repo:',repo.name);
135 |         }
136 |         push  : function( repo, method ){
137 |             console.log('PUSHed:', repo.name);
138 |             // Possibly do some deploy scripts etc.
139 |         }
140 |     }
141 | }
142 | 
143 | 144 | 145 | 146 |

When we start the git server, it will default to port 7000. We can test this using git on this (or another ) machine.

147 | 148 | 149 |
git clone http://localhost:7000/myrepo.git
150 | 
151 | 152 | 153 | 154 |

Since this repo does NOT allow anonymous reading, it will prompt us for a user/pass

155 | 156 |

To make this faster, we can use the basic auth structure:

157 | 158 | 159 |
git clone http://demo:demo@localhost:7000/myrepo.git
160 | 
161 | 162 | 163 | 164 |

This should not prompt you for any user/pass. Also in the future when you push changes, or pull, it will not ask you for this info again.

165 | 166 | 167 |
168 |

169 | 170 | ## HTTPS 171 |

172 |
173 | 174 | 175 |

The ability to use HTTPS is now implemented for the module (not the cli yet). This is important so that your username & password is encrypted when being sent over the wire. If you are not using username/password then you may want to disregard this section.

176 | 177 |

To enable HTTPS, send the module the 'cert' param:

178 | 179 | 180 |
var fs = require('fs');
181 | var certs = {
182 |     key     : fs.readFileSync('../certs/privatekey.pem')
183 |     cert    : fs.readFileSync('../certs/certificate.pem')
184 | };
185 | _g = new GitServer([ newRepo ], undefined, undefined, undefined, certs);
186 | 
187 | 188 | 189 | 190 |

To create these certs you can run:

191 | 192 | 193 |
openssl genrsa -out privatekey.pem 1024 
194 | openssl req -new -key privatekey.pem -out certrequest.csr 
195 | openssl x509 -req -in certrequest.csr -signkey privatekey.pem -out certificate.pem
196 | 
197 | 198 | 199 | 200 |

Also, be aware that when using HTTPS for the git server, when you try to clone,etc. It will give you an SSL error because git (which uses CURL) cannot verify the SSL Cert. To correct this, install a actual, verified SSL Cert ( Free ones here: StartCom )

201 | 202 |

If you want to keep using the self signed cert like we created above ^ just tell git to not verify the cert. ( Other ways to do it here )

203 | 204 | 205 |
export GIT_SSL_NO_VERIFY=true
206 | 
207 | 208 | 209 | 210 |

And you are good to go!

211 | 212 | 213 |
214 |

215 | 216 | ## CLI Usage 217 |

218 |
219 | 220 | 221 |

When you install this package globally using

222 | 223 | 224 |
sudo npm install -g git-server
225 | 
226 | 227 | 228 | 229 |

You will now have a CLI interface you can run and interact with.

230 | 231 |

Get started by typing git-server or gitserver into your terminal.

232 | 233 |

You should see something similar to this: 234 | image

235 | 236 |

With this interface you can type the following to see the available commands:

237 | 238 | 239 |
git-server> help
240 | 
241 | 242 | 243 | 244 |

You will see a list of possible commands, just enter a command and the prompt will ask you for any additional details needed.

245 | 246 | 247 |
248 |

249 | 250 | ## TODO Items 251 |

252 |
253 | 254 | 255 | 260 | 261 | 262 |
263 |

264 | 265 | This is a work in progress - please feel free to contribute! 266 |

267 |
268 | 269 | 270 |

please contribute

271 | 272 | 273 |
274 |

275 | 276 | #License 277 |

278 |
279 | 280 | 281 |

(The MIT License)

282 | 283 |

Copyright (c) 2010 Quinton Pike

284 | 285 |

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

286 | 287 |

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

288 | 289 |

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

290 |
291 | 292 | 293 | -------------------------------------------------------------------------------- /documentation/cli.coffee.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | cli.coffee 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 23 | 24 |
25 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | 45 | 47 | 48 | 49 | 55 | 71 | 72 | 73 | 79 | 92 | 93 | 94 | 100 | 109 | 110 | 111 | 117 | 129 | 130 | 131 | 137 | 149 | 150 | 151 | 157 | 165 | 166 | 167 | 173 | 184 | 185 | 186 | 192 | 201 | 202 | 203 | 227 | 233 | 234 | 235 | 241 | 252 | 253 | 254 | 260 | 272 | 273 | 274 | 280 | 286 | 287 | 288 | 294 | 300 | 301 | 302 | 308 | 315 | 316 | 317 | 323 | 341 | 342 | 343 | 349 | 372 | 373 | 374 | 380 | 405 | 406 | 407 | 426 | 436 | 437 | 438 | 457 | 467 | 468 | 469 | 475 | 490 | 491 | 492 | 498 | 506 | 507 | 508 | 514 | 523 | 524 | 525 | 531 | 541 | 542 | 543 | 549 | 555 | 556 | 557 | 563 | 571 | 572 | 573 | 579 | 588 | 589 | 590 | 596 | 606 | 607 | 608 | 614 | 624 | 625 | 626 | 632 | 637 | 638 | 639 |
29 |

cli.coffee

30 |
35 |
36 | 37 |
38 |
39 |

This is the CLI interface for using git-server.

40 |
41 |
42 |
43 |
44 |
46 |
50 |
51 | 52 |
53 |

Get required packages:

54 |
 56 | {EventEmitter}  = require 'events'
 57 | CLI       = require 'cli-listener'
 58 | GitServer     = require './host.js'
 59 | mkdirp      = require 'mkdirp'
 60 | fs        = require 'fs'
 61 | async     = require 'async'
 62 | path      = require 'path'
 63 | Table     = require 'cli-table'
 64 | commander   = require 'commander'
 65 | 
 66 | 
 67 | 
 68 | 
 69 | 
70 |
74 |
75 | 76 |
77 |

Ability to pass in certain settings

78 |
 80 | commander
 81 |   .version('0.0.1')
 82 |   .option( '-p, --port [value]', 'Port to run Git on',parseInt)
 83 |   .option( '-d, --directory [value]', 'Directory of the repos')
 84 |   .option( '-l, --logging', 'Verbose logging on or off')
 85 |   .parse( process.argv )
 86 | 
 87 | 
 88 | 
 89 | 
 90 | 
91 |
95 |
96 | 97 |
98 |

Set the port to either -p passed in, or fall back to port 7000

99 |
101 | repoPort    = commander.port || 7000
102 | logging     = commander.logging || false # set logging too
103 | 
104 | 
105 | 
106 | 
107 | 
108 |
112 |
113 | 114 |
115 |

Get this users home directory if we didnt pass in where the repo location is

116 |
118 | getUserHomeDir = ()->
119 |   if process.platform is 'win32'
120 |     dir = 'USERPROFILE' 
121 |   else dir = 'HOME'
122 |   process.env[dir]
123 | 
124 | 
125 | 
126 | 
127 | 
128 |
132 |
133 | 134 |
135 |

Set the repo location and repo.db file

136 |
138 | repoLocation  = commander.directory || path.join( getUserHomeDir(), './git-server/repos' )
139 | if commander.directory != undefined
140 |   repoDB    = commander.directory+'.db'
141 | else
142 |   repoDB    = path.join( getUserHomeDir(), './git-server/repos.db' )
143 | 
144 | 
145 | 
146 | 
147 | 
148 |
152 |
153 | 154 |
155 |

Create the folders if they dont exist

156 |
158 | mkdirp.sync repoLocation
159 | 
160 | 
161 | 
162 | 
163 | 
164 |
168 |
169 | 170 |
171 |

If we have a .db file use the data in it, otherwise use a blank object

172 |
174 | if fs.existsSync repoDB
175 |   repos = JSON.parse( fs.readFileSync( repoDB ) )
176 | else repos = { repos:[], users:[] }
177 | 
178 | 
179 | 
180 | 
181 | 
182 | 
183 |
187 |
188 | 189 |
190 |

GITCLI Class

191 |
193 | class GITCLI extends EventEmitter
194 | 
195 | 
196 | 
197 | 
198 | 
199 | 
200 |
204 |
205 | 206 |
207 |
208 |

Constructor for the CLI interface

209 |
210 |
211 |
212 |
213 |
Params
214 |
215 | gitServer 216 | Object 217 | Git-Server object instance 218 |
219 |
220 | users 221 | Array 222 | Users we are managing 223 |
224 |
225 |
226 |
228 |   constructor: ( @gitServer, @users = [] )->
229 |     
230 | 
231 | 
232 |
236 |
237 | 238 |
239 |

Available calls the user can make in the CLI

240 |
242 |     availableCalls = 
243 |       'create repo'   : @createRepo
244 |       'create user'   : @createUser
245 |       'list repos'    : @listRepos
246 |       'list users'    : @listUsers
247 |       'add user to repo'  : @addUserToRepo
248 |     
249 | 
250 | 
251 |
255 |
256 | 257 |
258 |

Our fabulous welcome message

259 |
261 |     welcomeMessage = """
262 |       Welcome to Git Server - Powered by NodeJS
263 |        - Repo Location:   #{repoLocation}
264 |        - Listening Port:  #{repoPort}
265 |        - Repo Count: #{@gitServer.repos.length}
266 |        - User Count: #{@users.length}
267 |     """
268 |     
269 | 
270 | 
271 |
275 |
276 | 277 |
278 |

Create the CLI Object

279 |
281 |     @cli = new CLI 'git-server', welcomeMessage, availableCalls
282 |     
283 | 
284 | 
285 |
289 |
290 | 291 |
292 |

If we trigger a changedData write the data to the .db file

293 |
295 |     @on 'changedData', @saveConfig
296 |     
297 | 
298 | 
299 |
303 |
304 | 305 |
306 |

Little hack to reset the input after the gitServer logs any messages.

307 |
309 |     setTimeout @cli.resetInput, 100
310 |   
311 |   
312 | 
313 | 
314 |
318 |
319 | 320 |
321 |

Create a new repo

322 |
324 |   createRepo: ( callback )=>
325 |     @cli.ask { name:'Repo Name: ', anonRead:'Anonymous Access? [y,N] :: ' }, ( err, results )=>
326 |       if err then throw err
327 |       name = results.name.toLowerCase()
328 |       anon = results.anonRead.toLowerCase()
329 |       if anon is 'y' then anon = true
330 |       else anon = false
331 |       @gitServer.createRepo name:name, anonRead:anon, users:[]
332 |       @emit 'changedData'
333 |       callback()
334 |       
335 |       
336 |       
337 |       
338 | 
339 | 
340 |
344 |
345 | 346 |
347 |

Create a new user

348 |
350 |   createUser: ( callback )=>
351 |     @cli.ask { username:'Users username: ', password:'Users password: ' }, ( err, answers )=>
352 |       if err then throw err
353 |       username = answers.username.toLowerCase()
354 |       user = @getUser username
355 |       if user != false
356 |         console.log 'This username already exists'
357 |         callback()
358 |       else
359 |         user = 
360 |           username : username
361 |           password : answers.password
362 |         @users.push user
363 |         @emit 'changedData'
364 |         callback()
365 |   
366 |   
367 |   
368 |   
369 | 
370 | 
371 |
375 |
376 | 377 |
378 |

Add a user to a repo

379 |
381 |   addUserToRepo: ( callback )=>
382 |     @cli.ask { repoName:'Repo Name: ', username:'Users username: ', permissions: 'Permissions (comma seperated: R,W ): ' }, ( err, answers )=>
383 |       repoName = answers.repoName.toLowerCase()
384 |       username = answers.username.toLowerCase()
385 |       repo = @gitServer.getRepo repoName+'.git'
386 |       user = @getUser username
387 |       permissions = answers.permissions.split(',')
388 |       permissions = ['R'] if permissions.length is 0
389 |       if repo is false
390 |         console.log 'Repo doesnt exist.'
391 |       else if user is false
392 |         console.log 'User doesnt exist.'
393 |       else
394 |         repo.users.push
395 |           user: user
396 |           permissions: permissions
397 |         @emit 'changedData'
398 |         callback()
399 |   
400 |   
401 |   
402 | 
403 | 
404 |
408 |
409 | 410 |
411 |
412 |

Loop through and find this user

413 |
414 |
415 |
416 |
417 |
Params
418 |
419 | username 420 | String 421 | Username of the user we are looking for 422 |
423 |
424 |
425 |
427 |   getUser: ( username )=>
428 |     for user in @users
429 |       return user if user.username is username
430 |     false
431 |   
432 |   
433 | 
434 | 
435 |
439 |
440 | 441 |
442 |
443 |

Get the number of columns needed from a % width

444 |
445 |
446 |
447 |
448 |
Params
449 |
450 | percentage 451 | Int 452 | Percentage of the console width 453 |
454 |
455 |
456 |
458 |   columnPercentage: ( percentage )=>
459 |     Math.floor process.stdout.columns*( percentage/100 )
460 |   
461 |   
462 |   
463 |   
464 | 
465 | 
466 |
470 |
471 | 472 |
473 |

List out all the current users and their associated repo's & permissions

474 |
476 |   listUsers: ( callback )=>
477 |     users = @users
478 |     for user in @users
479 |       user.repos = []
480 |       for repo in @gitServer.repos
481 |         for repoUser in repo.users
482 |           if repoUser.user.username is user.username
483 |             user.repos.push
484 |               name: repo.name
485 |               permissions: repoUser.permissions
486 |     
487 | 
488 | 
489 |
493 |
494 | 495 |
496 |

create new console table

497 |
499 |     table   = new Table
500 |       head:['Username','Password','Repos']
501 |       colWidths: [@columnPercentage(40)-1,@columnPercentage(20)-1,@columnPercentage(40)-1]
502 |     
503 | 
504 | 
505 |
509 |
510 | 511 |
512 |

Fill up the table

513 |
515 |     for user in @users
516 |       repos = for repo in user.repos
517 |         "#{repo.name} (#{repo.permissions.join(',')})"
518 |       table.push [ user.username, user.password, repos.join('\n') ]
519 |     
520 | 
521 | 
522 |
526 |
527 | 528 |
529 |

log it

530 |
532 |     console.log table.toString()
533 |     callback()
534 |   
535 |   
536 |   
537 |   
538 | 
539 | 
540 |
544 |
545 | 546 |
547 |

List out all the repo's and their associated users

548 |
550 |   listRepos: ( callback )=>
551 |     repos = @gitServer.repos
552 | 
553 | 
554 |
558 |
559 | 560 |
561 |

Create a new table

562 |
564 |     table = new Table
565 |       head:['Repo Name','Anonymous Reads','Users']
566 |       colWidths: [@columnPercentage(40)-1,@columnPercentage(20)-1,@columnPercentage(40)-1]
567 |     
568 | 
569 | 
570 |
574 |
575 | 576 |
577 |

Fill up the table

578 |
580 |     for repo in repos
581 |       users = for user in repo.users
582 |         "#{user.user.username} (#{user.permissions.join(',')})"
583 |       table.push [ repo.name, repo.anonRead, users.join('\n') ]
584 |     
585 | 
586 | 
587 |
591 |
592 | 593 |
594 |

log it

595 |
597 |     console.log table.toString()
598 |     callback()
599 |   
600 |   
601 |   
602 |   
603 | 
604 | 
605 |
609 |
610 | 611 |
612 |

Save the data to the .db file

613 |
615 |   saveConfig: ()=>
616 |     config = JSON.stringify({ repos:@gitServer.repos, users:@users })
617 |     fs.writeFileSync repoDB, config
618 | 
619 | 
620 | 
621 | 
622 | 
623 |
627 |
628 | 629 |
630 |

Start me up buttercup

631 |
633 | _g = new GitServer repos.repos, logging, repoLocation, repoPort
634 | _c = new GITCLI _g, repos.users
635 | 
636 |
640 |
641 | 642 | 643 | -------------------------------------------------------------------------------- /documentation/cli.sh.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | cli.sh 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 23 | 24 |
25 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | 40 | 357 | 358 | 359 |
29 |

cli.sh

30 |
35 |
36 | 37 |
38 | 39 |
#!/usr/bin/env node
 41 | // Generated by CoffeeScript 1.6.1
 42 | /*
 43 |   This is the CLI interface for using git-server.
 44 | */
 45 | 
 46 | var CLI, EventEmitter, GITCLI, GitServer, Table, async, commander, fs, getUserHomeDir, logging, mkdirp, path, repoDB, repoLocation, repoPort, repos, _c, _g,
 47 |   _this = this,
 48 |   __hasProp = {}.hasOwnProperty,
 49 |   __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
 50 | 
 51 | EventEmitter = require('events').EventEmitter;
 52 | 
 53 | CLI = require('cli-listener');
 54 | 
 55 | GitServer = require('./host.js');
 56 | 
 57 | mkdirp = require('mkdirp');
 58 | 
 59 | fs = require('fs');
 60 | 
 61 | async = require('async');
 62 | 
 63 | path = require('path');
 64 | 
 65 | Table = require('cli-table');
 66 | 
 67 | commander = require('commander');
 68 | 
 69 | commander.version('0.0.1').option('-p, --port [value]', 'Port to run Git on', parseInt).option('-d, --directory [value]', 'Directory of the repos').option('-l, --logging', 'Verbose logging on or off').parse(process.argv);
 70 | 
 71 | repoPort = commander.port || 7000;
 72 | 
 73 | logging = commander.logging || false;
 74 | 
 75 | getUserHomeDir = function() {
 76 |   var dir;
 77 |   if (process.platform === 'win32') {
 78 |     dir = 'USERPROFILE';
 79 |   } else {
 80 |     dir = 'HOME';
 81 |   }
 82 |   return process.env[dir];
 83 | };
 84 | 
 85 | repoLocation = commander.directory || path.join(getUserHomeDir(), './git-server/repos');
 86 | 
 87 | if (commander.directory !== void 0) {
 88 |   repoDB = commander.directory + '.db';
 89 | } else {
 90 |   repoDB = path.join(getUserHomeDir(), './git-server/repos.db');
 91 | }
 92 | 
 93 | mkdirp.sync(repoLocation);
 94 | 
 95 | if (fs.existsSync(repoDB)) {
 96 |   repos = JSON.parse(fs.readFileSync(repoDB));
 97 | } else {
 98 |   repos = {
 99 |     repos: [],
100 |     users: []
101 |   };
102 | }
103 | 
104 | GITCLI = (function(_super) {
105 | 
106 |   __extends(GITCLI, _super);
107 | 
108 |   /*
109 |       Constructor for the CLI interface
110 |       @param {Object} gitServer Git-Server object instance
111 |       @param {Array} users Users we are managing
112 |   */
113 | 
114 | 
115 |   function GITCLI(gitServer, users) {
116 |     var availableCalls, welcomeMessage,
117 |       _this = this;
118 |     this.gitServer = gitServer;
119 |     this.users = users != null ? users : [];
120 |     this.saveConfig = function() {
121 |       return GITCLI.prototype.saveConfig.apply(_this, arguments);
122 |     };
123 |     this.listRepos = function(callback) {
124 |       return GITCLI.prototype.listRepos.apply(_this, arguments);
125 |     };
126 |     this.listUsers = function(callback) {
127 |       return GITCLI.prototype.listUsers.apply(_this, arguments);
128 |     };
129 |     this.columnPercentage = function(percentage) {
130 |       return GITCLI.prototype.columnPercentage.apply(_this, arguments);
131 |     };
132 |     this.getUser = function(username) {
133 |       return GITCLI.prototype.getUser.apply(_this, arguments);
134 |     };
135 |     this.addUserToRepo = function(callback) {
136 |       return GITCLI.prototype.addUserToRepo.apply(_this, arguments);
137 |     };
138 |     this.createUser = function(callback) {
139 |       return GITCLI.prototype.createUser.apply(_this, arguments);
140 |     };
141 |     this.createRepo = function(callback) {
142 |       return GITCLI.prototype.createRepo.apply(_this, arguments);
143 |     };
144 |     availableCalls = {
145 |       'create repo': this.createRepo,
146 |       'create user': this.createUser,
147 |       'list repos': this.listRepos,
148 |       'list users': this.listUsers,
149 |       'add user to repo': this.addUserToRepo
150 |     };
151 |     welcomeMessage = "Welcome to Git Server - Powered by NodeJS\n - Repo Location:  " + repoLocation + "\n - Listening Port:  " + repoPort + "\n - Repo Count: " + this.gitServer.repos.length + "\n - User Count: " + this.users.length;
152 |     this.cli = new CLI('git-server', welcomeMessage, availableCalls);
153 |     this.on('changedData', this.saveConfig);
154 |   }
155 | 
156 |   GITCLI.prototype.createRepo = function(callback) {
157 |     var _this = this;
158 |     return this.cli.ask({
159 |       name: 'Repo Name: ',
160 |       anonRead: 'Anonymous Access? [y,N] :: '
161 |     }, function(err, results) {
162 |       var anon, name;
163 |       if (err) {
164 |         throw err;
165 |       }
166 |       name = results.name.toLowerCase();
167 |       anon = results.anonRead.toLowerCase();
168 |       if (anon === 'y') {
169 |         anon = true;
170 |       } else {
171 |         anon = false;
172 |       }
173 |       _this.gitServer.createRepo({
174 |         name: name,
175 |         anonRead: anon,
176 |         users: []
177 |       });
178 |       _this.emit('changedData');
179 |       return callback();
180 |     });
181 |   };
182 | 
183 |   GITCLI.prototype.createUser = function(callback) {
184 |     var _this = this;
185 |     return this.cli.ask({
186 |       username: 'Users username: ',
187 |       password: 'Users password: '
188 |     }, function(err, answers) {
189 |       var user, username;
190 |       if (err) {
191 |         throw err;
192 |       }
193 |       username = answers.username.toLowerCase();
194 |       user = _this.getUser(username);
195 |       if (user !== false) {
196 |         console.log('This username already exists');
197 |         return callback();
198 |       } else {
199 |         user = {
200 |           username: username,
201 |           password: answers.password
202 |         };
203 |         _this.users.push(user);
204 |         _this.emit('changedData');
205 |         return callback();
206 |       }
207 |     });
208 |   };
209 | 
210 |   GITCLI.prototype.addUserToRepo = function(callback) {
211 |     var _this = this;
212 |     return this.cli.ask({
213 |       repoName: 'Repo Name: ',
214 |       username: 'Users username: ',
215 |       permissions: 'Permissions (comma seperated: R,W ): '
216 |     }, function(err, answers) {
217 |       var permissions, repo, repoName, user, username;
218 |       repoName = answers.repoName.toLowerCase();
219 |       username = answers.username.toLowerCase();
220 |       repo = _this.gitServer.getRepo(repoName + '.git');
221 |       user = _this.getUser(username);
222 |       permissions = answers.permissions.split(',');
223 |       if (permissions.length === 0) {
224 |         permissions = ['R'];
225 |       }
226 |       if (repo === false) {
227 |         return console.log('Repo doesnt exist.');
228 |       } else if (user === false) {
229 |         return console.log('User doesnt exist.');
230 |       } else {
231 |         repo.users.push({
232 |           user: user,
233 |           permissions: permissions
234 |         });
235 |         _this.emit('changedData');
236 |         return callback();
237 |       }
238 |     });
239 |   };
240 | 
241 |   /*
242 |       Loop through and find this user
243 |       @param {String} username Username of the user we are looking for
244 |   */
245 | 
246 | 
247 |   GITCLI.prototype.getUser = function(username) {
248 |     var user, _i, _len, _ref;
249 |     _ref = this.users;
250 |     for (_i = 0, _len = _ref.length; _i < _len; _i++) {
251 |       user = _ref[_i];
252 |       if (user.username === username) {
253 |         return user;
254 |       }
255 |     }
256 |     return false;
257 |   };
258 | 
259 |   /*
260 |       Get the number of columns needed from a % width
261 |       @param {Int} percentage Percentage of the console width
262 |   */
263 | 
264 | 
265 |   GITCLI.prototype.columnPercentage = function(percentage) {
266 |     return Math.floor(process.stdout.columns * (percentage / 100));
267 |   };
268 | 
269 |   GITCLI.prototype.listUsers = function(callback) {
270 |     var repo, repoUser, table, user, users, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1, _ref2, _ref3;
271 |     users = this.users;
272 |     _ref = this.users;
273 |     for (_i = 0, _len = _ref.length; _i < _len; _i++) {
274 |       user = _ref[_i];
275 |       user.repos = [];
276 |       _ref1 = this.gitServer.repos;
277 |       for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
278 |         repo = _ref1[_j];
279 |         _ref2 = repo.users;
280 |         for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) {
281 |           repoUser = _ref2[_k];
282 |           if (repoUser.user.username === user.username) {
283 |             user.repos.push({
284 |               name: repo.name,
285 |               permissions: repoUser.permissions
286 |             });
287 |           }
288 |         }
289 |       }
290 |     }
291 |     table = new Table({
292 |       head: ['Username', 'Password', 'Repos'],
293 |       colWidths: [this.columnPercentage(40) - 1, this.columnPercentage(20) - 1, this.columnPercentage(40) - 1]
294 |     });
295 |     _ref3 = this.users;
296 |     for (_l = 0, _len3 = _ref3.length; _l < _len3; _l++) {
297 |       user = _ref3[_l];
298 |       repos = (function() {
299 |         var _len4, _m, _ref4, _results;
300 |         _ref4 = user.repos;
301 |         _results = [];
302 |         for (_m = 0, _len4 = _ref4.length; _m < _len4; _m++) {
303 |           repo = _ref4[_m];
304 |           _results.push("" + repo.name + " (" + (repo.permissions.join(',')) + ")");
305 |         }
306 |         return _results;
307 |       })();
308 |       table.push([user.username, user.password, repos.join('\n')]);
309 |     }
310 |     console.log(table.toString());
311 |     return callback();
312 |   };
313 | 
314 |   GITCLI.prototype.listRepos = function(callback) {
315 |     var repo, table, user, users, _i, _len;
316 |     repos = this.gitServer.repos;
317 |     table = new Table({
318 |       head: ['Repo Name', 'Anonymous Reads', 'Users'],
319 |       colWidths: [this.columnPercentage(40) - 1, this.columnPercentage(20) - 1, this.columnPercentage(40) - 1]
320 |     });
321 |     for (_i = 0, _len = repos.length; _i < _len; _i++) {
322 |       repo = repos[_i];
323 |       users = (function() {
324 |         var _j, _len1, _ref, _results;
325 |         _ref = repo.users;
326 |         _results = [];
327 |         for (_j = 0, _len1 = _ref.length; _j < _len1; _j++) {
328 |           user = _ref[_j];
329 |           _results.push("" + user.user.username + " (" + (user.permissions.join(',')) + ")");
330 |         }
331 |         return _results;
332 |       })();
333 |       table.push([repo.name, repo.anonRead, users.join('\n')]);
334 |     }
335 |     console.log(table.toString());
336 |     return callback();
337 |   };
338 | 
339 |   GITCLI.prototype.saveConfig = function() {
340 |     var config;
341 |     config = JSON.stringify({
342 |       repos: this.gitServer.repos,
343 |       users: this.users
344 |     });
345 |     return fs.writeFileSync(repoDB, config);
346 |   };
347 | 
348 |   return GITCLI;
349 | 
350 | })(EventEmitter);
351 | 
352 | _g = new GitServer(repos.repos, logging, repoLocation, repoPort);
353 | 
354 | _c = new GITCLI(_g, repos.users);
355 | 
356 |
360 |
361 | 362 | 363 | -------------------------------------------------------------------------------- /documentation/doc-filelist.js: -------------------------------------------------------------------------------- 1 | var tree={"files":["README.md","cli.coffee","cli.js","example.coffee","example.js","host.coffee","host.js"]}; -------------------------------------------------------------------------------- /documentation/doc-script.js: -------------------------------------------------------------------------------- 1 | // # res/script.js 2 | // 3 | // This is the script file that gets copied into the output. It mainly manages the display 4 | // of the folder tree. The idea of this script file is to be minimal and standalone. So 5 | // that means no jQuery. 6 | 7 | // Use localStorage to store data about the tree's state: whether or not 8 | // the tree is visible and which directories are expanded. Unless the state 9 | var sidebarVisible = (window.localStorage && window.localStorage.docker_showSidebar) ? 10 | window.localStorage.docker_showSidebar == 'yes' : 11 | defaultSidebar; 12 | 13 | /** 14 | * ## makeTree 15 | * 16 | * Consructs the folder tree view 17 | * 18 | * @param {object} treeData Folder structure as in [queueFile](../src/docker.js.html#docker.prototype.queuefile) 19 | * @param {string} root Path from current file to root (ie `'../../'` etc.) 20 | * @param {string} filename The current file name 21 | */ 22 | function makeTree(treeData, root, filename){ 23 | var treeNode = document.getElementById('tree'); 24 | var treeHandle = document.getElementById('sidebar-toggle'); 25 | treeHandle.addEventListener('click', toggleTree, false); 26 | 27 | // Build the html and add it to the container. 28 | treeNode.innerHTML = nodeHtml('', treeData, '', root); 29 | 30 | // Root folder (whole tree) should always be open 31 | treeNode.childNodes[0].className += ' open'; 32 | 33 | // Attach click event handler 34 | treeNode.addEventListener('click', nodeClicked, false); 35 | 36 | if(sidebarVisible) document.body.className += ' sidebar'; 37 | 38 | // Restore scroll position from localStorage if set. And attach scroll handler 39 | if(window.localStorage && window.localStorage.docker_treeScroll) treeNode.scrollTop = window.localStorage.docker_treeScroll; 40 | treeNode.onscroll = treeScrolled; 41 | 42 | // Only set a class to allow CSS transitions after the tree state has been painted 43 | setTimeout(function(){ document.body.className += ' slidey'; }, 100); 44 | } 45 | 46 | /** 47 | * ## treeScrolled 48 | * 49 | * Called when the tree is scrolled. Stores the scroll position in localStorage 50 | * so it can be restored on the next pageview. 51 | */ 52 | function treeScrolled(){ 53 | var tree = document.getElementById('tree'); 54 | if(window.localStorage) window.localStorage.docker_treeScroll = tree.scrollTop; 55 | } 56 | 57 | /** 58 | * ## nodeClicked 59 | * 60 | * Called when a directory is clicked. Toggles open state of the directory 61 | * 62 | * @param {Event} e The click event 63 | */ 64 | function nodeClicked(e){ 65 | 66 | // Find the target 67 | var t = e.target; 68 | 69 | // If the click target is actually a file (rather than a directory), ignore it 70 | if(t.tagName.toLowerCase() !== 'div' || t.className === 'children') return; 71 | 72 | // Recurse upwards until we find the actual directory node 73 | while(t && t.className.substring(0,3) != 'dir') t = t.parentNode; 74 | 75 | // If we're at the root node, then do nothing (we don't allow collapsing of the whole tree) 76 | if(!t || t.parentNode.id == 'tree') return; 77 | 78 | // Find the path and toggle the state, saving the state in the localStorage variable 79 | var path = t.getAttribute('rel'); 80 | if(t.className.indexOf('open') !== -1){ 81 | t.className=t.className.replace(/\s*open/g,''); 82 | if(window.localStorage) window.localStorage.removeItem('docker_openPath:' + path); 83 | }else{ 84 | t.className += ' open'; 85 | if(window.localStorage) window.localStorage['docker_openPath:' + path] = 'yes'; 86 | } 87 | } 88 | 89 | 90 | /** 91 | * ## nodeHtml 92 | * 93 | * Constructs the markup for a directory in the tree 94 | * 95 | * @param {string} nodename The node name. 96 | * @param {object} node Node object of same format as whole tree. 97 | * @param {string} path The path form the base to this node 98 | * @param {string} root Relative path from current page to root 99 | */ 100 | function nodeHtml(nodename, node, path, root){ 101 | // Firstly, figure out whether or not the directory is expanded from localStorage 102 | var isOpen = window.localStorage && window.localStorage['docker_openPath:' + path] == 'yes'; 103 | var out = '
'; 104 | out += '
' + nodename + '
'; 105 | out += '
'; 106 | 107 | // Loop through all child directories first 108 | if(node.dirs){ 109 | var dirs = []; 110 | for(var i in node.dirs){ 111 | if(node.dirs.hasOwnProperty(i)) dirs.push({ name: i, html: nodeHtml(i, node.dirs[i], path + i + '/', root) }); 112 | } 113 | // Have to store them in an array first and then sort them alphabetically here 114 | dirs.sort(function(a, b){ return (a.name > b.name) ? 1 : (a.name == b.name) ? 0 : -1; }); 115 | 116 | for(var k = 0; k < dirs.length; k += 1) out += dirs[k].html; 117 | } 118 | 119 | // Now loop through all the child files alphabetically 120 | if(node.files){ 121 | node.files.sort(); 122 | for(var j = 0; j < node.files.length; j += 1){ 123 | out += '' + node.files[j] + ''; 124 | } 125 | } 126 | 127 | // Close things off 128 | out += '
'; 129 | 130 | return out; 131 | } 132 | 133 | /** 134 | * ## toggleTree 135 | * 136 | * Toggles the visibility of the folder tree 137 | */ 138 | function toggleTree(){ 139 | // Do the actual toggling by modifying the class on the body element. That way we can get some nice CSS transitions going. 140 | if(sidebarVisible){ 141 | document.body.className = document.body.className.replace(/\s*sidebar/g,''); 142 | sidebarVisible = false; 143 | }else{ 144 | document.body.className += ' sidebar'; 145 | sidebarVisible = true; 146 | } 147 | if(window.localStorage){ 148 | if(sidebarVisible){ 149 | window.localStorage.docker_showSidebar = 'yes'; 150 | }else{ 151 | window.localStorage.docker_showSidebar = 'no'; 152 | } 153 | } 154 | } 155 | 156 | /** 157 | * ## wireUpTabs 158 | * 159 | * Wires up events on the sidebar tabe 160 | */ 161 | function wireUpTabs(){ 162 | var tabEl = document.getElementById('sidebar_switch'); 163 | var children = tabEl.childNodes; 164 | 165 | // Each tab has a class corresponding of the id of its tab pane 166 | for(var i = 0, l = children.length; i < l; i += 1){ 167 | // Ignore text nodes 168 | if(children[i].nodeType !== 1) continue; 169 | children[i].addEventListener('click', function(c){ 170 | return function(){ switchTab(c); }; 171 | }(children[i].className)); 172 | } 173 | } 174 | 175 | /** 176 | * ## switchTab 177 | * 178 | * Switches tabs in the sidebar 179 | * 180 | * @param {string} tab The ID of the tab to switch to 181 | */ 182 | function switchTab(tab){ 183 | var tabEl = document.getElementById('sidebar_switch'); 184 | var children = tabEl.childNodes; 185 | 186 | // Easiest way to go through tabs without any kind of selector is just to look at the tab bar 187 | for(var i = 0, l = children.length; i < l; i += 1){ 188 | // Ignore text nodes 189 | if(children[i].nodeType !== 1) continue; 190 | 191 | // Figure out what tab pane this tab button corresponts to 192 | var t = children[i].className.replace(/\s.*$/,''); 193 | if(t === tab){ 194 | // Show the tab pane, select the tab button 195 | document.getElementById(t).style.display = 'block'; 196 | if(children[i].className.indexOf('selected') === -1) children[i].className += ' selected'; 197 | }else{ 198 | // Hide the tab pane, deselect the tab button 199 | document.getElementById(t).style.display = 'none'; 200 | children[i].className = children[i].className.replace(/\sselected/,''); 201 | } 202 | } 203 | 204 | // Store the last open tab in localStorage 205 | if(window.localStorage) window.localStorage.docker_sidebarTab = tab; 206 | } 207 | 208 | /** 209 | * ## window.onload 210 | * 211 | * When the document is ready, make the sidebar and all that jazz 212 | */ 213 | window.onload = function(){ 214 | makeTree(tree, relativeDir, thisFile); 215 | wireUpTabs(); 216 | 217 | // Switch to the last viewed sidebar tab if stored, otherwise default to folder tree 218 | if(window.localStorage && window.localStorage.docker_sidebarTab){ 219 | switchTab(window.localStorage.docker_sidebarTab); 220 | }else{ 221 | switchTab('tree'); 222 | } 223 | }; -------------------------------------------------------------------------------- /documentation/doc-style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Palatino Linotype", "Book Antiqua", Palatino, FreeSerif, serif; 3 | font-size: 15px; 4 | line-height: 22px; 5 | margin: 0; 6 | padding: 0; } 7 | 8 | p, h1, h2, h3, h4, h5, h6 { 9 | margin: 0 0 15px 0; } 10 | 11 | h1 { 12 | margin-top: 40px; } 13 | 14 | #tree, #headings { 15 | position: absolute; 16 | top: 30px; 17 | left: 0; 18 | bottom: 0; 19 | width: 290px; 20 | padding: 10px 0; 21 | overflow: auto; } 22 | 23 | #sidebar_wrapper { 24 | position: fixed; 25 | top: 0; 26 | left: 0; 27 | bottom: 0; 28 | width: 0; 29 | overflow: hidden; } 30 | 31 | #sidebar_switch { 32 | position: absolute; 33 | top: 0; 34 | left: 0; 35 | width: 290px; 36 | height: 29px; 37 | border-bottom: 1px solid; } 38 | #sidebar_switch span { 39 | display: block; 40 | float: left; 41 | width: 50%; 42 | text-align: center; 43 | line-height: 29px; 44 | cursor: pointer; } 45 | #sidebar_switch .selected { 46 | font-weight: bold; } 47 | 48 | .slidey #sidebar_wrapper { 49 | -webkit-transition: width 250ms linear; 50 | -moz-transition: width 250ms linear; 51 | -ms-transition: width 250ms linear; 52 | -o-transition: width 250ms linear; 53 | transition: width 250ms linear; } 54 | 55 | .sidebar #sidebar_wrapper { 56 | width: 290px; } 57 | 58 | #tree .nodename { 59 | text-indent: 12px; 60 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAg0lEQVQYlWNIS0tbAcSK////Z8CHGTIzM7+mp6d/ASouwqswKyvrO1DRfyg+CcRaxCgE4Z9A3AjEbIQUgjHQOQvwKgS6+ffChQt3AiUDcCqsra29d/v27R6ghCVWN2ZnZ/9YuXLlRqBAPBALYvVMR0fHmQcPHrQBOUZ4gwfqFj5CAQ4Al6wLIYDwo9QAAAAASUVORK5CYII=); 61 | background-repeat: no-repeat; 62 | background-position: left center; 63 | cursor: pointer; } 64 | #tree .open > .nodename { 65 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAlElEQVQYlWNIS0tbCsT/8eCN////Z2B49OhRfHZ29jdsioDiP27evJkNVggkONeuXbscm8Jly5atA8rzwRSCsG5DQ8MtZEU1NTUPgOLGUHm4QgaQFVlZWT9BijIzM39fuHChDCaHohBkBdCq9SCF8+bN2wHkC+FSCMLGkyZNOvb9+3dbNHEMhSDsDsRMxCjEiolWCADeUBHgU/IGQQAAAABJRU5ErkJggg==); 66 | background-position: left 7px; } 67 | #tree .dir, #tree .file { 68 | position: relative; 69 | min-height: 20px; 70 | line-height: 20px; 71 | padding-left: 12px; } 72 | #tree .dir > .children, #tree .file > .children { 73 | display: none; } 74 | #tree .dir.open > .children, #tree .file.open > .children { 75 | display: block; } 76 | #tree .file { 77 | padding-left: 24px; 78 | display: block; 79 | text-decoration: none; } 80 | #tree > .dir { 81 | padding-left: 0; } 82 | 83 | #headings .heading a { 84 | text-decoration: none; 85 | padding-left: 10px; 86 | display: block; } 87 | #headings .h1 { 88 | padding-left: 0; 89 | margin-top: 10px; 90 | font-size: 1.3em; } 91 | #headings .h2 { 92 | padding-left: 10px; 93 | margin-top: 8px; 94 | font-size: 1.1em; } 95 | #headings .h3 { 96 | padding-left: 20px; 97 | margin-top: 5px; 98 | font-size: 1em; } 99 | #headings .h4 { 100 | padding-left: 30px; 101 | margin-top: 3px; 102 | font-size: 0.9em; } 103 | #headings .h5 { 104 | padding-left: 40px; 105 | margin-top: 1px; 106 | font-size: 0.8em; } 107 | #headings .h6 { 108 | padding-left: 50px; 109 | font-size: 0.75em; } 110 | 111 | #sidebar-toggle { 112 | position: fixed; 113 | top: 0; 114 | left: 0; 115 | width: 5px; 116 | bottom: 0; 117 | z-index: 2; 118 | cursor: pointer; } 119 | #sidebar-toggle:hover { 120 | width: 10px; } 121 | 122 | .slidey #sidebar-toggle, .slidey #container { 123 | -webkit-transition: all 250ms linear; 124 | -moz-transition: all 250ms linear; 125 | -ms-transition: all 250ms linear; 126 | -o-transition: all 250ms linear; 127 | transition: all 250ms linear; } 128 | 129 | .sidebar #sidebar-toggle { 130 | left: 290px; } 131 | 132 | #container { 133 | position: fixed; 134 | left: 5px; 135 | right: 0; 136 | top: 0; 137 | bottom: 0; 138 | overflow: auto; } 139 | 140 | .sidebar #container { 141 | left: 295px; } 142 | 143 | .no-sidebar #sidebar_wrapper, .no-sidebar #sidebar-toggle { 144 | display: none; } 145 | .no-sidebar #container { 146 | left: 0; } 147 | 148 | #page { 149 | padding-top: 40px; } 150 | 151 | table td { 152 | border: 0; 153 | outline: 0; } 154 | 155 | .docs.markdown { 156 | padding: 10px 50px; } 157 | 158 | td.docs { 159 | max-width: 450px; 160 | min-width: 450px; 161 | min-height: 5px; 162 | padding: 10px 25px 1px 50px; 163 | overflow-x: hidden; 164 | vertical-align: top; 165 | text-align: left; } 166 | 167 | .docs pre { 168 | margin: 15px 0 15px; 169 | padding: 5px; 170 | padding-left: 10px; 171 | border: 1px solid; 172 | font-size: 12px; 173 | overflow: auto; } 174 | .docs pre.code_stats { 175 | font-size: 60%; } 176 | .docs p tt, .docs p code, .docs li tt, .docs li code { 177 | border: 1px solid; 178 | font-size: 12px; 179 | padding: 0 0.2em; } 180 | 181 | .dox { 182 | border-top: 1px solid; 183 | padding-top: 10px; 184 | padding-bottom: 10px; } 185 | .dox .details { 186 | padding: 10px; 187 | border: 1px solid; 188 | margin-bottom: 10px; } 189 | .dox .dox_tag_title { 190 | font-weight: bold; } 191 | .dox .dox_tag_detail { 192 | margin-left: 10px; } 193 | .dox .dox_tag_detail span { 194 | margin-right: 5px; } 195 | .dox .dox_type { 196 | font-style: italic; } 197 | .dox .dox_tag_name { 198 | font-weight: bold; } 199 | 200 | .pilwrap { 201 | position: relative; 202 | padding-top: 1px; } 203 | .pilwrap .pilcrow { 204 | font: 12px Arial; 205 | text-decoration: none; 206 | color: #454545; 207 | position: absolute; 208 | top: 3px; 209 | left: -20px; 210 | padding: 1px 2px; 211 | opacity: 0; 212 | -webkit-transition: opacity 0.2s linear; 213 | -moz-transition: opacity 0.2s linear; 214 | -ms-transition: opacity 0.2s linear; 215 | -o-transition: opacity 0.2s linear; 216 | transition: opacity 0.2s linear; } 217 | .pilwrap:hover .pilcrow { 218 | opacity: 1; } 219 | 220 | td.code { 221 | padding: 8px 15px 8px 25px; 222 | width: 100%; 223 | vertical-align: top; 224 | border-left: 1px solid; } 225 | 226 | .background { 227 | border-left: 1px solid; 228 | position: absolute; 229 | z-index: -1; 230 | top: 0; 231 | right: 0; 232 | bottom: 0; 233 | left: 525px; } 234 | 235 | pre, tt, code { 236 | font-size: 12px; 237 | line-height: 18px; 238 | font-family: Menlo, Monaco, Consolas, "Lucida Console", monospace; 239 | margin: 0; 240 | padding: 0; 241 | white-space: pre-wrap; } 242 | 243 | .line-num { 244 | display: inline-block; 245 | width: 50px; 246 | text-align: right; 247 | opacity: 0.3; 248 | margin-left: -20px; 249 | text-decoration: none; } 250 | 251 | /* All the stuff that can depend on colour scheme goes below here: */ 252 | body { 253 | background: #fbffff; 254 | color: #2f2b2b; } 255 | 256 | a { 257 | color: #261a3b; } 258 | a:visited { 259 | color: #261a3b; } 260 | 261 | #sidebar_wrapper { 262 | background: #f0f3f3; } 263 | 264 | #sidebar_switch { 265 | background: #e9eded; 266 | border-bottom-color: #dadddd; } 267 | #sidebar_switch span { 268 | color: #2d2828; } 269 | #sidebar_switch span:hover { 270 | background: #f0f3f3; } 271 | #sidebar_switch .selected { 272 | background: #f6fafa; 273 | color: #252020; } 274 | 275 | #tree .file { 276 | color: #252020; } 277 | 278 | #headings .heading a { 279 | color: #252020; } 280 | 281 | #sidebar-toggle { 282 | background: #e5e8e8; } 283 | #sidebar-toggle:hover { 284 | background: #dadddd; } 285 | 286 | .docs.markdown { 287 | background: #fbffff; } 288 | .docs pre { 289 | border-color: #dadddd; } 290 | .docs p tt, .docs p code, .docs li tt, .docs li code { 291 | border-color: #dadddd; 292 | background: #f0f3f3; } 293 | 294 | .highlight { 295 | background: #f0f3f3; 296 | color: auto; } 297 | 298 | .dox { 299 | border-top-color: #e2e5e5; } 300 | .dox .details { 301 | background: #f0f3f3; 302 | border-color: #dadddd; } 303 | 304 | .pilwrap .pilcrow { 305 | color: #3a3636; } 306 | 307 | td.code, .background { 308 | border-left-color: #dadddd; } 309 | body .highlight .hll { background-color: #ffffcc } 310 | body .highlight { background: #f0f3f3; } 311 | body .highlight .c { color: #0099FF; font-style: italic } /* Comment */ 312 | body .highlight .err { color: #AA0000; background-color: #FFAAAA } /* Error */ 313 | body .highlight .k { color: #006699; font-weight: bold } /* Keyword */ 314 | body .highlight .o { color: #555555 } /* Operator */ 315 | body .highlight .cm { color: #0099FF; font-style: italic } /* Comment.Multiline */ 316 | body .highlight .cp { color: #009999 } /* Comment.Preproc */ 317 | body .highlight .c1 { color: #0099FF; font-style: italic } /* Comment.Single */ 318 | body .highlight .cs { color: #0099FF; font-weight: bold; font-style: italic } /* Comment.Special */ 319 | body .highlight .gd { background-color: #FFCCCC; border: 1px solid #CC0000 } /* Generic.Deleted */ 320 | body .highlight .ge { font-style: italic } /* Generic.Emph */ 321 | body .highlight .gr { color: #FF0000 } /* Generic.Error */ 322 | body .highlight .gh { color: #003300; font-weight: bold } /* Generic.Heading */ 323 | body .highlight .gi { background-color: #CCFFCC; border: 1px solid #00CC00 } /* Generic.Inserted */ 324 | body .highlight .go { color: #AAAAAA } /* Generic.Output */ 325 | body .highlight .gp { color: #000099; font-weight: bold } /* Generic.Prompt */ 326 | body .highlight .gs { font-weight: bold } /* Generic.Strong */ 327 | body .highlight .gu { color: #003300; font-weight: bold } /* Generic.Subheading */ 328 | body .highlight .gt { color: #99CC66 } /* Generic.Traceback */ 329 | body .highlight .kc { color: #006699; font-weight: bold } /* Keyword.Constant */ 330 | body .highlight .kd { color: #006699; font-weight: bold } /* Keyword.Declaration */ 331 | body .highlight .kn { color: #006699; font-weight: bold } /* Keyword.Namespace */ 332 | body .highlight .kp { color: #006699 } /* Keyword.Pseudo */ 333 | body .highlight .kr { color: #006699; font-weight: bold } /* Keyword.Reserved */ 334 | body .highlight .kt { color: #007788; font-weight: bold } /* Keyword.Type */ 335 | body .highlight .m { color: #FF6600 } /* Literal.Number */ 336 | body .highlight .s { color: #CC3300 } /* Literal.String */ 337 | body .highlight .na { color: #330099 } /* Name.Attribute */ 338 | body .highlight .nb { color: #336666 } /* Name.Builtin */ 339 | body .highlight .nc { color: #00AA88; font-weight: bold } /* Name.Class */ 340 | body .highlight .no { color: #336600 } /* Name.Constant */ 341 | body .highlight .nd { color: #9999FF } /* Name.Decorator */ 342 | body .highlight .ni { color: #999999; font-weight: bold } /* Name.Entity */ 343 | body .highlight .ne { color: #CC0000; font-weight: bold } /* Name.Exception */ 344 | body .highlight .nf { color: #CC00FF } /* Name.Function */ 345 | body .highlight .nl { color: #9999FF } /* Name.Label */ 346 | body .highlight .nn { color: #00CCFF; font-weight: bold } /* Name.Namespace */ 347 | body .highlight .nt { color: #330099; font-weight: bold } /* Name.Tag */ 348 | body .highlight .nv { color: #003333 } /* Name.Variable */ 349 | body .highlight .ow { color: #000000; font-weight: bold } /* Operator.Word */ 350 | body .highlight .w { color: #bbbbbb } /* Text.Whitespace */ 351 | body .highlight .mf { color: #FF6600 } /* Literal.Number.Float */ 352 | body .highlight .mh { color: #FF6600 } /* Literal.Number.Hex */ 353 | body .highlight .mi { color: #FF6600 } /* Literal.Number.Integer */ 354 | body .highlight .mo { color: #FF6600 } /* Literal.Number.Oct */ 355 | body .highlight .sb { color: #CC3300 } /* Literal.String.Backtick */ 356 | body .highlight .sc { color: #CC3300 } /* Literal.String.Char */ 357 | body .highlight .sd { color: #CC3300; font-style: italic } /* Literal.String.Doc */ 358 | body .highlight .s2 { color: #CC3300 } /* Literal.String.Double */ 359 | body .highlight .se { color: #CC3300; font-weight: bold } /* Literal.String.Escape */ 360 | body .highlight .sh { color: #CC3300 } /* Literal.String.Heredoc */ 361 | body .highlight .si { color: #AA0000 } /* Literal.String.Interpol */ 362 | body .highlight .sx { color: #CC3300 } /* Literal.String.Other */ 363 | body .highlight .sr { color: #33AAAA } /* Literal.String.Regex */ 364 | body .highlight .s1 { color: #CC3300 } /* Literal.String.Single */ 365 | body .highlight .ss { color: #FFCC33 } /* Literal.String.Symbol */ 366 | body .highlight .bp { color: #336666 } /* Name.Builtin.Pseudo */ 367 | body .highlight .vc { color: #003333 } /* Name.Variable.Class */ 368 | body .highlight .vg { color: #003333 } /* Name.Variable.Global */ 369 | body .highlight .vi { color: #003333 } /* Name.Variable.Instance */ 370 | body .highlight .il { color: #FF6600 } /* Literal.Number.Integer.Long */ 371 | -------------------------------------------------------------------------------- /documentation/example.coffee.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | example.coffee 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 32 | 33 |
34 | 35 | 36 | 37 | 40 | 41 | 42 | 43 | 49 | 56 | 57 | 58 | 72 | 88 | 89 | 90 | 104 | 125 | 126 | 127 | 133 | 141 | 142 | 143 | 160 | 164 | 165 | 166 |
38 |

example.coffee

39 |
44 |
45 | 46 |
47 |

Require the GitServer module

48 |
GitServer = require './host.js'
 50 | 
 51 | 
 52 | 
 53 | 
 54 | 
55 |
59 |
60 |
61 |

62 | 63 | Create example repo 1 64 |

65 |
66 | 67 |
68 |

This repo does NOT allow Anonymous access

69 |
70 |
71 |
 73 | repoOne = 
 74 |   name    : 'stackdot'
 75 |   anonRead  : false
 76 |   users   : [
 77 |     user:
 78 |       username: 'demo'
 79 |       password: 'demo'
 80 |     permissions:['R','W']
 81 |   ]
 82 | 
 83 | 
 84 | 
 85 | 
 86 | 
87 |
91 |
92 |
93 |

94 | 95 | Create example repo 2 96 |

97 |
98 | 99 |
100 |

This repo allows Anonymous reading (fetch,clone) access

101 |
102 |
103 |
105 | repoTwo =
106 |   name    : 'anon'
107 |   anonRead  : true
108 |   users   : [
109 |     user:
110 |       username: 'demo2'
111 |       password: 'demo2'
112 |     permissions:['R','W']
113 |   ]
114 |   onSuccessful :
115 |     fetch : ->
116 |       console.log 'Successful fetch on "anon" repo'
117 |     push : ->
118 |       console.log 'Success push on "anon" repo'
119 | 
120 | 
121 | 
122 | 
123 | 
124 |
128 |
129 | 130 |
131 |

Put these into arrays

132 |
134 | repos   = [ repoOne, repoTwo ] 
135 | 
136 | 
137 | 
138 | 
139 | 
140 |
144 |
145 |
146 |

147 | 148 | Create the GitServer object 149 |

150 |
151 | 152 |
153 |

We are passing in repos array for the list of Repos we want to run return 154 | We are passing in true to enable verbose logging return 155 | We are passing in /tmp/repos to specify where the .git repos should live return 156 | We are passing in 7000 for the port to run on ( port 80 requires sudo )

157 |
158 |
159 |
161 | _git = new GitServer repos, true, '/tmp/repos', 7000
162 | 
163 |
167 |
168 | 169 | 170 | -------------------------------------------------------------------------------- /documentation/example.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | example.js 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 32 | 33 |
34 | 35 | 36 | 37 | 40 | 41 | 42 | 43 | 49 | 56 | 57 | 58 | 72 | 92 | 93 | 94 | 108 | 138 | 139 | 140 | 157 | 163 | 164 | 165 |
38 |

example.js

39 |
44 |
45 | 46 |
47 |

Generated by CoffeeScript 1.6.1

48 |
var GitServer, repoOne, repoTwo, repos, _git;
 50 | 
 51 | GitServer = require('./host.js');
 52 | 
 53 | 
 54 | 
55 |
59 |
60 |
61 |

62 | 63 | Create example repo 1 64 |

65 |
66 | 67 |
68 |

This repo does NOT allow Anonymous access

69 |
70 |
71 |
 73 | 
 74 | 
 75 | repoOne = {
 76 |   name: 'stackdot',
 77 |   anonRead: false,
 78 |   users: [
 79 |     {
 80 |       user: {
 81 |         username: 'demo',
 82 |         password: 'demo'
 83 |       },
 84 |       permissions: ['R', 'W']
 85 |     }
 86 |   ]
 87 | };
 88 | 
 89 | 
 90 | 
91 |
95 |
96 |
97 |

98 | 99 | Create example repo 2 100 |

101 |
102 | 103 |
104 |

This repo allows Anonymous reading (fetch,clone) access

105 |
106 |
107 |
109 | 
110 | 
111 | repoTwo = {
112 |   name: 'anon',
113 |   anonRead: true,
114 |   users: [
115 |     {
116 |       user: {
117 |         username: 'demo2',
118 |         password: 'demo2'
119 |       },
120 |       permissions: ['R', 'W']
121 |     }
122 |   ],
123 |   onSuccessful: {
124 |     fetch: function() {
125 |       return console.log('Successful fetch on "anon" repo');
126 |     },
127 |     push: function() {
128 |       return console.log('Success push on "anon" repo');
129 |     }
130 |   }
131 | };
132 | 
133 | repos = [repoOne, repoTwo];
134 | 
135 | 
136 | 
137 |
141 |
142 |
143 |

144 | 145 | Create the GitServer object 146 |

147 |
148 | 149 |
150 |

We are passing in repos array for the list of Repos we want to run return 151 | We are passing in true to enable verbose logging return 152 | We are passing in /tmp/repos to specify where the .git repos should live return 153 | We are passing in 7000 for the port to run on ( port 80 requires sudo )

154 |
155 |
156 |
158 | 
159 | 
160 | _git = new GitServer(repos, true, '/tmp/repos', 7000);
161 | 
162 |
166 |
167 | 168 | 169 | -------------------------------------------------------------------------------- /documentation/host.coffee.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | host.coffee 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 23 | 24 |
25 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | 45 | 47 | 48 | 49 | 55 | 73 | 74 | 75 | 109 | 115 | 116 | 117 | 123 | 129 | 130 | 131 | 137 | 142 | 143 | 144 | 150 | 155 | 156 | 157 | 163 | 177 | 178 | 179 | 185 | 190 | 191 | 192 | 198 | 205 | 206 | 207 | 226 | 234 | 235 | 236 | 242 | 255 | 256 | 257 | 263 | 275 | 276 | 277 | 306 | 311 | 312 | 313 | 319 | 327 | 328 | 329 | 335 | 342 | 343 | 344 | 350 | 355 | 356 | 357 | 363 | 368 | 369 | 370 | 376 | 381 | 382 | 383 | 389 | 398 | 399 | 400 | 436 | 441 | 442 | 443 | 449 | 454 | 455 | 456 | 462 | 467 | 468 | 469 | 475 | 480 | 481 | 482 | 488 | 506 | 507 | 508 | 514 | 519 | 520 | 521 | 527 | 532 | 533 | 534 | 540 | 545 | 546 | 547 | 553 | 562 | 563 | 564 | 577 | 583 | 584 | 585 | 591 | 597 | 598 | 599 | 605 | 611 | 612 | 613 | 619 | 625 | 626 | 627 | 633 | 638 | 639 | 640 | 646 | 651 | 652 | 653 | 659 | 664 | 665 | 666 | 672 | 682 | 683 | 684 | 703 | 711 | 712 | 713 | 719 | 726 | 727 | 728 | 734 | 747 | 748 | 749 | 768 | 784 | 785 | 786 | 810 | 815 | 816 | 817 | 823 | 828 | 829 | 830 | 836 | 841 | 842 | 843 | 849 | 859 | 860 | 861 | 890 | 896 | 897 | 898 | 904 | 914 | 915 | 916 | 935 | 941 | 942 | 943 | 949 | 958 | 959 | 960 | 966 | 970 | 971 | 972 |
29 |

host.coffee

30 |
35 |
36 | 37 |
38 |
39 |

GOAL: A simple to setup and run, multi-tenant Git Server written in NodeJS.

40 |
41 |

This was initially created to be used as a multi-tenant git server with powerful event triggers.

42 |
43 |
44 |
46 |
50 |
51 | 52 |
53 |

Require the modules needed:

54 |
 56 | pushover  = require 'pushover'
 57 | http    = require 'http'
 58 | https   = require 'https'
 59 | async   = require 'async'
 60 | fs      = require 'fs'
 61 | 
 62 | 
 63 | 
 64 | 
 65 | class GitServer
 66 |   
 67 |   
 68 |   
 69 |   
 70 | 
 71 | 
72 |
76 |
77 | 78 |
79 |
80 |

Constructor function for each instance of GitServer

81 |
82 |
83 |
84 |
85 |
Params
86 |
87 | repos 88 | Array 89 | List of repositories 90 |
91 |
92 | repoLocation 93 | String 94 | Location where the repo's are/will be stored 95 |
96 |
97 | port 98 | Int 99 | Port on which to run this server. 100 |
101 |
102 | certs 103 | Object 104 | Object of 'key' and 'cert' with the location of the certs (only used for HTTPS) 105 |
106 |
107 |
108 |
110 |   constructor: ( @repos = [], @logging = false, @repoLocation = '/tmp/repos', @port = 7000, @certs )->
111 |     
112 | 
113 | 
114 |
118 |
119 | 120 |
121 |

Create the pushover git object:

122 |
124 |     @git    = pushover @repoLocation, autoCreate:false
125 |     @permMap  = fetch:'R', push:'W'
126 | 
127 | 
128 |
132 |
133 | 134 |
135 |

Setup the repo listeners:

136 |
138 |     @gitListeners()
139 | 
140 | 
141 |
145 |
146 | 147 |
148 |

Go through all the @repo's and create them if they dont exist:

149 |
151 |     @makeReposIfNull =>
152 | 
153 | 
154 |
158 |
159 | 160 |
161 |

Route requests to pushover:

162 |
164 |       if @certs?
165 |         @server = https.createServer @certs, @git.handle.bind(@git)
166 |       else
167 |         red   = `'\033[31m'`
168 |         reset = `'\033[0m'`
169 |         message = """
170 |           WARNING: No SSL certs passed in. Running as HTTP and not HTTPS.
171 |           Be careful, without HTTPS your user/pass will not be encrypted"""
172 |         console.log red + message + reset
173 |         @server = http.createServer @git.handle.bind(@git)
174 | 
175 | 
176 |
180 |
181 | 182 |
183 |

Open up the desired port ( 80 requires sudo )

184 |
186 |       @server.listen @port, =>
187 | 
188 | 
189 |
193 |
194 | 195 |
196 |

Just let the console know we have started.

197 |
199 |         @log 'Server listening on ', @port, '\r'
200 |   
201 |   
202 | 
203 | 
204 |
208 |
209 | 210 |
211 |
212 |

Create a repo on the fly

213 |
214 |
215 |
216 |
217 |
Params
218 |
219 | repoName 220 | Object 221 | Name of the repo we are creating. 222 |
223 |
224 |
225 |
227 |   createRepo: ( repo, callback )=>
228 |     if !repo.name? or !repo.anonRead?
229 |       @log 'Not enough details, need atleast .name and .anonRead'
230 |       false
231 | 
232 | 
233 |
237 |
238 | 239 |
240 |

make sure it doesnt already exist:

241 |
243 |     if !@getRepo repo.name
244 |       @log 'Creating repo', repo.name
245 |       @repos.push repo
246 |       @git.create repo.name, callback
247 |     else
248 |       @log 'This repo already exists'
249 |   
250 |   
251 |   
252 | 
253 | 
254 |
258 |
259 | 260 |
261 |

Log all arguments passed into this function IF @logging = true

262 |
264 |   log: ()=>
265 |     args = for key,value of arguments
266 |       "#{value}"
267 |     if @logging then console.log "LOG: ", args.join(' ')
268 |   
269 |   
270 |   
271 |   
272 | 
273 | 
274 |
278 |
279 | 280 |
281 |
282 |

Process the request and check for basic authentication.

283 |
284 |
285 |
286 |
287 |
Params
288 |
289 | gitObject 290 | Object 291 | Git object from the pushover module 292 |
293 |
294 | method 295 | String 296 | Method we are getting security for ['fetch','push'] 297 |
298 |
299 | repo 300 | Object 301 | Repo object that we are doing this method on 302 |
303 |
304 |
305 |
307 |   processSecurity: ( gitObject, method, repo )=>
308 | 
309 | 
310 |
314 |
315 | 316 |
317 |

Try to get the auth header

318 |
320 |     req   = gitObject.request
321 |     res   = gitObject.response
322 |     auth  = req.headers['authorization']
323 |     if auth is undefined
324 | 
325 | 
326 |
330 |
331 | 332 |
333 |

if they didnt send a auth header, tell them we need one:

334 |
336 |       res.statusCode = 401
337 |       res.setHeader 'WWW-Authenticate', 'Basic realm="Secure Area"'
338 |       res.end '<html><body>Need some creds son</body></html>'
339 | 
340 | 
341 |
345 |
346 | 347 |
348 |

now they should have responded with the auth headers:

349 |
351 |     else
352 | 
353 | 
354 |
358 |
359 | 360 |
361 |

Decode the auth string

362 |
364 |       plain_auth  = ( new Buffer( auth.split(' ')[1], 'base64' ) ).toString()
365 | 
366 | 
367 |
371 |
372 | 373 |
374 |

split the string to get username:password

375 |
377 |       creds = plain_auth.split ':'
378 | 
379 | 
380 |
384 |
385 | 386 |
387 |

Send off this user info and authorize it:

388 |
390 |       @permissableMethod creds[0], creds[1], method, repo, gitObject
391 |   
392 |   
393 |   
394 |   
395 | 
396 | 
397 |
401 |
402 | 403 |
404 |
405 |

Check to see if: return 406 | Username and password match return 407 | This user has permission to do this method on this repo

408 |
409 |
410 |
411 |
412 |
Params
413 |
414 | username 415 | String 416 | Username of the requesting user 417 |
418 |
419 | password 420 | String 421 | Password of the requesting user 422 |
423 |
424 | method 425 | String 426 | Method we are checking against ['fetch','push'] 427 |
428 |
429 | gitObject 430 | Object 431 | Git object from pushover module 432 |
433 |
434 |
435 |
437 |   permissableMethod: ( username, password, method, repo, gitObject )=>
438 | 
439 | 
440 |
444 |
445 | 446 |
447 |

Just let the console know someone is trying to do something that requires a password:

448 |
450 |     @log username,'is trying to', method,'on repo:',repo.name,'...'
451 | 
452 | 
453 |
457 |
458 | 459 |
460 |

Find the user object:

461 |
463 |     user = @getUser username, password, repo
464 | 
465 | 
466 |
470 |
471 | 472 |
473 |

check if the user exists:

474 |
476 |     if user is false
477 | 
478 | 
479 |
483 |
484 | 485 |
486 |

This user isnt in this repo's .users array:

487 |
489 |       @log username,'was rejected as this user doesnt exist, or password is wrong'
490 |       gitObject.reject(500,'Wrong username or password')
491 |     else
492 |       if @permMap[ method ] in user.permissions
493 |         @log username,'Successfully did a', method,'on',repo.name
494 |         @checkTriggers method, repo
495 |         gitObject.accept()
496 |       else
497 |         @log username,'was rejected, no permission to',method,'on',repo.name
498 |         gitObject.reject(500,"You dont have these permissions")
499 |   
500 |   
501 |   
502 |   
503 | 
504 | 
505 |
509 |
510 | 511 |
512 |

Setup the listeners for git events:

513 |
515 |   gitListeners: ()=>
516 | 
517 | 
518 |
522 |
523 | 524 |
525 |

On each git push request

526 |
528 |     @git.on 'push', @onPush
529 | 
530 | 
531 |
535 |
536 | 537 |
538 |

On each git fetch request

539 |
541 |     @git.on 'fetch', @onFetch
542 | 
543 | 
544 |
548 |
549 | 550 |
551 |

On each git info request

552 |
554 |     @git.on 'info', @onFetch
555 |   
556 |   
557 |   
558 |   
559 | 
560 | 
561 |
565 |
566 | 567 |
568 |
569 |

Checks all the passed in repo's to make sure they all have a real .git directory.

570 |
571 |
572 |
573 |
574 |
575 |
576 |
578 |   makeReposIfNull: ( callback )=>
579 |     @log 'Making repos if they dont exist';
580 | 
581 | 
582 |
586 |
587 | 588 |
589 |

Get all the repo names in an Array

590 |
592 |     repoNames = []
593 |     for repo in @repos
594 | 
595 | 
596 |
600 |
601 | 602 |
603 |

Make sure this repo has the require fields, if so, add to array:

604 |
606 |       if repo.name? and repo.anonRead? and repo.users?
607 |         repoNames.push("#{repo.name}.git")
608 | 
609 | 
610 |
614 |
615 | 616 |
617 |

This repo was missing some field we require

618 |
620 |       else
621 |         console.log 'Bad Repo', repo.name, 'is missing an attribute..'
622 | 
623 | 
624 |
628 |
629 | 630 |
631 |

Call .exists on each repo name

632 |
634 |     async.reject repoNames, @git.exists.bind(@git), ( results )=>
635 | 
636 | 
637 |
641 |
642 | 643 |
644 |

If we have repo's that need to be created:

645 |
647 |       if results.length > 0
648 | 
649 | 
650 |
654 |
655 | 656 |
657 |

Create each repo that doesn not exist:

658 |
660 |         console.log('Creating repo directory: ', repo ) for repo in results
661 | 
662 | 
663 |
667 |
668 | 669 |
670 |

call .create on each repo:

671 |
673 |         async.map results, @git.create.bind(@git), callback
674 |       else callback() # Otherwise, open up the server.
675 |   
676 |   
677 |   
678 |   
679 | 
680 | 
681 |
685 |
686 | 687 |
688 |
689 |

When the git fetch command is triggered, this is fired.

690 |
691 |
692 |
693 |
694 |
Params
695 |
696 | fetch 697 | Object 698 | Git object from pushover module. 699 |
700 |
701 |
702 |
704 |   onFetch: ( fetch )=>
705 |     @log 'Got a FETCH call for', fetch.repo
706 |     repo = @getRepo fetch.repo
707 |     if repo isnt false # if this repo actually exists:
708 | 
709 | 
710 |
714 |
715 | 716 |
717 |

This repo allows anyone to fetch it, so accept the request:

718 |
720 |       if repo.anonRead is true
721 |         @checkTriggers 'fetch', repo
722 |         fetch.accept()
723 | 
724 | 
725 |
729 |
730 | 731 |
732 |

this repo has no anon access, so we need to check the user/pass

733 |
735 |       else
736 |         @processSecurity fetch, 'fetch', repo
737 |     else # otherwise we need to reject this
738 |       @log 'Rejected - Repo',fetch.repo,'doesnt exist'
739 |       fetch.reject(500,'This repo doesnt exist')
740 |   
741 |   
742 |   
743 |   
744 | 
745 | 
746 |
750 |
751 | 752 |
753 |
754 |

When the git push command is triggered, this is fired.

755 |
756 |
757 |
758 |
759 |
Params
760 |
761 | push 762 | Object 763 | Git object from pushover module. 764 |
765 |
766 |
767 |
769 |   onPush: ( push )=>
770 |     @log 'Got a PUSH call for', push.repo
771 |     repo = @getRepo push.repo
772 |     if repo isnt false # if this repo actually exists:
773 |       @processSecurity push, 'push', repo
774 |     else
775 |       @log 'Rejected - Repo',push.repo,'doesnt exist'
776 |       push.reject(500,'This repo doesnt exist')
777 |   
778 |   
779 |   
780 |   
781 | 
782 | 
783 |
787 |
788 | 789 |
790 |
791 |

Check if this repo has onSuccessful triggers

792 |
793 |
794 |
795 |
796 |
Params
797 |
798 | method 799 | String 800 | fetch|push 801 |
802 |
803 | repo 804 | Object 805 | Repo object we are checking 806 |
807 |
808 |
809 |
811 |   checkTriggers: ( method, repo )=>
812 | 
813 | 
814 |
818 |
819 | 820 |
821 |

If .onSuccessful exists:

822 |
824 |     if repo.onSuccessful?
825 | 
826 | 
827 |
831 |
832 | 833 |
834 |

If this method exists in it:

835 |
837 |       if repo.onSuccessful[method]?
838 | 
839 | 
840 |
844 |
845 | 846 |
847 |

log it, and call it

848 |
850 |         @log 'On successful triggered: ', method, 'on',repo.name
851 |         repo.onSuccessful[method]?( repo, method )
852 |   
853 |   
854 |   
855 |   
856 | 
857 | 
858 |
862 |
863 | 864 |
865 |
866 |

Get the user object, check user/pass is correct and it exists in this repo.

867 |
868 |
869 |
870 |
871 |
Params
872 |
873 | username 874 | String 875 | Username to find 876 |
877 |
878 | password 879 | String 880 | Password of the Username 881 |
882 |
883 | repo 884 | Object 885 | Repo object this user should be in. 886 |
887 |
888 |
889 |
891 |   getUser: ( username, password, repo )=>
892 |     for userObject in repo.users
893 | 
894 | 
895 |
899 |
900 | 901 |
902 |

If we found this user, return it

903 |
905 |       return userObject if userObject.user.username is username and userObject.user.password is password
906 |     false # Otherwise, return a false
907 |   
908 |   
909 |   
910 |   
911 | 
912 | 
913 |
917 |
918 | 919 |
920 |
921 |

Get the repo from the array of repos

922 |
923 |
924 |
925 |
926 |
Params
927 |
928 | repoName 929 | String 930 | Name of the repo we are trying to find 931 |
932 |
933 |
934 |
936 |   getRepo: ( repoName )=>
937 |     for repo in @repos
938 | 
939 | 
940 |
944 |
945 | 946 |
947 |

If the repo exists, return it.

948 |
950 |       return repo if repo.name+'.git' is repoName
951 |     false # Otherwise, return a false
952 | 
953 | 
954 | 
955 | 
956 | 
957 |
961 |
962 | 963 |
964 |

Export this as a module:

965 |
967 | module.exports = GitServer
968 | 
969 |
973 |
974 | 975 | 976 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | var GitServer, repoOne, repoTwo, repos, _git; 2 | 3 | GitServer = require('./main.js'); 4 | 5 | /* 6 | ##Create example repo 1 7 | 8 | This repo does NOT allow Anonymous access 9 | */ 10 | 11 | 12 | repoOne = { 13 | name: 'stackdot', 14 | anonRead: false, 15 | users: [ 16 | { 17 | user: { 18 | username: 'demo', 19 | password: 'demo' 20 | }, 21 | permissions: ['R', 'W'] 22 | } 23 | ] 24 | }; 25 | 26 | /* 27 | ##Create example repo 2 28 | 29 | This repo allows Anonymous reading (fetch,clone) access 30 | */ 31 | 32 | 33 | repoTwo = { 34 | name: 'anon', 35 | anonRead: true, 36 | users: [ 37 | { 38 | user: { 39 | username: 'demo2', 40 | password: 'demo2' 41 | }, 42 | permissions: ['R', 'W'] 43 | } 44 | ], 45 | onSuccessful: { 46 | fetch: function() { 47 | return console.log('Successful fetch on "anon" repo'); 48 | }, 49 | push: function() { 50 | return console.log('Success push on "anon" repo'); 51 | } 52 | } 53 | }; 54 | 55 | repos = [repoOne, repoTwo]; 56 | 57 | /* 58 | #Create the GitServer object 59 | 60 | We are passing in `repos` array for the list of Repos we want to run return 61 | We are passing in `true` to enable verbose logging return 62 | We are passing in `/tmp/repos` to specify where the .git repos should live return 63 | We are passing in `7000` for the port to run on ( port 80 requires sudo ) 64 | */ 65 | 66 | options = { 67 | repos: repos, 68 | logging: true, 69 | repoPath: '/tmp/repos', 70 | port: 7000, 71 | httpApi: true 72 | } 73 | 74 | _git = new GitServer(options); 75 | -------------------------------------------------------------------------------- /header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackdot/NodeJS-Git-Server/3be1aa65d428568dcd105d7c9062ab90260b10e1/header.png -------------------------------------------------------------------------------- /header.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackdot/NodeJS-Git-Server/3be1aa65d428568dcd105d7c9062ab90260b10e1/header.psd -------------------------------------------------------------------------------- /lib/api.js: -------------------------------------------------------------------------------- 1 | var url = require('url'); 2 | var path = require('path'); 3 | var connect = require('connect'); 4 | var bodyParser = connect.bodyParser(); 5 | 6 | var api = { 7 | get: {}, 8 | put: {}, 9 | post: {}, 10 | delete: {} 11 | }; 12 | 13 | module.exports = function(req, res, git, next) { 14 | req.path = {}; 15 | req.path.url = url.parse(req.url.toLowerCase()); 16 | req.path.parts = req.path.url.pathname.split("/"); 17 | api.router(req, function(err, isApiCall, fn) { 18 | if(err) { 19 | throw new Error(err); 20 | } 21 | if(!isApiCall) { 22 | next(); 23 | } else { 24 | bodyParser(req, res, function() { 25 | fn(req, res, git, next); 26 | }); 27 | } 28 | }); 29 | } 30 | 31 | api.router = function(req, callback) { 32 | if(req.path.parts[1] !== 'api') { 33 | callback(null, false, null); 34 | return; 35 | } 36 | method = req.method.toLowerCase(); 37 | if(!api[method]) { 38 | callback(null, true, api.method_not_allowed); 39 | return; 40 | } 41 | if(!api[method][req.path.parts[2]]) { 42 | callback(null, true, api.not_found); 43 | return; 44 | } 45 | callback(null, true, api[method][req.path.parts[2]]); 46 | } 47 | 48 | api.method_not_allowed = function(req, res) { 49 | res.writeHead(405, {'Content-Type': 'text/plain'}); 50 | if(req.errmsg) { 51 | if(typeof req.errmsg !== 'string') { 52 | req.errmsg = req.errmsg.toString(); 53 | } 54 | res.end(req.errmsg); 55 | } else { 56 | res.end('Method not allowed'); 57 | } 58 | } 59 | api.not_found = function(req, res) { 60 | res.writeHead(404, {'Content-Type': 'text/plain'}); 61 | if(req.errmsg) { 62 | if(typeof req.errmsg !== 'string') { 63 | req.errmsg = req.errmsg.toString(); 64 | } 65 | res.end(req.errmsg); 66 | } else { 67 | res.end('404, Not found'); 68 | } 69 | 70 | } 71 | api.bad_request = function(req, res) { 72 | res.writeHead(400, {'Content-Type': 'text/plain'}); 73 | if(req.errmsg) { 74 | if(typeof req.errmsg !== 'string') { 75 | req.errmsg = req.errmsg.toString(); 76 | } 77 | res.end(req.errmsg); 78 | } else { 79 | res.end('Bad request'); 80 | } 81 | } 82 | 83 | api.get.repo = function(req, res, git) { 84 | repo = git.getRepo(req.path.parts[3]); 85 | if(repo) { 86 | tmp = repo; 87 | delete tmp.git_events 88 | delete tmp.event 89 | delete tmp.onSuccessful 90 | res.end(JSON.stringify(tmp)); 91 | } else { 92 | api.not_found(req, res); 93 | } 94 | } 95 | 96 | api.post.repo = function(req, res, git) { 97 | try { 98 | req.body.users = JSON.parse(req.body.users); 99 | } catch(e) { 100 | req.errmsg = "Invadid JSON in users array"; 101 | api.bad_request(req, res); 102 | return; 103 | } 104 | git.createRepo(req.body, function(err) { 105 | if(err) { 106 | req.errmsg = err; 107 | api.bad_request(req, res); 108 | return; 109 | } 110 | res.end('Repo created'); 111 | }); 112 | } 113 | 114 | api.delete.repo = function(req, res, git) { 115 | 116 | } 117 | 118 | api.put.repo = function(req, res, git) { 119 | 120 | } 121 | 122 | api.get.user = function(req, res, git) { 123 | var repo = git.getRepo(req.path.parts[3]); 124 | var user = req.path.parts[4]; 125 | 126 | } 127 | 128 | api.post.user = function(req, res, git) { 129 | 130 | } 131 | 132 | api.delete.user = function(req, res, git) { 133 | 134 | } 135 | 136 | api.put.user = function(req, res, git) { 137 | 138 | } -------------------------------------------------------------------------------- /lib/host.js: -------------------------------------------------------------------------------- 1 | var pushover = require('pushover'); 2 | var http = require('http'); 3 | var https = require('https'); 4 | var async = require('async'); 5 | var fs = require('fs'); 6 | var crypto = require('crypto'); 7 | var git_events = require('git-emit'); 8 | var path = require('path'); 9 | var EventEmitter = require('events').EventEmitter 10 | var proxy = require('event-proxy'); 11 | var api = require('./api'); 12 | 13 | GitServer = (function() { 14 | function GitServer(repos, logging, repoLocation, port, certs, enable_http_api) { 15 | var _this = this; 16 | this.repos = repos; 17 | this.logging = logging; 18 | this.repoLocation = repoLocation; 19 | this.port = port; 20 | this.certs = certs; 21 | this.api = enable_http_api; 22 | this.getRepo = function(repoName) { 23 | return GitServer.prototype.getRepo.apply(_this, arguments); 24 | }; 25 | this.getUser = function(username, password, repo) { 26 | return GitServer.prototype.getUser.apply(_this, arguments); 27 | }; 28 | this.checkTriggers = function(method, repo) { 29 | return GitServer.prototype.checkTriggers.apply(_this, arguments); 30 | }; 31 | this.onPush = function(push) { 32 | return GitServer.prototype.onPush.apply(_this, arguments); 33 | }; 34 | this.onFetch = function(fetch) { 35 | return GitServer.prototype.onFetch.apply(_this, arguments); 36 | }; 37 | this.makeReposIfNull = function(callback) { 38 | return GitServer.prototype.makeReposIfNull.apply(_this, arguments); 39 | }; 40 | this.gitListeners = function() { 41 | return GitServer.prototype.gitListeners.apply(_this, arguments); 42 | }; 43 | this.permissableMethod = function(username, password, method, repo, gitObject) { 44 | return GitServer.prototype.permissableMethod.apply(_this, arguments); 45 | }; 46 | this.processSecurity = function(gitObject, method, repo) { 47 | return GitServer.prototype.processSecurity.apply(_this, arguments); 48 | }; 49 | this.log = function() { 50 | return GitServer.prototype.log.apply(_this, arguments); 51 | }; 52 | this.createRepo = function(repo, callback) { 53 | return GitServer.prototype.createRepo.apply(_this, arguments); 54 | }; 55 | this.git = pushover(this.repoLocation, { 56 | autoCreate: false 57 | }); 58 | this.permMap = { 59 | fetch: 'R', 60 | push: 'W' 61 | }; 62 | this.gitListeners(); 63 | this.makeReposIfNull(function() { 64 | _this.bindEvents(function() { 65 | var message, red, reset; 66 | if (_this.certs != null) { 67 | if(_this.api) { 68 | _this.server = https.createServer(_this.certs); 69 | } else { 70 | _this.server = https.createServer(_this.certs, _this.git.handle.bind(_this.git)); 71 | } 72 | } else { 73 | red = '\033[31m'; 74 | reset = '\033[0m'; 75 | message = "WARNING: No SSL certs passed in. Running as HTTP and not HTTPS.\nBe careful, without HTTPS your user/pass will not be encrypted"; 76 | console.log(red + message + reset); 77 | if(_this.api) { 78 | _this.server = http.createServer(); 79 | } else { 80 | _this.server = http.createServer(_this.git.handle.bind(_this.git)); 81 | } 82 | } 83 | if(_this.api) { 84 | _this.registerApiServer(function() { 85 | _this.server.listen(_this.port, function() { 86 | _this.log('Server listening on ', _this.port, '\r'); 87 | }); 88 | }); 89 | } else { 90 | return _this.server.listen(_this.port, function() { 91 | return _this.log('Server listening on ', _this.port, '\r'); 92 | }); 93 | } 94 | }); 95 | }); 96 | } 97 | 98 | GitServer.prototype.registerApiServer = function(callback) { 99 | var self = this; 100 | this.server.on('request', function(req, res) { 101 | api(req, res, self, function() { 102 | self.git.handle(req, res); 103 | }); 104 | }); 105 | callback(); 106 | } 107 | 108 | GitServer.prototype.bindEvents = function(callback) { 109 | var self = this; 110 | for (var i in this.repos) { 111 | this.repos[i].path = path.normalize(this.repoLocation+"/"+this.repos[i].name+".git"); 112 | this.repos[i].git_events = git_events(this.repos[i].path); 113 | this.repos[i].last_commit = {}; 114 | this.repos[i].event = function(repo, update) { 115 | emitters = self.listeners(update.name).length; 116 | self.log("Emitting "+update.name+" event."); 117 | if(emitters < 1 && update.canAbort) { 118 | self.log("No event listeners on "+update.name+". Accepting...."); 119 | update.accept(); 120 | } else { 121 | self.emit(update.name, update, repo); 122 | } 123 | } 124 | var map = { 125 | "post-applypatch": this.repos[i].event, 126 | "post-commit": this.repos[i].event, 127 | "post-checkout": this.repos[i].event, 128 | "post-merge": this.repos[i].event, 129 | "post-receive": this.repos[i].event, 130 | "post-update": this.repos[i].event, 131 | "post-rewrite": this.repos[i].event, 132 | "applypatch-msg": this.repos[i].event, 133 | "pre-applypatch": this.repos[i].event, 134 | "pre-commit": this.repos[i].event, 135 | "prepare-commit-msg": this.repos[i].event, 136 | "commit-msg": this.repos[i].event, 137 | "pre-rebase": this.repos[i].event, 138 | "pre-receive": this.repos[i].event, 139 | "update": this.repos[i].event, 140 | "pre-auto-gc": this.repos[i].event 141 | } 142 | proxy(process, map, this.repos[i].git_events, this.repos[i]); 143 | } 144 | callback(); 145 | } 146 | 147 | GitServer.prototype.createRepo = function(repo, callback) { 148 | if ((repo.name == null) || (repo.anonRead == null)) { 149 | callback(new Error('Not enough details, need atleast .name and .anonRead'), null) 150 | this.log('Not enough details, need atleast .name and .anonRead'); 151 | return false; 152 | } 153 | if (!this.getRepo(repo.name)) { 154 | this.log('Creating repo', repo.name); 155 | this.repos.push(repo); 156 | return this.git.create(repo.name, callback); 157 | } else { 158 | callback(new Error('This repo already exists'), null) 159 | return this.log('This repo already exists'); 160 | } 161 | }; 162 | 163 | GitServer.prototype.log = function() { 164 | var args, key, value; 165 | args = (function() { 166 | var _results; 167 | _results = []; 168 | for (key in arguments) { 169 | value = arguments[key]; 170 | _results.push("" + value); 171 | } 172 | return _results; 173 | }).apply(this, arguments); 174 | if (this.logging) { 175 | return console.log("LOG: ", args.join(' ')); 176 | } 177 | }; 178 | 179 | GitServer.prototype.processSecurity = function(gitObject, method, repo) { 180 | var auth, creds, plain_auth, req, res; 181 | req = gitObject.request; 182 | res = gitObject.response; 183 | auth = req.headers['authorization']; 184 | if (auth === void 0) { 185 | res.statusCode = 401; 186 | res.setHeader('WWW-Authenticate', 'Basic realm="Secure Area"'); 187 | return res.end('Need some creds son'); 188 | } else { 189 | plain_auth = (new Buffer(auth.split(' ')[1], 'base64')).toString(); 190 | creds = plain_auth.split(':'); 191 | return this.permissableMethod(creds[0], creds[1], method, repo, gitObject); 192 | } 193 | }; 194 | 195 | GitServer.prototype.permissableMethod = function(username, password, method, repo, gitObject) { 196 | var user, _ref; 197 | this.log(username, 'is trying to', method, 'on repo:', repo.name, '...'); 198 | user = this.getUser(username, password, repo); 199 | if (user === false) { 200 | this.log(username, 'was rejected as this user doesnt exist, or password is wrong'); 201 | return gitObject.reject(500, 'Wrong username or password'); 202 | } else { 203 | if (_ref = this.permMap[method], user.permissions.indexOf(_ref) >= 0) { 204 | this.log(username, 'Successfully did a', method, 'on', repo.name); 205 | return this.checkTriggers(method, repo, gitObject); 206 | } else { 207 | this.log(username, 'was rejected, no permission to', method, 'on', repo.name); 208 | return gitObject.reject(500, "You dont have these permissions"); 209 | } 210 | } 211 | }; 212 | 213 | GitServer.prototype.gitListeners = function() { 214 | this.git.on('push', this.onPush); 215 | this.git.on('fetch', this.onFetch); 216 | return this.git.on('info', this.onFetch); 217 | }; 218 | 219 | GitServer.prototype.makeReposIfNull = function(callback) { 220 | var repo, repoNames, _i, _len, _ref, 221 | _this = this; 222 | this.log('Making repos if they dont exist'); 223 | repoNames = []; 224 | _ref = this.repos; 225 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 226 | repo = _ref[_i]; 227 | if ((repo.name != null) && (repo.anonRead != null) && (repo.users != null)) { 228 | repoNames.push("" + repo.name + ".git"); 229 | } else { 230 | console.log('Bad Repo', repo.name, 'is missing an attribute..'); 231 | } 232 | } 233 | return async.reject(repoNames, this.git.exists.bind(this.git), function(error, results) { 234 | var _j, _len1; 235 | if (!error && results.length > 0) { 236 | for (_j = 0, _len1 = results.length; _j < _len1; _j++) { 237 | repo = results[_j]; 238 | console.log('Creating repo directory: ', repo); 239 | } 240 | return async.map(results, _this.git.create.bind(_this.git), callback); 241 | } else { 242 | return callback(); 243 | } 244 | }); 245 | }; 246 | 247 | GitServer.prototype.onFetch = function(fetch) { 248 | var repo; 249 | this.log('Got a FETCH call for', fetch.repo); 250 | repo = this.getRepo(fetch.repo); 251 | if (repo !== false) { 252 | if (repo.anonRead === true) { 253 | return this.checkTriggers('fetch', repo, fetch); 254 | this.checkTriggers('fetch', repo, fetch); 255 | return fetch.accept(); 256 | } else { 257 | return this.processSecurity(fetch, 'fetch', repo); 258 | } 259 | } else { 260 | this.log('Rejected - Repo', fetch.repo, 'doesnt exist'); 261 | return fetch.reject(500, 'This repo doesnt exist'); 262 | } 263 | }; 264 | 265 | GitServer.prototype.onPush = function(push) { 266 | var repo; 267 | this.log('Got a PUSH call for', push.repo); 268 | repo = this.getRepo(push.repo); 269 | var data = { 270 | status: push.status, 271 | repo: push.repo, 272 | service: push.service, 273 | cwd: push.cwd, 274 | last: push.last, 275 | commit: push.commit, 276 | evName: push.evName, 277 | branch: push.branch 278 | } 279 | repo.last_commit = data; 280 | if (repo !== false) { 281 | return this.processSecurity(push, 'push', repo); 282 | } else { 283 | this.log('Rejected - Repo', push.repo, 'doesnt exist'); 284 | return push.reject(500, 'This repo doesnt exist'); 285 | } 286 | }; 287 | 288 | GitServer.prototype.checkTriggers = function(method, repo, gitObject) { 289 | var _base; 290 | gitObject.name = method; 291 | gitObject.canAbort = true; 292 | repo.event(repo, gitObject); 293 | }; 294 | 295 | GitServer.prototype.getUser = function(username, password, repo) { 296 | var userObject, _i, _len, _ref; 297 | crypted_password = crypto.createHash('sha1').update(password).digest('hex'); 298 | _ref = repo.users; 299 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 300 | userObject = _ref[_i]; 301 | if (userObject.user.username === username && (userObject.user.password === password || crypted_password === userObject.user.password)) { 302 | return userObject; 303 | } 304 | } 305 | return false; 306 | }; 307 | 308 | GitServer.prototype.getRepo = function(repoName) { 309 | var repo, _i, _len, _ref; 310 | _ref = this.repos; 311 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 312 | repo = _ref[_i]; 313 | if (repo.name + '.git' === repoName || repoName === repo.name) { 314 | return repo; 315 | } 316 | } 317 | return false; 318 | }; 319 | 320 | GitServer.prototype.__proto__ = EventEmitter.prototype; 321 | 322 | return GitServer; 323 | })(); 324 | 325 | module.exports = GitServer; 326 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | var server = require('./lib/host'); 2 | module.exports = function(options) { 3 | if(!options || !options.repos) { 4 | throw new Error("Options must be object with at least .repos property"); 5 | } else { 6 | logging = (options.logging) ? options.logging : false; 7 | repoLocation = (options.repoLocation) ? options.repoLocation : '/tmp/repos'; 8 | port = (options.port) ? options.port : 7000; 9 | certs = (options.certs) ? options.certs : null; 10 | http_api = (options.httpApi) ? options.httpApi : false; 11 | return new server(options.repos, logging, repoLocation, port, certs, http_api); 12 | } 13 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "git-server", 3 | "version": "0.2.1", 4 | "description": "A simple, multi-tenant git server written in NodeJS", 5 | "author": "Quinton Pike <@quinton.pike>", 6 | "contributors": [ 7 | "Quinton Pike ", 8 | "Justas Brazauskas ", 9 | "Gabriel J. Csapo " 10 | ], 11 | "dependencies": { 12 | "async": "^2.0.0", 13 | "cli-listener": "0.0.4", 14 | "cli-table": "^0.3.1", 15 | "commander": "^2.9.0", 16 | "connect": "~2.9.0", 17 | "event-proxy": "0.0.1", 18 | "git-emit": "0.0.0", 19 | "mkdirp": "^0.5.1", 20 | "pushover": "^1.3.6" 21 | }, 22 | "devDependencies": { 23 | "blanket": "~1.1.5", 24 | "chai": "^3.5.0", 25 | "coveralls": "^2.11.14", 26 | "mocha": "^3.1.2", 27 | "mocha-lcov-reporter": "^1.2.0" 28 | }, 29 | "scripts": { 30 | "test": "make test", 31 | "blanket": { 32 | "pattern": "host.js" 33 | } 34 | }, 35 | "keywords": [ 36 | "git", 37 | "github", 38 | "git server", 39 | "git hosting", 40 | "git http" 41 | ], 42 | "repository": { 43 | "type": "git", 44 | "url": "https://github.com/qrpike/NodeJS-Git-Server" 45 | }, 46 | "homepage": "http://qrpike.github.io/NodeJS-Git-Server/host.coffee.html", 47 | "main": "main.js", 48 | "bin": { 49 | "git-server": "cli.js", 50 | "gitserver": "cli.js" 51 | }, 52 | "engines": { 53 | "node": ">=4" 54 | }, 55 | "preferGlobal": true 56 | } 57 | -------------------------------------------------------------------------------- /test/helper.js: -------------------------------------------------------------------------------- 1 | var helper = {}; 2 | 3 | helper.random = function() { 4 | var text = ""; 5 | var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 6 | 7 | for( var i=0; i < 5; i++ ) { 8 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 9 | } 10 | 11 | return text; 12 | } 13 | 14 | 15 | module.exports = helper; -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var exec = require('child_process').exec; 2 | var assert = require('chai').assert; 3 | var expect = require('chai').expect; 4 | var helper = require('./helper'); 5 | var git_server = require('../main'); 6 | 7 | var test_octocat_name = helper.random(); 8 | var test_repo_name = helper.random(); 9 | var server; 10 | var user = { 11 | username: helper.random(), 12 | password: helper.random() 13 | }; 14 | var user2 = { 15 | username: helper.random(), 16 | password: helper.random() 17 | }; 18 | var user3 = { 19 | username: helper.random(), 20 | password: helper.random() 21 | }; 22 | var repo = { 23 | name: helper.random(), 24 | anonRead: true, 25 | users: [{ 26 | user: user, 27 | permissions: ['R', 'W'] 28 | }] 29 | }; 30 | var repo2 = { 31 | name: helper.random(), 32 | anonRead: false, 33 | users: [{ 34 | user: user, 35 | permissions: ['R', 'W'] 36 | }, { 37 | user: user2, 38 | permissions: ['W'] 39 | }, { 40 | user: user3, 41 | permissions: ['R'] 42 | }] 43 | } 44 | var repo3 = { 45 | name: helper.random(), 46 | anonRead: false, 47 | users: [{ 48 | user: user, 49 | permissions: ['R', 'W'] 50 | }] 51 | } 52 | var opts = { 53 | repos: [repo, repo2], 54 | logging: false, 55 | repoLocation: '/tmp/' + helper.random(), 56 | port: 9000, 57 | httpApi: true 58 | }; 59 | 60 | server = new git_server(opts); 61 | 62 | describe('git_server', function() { 63 | it('Should expose a function', function() { 64 | expect(git_server).to.be.a('function'); 65 | }); 66 | 67 | describe('server', function() { 68 | it('Should be an object', function() { 69 | expect(server).to.be.an('object'); 70 | }); 71 | describe('#repos', function() { 72 | it('Should be an Array', function() { 73 | expect(server.repos).to.be.an('array'); 74 | }); 75 | }); 76 | describe('#logging', function() { 77 | it('Should be a boolean', function() { 78 | expect(server.logging).to.be.a('boolean'); 79 | }); 80 | }); 81 | describe('#repoLocation', function() { 82 | it('Should be a string equals to ' + opts.repoLocation, function() { 83 | expect(server.repoLocation).to.be.a('string').and.to.be.equal(opts.repoLocation); 84 | }); 85 | }); 86 | describe('#port', function() { 87 | it('Should be an integer equals to ' + opts.port, function() { 88 | expect(server.port).to.be.a('number').and.to.be.equal(opts.port); 89 | }); 90 | }); 91 | describe('#on()', function() { 92 | it('Should be a function', function() { 93 | expect(server.on).to.be.a('function'); 94 | }); 95 | }); 96 | describe('#getRepo()', function() { 97 | it('Should be a function and return repo object', function() { 98 | expect(server.getRepo).to.be.a('function'); 99 | expect(server.getRepo(repo.name + ".git")).to.be.an('object').and.to.have.any.keys('name', 'anonRead', 'users'); 100 | }); 101 | }); 102 | describe('#getUser()', function() { 103 | it('Should be a function and return user object', function() { 104 | expect(server.getUser).to.be.a('function'); 105 | expect(server.getUser(user.username, user.password, repo)).to.be.an('object').and.to.have.any.keys('user'); 106 | expect(server.getUser(user.username, user.password, repo).user).to.be.an('object').and.to.have.any.keys('username', 'password') 107 | }); 108 | }); 109 | describe('#checkTriggers()', function() { 110 | it('Should be a function', function() { 111 | expect(server.checkTriggers).to.be.a('function'); 112 | }); 113 | }); 114 | describe('#onPush()', function() { 115 | it('Should be a function', function() { 116 | expect(server.onPush).to.be.a('function'); 117 | }); 118 | }); 119 | describe('#onFetch()', function() { 120 | it('Should be a function', function() { 121 | expect(server.onFetch).to.be.a('function'); 122 | }); 123 | }); 124 | describe('#makeReposIfNull()', function() { 125 | it('Should be a function', function() { 126 | expect(server.makeReposIfNull).to.be.a('function'); 127 | }); 128 | }); 129 | describe('#gitListeners()', function() { 130 | it('Should be a function', function() { 131 | expect(server.gitListeners).to.be.a('function'); 132 | }); 133 | }); 134 | describe('#permissableMethod()', function() { 135 | it('Should be a function', function() { 136 | expect(server.permissableMethod).to.be.a('function'); 137 | }); 138 | }); 139 | describe('#processSecurity()', function() { 140 | it('Should be a function', function() { 141 | expect(server.processSecurity).to.be.a('function'); 142 | }); 143 | }); 144 | describe('#log()', function() { 145 | it('Should be a function', function() { 146 | expect(server.log).to.be.a('function'); 147 | }); 148 | it('Should log an empty line', function(done) { 149 | logging = server.logging; 150 | log = console.log; 151 | server.logging = true; 152 | global.console.log = function() { 153 | server.logging = logging; 154 | global.console.log = log; 155 | expect(arguments[0]).to.be.a('string').and.to.be.eql("LOG: "); 156 | done(); 157 | } 158 | server.log(""); 159 | }); 160 | }); 161 | describe('#createRepo()', function() { 162 | it('Should be a function', function() { 163 | expect(server.createRepo).to.be.a('function'); 164 | }); 165 | it('Should create a repo', function(done) { 166 | server.createRepo(repo3, done); 167 | }); 168 | it('Should not create a repo', function(done) { 169 | repo4 = repo3; 170 | delete repo4.anonRead; 171 | server.createRepo(repo4, function(err, success) { 172 | expect(err).to.not.equal(""); 173 | done(); 174 | }); 175 | }); 176 | it('Should not create a repo, because this repo should exist', function(done) { 177 | server.createRepo(repo, function(err, success) { 178 | expect(err).to.not.equal(""); 179 | done(); 180 | }); 181 | }); 182 | }); 183 | describe('#git', function() { 184 | it('Should be an object', function() { 185 | expect(server.git).to.be.an('object'); 186 | expect(server.git).to.have.any.keys('dirMap', 'autoCreate', 'checkout'); 187 | }); 188 | }); 189 | describe('#permMap', function() { 190 | it('Should be an object', function() { 191 | expect(server.permMap).to.be.an('object').and.to.be.eql({ 192 | fetch: 'R', 193 | push: 'W' 194 | }); 195 | }); 196 | }); 197 | describe('#server', function() { 198 | it('Should be an object', function() { 199 | expect(server.server).to.be.an('object'); 200 | }); 201 | }); 202 | }); 203 | 204 | describe('behaviour', function() { 205 | describe('Clone a Spoon-Knife repo', function() { 206 | it('Should clone a repo', function(done) { 207 | exec('git clone --progress https://github.com/octocat/Spoon-Knife.git /tmp/' + test_octocat_name, function(error, stdout, stderr) { 208 | expect(stdout).to.be.a('string'); 209 | expect(stderr).to.be.a('string').and.to.contain('Checking connectivity... done.'); 210 | done(error); 211 | }); 212 | }); 213 | }); 214 | describe('Events', function() { 215 | describe('Abortable events', function() { 216 | describe('Fetch', function() { 217 | it('Should emit fetch event', function(done) { 218 | server.once('fetch', function(update, repo) { 219 | expect(repo).to.be.an('object').and.to.have.any.keys('name', 'anonRead', 'users'); 220 | expect(update).to.be.an('object').and.to.have.any.keys('canAbort'); 221 | expect(update.accept).to.be.a('function'); 222 | expect(update.reject).to.be.a('function'); 223 | expect(update.canAbort).to.be.a('boolean').and.to.be.equal(true); 224 | update.reject(); 225 | done(); 226 | }); 227 | exec('cd /tmp/' + test_octocat_name + ' && git push http://' + user.username + ':' + user.password + '@localhost:' + server.port + '/' + repo.name + '.git master', function(error, stdout, stderr) { 228 | expect(stdout).to.be.a('string'); 229 | expect(stderr).to.be.a('string'); 230 | }); 231 | }); 232 | }); 233 | describe('Pre-receive', function() { 234 | it('Should emit pre-receive event', function(done) { 235 | server.once('pre-receive', function(update, repo) { 236 | expect(repo).to.be.an('object').and.to.have.any.keys('name', 'anonRead', 'users'); 237 | expect(update).to.be.an('object'); 238 | expect(update.accept).to.be.a('function'); 239 | expect(update.reject).to.be.a('function'); 240 | expect(update.canAbort).to.be.a('boolean').and.to.be.equal(true); 241 | update.reject(); 242 | done(); 243 | }); 244 | exec('cd /tmp/' + test_octocat_name + ' && git push http://' + user.username + ':' + user.password + '@localhost:' + server.port + '/' + repo.name + '.git master', function(error, stdout, stderr) { 245 | expect(stdout).to.be.a('string'); 246 | expect(stderr).to.be.a('string'); 247 | }); 248 | }); 249 | }); 250 | describe('Update', function() { 251 | it('Should emit update event', function(done) { 252 | server.once('update', function(update, repo) { 253 | expect(repo).to.be.an('object').and.to.have.any.keys('name', 'anonRead', 'users'); 254 | expect(update).to.be.an('object'); 255 | expect(update.accept).to.be.a('function'); 256 | expect(update.reject).to.be.a('function'); 257 | expect(update.canAbort).to.be.a('boolean').and.to.be.equal(true); 258 | update.reject(); 259 | done(); 260 | }); 261 | exec('cd /tmp/' + test_octocat_name + ' && git push http://' + user.username + ':' + user.password + '@localhost:' + server.port + '/' + repo.name + '.git master', function(error, stdout, stderr) { 262 | expect(stdout).to.be.a('string'); 263 | expect(stderr).to.be.a('string'); 264 | }); 265 | }); 266 | }); 267 | describe('Push', function() { 268 | it('Should emit push event', function(done) { 269 | server.once('push', function(update, repo) { 270 | expect(repo).to.be.an('object').and.to.have.any.keys('name', 'anonRead', 'users'); 271 | expect(update).to.be.an('object').and.to.have.any.keys('canAbort'); 272 | expect(update.accept).to.be.a('function'); 273 | expect(update.reject).to.be.a('function'); 274 | expect(update.canAbort).to.be.a('boolean').and.to.be.equal(true); 275 | update.reject(); 276 | done(); 277 | }); 278 | exec('cd /tmp/' + test_octocat_name + ' && git push http://' + user.username + ':' + user.password + '@localhost:' + server.port + '/' + repo.name + '.git master', function(error, stdout, stderr) { 279 | expect(stdout).to.be.a('string'); 280 | expect(stderr).to.be.a('string'); 281 | }); 282 | }); 283 | }); 284 | }); 285 | describe('Passive events', function() { 286 | describe('Post-receive', function() { 287 | it('Should emit post-receive event', function(done) { 288 | server.once('post-receive', function(update, repo) { 289 | expect(repo).to.be.an('object').and.to.have.any.keys('name', 'anonRead', 'users'); 290 | expect(update).to.be.an('object').and.to.have.any.keys('canAbort') 291 | expect(update.canAbort).to.be.a('boolean').and.to.be.equal(false); 292 | done(); 293 | }); 294 | exec('cd /tmp/' + test_octocat_name + ' && git push http://' + user.username + ':' + user.password + '@localhost:' + server.port + '/' + repo.name + '.git master', function(error, stdout, stderr) { 295 | expect(stdout).to.be.a('string'); 296 | expect(stderr).to.be.a('string'); 297 | }); 298 | }); 299 | }); 300 | describe('Post-update', function() { 301 | it('Should emit post-update event', function(done) { 302 | server.once('post-update', function(update, repo) { 303 | expect(repo).to.be.an('object').and.to.have.any.keys('name', 'anonRead', 'users');; 304 | expect(update).to.be.an('object').and.to.have.any.keys('canAbort'); 305 | expect(update.canAbort).to.be.a('boolean').and.to.be.equal(false); 306 | done(); 307 | }); 308 | exec('cd /tmp/' + test_octocat_name + ' && git push http://' + user.username + ':' + user.password + '@localhost:' + server.port + '/' + repo.name + '.git master', function(error, stdout, stderr) { 309 | expect(stdout).to.be.a('string'); 310 | expect(stderr).to.be.a('string'); 311 | }); 312 | }); 313 | }); 314 | }); 315 | }); 316 | describe('Push', function() { 317 | describe('Authenticated', function() { 318 | it('Should push Spoon-Knife repo to ' + repo.name + ' repo', function(done) { 319 | exec('cd /tmp/' + test_octocat_name + ' && git push http://' + user.username + ':' + user.password + '@localhost:' + server.port + '/' + repo2.name + '.git master', function(error, stdout, stderr) { 320 | expect(stdout).to.be.a('string'); 321 | expect(stderr).to.be.a('string'); 322 | done(error); 323 | }); 324 | }); 325 | }); 326 | describe('Anonymously', function() { 327 | it('Should try to push Spoon-Knife repo anonymously to ' + repo2.name + ' repo and fail', function(done) { 328 | exec('cd /tmp/' + test_octocat_name + ' && git push http://localhost:' + server.port + '/' + repo2.name + '.git master', function(error, stdout, stderr) { 329 | expect(stdout).to.be.a('string'); 330 | expect(stderr).to.be.a('string').and.not.to.be.equal(''); 331 | expect(error).to.not.be.null; 332 | done(); 333 | }); 334 | }); 335 | }); 336 | describe('No write permissions', function() { 337 | it('Should try to push Spoon-Knife repo with lack of write permissions to ' + repo2.name + ' repo and fail', function(done) { 338 | exec('cd /tmp/' + test_octocat_name + ' && git push http://' + user3.username + ':' + user3.password + '@localhost:' + server.port + '/' + repo2.name + '.git master', function(error, stdout, stderr) { 339 | expect(stdout).to.be.a('string'); 340 | expect(stderr).to.be.a('string'); 341 | done(error); 342 | }); 343 | }); 344 | }); 345 | }); 346 | describe('Fetch', function() { 347 | describe('Anonymously', function() { 348 | it('Should fetch a local repo anonymously', function(done) { 349 | exec('cd /tmp/' + test_octocat_name + ' && git fetch http://localhost:' + server.port + '/' + repo.name + '.git', function(error, stdout, stderr) { 350 | expect(stdout).to.be.a('string'); 351 | expect(stderr).to.be.a('string'); 352 | done(error); 353 | }); 354 | }); 355 | it('Should fetch a local repo anonymously and fail', function(done) { 356 | exec('cd /tmp/' + test_octocat_name + ' && git fetch http://localhost:' + server.port + '/' + repo2.name + '.git', function(error, stdout, stderr) { 357 | expect(stdout).to.be.a('string'); 358 | expect(stderr).to.be.a('string').and.not.to.be.equal(''); 359 | expect(error).to.not.be.null; 360 | done(); 361 | }); 362 | }); 363 | }); 364 | describe('Non existent Repo', function() { 365 | it('Should try to fetch non existing repo', function(done) { 366 | exec('git fetch http://localhost:' + server.port + '/' + helper.random() + '.git /tmp/' + test_octocat_name, function(error, stdout, stderr) { 367 | expect(stdout).to.be.a('string'); 368 | expect(stderr).to.be.a('string').and.not.to.be.equal(''); 369 | expect(error).to.not.be.null; 370 | done(); 371 | }); 372 | }); 373 | }); 374 | }); 375 | describe('Clone', function() { 376 | describe('Anonymously', function() { 377 | it('Should clone a local repo anonymously', function(done) { 378 | exec('git clone --progress http://localhost:' + server.port + '/' + repo.name + '.git /tmp/' + helper.random(), function(error, stdout, stderr) { 379 | expect(stdout).to.be.a('string'); 380 | expect(stderr).to.be.a('string').and.to.contain('Checking connectivity... done.'); 381 | done(error); 382 | }); 383 | }); 384 | }); 385 | describe('Authenticated', function() { 386 | it('Should clone a local repo with autentication', function(done) { 387 | exec('git clone --progress http://' + user.username + ':' + user.password + '@localhost:' + server.port + '/' + repo.name + '.git /tmp/' + helper.random(), function(error, stdout, stderr) { 388 | expect(stdout).to.be.a('string'); 389 | expect(stderr).to.be.a('string').and.to.contain('Checking connectivity... done.'); 390 | done(error); 391 | }); 392 | }); 393 | }); 394 | describe('Wrong credentials', function() { 395 | it('Should try clone a local repo with wrong credentials', function(done) { 396 | exec('git clone --progress http://' + helper.random() + ':' + helper.random() + '@localhost:' + server.port + '/' + repo2.name + '.git /tmp/' + helper.random(), function(error, stdout, stderr) { 397 | expect(stdout).to.be.a('string'); 398 | expect(stderr).to.be.a('string').and.to.contain('fatal: unable to access'); 399 | done(); 400 | }); 401 | }); 402 | }); 403 | describe('No read permission', function() { 404 | it('Should try clone a local repo with lack of read permissions', function(done) { 405 | exec('git clone http://' + user2.password + ':' + user2.username + '@localhost:' + server.port + '/' + repo2.name + '.git /tmp/' + helper.random(), function(error, stdout, stderr) { 406 | expect(stdout).to.be.a('string'); 407 | expect(stderr).to.be.a('string').and.not.to.be.equal(''); 408 | expect(error).to.not.be.null; 409 | done(); 410 | }); 411 | }); 412 | }); 413 | }); 414 | }); 415 | }); 416 | --------------------------------------------------------------------------------