From 207b9a0961dd837d40b9b92de865bf3ba773acd7 Mon Sep 17 00:00:00 2001 From: Tomas Kopecek Date: Apr 14 2020 07:09:57 +0000 Subject: PR#1385: Add --no-delete option to clone-tag Merges #1385 https://pagure.io/koji/pull-request/1385 Fixes: #1384 https://pagure.io/koji/issue/1384 [RFE] Add --no-delete option to clone-tag --- diff --git a/cli/koji_cli/commands.py b/cli/koji_cli/commands.py index 8880732..6007a87 100644 --- a/cli/koji_cli/commands.py +++ b/cli/koji_cli/commands.py @@ -3377,6 +3377,9 @@ def handle_clone_tag(goptions, session, args): "the dest tag")) parser.add_option('--ts', type='int', metavar="TIMESTAMP", help=_('Clone tag at last event before specific timestamp')) + parser.add_option('--no-delete', action='store_false', dest="delete", + default=True, + help=_("Don't delete any existing content in dest tag.")) parser.add_option('--event', type='int', help=_('Clone tag at a specific event')) parser.add_option('--repo', type='int', @@ -3526,6 +3529,8 @@ def handle_clone_tag(goptions, session, args): inherited=True): dstpkgs[pkg['package_name']] = pkg if options.builds: + # listTagged orders builds latest-first + # so reversing that gives us oldest-first for build in reversed(session.listTagged(srctag['id'], event=event.get('id'), inherit=options.inherit_builds, @@ -3557,26 +3562,46 @@ def handle_clone_tag(goptions, session, args): pdellist.sort(key=lambda x: x['package_name']) baddlist = [] # list containing new builds to be added from src tag bdellist = [] # list containing new builds to be removed from dst tag - for (pkg, dstblds) in six.iteritems(dstbldsbypkg): - if pkg not in srcbldsbypkg: - bdellist.extend(dstblds.values()) + if options.delete: + # remove builds for packages that are absent from src tag + for (pkg, dstblds) in six.iteritems(dstbldsbypkg): + if pkg not in srcbldsbypkg: + bdellist.extend(dstblds.values()) + # add and/or remove builds from dst to match src contents and order for (pkg, srcblds) in six.iteritems(srcbldsbypkg): dstblds = dstbldsbypkg[pkg] ablds = [] dblds = [] - # firstly, remove builds from dst tag + # firstly, deal with extra builds in dst removed_nvrs = set(dstblds.keys()) - set(srcblds.keys()) - dnvrs = [] - for (dstnvr, dstbld) in six.iteritems(dstblds): - if dstnvr in removed_nvrs: - dnvrs.append(dstnvr) - dblds.append(dstbld) - for dnvr in dnvrs: - del dstblds[dnvr] + bld_order = srcblds.copy() + if options.delete: + # mark the extra builds for deletion + dnvrs = [] + for (dstnvr, dstbld) in six.iteritems(dstblds): + if dstnvr in removed_nvrs: + dnvrs.append(dstnvr) + dblds.append(dstbld) + # we also remove them from dstblds now so that they do not + # interfere with the order comparison below + for dnvr in dnvrs: + del dstblds[dnvr] + else: + # in the no-delete case, the extra builds should be forced + # to last in the tag + bld_order = OrderedDict() + for (dstnvr, dstbld) in six.iteritems(dstblds): + if dstnvr in removed_nvrs: + bld_order[dstnvr] = dstbld + for (nvr, srcbld) in six.iteritems(srcblds): + bld_order[nvr] = srcbld # secondly, add builds from src tag and adjust the order - for (nvr, srcbld) in six.iteritems(srcblds): + for (nvr, srcbld) in six.iteritems(bld_order): found = False out_of_order = [] + # note that dstblds is trimmed as we go, so we are only + # considering the tail corresponding to where we are at + # in the srcblds loop for (dstnvr, dstbld) in six.iteritems(dstblds): if nvr == dstnvr: found = True @@ -3592,11 +3617,13 @@ def handle_clone_tag(goptions, session, args): # loop del dstblds[nvr] else: + # missing from dst, so we need to add it ablds.append(srcbld) baddlist.extend(ablds) bdellist.extend(dblds) baddlist.sort(key=lambda x: x['package_name']) bdellist.sort(key=lambda x: x['package_name']) + gaddlist = [] # list containing new groups to be added from src tag for (grpname, group) in six.iteritems(srcgroups): if grpname not in dstgroups: @@ -3715,103 +3742,104 @@ def handle_clone_tag(goptions, session, args): force=options.force) if not options.test: session.multiCall(batch=options.batch) - # DEL packages. - ninhrtpdellist = [] - inhrtpdellist = [] - for pkg in pdellist: - if pkg['tag_name'] == dsttag['name']: - ninhrtpdellist.append(pkg) - else: - inhrtpdellist.append(pkg) - session.multicall = True - # delete only non-inherited packages. - for pkg in ninhrtpdellist: - # check if package have owned builds inside. - session.listTagged(dsttag['name'], - package=pkg['package_name'], - inherit=False) - bump_builds = session.multiCall(batch=options.batch) - if not options.test: + if options.delete: + # DEL packages + ninhrtpdellist = [] + inhrtpdellist = [] + for pkg in pdellist: + if pkg['tag_name'] == dsttag['name']: + ninhrtpdellist.append(pkg) + else: + inhrtpdellist.append(pkg) session.multicall = True - for pkg, [builds] in zip(ninhrtpdellist, bump_builds): - # remove all its builds first if there are any. - for build in builds: - # add missing 'name' field. - build['name'] = build['package_name'] - chgbldlist.append(('[del]', - build['package_name'], - build['nvr'], - koji.BUILD_STATES[build['state']], - build['owner_name'], - build['tag_name'])) - # so delete latest build(s) from new tag. - if not options.test: - session.untagBuildBypass(dsttag['name'], - build, - force=options.force, - notify=options.notify) - # now safe to remove package itself since we resolved its builds. - chgpkglist.append(('[del]', - pkg['package_name'], - pkg['blocked'], - pkg['owner_name'], - pkg['tag_name'])) - if not options.test: - session.packageListRemove(dsttag['name'], - pkg['package_name'], - force=False) - # mark as blocked inherited packages. - for pkg in inhrtpdellist: - chgpkglist.append(('[blk]', - pkg['package_name'], - pkg['blocked'], - pkg['owner_name'], - pkg['tag_name'])) + # delete only non-inherited packages. + for pkg in ninhrtpdellist: + # check if package have owned builds inside. + session.listTagged(dsttag['name'], + package=pkg['package_name'], + inherit=False) + bump_builds = session.multiCall(batch=options.batch) if not options.test: - session.packageListBlock(dsttag['name'], pkg['package_name']) - if not options.test: - session.multiCall(batch=options.batch) - # DEL groups. - if not options.test: - session.multicall = True - for group in gdellist: - # Only delete a group that isn't inherited - if group['tag_id'] == dsttag['id']: + session.multicall = True + for pkg, [builds] in zip(ninhrtpdellist, bump_builds): + # remove all its builds first if there are any. + for build in builds: + # add missing 'name' field. + build['name'] = build['package_name'] + chgbldlist.append(('[del]', + build['package_name'], + build['nvr'], + koji.BUILD_STATES[build['state']], + build['owner_name'], + build['tag_name'])) + # so delete latest build(s) from new tag. + if not options.test: + session.untagBuildBypass(dsttag['name'], + build, + force=options.force, + notify=options.notify) + # now safe to remove package itself since we resolved its builds. + chgpkglist.append(('[del]', + pkg['package_name'], + pkg['blocked'], + pkg['owner_name'], + pkg['tag_name'])) if not options.test: - session.groupListRemove(dsttag['name'], - group['name'], - force=options.force) - for pkg in group['packagelist']: - chggrplist.append(('[del]', pkg['package'], group['name'])) - # mark as blocked inherited groups. - else: + session.packageListRemove(dsttag['name'], + pkg['package_name'], + force=False) + # mark as blocked inherited packages. + for pkg in inhrtpdellist: + chgpkglist.append(('[blk]', + pkg['package_name'], + pkg['blocked'], + pkg['owner_name'], + pkg['tag_name'])) if not options.test: - session.groupListBlock(dsttag['name'], group['name']) - for pkg in group['packagelist']: - chggrplist.append(('[blk]', pkg['package'], group['name'])) - if not options.test: - session.multiCall(batch=options.batch) - # DEL group pkgs. - if not options.test: - session.multicall = True - for group in grpchanges: - for pkg in grpchanges[group]['dels']: + session.packageListBlock(dsttag['name'], pkg['package_name']) + if not options.test: + session.multiCall(batch=options.batch) + # DEL groups. + if not options.test: + session.multicall = True + for group in gdellist: # Only delete a group that isn't inherited - if not grpchanges[group]['inherited']: - chggrplist.append(('[del]', pkg, group)) + if group['tag_id'] == dsttag['id']: if not options.test: - session.groupPackageListRemove(dsttag['name'], - group, - pkg, - force=options.force) + session.groupListRemove(dsttag['name'], + group['name'], + force=options.force) + for pkg in group['packagelist']: + chggrplist.append(('[del]', pkg['package'], group['name'])) + # mark as blocked inherited groups. else: - chggrplist.append(('[blk]', pkg, group)) if not options.test: - session.groupPackageListBlock(dsttag['name'], - group, - pkg) - if not options.test: - session.multiCall(batch=options.batch) + session.groupListBlock(dsttag['name'], group['name']) + for pkg in group['packagelist']: + chggrplist.append(('[blk]', pkg['package'], group['name'])) + if not options.test: + session.multiCall(batch=options.batch) + # DEL group pkgs. + if not options.test: + session.multicall = True + for group in grpchanges: + for pkg in grpchanges[group]['dels']: + # Only delete a group that isn't inherited + if not grpchanges[group]['inherited']: + chggrplist.append(('[del]', pkg, group)) + if not options.test: + session.groupPackageListRemove(dsttag['name'], + group, + pkg, + force=options.force) + else: + chggrplist.append(('[blk]', pkg, group)) + if not options.test: + session.groupPackageListBlock(dsttag['name'], + group, + pkg) + if not options.test: + session.multiCall(batch=options.batch) # print final list of actions. if options.verbose: pfmt = ' %-7s %-28s %-10s %-10s %-10s\n' diff --git a/tests/test_cli/test_clone_tag.py b/tests/test_cli/test_clone_tag.py index 1fde1ce..3d30f5b 100644 --- a/tests/test_cli/test_clone_tag.py +++ b/tests/test_cli/test_clone_tag.py @@ -622,6 +622,209 @@ List of changes: [blk] cpkg group2 """) + @mock.patch('sys.stdout', new_callable=six.StringIO) + def test_handle_clone_tag_existing_dsttag_nodelete(self, stdout): + args = ['src-tag', 'dst-tag', '--all', '-v', '--no-delete'] + self.session.multiCall.return_value = [] + self.session.listPackages.return_value = [] + self.session.listTagged.side_effect = [[{'id': 1, + 'package_name': 'pkg', + 'nvr': 'pkg-1.0-23', + 'state': 1, + 'owner_name': 'b_owner', + 'tag_name': 'src-tag'}, + {'id': 2, + 'package_name': 'pkg', + 'nvr': 'pkg-1.0-21', + 'state': 1, + 'owner_name': 'b_owner', + 'tag_name': 'src-tag'}, + {'id': 3, + 'package_name': 'pkg', + 'nvr': 'pkg-0.1-1', + 'state': 1, + 'owner_name': 'b_owner', + 'tag_name': 'src-tag'}, + ], + [], + ] + self.session.getTagGroups.return_value = [] + self.session.getTag.side_effect = [{'id': 1, + 'name': 'src-tag', + 'arches': 'arch1 arch2', + 'perm_id': 1, + 'maven_support': False, + 'maven_include_all': True, + 'locked': False}, + {'id': 2, + 'name': 'dst-tag', + 'arches': 'arch1 arch2', + 'perm_id': 1, + 'maven_support': False, + 'maven_include_all': True, + 'locked': False}] + handle_clone_tag(self.options, self.session, args) + self.activate_session.assert_called_once() + self.assert_console_message(stdout, """ +List of changes: + + Action Package Blocked Owner From Tag + ------- ---------------------------- ---------- ---------- ---------- + + Action From/To Package Build(s) State Owner From Tag + ------- ---------------------------- ---------------------------------------- ---------- ---------- ---------- + [add] pkg pkg-0.1-1 COMPLETE b_owner src-tag + [add] pkg pkg-1.0-21 COMPLETE b_owner src-tag + [add] pkg pkg-1.0-23 COMPLETE b_owner src-tag + + Action Package Group + ------- ---------------------------- ---------------------------- +""") + + @mock.patch('sys.stdout', new_callable=six.StringIO) + def test_handle_clone_tag_existing_dsttag_nodelete_1(self, stdout): + args = ['src-tag', 'dst-tag', '--all', '-v', '--no-delete'] + self.session.multiCall.return_value = [] + self.session.listPackages.return_value = [] + self.session.listTagged.side_effect = [[{'id': 1, + 'package_name': 'pkg', + 'nvr': 'pkg-1.0-23', + 'state': 1, + 'owner_name': 'b_owner', + 'tag_name': 'src-tag'}, + {'id': 2, + 'package_name': 'pkg', + 'nvr': 'pkg-1.0-21', + 'state': 1, + 'owner_name': 'b_owner', + 'tag_name': 'src-tag'}, + {'id': 3, + 'package_name': 'pkg', + 'nvr': 'pkg-0.1-1', + 'state': 1, + 'owner_name': 'b_owner', + 'tag_name': 'src-tag'}, + ], + [{'id': 1, + 'package_name': 'pkg', + 'nvr': 'pkg-1.0-23', + 'state': 1, + 'owner_name': 'b_owner', + 'tag_name': 'src-tag'}, + {'id': 3, + 'package_name': 'pkg', + 'nvr': 'pkg-0.1-1', + 'state': 1, + 'owner_name': 'b_owner', + 'tag_name': 'src-tag'}, + {'id': 2, + 'package_name': 'pkg', + 'nvr': 'pkg-1.0-21', + 'state': 1, + 'owner_name': 'b_owner', + 'tag_name': 'src-tag'}, + ] + ] + self.session.getTagGroups.return_value = [] + self.session.getTag.side_effect = [{'id': 1, + 'name': 'src-tag', + 'arches': 'arch1 arch2', + 'perm_id': 1, + 'maven_support': False, + 'maven_include_all': True, + 'locked': False}, + {'id': 2, + 'name': 'dst-tag', + 'arches': 'arch1 arch2', + 'perm_id': 1, + 'maven_support': False, + 'maven_include_all': True, + 'locked': False}] + handle_clone_tag(self.options, self.session, args) + self.activate_session.assert_called_once() + self.assert_console_message(stdout, """ +List of changes: + + Action Package Blocked Owner From Tag + ------- ---------------------------- ---------- ---------- ---------- + + Action From/To Package Build(s) State Owner From Tag + ------- ---------------------------- ---------------------------------------- ---------- ---------- ---------- + [add] pkg pkg-1.0-21 COMPLETE b_owner src-tag + [add] pkg pkg-1.0-23 COMPLETE b_owner src-tag + + Action Package Group + ------- ---------------------------- ---------------------------- +""") + + @mock.patch('sys.stdout', new_callable=six.StringIO) + def test_handle_clone_tag_existing_dsttag_nodelete_2(self, stdout): + args = ['src-tag', 'dst-tag', '--all', '-v', '--no-delete'] + self.session.multiCall.return_value = [] + self.session.listPackages.return_value = [] + self.session.listTagged.side_effect = [[{'id': 1, + 'package_name': 'pkg', + 'nvr': 'pkg-1.0-23', + 'state': 1, + 'owner_name': 'b_owner', + 'tag_name': 'src-tag'}, + {'id': 2, + 'package_name': 'pkg', + 'nvr': 'pkg-1.0-21', + 'state': 1, + 'owner_name': 'b_owner', + 'tag_name': 'src-tag'}, + {'id': 3, + 'package_name': 'pkg', + 'nvr': 'pkg-0.1-1', + 'state': 1, + 'owner_name': 'b_owner', + 'tag_name': 'src-tag'}, + ], + [{'id': 2, + 'package_name': 'pkg', + 'nvr': 'pkg-1.0-21', + 'state': 1, + 'owner_name': 'b_owner', + 'tag_name': 'src-tag'}, + {'id': 3, + 'package_name': 'pkg', + 'nvr': 'pkg-0.1-1', + 'state': 1, + 'owner_name': 'b_owner', + 'tag_name': 'src-tag'}, + ] + ] + self.session.getTagGroups.return_value = [] + self.session.getTag.side_effect = [{'id': 1, + 'name': 'src-tag', + 'arches': 'arch1 arch2', + 'perm_id': 1, + 'maven_support': False, + 'maven_include_all': True, + 'locked': False}, + {'id': 2, + 'name': 'dst-tag', + 'arches': 'arch1 arch2', + 'perm_id': 1, + 'maven_support': False, + 'maven_include_all': True, + 'locked': False}] + handle_clone_tag(self.options, self.session, args) + self.activate_session.assert_called_once() + self.assert_console_message(stdout, """ +List of changes: + + Action Package Blocked Owner From Tag + ------- ---------------------------- ---------- ---------- ---------- + + Action From/To Package Build(s) State Owner From Tag + ------- ---------------------------- ---------------------------------------- ---------- ---------- ---------- + [add] pkg pkg-1.0-23 COMPLETE b_owner src-tag + + Action Package Group + ------- ---------------------------- ---------------------------- +""") def test_handle_clone_tag_help(self): self.assert_help( handle_clone_tag, @@ -640,6 +843,7 @@ Options: --inherit-builds Include all builds inherited into the source tag into the dest tag --ts=TIMESTAMP Clone tag at last event before specific timestamp + --no-delete Don't delete any existing content in dest tag. --event=EVENT Clone tag at a specific event --repo=REPO Clone tag at a specific repo event -v, --verbose show changes diff --git a/tests/test_cli/utils.py b/tests/test_cli/utils.py index a84a3e8..d1c2c9b 100644 --- a/tests/test_cli/utils.py +++ b/tests/test_cli/utils.py @@ -1,6 +1,5 @@ from __future__ import print_function from __future__ import absolute_import -import locale import mock import os import six