Repository: fedora-infra/bodhi

Issue:
`bodhi updates edit UPDATE` disables autokarma and autotime
I have been using `bodhi updates edit UPDATE` _with no options_ as workaround for unlocking submitted sidetag updates.
(Yes that is probably a hack but apart from some timing issues, it seems to work.)

However I noticed anyway that this command seems to disable autokarma and autotime?
which does not seem to desireable?

Admittedly running edit with no options is probably not that common (except for this workaround perhaps),
but I would not expect it to change the state of the update.


Discussion:

Resolving patch:
diff --git a/bodhi-client/bodhi/client/cli.py b/bodhi-client/bodhi/client/cli.py
--- a/bodhi-client/bodhi/client/cli.py
+++ b/bodhi-client/bodhi/client/cli.py
@@ -126,13 +126,16 @@ def _set_logging_debug(
     click.option('--notes', help='Update description'),
     click.option('--notes-file', help='Update description from a file'),
     click.option('--bugs', help='Comma-separated list of bug numbers', default=''),
-    click.option('--close-bugs', is_flag=True, help='Automatically close bugs'),
+    click.option('--close-bugs/--no-close-bugs', default=None,
+                 help='Automatically close bugs or not'),
     click.option('--request', help='Requested repository',
                  type=click.Choice(constants.REQUEST_TYPES)),
-    click.option('--autotime', is_flag=True, help='Enable stable push base on time in testing'),
+    click.option('--autotime/--no-autotime', default=None,
+                 help='Enable/Disable stable push based on time in testing'),
     click.option('--stable-days', type=click.INT,
                  help='Days in testing required to push to stable'),
-    click.option('--autokarma', is_flag=True, help='Enable karma automatism'),
+    click.option('--autokarma/--no-autokarma', default=None,
+                 help='Enable/Disable karma automatism'),
     click.option('--stable-karma', type=click.INT, help='Stable karma threshold'),
     click.option('--unstable-karma', type=click.INT, help='Unstable karma threshold'),
     click.option('--requirements',
@@ -141,9 +144,6 @@ def _set_logging_debug(
                  type=click.Choice(constants.SUGGEST_TYPES)),
     click.option('--display-name',
                  help='The name of the update', default=None),
-    click.option('--from-tag', help='Use builds from a Koji tag instead of specifying '
-                                    'them individually.',
-                 is_flag=True),
     staging_option]
 
 
@@ -401,6 +401,8 @@ def require_severity_for_security_update(type: str, severity: str):
 @click.option('--type', default='bugfix', help='Update type', required=True,
               type=click.Choice(constants.UPDATE_TYPES))
 @add_options(new_edit_options)
+@click.option('--from-tag', is_flag=True,
+              help='Use builds from a Koji tag instead of specifying them individually')
 @click.argument('builds_or_tag')
 @click.option('--file', help='A text file containing all the update details')
 @handle_errors
@@ -412,8 +414,8 @@ def new(url: str, id_provider: str, client_id: str, debug: bool, **kwargs):
     """
     Create a new update.
 
-    BUILDS: a comma separated list of Builds to be added to the update
-    (e.g. 0ad-0.0.21-4.fc26,2ping-3.2.1-4.fc26)
+    BUILDS_OR_TAG: a comma separated list of Builds to be added to the update
+    (e.g. 0ad-0.0.21-4.fc26,2ping-3.2.1-4.fc26) or a single side-tag name
     """
     # Developer Docs
     """
@@ -430,6 +432,12 @@ def new(url: str, id_provider: str, client_id: str, debug: bool, **kwargs):
         base_url=url, client_id=client_id, id_provider=id_provider, staging=kwargs['staging']
     )
 
+    # Because some flag options are common between new and edit, we cannot set the default
+    # to be a boolean, instead we set them here
+    for option in ['close_bugs', 'autotime', 'autokarma']:
+        if kwargs[option] is None:
+            kwargs[option] = False
+
     # Because bodhi.server.services.updates expects from_tag to be string
     # copy builds to from_tag and remove builds
     if kwargs['from_tag']:
@@ -531,37 +539,30 @@ def edit(url: str, id_provider: str, client_id: str, debug: bool, **kwargs):
 
         # Convert list of 'Bug' instances in DB to comma separated bug_ids for parsing.
         former_update = resp['updates'][0].copy()
+
+        # If these flags are not set by the user we load values from the existing update
+        for option in ['close_bugs', 'autotime', 'autokarma']:
+            if kwargs[option] is None:
+                kwargs[option] = former_update[option]
+
         if not kwargs['bugs']:
             kwargs['bugs'] = ",".join([str(bug['bug_id']) for bug in former_update['bugs']])
             former_update.pop('bugs', None)
 
         kwargs['builds'] = [b['nvr'] for b in former_update['builds']]
         kwargs['edited'] = former_update['alias']
-        # Because bodhi.server.services.updates expects from_tag to be string
-        # copy builds to from_tag and remove builds
-        if kwargs['from_tag']:
-            if not former_update.get('from_tag', None):
-                click.echo(
-                    "ERROR: This update was not created from a tag."
-                    " Please remove --from_tag and try again.", err=True
-                )
-                sys.exit(1)
-            if kwargs['addbuilds'] or kwargs['removebuilds']:
+
+        if former_update.get('from_tag', None):
+            # The build list is always refreshed from the side-tag by validate_from_tag()
+            if (kwargs['addbuilds'] or kwargs['removebuilds']):
                 click.echo(
-                    "ERROR: You have to use the web interface to update"
-                    " builds in a side-tag update.", err=True
+                    "ERROR: The --addbuilds and --removebuilds options"
+                    " cannot be used with a side-tag update.", err=True
                 )
                 sys.exit(1)
             kwargs['from_tag'] = former_update['from_tag']
             del kwargs['builds']
         else:
-            if former_update.get('from_tag', None):
-                click.echo(
-                    "ERROR: This update was created from a side-tag."
-                    " Please add --from_tag and try again.", err=True
-                )
-                sys.exit(1)
-            kwargs.pop('from_tag')
             if kwargs['addbuilds']:
                 for build in kwargs['addbuilds'].split(','):
                     if build not in kwargs['builds']:
@@ -569,6 +570,7 @@ def edit(url: str, id_provider: str, client_id: str, debug: bool, **kwargs):
             if kwargs['removebuilds']:
                 for build in kwargs['removebuilds'].split(','):
                     kwargs['builds'].remove(build)
+
         del kwargs['addbuilds']
         del kwargs['removebuilds']
 


Test patch:
diff --git a/bodhi-client/tests/test_cli.py b/bodhi-client/tests/test_cli.py
--- a/bodhi-client/tests/test_cli.py
+++ b/bodhi-client/tests/test_cli.py
@@ -1581,13 +1581,13 @@ def test_bugs_flag(self, mocked_client_class, mocker):
             mock.call(
                 'updates/', auth=True, verb='POST',
                 data={
-                    'close_bugs': False, 'stable_karma': 3, 'csrf_token': 'a_csrf_token',
+                    'close_bugs': True, 'stable_karma': 3, 'csrf_token': 'a_csrf_token',
                     'staging': False, 'builds': ['nodejs-grunt-wrap-0.3.0-2.fc25'],
-                    'autokarma': False, 'edited': 'FEDORA-2017-c95b33872d',
+                    'autokarma': True, 'edited': 'FEDORA-2017-c95b33872d',
                     'suggest': 'unspecified', 'notes': 'New package.',
                     'notes_file': None, 'request': None, 'unstable_karma': -3,
                     'bugs': '1234,5678', 'requirements': '', 'type': 'newpackage',
-                    'severity': 'low', 'display_name': None, 'autotime': False,
+                    'severity': 'low', 'display_name': None, 'autotime': True,
                     'stable_days': None}),
             mock.call(
                 'updates/FEDORA-EPEL-2016-3081a94111/get-test-results',
@@ -1610,13 +1610,13 @@ def test_severity_flag(self, mocked_client_class, mocker):
             mock.call(
                 'updates/', auth=True, verb='POST',
                 data={
-                    'close_bugs': False, 'stable_karma': 3, 'csrf_token': 'a_csrf_token',
+                    'close_bugs': True, 'stable_karma': 3, 'csrf_token': 'a_csrf_token',
                     'staging': False, 'builds': ['nodejs-grunt-wrap-0.3.0-2.fc25'],
-                    'autokarma': False, 'edited': 'FEDORA-2017-c95b33872d',
+                    'autokarma': True, 'edited': 'FEDORA-2017-c95b33872d',
                     'suggest': 'unspecified', 'notes': 'Updated package.',
                     'notes_file': None, 'request': None, 'unstable_karma': -3,
                     'bugs': '1420605', 'requirements': '', 'type': 'newpackage',
-                    'severity': 'low', 'display_name': None, 'autotime': False, 'stable_days': None
+                    'severity': 'low', 'display_name': None, 'autotime': True, 'stable_days': None
                 }
             ),
             mock.call(
@@ -1644,13 +1644,13 @@ def test_url_flag(self, mocked_client_class, mocker):
             mock.call(
                 'updates/', auth=True, verb='POST',
                 data={
-                    'close_bugs': False, 'stable_karma': 3, 'csrf_token': 'a_csrf_token',
+                    'close_bugs': True, 'stable_karma': 3, 'csrf_token': 'a_csrf_token',
                     'staging': False, 'builds': ['nodejs-grunt-wrap-0.3.0-2.fc25'],
-                    'autokarma': False, 'edited': 'FEDORA-2017-c95b33872d',
+                    'autokarma': True, 'edited': 'FEDORA-2017-c95b33872d',
                     'suggest': 'unspecified', 'notes': 'this is an edited note',
                     'notes_file': None, 'request': None, 'severity': 'low',
                     'bugs': '1420605', 'requirements': '', 'unstable_karma': -3,
-                    'type': 'newpackage', 'display_name': None, 'autotime': False,
+                    'type': 'newpackage', 'display_name': None, 'autotime': True,
                     'stable_days': None,
                 }
             ),
@@ -1683,13 +1683,13 @@ def test_notes_file(self, mocked_client_class, mocker):
                 mock.call(
                     'updates/', auth=True, verb='POST',
                     data={
-                        'close_bugs': False, 'stable_karma': 3, 'csrf_token': 'a_csrf_token',
+                        'close_bugs': True, 'stable_karma': 3, 'csrf_token': 'a_csrf_token',
                         'staging': False, 'builds': ['nodejs-grunt-wrap-0.3.0-2.fc25'],
-                        'autokarma': False, 'edited': 'FEDORA-2017-c95b33872d',
+                        'autokarma': True, 'edited': 'FEDORA-2017-c95b33872d',
                         'suggest': 'unspecified', 'notes': 'This is a --notes-file note!',
                         'notes_file': 'notefile.txt', 'request': None, 'severity': 'low',
                         'bugs': '1420605', 'requirements': '', 'unstable_karma': -3,
-                        'type': 'newpackage', 'display_name': None, 'autotime': False,
+                        'type': 'newpackage', 'display_name': None, 'autotime': True,
                         'stable_days': None
                     }
                 ),
@@ -1721,14 +1721,14 @@ def test_addbuilds_removebuilds(self, mocked_client_class, mocker):
             mock.call(
                 'updates/', auth=True, verb='POST',
                 data={
-                    'close_bugs': False, 'stable_karma': 3, 'csrf_token': 'a_csrf_token',
+                    'close_bugs': True, 'stable_karma': 3, 'csrf_token': 'a_csrf_token',
                     'staging': False, 'display_name': None,
                     'builds': ['tar-1.29-4.fc25', 'nedit-5.7-1.fc25'],
-                    'autokarma': False, 'edited': 'FEDORA-2017-c95b33872d',
+                    'autokarma': True, 'edited': 'FEDORA-2017-c95b33872d',
                     'suggest': u'unspecified', 'notes': u'add and remove builds',
                     'notes_file': None, 'request': None, 'severity': u'low',
                     'bugs': '1420605', 'requirements': u'', 'unstable_karma': -3,
-                    'type': 'newpackage', 'autotime': False, 'stable_days': None
+                    'type': 'newpackage', 'autotime': True, 'stable_days': None
                 }
             ),
             mock.call(
@@ -1738,66 +1738,9 @@ def test_addbuilds_removebuilds(self, mocked_client_class, mocker):
         ]
         assert mocked_client_class.send_request.mock_calls == calls
 
-    def test_from_tag_flag(self, mocked_client_class, mocker):
-        """
-        Assert correct behavior with the --from-tag flag.
-        """
-        mocked_client_class.send_request.return_value = client_test_data.EXAMPLE_UPDATE_MUNCH
-        data = client_test_data.EXAMPLE_QUERY_MUNCH.copy()
-        data.updates[0]['from_tag'] = 'fake_tag'
-        data.updates[0]['release']['composed_by_bodhi'] = False
-        mocked_client_class.query = mocker.Mock(return_value=data)
-        runner = testing.CliRunner()
-
-        result = runner.invoke(
-            cli.edit, ['FEDORA-2017-c95b33872d', '--from-tag',
-                       '--notes', 'Updated package.',
-                       '--url', 'http://localhost:6543'])
-
-        assert result.exit_code == 0
-        mocked_client_class.query.assert_called_with(updateid='FEDORA-2017-c95b33872d')
-        calls = [
-            mock.call(
-                'updates/', auth=True, verb='POST',
-                data={
-                    'close_bugs': False, 'stable_karma': 3, 'csrf_token': 'a_csrf_token',
-                    'autokarma': False, 'edited': 'FEDORA-2017-c95b33872d',
-                    'suggest': 'unspecified', 'notes': 'Updated package.',
-                    'notes_file': None, 'request': None, 'unstable_karma': -3,
-                    'bugs': '1420605', 'requirements': '', 'type': 'newpackage',
-                    'severity': u'low', 'display_name': None, 'autotime': False,
-                    'stable_days': None, 'from_tag': 'fake_tag',
-                    'staging': False,
-                }
-            ),
-            mock.call(
-                'updates/FEDORA-EPEL-2016-3081a94111/get-test-results',
-                verb='GET'
-            )
-        ]
-        assert mocked_client_class.send_request.mock_calls == calls
-
-    def test_from_tag_flag_no_tag(self, mocked_client_class, mocker):
-        """
-        Assert --from-tag bails out if the update wasn't created from a tag.
-        """
-        mocked_client_class.query = mocker.Mock(return_value=client_test_data.EXAMPLE_QUERY_MUNCH)
-        runner = testing.CliRunner()
-
-        result = runner.invoke(
-            cli.edit, ['FEDORA-2017-c95b33872d', '--from-tag',
-                       '--notes', 'Updated package.',
-                       '--url', 'http://localhost:6543'])
-
-        assert result.exit_code == 1
-        assert result.output == ("ERROR: This update was not created from a tag."
-                                 " Please remove --from_tag and try again.\n")
-        mocked_client_class.query.assert_called_with(
-            updateid='FEDORA-2017-c95b33872d')
-
     def test_from_tag_addbuilds(self, mocked_client_class, mocker):
         """
-        Assert --from-tag can't be used with --addbuilds.
+        Assert --addbuilds can't be used with an update created from a side-tag.
         """
         data = client_test_data.EXAMPLE_QUERY_MUNCH.copy()
         data.updates[0]['from_tag'] = 'fake_tag'
@@ -1806,19 +1749,19 @@ def test_from_tag_addbuilds(self, mocked_client_class, mocker):
         runner = testing.CliRunner()
 
         result = runner.invoke(
-            cli.edit, ['FEDORA-2017-c95b33872d', '--from-tag',
+            cli.edit, ['FEDORA-2017-c95b33872d',
                        '--addbuilds', 'tar-1.29-4.fc25,nedit-5.7-1.fc25',
                        '--notes', 'Updated package.',
                        '--url', 'http://localhost:6543'])
 
         assert result.exit_code == 1
-        assert result.output == ("ERROR: You have to use the web interface to update"
-                                 " builds in a side-tag update.\n")
+        assert result.output == ("ERROR: The --addbuilds and --removebuilds options"
+                                 " cannot be used with a side-tag update.\n")
         mocked_client_class.query.assert_called_with(updateid='FEDORA-2017-c95b33872d')
 
     def test_from_tag_removebuilds(self, mocked_client_class, mocker):
         """
-        Assert --from-tag can't be used with --removebuilds.
+        Assert --removebuilds can't be used with an update created from a side-tag.
         """
         data = client_test_data.EXAMPLE_QUERY_MUNCH.copy()
         data.updates[0]['from_tag'] = 'fake_tag'
@@ -1827,36 +1770,55 @@ def test_from_tag_removebuilds(self, mocked_client_class, mocker):
         runner = testing.CliRunner()
 
         result = runner.invoke(
-            cli.edit, ['FEDORA-2017-c95b33872d', '--from-tag',
+            cli.edit, ['FEDORA-2017-c95b33872d',
                        '--removebuilds', 'nodejs-grunt-wrap-0.3.0-2.fc25',
                        '--notes', 'Updated package.',
                        '--url', 'http://localhost:6543'])
 
         assert result.exit_code == 1
-        assert result.output == ("ERROR: You have to use the web interface to update"
-                                 " builds in a side-tag update.\n")
+        assert result.output == ("ERROR: The --addbuilds and --removebuilds options"
+                                 " cannot be used with a side-tag update.\n")
         mocked_client_class.query.assert_called_with(updateid='FEDORA-2017-c95b33872d')
 
-    def test_from_tag_missing_flag(self, mocked_client_class, mocker):
+    def test_from_tag(self, mocked_client_class, mocker):
         """
-        Assert --from-tag is required when editing a side-tag update.
+        The build list is always refreshed from the side-tag by validate_from_tag()
+        for a side-tag update.
         """
+        mocked_client_class.send_request.return_value = client_test_data.EXAMPLE_UPDATE_MUNCH
         data = client_test_data.EXAMPLE_QUERY_MUNCH.copy()
         data.updates[0]['from_tag'] = 'fake_tag'
+        data.updates[0]['release']['composed_by_bodhi'] = False
         mocked_client_class.query = mocker.Mock(return_value=data)
-
         runner = testing.CliRunner()
 
         result = runner.invoke(
             cli.edit, ['FEDORA-2017-c95b33872d',
-                       '--addbuilds', 'tar-1.29-4.fc25,nedit-5.7-1.fc25',
                        '--notes', 'Updated package.',
                        '--url', 'http://localhost:6543'])
 
-        assert result.exit_code == 1
-        assert result.output == ("ERROR: This update was created from a side-tag."
-                                 " Please add --from_tag and try again.\n")
+        assert result.exit_code == 0
         mocked_client_class.query.assert_called_with(updateid='FEDORA-2017-c95b33872d')
+        calls = [
+            mock.call(
+                'updates/', auth=True, verb='POST',
+                data={
+                    'close_bugs': True, 'stable_karma': 3, 'csrf_token': 'a_csrf_token',
+                    'autokarma': True, 'edited': 'FEDORA-2017-c95b33872d',
+                    'suggest': 'unspecified', 'notes': 'Updated package.',
+                    'notes_file': None, 'request': None, 'unstable_karma': -3,
+                    'bugs': '1420605', 'requirements': '', 'type': 'newpackage',
+                    'severity': u'low', 'display_name': None, 'autotime': True,
+                    'stable_days': None, 'from_tag': 'fake_tag',
+                    'staging': False,
+                }
+            ),
+            mock.call(
+                'updates/FEDORA-EPEL-2016-3081a94111/get-test-results',
+                verb='GET'
+            )
+        ]
+        assert mocked_client_class.send_request.mock_calls == calls
 
     def test_notes_and_notes_file(self, mocked_client_class):
         """
@@ -1927,14 +1889,14 @@ def test_required_tasks(self, mocked_client_class, mocker):
             mock.call(
                 'updates/', auth=True, verb='POST',
                 data={
-                    'close_bugs': False, 'stable_karma': 3, 'csrf_token': 'a_csrf_token',
+                    'close_bugs': True, 'stable_karma': 3, 'csrf_token': 'a_csrf_token',
                     'staging': False, 'builds': ['nodejs-grunt-wrap-0.3.0-2.fc25'],
-                    'autokarma': False, 'edited': 'FEDORA-2017-c95b33872d',
+                    'autokarma': True, 'edited': 'FEDORA-2017-c95b33872d',
                     'suggest': 'unspecified', 'notes': 'testing required tasks',
                     'notes_file': None, 'request': None, 'severity': 'low',
                     'bugs': '1420605', 'unstable_karma': -3, 'display_name': None,
                     'requirements': 'dist.depcheck dist.rpmdeplint', 'type': 'newpackage',
-                    'autotime': False, 'stable_days': None
+                    'autotime': True, 'stable_days': None
                 }
             ),
             mock.call(
@@ -1975,13 +1937,13 @@ def test_edit_bugless_update_without_bugs_param(self, mocked_client_class, mocke
             mock.call(
                 'updates/', auth=True, verb='POST',
                 data={
-                    'close_bugs': False, 'stable_karma': 3, 'csrf_token': 'a_csrf_token',
+                    'close_bugs': True, 'stable_karma': 3, 'csrf_token': 'a_csrf_token',
                     'staging': False, 'builds': ['nodejs-grunt-wrap-0.3.0-2.fc25'],
-                    'autokarma': False, 'edited': 'FEDORA-2017-c95b33872d',
+                    'autokarma': True, 'edited': 'FEDORA-2017-c95b33872d',
                     'suggest': 'unspecified', 'notes': 'New package.', 'display_name': None,
                     'notes_file': None, 'request': None, 'severity': 'low',
                     'bugs': '', 'requirements': '', 'unstable_karma': -3, 'type': 'newpackage',
-                    'autotime': False, 'stable_days': None
+                    'autotime': True, 'stable_days': None
                 }
             ),
             mock.call(