Home | History | Annotate | Download | only in utils
      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