1 #!/usr/bin/perl 2 3 # Copyright (C) 2007, 2008 Simon Josefsson <simon (at] josefsson.org> 4 # Copyright (C) 2007 Luis Mondesi <lemsx1 (at] gmail.com> 5 # * calls git directly. To use it just: 6 # cd ~/Project/my_git_repo; git2cl > ChangeLog 7 # * implements strptime() 8 # * fixes bugs in $comment parsing 9 # - copy input before we remove leading spaces 10 # - skip "merge branch" statements as they don't 11 # have information about files (i.e. we never 12 # go into $state 2) 13 # - behaves like a pipe/filter if input is given from the CLI 14 # else it calls git log by itself 15 # 16 # The functions mywrap, last_line_len, wrap_log_entry are derived from 17 # the cvs2cl tool, see <http://www.red-bean.com/cvs2cl/>: 18 # Copyright (C) 2001,2002,2003,2004 Martyn J. Pearce <fluffy (at] cpan.org> 19 # Copyright (C) 1999 Karl Fogel <kfogel (at] red-bean.com> 20 # 21 # git2cl is free software; you can redistribute it and/or modify it 22 # under the terms of the GNU General Public License as published by 23 # the Free Software Foundation; either version 2, or (at your option) 24 # any later version. 25 # 26 # git2cl is distributed in the hope that it will be useful, but 27 # WITHOUT ANY WARRANTY; without even the implied warranty of 28 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 29 # General Public License for more details. 30 # 31 # You should have received a copy of the GNU General Public License 32 # along with git2cl; see the file COPYING. If not, write to the Free 33 # Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 34 # 02111-1307, USA. 35 36 use strict; 37 use POSIX qw(strftime); 38 use Text::Wrap qw(wrap); 39 use FileHandle; 40 41 use constant EMPTY_LOG_MESSAGE => '*** empty log message ***'; 42 43 # this is a helper hash for stptime. 44 # Assumes you are calling 'git log ...' with LC_ALL=C 45 my %month = ( 46 'Jan'=>0, 47 'Feb'=>1, 48 'Mar'=>2, 49 'Apr'=>3, 50 'May'=>4, 51 'Jun'=>5, 52 'Jul'=>6, 53 'Aug'=>7, 54 'Sep'=>8, 55 'Oct'=>9, 56 'Nov'=>10, 57 'Dec'=>11, 58 ); 59 60 my $fh = new FileHandle; 61 62 sub key_ready 63 { 64 my ($rin, $nfd); 65 vec($rin, fileno(STDIN), 1) = 1; 66 return $nfd = select($rin, undef, undef, 0); 67 } 68 69 sub strptime { 70 my $str = shift; 71 return undef if not defined $str; 72 73 # we are parsing this format 74 # Fri Oct 26 00:42:56 2007 -0400 75 # to these fields 76 # sec, min, hour, mday, mon, year, wday = -1, yday = -1, isdst = -1 77 # Luis Mondesi <lemsx1 (at] gmail.com> 78 my @date; 79 if ($str =~ /([[:alpha:]]{3})\s+([[:alpha:]]{3})\s+([[:digit:]]{1,2})\s+([[:digit:]]{1,2}):([[:digit:]]{1,2}):([[:digit:]]{1,2})\s+([[:digit:]]{4})/){ 80 push(@date,$6,$5,$4,$3,$month{$2},($7 - 1900),-1,-1,-1); 81 } else { 82 die ("Cannot parse date '$str'\n'"); 83 } 84 return @date; 85 } 86 87 sub mywrap { 88 my ($indent1, $indent2, @text) = @_; 89 # If incoming text looks preformatted, don't get clever 90 my $text = Text::Wrap::wrap($indent1, $indent2, @text); 91 if ( grep /^\s+/m, @text ) { 92 return $text; 93 } 94 my @lines = split /\n/, $text; 95 $indent2 =~ s!^((?: {8})+)!"\t" x (length($1)/8)!e; 96 $lines[0] =~ s/^$indent1\s+/$indent1/; 97 s/^$indent2\s+/$indent2/ 98 for @lines[1..$#lines]; 99 my $newtext = join "\n", @lines; 100 $newtext .= "\n" 101 if substr($text, -1) eq "\n"; 102 return $newtext; 103 } 104 105 sub last_line_len { 106 my $files_list = shift; 107 my @lines = split (/\n/, $files_list); 108 my $last_line = pop (@lines); 109 return length ($last_line); 110 } 111 112 # A custom wrap function, sensitive to some common constructs used in 113 # log entries. 114 sub wrap_log_entry { 115 my $text = shift; # The text to wrap. 116 my $left_pad_str = shift; # String to pad with on the left. 117 118 # These do NOT take left_pad_str into account: 119 my $length_remaining = shift; # Amount left on current line. 120 my $max_line_length = shift; # Amount left for a blank line. 121 122 my $wrapped_text = ''; # The accumulating wrapped entry. 123 my $user_indent = ''; # Inherited user_indent from prev line. 124 125 my $first_time = 1; # First iteration of the loop? 126 my $suppress_line_start_match = 0; # Set to disable line start checks. 127 128 my @lines = split (/\n/, $text); 129 while (@lines) # Don't use `foreach' here, it won't work. 130 { 131 my $this_line = shift (@lines); 132 chomp $this_line; 133 134 if ($this_line =~ /^(\s+)/) { 135 $user_indent = $1; 136 } 137 else { 138 $user_indent = ''; 139 } 140 141 # If it matches any of the line-start regexps, print a newline now... 142 if ($suppress_line_start_match) 143 { 144 $suppress_line_start_match = 0; 145 } 146 elsif (($this_line =~ /^(\s*)\*\s+[a-zA-Z0-9]/) 147 || ($this_line =~ /^(\s*)\* [a-zA-Z0-9_\.\/\+-]+/) 148 || ($this_line =~ /^(\s*)\([a-zA-Z0-9_\.\/\+-]+(\)|,\s*)/) 149 || ($this_line =~ /^(\s+)(\S+)/) 150 || ($this_line =~ /^(\s*)- +/) 151 || ($this_line =~ /^()\s*$/) 152 || ($this_line =~ /^(\s*)\*\) +/) 153 || ($this_line =~ /^(\s*)[a-zA-Z0-9](\)|\.|\:) +/)) 154 { 155 $length_remaining = $max_line_length - (length ($user_indent)); 156 } 157 158 # Now that any user_indent has been preserved, strip off leading 159 # whitespace, so up-folding has no ugly side-effects. 160 $this_line =~ s/^\s*//; 161 162 # Accumulate the line, and adjust parameters for next line. 163 my $this_len = length ($this_line); 164 if ($this_len == 0) 165 { 166 # Blank lines should cancel any user_indent level. 167 $user_indent = ''; 168 $length_remaining = $max_line_length; 169 } 170 elsif ($this_len >= $length_remaining) # Line too long, try breaking it. 171 { 172 # Walk backwards from the end. At first acceptable spot, break 173 # a new line. 174 my $idx = $length_remaining - 1; 175 if ($idx < 0) { $idx = 0 }; 176 while ($idx > 0) 177 { 178 if (substr ($this_line, $idx, 1) =~ /\s/) 179 { 180 my $line_now = substr ($this_line, 0, $idx); 181 my $next_line = substr ($this_line, $idx); 182 $this_line = $line_now; 183 184 # Clean whitespace off the end. 185 chomp $this_line; 186 187 # The current line is ready to be printed. 188 $this_line .= "\n${left_pad_str}"; 189 190 # Make sure the next line is allowed full room. 191 $length_remaining = $max_line_length - (length ($user_indent)); 192 193 # Strip next_line, but then preserve any user_indent. 194 $next_line =~ s/^\s*//; 195 196 # Sneak a peek at the user_indent of the upcoming line, so 197 # $next_line (which will now precede it) can inherit that 198 # indent level. Otherwise, use whatever user_indent level 199 # we currently have, which might be none. 200 my $next_next_line = shift (@lines); 201 if ((defined ($next_next_line)) && ($next_next_line =~ /^(\s+)/)) { 202 $next_line = $1 . $next_line if (defined ($1)); 203 # $length_remaining = $max_line_length - (length ($1)); 204 $next_next_line =~ s/^\s*//; 205 } 206 else { 207 $next_line = $user_indent . $next_line; 208 } 209 if (defined ($next_next_line)) { 210 unshift (@lines, $next_next_line); 211 } 212 unshift (@lines, $next_line); 213 214 # Our new next line might, coincidentally, begin with one of 215 # the line-start regexps, so we temporarily turn off 216 # sensitivity to that until we're past the line. 217 $suppress_line_start_match = 1; 218 219 last; 220 } 221 else 222 { 223 $idx--; 224 } 225 } 226 227 if ($idx == 0) 228 { 229 # We bottomed out because the line is longer than the 230 # available space. But that could be because the space is 231 # small, or because the line is longer than even the maximum 232 # possible space. Handle both cases below. 233 234 if ($length_remaining == ($max_line_length - (length ($user_indent)))) 235 { 236 # The line is simply too long -- there is no hope of ever 237 # breaking it nicely, so just insert it verbatim, with 238 # appropriate padding. 239 $this_line = "\n${left_pad_str}${this_line}"; 240 } 241 else 242 { 243 # Can't break it here, but may be able to on the next round... 244 unshift (@lines, $this_line); 245 $length_remaining = $max_line_length - (length ($user_indent)); 246 $this_line = "\n${left_pad_str}"; 247 } 248 } 249 } 250 else # $this_len < $length_remaining, so tack on what we can. 251 { 252 # Leave a note for the next iteration. 253 $length_remaining = $length_remaining - $this_len; 254 255 if ($this_line =~ /\.$/) 256 { 257 $this_line .= " "; 258 $length_remaining -= 2; 259 } 260 else # not a sentence end 261 { 262 $this_line .= " "; 263 $length_remaining -= 1; 264 } 265 } 266 267 # Unconditionally indicate that loop has run at least once. 268 $first_time = 0; 269 270 $wrapped_text .= "${user_indent}${this_line}"; 271 } 272 273 # One last bit of padding. 274 $wrapped_text .= "\n"; 275 276 return $wrapped_text; 277 } 278 279 # main 280 281 my @date; 282 my $author; 283 my @files; 284 my $comment; 285 286 my $state; # 0-header 1-comment 2-files 287 my $done = 0; 288 289 $state = 0; 290 291 # if reading from STDIN, we assume that we are 292 # getting git log as input 293 if (key_ready()) 294 { 295 296 #my $dummyfh; # don't care about writing 297 #($fh,$dummyfh) = FileHandle::pipe; 298 $fh->fdopen(*STDIN, 'r'); 299 } 300 else 301 { 302 $fh->open("LC_ALL=C git log --pretty --numstat --summary|") 303 or die("Cannot execute git log...$!\n"); 304 } 305 306 while (my $_l = <$fh>) { 307 #print STDERR "debug ($state, " . (@date ? (strftime "%Y-%m-%d", @date) : "") . "): `$_'\n"; 308 if ($state == 0) { 309 if ($_l =~ m,^Author: (.*),) { 310 $author = $1; 311 } 312 if ($_l =~ m,^Date: (.*),) { 313 @date = strptime($1); 314 } 315 $state = 1 if ($_l =~ m,^$, and $author and (@date+0>0)); 316 } elsif ($state == 1) { 317 # * modifying our input text is a bad choice 318 # let's make a copy of it first, then we remove spaces 319 # * if we meet a "merge branch" statement, we need to start 320 # over and find a real entry 321 # Luis Mondesi <lemsx1 (at] gmail.com> 322 my $_s = $_l; 323 $_s =~ s/^ //g; 324 if ($_s =~ m/^Merge branch|^Merge remote branch/) 325 { 326 $state=0; 327 $author=0; 328 next; 329 } 330 $comment = $comment . $_s; 331 $state = 2 if ($_l =~ m,^$,); 332 } elsif ($state == 2) { 333 if ($_l =~ m,^([0-9]+)\t([0-9]+)\t(.*)$,) { 334 push @files, $3; 335 } 336 $done = 1 if ($_l =~ m,^$,); 337 } 338 339 if ($done) { 340 print (strftime "%Y-%m-%d $author\n\n", @date); 341 342 my $files = join (", ", @files); 343 $files = mywrap ("\t", "\t", "* $files"), ": "; 344 345 if (index($comment, EMPTY_LOG_MESSAGE) > -1 ) { 346 $comment = "[no log message]\n"; 347 } 348 349 my $files_last_line_len = 0; 350 $files_last_line_len = last_line_len($files) + 1; 351 my $msg = wrap_log_entry($comment, "\t", 69-$files_last_line_len, 69); 352 353 $msg =~ s/[ \t]+\n/\n/g; 354 355 print "$files: $msg\n"; 356 357 @date = (); 358 $author = ""; 359 @files = (); 360 $comment = ""; 361 362 $state = 0; 363 $done = 0; 364 } 365 } 366 367 if (@date + 0) 368 { 369 print (strftime "%Y-%m-%d $author\n\n", @date); 370 my $msg = wrap_log_entry($comment, "\t", 69, 69); 371 $msg =~ s/[ \t]+\n/\n/g; 372 print "\t* $msg\n"; 373 } 374