Merged libbe.diff upgrades and libbe.tree.Tree.has_descendant from be.wtk-rr.
[be.git] / becommands / comment.py
1 # Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc.
2 #                         Chris Ball <cjb@laptop.org>
3 #                         W. Trevor King <wking@drexel.edu>
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License along
16 # with this program; if not, write to the Free Software Foundation, Inc.,
17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 """Add a comment to a bug"""
19 from libbe import cmdutil, bugdir, comment, editor
20 import os
21 import sys
22 try: # import core module, Python >= 2.5
23     from xml.etree import ElementTree
24 except ImportError: # look for non-core module
25     from elementtree import ElementTree
26 __desc__ = __doc__
27
28 def execute(args, manipulate_encodings=True):
29     """
30     >>> import time
31     >>> bd = bugdir.simple_bug_dir()
32     >>> os.chdir(bd.root)
33     >>> execute(["a", "This is a comment about a"], manipulate_encodings=False)
34     >>> bd._clear_bugs()
35     >>> bug = bd.bug_from_shortname("a")
36     >>> bug.load_comments(load_full=False)
37     >>> comment = bug.comment_root[0]
38     >>> print comment.body
39     This is a comment about a
40     <BLANKLINE>
41     >>> comment.From == bd.user_id
42     True
43     >>> comment.time <= int(time.time())
44     True
45     >>> comment.in_reply_to is None
46     True
47
48     >>> if 'EDITOR' in os.environ:
49     ...     del os.environ["EDITOR"]
50     >>> execute(["b"], manipulate_encodings=False)
51     Traceback (most recent call last):
52     UserError: No comment supplied, and EDITOR not specified.
53
54     >>> os.environ["EDITOR"] = "echo 'I like cheese' > "
55     >>> execute(["b"], manipulate_encodings=False)
56     >>> bd._clear_bugs()
57     >>> bug = bd.bug_from_shortname("b")
58     >>> bug.load_comments(load_full=False)
59     >>> comment = bug.comment_root[0]
60     >>> print comment.body
61     I like cheese
62     <BLANKLINE>
63     """
64     parser = get_parser()
65     options, args = parser.parse_args(args)
66     complete(options, args, parser)
67     if len(args) == 0:
68         raise cmdutil.UsageError("Please specify a bug or comment id.")
69     if len(args) > 2:
70         raise cmdutil.UsageError("Too many arguments.")
71
72     shortname = args[0]
73     if shortname.count(':') > 1:
74         raise cmdutil.UserError("Invalid id '%s'." % shortname)
75     elif shortname.count(':') == 1:
76         # Split shortname generated by Comment.comment_shortnames()
77         bugname = shortname.split(':')[0]
78         is_reply = True
79     else:
80         bugname = shortname
81         is_reply = False
82
83     bd = bugdir.BugDir(from_disk=True,
84                        manipulate_encodings=manipulate_encodings)
85     bug = bd.bug_from_shortname(bugname)
86     bug.load_comments(load_full=False)
87     if is_reply:
88         parent = bug.comment_root.comment_from_shortname(shortname,
89                                                          bug_shortname=bugname)
90     else:
91         parent = bug.comment_root
92
93     if len(args) == 1: # try to launch an editor for comment-body entry
94         try:
95             if parent == bug.comment_root:
96                 parent_body = bug.summary+"\n"
97             else:
98                 parent_body = parent.body
99             estr = "Please enter your comment above\n\n> %s\n" \
100                 % ("\n> ".join(parent_body.splitlines()))
101             body = editor.editor_string(estr)
102         except editor.CantFindEditor, e:
103             raise cmdutil.UserError, "No comment supplied, and EDITOR not specified."
104         if body is None:
105             raise cmdutil.UserError("No comment entered.")
106         body = body.decode('utf-8')
107     elif args[1] == '-': # read body from stdin
108         binary = not (options.content_type == None
109                       or options.content_type.startswith("text/"))
110         if not binary:
111             body = sys.stdin.read()
112             if not body.endswith('\n'):
113                 body+='\n'
114         else: # read-in without decoding
115             body = sys.__stdin__.read()
116     else: # body = arg[1]
117         body = args[1]
118         if not body.endswith('\n'):
119             body+='\n'
120
121     if options.XML == False:
122         new = parent.new_reply(body=body)
123         if options.author != None:
124             new.From = options.author
125         if options.alt_id != None:
126             new.alt_id = options.alt_id
127         if options.content_type != None:
128             new.content_type = options.content_type
129     else: # import XML comment [list]
130         # read in the comments
131         str_body = body.encode("unicode_escape").replace(r'\n', '\n')
132         comment_list = ElementTree.XML(str_body)
133         if comment_list.tag not in ["bug", "comment-list"]:
134             raise comment.InvalidXML(
135                 comment_list, "root element must be <bug> or <comment-list>")
136         new_comments = []
137         ids = []
138         for c in bug.comment_root.traverse():
139             ids.append(c.uuid)
140             if c.alt_id != None:
141                 ids.append(c.alt_id)
142         for child in comment_list.getchildren():
143             if child.tag == "comment":
144                 new = comment.Comment(bug)
145                 new.from_xml(unicode(ElementTree.tostring(child)).decode("unicode_escape"))
146                 if new.alt_id in ids:
147                     raise cmdutil.UserError(
148                         "Clashing comment alt_id: %s" % new.alt_id)
149                 ids.append(new.uuid)
150                 if new.alt_id != None:
151                     ids.append(new.alt_id)
152                 if new.in_reply_to == None:
153                     new.in_reply_to = parent.uuid
154                 new_comments.append(new)
155             else:
156                 print >> sys.stderr, "Ignoring unknown tag %s in %s" \
157                     % (child.tag, comment_list.tag)
158         try:
159             comment.list_to_root(new_comments,bug,root=parent, # link new comments
160                                  ignore_missing_references=options.ignore_missing_references)
161         except comment.MissingReference, e:
162             raise cmdutil.UserError(e)
163         # Protect against programmer error causing data loss:
164         kids = [c.uuid for c in parent.traverse()]
165         for nc in new_comments:
166             assert nc.uuid in kids, "%s wasn't added to %s" % (nc.uuid, parent.uuid)
167             nc.save()
168
169 def get_parser():
170     parser = cmdutil.CmdOptionParser("be comment ID [COMMENT]")
171     parser.add_option("-a", "--author", metavar="AUTHOR", dest="author",
172                       help="Set the comment author", default=None)
173     parser.add_option("--alt-id", metavar="ID", dest="alt_id",
174                       help="Set an alternate comment ID", default=None)
175     parser.add_option("-c", "--content-type", metavar="MIME", dest="content_type",
176                       help="Set comment content-type (e.g. text/plain)", default=None)
177     parser.add_option("-x", "--xml", action="store_true", default=False,
178                       dest='XML', help="Use COMMENT to specify an XML comment description rather than the comment body.  The root XML element should be either <bug> or <comment-list> with one or more <comment> children.  The syntax for the <comment> elements should match that generated by 'be show --xml COMMENT-ID'.  Unrecognized tags are ignored.  Missing tags are left at the default value.  The comment UUIDs are always auto-generated, so if you set a <uuid> field, but no <alt-id> field, your <uuid> will be used as the comment's <alt-id>.  An exception is raised if <alt-id> conflicts with an existing comment.")
179     parser.add_option("-i", "--ignore-missing-references", action="store_true",
180                       dest="ignore_missing_references",
181                       help="For XML import, if any comment's <in-reply-to> refers to a non-existent comment, ignore it (instead of raising an exception).")
182     return parser
183
184 longhelp="""
185 To add a comment to a bug, use the bug ID as the argument.  To reply
186 to another comment, specify the comment name (as shown in "be show"
187 output).  COMMENT, if specified, should be either the text of your
188 comment or "-", in which case the text will be read from stdin.  If
189 you do not specify a COMMENT, $EDITOR is used to launch an editor.  If
190 COMMENT is unspecified and EDITOR is not set, no comment will be
191 created.
192 """
193
194 def help():
195     return get_parser().help_str() + longhelp
196
197 def complete(options, args, parser):
198     for option,value in cmdutil.option_value_pairs(options, parser):
199         if value == "--complete":
200             # no argument-options at the moment, so this is future-proofing
201             raise cmdutil.GetCompletions()
202     for pos,value in enumerate(args):
203         if value == "--complete":
204             if pos == 0: # fist positional argument is a bug or comment id
205                 if len(args) >= 2:
206                     partial = args[1].split(':')[0] # take only bugid portion
207                 else:
208                     partial = ""
209                 ids = []
210                 try:
211                     bd = bugdir.BugDir(from_disk=True,
212                                        manipulate_encodings=False)
213                     bugs = []
214                     for uuid in bd.list_uuids():
215                         if uuid.startswith(partial):
216                             bug = bd.bug_from_uuid(uuid)
217                             if bug.active == True:
218                                 bugs.append(bug)
219                     for bug in bugs:
220                         shortname = bd.bug_shortname(bug)
221                         ids.append(shortname)
222                         bug.load_comments(load_full=False)
223                         for id,comment in bug.comment_shortnames(shortname):
224                             ids.append(id)
225                 except bugdir.NoBugDir:
226                     pass
227                 raise cmdutil.GetCompletions(ids)
228             raise cmdutil.GetCompletions()