blob: 6a8a1e7e04a1cb1e9a6f3cacfb4e020b8e04230c [file] [log] [blame]
San Mehata430b2b2014-09-23 08:30:51 -07001#!/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
36use strict;
37use POSIX qw(strftime);
38use Text::Wrap qw(wrap);
39use FileHandle;
40
41use 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
45my %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
60my $fh = new FileHandle;
61
62sub key_ready
63{
64 my ($rin, $nfd);
65 vec($rin, fileno(STDIN), 1) = 1;
66 return $nfd = select($rin, undef, undef, 0);
67}
68
69sub 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
87sub 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
105sub 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.
114sub 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
281my @date;
282my $author;
283my @files;
284my $comment;
285
286my $state; # 0-header 1-comment 2-files
287my $done = 0;
288
289$state = 0;
290
291# if reading from STDIN, we assume that we are
292# getting git log as input
293if (key_ready())
294{
295
296 #my $dummyfh; # don't care about writing
297 #($fh,$dummyfh) = FileHandle::pipe;
298 $fh->fdopen(*STDIN, 'r');
299}
300else
301{
302 $fh->open("LC_ALL=C git log --pretty --numstat --summary|")
303 or die("Cannot execute git log...$!\n");
304}
305
306while (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
367if (@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}