Chris L Mason e00945
#!/usr/bin/perl
Michal Marek 0a417c
#############################################################################
Michal Marek 0a417c
# Copyright (c) 2004,2005 Novell, Inc.
Michal Marek 0a417c
# All Rights Reserved.
Michal Marek 0a417c
#
Michal Marek 0a417c
# This program is free software; you can redistribute it and/or
Michal Marek 0a417c
# modify it under the terms of version 2 of the GNU General Public License as
Michal Marek 0a417c
# published by the Free Software Foundation.
Michal Marek 0a417c
#
Michal Marek 0a417c
# This program is distributed in the hope that it will be useful,
Michal Marek 0a417c
# but WITHOUT ANY WARRANTY; without even the implied warranty of
Michal Marek 0a417c
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.   See the
Michal Marek 0a417c
# GNU General Public License for more details.
Michal Marek 0a417c
#
Michal Marek 0a417c
# You should have received a copy of the GNU General Public License
Michal Marek 0a417c
# along with this program; if not, contact Novell, Inc.
Michal Marek 0a417c
#
Michal Marek 0a417c
# To contact Novell about this file by physical or electronic mail,
Michal Marek 0a417c
# you may find current contact information at www.novell.com
Michal Marek 0a417c
#############################################################################
Chris L Mason e00945
#
Chris L Mason e00945
# patch-tag is meant to maintain a set of metadata tags in a diff.  Multiple
Chris L Mason e00945
# files can be specified on the command line and all options can be
Chris L Mason e00945
# given more than once.
Chris L Mason e00945
#
Chris L Mason e00945
# All options can be abbreviated.  --print is the same as -p
Chris L Mason e00945
#
Chris L Mason e00945
# All tags are changed so the first letter is uppercase and the rest are
Chris L Mason e00945
# lowercase.
Chris L Mason e00945
#
Chris L Mason e00945
# Example usage:
Chris L Mason e00945
#
Chris L Mason e00945
# patch-tag file
Chris L Mason e00945
#	print the entire header before the diff starts
Chris L Mason e00945
#
Chris L Mason e00945
# patch-tag -e filename
Chris L Mason e00945
#	Runs $EDITOR on filename.  If there are no tags in the file yet
Chris L Mason e00945
#	a default set of tags is filled in.  See $default_comment for the
Chris L Mason e00945
#	list.
Chris L Mason e00945
#
Chris L Mason e00945
# patch-tag -p Author -p Subject file
Chris L Mason e00945
#	print the author and subject tags only
Chris L Mason e00945
#
Chris L Mason 118480
# patch-tag -s [-p tag] file
Chris L Mason 118480
#	prints in summary form, default tags are Subject and References
Chris L Mason 118480
#
Chris L Mason e00945
# --print forces everything into readonly mode.  If you specify --tag 
Chris L Mason e00945
# along with --print, the file won't be changed although the output on stdout
Chris L Mason e00945
# will.
Chris L Mason e00945
#
Chris L Mason e2d213
# patch-tag -P file
Chris L Mason e2d213
#	Prints only the comments other then tags in the file.
Chris L Mason e2d213
#
Chris L Mason e00945
# patch-tag -t author=Mason -t subject="a patch to fix an oops"
Chris L Mason e00945
#	Add or modify the author and subject tags.  If more than one
Chris L Mason e00945
#	author tag is already present in the comment, only the first will
Chris L Mason e00945
#	be changed.
Chris L Mason e00945
#
Chris L Mason e00945
# patch-tag -a author -a Subject=patch
Chris L Mason e00945
#	Add an empty author tag and Subject: patch tag to the patch,
Chris L Mason e00945
#	but don't overwrite any existing values if these tags were present
Chris L Mason e00945
#	already.
Chris L Mason e00945
#
Chris L Mason e00945
# patch-tag -a filename
Chris L Mason e00945
#	Read in a list of tags for -a from filename
Chris L Mason e00945
#
Chris L Mason e00945
# patch-tag -A works the same as -a, but always adds the new tag, even
Chris L Mason e00945
#	if one is already present.
Chris L Mason e00945
#
Chris L Mason e00945
# patch-tag -c filename
Chris L Mason e00945
#	Read a whole new comment block from stdin for filename.
Chris L Mason c34d28
# patch-tag -C string filename
Chris L Mason c34d28
#	Replace the non-tag comment with string if it does not exist
Chris L Mason ba7397
# patch-tag -m filename
Chris L Mason ba7397
#	Concatenate multiline tags into one line
Chris L Mason e00945
#
Chris L Mason e00945
# The template files for -a can have comments starting with #.  Only lines
Chris L Mason e00945
# starting with string: will be used as tags.  The tags may have default
Chris L Mason ba7397
# values.  You can also place the template file into ~/.patchtag, it will
Chris L Mason ba7397
# be used automatically
Chris L Mason e00945
#
Chris L Mason e00945
use strict;
Chris L Mason e00945
use Getopt::Long qw(:config no_ignore_case);
Chris L Mason e00945
use File::Temp;
Chris L Mason e00945
use IO::File;
Chris L Mason e00945
Chris L Mason c34d28
my $VERSION = "0.11";
Chris L Mason e00945
Jean Delvare c99488
my $default_comment = "from:subject:patch-mainline:Git-commit:references:signed-off-by:acked-by:reviewed-by:";
Jean Delvare c99488
my $post_comment_tags = "signed-off-by acked-by reviewed-by";
Chris L Mason e00945
Chris L Mason c5851a
# when these are in the bk comment, and non-bk section, use the non-bk one
Chris L Mason c5851a
my $non_dup_tags = "from subject";
Chris L Mason c5851a
Chris L Mason e00945
# command line options
Chris L Mason e00945
my %tags;	  # hash of tags to be replaced
Chris L Mason e00945
my %print_tags;   # hash of tags for printing to stdout
Chris L Mason e00945
my %add_tags;	  # hash of tags to be added if not already present
Chris L Mason e00945
my %always_add_tags; # hash of tags to be added no matter what
Chris L Mason 2020e7
my %delete_tags;  # tags to be deleted entirely
Chris L Mason e00945
my $new_comment;  # boolean, replace comment with data read from stdin
Chris L Mason e00945
my $edit;         # invoke $EDITOR when done
Chris L Mason ba7397
my $multiline;	  # concatenate multiline tags
Chris L Mason e2d213
my $print_comment_only; # print only the comment block
Chris L Mason 118480
my $summary;	  # print output tags in summary form
Chris L Mason 1028a8
my $guard;	  # pattern to use for pulling guards from the filename
Chris L Mason c34d28
my $replace_empty_comment; # new value for empty non-tag comment
Chris L Mason e00945
Chris L Mason e00945
# globals
Chris L Mason c08781
my @output_array; # the finished comment as printed
Chris L Mason c08781
my @all_tags;   # array used to do final tag output
Chris L Mason c08781
my @bk_footer_tags; # holds signed-off-by and acked-by from bk
Chris L Mason e00945
my %replaced_tags; # copy of %tags so we can detect which ones are found
Chris L Mason e00945
my $replace = 0;   # should we overwrite the patch file?
Chris L Mason e00945
my $outfh;	   # current output file handle (could be a temp file)
Chris L Mason e00945
my $infh;	   # current input file handle
Chris L Mason e00945
my @files; 	   # list of all the files to be read
Chris L Mason e00945
my $input; 	   # the current patch file we're reading
Chris L Mason e00945
my $ret;
Chris L Mason c34d28
my $tag_re = '(^[^:\s#]*):(\s+|$)(.*)';
Brandon Philips b2d114
my $git_re = '^From ([0-9a-f]{40}) .*'; # mbox From line by git format-patch
Chris L Mason e00945
Chris L Mason e00945
sub print_usage() {
Chris L Mason e00945
    print STDERR "patch-tag version $VERSION\n";
Chris L Mason c34d28
    print STDERR "usage: patch-tag.pl [-cePms ] [-C val] [-aAtpd tag=val] patch ...\n";
Chris L Mason e00945
    print STDERR "\t--print a given tag\n";
Chris L Mason e00945
    print STDERR "\t--comment replace the comment block with text from stdin\n";
Chris L Mason c34d28
    print STDERR "\t--Comment val replace non-tag comment with val if it does not exist\n";
Chris L Mason e00945
    print STDERR "\t--edit invoke \$EDITOR on each file after processing\n";
Chris L Mason 2020e7
    print STDERR "\t--delete tag delete tag from header\n";
Chris L Mason e00945
    print STDERR "\t--tag tag[=value] Replace or add a given tag\n";
Chris L Mason e00945
    print STDERR "\t--add tag[=value] Add a tag if not already present\n";
Chris L Mason e00945
    print STDERR "\t--Add tag[=value] Unconditionally add a tag\n";
Chris L Mason e00945
    print STDERR "\t--add filename containing template of tags to add\n";
Chris L Mason ba7397
    print STDERR "\t\t~/.patchtag will be used as a default template file\n";
Chris L Mason ba7397
    print STDERR "\t--multiline concatenate multiline tags into one line\n";
Chris L Mason e2d213
    print STDERR "\t--Print-comment prints only the comment block without tags\n";
Chris L Mason 118480
    print STDERR "\t--summary print output tags in summary form\n";
Chris L Mason e00945
    print STDERR "\nAll options can be specified more than once, example\n";
Chris L Mason e00945
    print STDERR "usage and additional docs at the top of this script\n";
Chris L Mason e00945
    exit(1);
Chris L Mason e00945
}
Chris L Mason e00945
Chris L Mason e00945
# we want the hashes of tags in lower case, normalize whatever
Chris L Mason e00945
# crud the user sent us
Chris L Mason e00945
#
Chris L Mason e00945
sub lc_hash(%) {
Chris L Mason e00945
    my (%h) = (@_);
Chris L Mason e00945
    my %lch;
Chris L Mason e00945
    my $tag;
Chris L Mason e00945
    my $value;
Chris L Mason e00945
    foreach my $k (keys(%h)) {
Chris L Mason e00945
	$tag = lc($k);
Chris L Mason e00945
	$value = $h{$k};
Chris L Mason e00945
	# did they use --opt "tag: value"? If so, turn it into a tag value pair
Chris L Mason e00945
	if (($tag =~ m/(.+[^:]):\s*(.+)/) && $value eq "") {
Chris L Mason e00945
	    $tag = $1;
Chris L Mason e00945
	    $value = $2;
Chris L Mason e00945
	}
Chris L Mason e00945
	# strip off any : in the tag
Chris L Mason e00945
	$tag =~ s/://g;
Chris L Mason e00945
        $lch{$tag} = $value;
Chris L Mason e00945
    }
Chris L Mason e00945
    return %lch;
Chris L Mason e00945
}
Chris L Mason e00945
Chris L Mason ba7397
# check for and collect a multiline tag from the input stream
Chris L Mason ba7397
sub peek_multi_line($$$) {
Chris L Mason ba7397
    my ($infh, $buf, $line) = @_;
Chris L Mason ba7397
    my $next;
Chris L Mason ba7397
Chris L Mason ba7397
    $next = read_next_line($infh, $buf);
Chris L Mason 8286d5
    while($next =~ m/^\s/ && !($next =~ m/^\n/)) {
Chris L Mason ba7397
	if ($multiline) {
Chris L Mason ba7397
	    chomp $line;
Chris L Mason ba7397
	}
Chris L Mason ba7397
        $line .= $next;
Chris L Mason ba7397
	$next = read_next_line($infh, $buf);
Chris L Mason ba7397
    }
Chris L Mason ba7397
    push @$buf, $next;
Chris L Mason ba7397
    return $line;
Chris L Mason ba7397
}
Chris L Mason ba7397
Chris L Mason ba7397
# do tag replacement and other checks for a specific tag/value pair
Chris L Mason ba7397
# pushing it into the output tag array
Chris L Mason c08781
sub process_tag($$) {
Chris L Mason c08781
    my ($t, $value) = @_;
Chris L Mason ba7397
    # only do replacement on the first tag with a given key
Chris L Mason ba7397
    if (defined($tags{$t}) && defined($replaced_tags{$t})) {
Chris L Mason ba7397
	$value = $tags{$t};
Chris L Mason c08781
	push_output_tag($t, $value);
Chris L Mason ba7397
    } elsif (defined($print_tags{$t})) {
Chris L Mason c08781
	push_output_tag($t, $value);
Chris L Mason ba7397
    } elsif (!%print_tags) {
Chris L Mason c08781
	push_output_tag($t, $value);
Chris L Mason ba7397
    }
Chris L Mason ba7397
    delete $replaced_tags{$t};
Chris L Mason ba7397
}
Chris L Mason ba7397
Chris L Mason c08781
# tags that get pulled from bk comments get special treatment
Chris L Mason c08781
sub process_bk_tag($$) {
Chris L Mason c08781
    my ($tag, $value) = @_;
Jean Delvare c99488
    my $always_tags = "signed-off-by acked-by reviewed-by";
Chris L Mason c08781
Chris L Mason c08781
    if ($always_tags =~ m/$tag/) {
Chris L Mason c08781
	push @bk_footer_tags, [$tag, $value];
Chris L Mason c08781
	return;
Chris L Mason c08781
    }
Chris L Mason c08781
    # don't pull the bk tag out if it already exists
Chris L Mason c08781
    foreach my $v (@all_tags) {
Chris L Mason c34d28
	$v =~ m/$tag_re/;
Chris L Mason c08781
	my $t = $1;
Chris L Mason c08781
	if (lc($t) eq $tag) {
Chris L Mason c08781
	    return;
Chris L Mason c08781
	}
Chris L Mason c08781
    }
Chris L Mason c08781
    $replaced_tags{$tag} = $value;
Chris L Mason c08781
}
Chris L Mason c08781
Chris L Mason e00945
# look for any tags that we were asked to print or replace from
Chris L Mason e00945
# the command line.  Build the array of tags found in the comment
Chris L Mason ba7397
sub check_tags($$$) {
Chris L Mason ba7397
    my ($infh, $buf, $line) = @_;
Chris L Mason e00945
    my $filespec = "";
Chris L Mason ba7397
    my $bk_comment = 0;
Chris L Mason ba7397
    my $orig_line;
Chris L Mason e4801c
    my $bkold_style = 0;
Chris L Mason e00945
Chris L Mason ba7397
again:
Chris L Mason ba7397
    $orig_line = $line;
Chris L Mason ba7397
    if ($bk_comment) {
Chris L Mason e4801c
        $line =~ s/^#\s*//;
Chris L Mason ba7397
    }
Brandon Philips b2d114
    # Preserve git From line
Brandon Philips b2d114
    if ($line =~ m/$git_re/) {
Brandon Philips b2d114
        push @output_array, $orig_line;
Brandon Philips b2d114
        $line = read_next_line($infh, $buf);
Brandon Philips b2d114
        goto again;
Brandon Philips b2d114
    }
Chris L Mason c34d28
    if ($line =~ m/$tag_re/) {
Chris L Mason ba7397
	$line = peek_multi_line($infh, $buf, $line);
Chris L Mason ba7397
	# evaluate again in case the multi-line string changed
Chris L Mason ba7397
	# check it as a multi line re.  Clean up trailing newlines and
Chris L Mason ba7397
	# ws
Chris L Mason ba7397
	$line =~ s/[\s\n]*$//gs;
Chris L Mason ba7397
	$line =~ m/(^[^:\s#]*):\s*(.*)/s;
Chris L Mason c08781
	my $lc_tag = lc($1);
Chris L Mason c08781
	my $value = $2;
Chris L Mason c08781
	if ($bk_comment) {
Chris L Mason c08781
	    # only pull out specific tags from the bk comment stream
Chris L Mason c08781
	    if ($post_comment_tags =~ m/$lc_tag/) {
Chris L Mason c08781
		process_bk_tag($lc_tag, $value);
Chris L Mason c08781
	    }
Chris L Mason c08781
	    if (!%print_tags) {
Chris L Mason c08781
		push @output_array, $orig_line;
Chris L Mason c08781
	    }
Chris L Mason c08781
	} else {
Chris L Mason c08781
	    process_tag($lc_tag, $value);
Chris L Mason e00945
	}
Chris L Mason e00945
    } elsif (!%print_tags) {
Chris L Mason c08781
	push @output_array, $orig_line;
Chris L Mason ba7397
    } 
Chris L Mason ba7397
    # did we find a bitkeeper style patch header?
Chris L Mason ba7397
    # if so, just parse the whole thing here
Chris L Mason e4801c
    if ($line =~ m/^# The following is the BitKeeper ChangeSet Log/ ||
Chris L Mason e4801c
        $line =~ m/^# This is a BitKeeper generated diff -Nru style patch/) {
Chris L Mason e4801c
	# there are two bk patch styles
Chris L Mason e4801c
	# old:
Chris L Mason e4801c
	# # -------------------
Chris L Mason e4801c
	# # date author changset
Chris L Mason e4801c
	# # subject
Chris L Mason e4801c
	# new:
Chris L Mason e4801c
	# #
Chris L Mason e4801c
	# # Changeset
Chris L Mason e4801c
	# # date time author
Chris L Mason e4801c
	# # subject
Chris L Mason e4801c
Chris L Mason ba7397
	$bk_comment = 1;
Chris L Mason ba7397
	my $next = read_next_line($infh, $buf);
Chris L Mason c08781
	push @output_array, $next if (!%print_tags);
Chris L Mason e4801c
	if ($next =~ m/^# ---------/) {
Chris L Mason e4801c
	    $bkold_style = 1;
Chris L Mason e4801c
	} else {
Chris L Mason e4801c
	    # read empty line
Chris L Mason e4801c
	    $next = read_next_line($infh, $buf);
Chris L Mason c08781
	    push @output_array, $next if (!%print_tags);
Chris L Mason e4801c
	}
Chris L Mason ba7397
	$next = read_next_line($infh, $buf);
Chris L Mason c08781
	push @output_array, $next if (!%print_tags);
Chris L Mason e4801c
	my @words = split /\s+/, $next;
Chris L Mason e4801c
	if ($bkold_style) {
Chris L Mason c08781
	    process_bk_tag('from', $words[2]);
Chris L Mason e4801c
	} else {
Chris L Mason c08781
	    process_bk_tag('from', $words[3]);
Chris L Mason e4801c
	}
Chris L Mason ba7397
	$next = read_next_line($infh, $buf);
Chris L Mason c08781
	push @output_array, $next if (!%print_tags);
Chris L Mason ba7397
	chomp($next);
Chris L Mason e4801c
	$next =~ s/^#\s*//;
Chris L Mason c34d28
	# sometimes the bk comment is empty and there is just a filename
Chris L Mason c34d28
	# there's no good way to tell.
Chris L Mason c34d28
	if (!($next =~ m/(.*\/)+?.*?\.(c|h|s)$/)) {
Chris L Mason c34d28
	    process_bk_tag('subject', $next);
Chris L Mason c34d28
	}
Chris L Mason ba7397
Chris L Mason ba7397
	# we've read the from tag and subject tag, the goto again
Chris L Mason e4801c
	# will loop through the ChangeSet Log section looking
Chris L Mason ba7397
	# for other tags
Chris L Mason ba7397
    }
Chris L Mason e4801c
    if ($bk_comment) {
Chris L Mason ba7397
        $line = read_next_line($infh, $buf);
Chris L Mason e4801c
	# old style bk comments end with a line full of dashes
Chris L Mason e4801c
	# new style bk comments end with a # filename or no # at all
Chris L Mason e4801c
        if (($bkold_style && $line =~ m/^# --------/) ||
Chris L Mason e4801c
	    (!$bkold_style && $line =~ m/^# \S|^[^#]/)) {
Chris L Mason e4801c
	    push @$buf, $line;
Chris L Mason e4801c
	    return;
Chris L Mason e4801c
	}
Chris L Mason ba7397
        goto again;
Chris L Mason e00945
    }
Chris L Mason e00945
}
Chris L Mason e00945
Chris L Mason e00945
# print the array of tags found in the comment
Chris L Mason c08781
sub print_output_array($$) {
Chris L Mason 79142a
    my ($input_file, $array) = @_;
Chris L Mason e00945
    my $filespec = "";
Chris L Mason e00945
Chris L Mason 79142a
    # if there is more then one file, include some info about which file
Chris L Mason 79142a
    # we're printing tags from
Chris L Mason 118480
    if (scalar(@files) > 1 && %print_tags && !$summary) {
Chris L Mason e00945
        $filespec = "$input_file: ";
Chris L Mason 118480
    } elsif ($summary) {
Chris L Mason 118480
        $filespec = "# ";
Chris L Mason e00945
    }
Chris L Mason 79142a
    foreach my $s (@$array) {
Chris L Mason 118480
	if ($summary) {
Chris L Mason 118480
	    $s =~ s/\n\s/\n#\t/mg;
Chris L Mason 118480
	}
Chris L Mason c08781
	print $outfh "${filespec}$s";
Chris L Mason e00945
    }
Chris L Mason 118480
    if ($summary) {
Chris L Mason 118480
	print $outfh "$input_file\n\n";
Chris L Mason 118480
    }
Chris L Mason e00945
}
Chris L Mason e00945
Chris L Mason e00945
# for -a and -A, look for a filename as an arg instead of a tag.  If found
Chris L Mason e00945
# fill in the hash with the contents of the file
Chris L Mason e00945
#
Chris L Mason e00945
sub fill_hash_from_file($) {
Chris L Mason e00945
    my ($h) = @_;
Chris L Mason e00945
    # look for tags to add either from the command line or template files
Chris L Mason e00945
    if (%$h && keys(%$h) <= 1) {
Chris L Mason e00945
	my ($k, $v) = each %$h;
Chris L Mason e00945
	my $source;
Chris L Mason e00945
	if (defined($k) && (!defined($v) || $v eq "")) {
Chris L Mason e00945
	    if (-f $k) {
Chris L Mason e00945
		delete($h->{$k});
Chris L Mason e00945
		$source = $k;
Chris L Mason e00945
	    }
Chris L Mason e00945
	}
Chris L Mason e00945
	if (defined($source)) {
Chris L Mason e00945
	    print STDERR "Using $source as tag template source\n";
Chris L Mason e00945
	    # delete($$h{$k});
Chris L Mason e00945
	    my $template = new IO::File;
Chris L Mason e00945
	    $template->open("<$source") || die "Unable to open $source for reading";
Chris L Mason e00945
	    while(<$template>) {
Chris L Mason e00945
		# eat comments
Chris L Mason e00945
		s/#.*//;
Chris L Mason e00945
		# eat ws at the start of the line
Chris L Mason e00945
		s/^\s//;
Chris L Mason c34d28
    		if (m/$tag_re/) {
Chris L Mason c34d28
		    $h->{lc($1)} = $3;
Chris L Mason e00945
		}
Chris L Mason e00945
	    }
Chris L Mason e00945
	    $template->close();
Chris L Mason 79142a
	    return 1;
Chris L Mason e00945
	}
Chris L Mason e00945
    }
Chris L Mason 79142a
    return 0;
Chris L Mason 79142a
}
Chris L Mason 79142a
Chris L Mason 79142a
# helper function to pick the proper output array for the tags
Chris L Mason 79142a
# some go before the comment block and some go after
Chris L Mason c5851a
# send $allow_dup = 0 if you want to prevent duplicate tag names.
Chris L Mason c5851a
# Completely duplicate tag name,value pairs are always removed.
Chris L Mason c08781
sub push_output_tag($$) {
Chris L Mason c08781
    my ($tag, $value) = @_;
Chris L Mason c5851a
    my $uc_tag = ucfirst($tag);
Chris L Mason c5851a
    my $string = $uc_tag . ": $value";
Chris L Mason 2020e7
    # check against the delete hash
Chris L Mason 2020e7
    if (defined($delete_tags{$tag})) {
Chris L Mason 2020e7
        return;
Chris L Mason 2020e7
    }
Chris L Mason ba7397
    # check for dups;
Chris L Mason c08781
    foreach my $v (@all_tags) {
Chris L Mason ba7397
        if ($v eq $string) {
Chris L Mason ba7397
	    return;
Chris L Mason ba7397
	}
Chris L Mason 79142a
    }
Chris L Mason c08781
    push @all_tags, $string;
Chris L Mason c08781
    push @output_array, "$string\n";
Chris L Mason e00945
}
Chris L Mason e00945
Chris L Mason ba7397
# helper function to cherry pick output tags from a hash.
Chris L Mason 79142a
# This is used to print the From and Subject Tags first.
Chris L Mason e00945
sub add_output_tag ($$) {
Chris L Mason e00945
    my ($tag, $h) = @_;
Chris L Mason c08781
    my $value;
Chris L Mason c08781
    my $tag_end = 0;
Chris L Mason c08781
    my $line;
Chris L Mason c08781
Chris L Mason c08781
    if (!defined($$h{$tag})) {
Chris L Mason c08781
        return;
Chris L Mason c08781
    }
Chris L Mason c08781
    $value = $$h{$tag};
Chris L Mason c08781
    delete($$h{$tag});
Chris L Mason c08781
    # for post comment tags, just tack it onto the very end.
Chris L Mason c08781
    if ($post_comment_tags =~ m/$tag/) {
Chris L Mason c08781
	process_tag($tag, $value);
Chris L Mason c08781
	return;
Chris L Mason c08781
    }
Chris L Mason c08781
Chris L Mason c08781
    # find the end of the top tag section in the comment.
Chris L Mason c08781
    foreach $line (@output_array) {
Brandon Philips b2d114
	# Account git From line as tag header
Brandon Philips b2d114
	if ($line =~ m/$git_re/) {
Brandon Philips b2d114
	    $tag_end++;
Brandon Philips b2d114
	} elsif ($line =~ m/$tag_re/) {
Chris L Mason c08781
	    my $t = lc($1);
Chris L Mason c08781
	    # did we find our way into the post comment tag section?
Chris L Mason c08781
	    if ($post_comment_tags =~ m/$t/) {
Chris L Mason c08781
	        last;
Chris L Mason c08781
	    }
Chris L Mason c08781
	    $tag_end++;
Chris L Mason c08781
	} else {
Chris L Mason c08781
	    last;
Chris L Mason c08781
	}
Chris L Mason e00945
    }
Chris L Mason c08781
    my @tmp_array = @output_array[$tag_end .. scalar(@output_array)-1];
Chris L Mason c08781
    $#output_array = $tag_end - 1;
Chris L Mason c08781
    process_tag($tag, $value);
Chris L Mason c08781
    push @output_array, @tmp_array;
Chris L Mason e00945
}
Chris L Mason e00945
Chris L Mason c08781
# from the much harder then it should be category.  Walk the 
Chris L Mason c08781
# comment block and divide it into three parts.  Header, comment,
Chris L Mason c08781
# footer.  Make sure each part is separated by no more then one
Chris L Mason c08781
# blank line.
Chris L Mason c08781
#
Chris L Mason c08781
sub cleanup_blank_lines($) {
Chris L Mason c08781
    my ($ar) = @_;
Chris L Mason c08781
    my $line;
Chris L Mason c08781
    my $tag_end;
Chris L Mason c08781
    my $footer_start;
Chris L Mason c08781
    my @header_ar = ();
Chris L Mason c08781
    my @comment_ar = ();
Chris L Mason c08781
    my @footer_ar = ();
Chris L Mason c08781
Chris L Mason c08781
    # find the header
Chris L Mason c08781
    foreach $line (@$ar) {
Chris L Mason c34d28
	if ($line =~ m/$tag_re/) {
Chris L Mason c08781
	    my $t = lc($1);
Chris L Mason c08781
	    # did we find our way into the post comment tag section?
Chris L Mason c08781
	    if ($post_comment_tags =~ m/$t/) {
Chris L Mason c08781
	        last;
Chris L Mason c08781
	    }
Chris L Mason c08781
	    $tag_end++;
Chris L Mason c08781
	} else {
Chris L Mason c08781
	    last;
Chris L Mason c08781
	}
Chris L Mason c08781
    }
Chris L Mason c08781
    if ($tag_end) {
Chris L Mason c08781
	@header_ar = @$ar[0 .. $tag_end-1];
Chris L Mason c08781
    }
Chris L Mason c08781
    # eat all the blank lines
Chris L Mason c08781
    while($tag_end < scalar(@$ar)) {
Chris L Mason c08781
        $line = $$ar[$tag_end];
Chris L Mason c08781
	if ($line =~ m/^\s*\n$/) {
Chris L Mason c08781
	    $tag_end++;
Chris L Mason c08781
	} else {
Chris L Mason c08781
	    last;
Chris L Mason c08781
	}
Chris L Mason c08781
    }
Chris L Mason c08781
    # $tag_end is now the start of the comment block.
Chris L Mason c08781
    # pop ws off the end of the output array;
Chris L Mason c08781
    while($$ar[scalar(@$ar)-1] =~ m/^\s*\n$/) {
Chris L Mason c08781
        my $t = pop @$ar;
Chris L Mason c08781
    }
Chris L Mason c08781
Chris L Mason c08781
    # walk up the array to find the end of the footer.
Chris L Mason c08781
    for($footer_start = scalar(@$ar) - 1 ; $footer_start >= $tag_end;
Chris L Mason c08781
        $footer_start--) {
Chris L Mason c08781
        $line = $ar->[$footer_start];
Chris L Mason c34d28
	if (!($line =~ m/$tag_re/)) {
Chris L Mason c08781
	    last;
Chris L Mason c08781
	}
Chris L Mason c08781
    }
Chris L Mason c08781
    @footer_ar = @$ar[$footer_start+1 .. scalar(@$ar)-1];
Chris L Mason c08781
    # eat ws between the comment and the footer
Chris L Mason c08781
    while($footer_start > $tag_end && 
Chris L Mason c08781
          $ar->[$footer_start] =~ m/^\s*\n$/) {
Chris L Mason c08781
        $footer_start--;
Chris L Mason c08781
    }
Chris L Mason c08781
Chris L Mason c08781
    @comment_ar = @$ar[$tag_end .. $footer_start];
Chris L Mason c08781
    if ($print_comment_only) {
Chris L Mason c08781
        @$ar = @comment_ar;
Chris L Mason c08781
	return;
Chris L Mason c08781
    }
Chris L Mason c34d28
    if (defined($replace_empty_comment) && scalar(@comment_ar) == 0) {
Chris L Mason c34d28
        push @comment_ar, "$replace_empty_comment\n";
Chris L Mason c34d28
    }
Chris L Mason c08781
    @$ar = @header_ar;
Chris L Mason c08781
    if (scalar(@comment_ar)) {
Chris L Mason c08781
	if (scalar(@header_ar)) {
Chris L Mason c08781
	    push @$ar, "\n";
Chris L Mason c08781
	}
Chris L Mason c08781
	push @$ar, @comment_ar;
Chris L Mason c08781
    }
Chris L Mason c08781
    if (scalar(@footer_ar)) {
Chris L Mason c08781
        push @$ar, "\n";
Chris L Mason c08781
	push @$ar, @footer_ar;
Chris L Mason c08781
    }
Chris L Mason c08781
    if ($replace) {
Chris L Mason c08781
        push @$ar, "\n";
Chris L Mason c08781
    }
Chris L Mason c08781
}
Chris L Mason ba7397
# read a line from $fh, using anything queued up in @$buf first
Chris L Mason ba7397
#
Chris L Mason ba7397
sub read_next_line($$) {
Chris L Mason ba7397
    my ($fh, $buf) = @_;
Chris L Mason ba7397
    my $line;
Chris L Mason ba7397
Chris L Mason ba7397
    $line = pop @$buf;
Chris L Mason ba7397
    if (!defined($line)) {
Chris L Mason ba7397
        $line = $fh->getline();    
Chris L Mason ba7397
    }
Chris L Mason ba7397
    return $line;
Chris L Mason ba7397
}
Chris L Mason ba7397
Chris L Mason ba7397
# diff reading state machine.  When it returns a state of "done"
Chris L Mason ba7397
# that means the line is the start of the diff.  Any state other then
Chris L Mason ba7397
# "comment" might be the start of the diff, the only way to know for
Chris L Mason ba7397
# sure is to check the following line.
Chris L Mason ba7397
#
Chris L Mason ba7397
sub process_line($$) {
Chris L Mason ba7397
    my ($line, $state) = @_;
Chris L Mason ba7397
    my $return_state = "";
Chris L Mason ba7397
Chris L Mason ba7397
    # bk uses this: ===== fs/reiserfs/inode.c 1.49 vs 1.50 ===== 
Chris L Mason ba7397
    if ($line =~ m/(^Index:)|(^=====.*vs.*=====$)/) {
Chris L Mason ba7397
	$return_state = "index";
Chris L Mason ba7397
    } elsif ($line =~ m/^=================/ && $state eq "index") {
Chris L Mason ba7397
	$return_state = "done";
Chris L Mason ba7397
    } elsif ($line =~ m/^diff/) {
Chris L Mason ba7397
	$return_state = "diff";
Chris L Mason ba7397
    } elsif ($line =~ m/(^---)|(^\+\+\+)/) {
Chris L Mason ba7397
	$return_state = "done";
Chris L Mason ba7397
    } elsif ($state ne "comment") {
Chris L Mason ba7397
	$return_state = "comment";
Chris L Mason ba7397
    } else {
Chris L Mason ba7397
	$return_state = "comment";
Chris L Mason ba7397
    }
Chris L Mason ba7397
    return $return_state;
Chris L Mason ba7397
}
Chris L Mason ba7397
Chris L Mason e00945
$ret = GetOptions("add:s%" => \%add_tags,
Chris L Mason e00945
		  "Add:s%" => \%always_add_tags,
Chris L Mason 2020e7
		  "delete:s%" => \%delete_tags,
Chris L Mason e00945
		  "edit" => \$edit,
Chris L Mason 1028a8
		  "guard=s" => \$guard,
Chris L Mason e00945
		  "tag:s%" => \%tags,
Chris L Mason e00945
		  "print:s" => \%print_tags,
Chris L Mason e2d213
		  "Print-comment" => \$print_comment_only,
Chris L Mason e00945
		  "comment" => \$new_comment,
Chris L Mason c34d28
		  "Comment=s" => \$replace_empty_comment,
Chris L Mason ba7397
		  "multiline" => \$multiline,
Chris L Mason 118480
		  "summary" => \$summary,
Chris L Mason e00945
		  ) || print_usage();
Chris L Mason e00945
Chris L Mason e00945
@files = @ARGV;
Chris L Mason e00945
Chris L Mason e00945
if (scalar(@ARGV) < 1) {
Chris L Mason e00945
    print_usage();
Chris L Mason e00945
}
Chris L Mason e00945
Chris L Mason e00945
if ($new_comment && scalar(@ARGV) > 1) {
Chris L Mason e00945
    print STDERR "error: --comment can only be used on one file at a time\n";
Chris L Mason e00945
    print_usage();
Chris L Mason e00945
}
Chris L Mason e00945
Chris L Mason e00945
fill_hash_from_file(\%add_tags);
Chris L Mason e00945
fill_hash_from_file(\%always_add_tags);
Chris L Mason e00945
Chris L Mason 118480
if ($summary && !%print_tags) {
Chris L Mason 118480
    $print_tags{'subject'} = 1;
Chris L Mason 118480
    $print_tags{'references'} = 1;
Chris L Mason 118480
    $print_tags{'suse-bugzilla'} = 1;
Chris L Mason 118480
}
Chris L Mason 118480
Chris L Mason 79142a
# if we're in edit mode and no tags are provided, check for a default
Chris L Mason 79142a
# template file.
Chris L Mason 79142a
if ($edit && keys(%tags) == 0 && keys(%add_tags) == 0 && 
Chris L Mason 79142a
    keys(%always_add_tags) == 0 && 
Chris L Mason 79142a
    -r "$ENV{'HOME'}/.patchtag") {
Chris L Mason 79142a
    $add_tags{"$ENV{'HOME'}/.patchtag"} = "";    
Chris L Mason 79142a
    fill_hash_from_file(\%add_tags);
Chris L Mason 79142a
}
Chris L Mason 79142a
Chris L Mason e00945
# never overwrite the original when --print is used
Chris L Mason e00945
#
Chris L Mason 2020e7
if ((%add_tags || %always_add_tags || %tags || 
Chris L Mason c34d28
     $new_comment || $edit || %delete_tags || $replace_empty_comment) && 
Chris L Mason e2d213
    !(%print_tags || $print_comment_only)) {
Chris L Mason e00945
    $replace = 1;
Chris L Mason e00945
}
Chris L Mason e00945
Chris L Mason e00945
%tags = lc_hash(%tags);
Chris L Mason e00945
%print_tags = lc_hash(%print_tags);
Chris L Mason e00945
%add_tags = lc_hash(%add_tags);
Chris L Mason e00945
%always_add_tags = lc_hash(%always_add_tags);
Chris L Mason 2020e7
%delete_tags = lc_hash(%delete_tags);
Chris L Mason e00945
Chris L Mason e00945
# if we're editing setup the default tags
Chris L Mason 79142a
if ($edit && keys(%add_tags) == 0) {
Chris L Mason e00945
    my @words = split /:/, $default_comment;
Chris L Mason e00945
    foreach my $w (@words) {
Chris L Mason 79142a
	$add_tags{$w} = "";
Chris L Mason e00945
    }
Chris L Mason e00945
}
Chris L Mason e00945
Chris L Mason 1028a8
foreach my $guarded_input (@files) {
Chris L Mason e00945
    my $last = "";
Chris L Mason ba7397
    my $scan_state = "comment";
Chris L Mason ba7397
    my @input_buffer = ();
Chris L Mason 1028a8
    my $input;
Chris L Mason 1028a8
Chris L Mason 1028a8
    if ($summary && $guard && $guarded_input =~ m/($guard)(.+)/) {
Chris L Mason 1028a8
        $input = $2;
Chris L Mason 1028a8
    } else {
Chris L Mason 1028a8
        $input = $guarded_input;
Chris L Mason 1028a8
    }
Chris L Mason e00945
    $infh = new IO::File;
Chris L Mason e00945
    $infh->open("<$input") || die "Unable to open $input for reading";
Chris L Mason e00945
    %replaced_tags = (%tags, %add_tags);
Chris L Mason c08781
    @all_tags = ();
Chris L Mason c08781
    @output_array = ();
Chris L Mason c08781
    @bk_footer_tags = ();
Chris L Mason c34d28
    my %tmp_always_add = %always_add_tags;
Chris L Mason e00945
Chris L Mason e00945
    if ($replace) {
Chris L Mason e00945
	$outfh = new File::Temp(TEMPLATE => "$input.XXXXXX", UNLINK => 0) || 
Chris L Mason e00945
		 die "Unable to create temp file";
Chris L Mason e00945
    } else {
Chris L Mason e00945
	$outfh = new IO::File;
Chris L Mason e00945
	$outfh->open(">-") || die "Unable to open stdout";
Chris L Mason e00945
    }
Chris L Mason e00945
Chris L Mason e00945
    # loop through until the start of the diff.
Chris L Mason ba7397
    while($_ = read_next_line($infh, \@input_buffer)) {
Chris L Mason ba7397
	$scan_state = process_line($_, $scan_state);
Chris L Mason ba7397
	if ($scan_state eq "done")  {
Chris L Mason ba7397
	    push @input_buffer, $_;
Chris L Mason e00945
	    last;
Chris L Mason ba7397
	}
Chris L Mason ba7397
	if ($scan_state ne "comment") {
Chris L Mason ba7397
	    my $next = read_next_line($infh, \@input_buffer);
Chris L Mason ba7397
	    $scan_state = process_line($next, $scan_state);
Chris L Mason ba7397
	    if ($scan_state ne "comment") {
Chris L Mason ba7397
	        push @input_buffer, $next;
Chris L Mason ba7397
	        push @input_buffer, $_;
Chris L Mason ba7397
		last;
Chris L Mason e00945
	    }
Chris L Mason ba7397
	    push @input_buffer, $next;
Chris L Mason e00945
	}
Chris L Mason ba7397
	check_tags($infh, \@input_buffer, $_);
Chris L Mason e00945
    }
Chris L Mason c08781
    # pop ws off the end of the output array;
Chris L Mason c08781
    while($output_array[scalar(@output_array)-1] =~ m/^\s*\n$/) {
Chris L Mason c08781
        pop @output_array;
Chris L Mason c08781
    }
Chris L Mason c08781
    foreach my $h (@bk_footer_tags) {
Chris L Mason c08781
	process_tag($h->[0], $h->[1]);
Chris L Mason c08781
    }
Chris L Mason e00945
    # add any new tags left over, but do From and Subject first
Chris L Mason e00945
    add_output_tag('from', \%replaced_tags);
Chris L Mason e00945
    add_output_tag('subject', \%replaced_tags);
Chris L Mason e00945
    foreach my $h (sort(keys(%replaced_tags))) {
Chris L Mason 79142a
	add_output_tag($h, \%replaced_tags);
Chris L Mason e00945
    }
Chris L Mason e00945
    # add any of the tags from -A 
Chris L Mason c34d28
    foreach my $h (sort(keys(%tmp_always_add))) {
Chris L Mason c34d28
	add_output_tag($h, \%tmp_always_add);
Chris L Mason e00945
    }
Chris L Mason e00945
    # replace the comment entirely for -c
Chris L Mason e00945
    if ($new_comment) {
Chris L Mason e00945
        while(<STDIN>) {
Chris L Mason e00945
	    print $outfh $_;
Chris L Mason e00945
	}
Chris L Mason e00945
    } else {
Chris L Mason c08781
	cleanup_blank_lines(\@output_array);
Chris L Mason c08781
	print_output_array($guarded_input, \@output_array);
Chris L Mason e00945
    }
Chris L Mason e00945
    # in replace mode, copy our temp file over the original.
Chris L Mason e00945
    if ($replace) {
Chris L Mason ba7397
	while($_ = read_next_line($infh, \@input_buffer)) {
Chris L Mason e00945
	    print $outfh $_;
Chris L Mason e00945
	}
Chris L Mason e00945
	unlink "$input" || die "Unable to unlink $input";
Chris L Mason e00945
	rename $outfh->filename, $input || 
Chris L Mason e00945
	       die "Unable to rename $outfh->filename to $input";
Chris L Mason e00945
    }
Chris L Mason e00945
    if ($edit) {
Chris L Mason e00945
	my $editor = "vi";
Chris L Mason e00945
	if (defined($ENV{'EDITOR'})) {
Chris L Mason e00945
	    $editor = $ENV{'EDITOR'};
Chris L Mason e00945
	}
Chris L Mason e00945
        $ret = system("$editor $input");
Chris L Mason e00945
	if ($ret) {
Chris L Mason e00945
	    $ret = $ret >> 8;
Chris L Mason e00945
	    print STDERR "warning $editor exited with $ret\n";
Chris L Mason e00945
	}
Chris L Mason e00945
    }
Chris L Mason e00945
    $infh->close();
Chris L Mason e00945
    $outfh->close();
Chris L Mason e00945
}