-
Notifications
You must be signed in to change notification settings - Fork 58
/
Copy pathb.py
895 lines (753 loc) · 33.7 KB
/
b.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
# b.py - Distributed Bug Tracker Extention for Mercurial
#
# Copyright 2010-2011 Michael Diamond <[email protected]>
#
# 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
from datetime import date, datetime
from operator import itemgetter
from mercurial.i18n import _
from mercurial import hg,commands
#
# Version
#
_major_version = 0
_minor_version = 6
_fix_version = 2
_build_date = date(2012,3,4)
#
# 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:
bugsdir = bugs_dir(ui)
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():
ui.write(_("b Version %d.%d.%d - built %s\n") % (_major_version,_minor_version,_fix_version,_build_date))
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]"))}
#
# 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')