2015-01-25 20:49:49 +01:00
// Dependencies
2015-02-01 19:05:12 +01:00
var Ul = require ( "ul" )
, Fs = require ( "fs" )
2015-01-26 08:56:49 +01:00
, Moment = require ( "moment" )
2015-01-27 15:17:22 +01:00
, CliBox = require ( "cli-box" )
2015-01-25 20:49:49 +01:00
;
2015-01-27 11:44:05 +01:00
// Constants
const STORE _PATH = Ul . USER _DIR + "/.git-stats"
2015-02-08 17:45:23 +01:00
, LEVELS = [
2015-01-27 15:17:22 +01:00
"⬚"
, "▢"
, "▤"
, "▣"
2015-02-01 18:50:18 +01:00
, "■"
2015-01-27 15:17:22 +01:00
]
2015-01-27 16:46:56 +01:00
, DAYS = [
2015-01-27 12:16:22 +01:00
"Sun"
, "Mon"
, "Tue"
, "Wed"
, "Thu"
, "Fri"
, "Sat"
]
2015-01-30 13:55:46 +01:00
, DATE _FORMAT = "MMM D, YYYY"
2015-01-27 11:44:05 +01:00
;
2015-01-25 20:54:02 +01:00
2015-01-25 20:49:49 +01:00
// Constructor
var GitStats = module . exports = { } ;
2015-01-26 08:56:49 +01:00
/ * *
* record
* Records a new commit .
*
* @ name record
* @ function
* @ param { Object } data The commit data containing :
*
2015-01-26 09:47:13 +01:00
* - ` date ` ( String | Date ) : The date object or a string in a format that can be parsed .
2015-01-26 08:56:49 +01:00
* - ` url ` ( String ) : The repository remote url .
* - ` hash ` ( String ) : The commit hash .
*
* @ param { Function } callback The callback function .
2015-02-09 14:11:47 +01:00
* @ return { GitStats } The ` GitStats ` object .
2015-01-26 08:56:49 +01:00
* /
2015-01-25 20:54:02 +01:00
GitStats . record = function ( data , callback ) {
2015-01-26 08:58:03 +01:00
// Validate data
2015-01-26 08:56:49 +01:00
callback = callback || function ( err ) { if ( err ) throw err ; } ;
data = Object ( data ) ;
if ( typeof data . date === "string" ) {
2015-01-26 09:47:13 +01:00
data . date = new Moment ( new Date ( data . date ) ) ;
2015-01-26 08:56:49 +01:00
}
2015-01-26 09:21:40 +01:00
if ( ! data . date || ! /^Moment|Date$/ . test ( data . date . constructor . name ) ) {
2015-02-09 14:11:47 +01:00
callback ( new Error ( "The date field should be a string or a date object." ) ) ;
return GitStats ;
2015-01-26 08:56:49 +01:00
}
if ( typeof data . hash !== "string" || ! data . hash ) {
2015-02-09 14:11:47 +01:00
callback ( new Error ( "Invalid hash." ) ) ;
return GitStats ;
2015-01-26 08:56:49 +01:00
}
if ( typeof data . url !== "string" || ! data . url ) {
2015-02-19 04:21:04 +01:00
callback ( new Error ( "Invalid url field. This commit is not recorded into the git-stats history since you haven't added the remote url. You can import the previous commits using the git-stats-importer tool." ) ) ;
2015-02-09 14:11:47 +01:00
return GitStats ;
2015-01-26 08:56:49 +01:00
}
2015-01-26 08:58:03 +01:00
// Get stats
GitStats . get ( function ( err , stats ) {
2015-01-25 20:54:02 +01:00
stats = stats || { } ;
2015-01-30 13:55:46 +01:00
var day = data . date . format ( DATE _FORMAT )
2015-01-26 08:56:49 +01:00
, today = stats [ day ] = Object ( stats [ day ] )
, repo = today [ data . url ] = Object ( today [ data . url ] )
;
repo [ data . hash ] = { date : data . date } ;
2015-02-01 19:05:12 +01:00
GitStats . save ( stats , callback ) ;
2015-01-25 20:54:02 +01:00
} ) ;
2015-02-09 14:11:47 +01:00
return GitStats ;
2015-01-25 20:54:02 +01:00
} ;
2015-01-26 09:05:46 +01:00
/ * *
* get
* Gets the git stats .
*
* @ name get
* @ function
* @ param { Function } callback The callback function .
2015-02-09 14:11:47 +01:00
* @ return { GitStats } The ` GitStats ` object .
2015-01-26 09:05:46 +01:00
* /
2015-01-27 11:44:05 +01:00
GitStats . get = function ( callback ) {
2015-02-01 19:05:12 +01:00
Fs . readFile ( STORE _PATH , "utf-8" , function ( err , data ) {
2015-02-10 08:41:13 +01:00
if ( err && err . code === "ENOENT" ) {
return GitStats . save ( { } , function ( err ) {
callback ( err , { } ) ;
} ) ;
}
2015-02-01 19:05:12 +01:00
if ( err ) { return callback ( err ) ; }
try {
data = JSON . parse ( data ) ;
} catch ( e ) {
return callback ( e ) ;
}
callback ( null , data ) ;
} ) ;
2015-02-09 14:11:47 +01:00
return GitStats ;
2015-01-27 11:44:05 +01:00
} ;
2015-02-08 19:15:25 +01:00
/ * *
* save
* Saves the provided stats .
*
* @ name save
* @ function
* @ param { Object } stats The stats to be saved .
* @ param { Function } callback The callback function .
2015-02-09 14:11:47 +01:00
* @ return { GitStats } The ` GitStats ` object .
2015-02-08 19:15:25 +01:00
* /
2015-02-01 19:05:12 +01:00
GitStats . save = function ( stats , callback ) {
Fs . writeFile ( STORE _PATH , JSON . stringify ( stats , null , 1 ) , callback ) ;
2015-02-09 14:11:47 +01:00
return GitStats ;
2015-02-08 19:15:25 +01:00
} ;
2015-02-01 19:05:12 +01:00
2015-02-08 19:15:25 +01:00
/ * *
* iterateDays
2015-02-15 19:20:49 +01:00
* Iterate through the days , calling the callback function on each day .
2015-02-08 19:15:25 +01:00
*
* @ name iterateDays
* @ function
* @ param { Object } data An object containing the following fields :
*
* - ` start ` ( Moment ) : A ` Moment ` date object representing the start date ( default : * an year ago * ) .
* - ` end ` ( Moment ) : A ` Moment ` date object representing the end date ( default : * now * ) .
* - ` format ` ( String ) : The format of the date ( default : ` "MMM D, YYYY" ` ) .
*
* @ param { Function } callback The callback function called with the current day formatted ( type : string ) and the ` Moment ` date object .
2015-02-09 14:11:47 +01:00
* @ return { GitStats } The ` GitStats ` object .
2015-02-08 19:15:25 +01:00
* /
2015-01-27 11:44:05 +01:00
GitStats . iterateDays = function ( data , callback ) {
2015-01-25 20:54:02 +01:00
if ( typeof data === "function" ) {
callback = data ;
2015-01-27 11:44:05 +01:00
data = undefined ;
}
// Merge the defaults
data = Ul . merge ( {
end : Moment ( )
, start : Moment ( ) . subtract ( 1 , "years" )
2015-01-30 13:55:46 +01:00
, format : DATE _FORMAT
2015-01-27 11:44:05 +01:00
} , data ) ;
var start = data . start
, end = data . end
2015-02-01 09:45:03 +01:00
, tomrrow = Moment ( end . format ( DATE _FORMAT ) , DATE _FORMAT ) . add ( 1 , "days" )
, endStr = tomrrow . format ( DATE _FORMAT )
2015-01-27 11:44:05 +01:00
, cDay = null
;
2015-02-01 09:45:03 +01:00
while ( start . format ( DATE _FORMAT ) !== endStr ) {
2015-01-27 11:44:05 +01:00
cDay = start . format ( data . format ) ;
2015-01-27 12:07:15 +01:00
callback ( cDay , start ) ;
2015-02-08 19:16:08 +01:00
start . add ( 1 , "days" ) ;
2015-01-25 20:54:02 +01:00
}
2015-02-09 14:11:47 +01:00
return GitStats ;
2015-01-25 20:49:49 +01:00
} ;
2015-01-26 10:27:48 +01:00
2015-02-08 19:15:25 +01:00
/ * *
* graph
* Creates an object with the stats on the provided period ( default : * last year * ) .
*
* @ name graph
* @ function
* @ param { Object } data The object passed to the ` iterateDays ` method .
* @ param { Function } callback The callback function .
2015-02-09 14:11:47 +01:00
* @ return { GitStats } The ` GitStats ` object .
2015-02-08 19:15:25 +01:00
* /
2015-01-26 10:27:48 +01:00
GitStats . graph = function ( data , callback ) {
2015-01-27 11:44:05 +01:00
if ( typeof data === "function" ) {
callback = data ;
data = undefined ;
}
// Get commits
GitStats . get ( function ( err , stats ) {
2015-01-26 10:27:48 +01:00
if ( err ) { return callback ( err ) ; }
2015-01-27 11:44:05 +01:00
var cDayObj = null
, year = { }
2015-01-26 10:27:48 +01:00
;
2015-01-27 11:44:05 +01:00
// Iterate days
GitStats . iterateDays ( data , function ( cDay ) {
2015-01-26 10:27:48 +01:00
cDayObj = year [ cDay ] = {
_ : stats [ cDay ] || { }
, c : 0
} ;
Object . keys ( cDayObj . _ ) . forEach ( function ( c ) {
cDayObj . c += Object . keys ( cDayObj . _ [ c ] ) . length ;
} ) ;
2015-01-27 11:44:05 +01:00
} ) ;
2015-01-26 10:27:48 +01:00
callback ( null , year ) ;
2015-01-27 11:44:05 +01:00
} ) ;
2015-02-09 14:11:47 +01:00
return GitStats ;
2015-01-27 11:44:05 +01:00
} ;
2015-02-08 19:15:25 +01:00
/ * *
* calendar
* Creates the calendar data for the provided period ( default : * last year * ) .
*
* @ name calendar
* @ function
* @ param { Object } data The object passed to the ` graph ` method .
* @ param { Function } callback The callback function .
2015-02-09 14:11:47 +01:00
* @ return { GitStats } The ` GitStats ` object .
2015-02-08 19:15:25 +01:00
* /
2015-01-27 11:44:05 +01:00
GitStats . calendar = function ( data , callback ) {
GitStats . graph ( data , function ( err , graph ) {
if ( err ) { return callback ( err ) ; }
2015-02-01 09:45:03 +01:00
2015-02-01 14:43:45 +01:00
var cal = { total : 0 , days : { } , cStreak : 0 , lStreak : 0 , max : 0 }
2015-01-27 11:44:05 +01:00
, cDay = null
, days = Object . keys ( graph )
2015-02-01 09:45:03 +01:00
, levels = null
, cLevel = 0
2015-01-27 11:44:05 +01:00
;
days . forEach ( function ( c ) {
cDay = graph [ c ] ;
cal . total += cDay . c ;
2015-02-01 14:43:45 +01:00
if ( cDay . c > cal . max ) {
cal . max = cDay . c ;
2015-01-27 11:44:05 +01:00
}
2015-01-27 12:25:25 +01:00
if ( cDay . c > 0 ) {
if ( ++ cal . cStreak > cal . lStreak ) {
cal . lStreak = cal . cStreak ;
}
} else {
cal . cStreak = 0 ;
}
2015-01-27 11:44:05 +01:00
} ) ;
2015-02-10 08:51:15 +01:00
levels = cal . max / ( LEVELS . length * 2 ) ;
2015-01-27 11:44:05 +01:00
days . forEach ( function ( c ) {
cDay = graph [ c ] ;
cal . days [ c ] = {
c : cDay . c
2015-02-01 09:45:03 +01:00
, level : ! levels
2015-02-10 08:51:15 +01:00
? 0 : ( cLevel = Math . round ( cDay . c / levels ) ) >= 4
2015-02-01 09:45:03 +01:00
? 4 : ! cLevel && cDay . c > 0 ? 1 : cLevel
2015-01-27 11:44:05 +01:00
} ;
} ) ;
callback ( null , cal ) ;
} ) ;
2015-02-09 14:11:47 +01:00
return GitStats ;
2015-01-27 11:44:05 +01:00
} ;
2015-02-08 19:15:25 +01:00
/ * *
* ansiCalendar
* Creates the ANSI contributions calendar .
*
* @ name ansiCalendar
* @ function
* @ param { Object } data The object passed to the ` calendar ` method .
* @ param { Function } callback The callback function .
2015-02-09 14:11:47 +01:00
* @ return { GitStats } The ` GitStats ` object .
2015-02-08 19:15:25 +01:00
* /
2015-01-27 11:44:05 +01:00
GitStats . ansiCalendar = function ( data , callback ) {
if ( typeof data === "function" ) {
callback = data ;
data = undefined ;
}
2015-01-27 12:07:15 +01:00
var year = [ ]
2015-02-17 20:11:21 +01:00
, months = new Array ( 52 ) // Stores the months depending on their first week
2015-01-27 12:13:59 +01:00
, cWeek = [ " " , " " , " " , " " , " " , " " , " " ]
2015-02-01 15:19:03 +01:00
, monthHack = "MM"
2015-01-27 12:07:15 +01:00
, sDay = ""
, cDayObj = null
2015-01-27 12:13:59 +01:00
, strYear = ""
2015-02-17 20:11:21 +01:00
, strMonths = ""
2015-01-27 12:13:59 +01:00
, w = 0
, d = 0
2015-02-09 10:51:48 +01:00
, dataClone = {
start : data . start ? Moment ( data . start . format ( DATE _FORMAT ) , DATE _FORMAT ) : null
, end : data . end ? Moment ( data . end . format ( DATE _FORMAT ) , DATE _FORMAT ) : null
}
2015-01-27 12:07:15 +01:00
;
2015-01-27 11:44:05 +01:00
GitStats . calendar ( data , function ( err , cal ) {
if ( err ) { return callback ( err ) ; }
2015-02-09 10:51:48 +01:00
GitStats . iterateDays ( dataClone , function ( cDay , mDay ) {
2015-01-27 12:07:15 +01:00
sDay = mDay . format ( "ddd" ) ;
2015-02-01 15:01:11 +01:00
2015-01-27 12:07:15 +01:00
cDayObj = cal . days [ cDay ] ;
2015-02-01 09:45:03 +01:00
if ( ! cDayObj ) return ;
2015-01-27 12:07:15 +01:00
if ( sDay === "Sun" && Object . keys ( cWeek ) . length ) {
year . push ( cWeek ) ;
2015-01-27 12:13:59 +01:00
cWeek = [ " " , " " , " " , " " , " " , " " , " " ] ;
2015-01-27 12:07:15 +01:00
}
2015-02-17 20:11:21 +01:00
// Store the new month this week
if ( mDay . format ( "D" ) === "1" ) {
months [ year . length ] = mDay . format ( "MMM" ) ;
}
2015-02-08 17:45:23 +01:00
cWeek [ DAYS . indexOf ( sDay ) ] = LEVELS [ cDayObj . level ] ;
2015-01-27 12:07:15 +01:00
} ) ;
if ( cWeek . length ) {
year . push ( cWeek ) ;
}
2015-01-27 12:13:59 +01:00
for ( d = 0 ; d < 7 ; ++ d ) {
for ( w = 0 ; w < year . length ; ++ w ) {
strYear += " " + year [ w ] [ d ] ;
}
strYear += "\n" ;
}
2015-02-01 15:01:11 +01:00
// Add day names
2015-01-27 15:17:22 +01:00
strYear = strYear . split ( "\n" ) . map ( function ( c , i ) {
if ( i > 6 ) { return ; }
2015-01-27 16:46:56 +01:00
return DAYS [ i ] + c ;
2015-01-27 12:16:22 +01:00
} ) . join ( "\n" ) ;
2015-01-27 12:13:59 +01:00
2015-02-17 20:11:21 +01:00
// Months label
monthHack = "MMMM" ; //Left padding
2015-02-18 10:29:01 +01:00
for ( var i = 0 ; i < months . length ; i ++ ) {
// The length of strMonths should always be 2*(i+1) (at the i-th column)
if ( ! months [ i ] ) {
strMonths += new Array ( 2 * ( i + 1 ) - strMonths . length + 1 ) . join ( " " ) ;
} else {
strMonths += months [ i ] ;
}
2015-02-17 20:11:21 +01:00
}
2015-02-18 10:29:01 +01:00
2015-02-17 20:11:21 +01:00
strYear = monthHack + strMonths + "\n" + strYear ;
2015-01-27 12:25:25 +01:00
strYear +=
2015-02-01 14:36:26 +01:00
new Array ( 5 + 2 * Math . ceil ( 365 / 7 ) ) . join ( "-" )
+ "\n" + "Contributions in the last year: " + cal . total
+ " | " + "Longest Streak: " + cal . lStreak + " days"
+ " | " + "Current Streak: " + cal . cStreak + " days"
2015-02-01 14:43:45 +01:00
+ " | " + "Max a day: " + cal . max
2015-01-27 12:25:25 +01:00
;
2015-01-27 15:17:22 +01:00
strYear = new CliBox ( {
w : 10
, h : 10
, marks : {
nw : "╔"
, n : "═"
, ne : "╗"
, e : "║"
, se : "╝"
, s : "═"
, sw : "╚"
, w : "║"
, b : " "
}
} , {
text : strYear
, stretch : true
, hAlign : "left"
2015-02-01 15:01:11 +01:00
} ) . toString ( ) ;
2015-02-01 15:19:03 +01:00
strYear = strYear . replace ( monthHack , new Array ( monthHack . length + 1 ) . join ( " " ) ) ;
2015-01-27 15:17:22 +01:00
2015-02-01 15:01:11 +01:00
callback ( null , strYear ) ;
2015-01-26 10:27:48 +01:00
} ) ;
2015-02-09 14:11:47 +01:00
return GitStats ;
2015-01-26 10:27:48 +01:00
} ;