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
32 If you don't want git2cl to invoke git log internally, you can use it
34 It needs a git log generated with --pretty --numstat and --summary.
35 You can use it as follows:
37 git log --pretty --numstat --summary | git2cl > ChangeLog
41 Convert git logs to GNU ChangeLog format.
43 The tool invokes `git log` internally unless you pipe a log to it.
44 Thus, typically you would use it as follows:
48 Output format specification:
49 <http://www.gnu.org/prep/standards/html_node/Change-Logs.html>
53 git2cl is developed by Simon Josefsson <simon@josefsson.org>
54 and Luis Mondesi <lemsx1@gmail.com>
59 use POSIX
qw(strftime);
60 use Text
::Wrap
qw(wrap);
62 use open qw
/:std :utf8/;
64 use constant EMPTY_LOG_MESSAGE
=> '*** empty log message ***';
66 # this is a helper hash for stptime.
67 # Assumes you are calling 'git log ...' with LC_ALL=C
83 my $fh = new FileHandle
;
87 return undef if not defined $str;
89 # we are parsing this format
90 # Fri Oct 26 00:42:56 2007 -0400
92 # sec, min, hour, mday, mon, year, wday = -1, yday = -1, isdst = -1
93 # Luis Mondesi <lemsx1@gmail.com>
95 if ($str =~ /([[:alpha:]]{3})\s+([[:alpha:]]{3})\s+([[:digit:]]{1,2})\s+([[:digit:]]{1,2}):([[:digit:]]{1,2}):([[:digit:]]{1,2})\s+([[:digit:]]{4})/){
96 push(@date,$6,$5,$4,$3,$month{$2},($7 - 1900),-1,-1,-1);
98 die ("Cannot parse date '$str'\n'");
104 my ($indent1, $indent2, @text) = @_;
105 # If incoming text looks preformatted, don't get clever
106 my $text = Text
::Wrap
::wrap
($indent1, $indent2, @text);
107 if ( grep /^\s+/m, @text ) {
110 my @lines = split /\n/, $text;
111 $indent2 =~ s!^((?: {8})+)!"\t" x (length($1)/8)!e;
112 $lines[0] =~ s/^$indent1\s+/$indent1/;
113 s/^$indent2\s+/$indent2/
114 for @lines[1..$#lines];
115 my $newtext = join "\n", @lines;
117 if substr($text, -1) eq "\n";
122 my $files_list = shift;
123 my @lines = split (/\n/, $files_list);
124 my $last_line = pop (@lines);
125 return length ($last_line);
128 # A custom wrap function, sensitive to some common constructs used in
131 my $text = shift; # The text to wrap.
132 my $left_pad_str = shift; # String to pad with on the left.
134 # These do NOT take left_pad_str into account:
135 my $length_remaining = shift; # Amount left on current line.
136 my $max_line_length = shift; # Amount left for a blank line.
138 my $wrapped_text = ''; # The accumulating wrapped entry.
139 my $user_indent = ''; # Inherited user_indent from prev line.
141 my $first_time = 1; # First iteration of the loop?
142 my $suppress_line_start_match = 0; # Set to disable line start checks.
144 my @lines = split (/\n/, $text);
145 while (@lines) # Don't use `foreach' here, it won't work.
147 my $this_line = shift (@lines);
150 if ($this_line =~ /^(\s+)/) {
157 # If it matches any of the line-start regexps, print a newline now...
158 if ($suppress_line_start_match)
160 $suppress_line_start_match = 0;
162 elsif (($this_line =~ /^(\s*)\*\s+[a-zA-Z0-9]/)
163 || ($this_line =~ /^(\s*)\* [a-zA-Z0-9_\.\/\
+-]+/)
164 || ($this_line =~ /^(\s*)\([a-zA-Z0-9_\.\/\
+-]+(\
)|,\s
*)/)
165 || ($this_line =~ /^(\s+)(\S+)/)
166 || ($this_line =~ /^(\s*)- +/)
167 || ($this_line =~ /^()\s*$/)
168 || ($this_line =~ /^(\s*)\*\) +/)
169 || ($this_line =~ /^(\s*)[a-zA-Z0-9](\)|\.|\:) +/))
171 $length_remaining = $max_line_length - (length ($user_indent));
174 # Now that any user_indent has been preserved, strip off leading
175 # whitespace, so up-folding has no ugly side-effects.
176 $this_line =~ s/^\s*//;
178 # Accumulate the line, and adjust parameters for next line.
179 my $this_len = length ($this_line);
182 # Blank lines should cancel any user_indent level.
184 $length_remaining = $max_line_length;
186 elsif ($this_len >= $length_remaining) # Line too long, try breaking it.
188 # Walk backwards from the end. At first acceptable spot, break
190 my $idx = $length_remaining - 1;
191 if ($idx < 0) { $idx = 0 };
194 if (substr ($this_line, $idx, 1) =~ /\s/)
196 my $line_now = substr ($this_line, 0, $idx);
197 my $next_line = substr ($this_line, $idx);
198 $this_line = $line_now;
200 # Clean whitespace off the end.
203 # The current line is ready to be printed.
204 $this_line .= "\n${left_pad_str}";
206 # Make sure the next line is allowed full room.
207 $length_remaining = $max_line_length - (length ($user_indent));
209 # Strip next_line, but then preserve any user_indent.
210 $next_line =~ s/^\s*//;
212 # Sneak a peek at the user_indent of the upcoming line, so
213 # $next_line (which will now precede it) can inherit that
214 # indent level. Otherwise, use whatever user_indent level
215 # we currently have, which might be none.
216 my $next_next_line = shift (@lines);
217 if ((defined ($next_next_line)) && ($next_next_line =~ /^(\s+)/)) {
218 $next_line = $1 . $next_line if (defined ($1));
219 # $length_remaining = $max_line_length - (length ($1));
220 $next_next_line =~ s/^\s*//;
223 $next_line = $user_indent . $next_line;
225 if (defined ($next_next_line)) {
226 unshift (@lines, $next_next_line);
228 unshift (@lines, $next_line);
230 # Our new next line might, coincidentally, begin with one of
231 # the line-start regexps, so we temporarily turn off
232 # sensitivity to that until we're past the line.
233 $suppress_line_start_match = 1;
245 # We bottomed out because the line is longer than the
246 # available space. But that could be because the space is
247 # small, or because the line is longer than even the maximum
248 # possible space. Handle both cases below.
250 if ($length_remaining == ($max_line_length - (length ($user_indent))))
252 # The line is simply too long -- there is no hope of ever
253 # breaking it nicely, so just insert it verbatim, with
254 # appropriate padding.
255 $this_line = "\n${left_pad_str}${this_line}";
259 # Can't break it here, but may be able to on the next round...
260 unshift (@lines, $this_line);
261 $length_remaining = $max_line_length - (length ($user_indent));
262 $this_line = "\n${left_pad_str}";
266 else # $this_len < $length_remaining, so tack on what we can.
268 # Leave a note for the next iteration.
269 $length_remaining = $length_remaining - $this_len;
271 if ($this_line =~ /\.$/)
274 $length_remaining -= 2;
276 else # not a sentence end
279 $length_remaining -= 1;
283 # Unconditionally indicate that loop has run at least once.
286 $wrapped_text .= "${user_indent}${this_line}";
289 # One last bit of padding.
290 $wrapped_text .= "\n";
292 return $wrapped_text;
302 my $state; # 0-header 1-comment 2-files
307 # if reading from STDIN, we assume that we are
308 # getting git log as input
309 if (-f STDIN
or -l STDIN
or -p STDIN
)
311 #my $dummyfh; # don't care about writing
312 #($fh,$dummyfh) = FileHandle::pipe;
313 $fh->fdopen(*STDIN
, 'r');
317 $fh->open("LC_ALL=C git log --pretty --numstat --summary|")
318 or die("Cannot execute git log...$!\n");
321 while (my $_l = <$fh>) {
322 #print STDERR "debug ($state, " . (@date ? (strftime "%Y-%m-%d", @date) : "") . "): `$_'\n";
324 if ($_l =~ m
,^Author
: (.*),) {
327 if ($_l =~ m
,^Date
: (.*),) {
328 @date = strptime
($1);
330 $state = 1 if ($_l =~ m
,^$, and $author and (@date+0>0));
331 } elsif ($state == 1) {
332 # * modifying our input text is a bad choice
333 # let's make a copy of it first, then we remove spaces
334 # * if we meet a "merge branch" statement, we need to start
335 # over and find a real entry
336 # Luis Mondesi <lemsx1@gmail.com>
339 if ($_s =~ m/^Merge branch/)
344 $comment = $comment . $_s;
345 $state = 2 if ($_l =~ m
,^$,);
346 } elsif ($state == 2) {
347 if ($_l =~ m
,^([0-9]+)\t([0-9]+)\t(.*)$,) {
350 $done = 1 if ($_l =~ m
,^$,);
354 print (strftime
"%Y-%m-%d $author\n\n", @date);
356 my $files = join (", ", @files);
357 $files = mywrap
("\t", "\t", "* $files");
359 if (index($comment, EMPTY_LOG_MESSAGE
) > -1 ) {
360 $comment = "[no log message]\n";
363 my $files_last_line_len = 0;
364 $files_last_line_len = last_line_len
($files) + 1;
365 my $msg = wrap_log_entry
($comment, "\t", 69-$files_last_line_len, 69);
367 $msg =~ s/[ \t]+\n/\n/g;
369 print "$files: $msg\n";
383 print (strftime
"%Y-%m-%d $author\n\n", @date);
384 my $msg = wrap_log_entry
($comment, "\t", 69, 69);
385 $msg =~ s/[ \t]+\n/\n/g;