]> CyberLeo.Net >> Repos - FreeBSD/releng/8.1.git/blob - tools/tools/mfc/mfc.pl
Copy stable/8 to releng/8.1 in preparation for 8.1-RC1.
[FreeBSD/releng/8.1.git] / tools / tools / mfc / mfc.pl
1 #! /usr/bin/env perl
2 #
3 # mfc - perl script to generate patchsets from commit mail or message-id.
4 #
5 # Copyright (c) 2006 Florent Thoumie <flz@FreeBSD.org>
6 # All rights reserved.
7 #
8 # Redistribution and use in source and binary forms, with or without
9 # modification, are permitted provided that the following conditions
10 # are met:
11 # 1. Redistributions of source code must retain the above copyright
12 #    notice, this list of conditions and the following disclaimer.
13 # 2. Redistributions in binary form must reproduce the above copyright
14 #    notice, this list of conditions and the following disclaimer in the
15 #    documentation and/or other materials provided with the distribution.
16 #
17 # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
18 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 # ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
21 # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
23 # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
24 # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
26 # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
27 # SUCH DAMAGE.
28 #
29 # $FreeBSD$
30 #
31
32 # This perl scripts only uses programs that are part of the base system.
33 # Since some people use NO_FOO options, here's the list of used programs :
34 #  - cvs
35 #  - fetch
36 #  - perl (with getopt module)
37 #  - mkdir, cat, chmod, grep (hopefully everybody has them)
38 #  - cdiff or colordiff (optional)
39 #
40 #  This script is using 3 environment variables :
41 #  - MFCHOME: directory where patches, scripts and commit message will be stored.
42 #  - MFCCVSROOT: alternative CVSROOT used to generate diffs for new/dead files.
43 #  - MFCLOGIN: define this to your freefall login if you have commit rights.
44 #
45 # TODO: Look for XXX in the file.
46 #
47
48 use strict;
49 use warnings;
50
51 use Env;
52 use Env qw(MFCHOME MFCLOGIN MFCCVSROOT);
53 use Getopt::Std;
54 use IO::Handle;
55
56 my $mfchome = $MFCHOME ? $MFCHOME : "/var/tmp/mfc";
57 my $mfclogin = $MFCLOGIN ? $MFCLOGIN : "";
58 my $cvsroot = $MFCCVSROOT ? $MFCCVSROOT : ':pserver:anoncvs@anoncvs.at.FreeBSD.org:/home/ncvs';
59
60 my $version = "1.1.0";
61 my %opt;
62 my $commit_author;
63 my $commit_date;
64 my %mfc_files = ( );
65 my %new_files = ( );
66 my %dead_files = ( );
67 my @msgids = ( );
68 my @logmsg = ( );
69 my @commitmail = ( );
70 my $commiturl;
71 my @prs;
72 my @submitted_by;
73 my @reviewed_by;
74 my @obtained_from;
75 my $cdiff;
76 my $answer;
77 my $mfc_func = \&mfc_headers;
78
79 sub init()
80 {
81         # Enable autoflush of output to always show prompts.  Without this,
82         # piping output will fail to display a prompt.
83         autoflush STDOUT 1;
84
85         # Look for pre-requisites.
86         my @reqs = ( "fetch", "cvs", "mkdir", "cat", "chmod", "grep" );
87         my $cmd;
88         foreach (@reqs) {
89                 $cmd = `which $_`;
90                 die "$_ is missing. Please check pre-requisites." if ($cmd =~ /^$/);
91         }
92         $cdiff = `which cdiff`;
93         $cdiff = `which colordiff` if ($cdiff =~ /^$/);
94
95         # Parse command-line options.
96         my $opt_string = 'bf:hi:m:s:v';
97         getopts( "$opt_string", \%opt ) or usage();
98         usage() if !$opt{i} or $opt{h};
99         @msgids = split / /, $opt{m} if (defined($opt{m}));
100 }
101
102 sub usage()
103 {
104         print STDERR << "EOF";
105 $0 version $version 
106
107 Usage: $0 [-v] -h
108        $0 [-vb] -f file -i id
109        $0 [-vb] -m msg-id -i id
110        $0 [-vb] -s query -i id
111 Options:
112   -b         : generate a backout patch
113   -f file    : commit mail file to use ('-' for stdin)
114   -h         : this (help) message
115   -i id      : identifier used to save commit log message and patch
116   -m msg-id  : message-id referring to the original commit (you can use more than one)
117   -s query   : search commit mail archives (a filename with his revision is a good search)
118   -v         : be a little more verbose
119 Examples:
120   $0 -m 200601081417.k08EH4EN027418 -i uscanner
121   $0 -s "param.h 1.41" -i move_acpi
122   $0 -f commit.txt -i id
123   $0 -m "200601081417.k08EH4EN027418 200601110806.k0B86m9C054798" -i consecutive
124
125 Please report bugs to: Florent Thoumie <flz\@FreeBSD.org>
126 EOF
127         exit 1;
128 }
129
130 sub previous_revision($)
131 {
132         my ($rev) = @_;
133         my @rev;
134
135         # XXX - I'm not sure this is working as it should.
136         return 0 if ($rev =~ /^1\.1$/);
137         @rev = split '\.', $rev;
138         return undef unless @rev;
139         if (($#rev % 2) != 1) {
140                 pop @rev;
141                 return join ".", @rev;
142         }
143         if ($rev[-1] == 1) {
144                 pop @rev;
145                 return &previous_revision(join ".", @rev);
146         } else {
147                 $rev[-1]--;
148                 return join ".", @rev;
149         }
150 }
151
152 sub fetch_mail($)
153 {
154         my $msgid = $_[0];
155         my $url = "";
156
157         $msgid =~ s/<//;
158         $msgid =~ s/>//;
159         $msgid =~ s/@.*//;
160
161         $url = `fetch -q -o - 'http://www.freebsd.org/cgi/mid.cgi?id=$msgid'| grep getmsg.cgi | head -n 1`;
162
163         if ($url =~ /^$/) {
164                 print "No mail found for Message-Id <$msgid>.\n";
165                 exit 1;
166         }
167         $url =~ s/.*href="(.*)".*/$1/;
168         $url =~ s/\n$/\+raw/;
169         $url = "http://www.freebsd.org/cgi/$url";
170         return $url;
171 }
172
173 sub search_mail($)
174 {
175         my $query = $_[0];
176
177         $query =~ s/\s+/+/g;
178
179         # XXX - I guess we could take 5 first results instead of just the first
180         # but it has been working correctly for each search I've made so ...
181         my $result = `fetch -q -o - 'http://www.freebsd.org/cgi/search.cgi?words=$query&max=1&sort=score&index=recent&source=cvs-all' | grep getmsg.cgi`;
182
183         $result =~ s/.*href="(.*)">.*/http:\/\/www.freebsd.org\/cgi\/$1+raw/;
184         if ($result =~ /^$/) {
185                 print "No commit mail found for '$query'.\n";
186                 exit 1;
187         }
188         return $result;
189 }
190
191 sub fetch_diff($)
192 {
193         my $name = $_[0];
194         my $old = $mfc_files{$name}{"from"};
195         my $new = $mfc_files{$name}{"to"};
196
197         # CVSWeb uses rcsdiff instead of cvs rdiff, that's a problem for deleted and new files.
198         # Need to use cvs to generate reversed diff for backout commits.
199         if ($opt{b}) {
200                 print "    Generating reversed diff for $name using cvs diff...\n";
201                 system("cvs -d $cvsroot diff -u -j$new -j$old $name >> $mfchome/$opt{i}/patch 2>/dev/null");
202         } elsif (exists($new_files{$name}) or exists($dead_files{$name})) {
203                 print "    Generating diff for $name using cvs rdiff...\n";
204                 system("cvs -d $cvsroot rdiff -u -r$old -r$new $name >> $mfchome/$opt{i}/patch 2>/dev/null");
205         } else {
206                 print "    Fetching diff for $name from cvsweb.freebsd.org...\n";
207                 system("fetch -q -o - \"http://www.freebsd.org/cgi/cvsweb.cgi/$name.diff?r1=$old&r2=$new\" >> $mfchome/$opt{i}/patch");
208         }
209 }
210
211 sub mfc_headers($)
212 {
213         if ($_[0] =~ /^$/) {
214                 $mfc_func = \&mfc_author;
215         } elsif ($_[0] =~ /^(\w+)\s+(\S+\s\S+\s\S+)$/) {
216                 # Skipped headers (probably a copy/paste from sobomax MFC reminder).
217                 mfc_author($_[0]);
218         } else {
219                 if ($_[0] =~ /^Message-Id:\s*(\S+)$/ and ($opt{v} or $opt{s})) {
220                         print "Message-Id is $1.\n";
221                 }
222         }
223 }
224
225 sub mfc_author($)
226 {
227         if (!($_[0] =~ /^(\w+)\s+(\S+\s\S+\s\S+)$/)) {
228                 die "Can't determine commit author and date.";
229         }
230         $commit_author = $1;
231         $commit_date = $2;      
232
233         print "Committed by $commit_author on $commit_date.\n";
234
235         $mfc_func = \&mfc_modified_files;
236 }
237
238 sub mfc_modified_files($)
239 {
240         if ($_[0] =~ /^\s+Log:/) {
241                 $mfc_func = \&mfc_log;
242         } else {
243                 # Nothing
244         }
245 }
246
247 sub mfc_log($)
248 {
249         if ($_[0] =~ /^\s*Revision\s+Changes\s+Path\s*$/) {
250                 $mfc_func = \&mfc_revisions;
251         } else {
252                 push(@logmsg, $_[0]);
253         }
254 }
255
256 sub mfc_revisions($)
257 {
258         my $name;
259         my $rev;
260         my $prev;
261
262         return if ($_[0] =~ /^$/);
263         if (!($_[0] =~ /^\s+(\S+)\s+\S+\s+\S+\s+(\S+)/)) {
264                 # Probably two consecutive cut/paste commit mails.
265                 $mfc_func = \&mfc_headers;
266                 mfc_headers($_[0]);
267                 return;
268         } else {
269                 $_[0] =~ /\s+(\S+)\s+\S+\s+\S+\s+(\S+)/;
270                 $name = $2;
271                 $rev = $1;
272
273                 $new_files{$name} = undef if ($_[0] =~ /\(new\)$/);
274                 $dead_files{$name} = undef if ($_[0] =~ /\(dead\)$/);
275                 
276                 if (defined($mfc_files{$name}{"from"})) {
277                         $prev = previous_revision($rev);
278                         if ($mfc_files{$name}{"to"} =~ /^$prev$/) {
279                                 $mfc_files{$name}{"to"} = $rev;
280                         } else {
281                                 die "Non-consecutive revisions found for $name.";
282                         }
283                 } else {
284                         $mfc_files{$name}{"to"} = $rev;
285                         $mfc_files{$name}{"from"} = previous_revision($rev);
286                 }
287         }
288 }
289
290 sub strip_log(@) {
291         my $tmp;
292
293         while ($#logmsg >= 0 and ($logmsg[$#logmsg] =~ /^\s*$/ or $logmsg[$#logmsg] =~ /^\s\s\w+(\s\w+)*:\s+\w+(\s+\w+)*/)) {
294                 $tmp = pop(@logmsg);
295                 $tmp =~ s/^\s*//;
296                 chomp($tmp);
297                 if ($tmp =~ /^PR:\s+(.*)/) {
298                         push(@prs, $1);
299                 }
300                 if ($tmp =~ /^Submitted by:\s+(.*)/) {
301                         push(@submitted_by, $1);
302                 }
303                 if ($tmp =~ /^Reviewed by:\s+(.*)/) {
304                         push(@reviewed_by, $1);
305                 }
306                 if ($tmp =~ /^Obtained from:\s+(.*)/) {
307                         push(@obtained_from, $1);
308                 }
309         }
310 }
311
312 sub print_epilog {
313         my $tmp;
314
315         if ($#prs >= 0) {
316                 $tmp = join(", ", @prs);
317                 chomp($tmp);
318                 print MSG "PR:\t\t$tmp\n";
319         }
320         if ($#submitted_by >= 0) {
321                 $tmp = join(", ", @submitted_by);
322                 chomp($tmp);
323                 print MSG "Submitted by:\t$tmp\n";
324         }
325         if ($#reviewed_by >= 0) {
326                 $tmp = join(", ", @reviewed_by);
327                 chomp($tmp);
328                 print MSG "Reviewed by:\t$tmp\n";
329         }
330         if ($#obtained_from >= 0) {
331                 $tmp = join(", ", @obtained_from);
332                 chomp($tmp);
333                 print MSG "Obtained from:\t$tmp\n";
334         }
335 }
336
337 init();
338
339 if ($opt{s}) {
340         print "Searching commit mail on www.freebsd.org...\n";
341         $commiturl = search_mail($opt{s});
342         print "Fetching commit mail from www.freebsd.org...\n";
343         @commitmail = `fetch -q -o - $commiturl`;
344         $mfc_func->($_) foreach (@commitmail);
345         strip_log(@logmsg);
346 } elsif ($opt{f}) {
347         open MAIL, $opt{f} || die "Can't open $opt{f} for reading.";
348         @commitmail = <MAIL>;   
349         close MAIL;
350         $mfc_func->($_) foreach (@commitmail);
351         strip_log(@logmsg);
352 } else { # $opt{m}
353         foreach (@msgids) {
354                 print "Fetching commit mail from www.freebsd.org...\n";
355                 $commiturl = fetch_mail($_);
356                 @commitmail = `fetch -q -o - $commiturl`;
357                 $mfc_func->($_) foreach (@commitmail);
358                 strip_log(@logmsg);
359         }
360 }
361
362 die "Doesn't seem you gave me a real commit mail." if ($mfc_func == \&mfc_headers);
363 die "No file affected by commit?" if (scalar(keys(%mfc_files)) == 0);
364
365 # Create directory and truncate patch file.
366 system("mkdir -p $mfchome/$opt{i}");
367 system("cat /dev/null > $mfchome/$opt{i}/patch");
368
369 if ($opt{v} or $opt{s}) {
370         # Print files touched by commit(s).
371         print "Files touched by commit(s):\n";
372         print "    ", $_, ": rev ", $mfc_files{$_}{"from"}, " -> ", $mfc_files{$_}{"to"}, "\n" foreach (keys(%mfc_files));
373 }
374
375 if ($opt{s}) {
376         print "Is it the commit you were looking for ? [Yn] ";
377         $answer = <STDIN>;
378         chomp($answer);
379         if ($answer =~ /^[Nn]$/) {
380                 print "Sorry that I couldn't help you.\n";
381                 exit 0;
382         }
383 }
384
385 # Generating patch.
386 print "Processing patch...\n";
387 fetch_diff($_) foreach (keys(%mfc_files));
388
389 if ($mfclogin) {
390         # Create commit message from previous commit message.
391         print "Processing commit message...\n";
392         # Chop empty lines Template lines like "Approved by: (might be dangerous)".
393         open MSG, "> $mfchome/$opt{i}/msg" || die "Can't open $mfchome/$opt{i}/msg for writing.";
394         if ($opt{b}) {
395                 print MSG "Backout this commit:\n\n";
396         } else {
397                 print MSG "MFC:\n\n";
398         }
399         
400         # Append merged file names and revisions to the commit message.
401         print MSG $_ foreach (@logmsg);
402         if (!$opt{b}) {
403                 print MSG "\n";
404                 print MSG "      ", $_, ": rev ", $mfc_files{$_}{"from"}, " -> ", $mfc_files{$_}{"to"}, "\n" foreach (keys(%mfc_files));
405         }
406
407         # Append useful info gathered from Submitted/Obtained/... lines.
408         print MSG "\n";
409         print_epilog();
410         close MSG;
411
412         # Create commit script.
413         print "Processing commit script...\n";
414         open SCRIPT, "> $mfchome/$opt{i}/script" || die "Can't open $mfchome/$opt{i}/script for writing.";
415         print SCRIPT "#! /bin/sh\n\n";
416         print SCRIPT "# This script has been automatically generated by $0.\n\n";
417         print SCRIPT "export CVSROOT=\"$mfclogin\@ncvs.freebsd.org:/home/ncvs\"\n\n";
418
419         if (scalar(keys(%new_files)) or scalar(keys(%dead_files))) {
420                 if (scalar(keys(%new_files))) {
421                         print SCRIPT "cvs add";
422                         print SCRIPT " \\\n  $_" foreach (keys(%new_files));
423                         print SCRIPT "\n";
424                 }
425                 if (scalar(keys(%dead_files))) {
426                         print SCRIPT "cvs rm -f";
427                         print SCRIPT " \\\n  $_" foreach (keys(%dead_files));
428                         print SCRIPT "\n";
429                 }
430         }
431
432         print SCRIPT "cvs diff";
433         print SCRIPT " \\\n  $_" foreach (keys(%mfc_files));
434         if ($cdiff =~ /^$/) {
435                 print SCRIPT "\n";
436         } else {
437                 print SCRIPT " | $cdiff";
438         }
439
440         print SCRIPT "cvs ci";
441         print SCRIPT " \\\n  $_" foreach (keys(%mfc_files));
442         print SCRIPT "\n";
443
444         close SCRIPT;
445         system("chmod a+x $mfchome/$opt{i}/script");
446 }
447
448 print "Done, output directory is $mfchome/$opt{i}/\n";
449
450 exit 0;