From mboxrd@z Thu Jan 1 00:00:00 1970 From: John Wiegley Subject: Easier integration of org-mode and Bugzilla Date: Thu, 20 May 2010 18:45:24 -0400 Message-ID: Mime-Version: 1.0 (Apple Message framework v1078) Content-Type: multipart/mixed; boundary=Apple-Mail-4--264418981 Return-path: Received: from [140.186.70.92] (port=42101 helo=eggs.gnu.org) by lists.gnu.org with esmtp (Exim 4.43) id 1OFEUt-0000D9-H5 for emacs-orgmode@gnu.org; Thu, 20 May 2010 18:50:53 -0400 Received: from Debian-exim by eggs.gnu.org with spam-scanned (Exim 4.69) (envelope-from ) id 1OFEUc-00073G-8j for emacs-orgmode@gnu.org; Thu, 20 May 2010 18:45:32 -0400 Received: from mail-pw0-f41.google.com ([209.85.160.41]:45011) by eggs.gnu.org with esmtp (Exim 4.69) (envelope-from ) id 1OFEUb-000738-PP for emacs-orgmode@gnu.org; Thu, 20 May 2010 18:45:30 -0400 Received: by pwi7 with SMTP id 7so320428pwi.0 for ; Thu, 20 May 2010 15:45:29 -0700 (PDT) List-Id: "General discussions about Org-mode." List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Sender: emacs-orgmode-bounces+geo-emacs-orgmode=m.gmane.org@gnu.org Errors-To: emacs-orgmode-bounces+geo-emacs-orgmode=m.gmane.org@gnu.org To: emacs-orgmode Mode --Apple-Mail-4--264418981 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset=us-ascii I use Org-mode for tracking all tasks, including Bugzilla issues. But I = also use remember.el for quickyl creating new tasks, some of which = should then go into BZ. The following setup lets me just type "x z" on a task in the Agenda, and = a related Bugzilla task is automatically created. Note that this code = will have to be edited to work in your environment. First, I have a special link name for these bugs: #+LINK: cegbug https://portal/bugzilla/show_bug.cgi?id=3D Second, you need the bugzilla-submit script, attached to this mail. You = can also find this script in the Bugzilla distribution. Third is this function, several ports of which you will need to edit = (though it should be pretty obvious): (defun make-bugzilla-bug (product component version priority severity) (interactive (let ((omk (get-text-property (point) 'org-marker))) (with-current-buffer (marker-buffer omk) (save-excursion (goto-char omk) (let ((products (list (list "ABC" (list "Admin" "User" "Other" "CSR") (list "3.0")) (list "Bizcard" (list "Catalog" "Content Section" "Uploader" "Visual = Aesthetics" "webui") (list "unspecified")) (list "Adagio" (list "DTSX" "PTS" "Satellite" = "Zips" "Core") (list "unspecified")) (list "IT" (list "install" "network" "repair" = "misc") (list "unspecified")) (list "EVAprint" (list "misc") (list "1.0")))) (priorities (list "P1" "P2" "P3" "P4" "P5")) (severities (list "blocker" "critical" "major" "normal" "minor" "trivial")) (product (org-get-category))) (list product (let ((components (nth 1 (assoc product products)))) (if (=3D 1 (length components)) (car components) (ido-completing-read "Component: " components nil t nil nil (car (last = components))))) (let ((versions (nth 2 (assoc product products)))) (if (=3D 1 (length versions)) (car versions) (ido-completing-read "Version: " versions nil t nil nil (car (last = versions))))) (let ((orgpri (nth 3 (org-heading-components)))) (if (and orgpri (=3D ?A orgpri)) "P1" (ido-completing-read "Priority: " priorities nil t nil nil "P3"))) (ido-completing-read "Severity: " severities nil t nil = nil "normal") )))))) (if (string=3D product "Bizcard") (setq product "BizCard")) (let ((omk (get-text-property (point) 'org-marker))) (with-current-buffer (marker-buffer omk) (save-excursion (goto-char omk) (let ((heading (nth 4 (org-heading-components))) (contents (buffer-substring-no-properties (org-entry-beginning-position) (org-entry-end-position))) bug) (with-temp-buffer (insert contents) (goto-char (point-min)) (delete-region (point) (1+ (line-end-position))) (search-forward ":PROP") (delete-region (match-beginning 0) (point-max)) (goto-char (point-min)) (while (re-search-forward "^ " nil t) (delete-region (match-beginning 0) (match-end 0))) (goto-char (point-min)) (while (re-search-forward "^SCHE" nil t) (delete-region (match-beginning 0) (1+ = (line-end-position)))) (goto-char (point-min)) (when (eobp) (insert "No description file.") (goto-char (point-min))) (insert (format "Product: %s Component: %s Version: %s Priority: %s Severity: %s Hardware: Other OS: Other Summary: %s" product component version priority severity heading) ?\n = ?\n) (let ((buf (current-buffer))) (with-temp-buffer (let ((tmpbuf (current-buffer))) (if nil (insert "Bug 999 posted.") (with-current-buffer buf (shell-command-on-region (point-min) (point-max) "~/bin/bugzilla-submit https://portal/bugzilla/" tmpbuf))) (goto-char (point-min)) (re-search-forward "Bug \\([0-9]+\\) posted.") (setq bug (match-string 1)))))) (save-excursion (org-back-to-heading t) (re-search-forward = "\\(TODO\\|STARTED\\|WAITING\\|DELEGATED\\) \\(\\[#[ABC]\\] \\)?") (insert (format "[[cegbug:%s][#%s]] " bug bug))))))) (org-agenda-redo)) Fourth is the agenda keybinding, some of which you may prefer to delete: (eval-after-load "org-agenda" '(progn (dolist (map (list org-agenda-keymap org-agenda-mode-map)) (define-prefix-command 'org-todo-state-map) (define-key map "x" 'org-todo-state-map) (define-key org-todo-state-map "d" #'(lambda nil (interactive) (org-agenda-todo "DONE"))) (define-key org-todo-state-map "r" #'(lambda nil (interactive) (org-agenda-todo "DEFERRED"))) (define-key org-todo-state-map "y" #'(lambda nil (interactive) (org-agenda-todo "SOMEDAY"))) (define-key org-todo-state-map "g" #'(lambda nil (interactive) (org-agenda-todo "DELEGATED"))) (define-key org-todo-state-map "n" #'(lambda nil (interactive) (org-agenda-todo "NOTE"))) (define-key org-todo-state-map "s" #'(lambda nil (interactive) (org-agenda-todo "STARTED"))) (define-key org-todo-state-map "t" #'(lambda nil (interactive) (org-agenda-todo "TODO"))) (define-key org-todo-state-map "w" #'(lambda nil (interactive) (org-agenda-todo "WAITING"))) (define-key org-todo-state-map "x" #'(lambda nil (interactive) (org-agenda-todo "CANCELLED"))) (define-key org-todo-state-map "z" #'make-bugzilla-bug)))) John --Apple-Mail-4--264418981 Content-Disposition: attachment; filename=bugzilla-submit Content-Type: application/octet-stream; name="bugzilla-submit" Content-Transfer-Encoding: 7bit #!/usr/bin/env python # # bugzilla-submit: a command-line script to post bugs to a Bugzilla instance # # Authors: Christian Reis # Eric S. Raymond # # This is version 0.5. # # For a usage hint run bugzilla-submit --help # # TODO: use RDF output to pick up valid options, as in # http://www.async.com.br/~kiko/mybugzilla/config.cgi?ctype=rdf import sys, string def error(m): sys.stderr.write("bugzilla-submit: %s\n" % m) sys.stderr.flush() sys.exit(1) version = string.split(string.split(sys.version)[0], ".")[:2] if map(int, version) < [2, 3]: error("you must upgrade to Python 2.3 or higher to use this script.") import urllib, re, os, netrc, email.Parser, optparse class ErrorURLopener(urllib.URLopener): """URLopener that handles HTTP 404s""" def http_error_404(self, url, fp, errcode, errmsg, headers, *extra): raise ValueError, errmsg # 'File Not Found' # Set up some aliases -- partly to hide the less friendly fieldnames # behind the names actually used for them in the stock web page presentation, # and partly to provide a place for mappings if the Bugzilla fieldnames # ever change. field_aliases = (('hardware', 'rep_platform'), ('os', 'op_sys'), ('summary', 'short_desc'), ('description', 'comment'), ('depends_on', 'dependson'), ('status', 'bug_status'), ('severity', 'bug_severity'), ('url', 'bug_file_loc'),) def header_to_field(hdr): hdr = hdr.lower().replace("-", "_") for (alias, name) in field_aliases: if hdr == alias: hdr = name break return hdr def field_to_header(hdr): hdr = "-".join(map(lambda x: x.capitalize(), hdr.split("_"))) for (alias, name) in field_aliases: if hdr == name: hdr = alias break return hdr def setup_parser(): # Take override values from the command line parser = optparse.OptionParser(usage="usage: %prog [options] bugzilla-url") parser.add_option('-b', '--status', dest='bug_status', help='Set the Status field.') parser.add_option('-u', '--url', dest='bug_file_loc', help='Set the URL field.') parser.add_option('-p', '--product', dest='product', help='Set the Product field.') parser.add_option('-v', '--version', dest='version', help='Set the Version field.') parser.add_option('-c', '--component', dest='component', help='Set the Component field.') parser.add_option('-s', '--summary', dest='short_desc', help='Set the Summary field.') parser.add_option('-H', '--hardware', dest='rep_platform', help='Set the Hardware field.') parser.add_option('-o', '--os', dest='op_sys', help='Set the Operating System field.') parser.add_option('-r', '--priority', dest='priority', help='Set the Priority field.') parser.add_option('-x', '--severity', dest='bug_severity', help='Set the Severity field.') parser.add_option('-d', '--description', dest='comment', help='Set the Description field.') parser.add_option('-a', '--assigned-to', dest='assigned_to', help='Set the Assigned-To field.') parser.add_option('-C', '--cc', dest='cc', help='Set the Cc field.') parser.add_option('-k', '--keywords', dest='keywords', help='Set the Keywords field.') parser.add_option('-D', '--depends-on', dest='dependson', help='Set the Depends-On field.') parser.add_option('-B', '--blocked', dest='blocked', help='Set the Blocked field.') parser.add_option('-n', '--no-stdin', dest='read', default=True, action='store_false', help='Suppress reading fields from stdin.') return parser # Fetch user's credential for access to this Bugzilla instance. def get_credentials(bugzilla): # Work around a quirk in the Python implementation. # The URL has to be quoted, otherwise the parser barfs on the colon. # But the parser doesn't strip the quotes. authenticate_on = '"' + bugzilla + '"' try: credentials = netrc.netrc() except netrc.NetrcParseError, e: error("ill-formed .netrc: %s:%s %s" % (e.filename, e.lineno, e.msg)) except IOError, e: error("missing .netrc file %s" % str(e).split()[-1]) ret = credentials.authenticators(authenticate_on) if not ret: # Okay, the literal string passed in failed. Just to make sure, # try adding/removing a slash after the address and looking # again. We don't know what format was used in .netrc, which is # why this rather hackish approach is necessary. if bugzilla[-1] == "/": authenticate_on = '"' + bugzilla[:-1] + '"' else: authenticate_on = '"' + bugzilla + '/"' ret = credentials.authenticators(authenticate_on) if not ret: # Apparently, an invalid machine URL will cause credentials == None error("no credentials for Bugzilla instance at %s" % bugzilla) return ret def process_options(options): data = {} # Initialize bug report fields from message on standard input if options.read: message_parser = email.Parser.Parser() message = message_parser.parse(sys.stdin) for (key, value) in message.items(): data.update({header_to_field(key) : value}) if not 'comment' in data: data['comment'] = message.get_payload() # Merge in options from the command line; they override what's on stdin. for (key, value) in options.__dict__.items(): if key != 'read' and value != None: data[key] = value return data def ensure_defaults(data): # Provide some defaults if the user did not supply them. if 'op_sys' not in data: if sys.platform.startswith('linux'): data['op_sys'] = 'Linux' if 'rep_platform' not in data: data['rep_platform'] = 'PC' if 'bug_status' not in data: data['bug_status'] = 'NEW' if 'bug_severity' not in data: data['bug_severity'] = 'normal' if 'bug_file_loc' not in data: data['bug_file_loc'] = 'http://' # Yes, Bugzilla needs this if 'priority' not in data: data['priority'] = 'Normal' def validate_fields(data): # Fields for validation required_fields = ( "bug_status", "bug_file_loc", "product", "version", "component", "short_desc", "rep_platform", "op_sys", "priority", "bug_severity", "comment", ) legal_fields = required_fields + ( "assigned_to", "cc", "keywords", "dependson", "blocked", ) my_fields = data.keys() for field in my_fields: if field not in legal_fields: error("invalid field: %s" % field_to_header(field)) for field in required_fields: if field not in my_fields: error("required field missing: %s" % field_to_header(field)) if not data['short_desc']: error("summary for bug submission must not be empty") if not data['comment']: error("comment for bug submission must not be empty") # # POST-specific functions # def submit_bug_POST(bugzilla, data): # Move the request over the wire postdata = urllib.urlencode(data) try: url = ErrorURLopener().open("%s/post_bug.cgi" % bugzilla, postdata) except ValueError: error("Bugzilla site at %s not found (HTTP returned 404)" % bugzilla) ret = url.read() check_result_POST(ret, data) def check_result_POST(ret, data): # XXX We can move pre-validation out of here as soon as we pick up # the valid options from config.cgi -- it will become a simple # assertion and ID-grabbing step. # # XXX: We use get() here which may return None, but since the user # might not have provided these options, we don't want to die on # them. version = data.get('version') product = data.get('product') component = data.get('component') priority = data.get('priority') severity = data.get('bug_severity') status = data.get('bug_status') assignee = data.get('assigned_to') platform = data.get('rep_platform') opsys = data.get('op_sys') keywords = data.get('keywords') deps = data.get('dependson', '') + " " + data.get('blocked', '') deps = deps.replace(" ", ", ") # XXX: we should really not be using plain find() here, as it can # match the bug content inadvertedly if ret.find("A legal Version was not") != -1: error("version %r does not exist for component %s:%s" % (version, product, component)) if ret.find("A legal Priority was not") != -1: error("priority %r does not exist in " "this Bugzilla instance" % priority) if ret.find("A legal Severity was not") != -1: error("severity %r does not exist in " "this Bugzilla instance" % severity) if ret.find("A legal Status was not") != -1: error("status %r is not a valid creation status in " "this Bugzilla instance" % status) if ret.find("A legal Platform was not") != -1: error("platform %r is not a valid platform in " "this Bugzilla instance" % platform) if ret.find("A legal OS/Version was not") != -1: error("%r is not a valid OS in " "this Bugzilla instance" % opsys) if ret.find("Invalid Username") != -1: error("invalid credentials submitted") if ret.find("Component Needed") != -1: error("the component %r does not exist in " "this Bugzilla instance" % component) if ret.find("Unknown Keyword") != -1: error("keyword(s) %r not registered in " "this Bugzilla instance" % keywords) if ret.find("The product name") != -1: error("product %r does not exist in this " "Bugzilla instance" % product) # XXX: this should be smarter if ret.find("does not exist") != -1: error("could not mark dependencies for bugs %s: one or " "more bugs didn't exist in this Bugzilla instance" % deps) if ret.find("Match Failed") != -1: # XXX: invalid CC hits on this error too error("the bug assignee %r isn't registered in " "this Bugzilla instance" % assignee) # If all is well, return bug number posted if ret.find("process_bug.cgi") == -1: error("could not post bug to %s: are you sure this " "is Bugzilla instance's top-level directory?" % bugzilla) m = re.search("Bug ([0-9]+) Submitted", ret) if not m: print ret error("Internal error: bug id not found; please report a bug") id = m.group(1) print "Bug %s posted." % id # # # if __name__ == "__main__": parser = setup_parser() # Parser will print help and exit here if we specified --help (options, args) = parser.parse_args() if len(args) != 1: parser.error("missing Bugzilla host URL") bugzilla = args[0] data = process_options(options) login, account, password = get_credentials(bugzilla) if "@" not in login: # no use even trying to submit error("login %r is invalid (it should be an email address)" % login) ensure_defaults(data) validate_fields(data) # Attach authentication information data.update({'Bugzilla_login' : login, 'Bugzilla_password' : password, 'GoAheadAndLogIn' : 1, 'form_name' : 'enter_bug'}) submit_bug_POST(bugzilla, data) --Apple-Mail-4--264418981 Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Content-Disposition: inline _______________________________________________ Emacs-orgmode mailing list Please use `Reply All' to send replies to the list. Emacs-orgmode@gnu.org http://lists.gnu.org/mailman/listinfo/emacs-orgmode --Apple-Mail-4--264418981--