San Mehat | a430b2b | 2014-09-23 08:30:51 -0700 | [diff] [blame] | 1 | #!/usr/bin/perl |
| 2 | |
| 3 | # Copyright (C) 2007, 2008 Simon Josefsson <simon@josefsson.org> |
| 4 | # Copyright (C) 2007 Luis Mondesi <lemsx1@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@cpan.org> |
| 19 | # Copyright (C) 1999 Karl Fogel <kfogel@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@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@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 | } |