895 lines
34 KiB
Python
Raw Normal View History

2011-12-27 11:10:01 +13:00
# b.py - Distributed Bug Tracker Extention for Mercurial
#
# Copyright 2010-2011 Michael Diamond <michael@digitalgemstones.com>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.
# http://www.gnu.org/licenses/licenses.html
# http://www.gnu.org/licenses/gpl.html
""" A lightweight distributed bug tracker for Mercurial based projects
"The only way to make your bug list prettier is to fix some damn bugs."
b is a lightweight distributed bug tracker. Stripped of many of the
enterprise level bloat features common in larger bug systems, b
lets you track issues, bugs, and features without being bogged down
in extra metadata that is ultimately completely unhelpful.
b has functionality to add, rename, list, resolve and reopen bugs
and keep everything as simple as a single line of text describing each one.
But if and when you need more than that, b scales cleanly to allow
you to add details that can't be properly contained in a concise title
such as stack traces, line numbers, and the like, and allows you to
add comments to bugs as time goes on.
b also works with teams, allowing you to assign bugs to different users
and keep track of bugs assigned to you.
However, b is a lightweight tool, and if there are additional features
you know you need but aren't described here, it may not be the tool for you.
See the README file for more details on what you can, and can't, do with b.
"""
#
# Imports
#
import os, errno, re, hashlib, sys, subprocess, tempfile, time
2012-05-20 12:12:19 +01:00
from datetime import date, datetime
2011-12-27 11:10:01 +13:00
from operator import itemgetter
from mercurial.i18n import _
from mercurial import hg,commands
#
# Version
#
2012-05-20 12:12:19 +01:00
_major_version = 0
_minor_version = 6
_fix_version = 2
_build_date = date(2012,3,4)
2011-12-27 11:10:01 +13:00
#
# Static values / config settings
#
"""By default, IDs are made from title, time, and username when availible.
When true, only the title is used to make IDs."""
_simple_hash = False
#
# Exceptions
#
class InvalidDetailsFile(Exception):
def __init__(self,prefix):
"""Raised when a bug's details file is invalid (is a dir)"""
super(InvalidDetailsFile, self).__init__()
self.prefix = prefix
class InvalidTaskfile(Exception):
"""Raised when the path to a task file already exists as a directory."""
def __init__(self, reason=''):
super(InvalidTaskfile, self).__init__()
self.reason = reason
class AmbiguousPrefix(Exception):
"""Raised when trying to use a prefix that could identify multiple tasks."""
def __init__(self, prefix):
super(AmbiguousPrefix, self).__init__()
self.prefix = prefix
class UnknownPrefix(Exception):
"""Raised when trying to use a prefix that does not match any tasks."""
def __init__(self, prefix):
super(UnknownPrefix, self).__init__()
self.prefix = prefix
class AmbiguousUser(Exception):
"""Raised when trying to use a user prefix that could identify multiple users."""
def __init__(self, user, matched):
super(AmbiguousUser, self).__init__()
self.user = user
self.matched = matched
class UnknownUser(Exception):
"""Raised when trying to use a user prefix that does not match any users."""
def __init__(self, user):
super(UnknownUser, self).__init__()
self.user = user
class InvalidInput(Exception):
"""Raised when the input to a command is somehow invalid - for example,
a username with a | character will cause problems parsing the bugs file."""
def __init__(self, reason):
super(InvalidInput, self).__init__()
self.reason = reason
class AmbiguousCommand(Exception):
"""Raised when trying to run a command by prefix that matches more than one command."""
def __init__(self, cmd):
super(AmbiguousCommand, self).__init__()
self.cmd = cmd
class UnknownCommand(Exception):
"""Raised when trying to run an unknown command."""
def __init__(self, cmd):
super(UnknownCommand, self).__init__()
self.cmd = cmd
class NonReadOnlyCommand(Exception):
"""Raised when user tries to run a destructive command against a read only issue db."""
def __init__(self, cmd):
super(NonReadOnlyCommand, self).__init__()
self.cmd = cmd
#
# Helper Methods - often straight from t
#
def _datetime(t = ''):
""" Returns a formatted string of the time from a timestamp, or now if t is not set. """
if t == '':
t = datetime.now()
else:
t = datetime.fromtimestamp(float(t))
return t.strftime("%A, %B %d %Y %I:%M%p")
def _hash(text):
"""Return a hash of the given text for use as an id.
Currently SHA1 hashing is used. It should be plenty for our purposes.
"""
return hashlib.sha1(text.encode('utf-8')).hexdigest()
def _mkdir_p(path):
""" race condition handling recursive mkdir -p call
http://stackoverflow.com/questions/600268/mkdir-p-functionality-in-python
"""
try:
os.makedirs(path)
except OSError, exc:
if exc.errno == errno.EEXIST:
pass
else: raise
def _truth(str):
""" Indicates the truth of a string """
return str == 'True'
def _task_from_taskline(taskline):
"""Parse a taskline (from a task file) and return a task.
A taskline should be in the format:
summary text ... | meta1:meta1_value,meta2:meta2_value,...
The task returned will be a dictionary such as:
{ 'id': <hash id>,
'text': <summary text>,
... other metadata ... }
A taskline can also consist of only summary text, in which case the id
and other metadata will be generated when the line is read. This is
supported to enable editing of the taskfile with a simple text editor.
"""
try:
if '|' in taskline:
text, meta = taskline.rsplit('|',1)
task = { 'text': text.strip() }
for piece in meta.strip().split(','):
label, data = piece.split(':',1)
task[label.strip()] = data.strip()
else:
text = taskline.strip()
global _simple_hash
task = { 'id': _hash(text) if _simple_hash else _hash(text+str(time.time())), 'text': text, 'owner': '', 'open': 'True', 'time': time.time() }
return task
except Exception:
raise InvalidTaskfile(_("perhaps a missplaced '|'?\n"
"Line is: %s") % taskline)
def _tasklines_from_tasks(tasks):
"""Parse a list of tasks into tasklines suitable for writing to a file."""
tasklines = []
for task in tasks:
meta = [m for m in task.items() if m[0] != 'text']
meta_str = ', '.join('%s:%s' % m for m in meta)
tasklines.append('%s | %s\n' % (task['text'].ljust(60), meta_str))
return tasklines
def _prefixes(ids):
"""Return a mapping of ids to prefixes in O(n) time.
This is much faster than the naitive t function, which
takes O(n^2) time.
Each prefix will be the shortest possible substring of the ID that
can uniquely identify it among the given group of IDs.
If an ID of one task is entirely a substring of another task's ID, the
entire ID will be the prefix.
"""
pre = {}
for id in ids:
id_len = len(id)
for i in range(1, id_len+1):
""" identifies an empty prefix slot, or a singular collision """
prefix = id[:i]
if (not prefix in pre) or (pre[prefix] != ':' and prefix != pre[prefix]):
break
if prefix in pre:
""" if there is a collision """
collide = pre[prefix]
for j in range(i,id_len+1):
if collide[:j] == id[:j]:
pre[id[:j]] = ':'
else:
pre[collide[:j]] = collide
pre[id[:j]] = id
break
else:
pre[collide[:id_len+1]] = collide
pre[id] = id
else:
""" no collision, can safely add """
pre[prefix] = id
pre = dict(zip(pre.values(),pre.keys()))
if ':' in pre:
del pre[':']
return pre
def _describe_print(num,type,owner,filter):
""" Helper function used by list to describe the data just displayed """
typeName = 'open' if type else 'resolved'
out = _("Found %s %s bug%s") % (num, typeName, '' if num==1 else 's')
if owner != '*':
out = out+(_(" owned by %s") % ('Nobody' if owner=='' else owner))
if filter != '':
out = out+_(" whose title contains %s") % filter
return out
#
# Primary Class
#
class BugsDict(object):
"""A set of bugs, issues, and tasks, both finished and unfinished, for a given repository.
The list's file is read from disk when initialized. The items
can be written back out to disk with the write() function.
You can specify any taskdir you want, but the intent is to work from the cwd
and therefore anything calling this class ought to handle that change
(normally to the repo root)
"""
def __init__(self,bugsdir='.bugs',user='',fast_add=False):
"""Initialize by reading the task files, if they exist."""
self.bugsdir = bugsdir
self.user = user
self.fast_add = fast_add
self.file = 'bugs'
self.detailsdir = 'details'
self.last_added_id = None
self.bugs = {}
# this is the default contents of the bugs directory. If you'd like, you can
# modify this variable's contents. Be sure to leave [comments] as the last field.
# Remember that storing metadata like [reporter] in the details file is not secure.
# it is recommended that you use Mercurial's excellent data-mining tools such as log
# and annotate to get such information.
self.init_details = '\n'.join([
"# Lines starting with '#' and sections without content\n# are not displayed by a call to 'details'\n#",
#"[reporter]\n# The user who created this file\n# This field can be edited, and is just a convenience\n%s\n" % self.user,
"[Website]\n# If this applys to other websites, or other previous commits\n\n",
"[filters]\n# Copy your Adblock filters here\n\n",
"\n\n[other]\n# Any other details\n\n",
#"[actual]\n# What happened instead\n\n",
#"[stacktrace]\n# A stack trace or similar diagnostic info\n\n",
#"[reproduce]\n# Reproduction steps\n\n",
"[comments]\n# Leave your username"
])
path = os.path.join(os.path.expanduser(self.bugsdir), self.file)
if os.path.isdir(path):
raise InvalidTaskfile(_("The path where the bugs database should be is blocked and cannot be created."))
if os.path.exists(path):
tfile = open(path, 'r')
tlns = tfile.readlines()
tls = [tl.strip() for tl in tlns if tl.strip()]
tasks = map(_task_from_taskline, tls)
for task in tasks:
self.bugs[task['id']] = task
tfile.close()
def write(self):
"""Flush the finished and unfinished tasks to the files on disk."""
_mkdir_p(self.bugsdir)
path = os.path.join(os.path.expanduser(self.bugsdir), self.file)
if os.path.isdir(path):
raise InvalidTaskfile(_("The path where the bugs database should be is blocked and cannot be created."))
tasks = sorted(self.bugs.values(), key=itemgetter('id'))
tfile = open(path, 'w')
for taskline in _tasklines_from_tasks(tasks):
tfile.write(taskline)
tfile.close()
def __getitem__(self, prefix):
"""Return the task with the given prefix.
If more than one task matches the prefix an AmbiguousPrefix exception
will be raised, unless the prefix is the entire ID of one task.
If no tasks match the prefix an UnknownPrefix exception will be raised.
"""
matched = [item for item in self.bugs.keys() if item.startswith(prefix)]
if len(matched) == 1:
return self.bugs[matched[0]]
elif len(matched) == 0:
raise UnknownPrefix(prefix)
else:
matched = [item for item in self.bugs.keys() if item == prefix]
if len(matched) == 1:
return self.bugs[matched[0]]
else:
raise AmbiguousPrefix(prefix)
def _get_details_path(self,full_id):
""" Returns the directory and file path to the details specified by id """
dirpath = os.path.join(self.bugsdir,self.detailsdir)
path = os.path.join(dirpath,full_id+".txt")
return (dirpath,path)
def _make_details_file(self,full_id):
""" Create a details file for the given id """
(dirpath,path) = self._get_details_path(full_id)
if not os.path.exists(dirpath):
_mkdir_p(dirpath)
if os.path.isdir(path):
raise InvalidDetailsFile(full_id)
if not os.path.exists(path):
f = open(path, "w+")
f.write(self.init_details)
f.close()
return path
def _users_list(self):
""" Returns a mapping of usernames to the number of open bugs assigned to that user """
open = [item['owner'] for item in self.bugs.values() if _truth(item['open'])]
closed = [item['owner'] for item in self.bugs.values() if not _truth(item['open'])]
users = {}
for user in open:
if user in users:
users[user] += 1
else:
users[user] = 1
for user in closed:
if not user in users:
users[user] = 0
if '' in users:
users['Nobody'] = users['']
del users['']
return users
def _get_user(self,user,force=False):
""" Given a user prefix, returns the appropriate username, or fails if
the correct user cannot be identified.
'me' is a special username which maps to the username specified when
constructing the BugsDict.
'Nobody' (and prefixes of 'Nobody') is a special username which maps
internally to the empty string, indicating no assignment.
If force is true, the user 'Nobody' is used. This is unadvisable,
avoid forcing the username 'Nobody'.
If force is true, it assumes user is not a prefix and should be
assumed to exist already.
"""
if user == 'me':
return self.user
if user == 'Nobody':
return ''
users = self._users_list().keys()
if not force:
if not user in users:
usr = user.lower()
matched = [u for u in users if u.lower().startswith(usr)]
if len(matched) > 1:
raise AmbiguousUser(user,matched)
if len(matched) == 0:
raise UnknownUser(user)
user = matched[0]
if user == 'Nobody': # needed twice, since users can also type a prefix to get it
return ''
else: # we're forcing a new username
if '|' in user:
raise InvalidInput(_("Usernames cannot contain '|'."))
return user
def id(self, prefix):
""" Given a prefix, returns the full id of that bug """
return self[prefix]['id']
def add(self, text):
"""Adds a bug with no owner to the task list"""
global _simple_hash
task_id = _hash(text) if _simple_hash else _hash(text+self.user+str(time.time()))
self.bugs[task_id] = {'id': task_id, 'open': 'True', 'owner': self.user, 'text': text, 'time': time.time()}
self.last_added_id = task_id
if not self.fast_add:
prefix = _prefixes(self.bugs.keys())[task_id]
prefix = "%s:%s" % (prefix, task_id[len(prefix):10])
else:
prefix = "%s..." % task_id[:10]
return _("Added bug %s") % prefix
def rename(self, prefix, text):
"""Renames the bug
If more than one task matches the prefix an AmbiguousPrefix exception
will be raised, unless the prefix is the entire ID of one task.
If no tasks match the prefix an UnknownPrefix exception will be raised.
"""
task = self[prefix]
if text.startswith('s/') or text.startswith('/'):
text = re.sub('^s?/', '', text).rstrip('/')
find, _, repl = text.partition('/')
text = re.sub(find, repl, task['text'])
task['text'] = text
def users(self):
""" Prints a list of users along with the number of open bugs they have """
users = self._users_list()
if len(users) > 0:
ulen = max([len(user) for user in users.keys()])+1
else:
ulen = 0
out = _("Username: Open Bugs\n")
for (user,count) in users.items():
out += _("%s: %s\n") % (user,str(count).rjust(ulen-len(user)))
return out
def assign(self, prefix, user,force=False):
"""Specifies a new owner of the bug. Tries to guess the correct user,
or warns if it cannot find an appropriate user.
Using the -f flag will create a new user with that exact name,
it will not try to guess, or warn the user."""
task = self[prefix]
user = self._get_user(user,force)
task['owner'] = user
if user == '':
user = 'Nobody'
return _("Assigned %s: '%s' to %s" % (prefix, task['text'], user))
def details(self, prefix):
""" Provides additional details on the requested bug.
Metadata (like owner, and creation time) which are
not stored in the details file are displayed along with
the details.
Sections (denoted by a [text] line) with no content
are not displayed.
"""
task = self[prefix] # confirms prefix does exist
path = self._get_details_path(task['id'])[1]
if os.path.exists(path):
if os.path.isdir(path):
raise InvalidDetailsFile(prefix)
f = open(path)
text = f.read()
f.close()
text = re.sub("(?m)^#.*\n?", "", text)
while True:
oldtext = text
retext = re.sub("\[\w+\]\s+\[", "[", text)
text = retext
if oldtext == retext:
break
text = re.sub("\[\w+\]\s*$", "", text)
else:
text = _('No Details File Found.')
header = _("Title: %s\nID: %s\n") % (task['text'],task['id'])
if not _truth(task['open']):
header = header + _("*Resolved* ")
if task['owner'] != '':
header = header + (_("Owned By: %s\n") % task['owner'])
header = header + (_("Filed On: %s\n\n") % _datetime(task['time']))
text = header + text
return text.strip()
def edit(self, prefix, editor='notepad'):
"""Allows the user to edit the details of the specified bug"""
task = self[prefix] # confirms prefix does exist
path = self._get_details_path(task['id'])[1]
if not os.path.exists(path):
self._make_details_file(task['id'])
subprocess.call(editor.split() + [path])
#subprocess.call()
#print _timestamp()
def comment(self, prefix, comment):
"""Allows the user to add a comment to the bug without launching an editor.
If they have a username set, the comment will show who made it."""
task = self[prefix] # confirms prefix does exist
path = self._get_details_path(task['id'])[1]
if not os.path.exists(path):
self._make_details_file(task['id'])
comment = _("On: %s\n%s") % (_datetime(),comment)
if self.user != '':
comment = _("By: %s\n%s") % (self.user,comment)
f = open(path, "a")
f.write("\n\n"+comment)
f.close()
def resolve(self, prefix):
"""Marks a bug as resolved"""
task = self[prefix]
task['open'] = 'False'
def reopen(self, prefix):
"""Reopens a bug that was previously resolved"""
task = self[prefix]
task['open'] = 'True'
def list(self,open=True,owner='*',grep='',alpha=False,chrono=False,truncate=0):
"""Lists all bugs, applying the given filters"""
tasks = dict(self.bugs.items())
prefixes = _prefixes(tasks).items()
for task_id, prefix in prefixes:
tasks[task_id]['prefix'] = prefix
if owner != '*':
owner = self._get_user(owner)
small = [task for task in tasks.values() if _truth(task['open']) == open and
(owner == '*' or owner == task['owner']) and
(grep == '' or grep.lower() in task['text'].lower())]
if len(small) > 0:
plen = max([len(task['prefix']) for task in small])
else:
plen = 0
out = ''
if alpha:
small = sorted(small, key=lambda x: x['text'].lower())
if chrono:
small = sorted(small, key=itemgetter('time'))
for task in small:
line = _('%s - %s') % (task['prefix'].ljust(plen),task['text'])
if truncate > 0 and len(line) > truncate:
line = line[:truncate-4]+'...'
out += line+'\n'
return out + _describe_print(len(small),open,owner,grep)
#
# Mercurial Extention Operations
# These are used to allow the tool to work as a Hg Extention
#
def _track(ui,repo,dir):
""" Adds new files to Mercurial. """
if os.path.exists(dir):
ui.pushbuffer()
commands.add(ui,repo,dir)
ui.popbuffer()
def _cat(ui,repo,file,todir,rev=None):
ui.pushbuffer()
commands.cat(ui,repo,file,rev=rev,output=os.path.join(todir,file))
ui.popbuffer()
#
# Command line processing
#
def cmd(ui,repo,cmd = 'list',*args,**opts):
""" Distributed Bug Tracker For Mercurial
List of Commands::
add text [-e]
Adds a new open bug to the database, if user is set in the config files, assigns it to user
-e here and elsewhere launches the details editor for the issue upon successful execution of the command
rename prefix text [-e]
Renames The bug denoted by prefix to text. You can use sed-style substitution strings if so desired.
users [--rev rev]
Displays a list of all users, and the number of open bugs assigned to each of them
assign prefix username [-f] [-e]
Assigns bug denoted by prefix to username. Username can be a lowercase prefix of
another username and it will be mapped to that username. To avoid this functionality
and assign the bug to the exact username specified, or if the user does not already
exist in the bugs system, use the -f flag to force the name.
Use 'me' to assign the bug to the current user,
and 'Nobody' to remove its assignment.
details [--rev rev] prefix [-e]
Prints the extended details of the specified bug
edit prefix
Launches your specified editor to provide additional details
comment prefix comment [-e]
Appends comment to the details of the bug, along with the date
and, if specified, your username without needing to launch an editor
resolve prefix [-e]
Marks the specified bug as resolved
reopen prefix [-e]
Marks the specified bug as open
list [--rev rev] [-r] [-o owner] [-g search] [-a|-c]
Lists all bugs, with the following filters:
-r list resolved bugs.
-o list bugs assigned to owner. '*' will list all bugs, 'me' will list all bugs assigned to the current user, and 'Nobody' will list all unassigned bugs.
-g filter by the search string appearing in the title
-a list bugs alphabetically
-c list bugs chronologically
id [--rev rev] prefix [-e]
Takes a prefix and returns the full id of that bug
version
Outputs the version number of b being used in this repository
"""
text = (' '.join(args)).strip();
id = ''
subtext = ''
if len(args) > 0:
id = args[0]
if len(args) > 1:
subtext = (' '.join(args[1:])).strip()
try:
2012-05-20 12:12:19 +01:00
bugsdir = bugs_dir(ui)
2011-12-27 11:10:01 +13:00
user = ui.config("bugs","user",'')
fast_add = ui.configbool("bugs","fast_add",False)
if user == 'hg.user':
user = ui.username()
path = repo.root
os.chdir(path)
# handle other revisions
## The methodology here is to use or create a directory
## in the user's /tmp directory for the given revision
## and store whatever files are being accessed there,
## then simply set path to the temporary repodir
if opts['rev']:
# TODO error on non-readonly command
rev = str(repo[opts['rev']])
tempdir = tempfile.gettempdir()
revpath = os.path.join(tempdir,'b-'+rev)
_mkdir_p(os.path.join(revpath,bugsdir))
if not os.path.exists(os.path.join(revpath,bugsdir,'bugs')):
_cat(ui,repo,os.path.join(bugsdir,'bugs'),revpath,rev)
os.chdir(revpath)
bd = BugsDict(bugsdir,user,fast_add)
if opts['rev'] and 'details'.startswith(cmd):
# if it's a details command, try to get the details file
# if the lookup fails, we don't need to worry about it, the
# standard error handling will catch it and warn the user
fullid = bd.id(id)
detfile = os.path.join(bugsdir,'details',fullid+'.txt')
if not os.path.exists(os.path.join(revpath,detfile)):
_mkdir_p(os.path.join(revpath,bugsdir,'details'))
os.chdir(path)
_cat(ui,repo,detfile,revpath,rev)
os.chdir(revpath)
def _add():
ui.write(bd.add(text) + '\n')
bd.write()
def _rename():
bd.rename(id, subtext)
bd.write()
def _users():
ui.write(bd.users() + '\n')
def _assign():
ui.write(bd.assign(id, subtext, opts['force']) + '\n')
bd.write()
def _details():
ui.write(bd.details(id) + '\n')
def _edit():
bd.edit(id, ui.geteditor())
def _comment():
bd.comment(id, subtext)
def _resolve():
bd.resolve(id)
bd.write()
def _reopen():
bd.reopen(id)
bd.write()
def _list():
ui.write(bd.list(not opts['resolved'], opts['owner'], opts['grep'],
opts['alpha'], opts['chrono'], ui.termwidth() if opts['truncate'] else 0) + '\n')
def _id():
ui.write(bd.id(id) + '\n')
def _help():
commands.help_(ui,'b')
def _version():
2012-05-20 12:12:19 +01:00
ui.write(_("b Version %d.%d.%d - built %s\n") % (_major_version,_minor_version,_fix_version,_build_date))
2011-12-27 11:10:01 +13:00
readonly_cmds = set(['users','details','list','id'])
cmds = {
'add': _add,
'rename': _rename,
'users': _users,
'assign': _assign,
'details': _details,
'edit': _edit,
'comment': _comment,
'resolve': _resolve,
'reopen': _reopen,
'list': _list,
'id': _id,
'help': _help,
'version': _version,
}
candidates = [c for c in cmds if c.startswith(cmd)]
real_candidate = [c for c in candidates if c == cmd]
if real_candidate:
pass # already valid command
elif len(candidates) > 1:
raise AmbiguousCommand(candidates)
elif len(candidates) == 1:
cmd = candidates[0]
else:
raise UnknownCommand(cmd)
# ensure only read only commands can handle revision selection
if opts['rev'] and cmd not in readonly_cmds:
raise NonReadOnlyCommand(cmd)
cmds[cmd]()
# launch the editor - will fail on commands that don't have an issue prefix
if cmd != 'edit' and opts['edit']:
if opts['rev']:
raise NonReadOnlyCommand('edit')
if cmd == 'add':
id = bd.last_added_id
cmds['edit']()
# Add all new files to Mercurial - does not commit
if not opts['rev']:
_track(ui,repo,bugsdir)
except InvalidDetailsFile, e:
ui.warn(_("The path where %s's details should be is blocked and cannot be created. Are there directories in the details dir?\n"))
except InvalidTaskfile, e:
ui.warn(_("Invalid bugs database: %s\n") % e.reason)
except InvalidInput, e:
ui.warn(_("Invalid input: %s\n") % e.reason)
except AmbiguousPrefix, e:
if (id == ''):
ui.warn(_("You need to provide an issue prefix. Run list to get a unique prefix for the bug you are looking for.\n"))
else:
ui.warn(_("The provided prefix - %s - is ambiguous, and could point to multiple bugs. Run list to get a unique prefix for the bug you are looking for.\n") % e.prefix)
except UnknownPrefix, e:
if (id == ''):
ui.warn(_("You need to provide an issue prefix. Run list to get a unique prefix for the bug you are looking for.\n"))
else:
ui.warn(_("The provided prefix - %s - could not be found in the bugs database.\n") % e.prefix)
except AmbiguousUser, e:
ui.warn(_("The provided user - %s - matched more than one user: %s\n") % (e.user, e.matched))
except UnknownUser, e:
ui.warn(_("The provided user - %s - did not match any users in the system. Use -f to force the creation of a new user.\n") % e.user)
except UnknownCommand, e:
ui.warn(_("No such command '%s'\n") % e.cmd)
except AmbiguousCommand, e:
ui.warn(_("Command ambiguous between: %s\n") % (', '.join(e.cmd)))
except NonReadOnlyCommand, e:
ui.warn(_("'%s' is not a read-only command - cannot run against a past revision\n") % e.cmd)
#open=True,owner='*',grep='',verbose=False,quiet=False):
cmdtable = {"b|bug|bugs": (cmd,[
('f', 'force', False, _('Force this exact username')),
('e', 'edit', False, _('Launch details editor after running command')),
('r', 'resolved', False, _('List resolved bugs')),
('o', 'owner', '*', _('Specify an owner to list by')),
('g', 'grep', '', _('Filter titles by STRING')),
('a', 'alpha', False, _('Sort list alphabetically')),
('c', 'chrono', False, _('Sort list chronologically')),
('T', 'truncate', False, _('Truncate list output to fit window')),
('', 'rev', '', _('Run a read-only command against a different revision'))
]
,_("cmd [args]"))}
2012-05-20 12:12:19 +01:00
#
# Programmatic access to b
#
def version(version = None):
"""Returns a numerical representation of the version number, or takes a version string.
Can be used for comparison:
b.version() > b.version("0.7.0")
Note: Before version 0.6.2 these functions did not exist. A call to:
getattr(b,"version",None) == None
indicates a version before 0.6.2"""
def num_version(a,b,c):
return a*100+b+float('.%d' % c)
if(version):
a,b,c = [int(ver) for ver in version.split('.') if ver.isdigit()]
return num_version(a,b,c)
return num_version(_major_version,_minor_version,_fix_version)
def bugs_dir(ui):
"""Returns the path to the bugs dir, relative to the repo root"""
return ui.config("bugs","dir",".bugs")
def status(ui,repo,revision='tip',ignore=[]):
"""Indicates the state of a revision relative to the bugs database. In essence, this
function is a wrapper for `hg stat --change x` which strips out changes to the bugs directory.
A revision either:
* Does not touch the bugs directory:
This generally indicates a feature change or other improvement, in any case, b cannot draw any
conclusions about the revision.
Returns None.
* Only touches the bugs directory:
This would indicate a new bug report, comment, reassignment, or other internal
b housekeeping. No external files were touched, no progress is being made in
the rest of repository.
Returns an empty list.
* Touches the bugs directory, and other areas of the repository:
This is assumed to indicate a bug fix, or progress is being made on a bug. Committing unrelated
changes to the repository and the bugs database in the same revision should be discouraged.
Returns a list of files outside the bugs directory in the given changeset.
You may pass a list of Mercurial patterns (see `hg help patterns`) relative to the repository
root to exclude from the returned list.
"""
bugsdir = bugs_dir(ui)
ui.pushbuffer()
commands.status(ui,repo,change=revision,no_status=True,print0=True)
files = ui.popbuffer().split('\0')
bug_change = False
ret = []
for file in files:
if file.strip():
if file.startswith(bugsdir):
bug_change = True
else:
ret.append(file)
ui.write(ret if bug_change else None)
ui.write('\n')