# HG changeset patch # User Vadim Gelfer # Date 1146721677 25200 # Node ID ee90e5a9197f21357f41a0bc12cb5c40f5b3d1fc # Parent 2be3ac7abc210595efc271c06dd261cc08c11a28# Parent fb28ce04b349f7bef70df6c9cd761691bbf59e0a merge with crew. diff -r fb28ce04b349 -r ee90e5a9197f hgext/bugzilla.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext/bugzilla.py Wed May 03 22:47:57 2006 -0700 @@ -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 fb28ce04b349 -r ee90e5a9197f hgext/mq.py --- a/hgext/mq.py Wed May 03 22:47:08 2006 -0700 +++ b/hgext/mq.py Wed May 03 22:47:57 2006 -0700 @@ -1233,34 +1233,28 @@ repomap[repo] = queue(ui, repo.join("")) cmdtable = { - "qapplied": (applied, [], 'hg qapplied [patch]'), + "qapplied": (applied, [], 'hg qapplied [PATCH]'), "qcommit|qci": (commit, - [('A', 'addremove', None, _('run addremove during commit')), - ('I', 'include', [], _('include names matching the given patterns')), - ('X', 'exclude', [], _('exclude names matching the given patterns')), - ('m', 'message', '', _('use as commit message')), - ('l', 'logfile', '', _('read the commit message from ')), - ('d', 'date', '', _('record datecode as commit date')), - ('u', 'user', '', _('record user as commiter'))], - 'hg qcommit [options] [files]'), - "^qdiff": (diff, [], 'hg qdiff [files]'), - "qdelete": (delete, [], 'hg qdelete [patch]'), + commands.table["^commit|ci"][1], + 'hg qcommit [OPTION]... [FILE]...'), + "^qdiff": (diff, [], 'hg qdiff [FILE]...'), + "qdelete": (delete, [], 'hg qdelete PATCH'), "^qimport": (qimport, [('e', 'existing', None, 'import file in patch dir'), ('n', 'name', '', 'patch file name'), ('f', 'force', None, 'overwrite existing files')], - 'hg qimport'), + 'hg qimport [-e] [-n NAME] [-f] FILE...'), "^qinit": (init, [('c', 'create-repo', None, 'create patch repository')], - 'hg [-c] qinit'), + 'hg qinit [-c]'), "qnew": (new, [('m', 'message', '', 'commit message'), ('f', 'force', None, 'force')], - 'hg qnew [-m message ] patch'), + 'hg qnew [-m TEXT] [-f] PATCH'), "qnext": (next, [], 'hg qnext'), "qprev": (prev, [], 'hg qprev'), "^qpop": @@ -1268,7 +1262,7 @@ [('a', 'all', None, 'pop all patches'), ('n', 'name', '', 'queue name to pop'), ('f', 'force', None, 'forget any local changes')], - 'hg qpop [options] [patch/index]'), + 'hg qpop [-a] [-n NAME] [-f] [PATCH | INDEX]'), "^qpush": (push, [('f', 'force', None, 'apply if the patch has rejects'), @@ -1276,16 +1270,16 @@ ('a', 'all', None, 'apply all patches'), ('m', 'merge', None, 'merge from another queue'), ('n', 'name', '', 'merge queue name')], - 'hg qpush [options] [patch/index]'), + 'hg qpush [-f] [-l] [-a] [-m] [-n NAME] [PATCH | INDEX]'), "^qrefresh": (refresh, [('s', 'short', None, 'short refresh')], - 'hg qrefresh'), + 'hg qrefresh [-s]'), "qrestore": (restore, [('d', 'delete', None, 'delete save entry'), ('u', 'update', None, 'update queue working dir')], - 'hg qrestore rev'), + 'hg qrestore [-d] [-u] REV'), "qsave": (save, [('m', 'message', '', 'commit message'), @@ -1293,19 +1287,19 @@ ('n', 'name', '', 'copy directory name'), ('e', 'empty', None, 'clear queue status file'), ('f', 'force', None, 'force copy')], - 'hg qsave'), + 'hg qsave [-m TEXT] [-c] [-n NAME] [-e] [-f]'), "qseries": (series, [('m', 'missing', None, 'print patches not in series')], - 'hg qseries'), + 'hg qseries [-m]'), "^strip": (strip, [('f', 'force', None, 'force multi-head removal'), ('b', 'backup', None, 'bundle unrelated changesets'), ('n', 'nobackup', None, 'no backups')], - 'hg strip rev'), + 'hg strip [-f] [-b] [-n] REV'), "qtop": (top, [], 'hg qtop'), - "qunapplied": (unapplied, [], 'hg qunapplied [patch]'), + "qunapplied": (unapplied, [], 'hg qunapplied [PATCH]'), "qversion": (version, [], 'hg qversion') } diff -r fb28ce04b349 -r ee90e5a9197f mercurial/commands.py --- a/mercurial/commands.py Wed May 03 22:47:08 2006 -0700 +++ b/mercurial/commands.py Wed May 03 22:47:57 2006 -0700 @@ -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) @@ -809,13 +621,19 @@ repo.add(names) def addremove(ui, repo, *pats, **opts): - """add all new files, delete all missing files - + """add all new files, delete all missing files (DEPRECATED) + + (DEPRECATED) Add all new files and remove all missing files from the repository. New files are ignored if they match any of the patterns in .hgignore. As with add, these changes take effect at the next commit. + + This command is now deprecated and will be removed in a future + release. Please use add and remove --after instead. """ + ui.warn(_('(the addremove command is deprecated; use add and remove ' + '--after instead)\n')) return addremove_lock(ui, repo, pats, opts) def addremove_lock(ui, repo, pats, opts, wlock=None): @@ -1153,7 +971,7 @@ (logfile, inst.strerror)) if opts['addremove']: - addremove(ui, repo, *pats, **opts) + addremove_lock(ui, repo, pats, opts) fns, match, anypats = matchpats(repo, pats, opts) if pats: modified, added, removed, deleted, unknown = ( @@ -1894,7 +1712,7 @@ files = util.patch(strip, pf, ui) if len(files) > 0: - addremove(ui, repo, *files) + addremove_lock(ui, repo, files, {}) repo.commit(files, message, user) def incoming(ui, repo, source="default", **opts): @@ -2347,7 +2165,7 @@ return repo.verify() return 1 -def remove(ui, repo, pat, *pats, **opts): +def remove(ui, repo, *pats, **opts): """remove the specified files on the next commit Schedule the indicated files for removal from the repository. @@ -2355,29 +2173,36 @@ This command schedules the files to be removed at the next commit. This only removes files from the current branch, not from the entire project history. If the files still exist in the working - directory, they will be deleted from it. + directory, they will be deleted from it. If invoked with --after, + files that have been manually deleted are marked as removed. """ names = [] + if not opts['after'] and not pats: + raise util.Abort(_('no files specified')) def okaytoremove(abs, rel, exact): modified, added, removed, deleted, unknown = repo.changes(files=[abs]) reason = None - if modified and not opts['force']: + if not deleted and opts['after']: + reason = _('is still present') + elif modified and not opts['force']: reason = _('is modified') elif added: reason = _('has been marked for add') elif unknown: reason = _('is not managed') + elif removed: + return False if reason: if exact: ui.warn(_('not removing %s: file %s\n') % (rel, reason)) else: return True - for src, abs, rel, exact in walk(repo, (pat,) + pats, opts): + for src, abs, rel, exact in walk(repo, pats, opts): if okaytoremove(abs, rel, exact): if ui.verbose or not exact: ui.status(_('removing %s\n') % rel) names.append(abs) - repo.remove(names, unlink=True) + repo.remove(names, unlink=not opts['after']) def rename(ui, repo, *pats, **opts): """rename files; equivalent of copy + remove @@ -2916,7 +2741,7 @@ [('I', 'include', [], _('include names matching the given patterns')), ('X', 'exclude', [], _('exclude names matching the given patterns'))], _('hg add [OPTION]... [FILE]...')), - "addremove": + "debugaddremove|addremove": (addremove, [('I', 'include', [], _('include names matching the given patterns')), ('X', 'exclude', [], _('exclude names matching the given patterns'))], @@ -2976,7 +2801,8 @@ _('hg clone [OPTION]... SOURCE [DEST]')), "^commit|ci": (commit, - [('A', 'addremove', None, _('run addremove during commit')), + [('A', 'addremove', None, + _('mark new/missing files as added/removed before committing')), ('m', 'message', '', _('use as commit message')), ('l', 'logfile', '', _('read the commit message from ')), ('d', 'date', '', _('record datecode as commit date')), @@ -3161,7 +2987,8 @@ "recover": (recover, [], _('hg recover')), "^remove|rm": (remove, - [('f', 'force', None, _('remove file even if modified')), + [('', 'after', None, _('record remove that has already occurred')), + ('f', 'force', None, _('remove file even if modified')), ('I', 'include', [], _('include names matching the given patterns')), ('X', 'exclude', [], _('exclude names matching the given patterns'))], _('hg remove [OPTION]... FILE...')), diff -r fb28ce04b349 -r ee90e5a9197f mercurial/localrepo.py --- a/mercurial/localrepo.py Wed May 03 22:47:08 2006 -0700 +++ b/mercurial/localrepo.py Wed May 03 22:47:57 2006 -0700 @@ -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 fb28ce04b349 -r ee90e5a9197f mercurial/templater.py --- a/mercurial/templater.py Wed May 03 22:47:08 2006 -0700 +++ b/mercurial/templater.py Wed May 03 22:47:57 2006 -0700 @@ -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 fb28ce04b349 -r ee90e5a9197f tests/run-tests --- a/tests/run-tests Wed May 03 22:47:08 2006 -0700 +++ b/tests/run-tests Wed May 03 22:47:57 2006 -0700 @@ -69,7 +69,7 @@ INST="$HGTMP/install" PYTHONDIR="$INST/lib/python" cd .. -if ${PYTHON-python} setup.py install --home="$INST" \ +if ${PYTHON-python} setup.py clean --all install --force --home="$INST" \ --install-lib="$PYTHONDIR" > tests/install.err 2>&1 then rm tests/install.err diff -r fb28ce04b349 -r ee90e5a9197f tests/run-tests.py --- a/tests/run-tests.py Wed May 03 22:47:08 2006 -0700 +++ b/tests/run-tests.py Wed May 03 22:47:57 2006 -0700 @@ -69,8 +69,9 @@ installerrs = os.path.join("tests", "install.err") os.chdir("..") # Get back to hg root - cmd = '%s setup.py install --home="%s" --install-lib="%s" >%s 2>&1' % \ - (sys.executable, INST, PYTHONDIR, installerrs) + cmd = ('%s setup.py clean --all' + ' install --force --home="%s" --install-lib="%s" >%s 2>&1' + % (sys.executable, INST, PYTHONDIR, installerrs)) vlog("# Running", cmd) if os.system(cmd) == 0: if not verbose: diff -r fb28ce04b349 -r ee90e5a9197f tests/test-addremove.out --- a/tests/test-addremove.out Wed May 03 22:47:08 2006 -0700 +++ b/tests/test-addremove.out Wed May 03 22:47:57 2006 -0700 @@ -1,7 +1,9 @@ +(the addremove command is deprecated; use add and remove --after instead) adding dir/bar adding foo dir/bar foo +(the addremove command is deprecated; use add and remove --after instead) adding dir/bar_2 adding foo_2 dir/bar_2 diff -r fb28ce04b349 -r ee90e5a9197f tests/test-archive --- a/tests/test-archive Wed May 03 22:47:08 2006 -0700 +++ b/tests/test-archive Wed May 03 22:47:57 2006 -0700 @@ -29,8 +29,8 @@ % (node, archive)) sys.stdout.write(f.read()) EOF -http_proxy= python getarchive.py "$TIP" gz | gunzip -dc - | tar tf - | sed "s/$QTIP/TIP/" -http_proxy= python getarchive.py "$TIP" bz2 | bunzip2 -dc - | tar tf - | sed "s/$QTIP/TIP/" +http_proxy= python getarchive.py "$TIP" gz | gunzip | tar tf - | sed "s/$QTIP/TIP/" +http_proxy= python getarchive.py "$TIP" bz2 | bunzip2 | tar tf - | sed "s/$QTIP/TIP/" http_proxy= python getarchive.py "$TIP" zip > archive.zip unzip -t archive.zip | sed "s/$QTIP/TIP/" diff -r fb28ce04b349 -r ee90e5a9197f tests/test-archive.out --- a/tests/test-archive.out Wed May 03 22:47:08 2006 -0700 +++ b/tests/test-archive.out Wed May 03 22:47:57 2006 -0700 @@ -1,5 +1,8 @@ +(the addremove command is deprecated; use add and remove --after instead) adding foo +(the addremove command is deprecated; use add and remove --after instead) adding bar +(the addremove command is deprecated; use add and remove --after instead) adding baz/bletch test-archive-TIP/.hg_archival.txt test-archive-TIP/bar diff -r fb28ce04b349 -r ee90e5a9197f tests/test-backout --- a/tests/test-backout Wed May 03 22:47:08 2006 -0700 +++ b/tests/test-backout Wed May 03 22:47:57 2006 -0700 @@ -28,7 +28,7 @@ echo '# backout of backout is as if nothing happened' hg backout -d '3 0' --merge tip -cat a +cat a 2>/dev/null || echo cat: a: No such file or directory echo '# backout with merge' cd .. diff -r fb28ce04b349 -r ee90e5a9197f tests/test-help.out --- a/tests/test-help.out Wed May 03 22:47:08 2006 -0700 +++ b/tests/test-help.out Wed May 03 22:47:57 2006 -0700 @@ -38,92 +38,90 @@ list of commands (use "hg help -v" to show aliases and global options): - add add the specified files on the next commit - addremove add all new files, delete all missing files - annotate show changeset information per file line - archive create unversioned archive of a repository revision - backout reverse effect of earlier changeset - bundle create a changegroup file - cat output the latest or given revisions of files - clone make a copy of an existing repository - commit commit the specified files or all outstanding changes - copy mark files as copied for the next commit - diff diff repository (or selected files) - export dump the header and diffs for one or more changesets - grep search for a pattern in specified files and revisions - heads show current repository heads - help show help for a given command or all commands - identify print information about the working copy - import import an ordered set of patches - incoming show new changesets found in source - init create a new repository in the given directory - locate locate files matching specific patterns - log show revision history of entire repository or files - manifest output the latest or given revision of the project manifest - merge Merge working directory with another revision - outgoing show changesets not found in destination - parents show the parents of the working dir or revision - paths show definition of symbolic path names - pull pull changes from the specified source - push push changes to the specified destination - recover roll back an interrupted transaction - remove remove the specified files on the next commit - rename rename files; equivalent of copy + remove - revert revert modified files or dirs back to their unmodified states - root print the root (top) of the current working dir - serve export the repository via HTTP - status show changed files in the working directory - tag add a tag for the current tip or a given revision - tags list repository tags - tip show the tip revision - unbundle apply a changegroup file - undo undo the last commit or pull - update update or merge working directory - verify verify the integrity of the repository - version output version and copyright information - add add the specified files on the next commit - addremove add all new files, delete all missing files - annotate show changeset information per file line - archive create unversioned archive of a repository revision - backout reverse effect of earlier changeset - bundle create a changegroup file - cat output the latest or given revisions of files - clone make a copy of an existing repository - commit commit the specified files or all outstanding changes - copy mark files as copied for the next commit - diff diff repository (or selected files) - export dump the header and diffs for one or more changesets - grep search for a pattern in specified files and revisions - heads show current repository heads - help show help for a given command or all commands - identify print information about the working copy - import import an ordered set of patches - incoming show new changesets found in source - init create a new repository in the given directory - locate locate files matching specific patterns - log show revision history of entire repository or files - manifest output the latest or given revision of the project manifest - merge Merge working directory with another revision - outgoing show changesets not found in destination - parents show the parents of the working dir or revision - paths show definition of symbolic path names - pull pull changes from the specified source - push push changes to the specified destination - recover roll back an interrupted transaction - remove remove the specified files on the next commit - rename rename files; equivalent of copy + remove - revert revert modified files or dirs back to their unmodified states - root print the root (top) of the current working dir - serve export the repository via HTTP - status show changed files in the working directory - tag add a tag for the current tip or a given revision - tags list repository tags - tip show the tip revision - unbundle apply a changegroup file - undo undo the last commit or pull - update update or merge working directory - verify verify the integrity of the repository - version output version and copyright information + add add the specified files on the next commit + annotate show changeset information per file line + archive create unversioned archive of a repository revision + backout reverse effect of earlier changeset + bundle create a changegroup file + cat output the latest or given revisions of files + clone make a copy of an existing repository + commit commit the specified files or all outstanding changes + copy mark files as copied for the next commit + diff diff repository (or selected files) + export dump the header and diffs for one or more changesets + grep search for a pattern in specified files and revisions + heads show current repository heads + help show help for a given command or all commands + identify print information about the working copy + import import an ordered set of patches + incoming show new changesets found in source + init create a new repository in the given directory + locate locate files matching specific patterns + log show revision history of entire repository or files + manifest output the latest or given revision of the project manifest + merge Merge working directory with another revision + outgoing show changesets not found in destination + parents show the parents of the working dir or revision + paths show definition of symbolic path names + pull pull changes from the specified source + push push changes to the specified destination + recover roll back an interrupted transaction + remove remove the specified files on the next commit + rename rename files; equivalent of copy + remove + revert revert modified files or dirs back to their unmodified states + root print the root (top) of the current working dir + serve export the repository via HTTP + status show changed files in the working directory + tag add a tag for the current tip or a given revision + tags list repository tags + tip show the tip revision + unbundle apply a changegroup file + undo undo the last commit or pull + update update or merge working directory + verify verify the integrity of the repository + version output version and copyright information + add add the specified files on the next commit + annotate show changeset information per file line + archive create unversioned archive of a repository revision + backout reverse effect of earlier changeset + bundle create a changegroup file + cat output the latest or given revisions of files + clone make a copy of an existing repository + commit commit the specified files or all outstanding changes + copy mark files as copied for the next commit + diff diff repository (or selected files) + export dump the header and diffs for one or more changesets + grep search for a pattern in specified files and revisions + heads show current repository heads + help show help for a given command or all commands + identify print information about the working copy + import import an ordered set of patches + incoming show new changesets found in source + init create a new repository in the given directory + locate locate files matching specific patterns + log show revision history of entire repository or files + manifest output the latest or given revision of the project manifest + merge Merge working directory with another revision + outgoing show changesets not found in destination + parents show the parents of the working dir or revision + paths show definition of symbolic path names + pull pull changes from the specified source + push push changes to the specified destination + recover roll back an interrupted transaction + remove remove the specified files on the next commit + rename rename files; equivalent of copy + remove + revert revert modified files or dirs back to their unmodified states + root print the root (top) of the current working dir + serve export the repository via HTTP + status show changed files in the working directory + tag add a tag for the current tip or a given revision + tags list repository tags + tip show the tip revision + unbundle apply a changegroup file + undo undo the last commit or pull + update update or merge working directory + verify verify the integrity of the repository + version output version and copyright information hg add [OPTION]... [FILE]... add the specified files on the next commit diff -r fb28ce04b349 -r ee90e5a9197f tests/test-pull.out --- a/tests/test-pull.out Wed May 03 22:47:08 2006 -0700 +++ b/tests/test-pull.out Wed May 03 22:47:57 2006 -0700 @@ -1,3 +1,4 @@ +(the addremove command is deprecated; use add and remove --after instead) adding foo checking changesets checking manifests diff -r fb28ce04b349 -r ee90e5a9197f tests/test-remove --- a/tests/test-remove Wed May 03 22:47:08 2006 -0700 +++ b/tests/test-remove Wed May 03 22:47:57 2006 -0700 @@ -5,8 +5,12 @@ echo a > foo hg add foo hg commit -m 1 -d "1000000 0" +hg remove rm foo hg remove foo +hg revert +rm foo +hg remove --after hg commit -m 2 -d "1000000 0" hg export 0 hg export 1 diff -r fb28ce04b349 -r ee90e5a9197f tests/test-remove.out --- a/tests/test-remove.out Wed May 03 22:47:08 2006 -0700 +++ b/tests/test-remove.out Wed May 03 22:47:57 2006 -0700 @@ -1,3 +1,6 @@ +abort: no files specified +undeleting foo +removing foo # HG changeset patch # User test # Node ID 8ba83d44753d6259db5ce6524974dd1174e90f47 diff -r fb28ce04b349 -r ee90e5a9197f tests/test-simple-update.out --- a/tests/test-simple-update.out Wed May 03 22:47:08 2006 -0700 +++ b/tests/test-simple-update.out Wed May 03 22:47:57 2006 -0700 @@ -1,3 +1,4 @@ +(the addremove command is deprecated; use add and remove --after instead) adding foo checking changesets checking manifests diff -r fb28ce04b349 -r ee90e5a9197f tests/test-symlinks.out --- a/tests/test-symlinks.out Wed May 03 22:47:08 2006 -0700 +++ b/tests/test-symlinks.out Wed May 03 22:47:57 2006 -0700 @@ -1,4 +1,6 @@ +(the addremove command is deprecated; use add and remove --after instead) adding foo +(the addremove command is deprecated; use add and remove --after instead) adding bomb adding a.c adding dir/a.o diff -r fb28ce04b349 -r ee90e5a9197f tests/test-up-local-change.out --- a/tests/test-up-local-change.out Wed May 03 22:47:08 2006 -0700 +++ b/tests/test-up-local-change.out Wed May 03 22:47:57 2006 -0700 @@ -1,3 +1,4 @@ +(the addremove command is deprecated; use add and remove --after instead) adding a 1 files updated, 0 files merged, 0 files removed, 0 files unresolved 0 files updated, 0 files merged, 0 files removed, 0 files unresolved @@ -7,6 +8,7 @@ @@ -1,1 +1,1 @@ a -a +abc +(the addremove command is deprecated; use add and remove --after instead) adding b M a changeset: 0:33aaa84a386b @@ -88,6 +90,7 @@ -a2 +abc 1 files updated, 0 files merged, 1 files removed, 0 files unresolved +(the addremove command is deprecated; use add and remove --after instead) adding b M a changeset: 1:802f095af299 diff -r fb28ce04b349 -r ee90e5a9197f tests/test-walk.out --- a/tests/test-walk.out Wed May 03 22:47:08 2006 -0700 +++ b/tests/test-walk.out Wed May 03 22:47:57 2006 -0700 @@ -1,3 +1,4 @@ +(the addremove command is deprecated; use add and remove --after instead) adding beans/black adding beans/borlotti adding beans/kidney