gith
Version:
gith[ooks] - a simple node server that responds to github post-receive events with meaningful data
299 lines (247 loc) • 7.93 kB
JavaScript
/*
* gith
* https://github.com/danheberden/gith
*
* Copyright (c) 2012 Dan Heberden
* Licensed under the MIT license.
*/
var http = require("http");
var EventEmitter2 = require( "eventemitter2" ).EventEmitter2;
var util = require("util");
var _ = require("lodash");
var querystring = require("querystring");
var filterSettings = function( settings, payload ) {
if ( !payload ) {
return false;
}
settings = settings || {};
// add the matches object for later
payload.matches = {};
// check all the things
var checksPassed = true;
[ 'repo', 'branch', 'file', 'tag' ].forEach( function( thing ) {
var wat = settings[ thing ];
// default to a passed state
var passed = true;
// was a filter specified? and is it not a wildcard?
if ( wat && wat !== '*' ) {
// checking, so default passed to false
passed = false;
// make an array of the thing or all the files
var checks = [].concat( thing === 'file' ? payload.files.all : payload[ thing ] );
// all the checks - did any of them pass?
passed = checks.some( function( check ) {
// direct match
if ( wat === check ) {
return true;
}
// if negated match (!string)
if ( _.isString( wat ) && wat[0] === "!" &&
wat.slice(1) !== check ) {
return true;
}
// regex?
if ( _.isRegExp( wat ) ) {
var match = check.match( wat );
// did it match? huh? did it?
if ( match ) {
// goddamn files being different
if ( thing === 'file' ) {
if ( !payload.matches.files ) {
payload.matches.files = {};
}
payload.matches.files[ check ] = match;
} else {
payload.matches[ thing ] = match;
}
return true;
}
}
});
// usr function?
if ( _.isFunction( wat ) ) {
passed = wat( payload[ thing ], payload );
}
// assign the final result of this 'thing' to checksPassed
checksPassed = passed && checksPassed;
}
});
return checksPassed;
};
// Used by exports.module.create's returned function to create
// new gith objects that hold settings and emit events
var Gith = function( eventaur, settings ) {
var gith = this;
this.settings = settings || {};
// make this bad boy an event emitter
EventEmitter2.call( this, {
delimiter: ':',
maxListeners: 0
});
// handle bound payloads
eventaur.on( 'payload', function( originalPayload ) {
// make a simpler payload
var payload = gith.simplifyPayload( originalPayload );
// bother doing anything?
if ( filterSettings( settings, payload ) ) {
// all the things
gith.emit( 'all', payload );
// did we do any branch work?
if ( originalPayload.created && originalPayload.forced && payload.branch ) {
gith.emit( 'branch:add', payload );
}
if ( originalPayload.deleted && originalPayload.forced && payload.branch ) {
gith.emit( 'branch:delete', payload );
}
// how about files?
if ( payload.files.added.length > 0 ) {
gith.emit( 'file:add', payload );
}
if ( payload.files.deleted.length > 0 ) {
gith.emit( 'file:delete', payload );
}
if ( payload.files.modified.length > 0 ) {
gith.emit( 'file:modify', payload );
}
if ( payload.files.all.length > 0 ) {
gith.emit( 'file:all', payload );
}
// tagging?
if ( payload.tag && originalPayload.created ) {
gith.emit( 'tag:add', payload );
}
if ( payload.tag && originalPayload.deleted ) {
gith.emit( 'tag:delete', payload );
}
}
});
};
// inherit the EventEmitter2 stuff
util.inherits( Gith, EventEmitter2 );
// expose the simpliyPayload method on gith()
Gith.prototype.simplifyPayload = function( payload ) {
payload = payload || {};
var branch = '';
var tag = '';
var rRef = /refs\/(tags|heads)\/(.*)$/;
// break out if it was a tag or branch and assign
var refMatches = ( payload.ref || "" ).match( rRef );
if ( refMatches ) {
if ( refMatches[1] === "heads" ) {
branch = refMatches[2];
}
if ( refMatches[1] === "tags" ) {
tag = refMatches[2];
}
}
// if branch wasn't found, use base_ref if available
if ( !branch && payload.base_ref ) {
branch = payload.base_ref.replace( rRef, '$2' );
}
var simpler = {
original: payload,
files: {
all: [],
added: [],
deleted: [],
modified: []
},
tag: tag,
branch: branch,
repo: payload.repository ? (payload.repository.owner.name + '/' + payload.repository.name) : null,
sha: payload.after,
time: payload.repository ? payload.repository.pushed_at : null,
urls: {
head: payload.head_commit ? payload.head_commit.url : '',
branch: '',
tag: '',
repo: payload.repository ? payload.repository.url : null,
compare: payload.compare
},
reset: !payload.created && payload.forced,
pusher: payload.pusher ? payload.pusher.name : null,
owner: (payload.repository && payload.repository.owner) ? payload.repository.owner.name : null
};
if ( branch ) {
simpler.urls.branch = simpler.urls.branch + '/tree/' + branch;
}
if ( tag ) {
simpler.urls.tag = simpler.urls.head;
}
// populate files for every commit
(payload.commits || []).forEach( function( commit ) {
// github label and simpler label ( make 'removed' deleted to be consistant )
_.each( { added: 'added', modified: 'modified', removed: 'deleted' }, function( s, g ) {
simpler.files[ s ] = simpler.files[ s ].concat( commit[ g ] );
simpler.files.all = simpler.files.all.concat( commit[ g ] );
});
});
return simpler;
// todo: use github api to find what files were removed if the
// head was reset? maybe?
};
// the listen method - this gets added/bound in
// module.exports.create, fyi
var listen = function( eventaur, port ) {
// are we changing ports?
if ( port ) {
this.port = port;
}
if ( !this.port ) {
throw new Error('.listen() requires a port to be set');
}
this.server = http.createServer( function( req, res ) {
var data = "";
if ( req.method === "POST" ) {
req.on( "data", function( chunk ) {
data += chunk;
});
}
req.on( "end", function() {
if ( /^payload=/.test( data ) ) {
var payload = JSON.parse( querystring.unescape(data.slice(8)) );
eventaur.emit( "payload", payload );
res.writeHead( 200, {
'Content-type': 'text/html'
});
}
res.end();
});
}).listen( port );
};
// make require('gith')( 9001 ) work if someone really wants to
module.exports = function( port ) {
return module.exports.create( port );
};
// make the preferred way of `require('gith').create( 9001 ) work
module.exports.create = function( port ) {
// make an event emitter to use for the hardcore stuff
var eventaur = new EventEmitter2({
delimter: ':',
maxListeners: 0
});
// return a function that
// a) holds its own server/port/whatever
// b) exposes a listen method
// c) is a function that returns a new Gith object
var ret = function( map ) {
// make a new Gith with a reference to this factory
return new Gith( eventaur, map );
};
// add the listen method to the function - bind to ret
// and send eventaur to it
ret.listen = listen.bind( ret, eventaur );
// expose ability to close http server
ret.close = function(){
this.server.close();
};
ret.payload = function( payload ) {
eventaur.emit( 'payload', payload );
};
// if create was sent port, call listen automatically
if ( port ) {
ret.listen( port );
}
// return the new function
return ret;
};