3 # mfc - perl script to generate patchsets from commit mail or message-id.
5 # Copyright (c) 2006 Florent Thoumie <flz@FreeBSD.org>
8 # Redistribution and use in source and binary forms, with or without
9 # modification, are permitted provided that the following conditions
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.
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
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 :
36 # - perl (with getopt module)
37 # - mkdir, cat, chmod, grep (hopefully everybody has them)
38 # - cdiff or colordiff (optional)
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.
45 # TODO: Look for XXX in the file.
52 use Env qw(MFCHOME MFCLOGIN MFCCVSROOT);
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';
60 my $version = "1.1.0";
77 my $mfc_func = \&mfc_headers;
81 # Enable autoflush of output to always show prompts. Without this,
82 # piping output will fail to display a prompt.
85 # Look for pre-requisites.
86 my @reqs = ( "fetch", "cvs", "mkdir", "cat", "chmod", "grep" );
90 die "$_ is missing. Please check pre-requisites." if ($cmd =~ /^$/);
92 $cdiff = `which cdiff`;
93 $cdiff = `which colordiff` if ($cdiff =~ /^$/);
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}));
104 print STDERR << "EOF";
108 $0 [-vb] -f file -i id
109 $0 [-vb] -m msg-id -i id
110 $0 [-vb] -s query -i id
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
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
125 Please report bugs to: Florent Thoumie <flz\@FreeBSD.org>
130 sub previous_revision($)
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) {
141 return join ".", @rev;
145 return &previous_revision(join ".", @rev);
148 return join ".", @rev;
161 $url = `fetch -q -o - 'http://www.freebsd.org/cgi/mid.cgi?id=$msgid'| grep getmsg.cgi | head -n 1`;
164 print "No mail found for Message-Id <$msgid>.\n";
167 $url =~ s/.*href="(.*)".*/$1/;
168 $url =~ s/\n$/\+raw/;
169 $url = "http://www.freebsd.org/cgi/$url";
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`;
183 $result =~ s/.*href="(.*)">.*/http:\/\/www.freebsd.org\/cgi\/$1+raw/;
184 if ($result =~ /^$/) {
185 print "No commit mail found for '$query'.\n";
194 my $old = $mfc_files{$name}{"from"};
195 my $new = $mfc_files{$name}{"to"};
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.
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");
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");
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).
219 if ($_[0] =~ /^Message-Id:\s*(\S+)$/ and ($opt{v} or $opt{s})) {
220 print "Message-Id is $1.\n";
227 if (!($_[0] =~ /^(\w+)\s+(\S+\s\S+\s\S+)$/)) {
228 die "Can't determine commit author and date.";
233 print "Committed by $commit_author on $commit_date.\n";
235 $mfc_func = \&mfc_modified_files;
238 sub mfc_modified_files($)
240 if ($_[0] =~ /^\s+Log:/) {
241 $mfc_func = \&mfc_log;
249 if ($_[0] =~ /^\s*Revision\s+Changes\s+Path\s*$/) {
250 $mfc_func = \&mfc_revisions;
252 push(@logmsg, $_[0]);
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;
269 $_[0] =~ /\s+(\S+)\s+\S+\s+\S+\s+(\S+)/;
273 $new_files{$name} = undef if ($_[0] =~ /\(new\)$/);
274 $dead_files{$name} = undef if ($_[0] =~ /\(dead\)$/);
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;
281 die "Non-consecutive revisions found for $name.";
284 $mfc_files{$name}{"to"} = $rev;
285 $mfc_files{$name}{"from"} = previous_revision($rev);
293 while ($#logmsg >= 0 and ($logmsg[$#logmsg] =~ /^\s*$/ or $logmsg[$#logmsg] =~ /^\s\s\w+(\s\w+)*:\s+\w+(\s+\w+)*/)) {
297 if ($tmp =~ /^PR:\s+(.*)/) {
300 if ($tmp =~ /^Submitted by:\s+(.*)/) {
301 push(@submitted_by, $1);
303 if ($tmp =~ /^Reviewed by:\s+(.*)/) {
304 push(@reviewed_by, $1);
306 if ($tmp =~ /^Obtained from:\s+(.*)/) {
307 push(@obtained_from, $1);
316 $tmp = join(", ", @prs);
318 print MSG "PR:\t\t$tmp\n";
320 if ($#submitted_by >= 0) {
321 $tmp = join(", ", @submitted_by);
323 print MSG "Submitted by:\t$tmp\n";
325 if ($#reviewed_by >= 0) {
326 $tmp = join(", ", @reviewed_by);
328 print MSG "Reviewed by:\t$tmp\n";
330 if ($#obtained_from >= 0) {
331 $tmp = join(", ", @obtained_from);
333 print MSG "Obtained from:\t$tmp\n";
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);
347 open MAIL, $opt{f} || die "Can't open $opt{f} for reading.";
348 @commitmail = <MAIL>;
350 $mfc_func->($_) foreach (@commitmail);
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);
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);
365 # Create directory and truncate patch file.
366 system("mkdir -p $mfchome/$opt{i}");
367 system("cat /dev/null > $mfchome/$opt{i}/patch");
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));
376 print "Is it the commit you were looking for ? [Yn] ";
379 if ($answer =~ /^[Nn]$/) {
380 print "Sorry that I couldn't help you.\n";
386 print "Processing patch...\n";
387 fetch_diff($_) foreach (keys(%mfc_files));
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.";
395 print MSG "Backout this commit:\n\n";
397 print MSG "MFC:\n\n";
400 # Append merged file names and revisions to the commit message.
401 print MSG $_ foreach (@logmsg);
404 print MSG " ", $_, ": rev ", $mfc_files{$_}{"from"}, " -> ", $mfc_files{$_}{"to"}, "\n" foreach (keys(%mfc_files));
407 # Append useful info gathered from Submitted/Obtained/... lines.
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";
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));
425 if (scalar(keys(%dead_files))) {
426 print SCRIPT "cvs rm -f";
427 print SCRIPT " \\\n $_" foreach (keys(%dead_files));
432 print SCRIPT "cvs diff";
433 print SCRIPT " \\\n $_" foreach (keys(%mfc_files));
434 if ($cdiff =~ /^$/) {
437 print SCRIPT " | $cdiff";
440 print SCRIPT "cvs ci";
441 print SCRIPT " \\\n $_" foreach (keys(%mfc_files));
445 system("chmod a+x $mfchome/$opt{i}/script");
448 print "Done, output directory is $mfchome/$opt{i}/\n";