# HG changeset patch # User Benoit Boissinot # Date 1146744344 -7200 # Node ID 2a5d8af8eecc3e2d558588e17184bb874278614f # Parent f027bc2d3f4a9a6c07a4a017a769a67481bd0b04# Parent ee90e5a9197f21357f41a0bc12cb5c40f5b3d1fc merge with crew diff -r f027bc2d3f4a -r 2a5d8af8eecc hgext/bugzilla.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext/bugzilla.py Thu May 04 14:05:44 2006 +0200 @@ -0,0 +1,293 @@ +# bugzilla.py - bugzilla integration for mercurial +# +# Copyright 2006 Vadim Gelfer +# +# This software may be used and distributed according to the terms +# of the GNU General Public License, incorporated herein by reference. +# +# hook extension to update comments of bugzilla bugs when changesets +# that refer to bugs by id are seen. this hook does not change bug +# status, only comments. +# +# to configure, add items to '[bugzilla]' section of hgrc. +# +# to use, configure bugzilla extension and enable like this: +# +# [extensions] +# hgext.bugzilla = +# +# [hooks] +# # run bugzilla hook on every change pulled or pushed in here +# incoming.bugzilla = python:hgext.bugzilla.hook +# +# config items: +# +# REQUIRED: +# host = bugzilla # mysql server where bugzilla database lives +# password = ** # user's password +# version = 2.16 # version of bugzilla installed +# +# OPTIONAL: +# bzuser = ... # bugzilla user id to record comments with +# db = bugs # database to connect to +# hgweb = http:// # root of hg web site for browsing commits +# notify = ... # command to run to get bugzilla to send mail +# regexp = ... # regexp to match bug ids (must contain one "()" group) +# strip = 0 # number of slashes to strip for url paths +# style = ... # style file to use when formatting comments +# template = ... # template to use when formatting comments +# timeout = 5 # database connection timeout (seconds) +# user = bugs # user to connect to database as + +from mercurial.demandload import * +from mercurial.i18n import gettext as _ +from mercurial.node import * +demandload(globals(), 'cStringIO mercurial:templater,util os re time') + +try: + import MySQLdb +except ImportError: + raise util.Abort(_('python mysql support not available')) + +def buglist(ids): + return '(' + ','.join(map(str, ids)) + ')' + +class bugzilla_2_16(object): + '''support for bugzilla version 2.16.''' + + def __init__(self, ui): + self.ui = ui + host = self.ui.config('bugzilla', 'host', 'localhost') + user = self.ui.config('bugzilla', 'user', 'bugs') + passwd = self.ui.config('bugzilla', 'password') + db = self.ui.config('bugzilla', 'db', 'bugs') + timeout = int(self.ui.config('bugzilla', 'timeout', 5)) + self.ui.note(_('connecting to %s:%s as %s, password %s\n') % + (host, db, user, '*' * len(passwd))) + self.conn = MySQLdb.connect(host=host, user=user, passwd=passwd, + db=db, connect_timeout=timeout) + self.cursor = self.conn.cursor() + self.run('select fieldid from fielddefs where name = "longdesc"') + ids = self.cursor.fetchall() + if len(ids) != 1: + raise util.Abort(_('unknown database schema')) + self.longdesc_id = ids[0][0] + self.user_ids = {} + + def run(self, *args, **kwargs): + '''run a query.''' + self.ui.note(_('query: %s %s\n') % (args, kwargs)) + try: + self.cursor.execute(*args, **kwargs) + except MySQLdb.MySQLError, err: + self.ui.note(_('failed query: %s %s\n') % (args, kwargs)) + raise + + def filter_real_bug_ids(self, ids): + '''filter not-existing bug ids from list.''' + self.run('select bug_id from bugs where bug_id in %s' % buglist(ids)) + ids = [c[0] for c in self.cursor.fetchall()] + ids.sort() + return ids + + def filter_unknown_bug_ids(self, node, ids): + '''filter bug ids from list that already refer to this changeset.''' + + self.run('''select bug_id from longdescs where + bug_id in %s and thetext like "%%%s%%"''' % + (buglist(ids), short(node))) + unknown = dict.fromkeys(ids) + for (id,) in self.cursor.fetchall(): + self.ui.status(_('bug %d already knows about changeset %s\n') % + (id, short(node))) + unknown.pop(id, None) + ids = unknown.keys() + ids.sort() + return ids + + def notify(self, ids): + '''tell bugzilla to send mail.''' + + self.ui.status(_('telling bugzilla to send mail:\n')) + for id in ids: + self.ui.status(_(' bug %s\n') % id) + cmd = self.ui.config('bugzilla', 'notify', + 'cd /var/www/html/bugzilla && ' + './processmail %s nobody@nowhere.com') % id + fp = os.popen('(%s) 2>&1' % cmd) + out = fp.read() + ret = fp.close() + if ret: + self.ui.warn(out) + raise util.Abort(_('bugzilla notify command %s') % + util.explain_exit(ret)[0]) + self.ui.status(_('done\n')) + + def get_user_id(self, user): + '''look up numeric bugzilla user id.''' + try: + return self.user_ids[user] + except KeyError: + try: + userid = int(user) + except ValueError: + self.ui.note(_('looking up user %s\n') % user) + self.run('''select userid from profiles + where login_name like %s''', user) + all = self.cursor.fetchall() + if len(all) != 1: + raise KeyError(user) + userid = int(all[0][0]) + self.user_ids[user] = userid + return userid + + def add_comment(self, bugid, text, prefuser): + '''add comment to bug. try adding comment as committer of + changeset, otherwise as default bugzilla user.''' + try: + userid = self.get_user_id(prefuser) + except KeyError: + try: + defaultuser = self.ui.config('bugzilla', 'bzuser') + userid = self.get_user_id(defaultuser) + except KeyError: + raise util.Abort(_('cannot find user id for %s or %s') % + (prefuser, defaultuser)) + now = time.strftime('%Y-%m-%d %H:%M:%S') + self.run('''insert into longdescs + (bug_id, who, bug_when, thetext) + values (%s, %s, %s, %s)''', + (bugid, userid, now, text)) + self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid) + values (%s, %s, %s, %s)''', + (bugid, userid, now, self.longdesc_id)) + +class bugzilla(object): + # supported versions of bugzilla. different versions have + # different schemas. + _versions = { + '2.16': bugzilla_2_16, + } + + _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*' + r'((?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)') + + _bz = None + + def __init__(self, ui, repo): + self.ui = ui + self.repo = repo + + def bz(self): + '''return object that knows how to talk to bugzilla version in + use.''' + + if bugzilla._bz is None: + bzversion = self.ui.config('bugzilla', 'version') + try: + bzclass = bugzilla._versions[bzversion] + except KeyError: + raise util.Abort(_('bugzilla version %s not supported') % + bzversion) + bugzilla._bz = bzclass(self.ui) + return bugzilla._bz + + def __getattr__(self, key): + return getattr(self.bz(), key) + + _bug_re = None + _split_re = None + + def find_bug_ids(self, node, desc): + '''find valid bug ids that are referred to in changeset + comments and that do not already have references to this + changeset.''' + + if bugzilla._bug_re is None: + bugzilla._bug_re = re.compile( + self.ui.config('bugzilla', 'regexp', bugzilla._default_bug_re), + re.IGNORECASE) + bugzilla._split_re = re.compile(r'\D+') + start = 0 + ids = {} + while True: + m = bugzilla._bug_re.search(desc, start) + if not m: + break + start = m.end() + for id in bugzilla._split_re.split(m.group(1)): + ids[int(id)] = 1 + ids = ids.keys() + if ids: + ids = self.filter_real_bug_ids(ids) + if ids: + ids = self.filter_unknown_bug_ids(node, ids) + return ids + + def update(self, bugid, node, changes): + '''update bugzilla bug with reference to changeset.''' + + def webroot(root): + '''strip leading prefix of repo root and turn into + url-safe path.''' + count = int(self.ui.config('bugzilla', 'strip', 0)) + root = util.pconvert(root) + while count > 0: + c = root.find('/') + if c == -1: + break + root = root[c+1:] + count -= 1 + return root + + class stringio(object): + '''wrap cStringIO.''' + def __init__(self): + self.fp = cStringIO.StringIO() + + def write(self, *args): + for a in args: + self.fp.write(a) + + write_header = write + + def getvalue(self): + return self.fp.getvalue() + + mapfile = self.ui.config('bugzilla', 'style') + tmpl = self.ui.config('bugzilla', 'template') + sio = stringio() + t = templater.changeset_templater(self.ui, self.repo, mapfile, sio) + if not mapfile and not tmpl: + tmpl = _('changeset {node|short} in repo {root} refers ' + 'to bug {bug}.\ndetails:\n\t{desc|tabindent}') + if tmpl: + tmpl = templater.parsestring(tmpl, quoted=False) + t.use_template(tmpl) + t.show(changenode=node, changes=changes, + bug=str(bugid), + hgweb=self.ui.config('bugzilla', 'hgweb'), + root=self.repo.root, + webroot=webroot(self.repo.root)) + self.add_comment(bugid, sio.getvalue(), templater.email(changes[1])) + +def hook(ui, repo, hooktype, node=None, **kwargs): + '''add comment to bugzilla for each changeset that refers to a + bugzilla bug id. only add a comment once per bug, so same change + seen multiple times does not fill bug with duplicate data.''' + if node is None: + raise util.Abort(_('hook type %s does not pass a changeset id') % + hooktype) + try: + bz = bugzilla(ui, repo) + bin_node = bin(node) + changes = repo.changelog.read(bin_node) + ids = bz.find_bug_ids(bin_node, changes[4]) + if ids: + for id in ids: + bz.update(id, bin_node, changes) + bz.notify(ids) + return True + except MySQLdb.MySQLError, err: + raise util.Abort(_('database error: %s') % err[1]) + diff -r f027bc2d3f4a -r 2a5d8af8eecc mercurial/commands.py --- a/mercurial/commands.py Thu May 04 14:01:55 2006 +0200 +++ b/mercurial/commands.py Thu May 04 14:05:44 2006 +0200 @@ -398,194 +398,6 @@ user = revcache[rev] = ui.shortuser(name) return user -class changeset_templater(object): - '''use templater module to format changeset information.''' - - def __init__(self, ui, repo, mapfile): - self.t = templater.templater(mapfile, templater.common_filters, - cache={'parent': '{rev}:{node|short} ', - 'manifest': '{rev}:{node|short}'}) - self.ui = ui - self.repo = repo - - def use_template(self, t): - '''set template string to use''' - self.t.cache['changeset'] = t - - def write(self, thing, header=False): - '''write expanded template. - uses in-order recursive traverse of iterators.''' - for t in thing: - if hasattr(t, '__iter__'): - self.write(t, header=header) - elif header: - self.ui.write_header(t) - else: - self.ui.write(t) - - def write_header(self, thing): - self.write(thing, header=True) - - def show(self, rev=0, changenode=None, brinfo=None): - '''show a single changeset or file revision''' - log = self.repo.changelog - if changenode is None: - changenode = log.node(rev) - elif not rev: - rev = log.rev(changenode) - - changes = log.read(changenode) - - def showlist(name, values, plural=None, **args): - '''expand set of values. - name is name of key in template map. - values is list of strings or dicts. - plural is plural of name, if not simply name + 's'. - - expansion works like this, given name 'foo'. - - if values is empty, expand 'no_foos'. - - if 'foo' not in template map, return values as a string, - joined by space. - - expand 'start_foos'. - - for each value, expand 'foo'. if 'last_foo' in template - map, expand it instead of 'foo' for last key. - - expand 'end_foos'. - ''' - if plural: names = plural - else: names = name + 's' - if not values: - noname = 'no_' + names - if noname in self.t: - yield self.t(noname, **args) - return - if name not in self.t: - if isinstance(values[0], str): - yield ' '.join(values) - else: - for v in values: - yield dict(v, **args) - return - startname = 'start_' + names - if startname in self.t: - yield self.t(startname, **args) - vargs = args.copy() - def one(v, tag=name): - try: - vargs.update(v) - except (AttributeError, ValueError): - try: - for a, b in v: - vargs[a] = b - except ValueError: - vargs[name] = v - return self.t(tag, **vargs) - lastname = 'last_' + name - if lastname in self.t: - last = values.pop() - else: - last = None - for v in values: - yield one(v) - if last is not None: - yield one(last, tag=lastname) - endname = 'end_' + names - if endname in self.t: - yield self.t(endname, **args) - - if brinfo: - def showbranches(**args): - if changenode in brinfo: - for x in showlist('branch', brinfo[changenode], - plural='branches', **args): - yield x - else: - showbranches = '' - - if self.ui.debugflag: - def showmanifest(**args): - args = args.copy() - args.update(dict(rev=self.repo.manifest.rev(changes[0]), - node=hex(changes[0]))) - yield self.t('manifest', **args) - else: - showmanifest = '' - - def showparents(**args): - parents = [[('rev', log.rev(p)), ('node', hex(p))] - for p in log.parents(changenode) - if self.ui.debugflag or p != nullid] - if (not self.ui.debugflag and len(parents) == 1 and - parents[0][0][1] == rev - 1): - return - for x in showlist('parent', parents, **args): - yield x - - def showtags(**args): - for x in showlist('tag', self.repo.nodetags(changenode), **args): - yield x - - if self.ui.debugflag: - files = self.repo.changes(log.parents(changenode)[0], changenode) - def showfiles(**args): - for x in showlist('file', files[0], **args): yield x - def showadds(**args): - for x in showlist('file_add', files[1], **args): yield x - def showdels(**args): - for x in showlist('file_del', files[2], **args): yield x - else: - def showfiles(**args): - for x in showlist('file', changes[3], **args): yield x - showadds = '' - showdels = '' - - props = { - 'author': changes[1], - 'branches': showbranches, - 'date': changes[2], - 'desc': changes[4], - 'file_adds': showadds, - 'file_dels': showdels, - 'files': showfiles, - 'manifest': showmanifest, - 'node': hex(changenode), - 'parents': showparents, - 'rev': rev, - 'tags': showtags, - } - - try: - if self.ui.debugflag and 'header_debug' in self.t: - key = 'header_debug' - elif self.ui.quiet and 'header_quiet' in self.t: - key = 'header_quiet' - elif self.ui.verbose and 'header_verbose' in self.t: - key = 'header_verbose' - elif 'header' in self.t: - key = 'header' - else: - key = '' - if key: - self.write_header(self.t(key, **props)) - if self.ui.debugflag and 'changeset_debug' in self.t: - key = 'changeset_debug' - elif self.ui.quiet and 'changeset_quiet' in self.t: - key = 'changeset_quiet' - elif self.ui.verbose and 'changeset_verbose' in self.t: - key = 'changeset_verbose' - else: - key = 'changeset' - self.write(self.t(key, **props)) - except KeyError, inst: - raise util.Abort(_("%s: no key named '%s'") % (self.t.mapfile, - inst.args[0])) - except SyntaxError, inst: - raise util.Abort(_('%s: %s') % (self.t.mapfile, inst.args[0])) - class changeset_printer(object): '''show changeset information when templating not requested.''' @@ -672,7 +484,7 @@ if not mapname: mapname = templater.templatepath(mapfile) if mapname: mapfile = mapname try: - t = changeset_templater(ui, repo, mapfile) + t = templater.changeset_templater(ui, repo, mapfile) except SyntaxError, inst: raise util.Abort(inst.args[0]) if tmpl: t.use_template(tmpl) diff -r f027bc2d3f4a -r 2a5d8af8eecc mercurial/localrepo.py --- a/mercurial/localrepo.py Thu May 04 14:01:55 2006 +0200 +++ b/mercurial/localrepo.py Thu May 04 14:05:44 2006 +0200 @@ -105,7 +105,7 @@ '("%s" is not callable)') % (hname, funcname)) try: - r = obj(ui=ui, repo=repo, hooktype=name, **args) + r = obj(ui=self.ui, repo=self, hooktype=name, **args) except (KeyboardInterrupt, util.SignalInterrupt): raise except Exception, exc: diff -r f027bc2d3f4a -r 2a5d8af8eecc mercurial/templater.py --- a/mercurial/templater.py Thu May 04 14:01:55 2006 +0200 +++ b/mercurial/templater.py Thu May 04 14:05:44 2006 +0200 @@ -8,6 +8,7 @@ import re from demandload import demandload from i18n import gettext as _ +from node import * demandload(globals(), "cStringIO cgi re sys os time urllib util textwrap") esctable = { @@ -209,7 +210,7 @@ break yield text[start:m.start(0)], m.group(1) start = m.end(1) - + fp = cStringIO.StringIO() for para, rest in findparas(): fp.write(space_re.sub(' ', textwrap.fill(para, width))) @@ -241,7 +242,7 @@ r = author.find('>') if r == -1: r = None return author[author.find('<')+1:r] - + def person(author): '''get name of author, or else username.''' f = author.find('<') @@ -267,6 +268,7 @@ common_filters = { "addbreaks": nl2br, + "basename": os.path.basename, "age": age, "date": lambda x: util.datestr(x), "domain": domain, @@ -292,6 +294,7 @@ def templatepath(name=None): '''return location of template file or directory (if no name). returns None if not found.''' + # executable version (py2exe) doesn't support __file__ if hasattr(sys, 'frozen'): module = sys.executable @@ -303,3 +306,196 @@ p = os.path.join(os.path.dirname(module), *fl) if (name and os.path.exists(p)) or os.path.isdir(p): return os.path.normpath(p) + +class changeset_templater(object): + '''format changeset information.''' + + def __init__(self, ui, repo, mapfile, dest=None): + self.t = templater(mapfile, common_filters, + cache={'parent': '{rev}:{node|short} ', + 'manifest': '{rev}:{node|short}'}) + self.ui = ui + self.dest = dest + self.repo = repo + + def use_template(self, t): + '''set template string to use''' + self.t.cache['changeset'] = t + + def write(self, thing, header=False): + '''write expanded template. + uses in-order recursive traverse of iterators.''' + dest = self.dest or self.ui + for t in thing: + if hasattr(t, '__iter__'): + self.write(t, header=header) + elif header: + dest.write_header(t) + else: + dest.write(t) + + def write_header(self, thing): + self.write(thing, header=True) + + def show(self, rev=0, changenode=None, brinfo=None, changes=None, + **props): + '''show a single changeset or file revision''' + log = self.repo.changelog + if changenode is None: + changenode = log.node(rev) + elif not rev: + rev = log.rev(changenode) + if changes is None: + changes = log.read(changenode) + + def showlist(name, values, plural=None, **args): + '''expand set of values. + name is name of key in template map. + values is list of strings or dicts. + plural is plural of name, if not simply name + 's'. + + expansion works like this, given name 'foo'. + + if values is empty, expand 'no_foos'. + + if 'foo' not in template map, return values as a string, + joined by space. + + expand 'start_foos'. + + for each value, expand 'foo'. if 'last_foo' in template + map, expand it instead of 'foo' for last key. + + expand 'end_foos'. + ''' + if plural: names = plural + else: names = name + 's' + if not values: + noname = 'no_' + names + if noname in self.t: + yield self.t(noname, **args) + return + if name not in self.t: + if isinstance(values[0], str): + yield ' '.join(values) + else: + for v in values: + yield dict(v, **args) + return + startname = 'start_' + names + if startname in self.t: + yield self.t(startname, **args) + vargs = args.copy() + def one(v, tag=name): + try: + vargs.update(v) + except (AttributeError, ValueError): + try: + for a, b in v: + vargs[a] = b + except ValueError: + vargs[name] = v + return self.t(tag, **vargs) + lastname = 'last_' + name + if lastname in self.t: + last = values.pop() + else: + last = None + for v in values: + yield one(v) + if last is not None: + yield one(last, tag=lastname) + endname = 'end_' + names + if endname in self.t: + yield self.t(endname, **args) + + if brinfo: + def showbranches(**args): + if changenode in brinfo: + for x in showlist('branch', brinfo[changenode], + plural='branches', **args): + yield x + else: + showbranches = '' + + if self.ui.debugflag: + def showmanifest(**args): + args = args.copy() + args.update(dict(rev=self.repo.manifest.rev(changes[0]), + node=hex(changes[0]))) + yield self.t('manifest', **args) + else: + showmanifest = '' + + def showparents(**args): + parents = [[('rev', log.rev(p)), ('node', hex(p))] + for p in log.parents(changenode) + if self.ui.debugflag or p != nullid] + if (not self.ui.debugflag and len(parents) == 1 and + parents[0][0][1] == rev - 1): + return + for x in showlist('parent', parents, **args): + yield x + + def showtags(**args): + for x in showlist('tag', self.repo.nodetags(changenode), **args): + yield x + + if self.ui.debugflag: + files = self.repo.changes(log.parents(changenode)[0], changenode) + def showfiles(**args): + for x in showlist('file', files[0], **args): yield x + def showadds(**args): + for x in showlist('file_add', files[1], **args): yield x + def showdels(**args): + for x in showlist('file_del', files[2], **args): yield x + else: + def showfiles(**args): + for x in showlist('file', changes[3], **args): yield x + showadds = '' + showdels = '' + + defprops = { + 'author': changes[1], + 'branches': showbranches, + 'date': changes[2], + 'desc': changes[4], + 'file_adds': showadds, + 'file_dels': showdels, + 'files': showfiles, + 'manifest': showmanifest, + 'node': hex(changenode), + 'parents': showparents, + 'rev': rev, + 'tags': showtags, + } + props = props.copy() + props.update(defprops) + + try: + if self.ui.debugflag and 'header_debug' in self.t: + key = 'header_debug' + elif self.ui.quiet and 'header_quiet' in self.t: + key = 'header_quiet' + elif self.ui.verbose and 'header_verbose' in self.t: + key = 'header_verbose' + elif 'header' in self.t: + key = 'header' + else: + key = '' + if key: + self.write_header(self.t(key, **props)) + if self.ui.debugflag and 'changeset_debug' in self.t: + key = 'changeset_debug' + elif self.ui.quiet and 'changeset_quiet' in self.t: + key = 'changeset_quiet' + elif self.ui.verbose and 'changeset_verbose' in self.t: + key = 'changeset_verbose' + else: + key = 'changeset' + self.write(self.t(key, **props)) + except KeyError, inst: + raise util.Abort(_("%s: no key named '%s'") % (self.t.mapfile, + inst.args[0])) + except SyntaxError, inst: + raise util.Abort(_('%s: %s') % (self.t.mapfile, inst.args[0])) diff -r f027bc2d3f4a -r 2a5d8af8eecc mercurial/util.py --- a/mercurial/util.py Thu May 04 14:01:55 2006 +0200 +++ b/mercurial/util.py Thu May 04 14:05:44 2006 +0200 @@ -231,7 +231,7 @@ name_st = os.stat(name) except OSError: break - if os.path.samestat(name_st, root_st): + if samestat(name_st, root_st): rel.reverse() name = os.path.join(*rel) audit_path(name) @@ -561,6 +561,9 @@ makelock = _makelock_file readlock = _readlock_file + def samestat(s1, s2): + return False + def explain_exit(code): return _("exited with status %d") % code, code @@ -627,6 +630,7 @@ return path normpath = os.path.normpath + samestat = os.path.samestat def makelock(info, pathname): try: