var spawn = require( "spawnback" ); function extend( a, b ) { for ( var prop in b ) { a[ prop ] = b[ prop ]; } return a; } function copy( obj ) { return extend( {}, obj ); } function Repo( path ) { this.path = path; } Repo.parsePerson = (function() { var rPerson = /^(\S+)\s(.+)$/; return function( person ) { var matches = rPerson.exec( person ); return { email: matches[ 1 ], name: matches[ 2 ] }; }; })(); Repo.clone = function( options, callback ) { var dir = options.dir; var args = [ "clone", options.repo, dir ]; options = copy( options ); delete options.repo; delete options.dir; Object.keys( options ).forEach(function( option ) { args.push( "--" + option ); var value = options[ option ]; if ( value !== true ) { args.push( value ); } }); args.push(function( error ) { if ( error ) { return callback( error ); } callback( null, new Repo( dir ) ); }); var repo = new Repo( process.cwd() ); repo.exec.apply( repo, args ); }; Repo.isRepo = function( path, callback ) { var repo = new Repo( path ); repo.isRepo( callback ); }; Repo.prototype.exec = function() { var args = [].slice.call( arguments ); var callback = args.pop(); spawn( "git", args, { cwd: this.path }, function( error, stdout ) { if ( error ) { return callback( error ); } // Remove trailing newline stdout = stdout.replace( /\n$/, "" ); callback( null, stdout ); }); }; Repo.prototype.activeDays = function( committish, callback ) { if ( !callback ) { callback = committish; committish = "master"; } this.exec( "log", "--format=%at", committish, function( error, dates ) { if ( error ) { return callback( error ); } var dateMap = { activeDays: 0, commits: 0, dates: {}, years: {} }; dates.split( "\n" ).sort().forEach(function( timestamp ) { var date = new Date( timestamp * 1000 ); var year = date.getFullYear(); var month = date.getMonth() + 1; var day = date.getDate(); date = year + "-" + (month < 10 ? "0" : "") + month + "-" + (day < 10 ? "0" : "") + day; if ( !dateMap.dates[ date ] ) { dateMap.dates[ date ] = 0; } dateMap.commits++; dateMap.dates[ date ]++; if ( !dateMap.years[ year ] ) { dateMap.years[ year ] = { activeDays: 0, commits: 0, months: {} }; } dateMap.years[ year ].commits++; if ( !dateMap.years[ year ].months[ month ] ) { dateMap.years[ year ].months[ month ] = { activeDays: 0, commits: 0, days: {} }; } dateMap.years[ year ].months[ month ].commits++; if ( !dateMap.years[ year ].months[ month ].days[ day ] ) { dateMap.years[ year ].months[ month ].days[ day ] = { commits: 0 }; dateMap.activeDays++; dateMap.years[ year ].activeDays++; dateMap.years[ year ].months[ month ].activeDays++; } dateMap.years[ year ].months[ month ].days[ day ].commits++; }); callback( null, dateMap ); }); }; Repo.prototype.age = function( callback ) { this.exec( "log", "--reverse", "--format=%cr", function( error, stdout ) { if ( error ) { return callback( error ); } callback( null, stdout.split( "\n" )[ 0 ].replace( /\sago/, "" ) ); }); }; Repo.prototype.authors = function( committish, callback ) { if ( !callback ) { callback = committish; committish = "master"; } this.exec( "log", "--format=%aE %aN", committish, function( error, data ) { if ( error ) { return callback( error ); } // Logs on a boundary commit will have no output var authors = data.length ? data.split( "\n" ) : []; var authorMap = {}; var totalCommits = 0; authors.forEach(function( author ) { if ( !authorMap[ author ] ) { authorMap[ author ] = 0; } authorMap[ author ]++; totalCommits++; }); authors = Object.keys( authorMap ).map(function( author ) { var commits = authorMap[ author ]; return extend( Repo.parsePerson( author ), { commits: commits, commitsPercent: (commits * 100 / totalCommits).toFixed( 1 ) }); }).sort(function( a, b ) { return b.commits - a.commits; }); callback( null, authors ); }); }; Repo.prototype.blame = function( options, callback ) { var args = [ "blame", "-s" ]; if ( options.committish ) { args.push( options.committish ); } args.push( "--", options.path ); var rBlame = /^(\^?\w+)(\s(\S+))?\s+(\d+)\)\s(.*)$/; args.push(function( error, blame ) { if ( error ) { return callback( error ); } var lines = blame.split( /\r?\n/ ); lines = lines.map(function( line ) { var matches = rBlame.exec( line ); var commit = matches[ 1 ]; var boundary = /^\^/.test( commit ); if ( boundary ) { commit = commit.substring( 1 ); } return { commit: matches[ 1 ], boundary: boundary, path: matches[ 3 ] || options.path, lineNumber: parseInt( matches[ 4 ], 10 ), content: matches[ 5 ] }; }); callback( null, lines ); }); this.exec.apply( this, args ); }; Repo.prototype.branches = function( callback ) { this.exec( "for-each-ref", "--format=" + "%(refname:short)%0a" + "%(authordate:rfc2822)%0a" + "%(authoremail) %(authorname)%0a" + "%(subject)%0a" + "%(objectname)%0a", "refs/heads", function( error, data ) { if ( error ) { return callback( error ); } var branches = data.split( "\n\n" ).map(function( branch ) { var lines = branch.split( "\n" ); var name = lines[ 0 ]; var date = new Date( lines[ 1 ] ); var author = Repo.parsePerson( lines[ 2 ] ); var subject = lines[ 3 ]; var sha = lines[ 4 ]; return { name: name, sha: sha, date: date, subject: subject, author: author }; }).sort(function( a, b ) { return b.date - a.date; }); callback( null, branches ); }); }; Repo.prototype.config = function( name, callback ) { this.exec( "config", "--get", name, function( error, stdout ) { if ( error ) { if ( /^Command failed:\s+$/.test( error.message ) ) { return callback( null, null ); } return callback( error ); } callback( null, stdout.trim() ); }); }; Repo.prototype.currentBranch = function( callback ) { this.exec( "rev-parse", "--abbrev-ref", "HEAD", function( error, data ) { if ( error ) { return callback( error ); } var branch = data === "HEAD" ? null : data; callback( null, branch ); }); }; Repo.prototype.isRepo = function( callback ) { this.exec( "rev-parse", "--git-dir", function( error ) { if ( error ) { if ( error.message.indexOf( "Not a git repository" ) ) { return callback( null, false ); } // If the path doesn't exist, don't return an error if ( error.code === "ENOENT" ) { return callback( null, false ); } return callback( error ); } callback( null, true ); }); }; Repo.prototype.remotes = function( callback ) { this.exec( "remote", "-v", function( error, data ) { if ( error ) { return callback( error ); } var remotes = data.split( "\n" ); var rRemote = /^(\S+)\s(\S+)/; var remoteMap = {}; remotes.forEach(function( remote ) { var matches = rRemote.exec( remote ); // New repositories with no remotes will have `origin` but no URL if ( !matches ) { return; } var name = matches[ 1 ]; var url = matches[ 2 ]; remoteMap[ name ] = url; }); remotes = Object.keys( remoteMap ).map(function( remote ) { return { name: remote, url: remoteMap[ remote ] }; }); callback( null, remotes ); }); }; Repo.prototype.resolveCommittish = function( committish, callback ) { this.exec( "rev-parse", committish, callback ); }; Repo.prototype.tags = function( callback ) { this.exec( "for-each-ref", "--format=" + "%(refname:short)%0a" + "%(authordate)%(taggerdate)%0a" + "%(objectname)%0a", "refs/tags", function( error, data ) { if ( error ) { return callback( error ); } if ( !data ) { return callback( null, [] ); } var tags = data.split( "\n\n" ).map(function( tag ) { var lines = tag.split( "\n" ); var name = lines[ 0 ]; var date = new Date( lines[ 1 ] ); var sha = lines[ 2 ]; return { name: name, sha: sha, date: date }; }).sort(function( a, b ) { return b.date - a.date; }); callback( null, tags ); }); }; module.exports = Repo;