#!/usr/bin/ocamlrun ocaml (* bugs-report.ml * Written by Richard W.M. Jones * Copyright (C) 2009-2010 Red Hat Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. *) (* Note on using this script: * (0) chmod +x the script and run it directly or from crontab. * (1) You will need the following packages installed: * ocaml ocaml-findlib ocaml-extlib python-bugzilla * (2) If using emacs, install emacs-tuareg for better editing. *) #use "topfind";; #require "extlib";; #require "unix";; open ExtString open ExtList open Unix open Printf (* Replace with your email address. *) let email = "rjones@redhat.com" (* Sendmail headers, see http://unix.derkeiler.com/Mailing-Lists/FreeBSD/questions/2009-10/msg01145.html . *) let () = printf "To: %s\n" email; printf "From: %s\n" email; printf "Subject: Weekly Red Hat Bugzilla summary report\n"; printf "\n%!" (* Run a command and read all output lines as a list of strings. *) let input_cmd_lines cmd = let chan = open_process_in cmd in let lines = ref [] in (try while true do lines := input_line chan :: !lines done with End_of_file -> ()); (match close_process_in chan with | WEXITED 0 -> () | _ -> failwith "external command failed, see earlier errors"); List.rev !lines (* Bug status type. *) type bug_status = |NEEDINFO|NEW|ASSIGNED|ON_DEV|MODIFIED|POST|ON_QA|FAILS_QA|PASSES_QA |REOPENED|VERIFIED|RELEASE_PENDING|CLOSED let bug_status_of_string = function |"NEEDINFO" -> NEEDINFO |"NEW" -> NEW |"ASSIGNED" -> ASSIGNED |"ON_DEV" -> ON_DEV |"MODIFIED" -> MODIFIED |"POST" -> POST |"ON_QA" -> ON_QA |"FAILS_QA" -> FAILS_QA |"PASSES_QA" -> PASSES_QA |"REOPENED" -> REOPENED |"VERIFIED" -> VERIFIED |"RELEASE_PENDING" -> RELEASE_PENDING |"CLOSED" -> CLOSED | str -> failwith ("unknown bug status: " ^ str) let string_of_bug_status = function |NEEDINFO -> "NEEDINFO" |NEW -> "NEW" |ASSIGNED -> "ASSIGNED" |ON_DEV -> "ON_DEV" |MODIFIED -> "MODIFIED" |POST -> "POST" |ON_QA -> "ON_QA" |FAILS_QA -> "FAILS_QA" |PASSES_QA -> "PASSES_QA" |REOPENED -> "REOPENED" |VERIFIED -> "VERIFIED" |RELEASE_PENDING -> "RELEASE_PENDING" |CLOSED -> "CLOSED" let all_bug_states = [NEEDINFO;NEW;ASSIGNED;ON_DEV;MODIFIED;POST;ON_QA;FAILS_QA;PASSES_QA; REOPENED;VERIFIED;RELEASE_PENDING;CLOSED] (* To simplify bug classification, I prefer to group some bug statuses * together like this: *) type bug_group = |G_NEEDINFO|G_OPEN|G_FIXED|G_CLOSED let bug_group_of_status = function |NEEDINFO -> G_NEEDINFO |NEW|ASSIGNED|REOPENED -> G_OPEN |ON_DEV|MODIFIED|POST|ON_QA|FAILS_QA|PASSES_QA|VERIFIED|RELEASE_PENDING -> G_FIXED |CLOSED -> G_CLOSED let string_of_bug_group = function |G_NEEDINFO -> "NEEDINFO" |G_OPEN -> "OPEN" |G_FIXED -> "FIXED" |G_CLOSED -> "CLOSED" (* Get all interesting Bugzilla IDs. *) let bugids = (* Exclude closed bugs from the results. *) let bug_states = String.concat "," (List.map string_of_bug_status (List.filter (function CLOSED -> false | _ -> true) all_bug_states)) in (* Run the query command to get all reporter/CC/assigned bug IDs for email. *) let cmd = sprintf " for flag in -r -c -a; do bugzilla query -E exact $flag '%s' -t '%s' -i done | sort -n -u" email bug_states in List.map int_of_string (input_cmd_lines cmd) let () = printf "Total: %d bugs\n\n%!" (List.length bugids) (* Get the component, full description etc for each bug. *) let bugs = let comma_sep_bugids = String.concat "," (List.map string_of_int bugids) in let cmd = sprintf "bugzilla query -b '%s' --outputformat='%%{bug_status}|%%{component}|%%{product}|%%{bug_id}|%%{short_desc}'" comma_sep_bugids in let lines = input_cmd_lines cmd in List.map ( fun line -> let fields = String.nsplit line "|" in match fields with | status::component::product::id::rest -> let id = int_of_string id in let status = bug_status_of_string status in let group = bug_group_of_status status in let short_desc = String.concat "|" rest in (group, (component, (product, (id, status, short_desc)))) | _ -> failwith ("could not parse bugzilla query line: " ^ line) ) lines (* group_by is the fundamental operation we use to order the bugs. It * groups a list by the first element. This version was written by * Isaac Trotts. *) let group_by ?(cmp = Pervasives.compare) ls = let ls' = List.fold_left (fun acc (day1, x1) -> match acc with [] -> [day1, [x1]] | (day2, ls2) :: acctl -> if cmp day1 day2 = 0 then (day1, x1 :: ls2) :: acctl else (day1, [x1]) :: acc) [] ls in let ls' = List.rev ls' in List.map (fun (x, xs) -> x, List.rev xs) ls' (* Group by bug status, then component, and so on down. *) let bugs = List.sort bugs let bugs = group_by bugs let bugs = List.map ( fun (group, bugs) -> let bugs = group_by bugs in let bugs = List.map ( fun (component, bugs) -> let bugs = group_by bugs in component, bugs ) bugs in group, bugs ) bugs (* Display the groups. *) let () = List.iter ( fun (group, bugs) -> printf "Bugs in %s states:\n\n" (string_of_bug_group group); List.iter ( fun (component, bugs) -> printf "*** %s ***\n\n" component; List.iter ( fun (product, bugs) -> printf " In %s:\n" product; List.iter ( fun (id, status, short_desc) -> printf " %d %s\n" id (*string_of_bug_status status*) short_desc ) bugs ) bugs; printf "\n" ) bugs; printf "\n" ) bugs