079601e04ebaa42bbf09144dc86e9258fd6ce050
[be.git] / libbe / cmdutil.py
1 # Copyright (C) 2005 Aaron Bentley and Panometrics, Inc.
2 # <abentley@panoramicfeedback.com>
3 #
4 #    This program is free software; you can redistribute it and/or modify
5 #    it under the terms of the GNU General Public License as published by
6 #    the Free Software Foundation; either version 2 of the License, or
7 #    (at your option) any later version.
8 #
9 #    This program is distributed in the hope that it will be useful,
10 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
11 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 #    GNU General Public License for more details.
13 #
14 #    You should have received a copy of the GNU General Public License
15 #    along with this program; if not, write to the Free Software
16 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17 import bugdir
18 import plugin
19 import locale
20 import os
21 import optparse
22 from textwrap import TextWrapper
23 from StringIO import StringIO
24 import utility
25
26 def unique_name(bug, bugs):
27     chars = 1
28     for some_bug in bugs:
29         if bug.uuid == some_bug.uuid:
30             continue
31         while (bug.uuid[:chars] == some_bug.uuid[:chars]):
32             chars+=1
33         if chars < 3:
34             chars = 3
35     return bug.uuid[:chars]
36
37 class UserError(Exception):
38     def __init__(self, msg):
39         Exception.__init__(self, msg)
40
41 class UserErrorWrap(UserError):
42     def __init__(self, exception):
43         UserError.__init__(self, str(exception))
44         self.exception = exception
45
46 def get_bug(spec, bug_dir=None):
47     matches = []
48     try:
49         if bug_dir is None:
50             bug_dir = bugdir.tree_root('.')
51     except bugdir.NoBugDir, e:
52         raise UserErrorWrap(e)
53     bugs = list(bug_dir.list())
54     for bug in bugs:
55         if bug.uuid.startswith(spec):
56             matches.append(bug)
57     if len(matches) > 1:
58         raise UserError("More than one bug matches %s.  Please be more"
59                         " specific." % spec)
60     if len(matches) == 1:
61         return matches[0]
62         
63     matches = []
64     if len(matches) == 0:
65         raise UserError("No bug matches %s" % spec)
66     return matches[0]
67
68 def bug_summary(bug, bugs, no_target=False, shortlist=False):
69     target = bug.target
70     if target is None or no_target:
71         target = ""
72     else:
73         target = "  Target: %s" % target
74     if bug.assigned is None:
75         assigned = ""
76     else:
77         assigned = "  Assigned: %s" % bug.assigned
78     if shortlist == False:
79        return "  ID: %s\n  Severity: %s\n%s%s\n  Creator: %s \n%s\n" % \
80             (unique_name(bug, bugs), bug.severity, assigned, target,
81              bug.creator, bug.summary)
82     else:
83        return "%4s: %s\n" % (unique_name(bug, bugs), bug.summary)
84
85 def iter_commands():
86     for name, module in plugin.iter_plugins("becommands"):
87         yield name.replace("_", "-"), module
88
89 def get_command(command_name):
90     """Retrieves the module for a user command
91
92     >>> get_command("asdf")
93     Traceback (most recent call last):
94     UserError: Unknown command asdf
95     >>> repr(get_command("list")).startswith("<module 'becommands.list' from ")
96     True
97     """
98     cmd = plugin.get_plugin("becommands", command_name.replace("-", "_"))
99     if cmd is None:
100         raise UserError("Unknown command %s" % command_name)
101     return cmd
102
103 def execute(cmd, args):
104     encoding = locale.getpreferredencoding() or 'ascii'
105     return get_command(cmd).execute([a.decode(encoding) for a in args])
106
107 def help(cmd):
108     return get_command(cmd).help()
109
110
111 class GetHelp(Exception):
112     pass
113
114
115 class UsageError(Exception):
116     pass
117
118
119 def raise_get_help(option, opt, value, parser):
120     raise GetHelp
121
122
123 def iter_comment_name(bug, unique_name):
124     """Iterate through id, comment pairs, in date order.
125     (This is a user-friendly id, not the comment uuid)
126     """
127     def key(comment):
128         return comment.date
129     for num, comment in enumerate(sorted(bug.list_comments(), key=key)):
130         yield ("%s:%d" % (unique_name, num+1), comment)
131
132
133 def comment_from_name(bug, unique_name, name):
134     """Use a comment name to look up a comment"""
135     for cur_name, comment in iter_comment_name(bug, unique_name):
136         if name == cur_name:
137             return comment
138     raise KeyError(name)
139
140
141 def get_bug_and_comment(identifier, bug_dir=None):
142     ids = identifier.split(':')
143     bug = get_bug(ids[0], bug_dir)
144     if len(ids) == 2:
145         comment = comment_from_name(bug, ids[0], identifier)
146     else:
147         comment = None
148     return bug, comment
149
150         
151 class CmdOptionParser(optparse.OptionParser):
152     def __init__(self, usage):
153         optparse.OptionParser.__init__(self, usage)
154         self.remove_option("-h")
155         self.add_option("-h", "--help", action="callback", 
156                         callback=raise_get_help, help="Print a help message")
157
158     def error(self, message):
159         raise UsageError(message)
160
161     def iter_options(self):
162         return iter_combine([self._short_opt.iterkeys(), 
163                             self._long_opt.iterkeys()])
164
165     def help_str(self):
166         fs = utility.FileString()
167         self.print_help(fs)
168         return fs.str
169
170
171 def underlined(instring):
172     """Produces a version of a string that is underlined with '='
173
174     >>> underlined("Underlined String")
175     'Underlined String\\n================='
176     """
177     
178     return "%s\n%s" % (instring, "="*len(instring))
179
180
181 def print_threaded_comments(comments, name_map, indent=""):
182     """Print a threaded display of comments"""
183     tw = TextWrapper(initial_indent = indent, subsequent_indent = indent, 
184                      width=80)
185     for comment, children in comments:
186         s = StringIO()
187         print >> s, "--------- Comment ---------"
188         print >> s, "Name: %s" % name_map[comment.uuid]
189         print >> s, "From: %s" % comment.From
190         print >> s, "Date: %s\n" % utility.time_to_str(comment.date)
191         print >> s, comment.body.rstrip('\n')
192
193         s.seek(0)
194         for line in s:
195             print tw.fill(line).rstrip('\n')
196         print_threaded_comments(children, name_map, indent=indent+"    ")
197
198
199 def bug_tree(dir=None):
200     """Retrieve the bug tree specified by the user.  If no directory is
201     specified, the current working directory is used.
202
203     :param dir: The directory to search for the bug tree in.
204
205     >>> bug_tree() is not None
206     True
207     >>> bug_tree("/")
208     Traceback (most recent call last):
209     UserErrorWrap: The directory "/" has no bug directory.
210     """
211     if dir is None:
212         dir = os.getcwd()
213     try:
214         return bugdir.tree_root(dir)
215     except bugdir.NoBugDir, e:
216         raise UserErrorWrap(e)
217
218 def print_command_list():
219     cmdlist = []
220     print """Bugs Everywhere - Distributed bug tracking
221     
222 Supported commands"""
223     for name, module in iter_commands():
224         cmdlist.append((name, module.__doc__))
225     for name, desc in cmdlist:
226         print "be %s\n    %s" % (name, desc)
227
228 def _test():
229     import doctest
230     import sys
231     doctest.testmod()
232
233 if __name__ == "__main__":
234     _test()