1 # This Source Code Form is subject to the terms of the Mozilla Public
2 # License, v. 2.0. If a copy of the MPL was not distributed with this
3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
11 from distutils
.spawn
import find_executable
12 from distutils
.version
import StrictVersion
14 from mozbuild
.base
import MozbuildObject
15 from mozboot
.util
import get_state_dir
16 from mozterm
import Terminal
18 from ..cli
import BaseTryParser
19 from ..tasks
import generate_tasks
, filter_tasks_by_paths
20 from ..push
import check_working_directory
, push_to_try
, generate_try_task_config
21 from ..util
.manage_estimates
import (
22 download_task_history_data
,
23 make_trimmed_taskgraph_cache
,
26 from taskgraph
.target_tasks
import filter_by_uncommon_try_tasks
30 here
= os
.path
.abspath(os
.path
.dirname(__file__
))
31 build
= MozbuildObject
.from_environment(cwd
=here
)
33 PREVIEW_SCRIPT
= os
.path
.join(build
.topsrcdir
, "tools/tryselect/selectors/preview.py")
36 Could not find the `fzf` binary.
38 The `mach try fuzzy` command depends on fzf. Please install it following the
39 appropriate instructions for your platform:
41 https://github.com/junegunn/fzf#installation
43 Only the binary is required, if you do not wish to install the shell and
44 editor integrations, download the appropriate binary and put it on your $PATH:
46 https://github.com/junegunn/fzf/releases
49 FZF_VERSION_FAILED
= """
50 Could not obtain the 'fzf' version.
52 The 'mach try fuzzy' command depends on fzf, and requires version > 0.20.0
53 for some of the features. Please install it following the appropriate
54 instructions for your platform:
56 https://github.com/junegunn/fzf#installation
58 Only the binary is required, if you do not wish to install the shell and
59 editor integrations, download the appropriate binary and put it on your $PATH:
61 https://github.com/junegunn/fzf/releases
64 FZF_INSTALL_FAILED
= """
65 Failed to install fzf.
67 Please install fzf manually following the appropriate instructions for your
70 https://github.com/junegunn/fzf#installation
72 Only the binary is required, if you do not wish to install the shell and
73 editor integrations, download the appropriate binary and put it on your $PATH:
75 https://github.com/junegunn/fzf/releases
79 For more shortcuts, see {t.italic_white}mach help try fuzzy{t.normal} and {t.italic_white}man fzf
84 "ctrl-a": "select-all",
85 "ctrl-d": "deselect-all",
86 "ctrl-t": "toggle-all",
87 "alt-bspace": "beginning-of-line+kill-line",
88 "?": "toggle-preview",
91 fzf_header_shortcuts
= [
95 ("select-all", "ctrl-a"),
97 ("cursor-down", "down"),
101 class FuzzyParser(BaseTryParser
):
110 "help": "Use the given query instead of entering the selection "
111 "interface. Equivalent to typing <query><ctrl-a><enter> "
112 "from the interface. Specifying multiple times schedules "
113 "the union of computed tasks.",
117 ["-i", "--interactive"],
119 "action": "store_true",
121 "help": "Force running fzf interactively even when using presets or "
122 "queries with -q/--query.",
128 "dest": "intersection",
129 "action": "store_true",
131 "help": "When specifying queries on the command line with -q/--query, "
132 "use the intersection of tasks rather than the union. This is "
133 "especially useful for post filtering presets.",
139 "action": "store_true",
141 "help": "Enable exact match mode. Terms will use an exact match "
142 "by default, and terms prefixed with ' will become fuzzy.",
148 "action": "store_true",
150 "help": "Update fzf before running.",
154 ["-s", "--show-estimates"],
156 "action": "store_true",
158 "help": "Show task duration estimates.",
162 ["--disable-target-task-filter"],
164 "action": "store_true",
166 "help": "Some tasks run on mozilla-central but are filtered out "
167 "of the default list due to resource constraints. This flag "
168 "disables this filtering.",
172 common_groups
= ["push", "task", "preset"]
188 def run_cmd(cmd
, cwd
=None):
189 is_win
= platform
.system() == "Windows"
190 return subprocess
.call(cmd
, cwd
=cwd
, shell
=True if is_win
else False)
193 def run_fzf_install_script(fzf_path
):
194 if platform
.system() == "Windows":
195 cmd
= ["bash", "-c", "./install --bin"]
197 cmd
= ["./install", "--bin"]
199 if run_cmd(cmd
, cwd
=fzf_path
):
200 print(FZF_INSTALL_FAILED
)
204 def should_force_fzf_update(fzf_bin
):
205 cmd
= [fzf_bin
, "--version"]
207 fzf_version
= subprocess
.check_output(cmd
)
208 except subprocess
.CalledProcessError
:
209 print(FZF_VERSION_FAILED
)
212 # Some fzf versions have extra, e.g 0.18.0 (ff95134)
213 fzf_version
= six
.ensure_text(fzf_version
.split()[0])
215 # 0.20.0 introduced passing selections through a temporary file,
216 # which is good for large ctrl-a actions.
217 if StrictVersion(fzf_version
) < StrictVersion("0.20.0"):
218 print("fzf version is old, forcing update.")
223 def fzf_bootstrap(update
=False):
224 """Bootstrap fzf if necessary and return path to the executable.
226 The bootstrap works by cloning the fzf repository and running the included
227 `install` script. If update is True, we will pull the repository and re-run
230 fzf_bin
= find_executable("fzf")
231 if fzf_bin
and should_force_fzf_update(fzf_bin
):
234 if fzf_bin
and not update
:
237 fzf_path
= os
.path
.join(get_state_dir(), "fzf")
239 # Bug 1623197: We only want to run fzf's `install` if it's not in the $PATH
240 # Swap to os.path.commonpath when we're not on Py2
241 if fzf_bin
and update
and not fzf_bin
.startswith(fzf_path
):
243 "fzf installed somewhere other than {}, please update manually".format(
250 return find_executable("fzf", os
.path
.join(fzf_path
, "bin"))
252 if os
.path
.isdir(fzf_path
):
254 ret
= run_cmd(["git", "pull"], cwd
=fzf_path
)
256 print("Update fzf failed.")
259 run_fzf_install_script(fzf_path
)
263 if not fzf_bin
or should_force_fzf_update(fzf_bin
):
264 return fzf_bootstrap(update
=True)
269 install
= input("Could not detect fzf, install it now? [y/n]: ")
270 if install
.lower() != "y":
273 if not find_executable("git"):
274 print("Git not found.")
275 print(FZF_INSTALL_FAILED
)
278 cmd
= ["git", "clone", "--depth", "1", "https://github.com/junegunn/fzf.git"]
279 if subprocess
.call(cmd
, cwd
=os
.path
.dirname(fzf_path
)):
280 print(FZF_INSTALL_FAILED
)
283 run_fzf_install_script(fzf_path
)
285 print("Installed fzf to {}".format(fzf_path
))
291 for action
, key
in fzf_header_shortcuts
:
293 "{t.white}{action}{t.normal}: {t.yellow}<{key}>{t.normal}".format(
294 t
=terminal
, action
=action
, key
=key
297 return FZF_HEADER
.format(shortcuts
=", ".join(shortcuts
), t
=terminal
)
300 def run_fzf(cmd
, tasks
):
301 env
= dict(os
.environ
)
303 {"PYTHONPATH": os
.pathsep
.join([p
for p
in sys
.path
if "requests" in p
])}
305 proc
= subprocess
.Popen(
307 stdout
=subprocess
.PIPE
,
308 stdin
=subprocess
.PIPE
,
310 universal_newlines
=True,
312 out
= proc
.communicate("\n".join(tasks
))[0].splitlines()
319 return query
, selected
325 intersect_query
=None,
335 show_estimates
=False,
336 disable_target_task_filter
=False,
338 fzf
= fzf_bootstrap(update
)
344 check_working_directory(push
)
346 parameters
, full
=full
, disable_target_task_filter
=disable_target_task_filter
348 all_tasks
= sorted(tg
.tasks
.keys())
350 # graph_Cache created by generate_tasks, recreate the path to that file.
351 cache_dir
= os
.path
.join(get_state_dir(srcdir
=True), "cache", "taskgraph")
353 graph_cache
= os
.path
.join(cache_dir
, "full_task_graph")
354 dep_cache
= os
.path
.join(cache_dir
, "full_task_dependencies")
355 target_set
= os
.path
.join(cache_dir
, "full_task_set")
357 graph_cache
= os
.path
.join(cache_dir
, "target_task_graph")
358 dep_cache
= os
.path
.join(cache_dir
, "target_task_dependencies")
359 target_set
= os
.path
.join(cache_dir
, "target_task_set")
362 download_task_history_data(cache_dir
=cache_dir
)
363 make_trimmed_taskgraph_cache(graph_cache
, dep_cache
, target_file
=target_set
)
365 if not full
and not disable_target_task_filter
:
366 # Put all_tasks into a list because it's used multiple times, and "filter()"
367 # returns a consumable iterator.
368 all_tasks
= list(filter(filter_by_uncommon_try_tasks
, all_tasks
))
371 all_tasks
= filter_tasks_by_paths(all_tasks
, test_paths
)
375 key_shortcuts
= [k
+ ":" + v
for k
, v
in fzf_shortcuts
.items()]
380 ",".join(key_shortcuts
),
383 "--preview-window=right:30%",
391 '{} {} -g {} -s -c {} -t "{{+f}}"'.format(
392 sys
.executable
, PREVIEW_SCRIPT
, dep_cache
, cache_dir
400 '{} {} -t "{{+f}}"'.format(sys
.executable
, PREVIEW_SCRIPT
),
405 base_cmd
.append("--exact")
410 def get_tasks(query_arg
=None, candidate_tasks
=all_tasks
):
412 if query_arg
and query_arg
!= "INTERACTIVE":
413 cmd
.extend(["-f", query_arg
])
415 query_str
, tasks
= run_fzf(cmd
, sorted(candidate_tasks
))
416 queries
.append(query_str
)
419 for q
in query
or []:
420 selected |
= get_tasks(q
)
422 for q
in intersect_query
or []:
427 tasks
= get_tasks(q
, selected
)
431 selected
= get_tasks()
434 print("no tasks selected")
440 # build commit message
442 args
= ["query={}".format(q
) for q
in queries
]
444 args
.append("paths={}".format(":".join(test_paths
)))
446 msg
= "{} {}".format(msg
, "&".join(args
))
449 message
.format(msg
=msg
),
450 try_task_config
=generate_try_task_config("fuzzy", selected
, try_config
),
452 closed_tree
=closed_tree
,