├── README.md └── git-score.php /README.md: -------------------------------------------------------------------------------- 1 | # git-score 2 | 3 | *git score* is a script to compute some "scores" for committers in a git repo. Use it for fun or to brag about your involvement in the development of a project. 4 | 5 | This script is inspired by [git-score](https://github.com/msparks/git-score), a python script 6 | 7 | ## Use as a git alias: 8 | 9 | Add this to your git config (eg `~/.gitconfig`) 10 | 11 | ```ini 12 | [alias] 13 | score = "!php /full/path/to/git-score.php" 14 | ``` 15 | 16 | ## Usage 17 | 18 | In a repository, type: 19 | 20 | ```sh 21 | git score 22 | ``` 23 | 24 | This will output something like the following: 25 | 26 | ``` 27 | $ git score 28 | name commits delta (+) (-) files 29 | Ozh 2230 47906 66188 18282 500 30 | Léo Colombaro 145 1038 15438 14400 84 31 | lesterchan 43 553 1366 813 24 32 | Nic Waller 13 322 434 112 5 33 | BestNa.me Labs 12 10 21 11 4 34 | Preovaleo 11 -5 28 33 7 35 | Clayton Daley 9 13 29 16 2 36 | Diftraku 8 0 16 16 8 37 | Audrey 4 10 21 11 4 38 | ``` 39 | 40 | ## License 41 | 42 | Do whatever the hell you want to do with it 43 | -------------------------------------------------------------------------------- /git-score.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | 8 | 9 | namespace Ozh\Git; 10 | 11 | class Score { 12 | 13 | /** 14 | * @var const The git command which output we'll parse 15 | * 16 | * This will output something like: 17 | * 18 | * > ozh 19 | * 625 1747 includes/geo/geoip.inc 20 | * 21 | * > Joe 22 | * 2 0 .travis.yml 23 | * 24 | * > ozh 25 | * 22 0 CHANGELOG.md 26 | * 4 0 js/jquery-2.2.4.min.js 27 | * 28 | */ 29 | CONST GIT_CMD = 'git log --use-mailmap --numstat --pretty=format:"> %aN <%aE>" --no-merges'; 30 | 31 | /** 32 | * @var array Will collect the output of the git command 33 | */ 34 | private $gitlog = array(); 35 | 36 | 37 | /** 38 | * @var array Will eventually contain infos for each commit author 39 | * 40 | * Eventually this array will contain data structured like the following: 41 | * $this->authors = array( 42 | * 'ozh@ozh.org' => array( 43 | * 'name' => 'Ozh', 44 | * 'email' => 'ozh@ozh.org', 45 | * 'commits' => 69, 46 | * 'delta' => 850, 47 | * '(+)' => 1000, 48 | * '(-)' => 150, 49 | * 'files' => 13, 50 | * ), 51 | * ... 52 | * ); 53 | * 54 | */ 55 | private $authors = array(); 56 | 57 | 58 | /** 59 | * @var array Keys for the $authors array 60 | */ 61 | private $keys = array( 62 | 'name', 63 | 'commits', 64 | 'delta', 65 | '(+)', 66 | '(-)', 67 | 'files', 68 | ); 69 | 70 | /** 71 | * @var array Widest column in table output 72 | */ 73 | private $widest = array(); 74 | 75 | 76 | /** 77 | * @param $args Command line arguments as passed by class call, see end of file 78 | */ 79 | public function __construct($args) { 80 | $cmd = self::GIT_CMD; 81 | 82 | // remove first element of cmd line args, as it's the script file itself, and pass arguments to git 83 | array_shift($args); 84 | if (!empty($args)) { 85 | $cmd .= ' ' . implode(' ', $args); 86 | } 87 | 88 | $this->get_raw_gitlog($cmd); 89 | $this->parse_gitlog(); 90 | $this->set_stats_per_author(); 91 | $this->print_stats(); 92 | } 93 | 94 | /** 95 | * Exec git command and collect its output 96 | * 97 | * @param $args Command line arguments as passed by class call, see end of file 98 | */ 99 | public function get_raw_gitlog($cmd) { 100 | $this->gitlog = array(); 101 | 102 | $handle = popen($cmd, 'r'); 103 | while (!feof($handle)) { 104 | $this->gitlog[] = fgets($handle, 4096); 105 | } 106 | pclose($handle); 107 | } 108 | 109 | /** 110 | * Parse raw git output and collect info into the $authors array 111 | */ 112 | public function parse_gitlog() { 113 | $current_author = array(); 114 | 115 | foreach ($this->gitlog as $line) { 116 | 117 | $line = trim($line); 118 | /* 119 | $line can be one this 3 forms: 120 | "> ozh ", 121 | "1337 43 some/file.ext", 122 | "" 123 | */ 124 | 125 | if ($this->is_new_commit($line)) { 126 | $current_author = $this->get_author($line); 127 | 128 | if ($this->is_new_author($current_author['email'])) { 129 | $this->add_empty_author($current_author); 130 | $this->authors[$current_author['email']]['name'] = $current_author['name']; 131 | } 132 | 133 | $this->add_stats_to_author( 134 | $current_author['email'], 135 | array( 136 | 'commits' => 1 137 | ) 138 | ); 139 | 140 | $line = ''; 141 | } 142 | 143 | if ($line) { 144 | $stats = $this->get_stats($line); 145 | 146 | $this->add_stats_to_author( 147 | $current_author['email'], 148 | array( 149 | '(+)' => $stats['added'], 150 | '(-)' => $stats['deleted'], 151 | ) 152 | ); 153 | 154 | $this->add_files_to_author($current_author['email'], $stats['file']); 155 | } 156 | 157 | } 158 | 159 | } 160 | 161 | /** 162 | * Increment values of an author in $authors 163 | * 164 | * @param string $email Email which is a key of $this->authors 165 | * @param array $data Array of $keys=>$values used to increment $keys of $this->authors[$email] with $values 166 | */ 167 | public function add_stats_to_author($email, $data) { 168 | foreach ($data as $key => $value) { 169 | $this->authors[$email][$key] += (int)$value; 170 | } 171 | } 172 | 173 | /** 174 | * Add a value to an array if not already present 175 | * 176 | * @param string $email Email which is a key of $this->authors 177 | * @param string $file Filename to be uniquely added to $this->authors[$email][$files] 178 | */ 179 | public function add_files_to_author($email, $file) { 180 | if (!in_array($file, $this->authors[$email]['files'])) { 181 | $this->authors[$email]['files'][] = $file; 182 | } 183 | } 184 | 185 | /** 186 | * Print git scores 187 | */ 188 | public function print_stats() { 189 | // header 190 | $this->print_line(array_combine($this->keys, $this->keys)); 191 | 192 | // each line 193 | foreach ($this->authors as $author => $data) { 194 | $this->print_line($data); 195 | } 196 | } 197 | 198 | /** 199 | * Print tabular line of data 200 | * 201 | * First cell will be left aligned (padded right with spaces), subsequent cells are right aligned (padded left) 202 | * Line of data to be printed must be an array of keys=>values where keys match $this->keys 203 | * 204 | * @param array $data Array of (key=>values) to print 205 | */ 206 | public function print_line($data) { 207 | $pad = STR_PAD_RIGHT; 208 | foreach ($this->widest as $key => $len) { 209 | echo $this->mb_str_pad($data[$key], $len + 1, ' ', $pad) . ' '; 210 | $pad = STR_PAD_LEFT; 211 | 212 | } 213 | echo PHP_EOL; 214 | } 215 | 216 | /** 217 | * Loop over $this->authors values and compute some stats, also sets the widest value of each key in the $authors array 218 | */ 219 | public function set_stats_per_author() { 220 | 221 | // init widest cells, default value is the length of $keys (ie the header of the table to be printed) 222 | foreach ($this->keys as $key) { 223 | $this->widest[$key] = strlen($key); 224 | } 225 | 226 | // count files & delta, also get max width for each column 227 | foreach ($this->authors as $author => $stats) { 228 | $this->authors[$author]['files'] = $stats['files'] = count($stats['files']); 229 | $this->authors[$author]['delta'] = $stats['(+)'] - $stats['(-)']; 230 | 231 | foreach ($this->keys as $key) { 232 | $this->widest[$key] = $this->get_widest($key, $stats[$key]); 233 | } 234 | } 235 | 236 | // order author bys number of commits 237 | usort($this->authors, array($this, 'compare_by_commits')); 238 | } 239 | 240 | /** 241 | * Compare two values. Used to sort an array 242 | * 243 | * @param array $a Array 244 | * @param array $b Array 245 | * @return int 0, 1 or -1. See usort() 246 | */ 247 | public function compare_by_commits($a, $b) { 248 | return $a['commits'] < $b['commits']; 249 | } 250 | 251 | /** 252 | * Get widest column 253 | * 254 | * @param string $key Key 255 | * @param mixed $key Data (string or integer) 256 | * @return int 257 | */ 258 | public function get_widest($key, $stats) { 259 | return max($this->widest[$key], strlen((string)$stats)); 260 | } 261 | 262 | /** 263 | * Parse line of git output and return number of added lines, number of deleted, and file name 264 | * 265 | * @param string $line Line to parse 266 | * @return array 267 | */ 268 | public function get_stats($line) { 269 | // $line = "1337 43 some/file.ext" 270 | // $line = "- - some/file.bin" 271 | preg_match('/^([\-|\d]+)\s+([\-|\d]+)\s+(.*)$/', $line, $matches); 272 | return array('added' => $matches[1], 'deleted' => $matches[2], 'file' => $matches[3]); 273 | } 274 | 275 | /** 276 | * Parse line of git output and commit author name and email 277 | * 278 | * @param string $line Line to parse 279 | * @return array 280 | */ 281 | public function get_author($line) { 282 | // $line = "> ozh " 283 | preg_match('/^> (.*) <(.*)>$/', $line, $matches); 284 | return array('name' => $matches[1], 'email' => $matches[2]); 285 | } 286 | 287 | /** 288 | * Check if line begins with a '>' 289 | * 290 | * @param string $line Line to parse 291 | * @return bool 292 | */ 293 | public function is_new_commit($line) { 294 | // if line starts with a '>' : we're parsing a new commit 295 | return (strpos($line, '>') === 0); 296 | } 297 | 298 | /** 299 | * Check if a given author email is already registered in the authors array 300 | * 301 | * @param string $email Email 302 | * @return bool True if email isn't already a key of $this->authors, false otherwise 303 | */ 304 | public function is_new_author($email) { 305 | return !array_key_exists($email, $this->authors); 306 | } 307 | 308 | /** 309 | * Add empty array associated to a provided key 310 | * 311 | * The array is initialized with values of zero to allow being used in a loop 312 | * with '+=' without issuing warning. 313 | * 314 | * @param array $author Array of ('email'=>email, 'name'=>name) 315 | */ 316 | public function add_empty_author($author) { 317 | // Init all keys to 0 318 | foreach ($this->keys as $key) { 319 | $this->authors[$author['email']][$key] = 0; 320 | } 321 | // But then overwrite these two to something else 322 | $this->authors[$author['email']]['name'] = $author['name']; 323 | $this->authors[$author['email']]['files'] = array(); 324 | } 325 | 326 | /** 327 | * Multibyte str_pad 328 | * 329 | * @param string $input 330 | * @param int $pad_length 331 | * @param string $pad_string 332 | * @param int $pad_type 333 | * @return string 334 | * @author Kari "Haprog" Sderholm - https://gist.github.com/nebiros/226350 335 | */ 336 | function mb_str_pad($input, $pad_length, $pad_string = ' ', $pad_type = STR_PAD_RIGHT) { 337 | $diff = strlen($input) - mb_strlen($input, 'UTF-8'); 338 | return str_pad($input, $pad_length + $diff, $pad_string, $pad_type); 339 | } 340 | 341 | } 342 | 343 | new Score($argv); 344 | --------------------------------------------------------------------------------