git2cl 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. #!/usr/bin/perl
  2. # Copyright (C) 2007, 2008 Simon Josefsson <simon@josefsson.org>
  3. # Copyright (C) 2007 Luis Mondesi <lemsx1@gmail.com>
  4. # * calls git directly. To use it just:
  5. # cd ~/Project/my_git_repo; git2cl > ChangeLog
  6. # * implements strptime()
  7. # * fixes bugs in $comment parsing
  8. # - copy input before we remove leading spaces
  9. # - skip "merge branch" statements as they don't
  10. # have information about files (i.e. we never
  11. # go into $state 2)
  12. # - behaves like a pipe/filter if input is given from the CLI
  13. # else it calls git log by itself
  14. #
  15. # The functions mywrap, last_line_len, wrap_log_entry are derived from
  16. # the cvs2cl tool, see <http://www.red-bean.com/cvs2cl/>:
  17. # Copyright (C) 2001,2002,2003,2004 Martyn J. Pearce <fluffy@cpan.org>
  18. # Copyright (C) 1999 Karl Fogel <kfogel@red-bean.com>
  19. #
  20. # git2cl is free software; you can redistribute it and/or modify it
  21. # under the terms of the GNU General Public License as published by
  22. # the Free Software Foundation; either version 2, or (at your option)
  23. # any later version.
  24. #
  25. # git2cl is distributed in the hope that it will be useful, but
  26. # WITHOUT ANY WARRANTY; without even the implied warranty of
  27. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  28. # General Public License for more details.
  29. #
  30. # You should have received a copy of the GNU General Public License
  31. # along with git2cl; see the file COPYING. If not, write to the Free
  32. # Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
  33. # 02111-1307, USA.
  34. use strict;
  35. use POSIX qw(strftime);
  36. use Text::Wrap qw(wrap);
  37. use FileHandle;
  38. use constant EMPTY_LOG_MESSAGE => '*** empty log message ***';
  39. # this is a helper hash for stptime.
  40. # Assumes you are calling 'git log ...' with LC_ALL=C
  41. my %month = (
  42. 'Jan'=>0,
  43. 'Feb'=>1,
  44. 'Mar'=>2,
  45. 'Apr'=>3,
  46. 'May'=>4,
  47. 'Jun'=>5,
  48. 'Jul'=>6,
  49. 'Aug'=>7,
  50. 'Sep'=>8,
  51. 'Oct'=>9,
  52. 'Nov'=>10,
  53. 'Dec'=>11,
  54. );
  55. my $fh = new FileHandle;
  56. sub key_ready
  57. {
  58. my ($rin, $nfd);
  59. vec($rin, fileno(STDIN), 1) = 1;
  60. return $nfd = select($rin, undef, undef, 0);
  61. }
  62. sub strptime {
  63. my $str = shift;
  64. return undef if not defined $str;
  65. # we are parsing this format
  66. # Fri Oct 26 00:42:56 2007 -0400
  67. # to these fields
  68. # sec, min, hour, mday, mon, year, wday = -1, yday = -1, isdst = -1
  69. # Luis Mondesi <lemsx1@gmail.com>
  70. my @date;
  71. if ($str =~ /([[:alpha:]]{3})\s+([[:alpha:]]{3})\s+([[:digit:]]{1,2})\s+([[:digit:]]{1,2}):([[:digit:]]{1,2}):([[:digit:]]{1,2})\s+([[:digit:]]{4})/){
  72. push(@date,$6,$5,$4,$3,$month{$2},($7 - 1900),-1,-1,-1);
  73. } else {
  74. die ("Cannot parse date '$str'\n'");
  75. }
  76. return @date;
  77. }
  78. sub mywrap {
  79. my ($indent1, $indent2, @text) = @_;
  80. # If incoming text looks preformatted, don't get clever
  81. my $text = Text::Wrap::wrap($indent1, $indent2, @text);
  82. if ( grep /^\s+/m, @text ) {
  83. return $text;
  84. }
  85. my @lines = split /\n/, $text;
  86. $indent2 =~ s!^((?: {8})+)!"\t" x (length($1)/8)!e;
  87. $lines[0] =~ s/^$indent1\s+/$indent1/;
  88. s/^$indent2\s+/$indent2/
  89. for @lines[1..$#lines];
  90. my $newtext = join "\n", @lines;
  91. $newtext .= "\n"
  92. if substr($text, -1) eq "\n";
  93. return $newtext;
  94. }
  95. sub last_line_len {
  96. my $files_list = shift;
  97. my @lines = split (/\n/, $files_list);
  98. my $last_line = pop (@lines);
  99. return length ($last_line);
  100. }
  101. # A custom wrap function, sensitive to some common constructs used in
  102. # log entries.
  103. sub wrap_log_entry {
  104. my $text = shift; # The text to wrap.
  105. my $left_pad_str = shift; # String to pad with on the left.
  106. # These do NOT take left_pad_str into account:
  107. my $length_remaining = shift; # Amount left on current line.
  108. my $max_line_length = shift; # Amount left for a blank line.
  109. my $wrapped_text = ''; # The accumulating wrapped entry.
  110. my $user_indent = ''; # Inherited user_indent from prev line.
  111. my $first_time = 1; # First iteration of the loop?
  112. my $suppress_line_start_match = 0; # Set to disable line start checks.
  113. my @lines = split (/\n/, $text);
  114. while (@lines) # Don't use `foreach' here, it won't work.
  115. {
  116. my $this_line = shift (@lines);
  117. chomp $this_line;
  118. if ($this_line =~ /^(\s+)/) {
  119. $user_indent = $1;
  120. }
  121. else {
  122. $user_indent = '';
  123. }
  124. # If it matches any of the line-start regexps, print a newline now...
  125. if ($suppress_line_start_match)
  126. {
  127. $suppress_line_start_match = 0;
  128. }
  129. elsif (($this_line =~ /^(\s*)\*\s+[a-zA-Z0-9]/)
  130. || ($this_line =~ /^(\s*)\* [a-zA-Z0-9_\.\/\+-]+/)
  131. || ($this_line =~ /^(\s*)\([a-zA-Z0-9_\.\/\+-]+(\)|,\s*)/)
  132. || ($this_line =~ /^(\s+)(\S+)/)
  133. || ($this_line =~ /^(\s*)- +/)
  134. || ($this_line =~ /^()\s*$/)
  135. || ($this_line =~ /^(\s*)\*\) +/)
  136. || ($this_line =~ /^(\s*)[a-zA-Z0-9](\)|\.|\:) +/))
  137. {
  138. $length_remaining = $max_line_length - (length ($user_indent));
  139. }
  140. # Now that any user_indent has been preserved, strip off leading
  141. # whitespace, so up-folding has no ugly side-effects.
  142. $this_line =~ s/^\s*//;
  143. # Accumulate the line, and adjust parameters for next line.
  144. my $this_len = length ($this_line);
  145. if ($this_len == 0)
  146. {
  147. # Blank lines should cancel any user_indent level.
  148. $user_indent = '';
  149. $length_remaining = $max_line_length;
  150. }
  151. elsif ($this_len >= $length_remaining) # Line too long, try breaking it.
  152. {
  153. # Walk backwards from the end. At first acceptable spot, break
  154. # a new line.
  155. my $idx = $length_remaining - 1;
  156. if ($idx < 0) { $idx = 0 };
  157. while ($idx > 0)
  158. {
  159. if (substr ($this_line, $idx, 1) =~ /\s/)
  160. {
  161. my $line_now = substr ($this_line, 0, $idx);
  162. my $next_line = substr ($this_line, $idx);
  163. $this_line = $line_now;
  164. # Clean whitespace off the end.
  165. chomp $this_line;
  166. # The current line is ready to be printed.
  167. $this_line .= "\n${left_pad_str}";
  168. # Make sure the next line is allowed full room.
  169. $length_remaining = $max_line_length - (length ($user_indent));
  170. # Strip next_line, but then preserve any user_indent.
  171. $next_line =~ s/^\s*//;
  172. # Sneak a peek at the user_indent of the upcoming line, so
  173. # $next_line (which will now precede it) can inherit that
  174. # indent level. Otherwise, use whatever user_indent level
  175. # we currently have, which might be none.
  176. my $next_next_line = shift (@lines);
  177. if ((defined ($next_next_line)) && ($next_next_line =~ /^(\s+)/)) {
  178. $next_line = $1 . $next_line if (defined ($1));
  179. # $length_remaining = $max_line_length - (length ($1));
  180. $next_next_line =~ s/^\s*//;
  181. }
  182. else {
  183. $next_line = $user_indent . $next_line;
  184. }
  185. if (defined ($next_next_line)) {
  186. unshift (@lines, $next_next_line);
  187. }
  188. unshift (@lines, $next_line);
  189. # Our new next line might, coincidentally, begin with one of
  190. # the line-start regexps, so we temporarily turn off
  191. # sensitivity to that until we're past the line.
  192. $suppress_line_start_match = 1;
  193. last;
  194. }
  195. else
  196. {
  197. $idx--;
  198. }
  199. }
  200. if ($idx == 0)
  201. {
  202. # We bottomed out because the line is longer than the
  203. # available space. But that could be because the space is
  204. # small, or because the line is longer than even the maximum
  205. # possible space. Handle both cases below.
  206. if ($length_remaining == ($max_line_length - (length ($user_indent))))
  207. {
  208. # The line is simply too long -- there is no hope of ever
  209. # breaking it nicely, so just insert it verbatim, with
  210. # appropriate padding.
  211. $this_line = "\n${left_pad_str}${this_line}";
  212. }
  213. else
  214. {
  215. # Can't break it here, but may be able to on the next round...
  216. unshift (@lines, $this_line);
  217. $length_remaining = $max_line_length - (length ($user_indent));
  218. $this_line = "\n${left_pad_str}";
  219. }
  220. }
  221. }
  222. else # $this_len < $length_remaining, so tack on what we can.
  223. {
  224. # Leave a note for the next iteration.
  225. $length_remaining = $length_remaining - $this_len;
  226. if ($this_line =~ /\.$/)
  227. {
  228. $this_line .= " ";
  229. $length_remaining -= 2;
  230. }
  231. else # not a sentence end
  232. {
  233. $this_line .= " ";
  234. $length_remaining -= 1;
  235. }
  236. }
  237. # Unconditionally indicate that loop has run at least once.
  238. $first_time = 0;
  239. $wrapped_text .= "${user_indent}${this_line}";
  240. }
  241. # One last bit of padding.
  242. $wrapped_text .= "\n";
  243. return $wrapped_text;
  244. }
  245. # main
  246. my @date;
  247. my $author;
  248. my @files;
  249. my $comment;
  250. my $state; # 0-header 1-comment 2-files
  251. my $done = 0;
  252. $state = 0;
  253. # if reading from STDIN, we assume that we are
  254. # getting git log as input
  255. if (key_ready())
  256. {
  257. #my $dummyfh; # don't care about writing
  258. #($fh,$dummyfh) = FileHandle::pipe;
  259. $fh->fdopen(*STDIN, 'r');
  260. }
  261. else
  262. {
  263. $fh->open("LC_ALL=C git log --pretty --numstat --summary . |")
  264. or die("Cannot execute git log...$!\n");
  265. }
  266. while (my $_l = <$fh>) {
  267. #print STDERR "debug ($state, " . (@date ? (strftime "%Y-%m-%d", @date) : "") . "): `$_'\n";
  268. if ($state == 0) {
  269. if ($_l =~ m,^Author: (.*),) {
  270. $author = $1;
  271. }
  272. if ($_l =~ m,^Date: (.*),) {
  273. @date = strptime($1);
  274. }
  275. $state = 1 if ($_l =~ m,^$, and $author and (@date+0>0));
  276. } elsif ($state == 1) {
  277. # * modifying our input text is a bad choice
  278. # let's make a copy of it first, then we remove spaces
  279. # * if we meet a "merge branch" statement, we need to start
  280. # over and find a real entry
  281. # Luis Mondesi <lemsx1@gmail.com>
  282. my $_s = $_l;
  283. $_s =~ s/^ //g;
  284. if ($_s =~ m/^Merge branch/)
  285. {
  286. $state=0;
  287. next;
  288. }
  289. $comment = $comment . $_s;
  290. $state = 2 if ($_l =~ m,^$,);
  291. } elsif ($state == 2) {
  292. if ($_l =~ m,^([0-9]+)\t([0-9]+)\t(.*)$,) {
  293. push @files, $3;
  294. }
  295. $done = 1 if ($_l =~ m,^$,);
  296. }
  297. if ($done) {
  298. print (strftime "%Y-%m-%d $author\n\n", @date);
  299. my $files = join (", ", @files);
  300. $files = mywrap ("\t", "\t", "* $files"), ": ";
  301. if (index($comment, EMPTY_LOG_MESSAGE) > -1 ) {
  302. $comment = "[no log message]\n";
  303. }
  304. my $files_last_line_len = 0;
  305. $files_last_line_len = last_line_len($files) + 1;
  306. my $msg = wrap_log_entry($comment, "\t", 69-$files_last_line_len, 69);
  307. $msg =~ s/[ \t]+\n/\n/g;
  308. print "$files: $msg\n";
  309. @date = ();
  310. $author = "";
  311. @files = ();
  312. $comment = "";
  313. $state = 0;
  314. $done = 0;
  315. }
  316. }
  317. if (@date + 0)
  318. {
  319. print (strftime "%Y-%m-%d $author\n\n", @date);
  320. my $msg = wrap_log_entry($comment, "\t", 69, 69);
  321. $msg =~ s/[ \t]+\n/\n/g;
  322. print "\t* $msg\n";
  323. }