# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from __future__ import absolute_import, print_function, unicode_literals import os import platform import re import subprocess import sys from distutils.spawn import find_executable from mozboot.util import get_state_dir from mozterm import Terminal from six import string_types from .. import preset as pset from ..cli import BaseTryParser from ..tasks import generate_tasks, filter_tasks_by_paths from ..push import check_working_directory, push_to_try, vcs terminal = Terminal() here = os.path.abspath(os.path.dirname(__file__)) # Some tasks show up in the target task set, but are either special cases # or uncommon enough that they should only be selectable with --full. TARGET_TASK_FILTERS = ( '.*-ccov\/.*', ) FZF_NOT_FOUND = """ Could not find the `fzf` binary. The `mach try fuzzy` command depends on fzf. Please install it following the appropriate instructions for your platform: https://github.com/junegunn/fzf#installation Only the binary is required, if you do not wish to install the shell and editor integrations, download the appropriate binary and put it on your $PATH: https://github.com/junegunn/fzf-bin/releases """.lstrip() FZF_INSTALL_FAILED = """ Failed to install fzf. Please install fzf manually following the appropriate instructions for your platform: https://github.com/junegunn/fzf#installation Only the binary is required, if you do not wish to install the shell and editor integrations, download the appropriate binary and put it on your $PATH: https://github.com/junegunn/fzf-bin/releases """.lstrip() FZF_HEADER = """ For more shortcuts, see {t.italic_white}mach help try fuzzy{t.normal} and {t.italic_white}man fzf {shortcuts} """.strip() fzf_shortcuts = { 'ctrl-a': 'select-all', 'ctrl-d': 'deselect-all', 'ctrl-t': 'toggle-all', 'alt-bspace': 'beginning-of-line+kill-line', '?': 'toggle-preview', } fzf_header_shortcuts = { 'cursor-up': 'ctrl-k', 'cursor-down': 'ctrl-j', 'toggle-select': 'tab', 'select-all': 'ctrl-a', 'accept': 'enter', 'cancel': 'ctrl-c', } class FuzzyParser(BaseTryParser): name = 'fuzzy' arguments = [ [['-q', '--query'], {'metavar': 'STR', 'action': 'append', 'help': "Use the given query instead of entering the selection " "interface. Equivalent to typing " "from the interface. Specifying multiple times schedules " "the union of computed tasks.", }], [['-e', '--exact'], {'action': 'store_true', 'default': False, 'help': "Enable exact match mode. Terms will use an exact match " "by default, and terms prefixed with ' will become fuzzy." }], [['-u', '--update'], {'action': 'store_true', 'default': False, 'help': "Update fzf before running.", }], ] common_groups = ['push', 'task', 'preset'] templates = ['artifact', 'path', 'env', 'rebuild', 'chemspill-prio', 'gecko-profile'] def run(cmd, cwd=None): is_win = platform.system() == 'Windows' return subprocess.call(cmd, cwd=cwd, shell=True if is_win else False) def run_fzf_install_script(fzf_path): if platform.system() == 'Windows': cmd = ['bash', '-c', './install --bin'] else: cmd = ['./install', '--bin'] if run(cmd, cwd=fzf_path): print(FZF_INSTALL_FAILED) sys.exit(1) def fzf_bootstrap(update=False): """Bootstrap fzf if necessary and return path to the executable. The bootstrap works by cloning the fzf repository and running the included `install` script. If update is True, we will pull the repository and re-run the install script. """ fzf_bin = find_executable('fzf') if fzf_bin and not update: return fzf_bin fzf_path = os.path.join(get_state_dir(), 'fzf') if update and not os.path.isdir(fzf_path): print("fzf installed somewhere other than {}, please update manually".format(fzf_path)) sys.exit(1) def get_fzf(): return find_executable('fzf', os.path.join(fzf_path, 'bin')) if update: ret = run(['git', 'pull'], cwd=fzf_path) if ret: print("Update fzf failed.") sys.exit(1) run_fzf_install_script(fzf_path) return get_fzf() if os.path.isdir(fzf_path): fzf_bin = get_fzf() if fzf_bin: return fzf_bin # Fzf is cloned, but binary doesn't exist. Try running the install script return fzf_bootstrap(update=True) install = raw_input("Could not detect fzf, install it now? [y/n]: ") if install.lower() != 'y': return if not find_executable('git'): print("Git not found.") print(FZF_INSTALL_FAILED) sys.exit(1) cmd = ['git', 'clone', '--depth', '1', 'https://github.com/junegunn/fzf.git'] if subprocess.call(cmd, cwd=os.path.dirname(fzf_path)): print(FZF_INSTALL_FAILED) sys.exit(1) run_fzf_install_script(fzf_path) print("Installed fzf to {}".format(fzf_path)) return get_fzf() def format_header(): shortcuts = [] for action, key in sorted(fzf_header_shortcuts.iteritems()): shortcuts.append('{t.white}{action}{t.normal}: {t.yellow}<{key}>{t.normal}'.format( t=terminal, action=action, key=key)) return FZF_HEADER.format(shortcuts=', '.join(shortcuts), t=terminal) def run_fzf(cmd, tasks): proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE) out = proc.communicate('\n'.join(tasks))[0].splitlines() selected = [] query = None if out: query = out[0] selected = out[1:] return query, selected def filter_target_task(task): return not any(re.search(pattern, task) for pattern in TARGET_TASK_FILTERS) def run_fuzzy_try(update=False, query=None, templates=None, full=False, parameters=None, save=False, preset=None, mod_presets=False, push=True, message='{msg}', paths=None, **kwargs): if mod_presets: return getattr(pset, mod_presets)(section='fuzzy') fzf = fzf_bootstrap(update) if not fzf: print(FZF_NOT_FOUND) return 1 check_working_directory(push) tg = generate_tasks(parameters, full, root=vcs.path) all_tasks = sorted(tg.tasks.keys()) if not full: all_tasks = filter(filter_target_task, all_tasks) if paths: all_tasks = filter_tasks_by_paths(all_tasks, paths) if not all_tasks: return 1 key_shortcuts = [k + ':' + v for k, v in fzf_shortcuts.iteritems()] base_cmd = [ fzf, '-m', '--bind', ','.join(key_shortcuts), '--header', format_header(), # Using python to split the preview string is a bit convoluted, # but is guaranteed to be available on all platforms. '--preview', 'python -c "print(\\"\\n\\".join(sorted([s.strip(\\"\'\\") for s in \\"{+}\\".split()])))"', # noqa '--preview-window=right:20%', '--print-query', ] if kwargs['exact']: base_cmd.append('--exact') query = query or [] if isinstance(query, string_types): query = [query] if preset: query.append(pset.load(preset, section='fuzzy')[0]) commands = [] if query: for q in query: commands.append(base_cmd + ['-f', q]) else: commands.append(base_cmd) queries = [] selected = set() for command in commands: query, tasks = run_fzf(command, all_tasks) if tasks: queries.append(query) selected.update(tasks) if not selected: print("no tasks selected") return if save: pset.save('fuzzy', save, queries[0]) # build commit message msg = "Fuzzy" args = [] if paths: args.append("paths={}".format(':'.join(paths))) if query: args.extend(["query={}".format(q) for q in queries]) if args: msg = "{} {}".format(msg, '&'.join(args)) return push_to_try('fuzzy', message.format(msg=msg), selected, templates, push=push, closed_tree=kwargs["closed_tree"])