pw_watch: Use Git to determine which paths to ignore
- Do not trigger builds for any files not within or ignored by a Git
repo.
- Remove ignore_dirs from PigweedBuildWatcher. With .gitignore applied,
that is no longer necessary.
Change-Id: I008d155c458a7478f155b23d340fc19730a61431
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/41180
Pigweed-Auto-Submit: Wyatt Hepler <hepler@google.com>
Commit-Queue: Wyatt Hepler <hepler@google.com>
Reviewed-by: Keir Mierle <keir@google.com>
diff --git a/pw_watch/docs.rst b/pw_watch/docs.rst
index 17b55e1..6e0824a 100644
--- a/pw_watch/docs.rst
+++ b/pw_watch/docs.rst
@@ -40,12 +40,15 @@
# Find a directory and build python.tests, and build pw_apps in out/cmake
pw watch python.tests -C out/cmake pw_apps
-The ``--patterns`` and ``--ignore_patterns`` arguments can be used to include
-and exclude certain file patterns that will trigger rebuilds.
+``pw watch`` only rebuilds when a file that is not ignored by Git changes.
+Adding exclusions to a ``.gitignore`` causes watch to ignore them, even if the
+files were forcibly added to a repo. By default, only files matching certain
+extensions are applied, even if they're tracked by Git. The ``--patterns`` and
+``--ignore_patterns`` arguments can be used to include or exclude specific
+patterns. These patterns do not override Git's ignoring logic.
-The ``--exclude_list`` argument can be used to exclude directories from
-being watched by your system. This can decrease the inotify number in Linux
-system.
+The ``--exclude_list`` argument can be used to exclude directories from being
+watched. This decreases the number of files monitored with inotify in Linux.
While running ``pw watch``, press enter to immediately trigger a rebuild.
diff --git a/pw_watch/py/pw_watch/watch.py b/pw_watch/py/pw_watch/watch.py
index 5674e66..30d221f 100755
--- a/pw_watch/py/pw_watch/watch.py
+++ b/pw_watch/py/pw_watch/watch.py
@@ -47,8 +47,8 @@
from typing import (Iterable, List, NamedTuple, NoReturn, Optional, Sequence,
Tuple)
-from watchdog.events import FileSystemEventHandler # type: ignore
-from watchdog.observers import Observer # type: ignore
+from watchdog.events import FileSystemEventHandler # type: ignore[import]
+from watchdog.observers import Observer # type: ignore[import]
import pw_cli.branding
import pw_cli.color
@@ -120,6 +120,19 @@
return ' '.join(shlex.quote(arg) for arg in self.args())
+def git_ignored(file: Path) -> bool:
+ """Returns true if this file is in a Git repo and ignored by that repo.
+
+ Returns true for ignored files that were manually added to a repo.
+ """
+ returncode = subprocess.run(
+ ['git', 'check-ignore', '--quiet', '--no-index', file],
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL,
+ cwd=file.parent).returncode
+ return returncode in (0, 128)
+
+
class PigweedBuildWatcher(FileSystemEventHandler, DebouncedFunction):
"""Process filesystem events and launch builds if necessary."""
def __init__(
@@ -127,7 +140,6 @@
patterns: Sequence[str] = (),
ignore_patterns: Sequence[str] = (),
build_commands: Sequence[BuildCommand] = (),
- ignore_dirs: Iterable[Path] = (),
charset: WatchCharset = _ASCII_CHARSET,
restart: bool = False,
):
@@ -136,8 +148,6 @@
self.patterns = patterns
self.ignore_patterns = ignore_patterns
self.build_commands = build_commands
- self.ignore_dirs = list(ignore_dirs)
- self.ignore_dirs.extend(cmd.build_dir for cmd in self.build_commands)
self.charset: WatchCharset = charset
self.restart_on_changes = restart
@@ -147,7 +157,7 @@
# Track state of a build. These need to be members instead of locals
# due to the split between dispatch(), run(), and on_complete().
- self.matching_path: Optional[str] = None
+ self.matching_path: Optional[Path] = None
self.builds_succeeded: List[bool] = []
self.wait_for_keypress_thread = threading.Thread(
@@ -166,28 +176,10 @@
except (KeyboardInterrupt, EOFError):
_exit_due_to_interrupt()
- def _path_matches(self, raw_path: str) -> bool:
+ def _path_matches(self, path: Path) -> bool:
"""Returns true if path matches according to the watcher patterns"""
- modified_path = Path(raw_path).resolve()
-
- # Check for modifications inside the ignore directories, and skip them.
- # Ideally these events would never hit the watcher, but selectively
- # watching directories at the OS level is not trivial due to limitations
- # of the watchdog module.
- for ignore_dir in self.ignore_dirs:
- resolved_ignore_dir = ignore_dir.resolve()
- try:
- modified_path.relative_to(resolved_ignore_dir)
- # If no ValueError is raised by the .relative_to() call, then
- # this file is inside the ignore directory; so skip it.
- return False
- except ValueError:
- # Otherwise, the file isn't in the ignore directory, so run the
- # normal pattern checks below.
- pass
-
- return ((not any(modified_path.match(x) for x in self.ignore_patterns))
- and any(modified_path.match(x) for x in self.patterns))
+ return (not any(path.match(x) for x in self.ignore_patterns)
+ and any(path.match(x) for x in self.patterns))
def dispatch(self, event) -> None:
# There isn't any point in triggering builds on new directory creation.
@@ -202,16 +194,16 @@
paths.append(os.fsdecode(event.dest_path))
if event.src_path:
paths.append(os.fsdecode(event.src_path))
- for path in paths:
- _LOG.debug('File event: %s', path)
+ for raw_path in paths:
+ _LOG.debug('File event: %s', raw_path)
- # Check for matching paths among the one or two in the event.
- for path in paths:
- if self._path_matches(path):
+ # Check whether Git cares about any of these paths.
+ for path in (Path(p).resolve() for p in paths):
+ if not git_ignored(path) and self._path_matches(path):
self._handle_matched_event(path)
return
- def _handle_matched_event(self, matching_path: str) -> None:
+ def _handle_matched_event(self, matching_path: Path) -> None:
if self.matching_path is None:
self.matching_path = matching_path
@@ -356,7 +348,7 @@
parser.add_argument('--exclude_list',
nargs='+',
type=Path,
- help=('directories to ignore during pw watch'),
+ help='directories to ignore during pw watch',
default=[])
parser.add_argument('--restart',
action='store_true',
@@ -470,21 +462,6 @@
yield item, True
-def gitignore_patterns():
- """Load patterns in pw_root_dir/.gitignore and return as [str]"""
- pw_root_dir = Path(os.environ['PW_ROOT'])
-
- # Get top level .gitignore entries
- gitignore_path = pw_root_dir / Path('.gitignore')
- if gitignore_path.exists():
- for line in gitignore_path.read_text().splitlines():
- globname = line.strip()
- # If line is empty or a comment.
- if not globname or globname.startswith('#'):
- continue
- yield line
-
-
def get_common_excludes() -> List[Path]:
"""Find commonly excluded directories, and return them as a [Path]"""
exclude_list: List[Path] = []
@@ -599,8 +576,6 @@
# Ignore the user-specified patterns.
ignore_patterns = (ignore_patterns_string.split(_WATCH_PATTERN_DELIMITER)
if ignore_patterns_string else [])
- # Ignore top level pw_root_dir/.gitignore patterns.
- ignore_patterns += gitignore_patterns()
env = pw_cli.env.pigweed_environment()
if env.PW_EMOJI:
@@ -612,8 +587,6 @@
patterns=patterns.split(_WATCH_PATTERN_DELIMITER),
ignore_patterns=ignore_patterns,
build_commands=build_commands,
- ignore_dirs=[Path('.presubmit'),
- Path('.python3-env')],
charset=charset,
restart=restart,
)
@@ -658,7 +631,7 @@
observer.join()
-def main():
+def main() -> None:
"""Watch files for changes and rebuild."""
parser = argparse.ArgumentParser(
description=__doc__,