From nobody Tue Dec 16 00:00:28 2025 Delivered-To: importer2@patchew.org Received-SPF: pass (zohomail.com: domain of vger.kernel.org designates 23.128.96.18 as permitted sender) client-ip=23.128.96.18; envelope-from=linux-kernel-owner@vger.kernel.org; helo=vger.kernel.org; Authentication-Results: mx.zohomail.com; dkim=fail; spf=pass (zohomail.com: domain of vger.kernel.org designates 23.128.96.18 as permitted sender) smtp.mailfrom=linux-kernel-owner@vger.kernel.org; dmarc=pass(p=none dis=none) header.from=kernel.org ARC-Seal: i=1; a=rsa-sha256; t=1619162466; cv=none; d=zohomail.com; s=zohoarc; b=m5buh5U5JUtLX9PvcxSZeyWEhS3Z2Z80v9vLDsxSqV9VdEsSYraMWaKveJQFyy+PWgewHKNGaEund1DGvegKviGqQQza94Z4Po/fNZtG/bYGyTeUQf0ksFnwgVd56Gt01PaTvTkdss8C+zEmocX7et9oR4C8QHwH803L+rMO6Yc= ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zohomail.com; s=zohoarc; t=1619162466; h=Content-Transfer-Encoding:Cc:Date:From:In-Reply-To:List-Id:MIME-Version:Message-ID:References:Sender:Subject:To; bh=Brnxhhz6lx+CSH5DbgovNTEBasXv20qjdMeLTVdzLec=; b=g51Ph6znLCp3gNF4aE6JncBKk+BmFbpxXApjrHh9vDq6EYIaw4kTSHm+GwwDgywtTCy9kEngq6H1gB2RRTSKGel+TvJBgQHjD2f5g3uC3P1PqbSs5xVHpf1n5UaC5Asc0w/plxp3/hf8PPbSojqgsBBSShFH/s2jGN7TUZUIw50= ARC-Authentication-Results: i=1; mx.zohomail.com; dkim=fail; spf=pass (zohomail.com: domain of vger.kernel.org designates 23.128.96.18 as permitted sender) smtp.mailfrom=linux-kernel-owner@vger.kernel.org; dmarc=pass header.from= (p=none dis=none) header.from= Return-Path: Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mx.zohomail.com with SMTP id 1619162466870553.6545797575433; Fri, 23 Apr 2021 00:21:06 -0700 (PDT) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S240816AbhDWHVl (ORCPT ); Fri, 23 Apr 2021 03:21:41 -0400 Received: from mail.kernel.org ([198.145.29.99]:34482 "EHLO mail.kernel.org" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S229935AbhDWHVj (ORCPT ); Fri, 23 Apr 2021 03:21:39 -0400 Received: by mail.kernel.org (Postfix) with ESMTPSA id D93CA613DB; Fri, 23 Apr 2021 07:21:02 +0000 (UTC) Received: by mail.kernel.org with local (Exim 4.94) (envelope-from ) id 1lZq7Y-002h1g-A4; Fri, 23 Apr 2021 09:21:00 +0200 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=kernel.org; s=k20201202; t=1619162463; bh=Ecjcv7rCQzGfjeRxL6T0JbpHE99KffGApYjYGPM3LeU=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=T+TtFvh7HbQpfSRyvesZjdi32NIaA8Q5uZt9CaGVQcM6u81iqKr74BdKcEow9jpMK WhBGS1hab7Pfa+O8FnhC4CJxhrmmZ0BX7zO1TQSbcUr/28feSoc7vRugXE0hpF8kIR Cwhk6RLPOJ54aQXzk14gkU2eVseUZbXgWN6jssxjrT2mrcqjy+b5OH4CltYPFVj5P5 OX8KIxCK5q93D1fDEyEz8LFqAIr4GNDkQF1F/LGKDFJBh74HTVne9q4g/2lI19fqPr ZerUpjmTqykaxpAxfo2zivZ09t8YK6/bpFTOi/86CPMvMIA1iggisBUPck1SyZlXrH YnCHXKskGQWiA== From: Mauro Carvalho Chehab Cc: linuxarm@huawei.com, mauro.chehab@huawei.com, Mauro Carvalho Chehab , "James Bottomley" , "Joe Perches" , "Leon Romanovsky" , "Rob Herring" , "Steven Rostedt" , ksummit@lists.linux.dev, linux-kernel@vger.kernel.org, tools@linux.kernel.org Subject: [PATCH RFC] scripts: add a script for sending patches Date: Fri, 23 Apr 2021 09:20:50 +0200 Message-Id: X-Mailer: git-send-email 2.30.2 In-Reply-To: <20210423091320.4f2381b2@coco.lan> References: <20210423091320.4f2381b2@coco.lan> MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Sender: Mauro Carvalho Chehab To: unlisted-recipients:; (no To-header on input) Precedence: bulk List-ID: X-Mailing-List: linux-kernel@vger.kernel.org X-ZohoMail-DKIM: fail (Header signature does not verify) Content-Type: text/plain; charset="utf-8" This script send patches to an upstream maintainer's tree, using git send-email and producing the relevant c/c list, by using scripts/get_maintainers.pl. Signed-off-by: Mauro Carvalho Chehab --- As result of the current discussions about Rethinking the acceptance policy for "trivial" patches, I'm submitting the script I wrote in order to help me to send patches upstream using get_maintainers.pl. I've been playing with this script since 2015, and had to improve and add new options to it over time, based on some specific needs that I detected while submitting patches both to a single subsystem and to multiple ones. scripts/send-patches.pl | 658 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 658 insertions(+) create mode 100755 scripts/send-patches.pl diff --git a/scripts/send-patches.pl b/scripts/send-patches.pl new file mode 100755 index 000000000000..b4aa73979e4d --- /dev/null +++ b/scripts/send-patches.pl @@ -0,0 +1,658 @@ +#!/usr/bin/perl +# SPDX-License-Identifier: GPL-2.0 +# Copyright (c) 2015- by Mauro Carvalho Chehab + +use strict; +use warnings; +use Email::Simple; +use Email::Address; +use Getopt::Long; +use Pod::Usage; +use File::Path 'make_path'; +require Tk; +require Tk::Text; +require Tk::Font; +require Tk::Toplevel; + +# Where the patch series will be stored. If doesn't exits, +# it will be automatically created +my $tmp_dir =3D "patches/tmp"; + +# Used together with --avoid-resend +my $resend_cache_dir =3D "patches/last_patches"; +my $version_ctrl =3D ".version_control"; + +# +# File editor function. Currently relies on Tk to open +# a separate edit window. +# +sub edit_text($) { + my $fname =3D shift; + my $string =3D qx(cat $fname); + my $edited_text; + + my $toplvl =3D MainWindow->new(); + my $font =3D $toplvl->Font(family =3D> 'fixed', size =3D> 12); + + my $frame_txt =3D $toplvl->Frame(); + my $frame_btn =3D $toplvl->Frame(); + + $toplvl->configure(-title =3D> "Editing $fname"); + + my $text =3D $frame_txt->Scrolled('Text')->pack; + $text->configure(-height =3D> 30, + -background =3D> 'black', + -foreground =3D> 'gray', + -insertbackground =3D> 'white', + -width =3D> 80, + -wrap =3D> 'word', + -font =3D> $font); + + my $Button1 =3D $frame_btn->Button(); + $Button1->configure(-text =3D> 'OK', + -bg =3D> 'lightblue', + -width =3D> 5, + -height =3D> 1, + -command =3D> sub{$edited_text =3D $text->get("1.0", "end"); $top= lvl->destroy} ); + + $text->insert('1.0', $string); + + # Pack the widgets in the frames + $text->pack(); + $frame_txt->pack(); + $frame_btn->pack(); + $Button1->pack(); + + $text->waitWindow(); + + return $edited_text; +} + +# +# Argument handling +# +my $edit =3D 0; +my $cover =3D 0; +my $man =3D 0; +my $help =3D 0; +my $cmd_line =3D "git format-patch -o $tmp_dir --stat --summary --patience= --signoff --thread=3Dshallow"; +my $changeset =3D 0; +my $dont_send =3D 0; +my $avoid_resend =3D 0; +my $reply_patches =3D 0; +my $subject_prefix =3D "PATCH"; +my $dont_get_maintainer =3D 0; +my $dont_get_reviewer =3D 0; +my $reroll_count =3D ""; +my $git =3D 0; +my $nogit =3D 0; +my $add_everyone =3D 0; +my $unify =3D ""; +my $to_maintainers =3D 0; + +GetOptions( + "cover|letter" =3D> sub { $cmd_line .=3D " --cover-letter"; $cover =3D 1}, + "no-merge|no-merges|no-renames" =3D> sub { $cmd_line .=3D " --no-renames= " }, + "merge|M" =3D> sub { $cmd_line .=3D " -M01" }, + "delete|D" =3D> sub { $cmd_line .=3D " -D" }, + "unify|U=3Ds" =3D> \$unify, + "to=3Ds" =3D> sub { my ($opt, $arg) =3D @_; $cmd_line .=3D " --to '$arg'"= }, + "cc=3Ds" =3D> sub { my ($opt, $arg) =3D @_; $cmd_line .=3D " --cc '$arg'"= }, + "prefix|subject-prefix=3Ds" =3D> \$subject_prefix, + "edit|annotate" =3D> \$edit, + "dry-run|dont-send" =3D> \$dont_send, + "reply-patches" =3D> sub { $reply_patches =3D 1; $avoid_resend =3D 1; $cm= d_line .=3D " -N" }, + "avoid-resend" =3D> sub { $avoid_resend =3D 1; $cmd_line .=3D " -N" }, + "dont-get-maintainer" =3D> \$dont_get_maintainer, + "dont-get-reviewer" =3D> \$dont_get_reviewer, + "everyone|add-everyone" =3D> \$add_everyone, + "git" =3D> \$git, + "no-git-fallback" =3D> \$nogit, + "to-maintainers" =3D> \$to_maintainers, + "v|reroll_count=3Ds" =3D> \$reroll_count, + "help" =3D> \$help, + "man" =3D> \$man, +) or pod2usage(2); + +$help =3D 1 if (@ARGV < 1); + +if ($avoid_resend && $cover) { + printf ("Sorry, you can't avoid resend patches and add a cover yet.\n"); +} + +pod2usage(1) if $help; +pod2usage(-verbose =3D> 2) if $man; + +$cmd_line .=3D " --subject-prefix '$subject_prefix'"; +$cmd_line .=3D " -v $reroll_count" if ($reroll_count); +$cmd_line .=3D " -U$unify" if ($unify); +$cmd_line =3D join(' ', $cmd_line, @ARGV); + +$dont_get_reviewer =3D 1 if ($dont_get_maintainer); + +# +# Prepare to avoid resending patches +# + +my %cgid_to_msgid; +my %msgid_to_file; +my %msgid_to_subject; + +sub msgid_from_last_patch($) +{ + my $change_id =3D shift; + + return 0 if (!$change_id); + return 0 if (!$cgid_to_msgid{$change_id}); + + return $cgid_to_msgid{$change_id}; +} + +if ($avoid_resend) { + open IN, $version_ctrl or print "Can't find $version_ctrl\n"; + while () { + if (m/([^\t]+)\t([^\t]+).*\n/) { + $cgid_to_msgid{$1} =3D $2; + } + } + close IN; + + opendir(my $dh, $resend_cache_dir) || die "can't read patches at $resend_= cache_dir"; + while(readdir $dh) { + my $name =3D "$resend_cache_dir/$_"; + next if (-d $name); + + my $raw_email =3D qx(cat $name); + my $email =3D Email::Simple->new($raw_email); + my $msgid =3D $email->header("Message-Id"); + my $subject =3D $email->header("Subject"); + + $msgid_to_file{$msgid} =3D $name if ($msgid); + $msgid_to_subject{$msgid} =3D $subject if ($subject && $msgid); + } + closedir $dh; +} + +sub get_maintainer($$) +{ + my $cmd =3D $_[0]; + my @file_cc =3D @{$_[1]}; + my $role; + my $e_mail; + my %cc =3D ( + "linux-kernel\@vger.kernel.org" =3D> 1 + ); + + foreach $e_mail (@file_cc) { + my @addresses =3D Email::Address->parse($e_mail); + for my $address (@addresses) { + $cc{$address} =3D "cc"; + } + } + + $cmd =3D "./scripts/get_maintainer.pl " . $cmd; + + print "$cmd\n"; + open IN, "$cmd |" or die "can't run $cmd"; + while () { + $e_mail =3D $_; + $e_mail =3D~ s/(.*\@\S+)\s*\(.*/$1/; + $e_mail =3D~ s/\s*$//; + + + if (m/\(.*(open list|moderated list|subscriber list|maintainer|reviewer|= modified|chief|commit_signer).*\)/) { + $role =3D $1; + } else { + $role =3D "cc"; + } + # Discard myself + next if ($e_mail =3D~ m/(mchehab|mauro.?chehab)\S+\@\S+/); + $cc{$e_mail} =3D $role; + } + close IN; + + return %cc; +} + +# +# Generate patches with git format-patch +# +make_path($tmp_dir); +unlink glob "$tmp_dir/*"; + +print "\$ $cmd_line\n"; +system ("$cmd_line >>/dev/null") && die("Failed to run git format-patch"); + +# +# Add Cc: based on get_maintainer.pl script +# +my @patches; +my @send_patches; + +opendir(my $dh, $tmp_dir) || die "can't read patches at $tmp_dir"; +while(readdir $dh) { + my $name =3D "$tmp_dir/$_"; + push @patches,$name if (-f $name); +} +closedir $dh; + +my %changeids; + +my $has_cover; +my %cover_cc; + +foreach my $f(sort @patches) { + print "Checking $f\n"; + if ($f =3D~ m,0000-cover-letter.patch$,) { + push @send_patches, $f; + $has_cover =3D 1; + next; + } + + my $raw_email =3D qx(cat $f); + die "Can't read $f" if (!$raw_email); + + my $email =3D Email::Simple->new($raw_email); + + my $msgid =3D 0; + my $oldsubject; + my $change_id; + + if ($raw_email =3D~ m/\nChange-Id:\s+([^\n]+)\n/) { + $change_id =3D $1; + } + + if ($avoid_resend) { + $msgid =3D msgid_from_last_patch($change_id); + if ($msgid) { + if ($msgid_to_subject{$msgid}) { + $oldsubject =3D $msgid_to_subject{$msgid}; + + my $file =3D $msgid_to_file{$msgid}; + + my $old_md5 =3D qx(filterdiff $file | md5sum); + my $new_md5 =3D qx(filterdiff $f | md5sum); + + $old_md5 =3D~ s/(\S+).*$/$1/; + $new_md5 =3D~ s/(\S+).*$/$1/; + + if ($old_md5 eq $new_md5) { + printf " Skipping patch as it is identical to previous version\n"; + unlink $f; + next; + } + } + my $new_msgid =3D $email->header("Message-Id"); + $changeids{$change_id} =3D $new_msgid if ($new_msgid); + } + } + + # Patch was not avoided. Push to the list of patches to send + push @send_patches, $f; + + my $cmd =3D ""; + $cmd .=3D "--git" if ($git); + $cmd .=3D"--nogit-fallback --nogit-blame --nogit" if ($nogit); + $cmd .=3D" $f"; + + my @file_cc =3D $email->header("Cc"); + if ($to_maintainers) { + push @file_cc, $email->header("To") if ($email->header("To")); + } + my %cc_email_map =3D get_maintainer $cmd, \@file_cc; + my %maintainers; + + @file_cc =3D (); + my @file_to =3D (); + foreach my $cc (sort keys %cc_email_map) { + my $ml_added =3D 0; + my $role =3D $cc_email_map{$cc}; + + my $type =3D "Cc"; + $type =3D "To" if ($to_maintainers && $role =3D~ "maintainer"); + + if ($role =3D~ "maintainer") { + if (!$dont_get_maintainer) { + $ml_added =3D 1; + $cover_cc{$cc} =3D 1; + } + } elsif ($role =3D~ "reviewer") { + if (!$dont_get_reviewer) { + $ml_added =3D 1; + $cover_cc{$cc} =3D 1; + } + } elsif ($role =3D~ "list") { + $ml_added =3D 1; + $cover_cc{$cc} =3D 1; + } elsif ($add_everyone) { + $ml_added =3D 1; + $cover_cc{$cc} =3D 1; + } + + if ($type eq "To") { + push @file_to, $cc; + } else { + push @file_cc, $cc; + } + if ($ml_added && $cover) { + printf " $type + cover Cc: $cc (%s)\n", $role; + } else { + printf " $type: $cc (%s)\n", $role; + } + } + + $email->header_set("To", @file_to) if (@file_to); + $email->header_set("Cc", @file_cc) if (@file_cc); + + # Remove Change-Id meta-data from the e-mail to be submitted + my $body =3D $email->body; + $body =3D~ s/(\nChange-Id:\s+[^\n]+\n)/\n/; + $email->body_set($body); + + if ($avoid_resend) { + if (!$reply_patches && $msgid) { + $email->body_set("New version of $oldsubject\n\n$body"); + } else { + die "New patches in the series. Can't proceed." if (!$msgid); + die "Failed to find old subject. Can't proceed." if (!$oldsubject); + + $email->header_set("Subject", "Re: $oldsubject"); + } + $email->header_set("In-Reply-To", $msgid); + $email->header_set("References", $msgid); + } + + open OUT, ">$f"; + print OUT $email->as_string; + close OUT; +} + +# Sanity check +die "Something wrong when generating/detecting a cover" if ($cover && !$ha= s_cover); + +# +# Add everyone at the cover's to: field +# +if ($has_cover) { + my $count_cc =3D 0; + foreach my $f(sort @patches) { + next if (!($f =3D~ m,0000-cover-letter.patch$,)); + + print "$f:\n"; + my $raw_email =3D qx(cat $f); + die "Can't read $f" if (!$raw_email); + + my $email =3D Email::Simple->new($raw_email); + + my @file_cc =3D $email->header("Cc"); + + foreach my $e_mail (@file_cc) { + my @addresses =3D Email::Address->parse($e_mail); + for my $address (@addresses) { + $cover_cc{$address} =3D 1; + } + } + + foreach my $to(sort keys %cover_cc) { + print " Cc: $to\n"; + push @file_cc, $to; + $count_cc++; + } + + $email->header_set("Cc", @file_cc); + + open OUT, ">$f"; + print OUT $email->as_string; + close OUT; + + print "Number of Cc at cover: $count_cc\n"; + } +} + +# +# Renumber the patches +# +if ($avoid_resend && !$reply_patches) { + my $tot_patch =3D @send_patches; + + $tot_patch-- if ($cover); + + my $digits =3D int(log($tot_patch)/log(10)+0.99999); + my $patch =3D 1; + + foreach my $f(@send_patches) { + next if ($f =3D~ m,0000-cover-letter.patch$,); + + my $raw_email =3D qx(cat $f); + die "Can't read $f" if (!$raw_email); + + my $email =3D Email::Simple->new($raw_email); + + my $subject =3D $email->header("Subject"); + + my $number =3D sprintf("%0${digits}d/%0${digits}d", $patch, $tot_patch); + + $subject =3D~ s/^\[[^\]]+\]\s*//; + $subject =3D "[$subject_prefix $number] " . $subject; + printf("$subject\n"); + $email->header_set("Subject", $subject); + + $patch++; + + open OUT, ">$f"; + print OUT $email->as_string; + close OUT; + } +} + +# +# Open an editor if needed +# +if ($edit || $cover) { + foreach my $f(sort @send_patches) { + my $new_text; + + do { + $new_text =3D edit_text($f); + } while (!$new_text); + + open OUT, ">$f"; + print OUT $new_text; + close OUT; + + last if ($cover); + } +} + +# +# Send the emails +# + +if (!$dont_send) { + printf("\$ git send-email $tmp_dir\n"); + system("git send-email $tmp_dir"); +} else { + printf("Use git send-email $tmp_dir to send the patches\n"); +} + +# +# Update the change IDs with the new patches +# +foreach my $chgid (keys %changeids) { + $cgid_to_msgid{$chgid} =3D $changeids{$chgid}; +} + +open OUT,">$version_ctrl.new"; +foreach my $chgid (sort keys %cgid_to_msgid) { + printf OUT "%s\t%s\n", $chgid, $cgid_to_msgid{$chgid}; +} +close OUT; + +if ($dont_send) { + printf("New version control stored as: .version_control.new\n" . + "Don't forget rename it to .version_control for the next patch ser= ies after sending it.\n"); +} else { + rename $version_ctrl, "$version_ctrl.old"; + rename "$version_ctrl.new", $version_ctrl; +} + + +__END__ + +=3Dhead1 NAME + +send-patches.pl - Send patches upstream + +=3Dhead1 SYNOPSIS + +send-patches.pl [options] [changeset] -- [options for git format-patch] + +Options: + +--cover/--cover-letter +--no-renames/--no-merges/--no-renames +--merge/-M +--delete/-D +--unify/-U [level] +--to [e@mail] +--cc [e@mail] +--prefix/--subject-prefix +--edit +--dont-send/--dry-run +--avoid-resend +--reply-patches +--dont-get-maintainer +--not-everyone/--dont-add-everyone +--no-git-fallback +--to-maintainers +--help +--man +--reroll-count/-v [version number] + +=3Dhead1 OPTIONS + +=3Dover 8 + +=3Ditem B<--cover> or B<--cover-letter> + +Patch series will have a cover letter. Automatically enables edition + +=3Ditem B<--no-renames> or B<--no-merges> or B<--no-merge> + +Disables git merge detection with git show --no-renames + +=3Ditem B<--merge> or B<--M> + +Enables aggressive git merge detection with git show -M01 + +=3Ditem B<--delete> or B<--D> + +Omit the previous content on deletes, printing only the header but +not the diff between the removed files and /dev/null. + +=3Ditem B<--unify> or B<--U> + +Set the unify diff level (default=3D3). + +=3Ditem B<--to> + +Add one more recipient destination for the e-mail +(at the To: part of the email) + +=3Ditem B<--cc> + +Add one more recipient carbon copy destination for the e-mail +(at the Cc: part of the email) + +=3Ditem B<--prefix> or B<--subject-prefix> + +By default, the subject prefix will be "PATCH". This otpion allows changing +it. + +=3Ditem B<--edit> + +Allows editing each patch in the series, and the cover letter. + +=3Ditem B<--dont-send> or B<--dry-run> + +Do everything but calling git send-email. Useful to test the tool or +when you need to do more complex things. + +=3Ditem B<--reply-patches> + +Instead of sending a new series, reply to an existing one. This only +works if no new patches were added at the series. + +=3Ditem B<--avoid-resend> + +Don't resend patches that are identical to the previosly send +series of patches. The patches that will be send will be renumbered. + +Please notice that this option is currently incompatible with +a --cover, as we need to teach this script how to remove the +removed patches from the letter summary. + +=3Ditem B<--dont-get-maintainer> + +Ignore maintainers at the cover letter. + +=3Ditem B<--dont-get-reviewer> + +Ignore reviewers at the cover letter. + +=3Ditem B<--everyone>/<--add-everyone> + +The script/get_maintainers.pl returns maintainers, reviewers and mailing l= ists. +It also returns a list of usual contributors. + +By default, the usual contributors are ignored at the cover letter, being +added only at the patches themselves. When this flag is used, they'll also +be c/c to the cover letter. + +=3Ditem B<--git> + +Include recent git *-by: signers. + +=3Ditem B<--no-git-fallback> + +Use git when no exact MAINTAINERS pattern. This disables detection of the +usual contributors. + +=3Ditem B<--to-maintainers> + +Instead of placing patches on a series, send them individually +to their own maintainers. + +=3Ditem B<-v>/<--reroll-count> + +Change the version number on a patch series, by passing --reroll-count +to git format-patch. + +=3Ditem B<--help> + +Print a brief help message and exits. + +=3Ditem B<--man> + +Prints the manual page and exits. + +=3Dback + +=3Dhead1 DESCRIPTION +B will submit a patch series upstream. +=3Dcut + +=3Dhead1 BUGS + +Report bugs to Mauro Carvalho Chehab + +=3Dhead1 COPYRIGHT + +Copyright (c) 2015- by Mauro Carvalho Chehab . + +License GPLv2: GNU GPL version 2 . + +This is free software: you are free to change and redistribute it. +There is NO WARRANTY, to the extent permitted by law. + +=3Dcut --=20 2.30.2