Bump license to GPLv3+.
[git2cl.git] / git2cl
blob631b3b5e2f34d6ce2609ac5e4f22745990282821
1 #!/usr/bin/perl
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/>.
24 =head1 NAME
26 git2cl - tool to convert git logs to GNU ChangeLog
28 =head1 SYNOPSIS
30 git2cl > ChangeLog
32 If you don't want git2cl to invoke git log internally, you can use it
33 as a pipe.
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
39 =head1 DESCRIPTION
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:
46 =head1 SEE ALSO
48 Output format specification:
49 <http://www.gnu.org/prep/standards/html_node/Change-Logs.html>
51 =head1 AUTHORS
53 git2cl is developed by Simon Josefsson <simon@josefsson.org>
54 and Luis Mondesi <lemsx1@gmail.com>
56 =cut
58 use strict;
59 use POSIX qw(strftime);
60 use Text::Wrap qw(wrap);
61 use FileHandle;
63 use constant EMPTY_LOG_MESSAGE => '*** empty log message ***';
65 # this is a helper hash for stptime.
66 # Assumes you are calling 'git log ...' with LC_ALL=C
67 my %month = (
68 'Jan'=>0,
69 'Feb'=>1,
70 'Mar'=>2,
71 'Apr'=>3,
72 'May'=>4,
73 'Jun'=>5,
74 'Jul'=>6,
75 'Aug'=>7,
76 'Sep'=>8,
77 'Oct'=>9,
78 'Nov'=>10,
79 'Dec'=>11,
82 my $fh = new FileHandle;
84 sub strptime {
85 my $str = shift;
86 return undef if not defined $str;
88 # we are parsing this format
89 # Fri Oct 26 00:42:56 2007 -0400
90 # to these fields
91 # sec, min, hour, mday, mon, year, wday = -1, yday = -1, isdst = -1
92 # Luis Mondesi <lemsx1@gmail.com>
93 my @date;
94 if ($str =~ /([[:alpha:]]{3})\s+([[:alpha:]]{3})\s+([[:digit:]]{1,2})\s+([[:digit:]]{1,2}):([[:digit:]]{1,2}):([[:digit:]]{1,2})\s+([[:digit:]]{4})/){
95 push(@date,$6,$5,$4,$3,$month{$2},($7 - 1900),-1,-1,-1);
96 } else {
97 die ("Cannot parse date '$str'\n'");
99 return @date;
102 sub mywrap {
103 my ($indent1, $indent2, @text) = @_;
104 # If incoming text looks preformatted, don't get clever
105 my $text = Text::Wrap::wrap($indent1, $indent2, @text);
106 if ( grep /^\s+/m, @text ) {
107 return $text;
109 my @lines = split /\n/, $text;
110 $indent2 =~ s!^((?: {8})+)!"\t" x (length($1)/8)!e;
111 $lines[0] =~ s/^$indent1\s+/$indent1/;
112 s/^$indent2\s+/$indent2/
113 for @lines[1..$#lines];
114 my $newtext = join "\n", @lines;
115 $newtext .= "\n"
116 if substr($text, -1) eq "\n";
117 return $newtext;
120 sub last_line_len {
121 my $files_list = shift;
122 my @lines = split (/\n/, $files_list);
123 my $last_line = pop (@lines);
124 return length ($last_line);
127 # A custom wrap function, sensitive to some common constructs used in
128 # log entries.
129 sub wrap_log_entry {
130 my $text = shift; # The text to wrap.
131 my $left_pad_str = shift; # String to pad with on the left.
133 # These do NOT take left_pad_str into account:
134 my $length_remaining = shift; # Amount left on current line.
135 my $max_line_length = shift; # Amount left for a blank line.
137 my $wrapped_text = ''; # The accumulating wrapped entry.
138 my $user_indent = ''; # Inherited user_indent from prev line.
140 my $first_time = 1; # First iteration of the loop?
141 my $suppress_line_start_match = 0; # Set to disable line start checks.
143 my @lines = split (/\n/, $text);
144 while (@lines) # Don't use `foreach' here, it won't work.
146 my $this_line = shift (@lines);
147 chomp $this_line;
149 if ($this_line =~ /^(\s+)/) {
150 $user_indent = $1;
152 else {
153 $user_indent = '';
156 # If it matches any of the line-start regexps, print a newline now...
157 if ($suppress_line_start_match)
159 $suppress_line_start_match = 0;
161 elsif (($this_line =~ /^(\s*)\*\s+[a-zA-Z0-9]/)
162 || ($this_line =~ /^(\s*)\* [a-zA-Z0-9_\.\/\+-]+/)
163 || ($this_line =~ /^(\s*)\([a-zA-Z0-9_\.\/\+-]+(\)|,\s*)/)
164 || ($this_line =~ /^(\s+)(\S+)/)
165 || ($this_line =~ /^(\s*)- +/)
166 || ($this_line =~ /^()\s*$/)
167 || ($this_line =~ /^(\s*)\*\) +/)
168 || ($this_line =~ /^(\s*)[a-zA-Z0-9](\)|\.|\:) +/))
170 $length_remaining = $max_line_length - (length ($user_indent));
173 # Now that any user_indent has been preserved, strip off leading
174 # whitespace, so up-folding has no ugly side-effects.
175 $this_line =~ s/^\s*//;
177 # Accumulate the line, and adjust parameters for next line.
178 my $this_len = length ($this_line);
179 if ($this_len == 0)
181 # Blank lines should cancel any user_indent level.
182 $user_indent = '';
183 $length_remaining = $max_line_length;
185 elsif ($this_len >= $length_remaining) # Line too long, try breaking it.
187 # Walk backwards from the end. At first acceptable spot, break
188 # a new line.
189 my $idx = $length_remaining - 1;
190 if ($idx < 0) { $idx = 0 };
191 while ($idx > 0)
193 if (substr ($this_line, $idx, 1) =~ /\s/)
195 my $line_now = substr ($this_line, 0, $idx);
196 my $next_line = substr ($this_line, $idx);
197 $this_line = $line_now;
199 # Clean whitespace off the end.
200 chomp $this_line;
202 # The current line is ready to be printed.
203 $this_line .= "\n${left_pad_str}";
205 # Make sure the next line is allowed full room.
206 $length_remaining = $max_line_length - (length ($user_indent));
208 # Strip next_line, but then preserve any user_indent.
209 $next_line =~ s/^\s*//;
211 # Sneak a peek at the user_indent of the upcoming line, so
212 # $next_line (which will now precede it) can inherit that
213 # indent level. Otherwise, use whatever user_indent level
214 # we currently have, which might be none.
215 my $next_next_line = shift (@lines);
216 if ((defined ($next_next_line)) && ($next_next_line =~ /^(\s+)/)) {
217 $next_line = $1 . $next_line if (defined ($1));
218 # $length_remaining = $max_line_length - (length ($1));
219 $next_next_line =~ s/^\s*//;
221 else {
222 $next_line = $user_indent . $next_line;
224 if (defined ($next_next_line)) {
225 unshift (@lines, $next_next_line);
227 unshift (@lines, $next_line);
229 # Our new next line might, coincidentally, begin with one of
230 # the line-start regexps, so we temporarily turn off
231 # sensitivity to that until we're past the line.
232 $suppress_line_start_match = 1;
234 last;
236 else
238 $idx--;
242 if ($idx == 0)
244 # We bottomed out because the line is longer than the
245 # available space. But that could be because the space is
246 # small, or because the line is longer than even the maximum
247 # possible space. Handle both cases below.
249 if ($length_remaining == ($max_line_length - (length ($user_indent))))
251 # The line is simply too long -- there is no hope of ever
252 # breaking it nicely, so just insert it verbatim, with
253 # appropriate padding.
254 $this_line = "\n${left_pad_str}${this_line}";
256 else
258 # Can't break it here, but may be able to on the next round...
259 unshift (@lines, $this_line);
260 $length_remaining = $max_line_length - (length ($user_indent));
261 $this_line = "\n${left_pad_str}";
265 else # $this_len < $length_remaining, so tack on what we can.
267 # Leave a note for the next iteration.
268 $length_remaining = $length_remaining - $this_len;
270 if ($this_line =~ /\.$/)
272 $this_line .= " ";
273 $length_remaining -= 2;
275 else # not a sentence end
277 $this_line .= " ";
278 $length_remaining -= 1;
282 # Unconditionally indicate that loop has run at least once.
283 $first_time = 0;
285 $wrapped_text .= "${user_indent}${this_line}";
288 # One last bit of padding.
289 $wrapped_text .= "\n";
291 return $wrapped_text;
294 # main
296 my @date;
297 my $author;
298 my @files;
299 my $comment;
301 my $state; # 0-header 1-comment 2-files
302 my $done = 0;
304 $state = 0;
306 # if reading from STDIN, we assume that we are
307 # getting git log as input
308 if (-f STDIN or -l STDIN or -p STDIN)
310 #my $dummyfh; # don't care about writing
311 #($fh,$dummyfh) = FileHandle::pipe;
312 $fh->fdopen(*STDIN, 'r');
314 else
316 $fh->open("LC_ALL=C git log --pretty --numstat --summary|")
317 or die("Cannot execute git log...$!\n");
320 while (my $_l = <$fh>) {
321 #print STDERR "debug ($state, " . (@date ? (strftime "%Y-%m-%d", @date) : "") . "): `$_'\n";
322 if ($state == 0) {
323 if ($_l =~ m,^Author: (.*),) {
324 $author = $1;
326 if ($_l =~ m,^Date: (.*),) {
327 @date = strptime($1);
329 $state = 1 if ($_l =~ m,^$, and $author and (@date+0>0));
330 } elsif ($state == 1) {
331 # * modifying our input text is a bad choice
332 # let's make a copy of it first, then we remove spaces
333 # * if we meet a "merge branch" statement, we need to start
334 # over and find a real entry
335 # Luis Mondesi <lemsx1@gmail.com>
336 my $_s = $_l;
337 $_s =~ s/^ //g;
338 if ($_s =~ m/^Merge branch/)
340 $state=0;
341 next;
343 $comment = $comment . $_s;
344 $state = 2 if ($_l =~ m,^$,);
345 } elsif ($state == 2) {
346 if ($_l =~ m,^([0-9]+)\t([0-9]+)\t(.*)$,) {
347 push @files, $3;
349 $done = 1 if ($_l =~ m,^$,);
352 if ($done) {
353 print (strftime "%Y-%m-%d $author\n\n", @date);
355 my $files = join (", ", @files);
356 $files = mywrap ("\t", "\t", "* $files"), ": ";
358 if (index($comment, EMPTY_LOG_MESSAGE) > -1 ) {
359 $comment = "[no log message]\n";
362 my $files_last_line_len = 0;
363 $files_last_line_len = last_line_len($files) + 1;
364 my $msg = wrap_log_entry($comment, "\t", 69-$files_last_line_len, 69);
366 $msg =~ s/[ \t]+\n/\n/g;
368 print "$files: $msg\n";
370 @date = ();
371 $author = "";
372 @files = ();
373 $comment = "";
375 $state = 0;
376 $done = 0;
380 if (@date + 0)
382 print (strftime "%Y-%m-%d $author\n\n", @date);
383 my $msg = wrap_log_entry($comment, "\t", 69, 69);
384 $msg =~ s/[ \t]+\n/\n/g;
385 print "\t* $msg\n";