From b3e09d68df08947062017a1d508f17c643472bb6 Mon Sep 17 00:00:00 2001 From: mcarton Date: Thu, 16 Jul 2015 20:23:31 +0200 Subject: [PATCH 1/5] Start support for git aliases --- tests/test_main.py | 2 +- tests/test_utils.py | 11 ++++++++++- thefuck/main.py | 2 +- thefuck/utils.py | 20 ++++++++++++++++++++ 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index 7d07150d..8288d543 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -84,7 +84,7 @@ class TestGetCommand(object): shell=True, stdout=PIPE, stderr=PIPE, - env={'LANG': 'C'}) + env={'LANG': 'C', 'GIT_TRACE': 1}) @pytest.mark.parametrize('args, result', [ (['thefuck', 'ls', '-la'], 'ls -la'), diff --git a/tests/test_utils.py b/tests/test_utils.py index 0352fd7b..1acee00d 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,6 +1,6 @@ import pytest from mock import Mock -from thefuck.utils import sudo_support, wrap_settings, memoize, get_closest +from thefuck.utils import git_support, sudo_support, wrap_settings, memoize, get_closest from thefuck.types import Settings from tests.utils import Command @@ -26,6 +26,15 @@ def test_sudo_support(return_value, command, called, result): fn.assert_called_once_with(Command(called), None) +@pytest.mark.parametrize('called, command, stderr', [ + ('git co', "git 'checkout'", "19:22:36.299340 git.c:282 trace: alias expansion: co => 'checkout'"), + ('git com file', "git 'commit' '--verbose' file", "19:23:25.470911 git.c:282 trace: alias expansion: com => 'commit' '--verbose'")]) +def test_git_support(called, command, stderr): + @git_support + def fn(command, settings): return command.script + assert fn(Command(script=called, stderr=stderr), None) == command + + def test_memoize(): fn = Mock(__name__='fn') memoized = memoize(fn) diff --git a/thefuck/main.py b/thefuck/main.py index 8d1a3dac..e58550df 100644 --- a/thefuck/main.py +++ b/thefuck/main.py @@ -82,7 +82,7 @@ def get_command(settings, args): script = shells.from_shell(script) logs.debug('Call: {}'.format(script), settings) result = Popen(script, shell=True, stdout=PIPE, stderr=PIPE, - env=dict(os.environ, LANG='C')) + env=dict(os.environ, LANG='C', GIT_TRACE=1)) if wait_output(settings, result): return types.Command(script, result.stdout.read().decode('utf-8'), result.stderr.read().decode('utf-8')) diff --git a/thefuck/utils.py b/thefuck/utils.py index 2f58e527..7d7e670b 100644 --- a/thefuck/utils.py +++ b/thefuck/utils.py @@ -2,6 +2,7 @@ from difflib import get_close_matches from functools import wraps import os import pickle +import re import six from .types import Command @@ -73,6 +74,25 @@ def sudo_support(fn): return wrapper +def git_support(fn): + """Resolve git aliases.""" + @wraps(fn) + def wrapper(command, settings): + if (command.script.startswith('git') and + 'trace: alias expansion:' in command.stderr): + + search = re.search("trace: alias expansion: ([^ ]*) => ([^\n]*)", + command.stderr) + alias = search.group(1) + expansion = search.group(2) + new_script = command.script.replace(alias, expansion) + + command = Command._replace(command, script=new_script) + return fn(command, settings) + + return wrapper + + def memoize(fn): """Caches previous calls to the function.""" memo = {} From 707d91200e50b61a7ee50e8a235146114a441c28 Mon Sep 17 00:00:00 2001 From: mcarton Date: Fri, 17 Jul 2015 00:09:33 +0200 Subject: [PATCH 2/5] Make the environment a setting This would allow other rules to set the environment as needed for `@git_support` and `GIT_TRACE`. --- tests/test_main.py | 8 ++++---- thefuck/conf.py | 3 ++- thefuck/main.py | 8 ++++++-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index 8288d543..10294c3c 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -77,23 +77,23 @@ class TestGetCommand(object): monkeypatch.setattr('thefuck.shells.to_shell', lambda x: x) def test_get_command_calls(self, Popen): - assert main.get_command(Mock(), + assert main.get_command(Mock(env={}), ['thefuck', 'apt-get', 'search', 'vim']) \ == Command('apt-get search vim', 'stdout', 'stderr') Popen.assert_called_once_with('apt-get search vim', shell=True, stdout=PIPE, stderr=PIPE, - env={'LANG': 'C', 'GIT_TRACE': 1}) + env={}) @pytest.mark.parametrize('args, result', [ (['thefuck', 'ls', '-la'], 'ls -la'), (['thefuck', 'ls'], 'ls')]) def test_get_command_script(self, args, result): if result: - assert main.get_command(Mock(), args).script == result + assert main.get_command(Mock(env={}), args).script == result else: - assert main.get_command(Mock(), args) is None + assert main.get_command(Mock(env={}), args) is None class TestGetMatchedRule(object): diff --git a/thefuck/conf.py b/thefuck/conf.py index 14d33206..a1af962f 100644 --- a/thefuck/conf.py +++ b/thefuck/conf.py @@ -30,7 +30,8 @@ DEFAULT_SETTINGS = {'rules': DEFAULT_RULES, 'require_confirmation': False, 'no_colors': False, 'debug': False, - 'priority': {}} + 'priority': {}, + 'env': {'LANG': 'C', 'GIT_TRACE': '1'}} ENV_TO_ATTR = {'THEFUCK_RULES': 'rules', 'THEFUCK_WAIT_COMMAND': 'wait_command', diff --git a/thefuck/main.py b/thefuck/main.py index e58550df..b1e0f64d 100644 --- a/thefuck/main.py +++ b/thefuck/main.py @@ -81,8 +81,12 @@ def get_command(settings, args): script = shells.from_shell(script) logs.debug('Call: {}'.format(script), settings) - result = Popen(script, shell=True, stdout=PIPE, stderr=PIPE, - env=dict(os.environ, LANG='C', GIT_TRACE=1)) + + env = dict(os.environ) + env.update(settings.env) + logs.debug('Executing with env: {}'.format(env), settings) + + result = Popen(script, shell=True, stdout=PIPE, stderr=PIPE, env=env) if wait_output(settings, result): return types.Command(script, result.stdout.read().decode('utf-8'), result.stderr.read().decode('utf-8')) From f6a4902074fd58d917f637cbff852aafa54557b8 Mon Sep 17 00:00:00 2001 From: mcarton Date: Fri, 17 Jul 2015 11:55:44 +0200 Subject: [PATCH 3/5] Use @git_support in all git_* rules --- tests/rules/test_git_diff_staged.py | 10 +++------- tests/rules/test_git_stash.py | 20 +++++++++----------- thefuck/rules/git_add.py | 4 +++- thefuck/rules/git_branch_list.py | 4 +++- thefuck/rules/git_checkout.py | 4 +++- thefuck/rules/git_diff_staged.py | 9 ++++++++- thefuck/rules/git_not_command.py | 5 +++-- thefuck/rules/git_pull.py | 4 +++- thefuck/rules/git_push.py | 5 +++++ thefuck/rules/git_stash.py | 4 +++- 10 files changed, 43 insertions(+), 26 deletions(-) diff --git a/tests/rules/test_git_diff_staged.py b/tests/rules/test_git_diff_staged.py index 93fe2536..a4c62a37 100644 --- a/tests/rules/test_git_diff_staged.py +++ b/tests/rules/test_git_diff_staged.py @@ -3,15 +3,13 @@ from thefuck.rules.git_diff_staged import match, get_new_command from tests.utils import Command -@pytest.mark.parametrize('command', [ - Command(script='git diff'), - Command(script='git df'), - Command(script='git ds')]) +@pytest.mark.parametrize('command', [Command(script='git diff')]) def test_match(command): assert match(command, None) @pytest.mark.parametrize('command', [ + Command(script='git diff --staged'), Command(script='git tag'), Command(script='git branch'), Command(script='git log')]) @@ -20,8 +18,6 @@ def test_not_match(command): @pytest.mark.parametrize('command, new_command', [ - (Command('git diff'), 'git diff --staged'), - (Command('git df'), 'git df --staged'), - (Command('git ds'), 'git ds --staged')]) + (Command('git diff'), 'git diff --staged')]) def test_get_new_command(command, new_command): assert get_new_command(command, None) == new_command diff --git a/tests/rules/test_git_stash.py b/tests/rules/test_git_stash.py index c62a48aa..2aa06277 100644 --- a/tests/rules/test_git_stash.py +++ b/tests/rules/test_git_stash.py @@ -3,22 +3,20 @@ from thefuck.rules.git_stash import match, get_new_command from tests.utils import Command -@pytest.fixture -def cherry_pick_error(): - return ('error: Your local changes would be overwritten by cherry-pick.\n' - 'hint: Commit your changes or stash them to proceed.\n' - 'fatal: cherry-pick failed') +cherry_pick_error = ( + 'error: Your local changes would be overwritten by cherry-pick.\n' + 'hint: Commit your changes or stash them to proceed.\n' + 'fatal: cherry-pick failed') -@pytest.fixture -def rebase_error(): - return ('Cannot rebase: Your index contains uncommitted changes.\n' - 'Please commit or stash them.') +rebase_error = ( + 'Cannot rebase: Your index contains uncommitted changes.\n' + 'Please commit or stash them.') @pytest.mark.parametrize('command', [ - Command(script='git cherry-pick a1b2c3d', stderr=cherry_pick_error()), - Command(script='git rebase -i HEAD~7', stderr=rebase_error())]) + Command(script='git cherry-pick a1b2c3d', stderr=cherry_pick_error), + Command(script='git rebase -i HEAD~7', stderr=rebase_error)]) def test_match(command): assert match(command, None) diff --git a/thefuck/rules/git_add.py b/thefuck/rules/git_add.py index bc05d011..66f34726 100644 --- a/thefuck/rules/git_add.py +++ b/thefuck/rules/git_add.py @@ -1,13 +1,15 @@ import re -from thefuck import shells +from thefuck import utils, shells +@utils.git_support def match(command, settings): return ('git' in command.script and 'did not match any file(s) known to git.' in command.stderr and "Did you forget to 'git add'?" in command.stderr) +@utils.git_support def get_new_command(command, settings): missing_file = re.findall( r"error: pathspec '([^']*)' " diff --git a/thefuck/rules/git_branch_list.py b/thefuck/rules/git_branch_list.py index e0a2320c..9f6dd5f6 100644 --- a/thefuck/rules/git_branch_list.py +++ b/thefuck/rules/git_branch_list.py @@ -1,10 +1,12 @@ -from thefuck import shells +from thefuck import utils, shells +@utils.git_support def match(command, settings): # catches "git branch list" in place of "git branch" return command.script.split() == 'git branch list'.split() +@utils.git_support def get_new_command(command, settings): return shells.and_('git branch --delete list', 'git branch') diff --git a/thefuck/rules/git_checkout.py b/thefuck/rules/git_checkout.py index 6c9d259f..9a84ec95 100644 --- a/thefuck/rules/git_checkout.py +++ b/thefuck/rules/git_checkout.py @@ -1,13 +1,15 @@ import re -from thefuck import shells +from thefuck import shells, utils +@utils.git_support def match(command, settings): return ('git' in command.script and 'did not match any file(s) known to git.' in command.stderr and "Did you forget to 'git add'?" not in command.stderr) +@utils.git_support def get_new_command(command, settings): missing_file = re.findall( r"error: pathspec '([^']*)' " diff --git a/thefuck/rules/git_diff_staged.py b/thefuck/rules/git_diff_staged.py index 32e5dcbb..a35234e4 100644 --- a/thefuck/rules/git_diff_staged.py +++ b/thefuck/rules/git_diff_staged.py @@ -1,6 +1,13 @@ +from thefuck import utils + + +@utils.git_support def match(command, settings): - return command.script.startswith('git d') + return ('git' in command.script and + 'diff' in command.script and + '--staged' not in command.script) +@utils.git_support def get_new_command(command, settings): return '{} --staged'.format(command.script) diff --git a/thefuck/rules/git_not_command.py b/thefuck/rules/git_not_command.py index f851f10e..1fa707ab 100644 --- a/thefuck/rules/git_not_command.py +++ b/thefuck/rules/git_not_command.py @@ -1,8 +1,8 @@ -from difflib import get_close_matches import re -from thefuck.utils import get_closest +from thefuck.utils import get_closest, git_support +@git_support def match(command, settings): return ('git' in command.script and " is not a git command. See 'git --help'." in command.stderr @@ -18,6 +18,7 @@ def _get_all_git_matched_commands(stderr): yield line.strip() +@git_support def get_new_command(command, settings): broken_cmd = re.findall(r"git: '([^']*)' is not a git command", command.stderr)[0] diff --git a/thefuck/rules/git_pull.py b/thefuck/rules/git_pull.py index 98470b15..65f44ea5 100644 --- a/thefuck/rules/git_pull.py +++ b/thefuck/rules/git_pull.py @@ -1,12 +1,14 @@ -from thefuck import shells +from thefuck import shells, utils +@utils.git_support def match(command, settings): return ('git' in command.script and 'pull' in command.script and 'set-upstream' in command.stderr) +@utils.git_support def get_new_command(command, settings): line = command.stderr.split('\n')[-3].strip() branch = line.split(' ')[-1] diff --git a/thefuck/rules/git_push.py b/thefuck/rules/git_push.py index bb9818f1..5b6ea09a 100644 --- a/thefuck/rules/git_push.py +++ b/thefuck/rules/git_push.py @@ -1,8 +1,13 @@ +from thefuck import utils + + +@utils.git_support def match(command, settings): return ('git' in command.script and 'push' in command.script and 'set-upstream' in command.stderr) +@utils.git_support def get_new_command(command, settings): return command.stderr.split('\n')[-3].strip() diff --git a/thefuck/rules/git_stash.py b/thefuck/rules/git_stash.py index 9e9034a3..76cbdf46 100644 --- a/thefuck/rules/git_stash.py +++ b/thefuck/rules/git_stash.py @@ -1,12 +1,14 @@ -from thefuck import shells +from thefuck import shells, utils +@utils.git_support def match(command, settings): # catches "Please commit or stash them" and "Please, commit your changes or # stash them before you can switch branches." return 'git' in command.script and 'or stash them' in command.stderr +@utils.git_support def get_new_command(command, settings): formatme = shells.and_('git stash', '{}') return formatme.format(command.script) From 5d0912fee81fda3c73ac9a2766e381e1436c2612 Mon Sep 17 00:00:00 2001 From: mcarton Date: Fri, 17 Jul 2015 13:51:22 +0200 Subject: [PATCH 4/5] Unquote over-quoted commands in @git_support This allows writing rules more easily (eg. the git_branch_list rule tests for `command.script.split() == 'git branch list'.split()`) and looks nicer when `require_confirmation` is set. --- tests/test_utils.py | 4 ++-- thefuck/utils.py | 14 +++++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 1acee00d..b724bca2 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -27,8 +27,8 @@ def test_sudo_support(return_value, command, called, result): @pytest.mark.parametrize('called, command, stderr', [ - ('git co', "git 'checkout'", "19:22:36.299340 git.c:282 trace: alias expansion: co => 'checkout'"), - ('git com file', "git 'commit' '--verbose' file", "19:23:25.470911 git.c:282 trace: alias expansion: com => 'commit' '--verbose'")]) + ('git co', 'git checkout', "19:22:36.299340 git.c:282 trace: alias expansion: co => 'checkout'"), + ('git com file', 'git commit --verbose file', "19:23:25.470911 git.c:282 trace: alias expansion: com => 'commit' '--verbose'")]) def test_git_support(called, command, stderr): @git_support def fn(command, settings): return command.script diff --git a/thefuck/utils.py b/thefuck/utils.py index 7d7e670b..c27f337b 100644 --- a/thefuck/utils.py +++ b/thefuck/utils.py @@ -1,5 +1,6 @@ from difflib import get_close_matches from functools import wraps +from shlex import split import os import pickle import re @@ -10,11 +11,9 @@ from .types import Command DEVNULL = open(os.devnull, 'w') if six.PY2: - import pipes - quote = pipes.quote + from pipes import quote else: - import shlex - quote = shlex.quote + from shlex import quote def which(program): @@ -84,7 +83,12 @@ def git_support(fn): search = re.search("trace: alias expansion: ([^ ]*) => ([^\n]*)", command.stderr) alias = search.group(1) - expansion = search.group(2) + + # by default git quotes everthing, for example: + # 'commit' '--amend' + # which is surprising and does not allow to easily test for + # eg. 'git commit' + expansion = ' '.join(map(quote, split(search.group(2)))) new_script = command.script.replace(alias, expansion) command = Command._replace(command, script=new_script) From f90bac10edbd1495ae824db35a3ea2bbebfb3462 Mon Sep 17 00:00:00 2001 From: nvbn Date: Sun, 19 Jul 2015 21:29:28 +0300 Subject: [PATCH 5/5] #290: Fix typo --- thefuck/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/thefuck/utils.py b/thefuck/utils.py index c27f337b..ffedb98c 100644 --- a/thefuck/utils.py +++ b/thefuck/utils.py @@ -84,7 +84,7 @@ def git_support(fn): command.stderr) alias = search.group(1) - # by default git quotes everthing, for example: + # by default git quotes everything, for example: # 'commit' '--amend' # which is surprising and does not allow to easily test for # eg. 'git commit'