#!/usr/bin/perl
# Filename:	bugz
# Author:	David Ljung Madison <DaveSource.com>
# See License:	http://MarginalHacks.com/License/
# Description:	Fetch and output bugs from a bugzilla site
use strict;

##################################################
# Setup the variables
##################################################
my $PROGNAME = $0; $PROGNAME =~ s|.*/||;
my ($BASENAME,$PROGNAME) = ($0 =~ m|(.*)/(.+)|) ? ($1?$1:'/',$2) : ('.',$0);

# Bugzilla info here (or as commandline args)
my $DEFAULT_OPTS = {
	user        =>  '',
		# Password can be optionally saved here
	pass        =>  '',
		# Cookies file (if not in .mozilla)
	cookies     =>  '',
		# The bugzilla URL (ends in .cgi)
	bugzilla    =>  'https//some.site.somewhere/bugzilla/buglist.cgi',
		# The default output format (with %keys%)
	fmt         =>  'Bug %bug_id%: %priority% %bug_status%/%resolution% %short_short_desc%\n',
};

my $WGET = 'wget';

##################################################
# Usage
##################################################
sub fatal {
	foreach my $msg (@_) { print STDERR "[$PROGNAME] ERROR:  $msg\n"; }
	exit(-1);
}

sub usage {
	foreach my $msg (@_) { print STDERR "ERROR:  $msg\n"; }
	print STDERR <<USAGE;

Usage:\t$PROGNAME [-d] [-options]
  Fetch and output bugs from a bugzilla site
  
  -user           Bugzilla server username
  -pass           Bugzilla server password
  -cookies        alternate cookie file (if not in .mozilla/.netscape)
  -bugzilla       URL of bugzilla server
  -status         Status of bugs to print
  -component      Limit to specific components
  -product        Limit to specific products
  -fmt            Output format (see below)
  -d              Set debug mode

Multiple -status/-component/-product options can be given:

% $PROGNAME -status CLOSED -status RESOLVED

Also, the following status "aliases" are available:
  -status ALL     Show all bugs [default]
  -status OPEN    Show all bugs that aren't state "CLOSED"

Output format can be any string, and keys enclosed in '%' will be expanded:

% $PROGNAME -fmt "Bug id %bug_id% is currently %bug_status% and has description %short_short_desc%"

Known output format keys:
  %bug_id%              The bug number
  %bug_status%          NEW, CLOSED, RESOLVED, etc..
  %priority%            P1, P2, ...
  %assigned_to%         Email of bug owner
  %resolution%          FIXED, INVALID, DUPLICATE, ..
  %short_short_desc%    The bug "subject" or short description
  %bug_severity%        major, minor, etc...
  %op_sys%              Operating system

USAGE
	exit -1;
}

sub parse_args {
	my $opt = $DEFAULT_OPTS;
	while (my $arg=shift(@ARGV)) {
		if ($arg =~ /^-h$/) { usage(); }
		if ($arg =~ /^-user$/) { $opt->{user} = shift @ARGV; next; }
		if ($arg =~ /^-pass$/) { $opt->{pass} = shift @ARGV; next; }
		if ($arg =~ /^-cookies$/) { $opt->{cookies} = shift @ARGV; next; }
		if ($arg =~ /^-bugzilla$/) { $opt->{bugzilla} = shift @ARGV; next; }
		if ($arg =~ /^-status$/) { push(@{$opt->{status}},shift @ARGV); next; }
		if ($arg =~ /^-component$/) { push(@{$opt->{component}},shift @ARGV); next; }
		if ($arg =~ /^-product$/) { push(@{$opt->{product}},shift @ARGV); next; }
		if ($arg =~ /^-fmt$/) { $opt->{fmt} = shift @ARGV; next; }
		if ($arg =~ /^-d$/) { $MAIN::DEBUG=1; next; }
		if ($arg =~ /^-/) { usage("Unknown option: $arg"); }
		usage("Unknown option: $arg\n");
	}
	$opt;
}

sub debug {
	return unless $MAIN::DEBUG;
	foreach my $msg (@_) { print STDERR "[$PROGNAME] $msg\n"; }
}

##################################################
# Main code
##################################################
sub cookies() {
	my ($opt) = @_;
	my @cookies;
	push(@cookies,$opt->{cookies}) if $opt->{cookies};
	# Netscape
	push(@cookies,".netscape/cookies.txt") if -r ".netscape/cookies.txt";
	# Mozilla cookies?
	push(@cookies,glob("$ENV{HOME}/.mozilla/*/*/cookies.txt"));
	map { "--load-cookies=$_" } @cookies;
}

sub status {
	my ($opt,@s) = @_;
	@s = @{$opt->{status}} unless @s || !$opt->{status};
	@s = ('ALL') unless @s;
	map { uc } @s;
	my @status;
	foreach my $s ( @s ) {
		if ($s eq 'ALL') {
			push(@status,status($opt,qw(NEW ASSIGNED REOPENED RESOLVED VERIFIED CLOSED)));
		} elsif ($s eq 'OPEN') {
			push(@status,status($opt,qw(NEW ASSIGNED REOPENED RESOLVED VERIFIED)));
		} else {
			push(@status,$s);
		}
	}
	join('',map { "&bug_status=$_" } @status);
}

sub addKey {
	my ($opt,@key) = @_;
	my $ret;
	foreach my $key ( @key ) {
		next unless $opt->{$key};
		foreach my $v ( @{$opt->{$key}} ) {
			$ret .= "&$key=$v";
		}
	}
	$ret;
}

sub csvSplit() {
	my @csv;
	chomp;
	while ($_) {
		fatal("Couldn't parse csv: [$_]\n")
			unless s/^([^",][^,]*)(,|$)// || s/^"(.*?)"(,|$)// || s/^()(,|$)//;
		push(@csv,$1);
	}
	@csv;
}

sub command {
	my ($opt) = @_;
	my $url = "$opt->{bugzilla}?ctype=csv";

	$url .= status($opt);
	$url .= addKey($opt,'component','product');

	my @cmd = ($WGET,"--quiet",
		"--user=$opt->{user}", "--password=\Q$opt->{pass}\E",
		"--no-check-certificate",
		"-O","-", "\Q$url\E");
	push(@cmd,cookies);
	@cmd;
}

sub getBugz {
	my ($opt,@cmd) = @_;
	open(BUGZ,"@cmd |") || usage("Couldn't open pipe [$opt->{bugzilla}]");
	my @head;
	my (@bugz);
	while(<BUGZ>) {
		if ($.==1) {
			@head = csvSplit;
		} else {
			my @csv = csvSplit;
			fatal("Number of elements in csv changed??") if $#head != $#csv;
			my %bug;
			foreach my $h ( @head ) {
				$bug{$h} = shift @csv;
			}
			push(@bugz, \%bug);
		}
	}
	close BUGZ;
	@bugz;
}

sub outputBugz {
	my ($opt,@bugz) = @_;
	foreach my $b ( @bugz ) {
		my $fmt = $opt->{fmt};
		$fmt =~ s/\\n/\n/g;
		$fmt =~ s/%([^%]+)%/$b->{$1}/g;
		print $fmt;
	}
}

sub main {
	my $opt = parse_args();

	my @cmd = command($opt);

	my @bugz = getBugz($opt,@cmd);

	outputBugz($opt,@bugz);
}
main();
