def _mergeGroupChanges(self, uJob, primaryJobList, transitiveClosure,
                           redirectHack, recurse, ineligible, checkPrimaryPins,
                           installedPrimaries, installMissingRefs=False,
                           updateOnly=False, respectBranchAffinity=True,
                           alwaysFollowLocalChanges=False,
                           removeNotByDefault = False):

        def lookupPathHashes(infoList, old = False):
            result = []
            if old:
                for s in self.db.getPathHashesForTroveList(infoList):
                    if s is None:
                        result.append(set())
                    else:
                        result.append(s)

                return result
            else:
                troveSource = uJob.getTroveSource()
                # we can't assume that the troveSource has all
                # of the child troves for packages we can see.
                # Specifically, the troveSource could have come from
                # a relative changeset that only contains byDefault True
                # components - here we're looking at both byDefault
                # True and False components (in trove.diff)
                hasTroveList = troveSource.hasTroves(infoList)
                for info, hasTrove in itertools.izip(infoList, hasTroveList):
                    if hasTrove:
                        ph = troveSource.getTrove(withFiles = False, *info).\
                                        getPathHashes()
                        result.append(ph)
                    else:
                        result.append(set())
            return result

        def _findErasures(primaryErases, newJob, referencedTroves, recurse,
                          ineligible):
            # this batches a bunch of hasTrove/trovesArePinned calls. It
            # doesn't get the ones we find implicitly, unfortunately
            class ErasureInfoCache:

                def __init__(self, db):
                    self.db = db
                    self.hasTrovesCache = {}
                    self.pinnedCache = {}
                    self.referencesCache = {}

                def hasTrove(self, info):
                    return self.hasTrovesCache.get(info, None)

                def isPinned(self, info):
                    return self.pinnedCache[info]

                def getReferences(self, info):
                    return self.referencesCache[info]

                def populate(self, jobList):
                    erasures = [ (job[0], job[1][0], job[1][1]) for job
                                    in jobList if job[1][0] is not None ]
                    hasTroveList = self.db.hasTroves(erasures)
                    self.hasTrovesCache.update(
                                itertools.izip(erasures, hasTroveList))
                    present = [ x[0] for x in
                                    itertools.izip(erasures, hasTroveList)
                                    if x[1] ]
                    pinnedList = self.db.trovesArePinned(present)
                    self.pinnedCache.update(
                            itertools.izip(present, pinnedList))

                    referenceList = self.db.getTroveTroves(
                                                present, weakRefs = False,
                                                pristineOnly = False)
                    self.referencesCache.update(
                            itertools.izip(present, referenceList))

            # each node is a ((name, version, flavor), state, edgeList
            #                  fromUpdate)
            # state is ERASE, KEEP, or UNKNOWN
            #
            # fromUpdate is True if erasing this node reflects a trove being
            # replaced by a different one in the Job (an update, not an erase)
            # We need to track this to know what's being removed, but we don't
            # need to cause them to be removed.
            nodeList = []
            nodeIdx = {}
            ERASE = 1
            KEEP = 2
            UNKNOWN = 3

            infoCache = ErasureInfoCache(self.db)
            infoCache.populate(newJob)
            infoCache.populate(primaryErases)

            jobQueue = util.IterableQueue()
            # The order of this chain matters. It's important that we handle
            # all of the erasures we already know about before getting to the
            # ones which are implicit. That gets the state right for ones
            # which are explicit (since arriving there implicitly gets
            # ignored). Handling updates from newJob first prevents duplicates
            # from primaryErases
            for job, isPrimary in itertools.chain(
                        itertools.izip(newJob, itertools.repeat(False)),
                        itertools.izip(primaryErases, itertools.repeat(True)),
                        jobQueue):
                oldInfo = (job[0], job[1][0], job[1][1])

                if oldInfo[1] is None:
                    # skip new installs
                    continue

                if oldInfo in nodeIdx:
                    # See the note above about chain order (this wouldn't
                    # work w/o it)
                    continue

                present = infoCache.hasTrove(oldInfo)
                if present is None:
                    infoCache.populate(
                        [ job ] + [ x[0] for x in jobQueue.peekRemainder() ] )
                    present = infoCache.hasTrove(oldInfo)

                if not present:
                     # no need to erase something we don't have installed
                     continue

                # erasures which are part of an
                # update are guaranteed to occur
                if job in newJob:
                    assert(job[2][0])
                    state = ERASE
                    fromUpdate = True
                else:
                    # If it's pinned, we keep it.
                    if infoCache.isPinned(oldInfo):
                        state = KEEP
                        if isPrimary and checkPrimaryPins:
                            raise UpdatePinnedTroveError(oldInfo)
                    elif isPrimary:
                        # primary updates are guaranteed to occur (if the
                        # trove is not pinned).
                        state = ERASE
                    else:
                        state = UNKNOWN

                    fromUpdate = False

                assert(oldInfo not in nodeIdx)
                nodeIdx[oldInfo] = len(nodeList)
                nodeList.append([ oldInfo, state, [], fromUpdate ])

                if not recurse: continue

                if not trove.troveIsCollection(oldInfo[0]): continue
                for inclInfo in infoCache.getReferences(oldInfo):
                    # we only use strong references when erasing.
                    if inclInfo in ineligible:
                        continue

                    jobQueue.add(((inclInfo[0], inclInfo[1:], (None, None),
                                  False), False))

            # For nodes which we haven't decided to erase, we need to track
            # down all of the collections which include those troves.
            needParents = [ (nodeId, info) for nodeId, (info, state, edges,
                                                        alreadyHandled)
                                in enumerate(nodeList) if state == UNKNOWN ]
            while needParents:
                containers = self.db.getTroveContainers(
                                        x[1] for x in needParents)
                newNeedParents = []
                for (nodeId, nodeInfo), containerList in \
                                itertools.izip(needParents, containers):
                    for containerInfo in containerList:
                        if containerInfo in nodeIdx:
                            containerId = nodeIdx[containerInfo]
                            nodeList[containerId][2].append(nodeId)
                        else:
                            containerId = len(nodeList)
                            nodeIdx[containerInfo] = containerId
                            nodeList.append([ containerInfo, KEEP, [ nodeId ],
                                              False])
                            newNeedParents.append((containerId, containerInfo))
                needParents = newNeedParents

            # don't erase nodes which are referenced by troves we're about
            # to install - the only troves we list here are primary installs,
            # and they should never be erased.
            for info in referencedTroves:
                if info in nodeIdx:
                    node = nodeList[nodeIdx[info]]
                    node[1] = KEEP

            seen = [ False ] * len(nodeList)
            # DFS to mark troves as KEEP
            keepNodes = [ nodeId for nodeId, node in enumerate(nodeList)
                                if node[1] == KEEP ]
            while keepNodes:
                nodeId = keepNodes.pop()
                if seen[nodeId]: continue
                seen[nodeId] = True
                nodeList[nodeId][1] = KEEP
                keepNodes += nodeList[nodeId][2]

            # anything which isn't to KEEP is to erase, but skip those which
            # are already being removed by a trvCs
            eraseList = ((x[0][0], (x[0][1], x[0][2]), (None, None), False)
                         for x in nodeList if x[1] != KEEP and not x[3])

            return set(eraseList)

        def _troveTransitiveClosure(db, itemList):
            itemQueue = util.IterableQueue()
            fullSet = set()
            for item in itertools.chain(itemList, itemQueue):
                if item in fullSet: continue
                fullSet.add(item)

                if not trove.troveIsCollection(item[0]): continue
                trv = db.getTrove(withFiles = False, pristine = False, *item)

                for x in trv.iterTroveList(strongRefs=True, weakRefs=True):
                    itemQueue.add(x)

            return fullSet

        def _filterDoubleRemovesDueToLocalUpdates(newJob, replacedJobs):
            # it's possible that a locally removed half of a local update
            # trove will match up to something that is due to be installed
            # despite the fact that its match has been removed by the user.
            # We don't want to disallow that type of match - it is useful
            # information and may be corrrect.  However, we don't want to
            # have a double removal, which is what will happen in some cases
            # - conary will switch the erased version mentioned in the local
            # update to the installed version, which could be a part of
            # another update.  In such cases, we remove the "erase" part
            # of the local update.
            eraseCount = {}
            for job in newJob:
                oldInfo = job[0], job[1][0], job[1][1]
                if oldInfo in replacedJobs:
                    if oldInfo in eraseCount:
                        eraseCount[oldInfo] += 1
                    else:
                        eraseCount[oldInfo] = 1

            doubleErased = [ x[0] for x in eraseCount.iteritems() if x[1] > 1 ]
            for oldInfo in doubleErased:
                newJob.remove((oldInfo[0], (oldInfo[1], oldInfo[2]),
                               replacedJobs[oldInfo], False))
                newJob.add((oldInfo[0], (None, None),
                            replacedJobs[oldInfo], False))



        # def _mergeGroupChanges -- main body begins here
        erasePrimaries =    set(x for x in primaryJobList
                                    if x[2][0] is None)
        relativePrimaries = set(x for x in primaryJobList
                                    if x[2][0] is not None and
                                       not x[3])
        absolutePrimaries = set(x for x in primaryJobList
                                    if x[3])
        assert(len(relativePrimaries) + len(absolutePrimaries) +
               len(erasePrimaries) == len(primaryJobList))

        log.lowlevel('_mergeGroupChanges(recurse=%s,'
                      ' checkPrimaryPins=%s,'
                      ' installMissingRefs=%s, '
                      ' updateOnly=%s, '
                      ' respectBranchAffinity=%s,'
                      ' alwaysFollowLocalChanges=%s)',
                      recurse, checkPrimaryPins, installMissingRefs,
                      updateOnly, respectBranchAffinity,
                      alwaysFollowLocalChanges)

        troveSource = uJob.getTroveSource()


        # ineligible needs to be a transitive closure when recurse is set
        if recurse:
            ineligible = _troveTransitiveClosure(self.db, ineligible)

        for job in erasePrimaries:
            # an erase primary can't be part of an update (but their children
            # can, so add this after we've recursed)
            ineligible.add((job[0], job[1][0], job[1][1]))

        # Build the trove which contains all of the absolute change sets
        # we may need to install. Build a set of all of the trove names
        # in that trove as well.
        availableTrove = trove.Trove("@update", versions.NewVersion(),
                                     deps.Flavor(), None)

        names = set()
        for job in transitiveClosure:
            if job[2][0] is None: continue
            if not job[3]: continue
            if (job[0], job[2][0], job[2][1]) in ineligible: continue

            availableTrove.addTrove(job[0], job[2][0], job[2][1],
                                    presentOkay = True)
            names.add(job[0])

        avail = set(availableTrove.iterTroveList(strongRefs=True))

        # Build the set of all relative install jobs (transitive closure)
        relativeUpdateJobs = set(job for job in transitiveClosure if
                                    job[2][0] is not None and not job[3])

        # Look for relative updates whose sources are not currently installed
        relativeUpdates = [ ((x[0], x[1][0], x[1][1]), x)
                    for x in relativeUpdateJobs if x[1][0] is not None]
        isPresentList = self.db.hasTroves([ x[0] for x in relativeUpdates ])

        for (info, job), isPresent in itertools.izip(relativeUpdates,
                                                     isPresentList):
            if not isPresent:
                relativeUpdateJobs.remove(job)
                newTrove = job[0], job[2][0], job[2][1]

                if newTrove not in avail:
                    ineligible.add(newTrove)

        # skip relative installs that are already present.
        relativeInstalls = [ ((x[0], x[2][0], x[2][1]), x)
                         for x in relativeUpdateJobs if x[2][0] is not None]
        isPresentList = self.db.hasTroves([ x[0] for x in relativeInstalls ])
        for (newTrove, job), isPresent in itertools.izip(relativeInstalls,
                                                         isPresentList):
            if isPresent:
                relativeUpdateJobs.remove(job)
                ineligible.add(newTrove)
                if job[1][0]:
                    # this used to be a relative upgrade, but the target
                    # is installed, so turn it into an erase.
                    erasePrimaries.add((job[0], (job[1][0], job[1][1]),
                                                (None, None), False))

        # Get all of the currently installed and referenced troves which
        # match something being installed absolute. Troves being removed
        # through a relative changeset aren't allowed to be removed by
        # something else.
        (installedNotReferenced,
         installedAndReferenced,
         referencedStrong,
         referencedWeak) = self.db.db.getCompleteTroveSet(names)

        installedTroves = installedNotReferenced | installedAndReferenced
        referencedNotInstalled = referencedStrong | referencedWeak
        log.lowlevel('referencedNotInstalled: %s', referencedNotInstalled)
        log.lowlevel('ineligible: %s', ineligible)

        installedTroves.difference_update(ineligible)
        installedTroves.difference_update(
                (job[0], job[1][0], job[1][1]) for job in relativeUpdateJobs)
        referencedNotInstalled.difference_update(ineligible)
        referencedNotInstalled.difference_update(
                (job[0], job[1][0], job[1][1]) for job in relativeUpdateJobs)


        # The job between referencedTroves and installedTroves tells us
        # a lot about what the user has done to his system.
        primaryLocalUpdates = self.getPrimaryLocalUpdates(names)
        localUpdates = list(primaryLocalUpdates)
        if localUpdates:
            localUpdates += self.getChildLocalUpdates(uJob.getSearchSource(),
                                                      localUpdates,
                                                      installedTroves,
                                                      referencedNotInstalled)
            # make some assertions about the local updates:
            # 1. a missing trove can only be a part of one local update
            # 2. a present trove can only be a part of one local update

            # although we needed parent updates to get the correct set of
            # local updates related to this job, we don't care local updates
            # that aren't related to troves in our job.
            localUpdates = [ x for x in localUpdates if x[0] in names ]

        localUpdatesByPresent = \
                 dict( ((job[0], job[2][0], job[2][1]), job[1]) for
                        job in localUpdates if job[1][0] is not None and
                                               job[2][0] is not None)
        localUpdatesByMissing = \
                 dict( ((job[0], job[1][0], job[1][1]), job[2]) for
                        job in localUpdates if job[1][0] is not None and
                                               job[2][0] is not None)
        primaryLocalUpdates = set((job[0], job[2][0], job[2][1])
                                  for job in primaryLocalUpdates)
        localErases = set((job[0], job[1][0], job[1][1])
                           for job in localUpdates if job[2][0] is None)
        # Troves which were locally updated to version on the same branch
        # no longer need to be listed as referenced. The trove which replaced
        # it is always a better match for the new items (installed is better
        # than not installed as long as the branches are the same). This
        # doesn't apply if the trove which was originally installed is
        # part of this update though, as troves which are referenced and
        # part of the update are handled separately.

        # keep track of troves that are changes on the same branch,
        # since those are still explicit user requests and might
        # override implied updates that would downgrade this trove.
        sameBranchLocalUpdates = {}

        for job in sorted(localUpdates):
            if job[1][0] is not None and job[2][0] is not None:
                if (job[1][0].branch() == job[2][0].branch() and
                      (job[0], job[1][0], job[1][1]) not in avail):
                    del localUpdatesByPresent[(job[0], job[2][0], job[2][1])]
                    del localUpdatesByMissing[(job[0], job[1][0], job[1][1])]
                    referencedNotInstalled.remove((job[0], job[1][0], job[1][1]))
                    log.lowlevel('reworking same-branch local update: %s', job)
                    # track this update for since it means the user
                    # requested this version explicitly
                    sameBranchLocalUpdates[job[0], job[2][0], job[2][1]] = (job[1][0], job[1][1])
                else:
                    log.lowlevel('local update: %s', job)

        del localUpdates

        # Build the set of the incoming troves which are either already
        # installed or already referenced.
        alreadyInstalled = (installedTroves & avail) | installedPrimaries
        alreadyReferenced = referencedNotInstalled & avail

        del avail


        existsTrv = trove.Trove("@update", versions.NewVersion(),
                                deps.Flavor(), None)
        [ existsTrv.addTrove(*x) for x in installedTroves ]
        [ existsTrv.addTrove(*x) for x in referencedNotInstalled ]

        jobList = availableTrove.diff(existsTrv,
                                      getPathHashes=lookupPathHashes)[2]

        # alreadyReferenced troves are in both the update set
        # and the installed set.  They are a good match for themselves.
        jobList += [(x[0], (x[1], x[2]), (x[1], x[2]), 0) for x in alreadyReferenced ]

        installJobs = [ x for x in jobList if x[1][0] is     None and
                                              x[2][0] is not None ]
        updateJobs = [ x for x in jobList if x[1][0] is not None and
                                             x[2][0] is not None ]
        pins = self.db.trovesArePinned([ (x[0], x[1][0], x[1][1])
                                                    for x in updateJobs ])
        jobByNew = dict( ((job[0], job[2][0], job[2][1]), (job[1], pin)) for
                        (job, pin) in itertools.izip(updateJobs, pins) )
        jobByNew.update(
                   dict( ((job[0], job[2][0], job[2][1]), (job[1], False)) for
                        job in installJobs))

        del jobList, installJobs, updateJobs

        # Relative jobs override pins and need to match up against the
        # right thing.
        jobByNew.update(
                    dict( ((job[0], job[2][0], job[2][1]), (job[1], False)) for
                        job in relativeUpdateJobs))

        respectFlavorAffinity = True
        # the newTroves parameters are described below.
        newTroves = sorted((((x[0], x[2][0], x[2][1]),
                             True, {}, False, False, False, False, None,
                             respectBranchAffinity,
                             respectFlavorAffinity, True,
                             True, updateOnly)
                            for x in itertools.chain(absolutePrimaries,
                                                     relativePrimaries)),
                           # compare on the string of the version, since it might
                           # not have timestamps
                           key=lambda y: (y[0][0], str(y[0][1]), y[0][2]) + y[1:])

        newJob = set()
        notByDefaultRemovals = set()

        # ensure the user-specified respect branch affinity setting is not
        # lost.
        neverRespectBranchAffinity = not respectBranchAffinity
        replacedJobs = {}

        while newTroves:
            # newTroves tuple values
            # newInfo: the (n, v, f) of the trove to install
            # isPrimary: true if user specified this trove on the command line
            # byDefaultDict: mapping of trove tuple to byDefault setting, as
            #                specified by the primary parent trove
            # parentInstalled: True if the parent of this trove was installed.
            #                  Used to determine whether to install troves
            #                  with weak references.
            # parentReplacedWasPinned: True if this trove's parent would
            #                          have replaced a trove that is pinned.
            # parentUpdated: True if the parent of this trove was installed.
            #                or updated.
            # primaryInstalled: True if the primary that led to this update
            #                   was an install.
            # branchHint:  if newInfo's parent trove switched branches, this
            #              provides the to/from information on that switch.
            #              If this child trove is making the same switch, we
            #              allow it even if the switch is overriding branch
            #              affinity.
            # respectBranchAffinity: If true, we generally try to respect
            #              the user's choice to switch a trove from one branch
            #              to another.  We might not respect branch affinity
            #              if a) a primary trove update is overriding branch
            #              affinity, or b) the call to mergeGroupChanges
            #              had respectBranchAffinity False
            # respectFlavorAffinity: If true, we generally try to respect
            #              the user's choice to switch a trove from one flavor
            #              to another.  We might not respect flavor affinity
            #              for the same reasons we might not respect branch
            #              affinity.
            # installRedirects: If True, we install redirects even when they
            #              are not upgrades.
            # followLocalChanges: see the code where it is used for a
            #              description.
            # updateOnly:  If true, only update troves, don't install them
            #              fresh.
            (newInfo, isPrimary, byDefaultDict, parentInstalled,
             parentReplacedWasPinned, parentUpdated, primaryInstalled,
             branchHint, respectBranchAffinity, respectFlavorAffinity,
             installRedirects, followLocalChanges,
             updateOnly) = newTroves.pop(0)

            byDefault = isPrimary or byDefaultDict[newInfo]

            log.lowlevel('''\
*******
%s=%s[%s]
primary: %s  byDefault:%s  parentUpdated: %s parentInstalled: %s primaryInstalled: %s updateOnly: %s
branchHint: %s
branchAffinity: %s   flavorAffinity: %s installRedirects: %s
followLocalChanges: %s

''',
                      newInfo[0], newInfo[1], newInfo[2], isPrimary,
                      byDefault, parentUpdated, parentInstalled,
                      primaryInstalled, updateOnly, branchHint,
                      respectBranchAffinity, respectFlavorAffinity,
                      installRedirects, followLocalChanges)
            trv = None
            jobAdded = False
            replaced = (None, None)
            recurseThis = True
            childrenFollowLocalChanges = alwaysFollowLocalChanges
            pinned = False
            while True:
                # this loop should only be called once - it's basically a
                # way to create a quick GOTO structure, without needing
                # to call another function (which would be expensive in
                # this loop).
                if newInfo in alreadyInstalled:
                    # No need to install it twice
                    # but count it as 'added' for the purposes of
                    # whether or not to recurse
                    jobAdded = True
                    job = (newInfo[0], (newInfo[1], newInfo[2]),
                                       (newInfo[1], newInfo[2]), False)
                    log.lowlevel('SKIP: already installed')
                    break
                elif newInfo in ineligible:
                    log.lowlevel('SKIP: ineligible')
                    break
                elif newInfo in alreadyReferenced:
                    log.lowlevel('new trove in alreadyReferenced')
                    # meaning: this trove is referenced by something
                    # installed, but is not installed itself.

                    if isPrimary or installMissingRefs or primaryInstalled:
                        # They really want it installed this time. We removed
                        # this entry from the already-installed @update trove
                        # so localUpdates already tells us the best match for it.
                        pass
                    elif parentUpdated and newInfo in referencedWeak:
                        # The only link to this trove is a weak reference.
                        # A weak-only reference means an intermediate trove
                        # was missing.  But parentUpdated says we've now
                        # updated an intermediate trove, so install
                        # this trove too.
                        pass
                    else:
                        # We already know about this trove, and decided we
                        # don't want it. We do want to keep the item which
                        # replaced it though.
                        if newInfo in localUpdatesByMissing:
                            info = ((newInfo[0],)
                                     + localUpdatesByMissing[newInfo])
                            alreadyInstalled.add(info)
                            log.lowlevel('local update - marking present part %s'
                                      'as already installed', info)
                        log.lowlevel('SKIP: already referenced')
                        break

                replaced, pinned = jobByNew[newInfo]
                replacedInfo = (newInfo[0], replaced[0], replaced[1])

                log.lowlevel('replaces: %s', replacedInfo)

                if replaced[0] is not None:
                    if newInfo in alreadyReferenced:
                        # This section is the corrolary to the section
                        # above.  We only enter here if we've decided
                        # to install this trove even though its
                        # already referenced.
                        if replacedInfo in referencedNotInstalled:
                            # don't allow this trove to not be installed
                            # because the trove its replacing is not installed.
                            # Find an installed update or just install the trove
                            # fresh.
                            replaced = localUpdatesByMissing.get(replacedInfo,
                                                                 (None, None))
                            replacedInfo = (replacedInfo[0], replaced[0],
                                            replaced[1])
                            log.lowlevel('replaced is not installed, using local update %s instead', replacedInfo)
                            if replaced[0]:
                                log.lowlevel('following local changes')
                                childrenFollowLocalChanges = True
                                replacedJobs[replacedInfo] = (newInfo[1], newInfo[2])
                                pinned = self.db.trovesArePinned([replacedInfo])[0]
                    elif replacedInfo in referencedNotInstalled:
                        # the trove on the local system is one that's referenced
                        # but not installed, so, normally we would not install
                        # this trove.
                        # BUT if this is a primary (or in certain other cases)
                        # we always want to have the update happen.
                        # In the case of a primary trove,
                        # if the referenced trove is replaced by another trove
                        # on the the system (by a localUpdate) then we remove
                        # that trove instead.  If not, we just install this
                        # trove as a fresh update.
                        log.lowlevel('replaced trove is not installed')
                        if followLocalChanges:
                            skipLocal = False
                        elif installMissingRefs:
                            skipLocal = False
                        elif (parentInstalled and not parentReplacedWasPinned):
                            skipLocal = False
                        elif replacedInfo not in localErases and not parentReplacedWasPinned:
                            skipLocal = False
                        else:
                            skipLocal = True


                        if skipLocal:
                            # followLocalChanges states that, even though
                            # the given trove is not a primary, we still want
                            # replace a localUpdate if available instead of
                            # skipping the update.  This flag can be set if
                            # a) an ancestor of this trove is a primary trove
                            # that switched from a referencedNotInstalled
                            # to an installed trove or b) its passed in to
                            # the function that we _always_ follow local
                            # changes.
                            log.lowlevel('SKIP: not following local changes')
                            break

                        freshInstallOkay = (isPrimary or
                                            (parentInstalled
                                             and not parentReplacedWasPinned)
                                            or byDefault
                                            or installMissingRefs)
                        # we always want to install the trove even if there's
                        # no local update to match to if it's a primary, or
                        # if the trove's parent was just installed
                        # (if the parent was installed, we just added a
                        # strong reference, which overrides any other
                        # references that might suggest not to install it.)

                        replaced = localUpdatesByMissing.get(replacedInfo,
                                                             (None, None))


                        if (replaced[0] is None and not freshInstallOkay):
                            log.lowlevel('SKIP: not allowing fresh install')
                            break

                        childrenFollowLocalChanges = True

                        replacedInfo = (replacedInfo[0], replaced[0],
                                        replaced[1])
                        replacedJobs[replacedInfo] = (newInfo[1], newInfo[2])
                        if replaced[0]:
                            pinned = self.db.trovesArePinned([replacedInfo])[0]
                        log.lowlevel('using local update to replace %s, following local changes', replacedInfo)

                    elif not installRedirects:
                        if not redirectHack.get(newInfo, True):
                            # a parent redirect was added as an upgrade
                            # but this would be a new install of this child
                            # trove.  Skip it.
                            log.lowlevel('SKIP: is a redirect that would be'
                                      ' a fresh install, but '
                                      ' installRedirects=False')
                            break
                    elif redirectHack.get(newInfo, False):
                        # we are upgrading a redirect, so don't allow any child
                        # redirects to be installed unless they have a matching
                        # trove to redirect on the system.
                        log.lowlevel('INSTALL: upgrading redirect')
                        installRedirects = False

                    if replaced[0] and respectBranchAffinity:
                        log.lowlevel('checking branch affinity')
                        # do branch affinity checks

                        newBranch = newInfo[1].branch()
                        installedBranch = replacedInfo[1].branch()

                        if replacedInfo in localUpdatesByPresent:
                            notInstalledVer = localUpdatesByPresent[replacedInfo][0]
                            notInstalledBranch = notInstalledVer.branch()
                            # create alreadyBranchSwitch variable for
                            # readability
                            alreadyBranchSwitch = True
                        else:
                            notInstalledBranch = None
                            alreadyBranchSwitch = False


                        # Check to see if there's reason to be concerned
                        # about branch affinity.
                        if installedBranch == newBranch:
                            log.lowlevel('not branch switch')
                            # we didn't switch branches.  No branch
                            # affinity concerns.  If the user has made
                            # a local change that would make this new
                            # install a downgrade, skip it.
                            if (not isPrimary
                                and newInfo[1] < replaced[0]
                                and replacedInfo in sameBranchLocalUpdates
                                and (replacedInfo in primaryLocalUpdates
                                     or not parentUpdated)):
                                    log.lowlevel('SKIP: avoiding downgrade')

                                    # don't let this trove be erased, pretend
                                    # like it was explicitly requested.
                                    alreadyInstalled.add(replacedInfo)
                                    break
                        elif notInstalledBranch == installedBranch:
                            log.lowlevel('INSTALL: branch switch is reversion')
                            # we are reverting back to the branch we were
                            # on before.  We don't worry about downgrades
                            # because we're already overriding the user's
                            # branch choice
                            pass
                        else:
                            log.lowlevel('is a new branch switch')
                            # Either a) we've made a local change from branch 1
                            # to branch 2 and now we're updating to branch 3,
                            # or b) there's no local change but we're switching
                            # branches.

                            # Generally, we respect branch affinity and don't
                            # do branch switches.  There
                            # are a few exceptions:
                            if isPrimary:
                                # the user explicitly asked to switch
                                # to this branch, so we have to honor it.

                                if alreadyBranchSwitch:
                                    # it turns out the _current_ installed
                                    # trove is a local change.  The user
                                    # is messing with branches too much -
                                    # don't bother with branch affinity.
                                    respectBranchAffinity = False
                                    log.lowlevel('INSTALL: is a branch switch on top of a branch switch and is primary')
                                else:
                                    log.lowlevel('INSTALL: is a new branch switch and is primary')
                            elif (installedBranch, newBranch) == branchHint:
                                # Exception: if the parent trove
                                # just made this move, then allow it.
                                log.lowlevel('INSTALL: matches parent\'s branch switch')
                                pass
                            elif ((installedAndReferenced in replacedInfo
                                   or replacedInfo in sameBranchLocalUpdates)
                                  and not alreadyBranchSwitch
                                  and parentUpdated):
                                # Exception: The user has not switched this
                                # trove's branch explicitly, and now
                                # we have an implicit request to switch
                                # the branch.
                                log.lowlevel('INSTALL: implicit branch switch, parent installed')
                                pass
                            else:
                                # we're not installing this trove -
                                # It doesn't match any of our exceptions.
                                # It could be that it's a trove with
                                # no references to it on the system
                                # (and so a branch switch would be strange)
                                # or it could be that it is a switch
                                # to a third branch by the user.
                                # Since we're rejecting the update due to
                                # branch affinity, we don't consider any of its
                                # child troves for updates either.
                                log.lowlevel('SKIP: not installing branch switch')
                                recurseThis = False
                                alreadyInstalled.add(replacedInfo)
                                break

                        if replaced[0] and respectFlavorAffinity:
                            if replacedInfo in localUpdatesByPresent:
                                notInstalledFlavor = localUpdatesByPresent[replacedInfo][1]
                                # create alreadyBranchSwitch variable for
                                # readability
                                #alreadyFlavorSwitch = True
                                pass
                            elif replacedInfo in sameBranchLocalUpdates:
                                notInstalledFlavor = sameBranchLocalUpdates[replacedInfo][1]
                            else:
                                notInstalledFlavor = None

                            if (notInstalledFlavor is not None
                                and not deps.compatibleFlavors(notInstalledFlavor, replacedInfo[2])
                                and not deps.compatibleFlavors(replacedInfo[2], newInfo[2])):
                                if isPrimary:
                                    respectFlavorAffinity = False
                                else:
                                    log.lowlevel('SKIP: Not reverting'
                                              ' incompatible flavor switch')
                                    recurseThis = False
                                    alreadyInstalled.add(replacedInfo)
                                    break



                # below are checks to see if a fresh install should completed.
                # Since its possible that an update from above could be
                # converted into a fresh install, we start a new if/elif
                # branch here.
                if replaced[0]:
                    # we are dealing with a replacement, we've already
                    # decided it was okay above.
                    pass
                elif not byDefault:
                    # This trove is being newly installed, but it's not
                    # supposed to be installed by default
                    log.lowlevel('SKIP: not doing not-by-default fresh install')
                    break
                elif updateOnly:
                    # we're not installing trove, only updating installed
                    # troves.
                    log.lowlevel('SKIP: not doing install due to updateOnly')
                    break
                elif not isPrimary and self.cfg.excludeTroves.match(newInfo[0]):
                    # New trove matches excludeTroves
                    log.lowlevel('SKIP: trove matches excludeTroves')
                    break
                elif not installRedirects:
                    if not redirectHack.get(newInfo, True):
                        # a parent redirect was added as an upgrade
                        # but this would be a new install of this child
                        # trove.  Skip it.
                        log.lowlevel('SKIP: redirect would be a fresh install')
                        break
                elif redirectHack.get(newInfo, False):
                    # we are upgrading a redirect, so don't allow any child
                    # redirects to be installed unless they have a matching
                    # trove to redirect on the system.
                    log.lowlevel('installing redirect')
                    installRedirects = False

                job = (newInfo[0], replaced, (newInfo[1], newInfo[2]), False)
                if pinned and (not isPrimary or checkPrimaryPins):
                    job = self._splitPinnedJob(uJob, troveSource, job,
                                               force=not isPrimary)
                    if job is None:
                        recurseThis = False
                        break
                    elif (not isPrimary
                          and self.cfg.excludeTroves.match(newInfo[0])):
                        # New trove matches excludeTroves
                        log.lowlevel('SKIP: trove matches excludeTroves')
                        recurseThis = False
                        break

                log.lowlevel('JOB ADDED: %s', job)
                newJob.add(job)
                jobAdded = True
                break

            log.lowlevel('recurseThis: %s\nrecurse: %s', recurseThis, recurse)

            if jobAdded and removeNotByDefault and not byDefault:
                job = (newInfo[0], replaced, (newInfo[1], newInfo[2]), False)
                newJob.discard(job)
                if replaced[0]:
                    notByDefaultRemovals.add(
                                (newInfo[0], replaced, (None, None), False))
                elif newInfo in alreadyInstalled:
                    notByDefaultRemovals.add(
                                (newInfo[0], (newInfo[1], newInfo[2]),
                                             (None, None), False))

            if not recurseThis: continue
            if not recurse: continue
            if not trove.troveIsCollection(newInfo[0]): continue


            branchHint = None
            if replaced[0] and replaced[0].branch() == newInfo[1].branch():
                # if this trove didn't switch branches, then we respect branch
                # affinity for all child troves even the primary trove above us
                # did switch.  We assume the user at some point switched this
                # trove to the desired branch by hand already.
                log.lowlevel('respecting branch affinity for children')
                if not neverRespectBranchAffinity:
                    respectBranchAffinity = True
            elif replaced[0]:
                branchHint = (replaced[0].branch(), newInfo[1].branch())

            if replaced[0] and deps.compatibleFlavors(replaced[1], newInfo[2]):
                log.lowlevel('respecting flavor affinity for children')
                respectFlavorAffinity = True

            if trv is None:
                try:
                    trv = troveSource.getTrove(withFiles = False, *newInfo)
                except TroveMissing:
                    if self.db.hasTrove(*newInfo):
                        trv = self.db.getTrove(withFiles = False, *newInfo)
                    else:
                        # it's possible that the trove source we're using
                        # contains references to troves that it does not
                        # actually contain.  That's okay as long as the
                        # excluded trove is not actually trying to be
                        # installed.
                        if jobAdded:
                            raise
                        else:
                            continue

            if isPrimary:
                # byDefault status of troves is determined by the primary
                # trove.
                byDefaultDict = dict((x[0], x[1]) \
                                            for x in trv.iterTroveListInfo())

            updateOnly = updateOnly or not jobAdded
            # for all children, we only want to install them as new _if_ we
            # installed their parent.  If we did not install/upgrade foo, then
            # we do not install foo:runtime (though if it's installed, it
            # is reasonable to upgrade it).

            if isPrimary:
                primaryInstalled = jobAdded and not job[1][0]

            jobInstall = primaryInstalled or (jobAdded and not job[1][0])

            for info in sorted(trv.iterTroveList(strongRefs=True)):

                if not isPrimary:
                    if not jobAdded and info not in byDefaultDict:
                        continue

                    # support old-style collections.  _If_ this trove was not
                    # mentioned in its parent trove, then set its default
                    # value here.
                    childByDefault = (trv.includeTroveByDefault(*info)
                                      and jobAdded)
                    byDefaultDict.setdefault(info, childByDefault)

                newTroves.append((info, False,
                                  byDefaultDict, jobInstall, pinned, jobAdded,
                                  primaryInstalled,
                                  branchHint, respectBranchAffinity,
                                  respectFlavorAffinity, installRedirects,
                                  childrenFollowLocalChanges,
                                  updateOnly))

        _filterDoubleRemovesDueToLocalUpdates(newJob, replacedJobs)
        for job in notByDefaultRemovals:
            if job not in newJob:
                erasePrimaries.add((job[0], job[1], (None, None), False))
            alreadyInstalled.discard((job[0], job[1][0], job[1][1]))

        # items which were updated to redirects should be removed, no matter
        # what
        for info in set(itertools.chain(*redirectHack.values())):
            erasePrimaries.add((info[0], (info[1], info[2]), (None, None), False))

        eraseSet = _findErasures(erasePrimaries, newJob, alreadyInstalled,
                                 recurse, ineligible)
        assert(not [x for x in newJob if x[2][0] is None])
        newJob.update(eraseSet)
        return newJob
dataset/ETHPy150Open sassoftware/conary/conary/conaryclient/update.py/ClientUpdate._mergeGroupChanges/swapped operands of `in`