From b5d46ec13b5dde71d427bcb3e5dfc511e0abe907 Mon Sep 17 00:00:00 2001 From: akshatkarani Date: Tue, 13 Aug 2019 20:50:05 +0530 Subject: [PATCH 1/2] EditCommitMessageAction: New action This adds a new action for GitCommitBear. When applied it opens up a editor for user to edit commit message of the HEAD commit. This also makes changes is CommitBear.py to pass EditCommitMessageAction when Result is yielded. --- bears/vcs/CommitBear.py | 30 ++++++++++++------- bears/vcs/actions/EditCommitMessageAction.py | 17 +++++++++++ .../actions/EditCommitMessageActionTest.py | 24 +++++++++++++++ 3 files changed, 61 insertions(+), 10 deletions(-) create mode 100644 bears/vcs/actions/EditCommitMessageAction.py create mode 100644 tests/vcs/actions/EditCommitMessageActionTest.py diff --git a/bears/vcs/CommitBear.py b/bears/vcs/CommitBear.py index a9a225f2a2..295dace211 100644 --- a/bears/vcs/CommitBear.py +++ b/bears/vcs/CommitBear.py @@ -13,6 +13,7 @@ from coalib.settings.Setting import typed_list from coalib.settings.FunctionMetadata import FunctionMetadata from dependency_management.requirements.PipRequirement import PipRequirement +from bears.vcs.actions.EditCommitMessageAction import EditCommitMessageAction class _CommitBear(GlobalBear): @@ -188,13 +189,15 @@ def check_shortlog(self, shortlog, 'character(s). This is {} character(s) longer than ' 'the limit ({} > {}).'.format( len(shortlog), diff, - len(shortlog), shortlog_length)) + len(shortlog), shortlog_length), + actions=[EditCommitMessageAction()]) if (shortlog[-1] != '.') == shortlog_trailing_period: yield Result(self, 'Shortlog of HEAD commit contains no period at end.' if shortlog_trailing_period else - 'Shortlog of HEAD commit contains a period at end.') + 'Shortlog of HEAD commit contains a period at end.', + actions=[EditCommitMessageAction()]) if shortlog_regex: match = re.fullmatch(shortlog_regex, shortlog) @@ -202,7 +205,8 @@ def check_shortlog(self, shortlog, yield Result( self, 'Shortlog of HEAD commit does not match given regex:' - ' {regex}'.format(regex=shortlog_regex)) + ' {regex}'.format(regex=shortlog_regex), + actions=[EditCommitMessageAction()]) if shortlog_imperative_check: colon_pos = shortlog.find(':') @@ -214,7 +218,8 @@ def check_shortlog(self, shortlog, bad_word = has_flaws[0] yield Result(self, "Shortlog of HEAD commit isn't in imperative " - "mood! Bad words are '{}'".format(bad_word)) + "mood! Bad words are '{}'".format(bad_word), + actions=[EditCommitMessageAction()]) if shortlog_wip_check: if 'wip' in shortlog.lower()[:4]: yield Result( @@ -267,12 +272,15 @@ def check_body(self, body, """ if len(body) == 0: if force_body: - yield Result(self, 'No commit message body at HEAD.') + yield Result(self, 'No commit message body at HEAD.', + actions=[EditCommitMessageAction()]) return if body[0] != '\n': - yield Result(self, 'No newline found between shortlog and body at ' - 'HEAD commit. Please add one.') + yield Result(self, + 'No newline found between shortlog and body at ' + 'HEAD commit. Please add one.', + actions=[EditCommitMessageAction()]) return if body_regex and not re.fullmatch(body_regex, body.strip()): @@ -284,9 +292,11 @@ def check_body(self, body, if any((len(line) > body_line_length and not any(regex.search(line) for regex in ignore_regexes)) for line in body[1:]): - yield Result(self, 'Body of HEAD commit contains too long lines. ' - 'Commit body lines should not exceed {} ' - 'characters.'.format(body_line_length)) + yield Result(self, + 'Body of HEAD commit contains too long lines. ' + 'Commit body lines should not exceed {} ' + 'characters.'.format(body_line_length), + actions=[EditCommitMessageAction()]) def check_issue_reference(self, body, body_close_issue: bool = False, diff --git a/bears/vcs/actions/EditCommitMessageAction.py b/bears/vcs/actions/EditCommitMessageAction.py new file mode 100644 index 0000000000..6f72f0a147 --- /dev/null +++ b/bears/vcs/actions/EditCommitMessageAction.py @@ -0,0 +1,17 @@ +import subprocess +from coalib.results.result_actions.ResultAction import ResultAction + + +class EditCommitMessageAction(ResultAction): + """ + Opens an editor to edit the commit message of the HEAD commit. + """ + + SUCCESS_MESSAGE = 'Commit message edited successfully.' + + def apply(self, result, original_file_dict, file_diff_dict): + """ + Edit (C)ommit Message [Note: This may rewrite your commit history] + """ + subprocess.check_call(['git', 'commit', '-o', '--amend']) + return file_diff_dict diff --git a/tests/vcs/actions/EditCommitMessageActionTest.py b/tests/vcs/actions/EditCommitMessageActionTest.py new file mode 100644 index 0000000000..dd39b61d42 --- /dev/null +++ b/tests/vcs/actions/EditCommitMessageActionTest.py @@ -0,0 +1,24 @@ +import unittest +from unittest.mock import patch +from coala_utils.ContextManagers import retrieve_stdout +from coalib.results.Result import Result +from bears.vcs.actions.EditCommitMessageAction import EditCommitMessageAction + + +class EditCommitMessageActionTest(unittest.TestCase): + + def setUp(self): + self.uut = EditCommitMessageAction() + self.result = Result('origin', 'message') + + def test_is_applicable(self): + self.assertTrue(self.uut.is_applicable(self.result, {}, {})) + + def test_apply(self): + with retrieve_stdout() as stdout: + patcher = ('bears.vcs.actions.EditCommitMessageAction.' + 'subprocess.check_call') + with patch(patcher): + ret = self.uut.apply(self.result, {}, {'file': 'diff'}) + self.assertEqual(ret, {'file': 'diff'}) + self.assertEqual(stdout.getvalue(), '') From 7ffaabe2e436e153fbfb85fbcdd7fbf15daf930a Mon Sep 17 00:00:00 2001 From: akshatkarani Date: Sat, 29 Jun 2019 15:58:08 +0530 Subject: [PATCH 2/2] AddNewlineAction: New action for GitCommitBear This adds a new action which adds a newline between shortlog and body of commit message when applied. This also make changes in CommitBear.py to pass AddNewlineAction when Result is yielded. --- bears/vcs/CommitBear.py | 4 +- bears/vcs/actions/AddNewlineAction.py | 35 +++++++++ bears/vcs/actions/__init__.py | 0 tests/vcs/actions/AddNewlineActionTest.py | 96 +++++++++++++++++++++++ tests/vcs/actions/__init__.py | 0 5 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 bears/vcs/actions/AddNewlineAction.py create mode 100644 bears/vcs/actions/__init__.py create mode 100644 tests/vcs/actions/AddNewlineActionTest.py create mode 100644 tests/vcs/actions/__init__.py diff --git a/bears/vcs/CommitBear.py b/bears/vcs/CommitBear.py index 295dace211..69d1062ad0 100644 --- a/bears/vcs/CommitBear.py +++ b/bears/vcs/CommitBear.py @@ -14,6 +14,7 @@ from coalib.settings.FunctionMetadata import FunctionMetadata from dependency_management.requirements.PipRequirement import PipRequirement from bears.vcs.actions.EditCommitMessageAction import EditCommitMessageAction +from bears.vcs.actions.AddNewlineAction import AddNewlineAction class _CommitBear(GlobalBear): @@ -280,7 +281,8 @@ def check_body(self, body, yield Result(self, 'No newline found between shortlog and body at ' 'HEAD commit. Please add one.', - actions=[EditCommitMessageAction()]) + actions=[EditCommitMessageAction(), + AddNewlineAction()]) return if body_regex and not re.fullmatch(body_regex, body.strip()): diff --git a/bears/vcs/actions/AddNewlineAction.py b/bears/vcs/actions/AddNewlineAction.py new file mode 100644 index 0000000000..a8d39b7756 --- /dev/null +++ b/bears/vcs/actions/AddNewlineAction.py @@ -0,0 +1,35 @@ +from coalib.misc.Shell import run_shell_command +from coalib.results.result_actions.ResultAction import ResultAction + + +class AddNewlineAction(ResultAction): + """ + Adds a newline between shortlog and body of the commit message + of the HEAD commit. + """ + + SUCCESS_MESSAGE = 'New Line added successfully.' + + def is_applicable(self, + result, + original_file_dict, + file_diff_dict, + applied_actions=()): + new_message, _ = run_shell_command('git log -1 --pretty=%B') + new_message = new_message.rstrip('\n') + pos = new_message.find('\n') + self.shortlog = new_message[:pos] if pos != -1 else new_message + self.body = new_message[pos+1:] if pos != -1 else '' + if self.body[0] != '\n': + return True + else: + return False + + def apply(self, result, original_file_dict, file_diff_dict): + """ + Add New(L)ine [Note: This may rewrite your commit history] + """ + new_commit_message = '{}\n\n{}'.format(self.shortlog, self.body) + command = 'git commit -o --amend -m "{}"'.format(new_commit_message) + stdout, err = run_shell_command(command) + return file_diff_dict diff --git a/bears/vcs/actions/__init__.py b/bears/vcs/actions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/vcs/actions/AddNewlineActionTest.py b/tests/vcs/actions/AddNewlineActionTest.py new file mode 100644 index 0000000000..466296c19b --- /dev/null +++ b/tests/vcs/actions/AddNewlineActionTest.py @@ -0,0 +1,96 @@ +import unittest +import os +import platform +import shutil +from tempfile import mkdtemp, mkstemp +from unittest.mock import Mock + +from coalib.results.Result import Result +from bears.vcs.actions.AddNewlineAction import AddNewlineAction +from coala_utils.ContextManagers import retrieve_stdout +from coalib.misc.Shell import run_shell_command + + +class AddNewlineActionTest(unittest.TestCase): + + @staticmethod + def run_git_command(*args, stdin=None): + return run_shell_command(' '.join(('git',) + args), stdin) + + def setUp(self): + self.shortlog = 'file.py: Add something' + self.body = ('Added something, wrote some things\n' + 'Wrote tests\n' + '\n' + 'Fixes #issue') + self.uut = AddNewlineAction() + self.result = Result('origin', 'message') + + # Creating a temporary git repository and + # adding a commit to test + self._old_cwd = os.getcwd() + self.gitdir = mkdtemp() + os.chdir(self.gitdir) + self.gitfile = mkstemp(dir=self.gitdir) + self.run_git_command('init') + self.run_git_command('config', 'user.email coala@coala.io') + self.run_git_command('config', 'user.name coala') + self.msg = self.shortlog + '\n' + self.body + self.run_git_command('add .') + self.run_git_command('commit', + '--file=-', + stdin=self.msg) + + def tearDown(self): + # Deleting the temporary repository + os.chdir(self._old_cwd) + if platform.system() == 'Windows': + onerror = self._windows_rmtree_remove_readonly + else: + onerror = None + shutil.rmtree(self.gitdir, onerror=onerror) + + def test_is_applicable_apply(self): + # Applicable because there is no newline between shortlog and body + self.assertTrue(self.uut.is_applicable(self.result, {}, {})) + + with retrieve_stdout() as stdout: + self.uut.apply(self.result, {}, {}) + new_message, _ = run_shell_command('git log -1 --pretty=%B') + new_message = new_message.rstrip('\n') + self.assertEqual(new_message, + self.shortlog + '\n\n' + self.body) + self.assertEqual(stdout.getvalue(), '') + + # Not applicable after action is applied + self.assertFalse(self.uut.is_applicable(self.result, {}, {})) + + # Undoing the amend done by applying the action + self.run_git_command('commit', + '--amend', + '--file=-', + stdin=self.msg) + + def test_is_applicable_edited_message(self): + # Applicable because there is no newline between shortlog and body + self.assertTrue(self.uut.is_applicable(self.result, {}, {})) + + # Mocking EditCommitMessageAction to test cases where user first + # changes commit message by appying EditCommitMessageAction, then + # checking the applicability of AddNewlineAction + EditCommitMessageAction = Mock() + edited_msg1 = ('This is new commit message\n' + 'Still no new line') + edited_msg2 = ('This is lastest commit message\n' + '\n' + 'Finally a new line!!') + + EditCommitMessageAction.apply.side_effect = self.run_git_command( + 'commit', '--amend', '--file=-', stdin=edited_msg1) + EditCommitMessageAction.apply() + self.assertTrue(self.uut.is_applicable(self.result, {}, {})) + + EditCommitMessageAction.apply.side_effect = self.run_git_command( + 'commit', '--amend', '--file=-', stdin=edited_msg2) + EditCommitMessageAction.apply() + self.assertFalse(self.uut.is_applicable(self.result, {}, {})) diff --git a/tests/vcs/actions/__init__.py b/tests/vcs/actions/__init__.py new file mode 100644 index 0000000000..e69de29bb2