[yum-commits] Branch 'yum-3_2_X' - 8 commits - docs/yum.8 output.py test/merge-history-transactions-tests.py yum/history.py yum/__init__.py yum/rpmsack.py

James Antill james at osuosl.org
Sat Sep 25 15:07:08 UTC 2010


 docs/yum.8                               |    7 
 output.py                                |  140 ++++-
 test/merge-history-transactions-tests.py |  792 +++++++++++++++++++++++++++++++
 yum/__init__.py                          |   18 
 yum/history.py                           |  333 ++++++++++++-
 yum/rpmsack.py                           |   88 +++
 6 files changed, 1345 insertions(+), 33 deletions(-)

New commits:
commit 4f913552a7e5f73bb451ccb5c0c27ba8835879b7
Author: James Antill <james at and.org>
Date:   Fri Sep 24 13:52:31 2010 -0400

    Solve more merge problems with the merged transaction branch

diff --git a/output.py b/output.py
index 2d25e62..cfca45f 100755
--- a/output.py
+++ b/output.py
@@ -1681,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:
diff --git a/yum/history.py b/yum/history.py
index 6bcab99..a8004af 100644
--- a/yum/history.py
+++ b/yum/history.py
@@ -249,6 +249,15 @@ class YumMergedHistoryTransaction(YumHistoryTransaction):
 
         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
@@ -498,6 +507,25 @@ class YumMergedHistoryTransaction(YumHistoryTransaction):
                 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:
commit 99b7058b0ab73e1eb339b93106ee84e1fd3f25eb
Merge: d68af7e... 15d4562...
Author: James Antill <james at and.org>
Date:   Fri Sep 24 10:16:21 2010 -0400

    Fix conflicts in merged-transaction git merge.

diff --cc docs/yum.8
index 5586b24,4158453..b5b7295
--- a/docs/yum.8
+++ b/docs/yum.8
@@@ -308,37 -244,17 +308,44 @@@ transactions (assuming the history_reco
  info/list/summary to view what happened, undo/redo to act on that information
  and new to start a new history file.
  
 -The info/list/summary commands take either a transactions id or a package (with
 +The info/list/summary commands take either a transaction id or a package (with
  wildcards, as in \fBSpecifying package names\fP), all three can also be passed
  no arguments. list can be passed the keyword "all" to list all the transactions.
 -undo/redo just take a transaction id\&.
 +
 +The undo/redo commands take either a transaction id or the keyword last and
 +an offset from the last transaction (Eg. if you've done 250 transactions,
 +"last" refers to transaction 250, and "last-4" refers to transaction 246).
 +
 +In "history list" output the Altered column also gives some extra information
 +if there was something not good with the transaction.
 +
 +.I \fB>\fR - The rpmdb was changed, outside yum, after the transaction.
 +.br
 +.I \fB<\fR - The rpmdb was changed, outside yum, before the transaction.
 +.br
 +.I \fB*\fR - The transaction aborted before completion.
 +.br
 +.I \fB#\fR - The transaction completed, but with a non-zero status.
 +.br
 +.I \fBE\fR - The transaction completed fine, but had warning/error output during the transaction.
 +.br
 +.I \fBP\fR - The transaction completed fine, but problems already existed in the rpmdb.
 +.br
 +.I \fBs\fR - The transaction completed fine, but --skip-broken was enabled and had to skip some packages.
 +.br
 +
 +.IP
 +.IP "\fBcheck\fP"
 +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
diff --cc output.py
index ec4bd45,56343c9..2d25e62
--- a/output.py
+++ b/output.py
@@@ -1317,9 -1200,11 +1319,12 @@@ 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 on init.
 -        if uid is None or uid == 0xFFFFFFFF:
 +        # 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):
              loginid = _("<unset>")
              name = _("System") + " " + loginid
              if limit is not None and len(name) > limit:
@@@ -1471,21 -1322,58 +1476,60 @@@
          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:
 -            tids.add(old.tid)
 -            utids.add(old.tid)
 +            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'))
@@@ -1514,42 -1431,10 +1587,45 @@@
      def _historyInfoCmd(self, old, pats=[]):
          name = self._pwd_ui_username(old.loginuid)
  
 +        _pkg_states_installed = {'i' : _('Installed'), 'e' : _('Erased'),
 +                                 'o' : _('Updated'), 'n' : _('Downgraded')}
 +        _pkg_states_available = {'i' : _('Installed'), 'e' : _('Not installed'),
 +                                 'o' : _('Older'), 'n' : _('Newer')}
 +        # max() only in 2.5.z
 +        maxlen = sorted([len(x) for x in (_pkg_states_installed.values() +
 +                                          _pkg_states_available.values())])[-1]
 +        _pkg_states_installed['maxlen'] = maxlen
 +        _pkg_states_available['maxlen'] = maxlen
 +        def _simple_pkg(pkg, prefix_len, was_installed=False, highlight=False):
 +            prefix = " " * prefix_len
 +            if was_installed:
 +                _pkg_states = _pkg_states_installed
 +            else:
 +                _pkg_states = _pkg_states_available
 +            state  = _pkg_states['i']
 +            ipkgs = self.rpmdb.searchNames([hpkg.name])
 +            ipkgs.sort()
 +            if not ipkgs:
 +                state  = _pkg_states['e']
 +            elif hpkg.pkgtup in (ipkg.pkgtup for ipkg in ipkgs):
 +                pass
 +            elif ipkgs[-1] > hpkg:
 +                state  = _pkg_states['o']
 +            elif ipkgs[0] < hpkg:
 +                state  = _pkg_states['n']
 +            else:
 +                assert False, "Impossible, installed not newer and not older"
 +            if highlight:
 +                (hibeg, hiend) = self._highlight('bold')
 +            else:
 +                (hibeg, hiend) = self._highlight('normal')
 +            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:
diff --cc yum/history.py
index 332a3e9,f1c287e..6bcab99
--- a/yum/history.py
+++ b/yum/history.py
@@@ -27,8 -27,9 +27,9 @@@ import yum.misc as mis
  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'
  
@@@ -111,39 -136,16 +112,48 @@@ class YumHistoryPackage(PackageObject)
          else:
              chk = checksum.split(':')
              self._checksums = [(chk[0], chk[1], 0)] # (type, checksum, id(0,1))
+         # 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. """
 +
 +    def __init__(self, history, rpid, problem, text):
 +        self._history = weakref(history)
 +
 +        self.rpid = rpid
 +        self.problem = problem
 +        self.text = text
 +
 +        self._loaded_P = None
 +
 +    def __cmp__(self, other):
 +        if other is None:
 +            return 1
 +        ret = cmp(self.problem, other.problem)
 +        if ret: return -ret
 +        ret = cmp(self.rpid, other.rpid)
 +        return ret
 +
 +    def _getProbPkgs(self):
 +        if self._loaded_P is None:
 +            self._loaded_P = sorted(self._history._old_prob_pkgs(self.rpid))
 +        return self._loaded_P
 +
 +    packages = property(fget=lambda self: self._getProbPkgs())
 +
 +
  class YumHistoryTransaction:
      """ Holder for a history transaction. """
  
commit d68af7e711cdec200f3b7a8201eeea9ec3355006
Author: James Antill <james at and.org>
Date:   Thu Sep 23 23:55:40 2010 -0400

     Fix possible bug when "manually" setting checksum_type or checksum_data.
    
     We kind of got around this before by always reloading at the end of a
    transaction, so any bugs were limited until the next transaction
    performed. But now we almost always just use the index, so we should fix
    it.
    
     Basically just rm the cached version if anyone changes them, this is
    fine in a transaction as we'll be writting a new version anyway.

diff --git a/yum/rpmsack.py b/yum/rpmsack.py
index 7b699ca..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. 
@@ -315,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)
@@ -1493,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
@@ -1657,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
commit 8a3c10072407640724399f547ef60d6916db6b93
Author: James Antill <james at and.org>
Date:   Thu Sep 23 23:41:26 2010 -0400

     Remove the bad dropCachedData() calls. Use dropCachedDataPostTransaction().
    This is a major speedup, and fixes some weird bugs with lookups giving
    back different packages than are in the transaction.
    
     Move futureRpmDBVersion() to be before we need the checksums in
    history.
    
     Always calc. the new simpleVersion() ... as it's much cheaper now
    than when we next start (who runs without history anyway?:)

diff --git a/yum/__init__.py b/yum/__init__.py
index 1cf1f81..afe5354 100644
--- a/yum/__init__.py
+++ b/yum/__init__.py
@@ -986,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
 
@@ -1321,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]
@@ -1340,6 +1341,8 @@ 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
@@ -1353,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 
@@ -1394,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):
@@ -1417,7 +1421,6 @@ class YumBase(depsolve.Depsolve):
         # and the install reason
         
         vt_st = time.time()
-        self.rpmdb.dropCachedData()
         self.plugins.run('preverifytrans')
         for txmbr in self.tsInfo:
             if txmbr.output_state in TS_INSTALL_STATES:
@@ -1495,12 +1498,13 @@ 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(rpmdbv, ret)
         self.rpmdb.dropCachedData()
         self.verbose_logger.debug('VerifyTransaction time: %0.3f' % (time.time() - vt_st))
 
commit 4209fd1f22859297638596520d2b8b9777161752
Author: James Antill <james at and.org>
Date:   Thu Sep 23 23:34:49 2010 -0400

     Add dropCachedDataPostTransaction() API and document dropCachedData().
    
     Calling dropCachedData() before we are "done" is pretty bad, as we can
    load a package more than once. It's also a giant perf. hit, esp. for
    yumdb checksum*. So we Document all that and add a special API to call
    just after a transaction has run, to cleanup just the stuff we want.

diff --git a/yum/rpmsack.py b/yum/rpmsack.py
index ae73c32..7b699ca 100644
--- a/yum/rpmsack.py
+++ b/yum/rpmsack.py
@@ -210,6 +210,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 +240,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. """
@@ -1566,7 +1637,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
 
commit 15d4562167a1092b47cf7fb2d942030439d1e830
Author: James Antill <james at and.org>
Date:   Mon Nov 23 09:06:00 2009 -0500

    Fix installonly installs and "broken" rpmdbv stuff from tracebacks

diff --git a/test/merge-history-transactions-tests.py b/test/merge-history-transactions-tests.py
index 96fc625..569ba8d 100644
--- a/test/merge-history-transactions-tests.py
+++ b/test/merge-history-transactions-tests.py
@@ -509,9 +509,9 @@ class MergeHistTransTests(unittest.TestCase):
         self.assertEquals(pkgs[3], opkg3)
         self.assertEquals(pkgs[3].state, opkg3.state)
 
-    def testInRmMerge1(self, xstate='Install'):
+    def testInRmMerge1(self, xstate='Install', estate='Erase'):
         npkg1 = self._pkg_new('foo', state=xstate)
-        npkg2 = self._pkg_new('foo', state='Erase')
+        npkg2 = self._pkg_new('foo', state=estate)
         npkg3 = self._pkg_new('bar', version='6', state='True-Install')
 
         trans = []
@@ -525,6 +525,36 @@ class MergeHistTransTests(unittest.TestCase):
         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')
@@ -664,6 +694,37 @@ class MergeHistTransTests(unittest.TestCase):
     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
@@ -697,6 +758,12 @@ class MergeHistTransTests(unittest.TestCase):
         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):
diff --git a/yum/history.py b/yum/history.py
index eda0dd3..f1c287e 100644
--- a/yum/history.py
+++ b/yum/history.py
@@ -320,10 +320,14 @@ class YumMergedHistoryTransaction(YumHistoryTransaction):
             self._move_pkg(sk, nstate, npkgtup2pkg, npkgstate2pkg)
         def _del1_n(pkg):
             del npkgtup2pkg[pkg.pkgtup]
-            del npkgstate2pkg[self._p2sk(pkg)]
+            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]
-            del fpkgstate2pkg[self._p2sk(pkg)]
+            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)
@@ -458,20 +462,7 @@ class YumMergedHistoryTransaction(YumHistoryTransaction):
                 fpkgtup2pkg[x] = npkgtup2pkg[x]
             for x in npkgstate2pkg:
                 fpkgstate2pkg[x] = npkgstate2pkg[x]
-        if True:
-            return sorted(fpkgstate2pkg.values())
-
-        # This just dumps "everything", and is thus. pretty crappy
-        ret = []
-        filt = set()
-        for obj in self._merged_objs:
-            for pkg in obj.trans_data:
-                key = (pkg.state, pkg.pkgtup)
-                if key in filt:
-                    continue
-                filt.add(key)
-                ret.append(pkg)
-        return sorted(ret)
+        return sorted(fpkgtup2pkg.values())
 
     def _getErrors(self):
         ret = []
commit b0581428c57a7ebfc1f6c9eb3d62ec5137e74e82
Author: James Antill <james at and.org>
Date:   Mon Nov 23 00:10:04 2009 -0500

     UI for merged transactions in "history info".
    
       Eg. yum histry merge 1..2 => Will show the first two transactions
       as though they were a single transaction. All the complicated bits
       are in YumMergedTransaction.merge(), this is just a UI patch.
     Adds this to the man page.
    
     Also changes transaction time to have minutes/hours/days.
    
     Also fixes info's package name chopping to dtrt. for Obsoleting
    packages.
    
     Also makes "history info" report an error if there are no transactions.

diff --git a/docs/yum.8 b/docs/yum.8
index 7ac9772..4158453 100644
--- a/docs/yum.8
+++ b/docs/yum.8
@@ -247,7 +247,14 @@ and new to start a new history file.
 The info/list/summary commands take either a transactions id or a package (with
 wildcards, as in \fBSpecifying package names\fP), all three can also be passed
 no arguments. list can be passed the keyword "all" to list all the transactions.
-undo/redo just take a transaction id.
+undo/redo just take a transaction id\&.
+
+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
diff --git a/output.py b/output.py
index 4a94bc4..56343c9 100755
--- a/output.py
+++ b/output.py
@@ -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():
@@ -1198,6 +1200,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 on init.
         if uid is None or uid == 0xFFFFFFFF:
             loginid = _("<unset>")
@@ -1317,21 +1322,58 @@ 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()
-            if old is not None:
-                tids.add(old.tid)
+            tids.add(old.tid)
+            utids.add(old.tid)
 
         if not tids:
             self.logger.critical(_('No transaction ID, or package, given'))
@@ -1343,6 +1385,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
@@ -1352,15 +1398,43 @@ 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)
 
-        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:
@@ -1381,15 +1455,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
commit 6e949500aa3300c196f1cb82cef7d53795f79aac
Author: James Antill <james at and.org>
Date:   Sun Nov 22 10:41:19 2009 -0500

     Add merged history API, this is a combined history for multiple history
    transactions within a single object.
     The trans_data member is pretty tricky, so there is a bunch of
    testcases for that. Mainly because we simplify the output for the user
    so:
    
             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
    
    ...stays as is.
    
     Esp. worth of note is that the "Obsoleting" state is a mask for any of:
    
        Install, Dep-Install, True-Install, Downgrade, Reinstall, Update
    
    ...which I should have worked out before, and not made it a state in
    history ... but, oh well.
    
     Also add a repoid to YumHistoryPackages so that PackageObject can run __eq__

diff --git a/test/merge-history-transactions-tests.py b/test/merge-history-transactions-tests.py
new file mode 100644
index 0000000..96fc625
--- /dev/null
+++ b/test/merge-history-transactions-tests.py
@@ -0,0 +1,725 @@
+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'):
+        npkg1 = self._pkg_new('foo', state=xstate)
+        npkg2 = self._pkg_new('foo', state='Erase')
+        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 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')
+
+    #  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 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/yum/history.py b/yum/history.py
index 095c76b..eda0dd3 100644
--- a/yum/history.py
+++ b/yum/history.py
@@ -29,6 +29,7 @@ from yum.constants import *
 from yum.packages import YumInstalledPackage, YumAvailablePackage, PackageObject
 from yum.i18n import to_unicode
 
+from rpmUtils.arch import getBaseArch
 
 _history_dir = '/var/lib/yum/history'
 
@@ -122,7 +123,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
@@ -135,6 +136,15 @@ class YumHistoryPackage(PackageObject):
         else:
             chk = checksum.split(':')
             self._checksums = [(chk[0], chk[1], 0)] # (type, checksum, id(0,1))
+        # 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
 
 class YumHistoryTransaction:
     """ Holder for a history transaction. """
@@ -193,6 +203,304 @@ 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
+
+        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]
+            del npkgstate2pkg[self._p2sk(pkg)]
+        def _del1_f(pkg):
+            del fpkgtup2pkg[pkg.pkgtup]
+            del fpkgstate2pkg[self._p2sk(pkg)]
+        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]
+        if True:
+            return sorted(fpkgstate2pkg.values())
+
+        # This just dumps "everything", and is thus. pretty crappy
+        ret = []
+        filt = set()
+        for obj in self._merged_objs:
+            for pkg in obj.trans_data:
+                key = (pkg.state, pkg.pkgtup)
+                if key in filt:
+                    continue
+                filt.add(key)
+                ret.append(pkg)
+        return sorted(ret)
+
+    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. """
 
@@ -453,9 +761,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


More information about the Yum-commits mailing list