Files
taimed/node_modules/git-tools/git-tools.js
2025-07-24 17:21:45 +08:00

391 lines
8.1 KiB
JavaScript

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;