From bd9ce26461ee138f996397522180f4fd85b4b9ae Mon Sep 17 00:00:00 2001 From: Tomas Kopecek Date: Apr 14 2020 07:09:49 +0000 Subject: clone-tag --no-delete mode Fixes: https://pagure.io/koji/issue/1384 --- diff --git a/cli/koji_cli/commands.py b/cli/koji_cli/commands.py index 8880732..3350731 100644 --- a/cli/koji_cli/commands.py +++ b/cli/koji_cli/commands.py @@ -3377,6 +3377,11 @@ 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. " + "Note, that you can end with older latest builds in dest " + "than in src, if they are already tagged.")) parser.add_option('--event', type='int', help=_('Clone tag at a specific event')) parser.add_option('--repo', type='int', @@ -3644,27 +3649,28 @@ def handle_clone_tag(goptions, session, args): if not options.test: session.multiCall(batch=options.batch) # DEL builds. To keep the order we should untag builds at first - if not options.test: - session.multicall = True - for build in bdellist: - # don't delete an inherited build. - if build['tag_name'] == dsttag['name']: - # 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'])) - # go on del builds from new tag. - if not options.test: - session.untagBuildBypass(dsttag['name'], - build, - force=options.force, - notify=options.notify) - if not options.test: - session.multiCall(batch=options.batch) + if options.delete: + if not options.test: + session.multicall = True + for build in bdellist: + # don't delete an inherited build. + if build['tag_name'] == dsttag['name']: + # 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'])) + # go on del builds from new tag. + if not options.test: + session.untagBuildBypass(dsttag['name'], + build, + force=options.force, + notify=options.notify) + if not options.test: + session.multiCall(batch=options.batch) # ADD builds. if not options.test: session.multicall = True @@ -3715,103 +3721,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'])) + # 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.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.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..5b64bef 100644 --- a/tests/test_cli/test_clone_tag.py +++ b/tests/test_cli/test_clone_tag.py @@ -622,6 +622,256 @@ List of changes: [blk] cpkg group2 """) + @mock.patch('sys.stdout', new_callable=six.StringIO) + def test_handle_clone_tag_existing_dsttag_add(self, stdout): + args = ['src-tag', 'dst-tag', '--all', '-v', '--no-delete'] + self.session.multiCall.return_value = [] + self.session.listPackages.side_effect = [[{'package_id': 1, + 'package_name': 'pkg1', + 'blocked': False, + 'owner_name': 'userA', + 'tag_name': 'src-tag', + 'extra_arches': None}, + {'package_id': 2, + 'package_name': 'pkg2', + 'blocked': True, + 'owner_name': 'userB', + 'tag_name': 'src-tag-p', + 'extra_arches': 'arch3 arch4'}, + {'package_id': 3, + 'package_name': 'apkg', + 'blocked': False, + 'owner_name': 'userA', + 'tag_name': 'src-tag-p', + 'extra_arches': 'arch4'}], + [{'package_id': 1, + 'package_name': 'pkg1', + 'blocked': False, + 'owner_name': 'userA', + 'tag_name': 'src-tag', + 'extra_arches': None}, + {'package_id': 3, + 'package_name': 'apkg', + 'blocked': False, + 'owner_name': 'userA', + 'tag_name': 'src-tag-p', + 'extra_arches': 'arch4'}, + {'package_id': 4, + 'package_name': 'bpkg', + 'blocked': False, + 'owner_name': 'userC', + 'tag_name': 'src-tag', + 'extra_arches': 'arch4'}, + {'package_id': 5, + 'package_name': 'cpkg', + 'blocked': True, + 'owner_name': 'userC', + 'tag_name': 'src-tag-p', + 'extra_arches': 'arch4'}, + {'package_id': 6, + 'package_name': 'dpkg', + 'blocked': True, + 'owner_name': 'userC', + 'tag_name': 'src-tag', + 'extra_arches': 'arch4'} + ]] + self.session.listTagged.side_effect = [[{'package_name': 'pkg1', + 'nvr': 'pkg1-1.1-2', + 'state': 1, + 'owner_name': 'b_owner', + 'tag_name': 'src-tag'}, + {'package_name': 'pkg1', + 'nvr': 'pkg1-1.0-2', + 'state': 1, + 'owner_name': 'b_owner', + 'tag_name': 'src-tag'}, + {'package_name': 'pkg1', + 'nvr': 'pkg1-0.1-1', + 'state': 1, + 'owner_name': 'b_owner', + 'tag_name': 'src-tag'}, + {'package_name': 'pkg1', + 'nvr': 'pkg1-1.0-1', + 'state': 1, + 'owner_name': 'b_owner', + 'tag_name': 'src-tag'}, + {'package_name': 'pkg2', + 'nvr': 'pkg2-1.0-1', + 'state': 2, + 'owner_name': 'b_owner', + 'tag_name': 'src-tag-p'} + ], + [{'package_name': 'pkg1', + 'nvr': 'pkg1-2.1-2', + 'state': 1, + 'owner_name': 'b_owner', + 'tag_name': 'dst-tag'}, + {'package_name': 'pkg1', + 'nvr': 'pkg1-1.0-1', + 'state': 1, + 'owner_name': 'b_owner', + 'tag_name': 'dst-tag'}, + {'package_name': 'pkg1', + 'nvr': 'pkg1-0.1-1', + 'state': 1, + 'owner_name': 'b_owner', + 'tag_name': 'dst-tag'}, + {'package_name': 'pkg2', + 'nvr': 'pkg2-1.0-1', + 'state': 2, + 'owner_name': 'b_owner', + 'tag_name': 'dst-tag'}, + {'package_name': 'pkg3', + 'nvr': 'pkg3-1.0-1', + 'state': 1, + 'owner_name': 'b_owner', + 'tag_name': 'dst-tag'} + ]] + self.session.getTagGroups.side_effect = [[{'name': 'group1', + 'tag_id': 1, + 'packagelist': [ + {'package': 'pkg1', + 'blocked': False}, + {'package': 'pkg2', + 'blocked': False}, + {'package': 'pkg3', + 'blocked': False}, + {'package': 'pkg4', + 'blocked': False} + ]}, + {'name': 'group2', + 'tag_id': 1, + 'packagelist': [ + {'package': 'apkg', + 'blocked': False}, + {'package': 'bpkg', + 'blocked': False}] + }], + [{'name': 'group1', + 'tag_id': 2, + 'packagelist': [ + {'package': 'pkg1', + 'blocked': False}, + {'package': 'pkg5', + 'blocked': False} + ]}, + {'name': 'group2', + 'tag_id': 3, + 'packagelist': [ + {'package': 'apkg', + 'blocked': False}, + {'package': 'cpkg', + 'blocked': False}]}, + {'name': 'group3', + 'tag_id': 2, + 'packagelist': [ + {'package': 'cpkg', + 'blocked': False}, + {'package': 'dpkg', + 'blocked': False}]}, + {'name': 'group4', + 'tag_id': 3, + 'packagelist': [ + {'package': 'epkg', + 'blocked': False}, + {'package': 'fpkg', + 'blocked': False}]} + ]] + 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.session.assert_has_calls([call.hasPerm('admin'), + call.getTag('src-tag'), + call.getTag('dst-tag'), + call.listPackages(event=None, + inherited=True, + tagID=1), + call.listPackages(inherited=True, + tagID=2), + call.listTagged(1, event=None, + inherit=None, + latest=None), + call.listTagged(2, inherit=False, + latest=False), + call.getTagGroups('src-tag', + event=None), + call.getTagGroups('dst-tag'), + call.packageListAdd('dst-tag', 'pkg2', + block=True, + extra_arches='arch3 arch4', + owner='userB'), + call.multiCall(batch=1000), + call.tagBuildBypass('dst-tag', { + 'owner_name': 'b_owner', + 'nvr': 'pkg1-0.1-1', + 'package_name': 'pkg1', 'state': 1, + 'tag_name': 'src-tag', + 'name': 'pkg1'}, force=None), + call.tagBuildBypass('dst-tag', { + 'owner_name': 'b_owner', + 'nvr': 'pkg1-1.0-2', + 'package_name': 'pkg1', 'state': 1, + 'tag_name': 'src-tag', + 'name': 'pkg1'}, force=None), + call.tagBuildBypass('dst-tag', { + 'owner_name': 'b_owner', + 'nvr': 'pkg1-1.1-2', + 'package_name': 'pkg1', 'state': 1, + 'tag_name': 'src-tag', + 'name': 'pkg1'}, force=None), + call.multiCall(batch=1000), + call.multiCall(batch=1000), + call.groupPackageListAdd('dst-tag', + 'group1', + 'pkg2', + force=None), + call.groupPackageListAdd('dst-tag', + 'group1', + 'pkg3', + force=None), + call.groupPackageListAdd('dst-tag', + 'group1', + 'pkg4', + force=None), + call.groupPackageListAdd('dst-tag', + 'group2', + 'bpkg', + force=None), + call.multiCall(batch=1000)]) + self.assert_console_message(stdout, """ +List of changes: + + Action Package Blocked Owner From Tag + ------- ---------------------------- ---------- ---------- ---------- + [add] pkg2 True userB src-tag-p + + Action From/To Package Build(s) State Owner From Tag + ------- ---------------------------- ---------------------------------------- ---------- ---------- ---------- + [add] pkg1 pkg1-0.1-1 COMPLETE b_owner src-tag + [add] pkg1 pkg1-1.0-2 COMPLETE b_owner src-tag + [add] pkg1 pkg1-1.1-2 COMPLETE b_owner src-tag + + Action Package Group + ------- ---------------------------- ---------------------------- + [new] pkg2 group1 + [new] pkg3 group1 + [new] pkg4 group1 + [new] bpkg group2 +""") + def test_handle_clone_tag_help(self): self.assert_help( handle_clone_tag, @@ -640,6 +890,9 @@ 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. Note, that + you can end with older latest builds in dest than in src, + if they are already tagged. --event=EVENT Clone tag at a specific event --repo=REPO Clone tag at a specific repo event -v, --verbose show changes