[yum] - latest head - Speedup install/remove/etc a lot. - Add merged history. - Fix unique comps/pkgtags l

James Antill james at fedoraproject.org
Sat Sep 25 15:13:04 UTC 2010


commit b88e11eecaba491a9b1f9f88fe166df20dd5f8ad
Author: James Antill <james at and.org>
Date:   Sat Sep 25 11:12:52 2010 -0400

    - latest head
    - Speedup install/remove/etc a lot.
    - Add merged history.
    - Fix unique comps/pkgtags leftovers.

 yum-HEAD.patch | 2037 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--
 yum.spec       |   11 +-
 2 files changed, 1983 insertions(+), 65 deletions(-)
---
diff --git a/yum-HEAD.patch b/yum-HEAD.patch
index 29d2fd5..03c23e0 100644
--- a/yum-HEAD.patch
+++ b/yum-HEAD.patch
@@ -115,7 +115,7 @@ index 9542b7a..0accd35 100644
              if arg not in akeys:
                  self.logger.warning(_('Warning: No matches found for: %s'), arg)
 diff --git a/docs/yum.8 b/docs/yum.8
-index 281bf17..5586b24 100644
+index 281bf17..b5b7295 100644
 --- a/docs/yum.8
 +++ b/docs/yum.8
 @@ -155,9 +155,14 @@ or file. Just use a specific name or a file-glob-syntax wildcards to list
@@ -136,7 +136,21 @@ index 281bf17..5586b24 100644
  .IP 
  .IP "\fBinfo\fP"
  Is used to list a description and summary information about available
-@@ -549,11 +554,6 @@ Eliminate the sqlite cache used for faster access to metadata.
+@@ -334,6 +339,13 @@ if there was something not good with the transaction.
+ Checks the local rpmdb and produces information on any problems it finds. You
+ can pass the check command the arguments "dependencies" or "duplicates", to
+ limit the checking that is performed (the default is "all" which does both).
++
++The info command can also take ranges of transaction ids, of the form
++start..end, which will then display a merged history as if all the
++transactions in the range had happened at once\&.
++.br
++Eg. "history info 1..4" will merge the first four transactions and display them
++as a single transaction.
+ .IP
+ .IP "\fBhelp\fP"
+ Produces help, either for all commands or if given a command name then the help
+@@ -549,11 +561,6 @@ Eliminate the sqlite cache used for faster access to metadata.
  Using this option will force yum to download the sqlite metadata the next time
  it is run, or recreate the sqlite metadata if using an older repo.
  
@@ -149,7 +163,7 @@ index 281bf17..5586b24 100644
  Eliminate any cached data from the local rpmdb.
  
 diff --git a/docs/yum.conf.5 b/docs/yum.conf.5
-index 49d98c6..8acc8f4 100644
+index 49d98c6..20a6e67 100644
 --- a/docs/yum.conf.5
 +++ b/docs/yum.conf.5
 @@ -134,9 +134,13 @@ an i686 package to update an i386 package. Default is `1'.
@@ -169,7 +183,17 @@ index 49d98c6..8acc8f4 100644
  
  .IP
  \fBinstallonly_limit \fR
-@@ -361,6 +365,14 @@ username to use for proxy
+@@ -217,8 +221,7 @@ diskspace before a RPM transaction is run. Default is `1' (perform the check).
+ .IP
+ \fBtsflags\fR
+ Comma or space separated list of transaction flags to pass to the rpm
+-transaction set. These include 'noscripts', 'notriggers', 'nodocs', 'test',
+-'justdb' and 'nocontexts'. 'repackage' is also available but that does nothing
++transaction set. These include 'noscripts', 'notriggers', 'nodocs', 'test', 'justdb' and 'nocontexts'. 'repackage' is also available but that does nothing
+ with newer rpm versions.
+ You can set all/any of them. However, if you don't know what these do in the
+ context of an rpm transaction set you're best leaving it alone. Default is
+@@ -361,6 +364,14 @@ username to use for proxy
  password for this proxy
  
  .IP
@@ -184,7 +208,7 @@ index 49d98c6..8acc8f4 100644
  \fBplugins \fR
  Either `0' or `1'. Global switch to enable or disable yum plugins. Default is
  `0' (plugins disabled). See the \fBPLUGINS\fR section of the \fByum(8)\fR man
-@@ -734,6 +746,17 @@ If this is unset it inherits it from the global setting
+@@ -734,6 +745,17 @@ If this is unset it inherits it from the global setting
  password for this proxy.
  If this is unset it inherits it from the global setting
  
@@ -253,10 +277,29 @@ index 3e6e243..8ca00fd 100644
              ;;
  
 diff --git a/output.py b/output.py
-index 3a90995..ec4bd45 100755
+index 3a90995..cfca45f 100755
 --- a/output.py
 +++ b/output.py
-@@ -1378,7 +1378,7 @@ to exit.
+@@ -43,6 +43,8 @@ from yum.rpmtrans import RPMBaseCallback
+ from yum.packageSack import packagesNewestByNameArch
+ import yum.packages
+ 
++import yum.history
++
+ from yum.i18n import utf8_width, utf8_width_fill, utf8_text_fill
+ 
+ def _term_width():
+@@ -1317,6 +1319,9 @@ to exit.
+         return count, "".join(list(actions))
+ 
+     def _pwd_ui_username(self, uid, limit=None):
++        if type(uid) == type([]):
++            return [self._pwd_ui_username(u, limit) for u in uid]
++
+         # loginuid is set to      -1 (0xFFFF_FFFF) on init, in newer kernels.
+         # loginuid is set to INT_MAX (0x7FFF_FFFF) on init, in older kernels.
+         if uid is None or uid in (0xFFFFFFFF, 0x7FFFFFFF):
+@@ -1378,7 +1383,7 @@ to exit.
  
          fmt = "%s | %s | %s | %s | %s"
          print fmt % (utf8_width_fill(_("ID"), 6, 6),
@@ -265,7 +308,7 @@ index 3a90995..ec4bd45 100755
                       utf8_width_fill(_("Date and time"), 16, 16),
                       utf8_width_fill(_("Action(s)"), 14, 14),
                       utf8_width_fill(_("Altered"), 7, 7))
-@@ -1393,11 +1393,11 @@ to exit.
+@@ -1393,11 +1398,11 @@ to exit.
                  break
  
              done += 1
@@ -279,7 +322,203 @@ index 3a90995..ec4bd45 100755
              uiacts = utf8_width_fill(uiacts, 14, 14)
              rmark = lmark = ' '
              if old.return_code is None:
-@@ -1629,23 +1629,24 @@ to exit.
+@@ -1471,21 +1476,60 @@ to exit.
+         return old[0]
+ 
+     def historyInfoCmd(self, extcmds):
++        def str2int(x):
++            try:
++                return int(x)
++            except ValueError:
++                return None
++
+         tids = set()
++        mtids = set()
+         pats = []
++        old = self.history.last()
++        if old is None:
++            self.logger.critical(_('No transactions'))
++            return 1, ['Failed history info']
++
+         for tid in extcmds[1:]:
+-            try:
+-                int(tid)
+-                tids.add(tid)
+-            except ValueError:
+-                pats.append(tid)
++            if '..' in tid:
++                btid, etid = tid.split('..', 2)
++                btid = str2int(btid)
++                if btid > old.tid:
++                    btid = None
++                elif btid <= 0:
++                    btid = None
++                etid = str2int(etid)
++                if etid > old.tid:
++                    etid = None
++                if btid is not None and etid is not None:
++                    # Have a range ... do a "merged" transaction.
++                    if btid > etid:
++                        btid, etid = etid, btid
++                    mtids.add((btid, etid))
++                    continue
++            elif str2int(tid) is not None:
++                tids.add(str2int(tid))
++                continue
++            pats.append(tid)
+         if pats:
+             tids.update(self.history.search(pats))
++        utids = tids.copy()
++        if mtids:
++            mtids = sorted(mtids)
++            last_end = -1 # This just makes displaying it easier...
++            for mtid in mtids:
++                if mtid[0] < last_end:
++                    self.logger.warn(_('Skipping merged transaction %d to %d, as it overlaps', mtid[0], mtid[1]))
++                    continue # Don't do overlapping
++                last_end = mtid[1]
++                for num in range(mtid[0], mtid[1] + 1):
++                    tids.add(num)
+ 
+         if not tids and len(extcmds) < 2:
+             old = self.history.last(complete_transactions_only=False)
+             if old is not None:
+                 tids.add(old.tid)
++                utids.add(old.tid)
+ 
+         if not tids:
+             self.logger.critical(_('No transaction ID, or package, given'))
+@@ -1497,6 +1541,10 @@ to exit.
+             lastdbv = lastdbv.end_rpmdbversion
+ 
+         done = False
++        bmtid, emtid = -1, -1
++        mobj = None
++        if mtids:
++            bmtid, emtid = mtids.pop(0)
+         for tid in self.history.old(tids):
+             if lastdbv is not None and tid.tid == lasttid:
+                 #  If this is the last transaction, is good and it doesn't
+@@ -1506,10 +1554,35 @@ to exit.
+                     tid.altered_gt_rpmdb = True
+             lastdbv = None
+ 
++            if tid.tid >= bmtid and tid.tid <= emtid:
++                if mobj is None:
++                    mobj = yum.history.YumMergedHistoryTransaction(tid)
++                else:
++                    mobj.merge(tid)
++            elif mobj is not None:
++                if done:
++                    print "-" * 79
++                done = True
++
++                self._historyInfoCmd(mobj)
++                mobj = None
++                if mtids:
++                    bmtid, emtid = mtids.pop(0)
++                    if tid.tid >= bmtid and tid.tid <= emtid:
++                        mobj = yum.history.YumMergedHistoryTransaction(tid)
++
++            if tid.tid in utids:
++                if done:
++                    print "-" * 79
++                done = True
++
++                self._historyInfoCmd(tid, pats)
++
++        if mobj is not None:
+             if done:
+                 print "-" * 79
+-            done = True
+-            self._historyInfoCmd(tid, pats)
++
++            self._historyInfoCmd(mobj)
+ 
+     def _historyInfoCmd(self, old, pats=[]):
+         name = self._pwd_ui_username(old.loginuid)
+@@ -1549,7 +1622,10 @@ to exit.
+             state = utf8_width_fill(state, _pkg_states['maxlen'])
+             print "%s%s%s%s %s" % (prefix, hibeg, state, hiend, hpkg)
+ 
+-        print _("Transaction ID :"), old.tid
++        if type(old.tid) == type([]):
++            print _("Transaction ID :"), "%u..%u" % (old.tid[0], old.tid[-1])
++        else:
++            print _("Transaction ID :"), old.tid
+         begtm = time.ctime(old.beg_timestamp)
+         print _("Begin time     :"), begtm
+         if old.beg_rpmdbversion is not None:
+@@ -1570,15 +1646,34 @@ to exit.
+                         break
+                     sofar += len(begtms[i]) + 1
+                 endtm = (' ' * sofar) + endtm[sofar:]
+-            diff = _("(%s seconds)") % (old.end_timestamp - old.beg_timestamp)
++            diff = old.end_timestamp - old.beg_timestamp
++            if diff < 5 * 60:
++                diff = _("(%u seconds)") % diff
++            elif diff < 5 * 60 * 60:
++                diff = _("(%u minutes)") % (diff / 60)
++            elif diff < 5 * 60 * 60 * 24:
++                diff = _("(%u hours)") % (diff / (60 * 60))
++            else:
++                diff = _("(%u days)") % (diff / (60 * 60 * 24))
+             print _("End time       :"), endtm, diff
+         if old.end_rpmdbversion is not None:
+             if old.altered_gt_rpmdb:
+                 print _("End rpmdb      :"), old.end_rpmdbversion, "**"
+             else:
+                 print _("End rpmdb      :"), old.end_rpmdbversion
+-        print _("User           :"), name
+-        if old.return_code is None:
++        if type(name) == type([]):
++            for name in name:
++                print _("User           :"), name
++        else:
++            print _("User           :"), name
++        if type(old.return_code) == type([]):
++            codes = old.return_code
++            if codes[0] is None:
++                print _("Return-Code    :"), "**", _("Aborted"), "**"
++                codes = codes[1:]
++            if codes:
++                print _("Return-Code    :"), _("Failures:"), ", ".join(codes)
++        elif old.return_code is None:
+             print _("Return-Code    :"), "**", _("Aborted"), "**"
+         elif old.return_code:
+             print _("Return-Code    :"), _("Failure:"), old.return_code
+@@ -1586,16 +1681,21 @@ to exit.
+             print _("Return-Code    :"), _("Success")
+             
+         if old.cmdline is not None:
+-            print _("Command Line   :"), old.cmdline
++            if type(old.cmdline) == type([]):
++                for cmdline in old.cmdline:
++                    print _("Command Line   :"), cmdline
++            else:
++                print _("Command Line   :"), old.cmdline
+ 
+-        addon_info = self.history.return_addon_data(old.tid)
+-        
+-        # for the ones we create by default - don't display them as there
+-        default_addons = set(['config-main', 'config-repos'])
+-        non_default = set(addon_info).difference(default_addons)
+-        if len(non_default) > 0:
+-                print _("Additional non-default information stored: %d" 
+-                            % len(non_default))
++        if type(old.tid) != type([]):
++            addon_info = self.history.return_addon_data(old.tid)
++
++            # for the ones we create by default - don't display them as there
++            default_addons = set(['config-main', 'config-repos'])
++            non_default = set(addon_info).difference(default_addons)
++            if len(non_default) > 0:
++                    print _("Additional non-default information stored: %d" 
++                                % len(non_default))
+ 
+         print _("Transaction performed with:")
+         for hpkg in old.trans_with:
+@@ -1629,23 +1729,24 @@ to exit.
                  num += 1
                  print "%4d" % num, line
  
@@ -316,7 +555,7 @@ index 3a90995..ec4bd45 100755
          maxlen = 0
          for hpkg in old.trans_data:
              uistate = all_uistates.get(hpkg.state, hpkg.state)
-@@ -1754,13 +1755,14 @@ to exit.
+@@ -1754,13 +1855,14 @@ to exit.
          tid = None
          if len(extcmds) > 1:
              tid = extcmds[1]
@@ -338,7 +577,7 @@ index 3a90995..ec4bd45 100755
  
          if tid is not None:
              old = self.history.old(tids=[tid])
-@@ -1794,6 +1796,85 @@ to exit.
+@@ -1794,6 +1896,85 @@ to exit.
  
              print ''
  
@@ -424,7 +663,7 @@ index 3a90995..ec4bd45 100755
  
  
  class DepSolveProgressCallBack:
-@@ -1882,10 +1963,12 @@ class DepSolveProgressCallBack:
+@@ -1882,10 +2063,12 @@ class DepSolveProgressCallBack:
                  msg += _('\n        Not found')
              return msg
  
@@ -440,7 +679,7 @@ index 3a90995..ec4bd45 100755
              action = _('Installed')
              rmed = yb.tsInfo.getMembersWithState(pkg.pkgtup, TS_REMOVE_STATES)
              if rmed:
-@@ -1901,21 +1984,40 @@ class DepSolveProgressCallBack:
+@@ -1901,21 +2084,40 @@ class DepSolveProgressCallBack:
                      if rtype not in relmap:
                          continue
                      nevr = (rpkg.name, rpkg.epoch, rpkg.version, rpkg.release)
@@ -28617,11 +28856,47 @@ index 524db82..889353e 100644
  up.doUpdates()
  up.condenseUpdates()
  
+diff --git a/rpmUtils/updates.py b/rpmUtils/updates.py
+index c61788b..3c4bbb4 100644
+--- a/rpmUtils/updates.py
++++ b/rpmUtils/updates.py
+@@ -56,7 +56,8 @@ class Updates:
+ 
+         # make some dicts from installed and available
+         self.installdict = self.makeNADict(self.installed, 1)
+-        self.availdict = self.makeNADict(self.available, 0) # Done in doUpdate
++        self.availdict = self.makeNADict(self.available, 0, # Done in doUpdate
++                                         filter=self.installdict)
+ 
+         # holder for our updates dict
+         self.updatesdict = {}
+@@ -104,13 +105,15 @@ class Updates:
+         if self.debug:
+             print msg
+ 
+-    def makeNADict(self, pkglist, Nonelists):
++    def makeNADict(self, pkglist, Nonelists, filter=None):
+         """return lists of (e,v,r) tuples as value of a dict keyed on (n, a)
+             optionally will return a (n, None) entry with all the a for that
+             n in tuples of (a,e,v,r)"""
+             
+         returndict = {}
+         for (n, a, e, v, r) in pkglist:
++            if filter and (n, None) not in filter:
++                continue
+             if (n, a) not in returndict:
+                 returndict[(n, a)] = []
+             if (e,v,r) in returndict[(n, a)]:
 diff --git a/shell.py b/shell.py
-index f1c82a3..a32e880 100644
+index f1c82a3..7eef413 100644
 --- a/shell.py
 +++ b/shell.py
-@@ -50,6 +50,14 @@ class YumShell(cmd.Cmd):
+@@ -1,3 +1,4 @@
++#! /usr/bin/python -tt
+ # This program 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
+@@ -50,6 +51,14 @@ class YumShell(cmd.Cmd):
          self.logger = logging.getLogger("yum.cli")
          self.verbose_logger = logging.getLogger("yum.verbose.cli")
  
@@ -28636,7 +28911,7 @@ index f1c82a3..a32e880 100644
  
      def _shlex_split(self, input_string):
          """split the input using shlex rules, and error or exit accordingly"""
-@@ -98,6 +106,8 @@ class YumShell(cmd.Cmd):
+@@ -98,6 +107,8 @@ class YumShell(cmd.Cmd):
              self.base.cmds = self._shlex_split(self.base.cmdstring)
              self.base.plugins.run('args', args=self.base.cmds)
  
@@ -28645,7 +28920,7 @@ index f1c82a3..a32e880 100644
              try:
                  self.base.parseCommands()
              except Errors.YumBaseError:
-@@ -266,6 +276,8 @@ class YumShell(cmd.Cmd):
+@@ -266,6 +277,8 @@ class YumShell(cmd.Cmd):
              cmds.insert(0, 'repolist')
              self.base.cmds = cmds
  
@@ -28654,6 +28929,804 @@ index f1c82a3..a32e880 100644
              try:
                  self.base.parseCommands()
              except Errors.YumBaseError:
+diff --git a/test/merge-history-transactions-tests.py b/test/merge-history-transactions-tests.py
+new file mode 100644
+index 0000000..569ba8d
+--- /dev/null
++++ b/test/merge-history-transactions-tests.py
+@@ -0,0 +1,792 @@
++import unittest
++
++import yum.history as hist
++
++_fake_count = 0
++class FakeYumHistoryTransaction(hist.YumHistoryTransaction):
++    def __init__(self, pkgs, tid=None, beg_timestamp=None, end_timestamp=None,
++                 beg_rpmdbversion=None, end_rpmdbversion=None,
++                 loginuid=0, return_code=0, pkgs_with=[],
++                 errors=[], output=[]):
++        global _fake_count
++
++        if tid is None:
++            _fake_count += 1
++            tid = _fake_count
++        if beg_timestamp is None:
++            _fake_count += 1
++            beg_timestamp = _fake_count
++        if end_timestamp is None:
++            _fake_count += 1
++            end_timestamp = _fake_count
++
++        if beg_rpmdbversion is None:
++            _fake_count += 1
++            beg_rpmdbversion = '?:<n/a>,' + str(_fake_count)
++        if end_rpmdbversion is None:
++            _fake_count += 1
++            end_rpmdbversion = '?:<n/a>,' + str(_fake_count)
++
++        self.tid              = tid
++        self.beg_timestamp    = beg_timestamp
++        self.beg_rpmdbversion = beg_rpmdbversion
++        self.end_timestamp    = end_timestamp
++        self.end_rpmdbversion = end_rpmdbversion
++        self.loginuid         = loginuid
++        self.return_code      = return_code
++
++        self._loaded_TW = pkgs_with
++        self._loaded_TD = pkgs
++
++        self._loaded_ER = errors
++        self._loaded_OT = output
++
++        self.altered_lt_rpmdb = None
++        self.altered_gt_rpmdb = None
++
++def _dump_trans_data(pkgs):
++    """ For debugging to see WTF is going on with .trans_data. """
++    return [(str(pkg), pkg.state) for pkg in pkgs]
++
++class MergeHistTransTests(unittest.TestCase):
++
++    def __init__(self, methodName='runTest'):
++        unittest.TestCase.__init__(self, methodName)
++
++    def setUp(self):
++        pass
++    def tearDown(self):
++        pass
++
++    def _merge_new(self, trans):
++        merged = hist.YumMergedHistoryTransaction(trans[0])
++        for pkg in trans[1:]:
++            merged.merge(pkg)
++        return merged
++
++    def _trans_new(self, *args, **kwargs):
++        return FakeYumHistoryTransaction(*args, **kwargs)
++
++    def _pkg_new(self, name, version='1', release='2',
++                 arch='noarch', epoch='0', checksum=None, state='Install'):
++        self.assertTrue(state in hist._sttxt2stcode)
++        pkg = hist.YumHistoryPackageState(name,arch,epoch,version,release,
++                                          state, checksum)
++        return pkg
++
++    def assertMergedBeg(self, merged, beg):
++        self.assertTrue(beg.tid in merged.tid)
++        self.assertEquals(beg.beg_timestamp, merged.beg_timestamp)
++        self.assertEquals(beg.beg_rpmdbversion, merged.beg_rpmdbversion)
++    def assertMergedEnd(self, merged, end):
++        self.assertTrue(end.tid in merged.tid)
++        self.assertEquals(end.end_timestamp, merged.end_timestamp)
++        self.assertEquals(end.end_rpmdbversion, merged.end_rpmdbversion)
++    def assertMergedCodes(self, merged, trans):
++        ret = set()
++        uid = set()
++        for trans in trans:
++            ret.add(trans.loginuid)
++            uid.add(trans.return_code)
++        if len(ret) == 1:
++            self.assertEquals(list(ret)[0], merged.return_code)
++        else:
++            for ret in ret:
++                self.assertTrue(ret in merged.return_code)
++        if len(uid) == 1:
++            self.assertEquals(list(uid)[0], merged.loginuid)
++        else:
++            for uid in uid:
++                self.assertTrue(uid in merged.loginuid)
++
++    def assertMergedMain(self, merged, trans):
++        self.assertMergedBeg(merged, trans[0])
++        self.assertMergedEnd(merged, trans[-1])
++        self.assertMergedCodes(merged, trans)
++
++    def testSimpleInMerge1(self, xstate='Install'):
++        pkg1 = self._pkg_new('foo', state=xstate)
++        pkg2 = self._pkg_new('xbar', version='4')
++        trans = []
++        trans.append(self._trans_new([pkg1]))
++        trans.append(self._trans_new([pkg2]))
++        merged = self._merge_new(trans)
++        self.assertMergedMain(merged, trans)
++        pkgs = merged.trans_data
++        self.assertEquals(len(pkgs), 2)
++        self.assertEquals(pkgs[0], pkg1)
++        self.assertEquals(pkgs[0].state, xstate)
++        self.assertEquals(pkgs[1], pkg2)
++        self.assertEquals(pkgs[1].state, pkg2.state)
++
++    def testSimpleInMerge2(self, xstate='Install'):
++        pkg1 = self._pkg_new('foo', state=xstate)
++        pkg2 = self._pkg_new('bar',  version='4')
++        pkg3 = self._pkg_new('xbar', version='6')
++        pkg4 = self._pkg_new('xfoo', version='3')
++        trans = []
++        trans.append(self._trans_new([pkg1, pkg3]))
++        trans.append(self._trans_new([pkg2, pkg4]))
++        merged = self._merge_new(trans)
++        self.assertMergedMain(merged, trans)
++        pkgs = merged.trans_data
++        self.assertEquals(len(pkgs), 4)
++        self.assertEquals(pkgs[0], pkg2)
++        self.assertEquals(pkgs[0].state, pkg2.state)
++        self.assertEquals(pkgs[1], pkg1)
++        self.assertEquals(pkgs[1].state, xstate)
++        self.assertEquals(pkgs[2], pkg3)
++        self.assertEquals(pkgs[2].state, pkg3.state)
++        self.assertEquals(pkgs[3], pkg4)
++        self.assertEquals(pkgs[3].state, pkg4.state)
++
++    def testSimpleUpMerge1(self, xstate='Update'):
++        opkg1 = self._pkg_new('foo',              state='Updated')
++        npkg1 = self._pkg_new('foo', version='3', state=xstate)
++        opkg2 = self._pkg_new('bar', version='4', state='Updated')
++        npkg2 = self._pkg_new('bar', version='6', state='Update')
++
++        trans = []
++        trans.append(self._trans_new([opkg1, npkg1]))
++        trans.append(self._trans_new([opkg2, npkg2]))
++        merged = self._merge_new(trans)
++        self.assertMergedMain(merged, trans)
++        pkgs = merged.trans_data
++        self.assertEquals(len(pkgs), 4)
++        self.assertEquals(pkgs[0], opkg2)
++        self.assertEquals(pkgs[0].state, opkg2.state)
++        self.assertEquals(pkgs[1], npkg2)
++        self.assertEquals(pkgs[1].state, npkg2.state)
++        self.assertEquals(pkgs[2], opkg1)
++        self.assertEquals(pkgs[2].state, opkg1.state)
++        self.assertEquals(pkgs[3], npkg1)
++        self.assertEquals(pkgs[3].state, xstate)
++
++    def testSimpleUpMerge2(self, xstate='Update'):
++        opkg1 = self._pkg_new('foo',              state='Updated')
++        npkg1 = self._pkg_new('foo', version='3', state=xstate)
++        opkg2 = self._pkg_new('bar', version='4', state='Updated')
++        npkg2 = self._pkg_new('bar', version='6', state='Update')
++        opkg3 = self._pkg_new('foo', version='3', state='Updated')
++        npkg3 = self._pkg_new('foo', version='5', state='Update')
++
++        trans = []
++        trans.append(self._trans_new([opkg2, npkg2, opkg1, npkg1]))
++        trans.append(self._trans_new([opkg3, npkg3]))
++        merged = self._merge_new(trans)
++        self.assertMergedMain(merged, trans)
++        pkgs = merged.trans_data
++        self.assertEquals(len(pkgs), 4)
++        self.assertEquals(pkgs[0], opkg2)
++        self.assertEquals(pkgs[0].state, opkg2.state)
++        self.assertEquals(pkgs[1], npkg2)
++        self.assertEquals(pkgs[1].state, npkg2.state)
++        self.assertEquals(pkgs[2], opkg1)
++        self.assertEquals(pkgs[2].state, opkg1.state)
++        self.assertEquals(pkgs[3], npkg3)
++        self.assertEquals(pkgs[3].state, xstate)
++
++    def testSimpleUpMerge3(self, xstate='Install'):
++        opkg1 = self._pkg_new('foo', state=xstate)
++        opkg2 = self._pkg_new('bar', version='4', state='Updated')
++        npkg2 = self._pkg_new('bar', version='6', state='Update')
++        opkg3 = self._pkg_new('foo',              state='Updated')
++        npkg3 = self._pkg_new('foo', version='5', state='Update')
++
++        trans = []
++        trans.append(self._trans_new([opkg2, npkg2, opkg1]))
++        trans.append(self._trans_new([opkg3, npkg3]))
++        merged = self._merge_new(trans)
++        self.assertMergedMain(merged, trans)
++        pkgs = merged.trans_data
++        self.assertEquals(len(pkgs), 3)
++        self.assertEquals(pkgs[0], opkg2)
++        self.assertEquals(pkgs[0].state, opkg2.state)
++        self.assertEquals(pkgs[1], npkg2)
++        self.assertEquals(pkgs[1].state, npkg2.state)
++        self.assertEquals(pkgs[2], npkg3)
++        self.assertEquals(pkgs[2].state, xstate)
++
++    def testSimpleUpMultiMerge1(self, xstate='Install'):
++        opkg1 = self._pkg_new('foo', arch='i586',              state=xstate)
++        opkg2 = self._pkg_new('bar', version='4', state='Updated')
++        npkg2 = self._pkg_new('bar', version='6', state='Update')
++        opkg3 = self._pkg_new('foo', arch='i586',              state='Updated')
++        npkg3 = self._pkg_new('foo', arch='i686', version='5', state='Update')
++
++        trans = []
++        trans.append(self._trans_new([opkg2, npkg2, opkg1]))
++        trans.append(self._trans_new([opkg3, npkg3]))
++        merged = self._merge_new(trans)
++        self.assertMergedMain(merged, trans)
++        pkgs = merged.trans_data
++        self.assertEquals(len(pkgs), 3)
++        self.assertEquals(pkgs[0], opkg2)
++        self.assertEquals(pkgs[0].state, opkg2.state)
++        self.assertEquals(pkgs[1], npkg2)
++        self.assertEquals(pkgs[1].state, npkg2.state)
++        self.assertEquals(pkgs[2], npkg3)
++        self.assertEquals(pkgs[2].state, xstate)
++
++    def testUpDownMerge1(self, xstate='Update'):
++        opkg1 = self._pkg_new('foo', version='0', state='Updated')
++        npkg1 = self._pkg_new('foo',              state=xstate)
++        opkg2 = self._pkg_new('bar', version='4', state='Updated')
++        npkg2 = self._pkg_new('bar', version='6', state='Update')
++        opkg3 = self._pkg_new('foo',              state='Updated')
++        npkg3 = self._pkg_new('foo', version='7', state='Update')
++        opkg4 = self._pkg_new('foo', version='7', state='Downgraded')
++        npkg4 = self._pkg_new('foo', version='5', state='Downgrade')
++
++        trans = []
++        trans.append(self._trans_new([opkg2, npkg2, opkg1, npkg1]))
++        trans.append(self._trans_new([opkg3, npkg3]))
++        trans.append(self._trans_new([opkg4, npkg4]))
++        merged = self._merge_new(trans)
++        self.assertMergedMain(merged, trans)
++        pkgs = merged.trans_data
++        self.assertEquals(len(pkgs), 4)
++        self.assertEquals(pkgs[0], opkg2)
++        self.assertEquals(pkgs[1], npkg2)
++        self.assertEquals(pkgs[2], opkg1)
++        self.assertNotEquals(pkgs[3], opkg3)
++        self.assertNotEquals(pkgs[3], npkg3)
++        self.assertNotEquals(pkgs[3], opkg4)
++        self.assertNotEquals(pkgs[3].state, npkg4.state)
++        self.assertEquals(pkgs[3].pkgtup, npkg4.pkgtup)
++        self.assertEquals(pkgs[3].state, xstate)
++
++    def testUpDownMerge2(self, xstate='Install'):
++        opkg1 = self._pkg_new('foo')
++        opkg2 = self._pkg_new('bar', version='4', state='Updated')
++        npkg2 = self._pkg_new('bar', version='6', state='Update')
++        opkg3 = self._pkg_new('foo',              state='Updated')
++        npkg3 = self._pkg_new('foo', version='7', state=xstate)
++        opkg4 = self._pkg_new('foo', version='7', state='Downgraded')
++        npkg4 = self._pkg_new('foo', version='5', state='Downgrade')
++
++        trans = []
++        trans.append(self._trans_new([opkg2, npkg2, opkg1]))
++        trans.append(self._trans_new([opkg3, npkg3]))
++        trans.append(self._trans_new([opkg4, npkg4]))
++        merged = self._merge_new(trans)
++        self.assertMergedMain(merged, trans)
++        pkgs = merged.trans_data
++        self.assertEquals(len(pkgs), 3)
++        self.assertEquals(pkgs[0], opkg2)
++        self.assertEquals(pkgs[1], npkg2)
++        self.assertNotEquals(pkgs[2], opkg1)
++        self.assertNotEquals(pkgs[2], opkg3)
++        self.assertNotEquals(pkgs[2], npkg3)
++        self.assertNotEquals(pkgs[2], opkg4)
++        self.assertNotEquals(pkgs[2].state, npkg4.state)
++        self.assertEquals(pkgs[2].pkgtup, npkg4.pkgtup)
++        self.assertEquals(pkgs[2].state, xstate)
++
++    def testUpDownMerge3(self):
++        opkg1 = self._pkg_new('foo')
++        opkg2 = self._pkg_new('bar', version='4', state='Updated')
++        npkg2 = self._pkg_new('bar', version='6', state='Update')
++        opkg3 = self._pkg_new('foo', version='3', state='Updated') # rpmdbv
++        npkg3 = self._pkg_new('foo', version='7', state='Update')
++        opkg4 = self._pkg_new('foo', version='7', state='Downgraded')
++        npkg4 = self._pkg_new('foo', version='3', state='Downgrade')
++
++        trans = []
++        trans.append(self._trans_new([opkg2, npkg2, opkg1]))
++        trans.append(self._trans_new([opkg3, npkg3]))
++        trans.append(self._trans_new([opkg4, npkg4]))
++        merged = self._merge_new(trans)
++        self.assertMergedMain(merged, trans)
++        pkgs = merged.trans_data
++        self.assertEquals(len(pkgs), 4)
++        self.assertEquals(pkgs[0], opkg2)
++        self.assertEquals(pkgs[1], npkg2)
++        self.assertEquals(pkgs[2], opkg1)
++        self.assertEquals(pkgs[2].state, opkg1.state)
++        self.assertNotEquals(pkgs[3], opkg1)
++        self.assertNotEquals(pkgs[3].state, opkg3.state)
++        self.assertNotEquals(pkgs[3], npkg3)
++        self.assertNotEquals(pkgs[3], opkg4)
++        self.assertNotEquals(pkgs[3].state, npkg4.state)
++        self.assertEquals(pkgs[3].pkgtup, npkg4.pkgtup)
++        self.assertEquals(pkgs[3].state, 'Reinstall')
++
++    def testUpDownMerge4(self, xstate='Update'):
++        opkg2 = self._pkg_new('bar', version='4', state='Updated')
++        npkg2 = self._pkg_new('bar', version='6', state='Update')
++        opkg3 = self._pkg_new('foo', version='3', state='Updated')
++        npkg3 = self._pkg_new('foo', version='7', state=xstate)
++        opkg4 = self._pkg_new('foo', version='7', state='Downgraded')
++        npkg4 = self._pkg_new('foo', version='3', state='Downgrade')
++
++        trans = []
++        trans.append(self._trans_new([opkg2, npkg2]))
++        trans.append(self._trans_new([opkg3, npkg3]))
++        trans.append(self._trans_new([opkg4, npkg4]))
++        merged = self._merge_new(trans)
++        self.assertMergedMain(merged, trans)
++        pkgs = merged.trans_data
++        self.assertEquals(len(pkgs), 3)
++        self.assertEquals(pkgs[0], opkg2)
++        self.assertEquals(pkgs[1], npkg2)
++        self.assertNotEquals(pkgs[2].state, opkg3.state)
++        self.assertNotEquals(pkgs[2], npkg3)
++        self.assertNotEquals(pkgs[2], opkg4)
++        self.assertNotEquals(pkgs[2].state, npkg4.state)
++        self.assertEquals(pkgs[2].pkgtup, opkg3.pkgtup)
++        if xstate == 'Obsoleting':
++            self.assertEquals(pkgs[2].state, 'Obsoleting')
++        else:
++            self.assertEquals(pkgs[2].state, 'Reinstall')
++
++    def testUpDownMerge5(self, xstate='Update'):
++        opkg2 = self._pkg_new('bar', version='4', state='Updated')
++        npkg2 = self._pkg_new('bar', version='6', state='Update')
++        opkg3 = self._pkg_new('foo', version='3', state='Updated')
++        npkg3 = self._pkg_new('foo', version='21', state=xstate)
++        opkg4 = self._pkg_new('foo', version='21', state='Downgraded')
++        npkg4 = self._pkg_new('foo', version='19', state='Downgrade')
++        opkg5 = self._pkg_new('foo', version='19', state='Downgraded')
++        npkg5 = self._pkg_new('foo', version='13', state='Downgrade')
++
++        trans = []
++        trans.append(self._trans_new([opkg2, npkg2]))
++        trans.append(self._trans_new([opkg3, npkg3]))
++        trans.append(self._trans_new([opkg4, npkg4]))
++        trans.append(self._trans_new([opkg5, npkg5]))
++        merged = self._merge_new(trans)
++        self.assertMergedMain(merged, trans)
++        pkgs = merged.trans_data
++        self.assertEquals(len(pkgs), 4)
++        self.assertEquals(pkgs[0], opkg2)
++        self.assertEquals(pkgs[0].state, opkg2.state)
++        self.assertEquals(pkgs[1], npkg2)
++        self.assertEquals(pkgs[1].state, npkg2.state)
++        self.assertEquals(pkgs[2], opkg3)
++        self.assertEquals(pkgs[2].state, opkg3.state)
++        self.assertEquals(pkgs[3], npkg5)
++        self.assertEquals(pkgs[3].state, xstate)
++
++    def testDownUpMerge1(self, xstate='Downgrade'):
++        opkg1 = self._pkg_new('foo', version='10', state='Downgraded')
++        npkg1 = self._pkg_new('foo', version='9',  state=xstate)
++        opkg2 = self._pkg_new('bar', version='4', state='Updated')
++        npkg2 = self._pkg_new('bar', version='6', state='Update')
++        opkg3 = self._pkg_new('foo', version='7',  state='Updated')
++        npkg3 = self._pkg_new('foo', version='8',  state='Update')
++        opkg4 = self._pkg_new('foo', version='9',  state='Downgraded')
++        npkg4 = self._pkg_new('foo', version='7',  state='Downgrade')
++
++        trans = []
++        trans.append(self._trans_new([opkg2, npkg2, opkg1, npkg1]))
++        trans.append(self._trans_new([opkg4, npkg4]))
++        trans.append(self._trans_new([opkg3, npkg3]))
++        merged = self._merge_new(trans)
++        self.assertMergedMain(merged, trans)
++        pkgs = merged.trans_data
++        self.assertEquals(len(pkgs), 4)
++        self.assertEquals(pkgs[0], opkg2)
++        self.assertEquals(pkgs[1], npkg2)
++        self.assertNotEquals(pkgs[2], opkg3)
++        self.assertNotEquals(pkgs[2].state, npkg3.state)
++        self.assertNotEquals(pkgs[2], opkg4)
++        self.assertNotEquals(pkgs[2], npkg4)
++        self.assertEquals(pkgs[2].pkgtup, npkg3.pkgtup)
++        self.assertEquals(pkgs[2].state, xstate)
++        self.assertEquals(pkgs[3], opkg1)
++        self.assertEquals(pkgs[3].state, opkg1.state)
++
++    def testDownUpMerge2(self, xstate='Install'):
++        opkg1 = self._pkg_new('foo', version='7', state=xstate)
++        opkg2 = self._pkg_new('bar', version='4', state='Updated')
++        npkg2 = self._pkg_new('bar', version='6', state='Update')
++        opkg3 = self._pkg_new('foo', version='5', state='Updated')
++        npkg3 = self._pkg_new('foo', version='6', state='Update')
++        opkg4 = self._pkg_new('foo', version='7', state='Downgraded')
++        npkg4 = self._pkg_new('foo', version='5', state='Downgrade')
++
++        trans = []
++        trans.append(self._trans_new([opkg2, npkg2, opkg1]))
++        trans.append(self._trans_new([opkg4, npkg4]))
++        trans.append(self._trans_new([opkg3, npkg3]))
++        merged = self._merge_new(trans)
++        self.assertMergedMain(merged, trans)
++        pkgs = merged.trans_data
++        self.assertEquals(len(pkgs), 3)
++        self.assertEquals(pkgs[0], opkg2)
++        self.assertEquals(pkgs[1], npkg2)
++        self.assertNotEquals(pkgs[2], opkg1)
++        self.assertNotEquals(pkgs[2], opkg3)
++        self.assertNotEquals(pkgs[2], opkg4)
++        self.assertNotEquals(pkgs[2], npkg4)
++        self.assertNotEquals(pkgs[2].state, npkg3.state)
++        self.assertEquals(pkgs[2].pkgtup, npkg3.pkgtup)
++        self.assertEquals(pkgs[2].state, xstate)
++
++    def testDownUpMerge3(self):
++        opkg1 = self._pkg_new('foo')
++        opkg2 = self._pkg_new('bar', version='4', state='Updated')
++        npkg2 = self._pkg_new('bar', version='6', state='Update')
++        opkg3 = self._pkg_new('foo', version='3', state='Updated')
++        npkg3 = self._pkg_new('foo', version='7', state='Update')
++        opkg4 = self._pkg_new('foo', version='7', state='Downgraded') # rpmdbv
++        npkg4 = self._pkg_new('foo', version='3', state='Downgrade')
++
++        trans = []
++        trans.append(self._trans_new([opkg2, npkg2, opkg1]))
++        trans.append(self._trans_new([opkg4, npkg4]))
++        trans.append(self._trans_new([opkg3, npkg3]))
++        merged = self._merge_new(trans)
++        self.assertMergedMain(merged, trans)
++        pkgs = merged.trans_data
++        self.assertEquals(len(pkgs), 4)
++        self.assertEquals(pkgs[0], opkg2)
++        self.assertEquals(pkgs[1], npkg2)
++        self.assertEquals(pkgs[2], opkg1)
++        self.assertEquals(pkgs[2].state, opkg1.state)
++        self.assertNotEquals(pkgs[3], opkg1)
++        self.assertNotEquals(pkgs[3], opkg3)
++        self.assertNotEquals(pkgs[3].state, npkg3.state)
++        self.assertNotEquals(pkgs[3].state, opkg4.state)
++        self.assertNotEquals(pkgs[3], npkg4)
++        self.assertEquals(pkgs[3].pkgtup, npkg3.pkgtup)
++        self.assertEquals(pkgs[3].state, 'Reinstall')
++
++    def testDownUpMerge4(self, xstate='Update'):
++        opkg2 = self._pkg_new('bar', version='4', state='Updated')
++        npkg2 = self._pkg_new('bar', version='6', state='Update')
++        opkg3 = self._pkg_new('foo', version='3', state='Updated')
++        npkg3 = self._pkg_new('foo', version='7', state=xstate)
++        opkg4 = self._pkg_new('foo', version='7', state='Downgraded')
++        npkg4 = self._pkg_new('foo', version='3', state='Downgrade')
++
++        trans = []
++        trans.append(self._trans_new([opkg2, npkg2]))
++        trans.append(self._trans_new([opkg4, npkg4]))
++        trans.append(self._trans_new([opkg3, npkg3]))
++        merged = self._merge_new(trans)
++        self.assertMergedMain(merged, trans)
++        pkgs = merged.trans_data
++        self.assertEquals(len(pkgs), 3)
++        self.assertEquals(pkgs[0], opkg2)
++        self.assertEquals(pkgs[1], npkg2)
++        self.assertNotEquals(pkgs[2], opkg3)
++        self.assertNotEquals(pkgs[2].state, 'Update')
++        self.assertNotEquals(pkgs[2].state, opkg4.state)
++        self.assertNotEquals(pkgs[2], npkg4)
++        self.assertEquals(pkgs[2].pkgtup, npkg3.pkgtup)
++        if xstate == 'Obsoleting':
++            self.assertEquals(pkgs[2].state, 'Obsoleting')
++        else:
++            self.assertEquals(pkgs[2].state, 'Reinstall')
++
++    def testDownUpMerge5(self, xstate='Downgrade'):
++        opkg2 = self._pkg_new('bar', version='4', state='Updated')
++        npkg2 = self._pkg_new('bar', version='6', state='Update')
++        opkg3 = self._pkg_new('foo', version='21', state='Downgraded')
++        npkg3 = self._pkg_new('foo', version='3',  state=xstate)
++        opkg4 = self._pkg_new('foo', version='3',  state='Updated')
++        npkg4 = self._pkg_new('foo', version='7',  state='Update')
++        opkg5 = self._pkg_new('foo', version='7',  state='Updated')
++        npkg5 = self._pkg_new('foo', version='13', state='Update')
++
++        trans = []
++        trans.append(self._trans_new([opkg2, npkg2]))
++        trans.append(self._trans_new([opkg3, npkg3]))
++        trans.append(self._trans_new([opkg4, npkg4]))
++        trans.append(self._trans_new([opkg5, npkg5]))
++        merged = self._merge_new(trans)
++        self.assertMergedMain(merged, trans)
++        pkgs = merged.trans_data
++        self.assertEquals(len(pkgs), 4)
++        self.assertEquals(pkgs[0], opkg2)
++        self.assertEquals(pkgs[0].state, opkg2.state)
++        self.assertEquals(pkgs[1], npkg2)
++        self.assertEquals(pkgs[1].state, npkg2.state)
++        self.assertEquals(pkgs[2], npkg5)
++        self.assertEquals(pkgs[2].state, xstate)
++        self.assertEquals(pkgs[3], opkg3)
++        self.assertEquals(pkgs[3].state, opkg3.state)
++
++    def testInRmMerge1(self, xstate='Install', estate='Erase'):
++        npkg1 = self._pkg_new('foo', state=xstate)
++        npkg2 = self._pkg_new('foo', state=estate)
++        npkg3 = self._pkg_new('bar', version='6', state='True-Install')
++
++        trans = []
++        trans.append(self._trans_new([npkg1]))
++        trans.append(self._trans_new([npkg2]))
++        trans.append(self._trans_new([npkg3]))
++        merged = self._merge_new(trans)
++        self.assertMergedMain(merged, trans)
++        pkgs = merged.trans_data
++        self.assertEquals(len(pkgs), 1)
++        self.assertEquals(pkgs[0], npkg3)
++        self.assertEquals(pkgs[0].state, npkg3.state)
++
++    def testInRmMerge2(self, xstate='Install'):
++        self.testInRmMerge1(xstate, 'Obsoleted')
++
++    def testInRmInonlyMerge1(self, xstate='True-Install', estate='Erase'):
++        npkg1 = self._pkg_new('foo', state=xstate)
++        npkg2 = self._pkg_new('foo', version='2', state=xstate)
++        npkg3 = self._pkg_new('foo', version='3', state=xstate)
++        npkg4 = self._pkg_new('foo', state=estate)
++        npkg5 = self._pkg_new('foo', version='2', state=estate)
++        npkg6 = self._pkg_new('foo', version='3', state=estate)
++        npkg9 = self._pkg_new('bar', version='6', state=xstate)
++
++        trans = []
++        trans.append(self._trans_new([npkg1]))
++        trans.append(self._trans_new([npkg2]))
++        trans.append(self._trans_new([npkg3]))
++        trans.append(self._trans_new([npkg4]))
++        trans.append(self._trans_new([npkg5]))
++        trans.append(self._trans_new([npkg6]))
++        trans.append(self._trans_new([npkg9]))
++        merged = self._merge_new(trans)
++        self.assertMergedMain(merged, trans)
++        pkgs = merged.trans_data
++        self.assertEquals(len(pkgs), 1)
++        self.assertEquals(pkgs[0], npkg9)
++        self.assertEquals(pkgs[0].state, npkg9.state)
++
++    def testInRmInonlyMerge2(self, xstate='True-Install'):
++        self.testInRmInonlyMerge1(xstate, 'Obsoleted')
++
++    def testUpRmMerge1(self, xstate='Update'):
++        npkg1 = self._pkg_new('foo')
++        opkg2 = self._pkg_new('bar', version='4', state='Updated')
++        npkg2 = self._pkg_new('bar', version='6', state=xstate)
++        npkg3 = self._pkg_new('bar', version='6', state='Erase')
++
++        trans = []
++        trans.append(self._trans_new([npkg1]))
++        trans.append(self._trans_new([opkg2, npkg2]))
++        trans.append(self._trans_new([npkg3]))
++        merged = self._merge_new(trans)
++        self.assertMergedMain(merged, trans)
++        pkgs = merged.trans_data
++        self.assertEquals(len(pkgs), 2)
++        self.assertEquals(pkgs[0], opkg2)
++        self.assertEquals(pkgs[0].state, npkg3.state)
++        self.assertEquals(pkgs[1], npkg1)
++        self.assertEquals(pkgs[1].state, npkg1.state)
++
++    def testUpRmMerge2(self, xstate='True-Install'):
++        npkg1 = self._pkg_new('foo')
++        npkg4 = self._pkg_new('bar', version='4', state=xstate)
++        opkg2 = self._pkg_new('bar', version='4', state='Updated')
++        npkg2 = self._pkg_new('bar', version='6', state='Update')
++        npkg3 = self._pkg_new('bar', version='6', state='Erase')
++
++        trans = []
++        trans.append(self._trans_new([npkg1, npkg4]))
++        trans.append(self._trans_new([opkg2, npkg2]))
++        trans.append(self._trans_new([npkg3]))
++        merged = self._merge_new(trans)
++        self.assertMergedMain(merged, trans)
++        pkgs = merged.trans_data
++        self.assertEquals(len(pkgs), 1)
++        self.assertEquals(pkgs[0], npkg1)
++        self.assertEquals(pkgs[0].state, npkg1.state)
++
++    def testUpRmMerge3(self, xstate='Update'):
++        npkg1 = self._pkg_new('foo')
++        npkg4 = self._pkg_new('bar', version='4', state='Dep-Install')
++        opkg2 = self._pkg_new('bar', version='4', state='Updated')
++        npkg2 = self._pkg_new('bar', version='6', state=xstate)
++        npkg3 = self._pkg_new('bar', version='6', state='Erase')
++
++        trans = []
++        trans.append(self._trans_new([npkg1, npkg4]))
++        trans.append(self._trans_new([opkg2, npkg2]))
++        trans.append(self._trans_new([npkg3]))
++        merged = self._merge_new(trans)
++        self.assertMergedMain(merged, trans)
++        pkgs = merged.trans_data
++        self.assertEquals(len(pkgs), 1)
++        self.assertEquals(pkgs[0], npkg1)
++        self.assertEquals(pkgs[0].state, npkg1.state)
++
++    def testRmInMerge1(self, xstate='Install', estate='Erase'):
++        npkg1 = self._pkg_new('foo', state=xstate)
++        npkg2 = self._pkg_new('foo', state=estate)
++        npkg3 = self._pkg_new('bar', version='6', state='True-Install')
++
++        trans = []
++        trans.append(self._trans_new([npkg2]))
++        trans.append(self._trans_new([npkg1]))
++        trans.append(self._trans_new([npkg3]))
++        merged = self._merge_new(trans)
++        self.assertMergedMain(merged, trans)
++        pkgs = merged.trans_data
++        self.assertEquals(len(pkgs), 2)
++        self.assertEquals(pkgs[0], npkg3)
++        self.assertEquals(pkgs[0].state, npkg3.state)
++        self.assertEquals(pkgs[1], npkg1)
++        if xstate == 'Obsoleting':
++            self.assertEquals(pkgs[1].state, 'Obsoleting')
++        else:
++            self.assertEquals(pkgs[1].state, 'Reinstall')
++
++    def testRmInMerge2(self, xstate='Install'):
++        self.testRmInMerge1(xstate, 'Obsoleted')
++
++    def testUpRmInlMerge1(self, xstate='Update', ystate='Install',
++                          estate='Erase'):
++        npkg1 = self._pkg_new('bar', version='6', state='True-Install')
++        opkg2 = self._pkg_new('foo', version='3',  state='Updated')
++        npkg2 = self._pkg_new('foo', version='7',  state=xstate)
++        npkg3 = self._pkg_new('foo', version='7',  state=estate)
++        npkg4 = self._pkg_new('foo',               state=ystate)
++
++        trans = []
++        trans.append(self._trans_new([npkg1]))
++        trans.append(self._trans_new([opkg2, npkg2]))
++        trans.append(self._trans_new([npkg3]))
++        trans.append(self._trans_new([npkg4]))
++        merged = self._merge_new(trans)
++        self.assertMergedMain(merged, trans)
++        pkgs = merged.trans_data
++        self.assertEquals(len(pkgs), 3)
++        self.assertEquals(pkgs[0], npkg1)
++        self.assertEquals(pkgs[0].state, npkg1.state)
++        self.assertEquals(pkgs[1].pkgtup, npkg4.pkgtup)
++        if ystate == 'Obsoleting':
++            self.assertEquals(pkgs[1].state, "Obsoleting")
++        else:
++            self.assertEquals(pkgs[1].state, "Downgrade")
++        self.assertEquals(pkgs[2].pkgtup, opkg2.pkgtup)
++        self.assertEquals(pkgs[2].state, "Downgraded")
++
++    def testUpRmInlMerge2(self, xstate='Update', ystate='Install'):
++        self.testUpRmInlMerge1(xstate, ystate, 'Obsoleted')
++
++    def testUpRmInuMerge1(self, xstate='Update', ystate='Install',
++                          estate='Erase'):
++        npkg1 = self._pkg_new('bar', version='6', state='True-Install')
++        opkg2 = self._pkg_new('foo', version='3',  state='Updated')
++        npkg2 = self._pkg_new('foo', version='7',  state=xstate)
++        npkg3 = self._pkg_new('foo', version='7',  state=estate)
++        npkg4 = self._pkg_new('foo', version='4',  state=ystate)
++
++        trans = []
++        trans.append(self._trans_new([npkg1]))
++        trans.append(self._trans_new([opkg2, npkg2]))
++        trans.append(self._trans_new([npkg3]))
++        trans.append(self._trans_new([npkg4]))
++        merged = self._merge_new(trans)
++        self.assertMergedMain(merged, trans)
++        pkgs = merged.trans_data
++        self.assertEquals(len(pkgs), 3)
++        self.assertEquals(pkgs[0], npkg1)
++        self.assertEquals(pkgs[0].state, npkg1.state)
++        self.assertEquals(pkgs[1].pkgtup, opkg2.pkgtup)
++        self.assertEquals(pkgs[1].state,  "Updated")
++        self.assertEquals(pkgs[2].pkgtup, npkg4.pkgtup)
++        if ystate == 'Obsoleting':
++            self.assertEquals(pkgs[2].state, "Obsoleting")
++        else:
++            self.assertEquals(pkgs[2].state, "Update")
++
++    def testUpRmInuMerge2(self, xstate='Update', ystate='Install'):
++        self.testUpRmInuMerge1(xstate, ystate, 'Obsoleted')
++
++    def testBrokenUpMerge1(self, xstate='Update', estate='Erase'):
++        # This is "broken", so as long as we don't die it's all good.
++        # The below test basically documents what we do.
++        opkg1 = self._pkg_new('foo', version='1',   state='Updated')
++        npkg1 = self._pkg_new('foo', version='2',   state=xstate)
++        opkg2 = self._pkg_new('foo', version='11',  state='Updated')
++        npkg2 = self._pkg_new('foo', version='21',  state=xstate)
++        opkg3 = self._pkg_new('foo', version='110', state='Updated')
++        npkg3 = self._pkg_new('foo', version='210', state=xstate)
++        npkg4 = self._pkg_new('foo', version='2',   state=estate)
++        npkg5 = self._pkg_new('foo', version='21',  state=estate)
++        npkg6 = self._pkg_new('foo', version='210', state=estate)
++
++        trans = []
++        trans.append(self._trans_new([opkg1, npkg1]))
++        trans.append(self._trans_new([opkg2, npkg2]))
++        trans.append(self._trans_new([opkg3, npkg3]))
++        trans.append(self._trans_new([npkg4]))
++        trans.append(self._trans_new([npkg5]))
++        trans.append(self._trans_new([npkg6]))
++        merged = self._merge_new(trans)
++        self.assertMergedMain(merged, trans)
++        pkgs = merged.trans_data
++        self.assertEquals(len(pkgs), 3)
++        self.assertEquals(pkgs[0], opkg1)
++        self.assertEquals(pkgs[0].state, 'Updated')
++        self.assertEquals(pkgs[1], opkg2)
++        self.assertEquals(pkgs[1].state, 'Updated')
++        self.assertEquals(pkgs[2], opkg3)
++        self.assertEquals(pkgs[2].state, estate)
++
++    #  Obsoleting is the _painful_ one because it really should be a state, but
++    # an attribute. So "Obsoleting" can be any of:
++    #     Install*, Reinstall, Update, Downgrade
++    def testObsSIM1(self):
++        self.testSimpleInMerge1(xstate='Obsoleting')
++    def testObsSIM2(self):
++        self.testSimpleInMerge2(xstate='Obsoleting')
++    def testObsSUM1(self):
++        self.testSimpleUpMerge1(xstate='Obsoleting')
++    def testObsSUM2(self):
++        self.testSimpleUpMerge2(xstate='Obsoleting')
++    def testObsSUM3(self):
++        self.testSimpleUpMerge3(xstate='Obsoleting')
++    def testObsSUMM1(self):
++        self.testSimpleUpMultiMerge1(xstate='Obsoleting')
++    def testObsUDM1(self):
++        self.testUpDownMerge1(xstate='Obsoleting')
++    def testObsUDM2(self):
++        self.testUpDownMerge2(xstate='Obsoleting')
++    def testObsUDM4(self):
++        self.testUpDownMerge4(xstate='Obsoleting')
++    def testObsUDM5(self):
++        self.testUpDownMerge5(xstate='Obsoleting')
++    def testObsDUM1(self):
++        self.testDownUpMerge1(xstate='Obsoleting')
++    def testObsDUM2(self):
++        self.testDownUpMerge2(xstate='Obsoleting')
++    def testObsDUM4(self):
++        self.testDownUpMerge4(xstate='Obsoleting')
++    def testObsDUM5(self):
++        self.testDownUpMerge5(xstate='Obsoleting')
++    def testObsIRM1(self):
++        self.testInRmMerge1(xstate='Obsoleting')
++    def testObsIRM2(self):
++        self.testInRmMerge2(xstate='Obsoleting')
++    def testObsIRMM1(self):
++        self.testInRmInonlyMerge1(xstate='Obsoleting')
++    def testObsIRMM2(self):
++        self.testInRmInonlyMerge1(xstate='Obsoleting')
++    def testObsURM1(self):
++        self.testUpRmMerge1(xstate='Obsoleting')
++    def testObsURM2(self):
++        self.testUpRmMerge2(xstate='Obsoleting')
++    def testObsURM3(self):
++        self.testUpRmMerge3(xstate='Obsoleting')
++    def testObsRIM1(self):
++        self.testRmInMerge1(xstate='Obsoleting')
++    def testObsRIM2(self):
++        self.testRmInMerge2(xstate='Obsoleting')
++    def testObsURIlM1(self):
++        self.testUpRmInlMerge1(xstate='Obsoleting')
++        self.testUpRmInlMerge1(ystate='Obsoleting')
++        self.testUpRmInlMerge1(xstate='Obsoleting', ystate='Obsoleting')
++    def testObsURIlM2(self):
++        self.testUpRmInlMerge2(xstate='Obsoleting')
++        self.testUpRmInlMerge2(ystate='Obsoleting')
++        self.testUpRmInlMerge2(xstate='Obsoleting', ystate='Obsoleting')
++    def testObsURIuM1(self):
++        self.testUpRmInuMerge1(xstate='Obsoleting')
++        self.testUpRmInuMerge1(ystate='Obsoleting')
++        self.testUpRmInuMerge1(xstate='Obsoleting', ystate='Obsoleting')
++    def testObsURIuM2(self):
++        self.testUpRmInuMerge2(xstate='Obsoleting')
++        self.testUpRmInuMerge2(ystate='Obsoleting')
++        self.testUpRmInuMerge2(xstate='Obsoleting', ystate='Obsoleting')
 diff --git a/test/packagetests.py b/test/packagetests.py
 index dac8abd..1e3302b 100644
 --- a/test/packagetests.py
@@ -28709,7 +29782,7 @@ index c3c7133..fc05aa6 100644
  Requires: python-iniparse
  Requires: pygpgme
 diff --git a/yum/__init__.py b/yum/__init__.py
-index 2ea9f20..5c689e5 100644
+index 2ea9f20..afe5354 100644
 --- a/yum/__init__.py
 +++ b/yum/__init__.py
 @@ -294,6 +294,11 @@ class YumBase(depsolve.Depsolve):
@@ -28737,7 +29810,53 @@ index 2ea9f20..5c689e5 100644
  
          # Ensure that the repo name is set
          if not repo.name:
-@@ -1334,6 +1343,8 @@ class YumBase(depsolve.Depsolve):
+@@ -752,7 +761,7 @@ class YumBase(depsolve.Depsolve):
+             groupfile = repo.getGroups()
+             # open it up as a file object so iterparse can cope with our compressed file
+             if groupfile:
+-                groupfile = misc.decompress(groupfile)
++                groupfile = misc.repo_gen_decompress(groupfile, 'groups.xml')
+                 
+             try:
+                 self._comps.add(groupfile)
+@@ -790,7 +799,8 @@ class YumBase(depsolve.Depsolve):
+                 # fetch the sqlite tagdb
+                 try:
+                     tag_md = repo.retrieveMD('pkgtags')
+-                    tag_sqlite  = yum.misc.decompress(tag_md)
++                    tag_sqlite  = misc.repo_gen_decompress(tag_md,
++                                                           'pkgtags.sqlite')
+                     # feed it into _tags.add()
+                     self._tags.add(repo.id, tag_sqlite)
+                 except (Errors.RepoError, Errors.PkgTagsError), e:
+@@ -976,8 +986,6 @@ class YumBase(depsolve.Depsolve):
+                 restring.append(_('Trying to remove "%s", which is protected') %
+                                 pkgname)
+ 
+-        self.rpmdb.dropCachedData()
+-
+         self.verbose_logger.debug('Depsolve time: %0.3f' % (time.time() - ds_st))
+         return rescode, restring
+ 
+@@ -1311,7 +1319,10 @@ class YumBase(depsolve.Depsolve):
+                 self.run_with_package_names.add('yum-metadata-parser')
+                 break
+ 
+-        if self.conf.history_record and not self.ts.isTsFlagSet(rpm.RPMTRANS_FLAG_TEST):
++        if (not self.conf.history_record or
++            self.ts.isTsFlagSet(rpm.RPMTRANS_FLAG_TEST)):
++            frpmdbv = self.tsInfo.futureRpmDBVersion()
++        else:
+             using_pkgs_pats = list(self.run_with_package_names)
+             using_pkgs = self.rpmdb.returnPackages(patterns=using_pkgs_pats)
+             rpmdbv  = self.rpmdb.simpleVersion(main_only=True)[0]
+@@ -1330,10 +1341,14 @@ class YumBase(depsolve.Depsolve):
+                 cmdline = ' '.join(self.args)
+             elif hasattr(self, 'cmds') and self.cmds:
+                 cmdline = ' '.join(self.cmds)
++
++            frpmdbv = self.tsInfo.futureRpmDBVersion()
+             self.history.beg(rpmdbv, using_pkgs, list(self.tsInfo),
                               self.skipped_packages, rpmdb_problems, cmdline)
              # write out our config and repo data to additional history info
              self._store_config_in_history()
@@ -28746,25 +29865,54 @@ index 2ea9f20..5c689e5 100644
              
              self.plugins.run('historybegin')
          #  Just before we update the transaction, update what we think the
-@@ -1403,7 +1414,8 @@ class YumBase(depsolve.Depsolve):
+@@ -1341,7 +1356,7 @@ class YumBase(depsolve.Depsolve):
+         # "something" happens and the rpmdb is different from what we think it
+         # will be we store what we thought, not what happened (so it'll be an
+         # invalid cache).
+-        self.rpmdb.transactionResultVersion(self.tsInfo.futureRpmDBVersion())
++        self.rpmdb.transactionResultVersion(frpmdbv)
+ 
+         errors = self.ts.run(cb.callback, '')
+         # ts.run() exit codes are, hmm, "creative": None means all ok, empty 
+@@ -1382,7 +1397,8 @@ class YumBase(depsolve.Depsolve):
+                 except (IOError, OSError), e:
+                     self.logger.critical(_('Failed to remove transaction file %s') % fn)
+ 
+-        self.rpmdb.dropCachedData() # drop out the rpm cache so we don't step on bad hdr indexes
++        # drop out the rpm cache so we don't step on bad hdr indexes
++        self.rpmdb.dropCachedDataPostTransaction(list(self.tsInfo))
+         self.plugins.run('posttrans')
+         # sync up what just happened versus what is in the rpmdb
+         if not self.ts.isTsFlagSet(rpm.RPMTRANS_FLAG_TEST):
+@@ -1403,8 +1419,8 @@ class YumBase(depsolve.Depsolve):
          #    that there is not also an install of this pkg in the tsInfo (reinstall)
          # for any kind of install add from_repo to the yumdb, and the cmdline
          # and the install reason
 -
+-        self.rpmdb.dropCachedData()
 +        
 +        vt_st = time.time()
-         self.rpmdb.dropCachedData()
          self.plugins.run('preverifytrans')
          for txmbr in self.tsInfo:
-@@ -1489,6 +1501,7 @@ class YumBase(depsolve.Depsolve):
+             if txmbr.output_state in TS_INSTALL_STATES:
+@@ -1482,13 +1498,15 @@ class YumBase(depsolve.Depsolve):
+                 self.verbose_logger.log(logginglevels.DEBUG_2, 'What is this? %s' % txmbr.po)
+ 
+         self.plugins.run('postverifytrans')
++        rpmdbv = self.rpmdb.simpleVersion(main_only=True)[0]
+         if self.conf.history_record and not self.ts.isTsFlagSet(rpm.RPMTRANS_FLAG_TEST):
+             ret = -1
+             if resultobject is not None:
+                 ret = resultobject.return_code
              self.plugins.run('historyend')
-             self.history.end(self.rpmdb.simpleVersion(main_only=True)[0], ret)
+-            self.history.end(self.rpmdb.simpleVersion(main_only=True)[0], ret)
++            self.history.end(rpmdbv, ret)
          self.rpmdb.dropCachedData()
 +        self.verbose_logger.debug('VerifyTransaction time: %0.3f' % (time.time() - vt_st))
  
      def costExcludePackages(self):
          """ Create an excluder for repos. with higher cost. Eg.
-@@ -3100,6 +3113,9 @@ class YumBase(depsolve.Depsolve):
+@@ -3100,6 +3118,9 @@ class YumBase(depsolve.Depsolve):
  
      def _find_obsoletees(self, po):
          """ Return the pkgs. that are obsoleted by the po we pass in. """
@@ -28774,7 +29922,7 @@ index 2ea9f20..5c689e5 100644
          if not isinstance(po, YumLocalPackage):
              for (obstup, inst_tup) in self.up.getObsoletersTuples(name=po.name):
                  if po.pkgtup == obstup:
-@@ -3160,7 +3176,7 @@ class YumBase(depsolve.Depsolve):
+@@ -3160,7 +3181,7 @@ class YumBase(depsolve.Depsolve):
                      try:
                          mypkgs = self.returnPackagesByDep(arg)
                      except yum.Errors.YumBaseError, e:
@@ -28783,7 +29931,7 @@ index 2ea9f20..5c689e5 100644
                      else:
                          # install MTA* == fail, because provides don't do globs
                          # install /usr/kerberos/bin/* == success (and we want
-@@ -3490,7 +3506,7 @@ class YumBase(depsolve.Depsolve):
+@@ -3490,7 +3511,7 @@ class YumBase(depsolve.Depsolve):
              availpkgs.extend(m)
  
              if not availpkgs and not instpkgs:
@@ -28792,7 +29940,7 @@ index 2ea9f20..5c689e5 100644
          
          else: # we have kwargs, sort them out.
              nevra_dict = self._nevra_kwarg_parse(kwargs)
-@@ -3669,7 +3685,8 @@ class YumBase(depsolve.Depsolve):
+@@ -3669,7 +3690,8 @@ class YumBase(depsolve.Depsolve):
                          self.logger.critical(_('%s') % e)
                      
                      if not depmatches:
@@ -28802,7 +29950,7 @@ index 2ea9f20..5c689e5 100644
                      else:
                          pkgs.extend(depmatches)
                  
-@@ -3690,6 +3707,14 @@ class YumBase(depsolve.Depsolve):
+@@ -3690,6 +3712,14 @@ class YumBase(depsolve.Depsolve):
              if self.conf.protected_packages and po.pkgtup == kern_pkgtup:
                  self.logger.warning(_("Skipping the running kernel: %s") % po)
                  continue
@@ -28817,7 +29965,7 @@ index 2ea9f20..5c689e5 100644
              txmbr = self.tsInfo.addErase(po)
              tx_return.append(txmbr)
          
-@@ -3975,7 +4000,7 @@ class YumBase(depsolve.Depsolve):
+@@ -3975,7 +4005,7 @@ class YumBase(depsolve.Depsolve):
                      try:
                          apkgs = self.returnPackagesByDep(arg)
                      except yum.Errors.YumBaseError, e:
@@ -28826,7 +29974,7 @@ index 2ea9f20..5c689e5 100644
  
          else:
              nevra_dict = self._nevra_kwarg_parse(kwargs)
-@@ -4276,12 +4301,11 @@ class YumBase(depsolve.Depsolve):
+@@ -4276,12 +4306,11 @@ class YumBase(depsolve.Depsolve):
          keyurls = repo.gpgkey
          key_installed = False
  
@@ -28840,7 +29988,7 @@ index 2ea9f20..5c689e5 100644
                  # Check if key is already installed
                  if misc.keyInstalled(ts, info['keyid'], info['timestamp']) >= 0:
                      self.logger.info(_('GPG key at %s (0x%s) is already installed') % (
-@@ -4306,6 +4330,7 @@ class YumBase(depsolve.Depsolve):
+@@ -4306,6 +4335,7 @@ class YumBase(depsolve.Depsolve):
                      raise Errors.YumBaseError, _("Not installing key")
                  
                  # Import the key
@@ -28848,6 +29996,15 @@ index 2ea9f20..5c689e5 100644
                  result = ts.pgpImportPubkey(misc.procgpgkey(info['raw_key']))
                  if result != 0:
                      raise Errors.YumBaseError, \
+diff --git a/yum/comps.py b/yum/comps.py
+index 5ccfba2..408bb1c 100755
+--- a/yum/comps.py
++++ b/yum/comps.py
+@@ -1,3 +1,4 @@
++#! /usr/bin/python -tt
+ # This program 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
 diff --git a/yum/config.py b/yum/config.py
 index 650d7b9..49411e3 100644
 --- a/yum/config.py
@@ -28870,6 +30027,15 @@ index 650d7b9..49411e3 100644
  
      # FIXME: rename gpgcheck to pkgs_gpgcheck
      gpgcheck = Inherit(YumConf.gpgcheck)
+diff --git a/yum/constants.py b/yum/constants.py
+index 06d5a6b..5c728d4 100644
+--- a/yum/constants.py
++++ b/yum/constants.py
+@@ -1,3 +1,4 @@
++#! /usr/bin/python -tt
+ # This program 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
 diff --git a/yum/depsolve.py b/yum/depsolve.py
 index e9b3fa7..886cca8 100644
 --- a/yum/depsolve.py
@@ -28886,29 +30052,373 @@ index e9b3fa7..886cca8 100644
                  # just b/c they're not installed pkgs doesn't mean they should
                  # be ignored entirely. Just not preferred
 diff --git a/yum/history.py b/yum/history.py
-index 502b908..332a3e9 100644
+index 502b908..a8004af 100644
 --- a/yum/history.py
 +++ b/yum/history.py
-@@ -27,7 +27,7 @@ import yum.misc as misc
+@@ -27,8 +27,9 @@ import yum.misc as misc
  import yum.constants
  from yum.constants import *
  from yum.packages import YumInstalledPackage, YumAvailablePackage, PackageObject
 -from yum.i18n import to_unicode
 +from yum.i18n import to_unicode, to_utf8
  
++from rpmUtils.arch import getBaseArch
  
  _history_dir = '/var/lib/yum/history'
-@@ -112,6 +112,9 @@ class YumHistoryPackage(PackageObject):
+ 
+@@ -98,7 +99,7 @@ def _setupHistorySearchSQL(patterns=None, ignore_case=False):
+ 
+ class YumHistoryPackage(PackageObject):
+ 
+-    def __init__(self, name, arch, epoch, version, release, checksum):
++    def __init__(self, name, arch, epoch, version, release, checksum=None):
+         self.name    = name
+         self.version = version
+         self.release = release
+@@ -111,6 +112,18 @@ class YumHistoryPackage(PackageObject):
+         else:
              chk = checksum.split(':')
              self._checksums = [(chk[0], chk[1], 0)] # (type, checksum, id(0,1))
- 
-+        self.repoid = '<history>'
++        # Needed for equality comparisons in PackageObject
++        self.repoid = "<history>"
++
++class YumHistoryPackageState(YumHistoryPackage):
++    def __init__(self, name,arch, epoch,version,release, state, checksum=None):
++        YumHistoryPackage.__init__(self, name,arch, epoch,version,release,
++                                   checksum)
++        self.done  = None
++        self.state = state
 +
++        self.repoid = '<history>'
 +
+ 
  class YumHistoryRpmdbProblem(PackageObject):
      """ Class representing an rpmdb problem that existed at the time of the
-         transaction. """
-@@ -425,12 +428,15 @@ class YumHistory:
+@@ -224,6 +237,323 @@ class YumHistoryTransaction:
+     errors     = property(fget=lambda self: self._getErrors())
+     output     = property(fget=lambda self: self._getOutput())
+ 
++class YumMergedHistoryTransaction(YumHistoryTransaction):
++    def __init__(self, obj):
++        self._merged_tids = set([obj.tid])
++        self._merged_objs = [obj]
++
++        self.beg_timestamp    = obj.beg_timestamp
++        self.beg_rpmdbversion = obj.beg_rpmdbversion
++        self.end_timestamp    = obj.end_timestamp
++        self.end_rpmdbversion = obj.end_rpmdbversion
++
++        self._loaded_TW = None
++        self._loaded_TD = None
++        #  Hack, this is difficult ... not sure if we want to list everything
++        # that was skipped. Just those things which were skipped and then not
++        # updated later ... or nothing. Nothing is much easier.
++        self._loaded_TS = []
++
++        self._loaded_PROB = None
++
++        self._have_loaded_CMD = False # cmdline can validly be None
++        self._loaded_CMD = None
++
++        self._loaded_ER = None
++        self._loaded_OT = None
++
++        self.altered_lt_rpmdb = None
++        self.altered_gt_rpmdb = None
++
++    def _getAllTids(self):
++        return sorted(self._merged_tids)
++    tid         = property(fget=lambda self: self._getAllTids())
++
++    def _getLoginUIDs(self):
++        ret = set((tid.loginuid for tid in self._merged_objs))
++        if len(ret) == 1:
++            return list(ret)[0]
++        return sorted(ret)
++    loginuid    = property(fget=lambda self: self._getLoginUIDs())
++
++    def _getReturnCodes(self):
++        ret_codes = set((tid.return_code for tid in self._merged_objs))
++        if len(ret_codes) == 1 and 0 in ret_codes:
++            return 0
++        if 0 in ret_codes:
++            ret_codes.remove(0)
++        return sorted(ret_codes)
++    return_code = property(fget=lambda self: self._getReturnCodes())
++
++    def _getTransWith(self):
++        ret = []
++        filt = set()
++        for obj in self._merged_objs:
++            for pkg in obj.trans_with:
++                if pkg.pkgtup in filt:
++                    continue
++                filt.add(pkg.pkgtup)
++                ret.append(pkg)
++        return sorted(ret)
++
++    # This is the real tricky bit, we want to "merge" so that:
++    #     pkgA-1 => pkgA-2
++    #     pkgA-2 => pkgA-3
++    #     pkgB-1 => pkgB-2
++    #     pkgB-2 => pkgB-1
++    # ...becomes:
++    #     pkgA-1 => pkgA-3
++    #     pkgB-1 => pkgB-1 (reinstall)
++    # ...note that we just give up if "impossible" things happen, Eg.
++    #     pkgA-1 => pkgA-2
++    #     pkgA-4 => pkgA-5
++    @staticmethod
++    def _p2sk(pkg, state=None):
++        """ Take a pkg and return the key for it's state lookup. """
++        if state is None:
++            state = pkg.state
++        #  Arch is needed so multilib. works, dito. getBaseArch() -- (so .i586
++        # => .i686 moves are seen)
++        return (pkg.name, getBaseArch(pkg.arch), state)
++
++    @staticmethod
++    def _list2dict(pkgs):
++        pkgtup2pkg   = {}
++        pkgstate2pkg = {}
++        for pkg in pkgs:
++            key = YumMergedHistoryTransaction._p2sk(pkg)
++            pkgtup2pkg[pkg.pkgtup] = pkg
++            pkgstate2pkg[key]      = pkg
++        return pkgtup2pkg, pkgstate2pkg
++    @staticmethod
++    def _conv_pkg_state(pkg, state):
++        npkg = YumHistoryPackageState(pkg.name, pkg.arch,
++                                      pkg.epoch,pkg.version,pkg.release, state)
++        npkg._checksums = pkg._checksums
++        npkg.done = pkg.done
++        if _sttxt2stcode[npkg.state] in TS_INSTALL_STATES:
++            npkg.state_installed = True
++        if _sttxt2stcode[npkg.state] in TS_REMOVE_STATES:
++            npkg.state_installed = False
++        return npkg
++    @staticmethod
++    def _get_pkg(sk, pkgstate2pkg):
++        if type(sk) != type((0,1)):
++            sk = YumMergedHistoryTransaction._p2sk(sk)
++        if sk not in pkgstate2pkg:
++            return None
++        return pkgstate2pkg[sk]
++    def _move_pkg(self, sk, nstate, pkgtup2pkg, pkgstate2pkg):
++        xpkg = self._get_pkg(sk, pkgstate2pkg)
++        if xpkg is None:
++            return
++        del pkgstate2pkg[self._p2sk(xpkg)]
++        xpkg = self._conv_pkg_state(xpkg, nstate)
++        pkgtup2pkg[xpkg.pkgtup] = xpkg
++        pkgstate2pkg[self._p2sk(xpkg)] = xpkg
++
++    def _getTransData(self):
++        def _get_pkg_f(sk):
++            return self._get_pkg(sk, fpkgstate2pkg)
++        def _get_pkg_n(sk):
++            return self._get_pkg(sk, npkgstate2pkg)
++        def _move_pkg_f(sk, nstate):
++            self._move_pkg(sk, nstate, fpkgtup2pkg, fpkgstate2pkg)
++        def _move_pkg_n(sk, nstate):
++            self._move_pkg(sk, nstate, npkgtup2pkg, npkgstate2pkg)
++        def _del1_n(pkg):
++            del npkgtup2pkg[pkg.pkgtup]
++            key = self._p2sk(pkg)
++            if key in npkgstate2pkg: # For broken rpmdbv's and installonly
++                del npkgstate2pkg[key]
++        def _del1_f(pkg):
++            del fpkgtup2pkg[pkg.pkgtup]
++            key = self._p2sk(pkg)
++            if key in fpkgstate2pkg: # For broken rpmdbv's and installonly
++                del fpkgstate2pkg[key]
++        def _del2(fpkg, npkg):
++            assert fpkg.pkgtup == npkg.pkgtup
++            _del1_f(fpkg)
++            _del1_n(npkg)
++        fpkgtup2pkg   = {}
++        fpkgstate2pkg = {}
++        #  We need to go from oldest to newest here, so we can see what happened
++        # in the correct chronological order.
++        for obj in self._merged_objs:
++            npkgtup2pkg, npkgstate2pkg = self._list2dict(obj.trans_data)
++
++            # Handle Erase => Install, as update/reinstall/downgrade
++            for key in list(fpkgstate2pkg.keys()):
++                (name, arch, state) = key
++                if state not in  ('Obsoleted', 'Erase'):
++                    continue
++                fpkg = fpkgstate2pkg[key]
++                for xstate in ('Install', 'True-Install', 'Dep-Install',
++                               'Obsoleting'):
++                    npkg = _get_pkg_n(self._p2sk(fpkg, xstate))
++                    if npkg is not None:
++                        break
++                else:
++                    continue
++
++                if False: pass
++                elif fpkg > npkg:
++                    _move_pkg_f(fpkg, 'Downgraded')
++                    if xstate != 'Obsoleting':
++                        _move_pkg_n(npkg, 'Downgrade')
++                elif fpkg < npkg:
++                    _move_pkg_f(fpkg, 'Updated')
++                    if xstate != 'Obsoleting':
++                        _move_pkg_n(npkg, 'Update')
++                else:
++                    _del1_f(fpkg)
++                    if xstate != 'Obsoleting':
++                        _move_pkg_n(npkg, 'Reinstall')
++
++            sametups = set(npkgtup2pkg.keys()).intersection(fpkgtup2pkg.keys())
++            for pkgtup in sametups:
++                if pkgtup not in fpkgtup2pkg or pkgtup not in npkgtup2pkg:
++                    continue
++                fpkg = fpkgtup2pkg[pkgtup]
++                npkg = npkgtup2pkg[pkgtup]
++                if False: pass
++                elif fpkg.state == 'Reinstall':
++                    if npkg.state in ('Reinstall', 'Erase', 'Obsoleted',
++                                      'Downgraded', 'Updated'):
++                        _del1_f(fpkg)
++                elif fpkg.state in ('Obsoleted', 'Erase'):
++                    #  Should be covered by above loop which deals with
++                    # all goood state changes.
++                    good_states = ('Install', 'True-Install', 'Dep-Install',
++                                   'Obsoleting')
++                    assert npkg.state not in good_states
++
++                elif fpkg.state in ('Install', 'True-Install', 'Dep-Install'):
++                    if False: pass
++                    elif npkg.state in ('Erase', 'Obsoleted'):
++                        _del2(fpkg, npkg)
++                    elif npkg.state == 'Updated':
++                        _del2(fpkg, npkg)
++                        #  Move '*Install' state along to newer pkg. (not for
++                        # obsoletes).
++                        _move_pkg_n(self._p2sk(fpkg, 'Update'), fpkg.state)
++                    elif npkg.state == 'Downgraded':
++                        _del2(fpkg, npkg)
++                        #  Move '*Install' state along to newer pkg. (not for
++                        # obsoletes).
++                        _move_pkg_n(self._p2sk(fpkg, 'Downgrade'), fpkg.state)
++
++                elif fpkg.state in ('Downgrade', 'Update', 'Obsoleting'):
++                    if False: pass
++                    elif npkg.state == 'Reinstall':
++                        _del1_n(npkg)
++                    elif npkg.state in ('Erase', 'Obsoleted'):
++                        _del2(fpkg, npkg)
++
++                        # Move 'Erase'/'Obsoleted' state to orig. pkg.
++                        _move_pkg_f(self._p2sk(fpkg, 'Updated'),    npkg.state)
++                        _move_pkg_f(self._p2sk(fpkg, 'Downgraded'), npkg.state)
++
++                    elif npkg.state in ('Downgraded', 'Updated'):
++                        xfpkg = _get_pkg_f(self._p2sk(fpkg, 'Updated'))
++                        if xfpkg is None:
++                            xfpkg = _get_pkg_f(self._p2sk(fpkg, 'Downgraded'))
++                        if xfpkg is None:
++                            if fpkg.state != 'Obsoleting':
++                                continue
++                            # Was an Install*/Reinstall with Obsoletes
++                            xfpkg = fpkg
++                        xnpkg = _get_pkg_n(self._p2sk(npkg, 'Update'))
++                        if xnpkg is None:
++                            xnpkg = _get_pkg_n(self._p2sk(npkg, 'Downgrade'))
++                        if xnpkg is None:
++                            xnpkg = _get_pkg_n(self._p2sk(npkg, 'Obsoleting'))
++                        if xnpkg is None:
++                            continue
++
++                        #  Now we have 4 pkgs, f1, f2, n1, n2, and 3 pkgtups
++                        # f2.pkgtup == n1.pkgtup. So we need to find out if
++                        # f1 => n2 is an Update or a Downgrade.
++                        _del2(fpkg, npkg)
++                        if xfpkg == xnpkg:
++                            nfstate = 'Reinstall'
++                            if 'Obsoleting' in (fpkg.state, xnpkg.state):
++                                nfstate = 'Obsoleting'
++                            if xfpkg != fpkg:
++                                _move_pkg_f(xfpkg, nfstate)
++                            _del1_n(xnpkg)
++                        elif xfpkg < xnpkg:
++                            # Update...
++                            nfstate = 'Updated'
++                            nnstate = 'Update'
++                            if 'Obsoleting' in (fpkg.state, xnpkg.state):
++                                nnstate = 'Obsoleting'
++                            if xfpkg != fpkg:
++                                _move_pkg_f(xfpkg, nfstate)
++                            _move_pkg_n(xnpkg, nnstate)
++                        else:
++                            # Downgrade...
++                            nfstate = 'Downgraded'
++                            nnstate = 'Downgrade'
++                            if 'Obsoleting' in (fpkg.state, xnpkg.state):
++                                nnstate = 'Obsoleting'
++                            if xfpkg != fpkg:
++                                _move_pkg_f(xfpkg, nfstate)
++                            _move_pkg_n(xnpkg, nnstate)
++
++            for x in npkgtup2pkg:
++                fpkgtup2pkg[x] = npkgtup2pkg[x]
++            for x in npkgstate2pkg:
++                fpkgstate2pkg[x] = npkgstate2pkg[x]
++        return sorted(fpkgtup2pkg.values())
++
++    def _getProblems(self):
++        probs = set()
++        for tid in self._merged_objs:
++            for prob in tid.rpmdb_problems:
++                probs.add(prob)
++        return sorted(probs)
++
++    def _getCmdline(self):
++        cmdlines = []
++        for tid in self._merged_objs:
++            if not tid.cmdline:
++                continue
++            if cmdlines and cmdlines[-1] == tid.cmdline:
++                continue
++            cmdlines.append(tid.cmdline)
++        if not cmdlines:
++            return None
++        return cmdlines
++
++    def _getErrors(self):
++        ret = []
++        for obj in self._merged_objs:
++            ret.extend(obj.errors)
++        return ret
++    def _getOutput(self):
++        ret = []
++        for obj in self._merged_objs:
++            ret.extend(obj.output)
++        return ret
++
++    def merge(self, obj):
++        if obj.tid in self._merged_tids:
++            return # Already done, signal an error?
++
++        self._merged_tids.add(obj.tid)
++        self._merged_objs.append(obj)
++        # Oldest first...
++        self._merged_objs.sort(reverse=True)
++
++        if self.beg_timestamp > obj.beg_timestamp:
++            self.beg_timestamp    = obj.beg_timestamp
++            self.beg_rpmdbversion = obj.beg_rpmdbversion
++        if self.end_timestamp < obj.end_timestamp:
++            self.end_timestamp    = obj.end_timestamp
++            self.end_rpmdbversion = obj.end_rpmdbversion
++
++
+ class YumHistory:
+     """ API for accessing the history sqlite data. """
+ 
+@@ -425,12 +755,15 @@ class YumHistory:
          cur = self._get_cursor()
          if cur is None or not self._update_db_file_2():
              return None
@@ -28925,7 +30435,7 @@ index 502b908..332a3e9 100644
          rpid = cur.lastrowid
  
          if not rpid:
-@@ -467,7 +473,7 @@ class YumHistory:
+@@ -467,7 +800,7 @@ class YumHistory:
          res = executeSQL(cur,
                           """INSERT INTO trans_cmdline
                           (tid, cmdline)
@@ -28934,7 +30444,7 @@ index 502b908..332a3e9 100644
          return cur.lastrowid
  
      def beg(self, rpmdb_version, using_pkgs, txmbrs, skip_packages=[],
-@@ -610,7 +616,7 @@ class YumHistory:
+@@ -610,7 +943,7 @@ class YumHistory:
              # open file in append
              fo = open(data_fn, 'w+')
              # write data
@@ -28943,7 +30453,19 @@ index 502b908..332a3e9 100644
              # flush data
              fo.flush()
              fo.close()
-@@ -914,17 +920,19 @@ class YumHistory:
+@@ -657,9 +990,9 @@ class YumHistory:
+                       ORDER BY name ASC, epoch ASC, state DESC""", (tid,))
+         ret = []
+         for row in cur:
+-            obj = YumHistoryPackage(row[0],row[1],row[2],row[3],row[4], row[5])
++            obj = YumHistoryPackageState(row[0],row[1],row[2],row[3],row[4],
++                                         row[7], row[5])
+             obj.done     = row[6] == 'TRUE'
+-            obj.state    = row[7]
+             obj.state_installed = None
+             if _sttxt2stcode[obj.state] in TS_INSTALL_STATES:
+                 obj.state_installed = True
+@@ -914,17 +1247,19 @@ class YumHistory:
              version || '-' || release || '.' || arch AS nevra
       FROM trans_skip_pkgs JOIN pkgtups USING(pkgtupid)
       ORDER BY name;
@@ -28969,11 +30491,34 @@ index 502b908..332a3e9 100644
       ORDER BY name;
  ''']
  
+diff --git a/yum/logginglevels.py b/yum/logginglevels.py
+index 9534984..8fae5ab 100644
+--- a/yum/logginglevels.py
++++ b/yum/logginglevels.py
+@@ -1,3 +1,4 @@
++#! /usr/bin/python -tt
+ # This program 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
+diff --git a/yum/mdparser.py b/yum/mdparser.py
+index 194d5d1..8631f06 100644
+--- a/yum/mdparser.py
++++ b/yum/mdparser.py
+@@ -1,3 +1,4 @@
++#! /usr/bin/python -tt
+ 
+ # This program is free software; you can redistribute it and/or modify
+ # it under the terms of the GNU General Public License as published by
 diff --git a/yum/misc.py b/yum/misc.py
-index 0f80d7d..7e2d745 100644
+index 0f80d7d..4fa5ed9 100644
 --- a/yum/misc.py
 +++ b/yum/misc.py
-@@ -35,7 +35,7 @@ except ImportError:
+@@ -1,3 +1,4 @@
++#! /usr/bin/python -tt
+ """
+ Assorted utility functions for yum.
+ """
+@@ -35,7 +36,7 @@ except ImportError:
      gpgme = None
  try:
      import hashlib
@@ -28982,6 +30527,125 @@ index 0f80d7d..7e2d745 100644
      _default_checksums = ['sha256']
  except ImportError:
      # Python-2.4.z ... gah!
+@@ -1035,7 +1036,7 @@ def get_uuid(savepath):
+         
+         return myid
+         
+-def decompress(filename, dest=None, fn_only=False):
++def decompress(filename, dest=None, fn_only=False, check_timestamps=False):
+     """take a filename and decompress it into the same relative location.
+        if the file is not compressed just return the file"""
+     
+@@ -1066,10 +1067,26 @@ def decompress(filename, dest=None, fn_only=False):
+         ztype = None
+     
+     if ztype and not fn_only:
++        if check_timestamps:
++            fi = stat_f(filename)
++            fo = stat_f(out)
++            if fi and fo and fo.st_mtime > fi.st_mtime:
++                return out
++
+         _decompress_chunked(filename, out, ztype)
+         
+     return out
+     
++def repo_gen_decompress(filename, generated_name):
++    """ This is a wrapper around decompress, where we work out a cached
++        generated name, and use check_timestamps. filename _must_ be from
++        a repo. and generated_name is the type of the file. """
++    dest = os.path.dirname(filename)
++    dest += '/gen'
++    if not os.path.exists(dest):
++        os.makedirs(dest, mode=0755)
++    dest += '/' + generated_name
++    return decompress(filename, dest=dest, check_timestamps=True)
+     
+ def read_in_items_from_dot_dir(thisglob, line_as_list=True):
+     """takes a glob of a dir (like /etc/foo.d/*.foo)
+diff --git a/yum/packageSack.py b/yum/packageSack.py
+index d822394..153edbb 100644
+--- a/yum/packageSack.py
++++ b/yum/packageSack.py
+@@ -24,6 +24,7 @@ import re
+ import fnmatch
+ import misc
+ from packages import parsePackages
++from rpmUtils.miscutils import compareEVR
+ 
+ class PackageSackVersion:
+     def __init__(self):
+@@ -98,7 +99,7 @@ class PackageSackBase(object):
+         """return list of pkgobjects matching the nevra requested"""
+         raise NotImplementedError()
+ 
+-    def searchNames(self, names=[]):
++    def searchNames(self, names=[], return_pkgtups=False):
+         raise NotImplementedError()
+ 
+     def searchPO(self, po):
+@@ -405,8 +406,8 @@ class MetaSack(PackageSackBase):
+         """return list of pkgobjects matching the nevra requested"""
+         return self._computeAggregateListResult("searchNevra", name, epoch, ver, rel, arch)
+ 
+-    def searchNames(self, names=[]):
+-        return self._computeAggregateListResult("searchNames", names)
++    def searchNames(self, names=[], return_pkgtups=False):
++        return self._computeAggregateListResult("searchNames", names, return_pkgtups)
+ 
+     def getProvides(self, name, flags=None, version=(None, None, None)):
+         """return dict { packages -> list of matching provides }"""
+@@ -443,13 +444,26 @@ class MetaSack(PackageSackBase):
+         nobsdict = {}
+         last_name = ''
+         last_pkg = None
+-        for pkg in reversed(sorted(self.searchNames(names))):
+-            if last_name == pkg.name and not pkg.verEQ(last_pkg):
++        #  It takes about 0.2 of a second to convert these into packages, just
++        # so we can sort them, which is ~40% of this functions time. So we sort
++        # the pkgtups "by hand".
++        def _pkgtup_nevr_cmp(x, y):
++            """ Compare two pkgtup's (ignore arch): n, a, e, v, r. """
++            ret = cmp(x[0], y[0])
++            if ret: return ret
++            # This is negated so we get higher versions first, in the list.
++            return -compareEVR((x[2], x[3], x[4]), (y[2], y[3], y[4]))
++        def _pkgtup_nevr_eq(x, y):
++            return _pkgtup_nevr_cmp(x, y) == 0
++        for pkgtup in sorted(self.searchNames(names, return_pkgtups=True),
++                             cmp=_pkgtup_nevr_cmp):
++            name = pkgtup[0]
++            if last_name == name and not _pkgtup_nevr_eq(last_pkgtup, pkgtup):
+                 continue
+-            last_name = pkg.name
+-            last_pkg = pkg
+-            if pkg.pkgtup in obsdict:
+-                nobsdict[pkg.pkgtup] = obsdict[pkg.pkgtup]
++            last_name = name
++            last_pkgtup = pkgtup
++            if pkgtup in obsdict:
++                nobsdict[pkgtup] = obsdict[pkgtup]
+         return nobsdict
+         
+     def searchFiles(self, name):
+@@ -671,7 +685,7 @@ class PackageSack(PackageSackBase):
+             result.append(po)
+         return result
+         
+-    def searchNames(self, names=[]):
++    def searchNames(self, names=[], return_pkgtups=False):
+         """return list of pkgobjects matching the names requested"""
+         self._checkIndexes(failure='build')
+         result = []
+@@ -681,6 +695,8 @@ class PackageSack(PackageSackBase):
+                 continue
+             done.add(name)
+             result.extend(self.nevra.get((name, None, None, None, None), []))
++        if return_pkgtups:
++            return [pkg.pkgtup for pkg in result]
+         return result
+ 
+     def getProvides(self, name, flags=None, version=(None, None, None)):
 diff --git a/yum/packages.py b/yum/packages.py
 index b5a7d40..38f305c 100644
 --- a/yum/packages.py
@@ -29022,11 +30686,25 @@ index b5a7d40..38f305c 100644
  
          return hdr
  
+diff --git a/yum/parser.py b/yum/parser.py
+index dd45d6d..e46d611 100644
+--- a/yum/parser.py
++++ b/yum/parser.py
+@@ -1,3 +1,4 @@
++#! /usr/bin/python -tt
+ import re
+ import urlparse
+ import urlgrabber
 diff --git a/yum/pgpmsg.py b/yum/pgpmsg.py
-index 9cf8217..b23eabc 100644
+index 9cf8217..ee825c6 100644
 --- a/yum/pgpmsg.py
 +++ b/yum/pgpmsg.py
-@@ -1186,7 +1186,7 @@ def decode(msg) :
+@@ -1,3 +1,4 @@
++#! /usr/bin/python -tt
+ ##Copyright (C) 2003,2005,2009  Jens B. Jorgensen <jbj1 at ultraemail.net>
+ ##
+ ##This program is free software; you can redistribute it and/or
+@@ -1186,7 +1187,7 @@ def decode(msg) :
          idx = idx + pkt_len
      return pkt_list
  
@@ -29035,7 +30713,7 @@ index 9cf8217..b23eabc 100644
      """decode_msg(msg) ==> list of OpenPGP "packet" objects
  Takes an ascii-armored PGP block and returns a list of objects each of which
  corresponds to a PGP "packets".
-@@ -1242,11 +1242,17 @@ a PGP "certificate" includes a public key, user id(s), and signature.
+@@ -1242,11 +1243,17 @@ a PGP "certificate" includes a public key, user id(s), and signature.
                  pkt_idx = cert.load(pkt_list)
                  cert_list.append(cert)
                  pkt_list[0:pkt_idx] = []
@@ -29053,7 +30731,7 @@ index 9cf8217..b23eabc 100644
      return []
  
  def decode_multiple_keys(msg):
-@@ -1266,7 +1272,7 @@ def decode_multiple_keys(msg):
+@@ -1266,7 +1273,7 @@ def decode_multiple_keys(msg):
          block += '%s\n' % l
          if l == '-----END PGP PUBLIC KEY BLOCK-----':
              in_block = 0
@@ -29062,21 +30740,236 @@ index 9cf8217..b23eabc 100644
              if thesecerts:
                  certs.extend(thesecerts)
              block = ''
+diff --git a/yum/plugins.py b/yum/plugins.py
+index 9cd2040..bfc49b7 100644
+--- a/yum/plugins.py
++++ b/yum/plugins.py
+@@ -1,3 +1,4 @@
++#! /usr/bin/python -tt
+ # This program 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
+diff --git a/yum/rpmsack.py b/yum/rpmsack.py
+index ae73c32..52e771b 100644
+--- a/yum/rpmsack.py
++++ b/yum/rpmsack.py
+@@ -195,7 +195,9 @@ class RPMDBPackageSack(PackageSackBase):
+             }
+         
+         addldb_path = os.path.normpath(self._persistdir + '/yumdb')
+-        self.yumdb = RPMDBAdditionalData(db_path=addldb_path)
++        version_path = os.path.normpath(cachedir + '/version')
++        self.yumdb = RPMDBAdditionalData(db_path=addldb_path,
++                                         version_path=version_path)
+ 
+     def _get_pkglist(self):
+         '''Getter for the pkglist property. 
+@@ -210,6 +212,10 @@ class RPMDBPackageSack(PackageSackBase):
+     pkglist = property(_get_pkglist, None)
+ 
+     def dropCachedData(self):
++        """ Drop all cached data, this is a big perf. hit if we need to load
++            the data back in again. Also note that if we ever call this while
++            a transaction is ongoing we'll have multiple copies of packages
++            which is _bad_. """
+         self._idx2pkg = {}
+         self._name2pkg = {}
+         self._pkgnames_loaded = set()
+@@ -236,6 +242,73 @@ class RPMDBPackageSack(PackageSackBase):
+         self.transactionReset() # Should do nothing, but meh...
+         self._cached_rpmdb_mtime = None
+ 
++    def dropCachedDataPostTransaction(self, txmbrs):
++        """ Drop cached data that is assocciated with the given transaction,
++            this tries to keep as much data as possible and even does a
++            "preload" on the checksums. This should be called once, when a
++            transaction is complete. """
++        # -- Below -- self._idx2pkg = {}
++        # -- Below -- self._name2pkg = {}
++        # -- Below -- self._pkgnames_loaded = set()
++        # -- Below -- self._tup2pkg = {}
++        self._completely_loaded = False
++        self._pkgmatch_fails = set()
++        # -- Below -- self._pkgname_fails = set()
++        self._provmatch_fails = set()
++        self._simple_pkgtup_list = []
++        self._get_pro_cache = {}
++        self._get_req_cache = {}
++        #  We can be called on python shutdown (due to yb.__del__), at which
++        # point other modules might not be available.
++        if misc is not None:
++            misc.unshare_data()
++        self._cache = {
++            'provides' : { },
++            'requires' : { },
++            'conflicts' : { },
++            'obsoletes' : { },
++            }
++        self._have_cached_rpmdbv_data = None
++        self._cached_conflicts_data = None
++        self.transactionReset() # Should do nothing, but meh...
++
++        #  We are keeping some data from before, and sometimes (Eg. remove only)
++        # we never open the rpmdb again ... so get the mtime now.
++        rpmdbfname  = self.root + "/var/lib/rpm/Packages"
++        self._cached_rpmdb_mtime = os.path.getmtime(rpmdbfname)
++
++        precache = []
++        for txmbr in txmbrs:
++            self._pkgnames_loaded.discard(txmbr.name)
++            if txmbr.name in self._name2pkg:
++                del self._name2pkg[txmbr.name]
++
++            if txmbr.output_state in constants.TS_INSTALL_STATES:
++                self._pkgname_fails.discard(txmbr.name)
++                precache.append(txmbr)
++            if txmbr.output_state in constants.TS_REMOVE_STATES:
++                del self._idx2pkg[txmbr.po.idx]
++                del self._tup2pkg[txmbr.pkgtup]
++
++        for txmbr in precache:
++            (n, a, e, v, r) = txmbr.pkgtup
++            pkg = self.searchNevra(n, e, v, r, a)
++            if not pkg:
++                # Wibble?
++                self._deal_with_bad_rpmdbcache("dCDPT(pkg checksums)")
++
++            pkg = pkg[0]
++            csum = txmbr.po.returnIdSum()
++            if csum is None:
++                continue
++
++            (T, D) = (str(csum[0]), str(csum[1]))
++            if ('checksum_type' in pkg.yumdb_info._read_cached_data or
++                'checksum_data' in pkg.yumdb_info._read_cached_data):
++                continue
++            pkg.yumdb_info._read_cached_data['checksum_type'] = T
++            pkg.yumdb_info._read_cached_data['checksum_data'] = D
++
+     def setCacheDir(self, cachedir):
+         """ Sets the internal cachedir value for the rpmdb, to be the
+             "rpmdb-indexes" directory in the persisent yum storage. """
+@@ -244,6 +317,10 @@ class RPMDBPackageSack(PackageSackBase):
+         else:
+             self._cachedir = '/' + cachedir
+ 
++        if hasattr(self, 'yumdb'): # Need to keep this upto date, after init.
++            version_path = os.path.normpath(self._cachedir + '/version')
++            self.yumdb.conf.version_path = version_path
++
+     def readOnlyTS(self):
+         if not self.ts:
+             self.ts =  initReadOnlyTransaction(root=self.root)
+@@ -1422,9 +1499,10 @@ class RPMDBAdditionalData(object):
+     # dirs have files per piece of info we're keeping
+     #    repoid, install reason, status, blah, (group installed for?), notes?
+     
+-    def __init__(self, db_path='/var/lib/yum/yumdb'):
++    def __init__(self, db_path='/var/lib/yum/yumdb', version_path=None):
+         self.conf = misc.GenericHolder()
+         self.conf.db_path = db_path
++        self.conf.version_path = version_path
+         self.conf.writable = False
+         
+         self._packages = {} # pkgid = dir
+@@ -1566,7 +1644,6 @@ class RPMDBAdditionalDataPackage(object):
+ 
+         self._yumdb_cache['attr'][value][2].add(fn)
+         self._yumdb_cache[fn] = value
+-        self._read_cached_data['attr'] = value
+ 
+         return True
+ 
+@@ -1587,6 +1664,11 @@ class RPMDBAdditionalDataPackage(object):
+         if attr.endswith('.tmp'):
+             raise AttributeError, "Cannot set attribute %s on %s" % (attr, self)
+ 
++        #  These two are special, as they have an index and are used as our
++        # cache-breaker.
++        if attr in ('checksum_type', 'checksum_data'):
++            misc.unlink_f(self._conf.version_path)
++
+         # Auto hardlink some of the attrs...
+         if self._link_yumdb_cache(fn, value):
+             return
+diff --git a/yum/sqlitesack.py b/yum/sqlitesack.py
+index 820f003..fb8333f 100644
+--- a/yum/sqlitesack.py
++++ b/yum/sqlitesack.py
+@@ -1263,7 +1263,7 @@ class YumSqlitePackageSack(yumRepo.YumPackageSack):
+         return self._search("requires", name, flags, version)
+ 
+     @catchSqliteException
+-    def searchNames(self, names=[]):
++    def searchNames(self, names=[], return_pkgtups=False):
+         """return a list of packages matching any of the given names. This is 
+            only a match on package name, nothing else"""
+         
+@@ -1283,6 +1283,8 @@ class YumSqlitePackageSack(yumRepo.YumPackageSack):
+             else:
+                 names.append(pkgname)
+ 
++        if return_pkgtups:
++            returnList = [pkg.pkgtup for pkg in returnList]
+         if not names:
+             return returnList
+ 
+@@ -1290,7 +1292,7 @@ class YumSqlitePackageSack(yumRepo.YumPackageSack):
+         if len(names) > max_entries:
+             # Unique is done at user_names time, above.
+             for names in seq_max_split(names, max_entries):
+-                returnList.extend(self.searchNames(names))
++                returnList.extend(self.searchNames(names, return_pkgtups))
+             return returnList
+ 
+         pat_sqls = []
+@@ -1304,10 +1306,19 @@ class YumSqlitePackageSack(yumRepo.YumPackageSack):
+             cur = cache.cursor()
+             executeSQL(cur, qsql, names)
+ 
++            if return_pkgtups:
++                for ob in cur:
++                    pkgtup = self._pkgtupByKeyData(repo, ob['pkgKey'], ob)
++                    if pkgtup is None:
++                        continue
++                    returnList.append(pkgtup)
++                continue
++
+             self._sql_pkgKey2po(repo, cur, returnList, have_data=True)
+ 
+-        # Mark all the processed pkgnames as fully loaded
+-        self._pkgnames_loaded.update([name for name in names])
++        if not return_pkgtups:
++            # Mark all the processed pkgnames as fully loaded
++            self._pkgnames_loaded.update([name for name in names])
+ 
+         return returnList
+  
+diff --git a/yum/transactioninfo.py b/yum/transactioninfo.py
+index 31d3569..f34a45e 100644
+--- a/yum/transactioninfo.py
++++ b/yum/transactioninfo.py
+@@ -1,3 +1,4 @@
++#! /usr/bin/python -tt
+ # This program 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
 diff --git a/yum/update_md.py b/yum/update_md.py
-index 9e492ba..3d05d19 100644
+index 9e492ba..74db5ad 100644
 --- a/yum/update_md.py
 +++ b/yum/update_md.py
-@@ -56,6 +56,9 @@ class UpdateNotice(object):
+@@ -56,6 +56,10 @@ class UpdateNotice(object):
              'issued'           : '',
              'updated'          : '',
              'description'      : '',
 +            'rights'           : '',
++            'severity'         : '',
 +            'summary'          : '',
 +            'solution'         : '',
              'references'       : [],
              'pkglist'          : [],
              'reboot_suggested' : False
-@@ -71,7 +74,7 @@ class UpdateNotice(object):
+@@ -71,7 +75,7 @@ class UpdateNotice(object):
      def __setitem__(self, item, val):
          self._md[item] = val
  
@@ -29085,7 +30978,7 @@ index 9e492ba..3d05d19 100644
          head = """
  ===============================================================================
    %(title)s
-@@ -88,7 +91,7 @@ class UpdateNotice(object):
+@@ -88,7 +92,7 @@ class UpdateNotice(object):
  
          # Add our bugzilla references
          bzs = filter(lambda r: r['type'] == 'bugzilla', self._md['references'])
@@ -29094,7 +30987,7 @@ index 9e492ba..3d05d19 100644
              buglist = "       Bugs :"
              for bz in bzs:
                  buglist += " %s%s\n\t    :" % (bz['id'], 'title' in bz
-@@ -97,17 +100,35 @@ class UpdateNotice(object):
+@@ -97,17 +101,40 @@ class UpdateNotice(object):
  
          # Add our CVE references
          cves = filter(lambda r: r['type'] == 'cve', self._md['references'])
@@ -29126,13 +31019,18 @@ index 9e492ba..3d05d19 100644
 +                                  subsequent_indent=' ' * 12 + ': ')
 +            head += "     Rights : %s\n" % '\n'.join(data)
 +
++        if self._md['severity'] and 'severity' not in skip_data:
++            data = utf8_text_wrap(self._md['severity'], width=64,
++                                  subsequent_indent=' ' * 12 + ': ')
++            head += "   Severity : %s\n" % '\n'.join(data)
++
 +        if 'files' in skip_data:
 +            return head[:-1] # chop the last '\n'
 +
          #  Get a list of arches we care about:
          #XXX ARCH CHANGE - what happens here if we set the arch - we need to
          # pass this in, perhaps
-@@ -123,6 +144,9 @@ class UpdateNotice(object):
+@@ -123,6 +150,9 @@ class UpdateNotice(object):
  
          return head
  
@@ -29142,21 +31040,24 @@ index 9e492ba..3d05d19 100644
      def get_metadata(self):
          """ Return the metadata dict. """
          return self._md
-@@ -132,7 +156,7 @@ class UpdateNotice(object):
+@@ -132,7 +162,8 @@ class UpdateNotice(object):
          Parse an update element::
  
              <!ELEMENT update (id, synopsis?, issued, updated,
 -                              references, description, pkglist)>
-+                              references, description, rights?, summary?, solution?, pkglist)>
++                              references, description, rights?,
++                              severity?, summary?, solution?, pkglist)>
                  <!ATTLIST update type (errata|security) "errata">
                  <!ATTLIST update status (final|testing) "final">
                  <!ATTLIST update version CDATA #REQUIRED>
-@@ -156,6 +180,12 @@ class UpdateNotice(object):
+@@ -156,6 +187,14 @@ class UpdateNotice(object):
                      self._parse_references(child)
                  elif child.tag == 'description':
                      self._md['description'] = child.text
 +                elif child.tag == 'rights':
 +                    self._md['rights'] = child.text
++                elif child.tag == 'severity':
++                    self._md[child.tag] = child.text
 +                elif child.tag == 'summary':
 +                    self._md['summary'] = child.text
 +                elif child.tag == 'solution':
@@ -29164,7 +31065,7 @@ index 9e492ba..3d05d19 100644
                  elif child.tag == 'pkglist':
                      self._parse_pkglist(child)
                  elif child.tag == 'title':
-@@ -172,7 +202,7 @@ class UpdateNotice(object):
+@@ -172,7 +211,7 @@ class UpdateNotice(object):
              <!ELEMENT references (reference*)>
              <!ELEMENT reference>
                  <!ATTLIST reference href CDATA #REQUIRED>
@@ -29173,7 +31074,7 @@ index 9e492ba..3d05d19 100644
                  <!ATTLIST reference id CDATA #IMPLIED>
                  <!ATTLIST reference title CDATA #IMPLIED>
          """
-@@ -225,6 +255,12 @@ class UpdateNotice(object):
+@@ -225,6 +264,12 @@ class UpdateNotice(object):
          package = {}
          for pkgfield in ('arch', 'epoch', 'name', 'version', 'release', 'src'):
              package[pkgfield] = elem.attrib.get(pkgfield)
@@ -29186,7 +31087,7 @@ index 9e492ba..3d05d19 100644
          for child in elem:
              if child.tag == 'filename':
                  package['filename'] = child.text
-@@ -248,7 +284,13 @@ class UpdateNotice(object):
+@@ -248,7 +293,16 @@ class UpdateNotice(object):
                  to_xml(self._md['title']), to_xml(self._md['release']),
                  to_xml(self._md['issued'], attrib=True),
                  to_xml(self._md['description']))
@@ -29198,14 +31099,22 @@ index 9e492ba..3d05d19 100644
 +            msg += """  <solution>%s</solution>\n""" % (to_xml(self._md['solution']))
 +        if self._md['rights']:
 +            msg += """  <rights>%s</rights>\n""" % (to_xml(self._md['rights']))        
++        if self._md['severity']:
++            msg += """  <severity>%s</severity>\n""" % (to_xml(self._md['severity']))
++
          if self._md['references']:
              msg += """  <references>\n"""
              for ref in self._md['references']:
 diff --git a/yum/yumRepo.py b/yum/yumRepo.py
-index dd595f0..4016ce5 100644
+index dd595f0..b0e23c6 100644
 --- a/yum/yumRepo.py
 +++ b/yum/yumRepo.py
-@@ -501,6 +501,8 @@ class YumRepository(Repository, config.RepoConf):
+@@ -1,3 +1,4 @@
++#! /usr/bin/python -tt
+ # This program 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
+@@ -501,6 +502,8 @@ class YumRepository(Repository, config.RepoConf):
                   'ssl_cert': self.sslclientcert,
                   'ssl_key': self.sslclientkey,
                   'user_agent': default_grabber.opts.user_agent,
@@ -29214,7 +31123,7 @@ index dd595f0..4016ce5 100644
                   }
          return opts
  
-@@ -1117,6 +1119,7 @@ class YumRepository(Repository, config.RepoConf):
+@@ -1117,6 +1120,7 @@ class YumRepository(Repository, config.RepoConf):
          if repoXML.length != repomd.size:
              return False
  
@@ -29222,7 +31131,7 @@ index dd595f0..4016ce5 100644
          for checksum in repoXML.checksums:
              if checksum not in repomd.chksums:
                  continue
-@@ -1124,11 +1127,11 @@ class YumRepository(Repository, config.RepoConf):
+@@ -1124,11 +1128,11 @@ class YumRepository(Repository, config.RepoConf):
              if repoXML.checksums[checksum] != repomd.chksums[checksum]:
                  return False
  
@@ -29238,7 +31147,7 @@ index dd595f0..4016ce5 100644
  
      def _checkRepoMetalink(self, repoXML=None, metalink_data=None):
          """ Check the repomd.xml against the metalink data, if we have it. """
-@@ -1312,6 +1315,16 @@ class YumRepository(Repository, config.RepoConf):
+@@ -1312,6 +1316,16 @@ class YumRepository(Repository, config.RepoConf):
                      os.rename(local, local + '.old.tmp')
                      reverts.append(local)
  
diff --git a/yum.spec b/yum.spec
index f08f90d..9b26e2b 100644
--- a/yum.spec
+++ b/yum.spec
@@ -5,7 +5,7 @@
 Summary: RPM installer/updater
 Name: yum
 Version: 3.2.28
-Release: 7%{?dist}
+Release: 8%{?dist}
 License: GPLv2+
 Group: System Environment/Base
 Source0: http://yum.baseurl.org/download/3.2/%{name}-%{version}.tar.gz
@@ -81,6 +81,9 @@ can notify you when they are available via email, syslog or dbus.
 %build
 make
 
+%check
+make check
+
 %install
 rm -rf $RPM_BUILD_ROOT
 make DESTDIR=$RPM_BUILD_ROOT install
@@ -144,6 +147,12 @@ rm -rf $RPM_BUILD_ROOT
 %dir %{yum_pluginslib}
 
 %changelog
+* Sat Sep 25 2010 James Antill <james at fedoraproject.org> - 3.2.28-8
+- latest head 
+- Speedup install/remove/etc a lot.
+- Add merged history.
+- Fix unique comps/pkgtags leftovers.
+
 * Tue Sep 14 2010 James Antill <james at fedoraproject.org> - 3.2.28-7
 - latest head 
 - Fix PK/auto-close GPG import bug.


More information about the scm-commits mailing list