Rewrite commands to use bugdirs instead of a single bugdir.
[be.git] / libbe / bug.py
index 737d92d596fe88b661c92108c59ac398cd23f837..8b81842b706cbc626d1422e77d2cacb6783a3d6b 100644 (file)
@@ -1,21 +1,24 @@
-# Copyright (C) 2008-2011 Gianluca Montecchi <gian@grys.it>
+# Copyright (C) 2008-2012 Chris Ball <cjb@laptop.org>
+#                         Gianluca Montecchi <gian@grys.it>
+#                         Robert Lehmann <mail@robertlehmann.de>
 #                         Thomas Habets <thomas@habets.pp.se>
+#                         Valtteri Kokkoniemi <rvk@iki.fi>
 #                         W. Trevor King <wking@drexel.edu>
 #
 # This file is part of Bugs Everywhere.
 #
-# Bugs Everywhere is free software; you can redistribute it and/or modify it
-# under the terms of the GNU General Public License as published by the
-# Free Software Foundation, either version 2 of the License, or (at your
-# option) any later version.
+# Bugs Everywhere is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 2 of the License, or (at your option) any
+# later version.
 #
 # Bugs Everywhere is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-# General Public License for more details.
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
+# more details.
 #
-# You should have received a copy of the GNU General Public License
-# along with Bugs Everywhere.  If not, see <http://www.gnu.org/licenses/>.
+# You should have received a copy of the GNU General Public License along with
+# Bugs Everywhere.  If not, see <http://www.gnu.org/licenses/>.
 
 """Define the :class:`Bug` class for representing bugs.
 """
@@ -47,11 +50,6 @@ if libbe.TESTING == True:
     import doctest
 
 
-class DiskAccessRequired (Exception):
-    def __init__(self, goal):
-        msg = "Cannot %s without accessing the disk" % goal
-        Exception.__init__(self, msg)
-
 ### Define and describe valid bug categories
 # Use a tuple of (category, description) tuples since we don't have
 # ordered dicts in Python yet http://www.python.org/dev/peps/pep-0372/
@@ -199,10 +197,19 @@ class Bug (settings_object.SavedSettingsObject):
 
     def _get_time(self):
         if self.time_string == None:
+            self._cached_time_string = None
+            self._cached_time = None
             return None
-        return utility.str_to_time(self.time_string)
+        if (not hasattr(self, '_cached_time_string')
+            or self.time_string != self._cached_time_string):
+            self._cached_time_string = self.time_string
+            self._cached_time = utility.str_to_time(self.time_string)
+        return self._cached_time
     def _set_time(self, value):
-        self.time_string = utility.time_to_str(value)
+        if not hasattr(self, '_cached_time') or value != self._cached_time:
+            self.time_string = utility.time_to_str(value)
+            self._cached_time_string = self.time_string
+            self._cached_time = value
     time = property(fget=_get_time,
                     fset=_set_time,
                     doc="An integer version of .time_string")
@@ -290,6 +297,8 @@ class Bug (settings_object.SavedSettingsObject):
                     ("Reporter", self._setting_attr_string("reporter")),
                     ("Creator", self._setting_attr_string("creator")),
                     ("Created", timestring)]
+            for estr in self.extra_strings:
+                info.append(('Extra string', estr))
             longest_key_len = max([len(k) for k,v in info])
             infolines = ["  %*s : %s\n" %(longest_key_len,k,v) for k,v in info]
             bugout = "".join(infolines) + "%s" % self.summary.rstrip('\n')
@@ -308,6 +317,97 @@ class Bug (settings_object.SavedSettingsObject):
         return output
 
     def xml(self, indent=0, show_comments=False):
+        """
+        >>> bugA = Bug(uuid='0123', summary='Need to test Bug.xml()')
+        >>> bugA.uuid = 'bugA'
+        >>> bugA.time_string = 'Thu, 01 Jan 1970 00:00:00 +0000'
+        >>> bugA.creator = u'Frank'
+        >>> bugA.extra_strings += ['TAG: very helpful']
+        >>> commA = bugA.comment_root.new_reply(body='comment A')
+        >>> commA.uuid = 'commA'
+        >>> commA.date = 'Thu, 01 Jan 1970 00:01:00 +0000'
+        >>> commB = commA.new_reply(body='comment B')
+        >>> commB.uuid = 'commB'
+        >>> commB.date = 'Thu, 01 Jan 1970 00:02:00 +0000'
+        >>> commC = commB.new_reply(body='comment C')
+        >>> commC.uuid = 'commC'
+        >>> commC.date = 'Thu, 01 Jan 1970 00:03:00 +0000'
+        >>> print(bugA.xml(show_comments=True))  # doctest: +REPORT_UDIFF
+        <bug>
+          <uuid>bugA</uuid>
+          <short-name>/bug</short-name>
+          <severity>minor</severity>
+          <status>open</status>
+          <creator>Frank</creator>
+          <created>Thu, 01 Jan 1970 00:00:00 +0000</created>
+          <summary>Need to test Bug.xml()</summary>
+          <extra-string>TAG: very helpful</extra-string>
+          <comment>
+            <uuid>commA</uuid>
+            <short-name>/bug/commA</short-name>
+            <author></author>
+            <date>Thu, 01 Jan 1970 00:01:00 +0000</date>
+            <content-type>text/plain</content-type>
+            <body>comment A</body>
+          </comment>
+          <comment>
+            <uuid>commB</uuid>
+            <short-name>/bug/commB</short-name>
+            <in-reply-to>commA</in-reply-to>
+            <author></author>
+            <date>Thu, 01 Jan 1970 00:02:00 +0000</date>
+            <content-type>text/plain</content-type>
+            <body>comment B</body>
+          </comment>
+          <comment>
+            <uuid>commC</uuid>
+            <short-name>/bug/commC</short-name>
+            <in-reply-to>commB</in-reply-to>
+            <author></author>
+            <date>Thu, 01 Jan 1970 00:03:00 +0000</date>
+            <content-type>text/plain</content-type>
+            <body>comment C</body>
+          </comment>
+        </bug>
+        >>> print(bugA.xml(show_comments=True, indent=2))
+        ... # doctest: +REPORT_UDIFF
+          <bug>
+            <uuid>bugA</uuid>
+            <short-name>/bug</short-name>
+            <severity>minor</severity>
+            <status>open</status>
+            <creator>Frank</creator>
+            <created>Thu, 01 Jan 1970 00:00:00 +0000</created>
+            <summary>Need to test Bug.xml()</summary>
+            <extra-string>TAG: very helpful</extra-string>
+            <comment>
+              <uuid>commA</uuid>
+              <short-name>/bug/commA</short-name>
+              <author></author>
+              <date>Thu, 01 Jan 1970 00:01:00 +0000</date>
+              <content-type>text/plain</content-type>
+              <body>comment A</body>
+            </comment>
+            <comment>
+              <uuid>commB</uuid>
+              <short-name>/bug/commB</short-name>
+              <in-reply-to>commA</in-reply-to>
+              <author></author>
+              <date>Thu, 01 Jan 1970 00:02:00 +0000</date>
+              <content-type>text/plain</content-type>
+              <body>comment B</body>
+            </comment>
+            <comment>
+              <uuid>commC</uuid>
+              <short-name>/bug/commC</short-name>
+              <in-reply-to>commB</in-reply-to>
+              <author></author>
+              <date>Thu, 01 Jan 1970 00:03:00 +0000</date>
+              <content-type>text/plain</content-type>
+              <body>comment C</body>
+            </comment>
+          </bug>
+        """
         if self.time == None:
             timestring = ""
         else:
@@ -330,14 +430,15 @@ class Bug (settings_object.SavedSettingsObject):
             lines.append('  <extra-string>%s</extra-string>' % estr)
         if show_comments == True:
             comout = self.comment_root.xml_thread(indent=indent+2)
-            if len(comout) > 0:
+            if comout:
+                comout = comout[indent:]  # strip leading indent spaces
                 lines.append(comout)
         lines.append('</bug>')
         istring = ' '*indent
         sep = '\n' + istring
         return istring + sep.join(lines).rstrip('\n')
 
-    def from_xml(self, xml_string, verbose=True):
+    def from_xml(self, xml_string, preserve_uuids=False, verbose=True):
         u"""
         Note: If a bug uuid is given, set .alt_id to it's value.
         >>> bugA = Bug(uuid="0123", summary="Need to test Bug.from_xml()")
@@ -359,9 +460,13 @@ class Bug (settings_object.SavedSettingsObject):
         >>> bugB.xml(show_comments=True) == xml
         True
         >>> bugB.explicit_attrs  # doctest: +NORMALIZE_WHITESPACE
-        ['severity', 'status', 'creator', 'created', 'summary']
+        ['severity', 'status', 'creator', 'time', 'summary']
         >>> len(list(bugB.comments()))
         3
+        >>> bugC = Bug()
+        >>> bugC.from_xml(xml, preserve_uuids=True)
+        >>> bugC.uuid == bugA.uuid
+        True
         """
         if type(xml_string) == types.UnicodeType:
             xml_string = xml_string.strip().encode('unicode_escape')
@@ -371,7 +476,7 @@ class Bug (settings_object.SavedSettingsObject):
             bug = ElementTree.XML(xml_string)
         if bug.tag != 'bug':
             raise utility.InvalidXML( \
-                'bug', bug, 'root element must be <comment>')
+                'bug', bug, 'root element must be <bug>')
         tags=['uuid','short-name','severity','status','assigned',
               'reporter', 'creator','created','summary','extra-string']
         self.explicit_attrs = []
@@ -383,7 +488,8 @@ class Bug (settings_object.SavedSettingsObject):
                 pass
             elif child.tag == 'comment':
                 comm = comment.Comment(bug=self)
-                comm.from_xml(child)
+                comm.from_xml(
+                    child, preserve_uuids=preserve_uuids, verbose=verbose)
                 comments.append(comm)
                 continue
             elif child.tag in tags:
@@ -391,10 +497,17 @@ class Bug (settings_object.SavedSettingsObject):
                     text = settings_object.EMPTY
                 else:
                     text = xml.sax.saxutils.unescape(child.text)
-                    text = text.decode('unicode_escape').strip()
-                if child.tag == 'uuid':
+                    if not isinstance(text, unicode):
+                        text = text.decode('unicode_escape')
+                    text = text.strip()
+                if child.tag == 'uuid' and not preserve_uuids:
                     uuid = text
                     continue # don't set the bug's uuid tag.
+                elif child.tag == 'created':
+                    if text is not settings_object.EMPTY:
+                        self.time = utility.str_to_time(text)
+                        self.explicit_attrs.append('time')
+                    continue
                 elif child.tag == 'extra-string':
                     estrs.append(text)
                     continue # don't set the bug's extra_string yet.
@@ -587,20 +700,21 @@ class Bug (settings_object.SavedSettingsObject):
           </comment>
         </bug>
         """
-        for attr in other.explicit_attrs:
-            old = getattr(self, attr)
-            new = getattr(other, attr)
-            if old != new:
-                if accept_changes == True:
-                    setattr(self, attr, new)
-                elif change_exception == True:
-                    raise ValueError, \
-                        'Merge would change %s "%s"->"%s" for bug %s' \
-                        % (attr, old, new, self.uuid)
+        if hasattr(other, 'explicit_attrs'):
+            for attr in other.explicit_attrs:
+                old = getattr(self, attr)
+                new = getattr(other, attr)
+                if old != new:
+                    if accept_changes:
+                        setattr(self, attr, new)
+                    elif change_exception:
+                        raise ValueError(
+                            ('Merge would change {} "{}"->"{}" for bug {}'
+                             ).format(attr, old, new, self.uuid))
         for estr in other.extra_strings:
             if not estr in self.extra_strings:
                 if accept_extra_strings == True:
-                    self.extra_strings.append(estr)
+                    self.extra_strings += [estr]
                 elif change_exception == True:
                     raise ValueError, \
                         'Merge would add extra string "%s" for bug %s' \
@@ -632,8 +746,8 @@ class Bug (settings_object.SavedSettingsObject):
 
     def load_settings(self, settings_mapfile=None):
         if settings_mapfile == None:
-            settings_mapfile = \
-                self.storage.get(self.id.storage('values'), default='\n')
+            settings_mapfile = self.storage.get(
+                self.id.storage('values'), '\n')
         try:
             settings = mapfile.parse(settings_mapfile)
         except mapfile.InvalidMapfileContents, e:
@@ -799,6 +913,12 @@ cmp_extra_strings = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "extra_strings"
 # chronological rankings (newer < older)
 cmp_time = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "time", invert=True)
 
+def cmp_mine(bug_1, bug_2):
+    user_id = libbe.ui.util.user.get_user_id(bug_1.storage)
+    mine_1 = bug_1.assigned != user_id
+    mine_2 = bug_2.assigned != user_id
+    return cmp(mine_1, mine_2)
+
 def cmp_comments(bug_1, bug_2):
     """
     Compare two bugs' comments lists.  Doesn't load any new comments,