3 # Copyright (C) 2007-2024 Simon Josefsson <simon@josefsson.org>
4 # Copyright (C) 2007 Luis Mondesi <lemsx1@gmail.com>
6 # The functions mywrap, last_line_len, wrap_log_entry are derived from
7 # the GPLv2+ cvs2cl tool, see <http://www.red-bean.com/cvs2cl/>:
8 # Copyright (C) 2001,2002,2003,2004 Martyn J. Pearce <fluffy@cpan.org>
9 # Copyright (C) 1999 Karl Fogel <kfogel@red-bean.com>
11 # This program is free software: you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation, either version 3 of the License, or
14 # (at your option) any later version.
16 # This program is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
21 # You should have received a copy of the GNU General Public License
22 # along with this program. If not, see <https://www.gnu.org/licenses/>.
26 git2cl - tool to convert git logs to GNU ChangeLog
34 Convert git logs to GNU ChangeLog format.
36 If you don't want git2cl to invoke git log internally, you can use it
37 as a pipe. It needs a git log generated with --pretty --numstat and
38 --summary. You can use it as follows:
40 git log --pretty --numstat --summary | git2cl > ChangeLog
44 Output format specification:
45 <http://www.gnu.org/prep/standards/html_node/Change-Logs.html>
49 git2cl is developed by Simon Josefsson <simon@josefsson.org>
50 and Luis Mondesi <lemsx1@gmail.com>
55 use POSIX
qw(strftime);
56 use Text
::Wrap
qw(wrap);
58 use open qw
/:std :utf8/;
60 use constant EMPTY_LOG_MESSAGE
=> '*** empty log message ***';
62 # this is a helper hash for stptime.
63 # Assumes you are calling 'git log ...' with LC_ALL=C
79 my $fh = new FileHandle
;
83 return undef if not defined $str;
85 # we are parsing this format
86 # Fri Oct 26 00:42:56 2007 -0400
88 # sec, min, hour, mday, mon, year, wday = -1, yday = -1, isdst = -1
89 # Luis Mondesi <lemsx1@gmail.com>
91 if ($str =~ /([[:alpha:]]{3})\s+([[:alpha:]]{3})\s+([[:digit:]]{1,2})\s+([[:digit:]]{1,2}):([[:digit:]]{1,2}):([[:digit:]]{1,2})\s+([[:digit:]]{4})/){
92 push(@date,$6,$5,$4,$3,$month{$2},($7 - 1900),-1,-1,-1);
94 die ("Cannot parse date '$str'\n'");
100 my ($indent1, $indent2, @text) = @_;
101 # If incoming text looks preformatted, don't get clever
102 my $text = Text
::Wrap
::wrap
($indent1, $indent2, @text);
103 if ( grep /^\s+/m, @text ) {
106 my @lines = split /\n/, $text;
107 $indent2 =~ s!^((?: {8})+)!"\t" x (length($1)/8)!e;
108 $lines[0] =~ s/^$indent1\s+/$indent1/;
109 s/^$indent2\s+/$indent2/
110 for @lines[1..$#lines];
111 my $newtext = join "\n", @lines;
113 if substr($text, -1) eq "\n";
118 my $files_list = shift;
119 my @lines = split (/\n/, $files_list);
120 my $last_line = pop (@lines);
121 return length ($last_line);
124 # A custom wrap function, sensitive to some common constructs used in
127 my $text = shift; # The text to wrap.
128 my $left_pad_str = shift; # String to pad with on the left.
130 # These do NOT take left_pad_str into account:
131 my $length_remaining = shift; # Amount left on current line.
132 my $max_line_length = shift; # Amount left for a blank line.
134 my $wrapped_text = ''; # The accumulating wrapped entry.
135 my $user_indent = ''; # Inherited user_indent from prev line.
137 my $first_time = 1; # First iteration of the loop?
138 my $suppress_line_start_match = 0; # Set to disable line start checks.
140 my @lines = split (/\n/, $text);
141 while (@lines) # Don't use `foreach' here, it won't work.
143 my $this_line = shift (@lines);
146 if ($this_line =~ /^(\s+)/) {
153 # If it matches any of the line-start regexps, print a newline now...
154 if ($suppress_line_start_match)
156 $suppress_line_start_match = 0;
158 elsif (($this_line =~ /^(\s*)\*\s+[a-zA-Z0-9]/)
159 || ($this_line =~ /^(\s*)\* [a-zA-Z0-9_\.\/\
+-]+/)
160 || ($this_line =~ /^(\s*)\([a-zA-Z0-9_\.\/\
+-]+(\
)|,\s
*)/)
161 || ($this_line =~ /^(\s+)(\S+)/)
162 || ($this_line =~ /^(\s*)- +/)
163 || ($this_line =~ /^()\s*$/)
164 || ($this_line =~ /^(\s*)\*\) +/)
165 || ($this_line =~ /^(\s*)[a-zA-Z0-9](\)|\.|\:) +/))
167 $length_remaining = $max_line_length - (length ($user_indent));
170 # Now that any user_indent has been preserved, strip off leading
171 # whitespace, so up-folding has no ugly side-effects.
172 $this_line =~ s/^\s*//;
174 # Accumulate the line, and adjust parameters for next line.
175 my $this_len = length ($this_line);
178 # Blank lines should cancel any user_indent level.
180 $length_remaining = $max_line_length;
182 elsif ($this_len >= $length_remaining) # Line too long, try breaking it.
184 # Walk backwards from the end. At first acceptable spot, break
186 my $idx = $length_remaining - 1;
187 if ($idx < 0) { $idx = 0 };
190 if (substr ($this_line, $idx, 1) =~ /\s/)
192 my $line_now = substr ($this_line, 0, $idx);
193 my $next_line = substr ($this_line, $idx);
194 $this_line = $line_now;
196 # Clean whitespace off the end.
199 # The current line is ready to be printed.
200 $this_line .= "\n${left_pad_str}";
202 # Make sure the next line is allowed full room.
203 $length_remaining = $max_line_length - (length ($user_indent));
205 # Strip next_line, but then preserve any user_indent.
206 $next_line =~ s/^\s*//;
208 # Sneak a peek at the user_indent of the upcoming line, so
209 # $next_line (which will now precede it) can inherit that
210 # indent level. Otherwise, use whatever user_indent level
211 # we currently have, which might be none.
212 my $next_next_line = shift (@lines);
213 if ((defined ($next_next_line)) && ($next_next_line =~ /^(\s+)/)) {
214 $next_line = $1 . $next_line if (defined ($1));
215 # $length_remaining = $max_line_length - (length ($1));
216 $next_next_line =~ s/^\s*//;
219 $next_line = $user_indent . $next_line;
221 if (defined ($next_next_line)) {
222 unshift (@lines, $next_next_line);
224 unshift (@lines, $next_line);
226 # Our new next line might, coincidentally, begin with one of
227 # the line-start regexps, so we temporarily turn off
228 # sensitivity to that until we're past the line.
229 $suppress_line_start_match = 1;
241 # We bottomed out because the line is longer than the
242 # available space. But that could be because the space is
243 # small, or because the line is longer than even the maximum
244 # possible space. Handle both cases below.
246 if ($length_remaining == ($max_line_length - (length ($user_indent))))
248 # The line is simply too long -- there is no hope of ever
249 # breaking it nicely, so just insert it verbatim, with
250 # appropriate padding.
251 $this_line = "\n${left_pad_str}${this_line}";
255 # Can't break it here, but may be able to on the next round...
256 unshift (@lines, $this_line);
257 $length_remaining = $max_line_length - (length ($user_indent));
258 $this_line = "\n${left_pad_str}";
262 else # $this_len < $length_remaining, so tack on what we can.
264 # Leave a note for the next iteration.
265 $length_remaining = $length_remaining - $this_len;
267 if ($this_line =~ /\.$/)
270 $length_remaining -= 2;
272 else # not a sentence end
275 $length_remaining -= 1;
279 # Unconditionally indicate that loop has run at least once.
282 $wrapped_text .= "${user_indent}${this_line}";
285 # One last bit of padding.
286 $wrapped_text .= "\n";
288 return $wrapped_text;
298 my $state; # 0-header 1-comment 2-files
303 # if reading from STDIN, we assume that we are
304 # getting git log as input
305 if (-f STDIN
or -l STDIN
or -p STDIN
)
307 #my $dummyfh; # don't care about writing
308 #($fh,$dummyfh) = FileHandle::pipe;
309 $fh->fdopen(*STDIN
, 'r');
313 $fh->open("LC_ALL=C git log --pretty --numstat --summary|")
314 or die("Cannot execute git log...$!\n");
317 while (my $_l = <$fh>) {
318 #print STDERR "debug ($state, " . (@date ? (strftime "%Y-%m-%d", @date) : "") . "): `$_'\n";
320 if ($_l =~ m
,^Author
: (.*),) {
323 if ($_l =~ m
,^Date
: (.*),) {
324 @date = strptime
($1);
326 $state = 1 if ($_l =~ m
,^$, and $author and (@date+0>0));
327 } elsif ($state == 1) {
328 # * modifying our input text is a bad choice
329 # let's make a copy of it first, then we remove spaces
330 # * if we meet a "merge branch" statement, we need to start
331 # over and find a real entry
332 # Luis Mondesi <lemsx1@gmail.com>
335 if ($_s =~ m/^Merge branch/)
340 $comment = $comment . $_s;
341 $state = 2 if ($_l =~ m
,^$,);
342 } elsif ($state == 2) {
343 if ($_l =~ m
,^([0-9]+)\t([0-9]+)\t(.*)$,) {
346 $done = 1 if ($_l =~ m
,^$,);
350 print (strftime
"%Y-%m-%d $author\n\n", @date);
352 my $files = join (", ", @files);
353 $files = mywrap
("\t", "\t", "* $files");
355 if (index($comment, EMPTY_LOG_MESSAGE
) > -1 ) {
356 $comment = "[no log message]\n";
359 my $files_last_line_len = 0;
360 $files_last_line_len = last_line_len
($files) + 1;
361 my $msg = wrap_log_entry
($comment, "\t", 69-$files_last_line_len, 69);
363 $msg =~ s/[ \t]+\n/\n/g;
365 print "$files: $msg\n";
379 print (strftime
"%Y-%m-%d $author\n\n", @date);
380 my $msg = wrap_log_entry
($comment, "\t", 69, 69);
381 $msg =~ s/[ \t]+\n/\n/g;