gecko-dev/tools/tryselect/selectors/fuzzy.py
Andrew Halberstadt 8bc7850637 Bug 1483228 - [mozboot] Simplify get_state_dir()'s return value r=nalexander
mozboot.util.get_state_dir() returns a tuple of (<path>, <bool). The bool
denotes whether or not the state dir came from an environment variable.

But this value is only used in a single place, and is very easy to test for
anyway. It's not worth the added complexity it imposes on all other consumers
of this function. Let's just make this function return the path.

Differential Revision: https://phabricator.services.mozilla.com/D15723

--HG--
extra : moz-landing-system : lando
2019-02-04 20:52:31 +00:00

285 lines
8.3 KiB
Python

# 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 <query><ctrl-a><enter> "
"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"])