コード例 #1
0
    def test_removePackage(self):
        controlfile = \
            "Source: foo\n" \
            "Build-Depends: bar\n" \
            "\n" \
            "Package: foo1\n" \
            "Suggests: something\n" \
            "\n" \
            "Package: foo2\n" \
            "Suggests: something\n" \
            "\n" \
            "Package: foo3\n" \
            "Suggests: something\n" \
            "\n" \
            "Package: foo4\n" \
            "Suggests: something\n"

        parser = ControlFileParser(text=controlfile)
        parser.remove_package('foo1')
        parser.remove_package('foo2')
        parser.remove_package('foo4')
        self.assertEqual(
            parser.get_text(), "Source: foo\n"
            "Build-Depends: bar\n"
            "\n"
            "Package: foo3\n"
            "Suggests: something\n")
コード例 #2
0
class DebControlMerger(object):
    def __init__(self, control_path, left_dir, left_name, right_dir,
                 right_name, base_dir, merged_dir):
        # Merge notes recorded here
        self.notes = []
        # If the merged file was modified (relative to the right version)
        self.modified = False

        self.left_name = left_name
        self.right_name = right_name

        self.left_dir = left_dir
        self.right_dir = right_dir
        self.base_dir = base_dir
        self.merged_dir = merged_dir

        self.left_control_path = os.path.join(left_dir, control_path)
        self.right_control_path = os.path.join(right_dir, control_path)
        self.base_control_path = os.path.join(base_dir, control_path)
        self.merged_control_path = os.path.join(merged_dir, control_path)
        tree.ensure(self.merged_control_path)

        self.base_control = ControlFileParser(filename=self.base_control_path)
        self.left_control = ControlFileParser(filename=self.left_control_path)
        self.right_control = \
            ControlFileParser(filename=self.right_control_path)

        self.orig_right_md5sum = md5sum(self.right_control_path)

    def record_note(self, note, changelog_worthy=False):
        logger.debug(note)
        self.notes.append((note, changelog_worthy))

    def do_diff3(self):
        rc = subprocess.call(
            ("diff3", "-E", "-m", "-L", self.left_name, self.left_control_path,
             "-L", "BASE", self.base_control_path, "-L", self.right_name,
             self.right_control_path),
            stdout=open(self.merged_control_path, 'w'))
        if rc != 0:
            return False

        self.modified = \
            md5sum(self.merged_control_path) != self.orig_right_md5sum
        return True

    def run(self):
        if self.do_diff3():
            return True

        merge_funcs = (
            (self.merge_uploaders, ()),
            (self.merge_paragraphs, ()),
            (self.merge_recommends, ()),
            (self.merge_suggests, ()),
            (self.merge_architecture, ()),
            (self.merge_depends, (None, 'Build-Depends')),
            (self.merge_depends, (None, 'Build-Depends-Indep')),
            (self.merge_depends, (None, 'Build-Conflicts')),
            (self.merge_package_depends, ('Depends', )),
            (self.merge_package_depends, ('Replaces', )),
            (self.merge_package_depends, ('Provides', )),
            (self.remove_comments, ()),
        )

        # Try all the merge strategies until diff3 is happy
        for func, args in merge_funcs:
            func(*args)
            if self.do_diff3():
                return True

        return False

    def __restore_original_value(self, package, field):
        left_para = self.left_control.get_paragraph(package)
        base_para = self.base_control.get_paragraph(package)
        if left_para is None or base_para is None:
            return False

        # See if we've modified the field in our version
        base_value = base_para.get(field)
        if base_value == left_para.get(field):
            return False

        # Yes, it's been modified.
        logger.debug('Restoring %s to base value', field_name(package, field))

        if field not in left_para:
            # If we totally removed the field, we should restore it.
            # But because removing is easier than readding, we just remove
            # the field from the base version.
            self.base_control.remove_field(package, field)
            self.base_control.write()
        elif base_value is None:
            # If the field was not present in the base vesion, just remove
            # it from the left version
            self.left_control.remove_field(package, field)
            self.left_control.write()
        else:
            # Otherwise restore the left field with the value from base
            self.left_control.patch(package, field, base_value)
            self.left_control.write()
        return True

    def __restore_original_package_values(self, field):
        modified_packages = []

        for left_pkg in self.left_control.parse():
            if 'Package' not in left_pkg:
                continue
            pkg = left_pkg['Package']
            modified = \
                self.__restore_original_value(pkg, field)
            if modified:
                modified_packages.append(pkg)

        return modified_packages

    # Endless no longer install recommended packages, so we can eliminate
    # merge noise by dropping changes we had made to Recommends.
    def merge_recommends(self):
        modified_packages = \
            self.__restore_original_package_values('Recommends')
        for pkg in modified_packages:
            self.record_note(
                'Dropped uninteresting %s Recommends change' % pkg, True)

    # Endless doesn't install suggested packages, so we can eliminate
    # merge noise by dropping any changes to Suggests.
    def merge_suggests(self):
        modified_packages = \
            self.__restore_original_package_values('Suggests')
        for pkg in modified_packages:
            self.record_note('Dropped uninteresting %s Suggests change' % pkg,
                             True)

    # Drop Uploaders changes from the left side as they are irrelevant and
    # just generate merge noise.
    def merge_uploaders(self):
        if self.__restore_original_value(None, 'Uploaders'):
            self.record_note('Dropped uninteresting Uploaders change')

    # If the left side appended to the Architectures list for a package,
    # re-append the same additions to the right side packages
    def merge_architecture(self):
        for left_pkg in self.left_control.parse():
            if 'Package' not in left_pkg or 'Architecture' not in left_pkg:
                continue

            package = left_pkg['Package']
            base_pkg = self.base_control.get_paragraph(package)
            if not base_pkg or 'Architecture' not in base_pkg:
                continue

            base_arch = unicode(base_pkg['Architecture'])
            left_arch = unicode(left_pkg['Architecture'])

            if base_arch == left_arch or not left_arch.startswith(base_arch):
                continue

            # Left package appended something onto the Architecture list.
            added_part = left_arch[len(base_arch):]

            # If the package was removed on the right, just drop our
            # local change.
            right_pkg = self.right_control.get_paragraph(package)
            if right_pkg is None:
                self.__restore_original_value(package, 'Architecture')
                self.record_note(
                    'Dropped change to add %s architecture(s) '
                    '%s because this package was removed '
                    'upstream' % (package, added_part), True)
                continue

            if 'Architecture' not in right_pkg:
                continue

            # Re-apply those changes on the right
            right_arch = unicode(right_pkg['Architecture'])
            new_value = right_arch + added_part
            self.right_control.patch(package, 'Architecture', new_value)
            self.right_control.write()

            # And drop our local change to prevent it being remerged
            self.left_control.patch(package, 'Architecture', base_arch)
            self.left_control.write()

            self.record_note('Readded architecture(s) %s to %s' %
                             (added_part.strip(), package))

    def merge_paragraph(self, package):
        base_para = self.base_control.get_paragraph(package)
        left_para = self.left_control.get_paragraph(package)
        right_para = self.right_control.get_paragraph(package)

        added_fields = set(left_para.keys()) - set(base_para.keys())
        if right_para is not None:
            added_fields -= set(right_para.keys())
        else:
            added_fields.clear()

        for field in added_fields:
            self.right_control.add_field(package, field, left_para[field])
            self.right_control.write()

            # Remove the field from left version to ease the merge
            self.left_control.remove_field(package, field)
            self.left_control.write()

            self.record_note('Readded %s %s' % (package or '', field))

    # Perform simple merge operations on paragraphs and their contents
    def merge_paragraphs(self):
        self.merge_paragraph(None)

        for pkg in self.base_control.get_package_names():
            if self.left_control.get_paragraph(pkg):
                self.merge_paragraph(pkg)
                continue

            logger.debug('Carrying forward %s package removal', pkg)

            # Package was removed on left, so remove it on the right
            self.right_control.remove_package(pkg)
            self.right_control.write()

            # To ease the merge, also remove it from the base version
            self.base_control.remove_package(pkg)
            self.base_control.write()

            self.record_note('Carried forward removal of %s binary package ' %
                             pkg)

        for pkg in self.left_control.get_package_names():
            if self.base_control.get_paragraph(pkg):
                continue

            logger.debug('Carrying forward %s package addition', pkg)
            para = self.left_control.get_paragraph(pkg)

            # Package was added on left, so add it on the right
            self.right_control.add_paragraph(unicode(para))
            self.right_control.write()

            # To ease the merge, drop the package addition from the left
            self.left_control.remove_package(pkg)
            self.left_control.write()

            self.record_note('Carried forward addition of %s binary package' %
                             pkg)

    def __merge_modified_version_constraint(self, package, field, entry,
                                            base_dep, left_dep, right_dep):
        if right_dep is None:
            logging.debug('Drop modified %s %s', field_name(package, field),
                          entry)
            self.left_control.patch_at_offset(left_dep.position, base_dep)
            self.left_control.write()

            self.record_note(
                'Dropped modified %s %s because this '
                'dependency disappeared from the upstream '
                'version' % (field_name(package, field), entry), True)
            return

        base_vc = base_dep.version_constraint
        left_vc = left_dep.version_constraint
        right_vc = right_dep.version_constraint

        # If the left version increases the minimum version required,
        # but the right version increases it even more, then drop our
        # left-side change.
        if right_vc is not None and right_vc.startswith(">") \
                and base_vc is not None and base_vc.startswith(">") \
                and left_vc is not None and left_vc.startswith(">"):
            base_min = Version(base_vc.split()[1])
            left_min = Version(left_vc.split()[1])
            right_min = Version(right_vc.split()[1])
            if left_min > base_min and right_min > left_min:
                # Restore original version constraint on the left
                self.left_control.patch_at_offset(left_vc.position, base_vc)
                self.left_control.write()

                self.record_note(
                    'Dropped modified %s %s %s version '
                    'constraint because upstream increased it '
                    'further' % (field_name(package, field), left_vc, entry),
                    True)
                return

        if right_vc is not None:
            logging.debug('Apply %s %s modified version constraint',
                          field_name(package, field), entry)

            self.right_control.patch_at_offset(right_vc.position, left_vc)
            self.right_control.write()

            # Restore original version constraint on the left to ease the merge
            self.left_control.patch_at_offset(left_vc.position, base_vc)
            self.left_control.write()

            self.record_note('Reapplied modified %s %s %s version '
                             'constraint' %
                             (field_name(package, field), left_vc, entry))

    def __merge_added_dep(self, package, field, entry, left_dep, right_dep):
        if right_dep is None:
            # If a dependency was added on the left, but is not present on
            # the right, add it there.
            self.right_control.add_depends_entry(package, field,
                                                 unicode(left_dep))
            self.right_control.write()
            self.record_note('Carried forward change to add %s %s' %
                             (field_name(package, field), entry))
        else:
            # If a dependency was added on the left and is now present
            # on the right, we can just drop the change on the left.
            self.record_note(
                'Dropped %s %s addition since it is now present '
                'on the right' % (field_name(package, field), entry), True)

        # Drop our change on the left to ease the merge
        self.left_control.remove_depends_entry(package, field, entry)
        self.left_control.write()

    # Merge Dependencies fields
    def merge_depends(self, package, field):
        base_text = self.base_control.get_paragraph(package).get(field)
        left_text = self.left_control.get_paragraph(package).get(field)

        if base_text == left_text:
            return

        base_deps = self.base_control.parse_depends(package, field)
        left_deps = self.left_control.parse_depends(package, field)
        right_deps = self.right_control.parse_depends(package, field)

        logger.debug('Detected %s difference, trying to merge',
                     field_name(package, field))

        # For each build dependency on the left, check if it has a modified
        # version constraint compared to the base.
        # If so, and if the right version doesn't have that build dependency
        # at all, drop our local change by restoring the original version
        # constraint.
        for pkg in left_deps.keys():
            if pkg not in base_deps:
                continue

            base_bdep = base_deps[pkg]
            left_bdep = left_deps[pkg]
            if left_bdep.version_constraint == base_bdep.version_constraint:
                continue

            right_bdep = right_deps.get(pkg)
            self.__merge_modified_version_constraint(package, field, pkg,
                                                     base_bdep, left_bdep,
                                                     right_bdep)

            # Reparse after changes
            left_deps = self.left_control.parse_depends(package, field)
            right_deps = self.right_control.parse_depends(package, field)

        # If a package was removed in the left version and also on the right,
        # remove it from the base version in order to ease up the merge.
        for pkg in base_deps.keys():
            if pkg in left_deps or pkg in right_deps:
                continue

            logging.debug(
                'Dropping %s %s from base as it was dropped from '
                'left and right', field_name(package, field), pkg)
            self.base_control.remove_depends_entry(package, field, pkg)
            self.base_control.write()

            # Reparse after changes
            base_deps = self.base_control.parse_depends(package, field)

            self.record_note(
                'Dropped removed %s %s because this '
                'dependency also disappeared from the upstream '
                'version' % (field_name(package, field), pkg), True)

        # If a package was removed in the left version and can still be
        # removed from the right, remove it from base and right to ease up
        # the merge. (The more logical alternative of reintroducing the
        # dep on the left side is more complicated than removing...)
        for pkg in base_deps.keys():
            if pkg in left_deps or pkg not in right_deps:
                continue

            logging.debug(
                'Dropping %s %s from base and right as it was '
                'dropped from left', field_name(package, field), pkg)
            self.base_control.remove_depends_entry(package, field, pkg)
            self.base_control.write()

            self.right_control.remove_depends_entry(package, field, pkg)
            self.right_control.write()

            # Reparse after changes
            base_deps = self.base_control.parse_depends(package, field)
            right_deps = self.right_control.parse_depends(package, field)

            self.record_note('Carried forward removal of %s %s' %
                             (field_name(package, field), pkg))

        # Handle dependencies added on the left
        for pkg in left_deps.keys():
            if pkg in base_deps:
                continue

            self.__merge_added_dep(package, field, pkg, left_deps[pkg],
                                   right_deps.get(pkg))

            # Reparse after changes
            left_deps = self.left_control.parse_depends(package, field)
            right_deps = self.right_control.parse_depends(package, field)

        # Handle dependencies where an arch list was added on the left,
        # where the base did not have any list.
        for pkg in base_deps.keys():
            if pkg not in left_deps or pkg not in right_deps:
                continue

            left_bdep = left_deps[pkg]
            base_bdep = base_deps[pkg]

            if base_bdep.arch_list is not None or left_bdep.arch_list is None:
                continue

            right_bdep = right_deps[pkg]

            logger.debug('Carrying over %s %s added arch list',
                         field_name(package, field), pkg)

            new_val = unicode(right_bdep) + " [" + left_bdep.arch_list + "]"
            self.right_control.patch_at_offset(right_bdep.position, new_val)
            self.right_control.write()

            # Drop the original change on the left side by restoring the
            # dependency from the base version, in hope of aiding the merge.
            self.left_control.patch_at_offset(left_bdep.position, base_bdep)
            self.left_control.write()

            self.record_note('Carried forward change to add arch list '
                             '[%s] to %s %s' %
                             (left_bdep.arch_list, field, pkg))

        # If there's no remaining difference in the actual build-depends list
        # and it's just changes in spaces/commas/etc then just drop our
        # changes on the left.
        base_text = self.base_control.get_paragraph(package).get(field)
        left_text = self.left_control.get_paragraph(package).get(field)
        if base_text != left_text \
                and sorted(left_deps.items()) == sorted(base_deps.items()):
            self.__restore_original_value(package, field)
            self.record_note('Tweaked %s syntax to ease the merge' %
                             field_name(package, field))

    def merge_package_depends(self, field):
        for pkg in self.base_control.get_package_names():
            if self.left_control.get_paragraph(pkg) \
                    and self.right_control.get_paragraph(pkg):
                self.merge_depends(pkg, field)

    def __write_non_comments(self, filename):
        with open(filename, 'r') as fd:
            base_text = [line for line in fd if not line.startswith('#')]
        with open(filename, 'w') as fd:
            fd.writelines(base_text)

    # If we couldn't find a more intelligent merge, try to drop any changes
    # that were made to the comments in the file. This is done with a heavy
    # handed approach of removing all the comments from base and left.
    def remove_comments(self):
        with open(self.base_control_path, 'r') as fd:
            base_comments = [line for line in fd if line.startswith('#')]

        with open(self.left_control_path, 'r') as fd:
            left_comments = [line for line in fd if line.startswith('#')]

        if base_comments == left_comments:
            return

        logger.debug('Drop all comments from base and left to ease the merge')
        self.__write_non_comments(self.base_control_path)
        self.__write_non_comments(self.left_control_path)
        self.record_note('Dropped comment changes to simplify the merge', True)