blob: da6e75f118500921b148cebc744b9c2e2a9ecc5e [file]
#!/usr/bin/env python3
# Copyright 2024 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
"""git repo module tests"""
import os
import sys
import tempfile
from pathlib import Path
from subprocess import CompletedProcess
from typing import Any
import re
import shlex
import unittest
from unittest import mock
from pw_cli.tool_runner import ToolRunner
from pw_cli.git_repo import GitRepo, GitRepoFinder, RebaseInfo
from pw_cli.file_filter import FileFilter
from pw_cli.git_repo import collect_files
from pyfakefs import fake_filesystem_unittest
class FakeGitToolRunner(ToolRunner):
def __init__(self, command_results: dict[str, CompletedProcess]) -> None:
self._results = command_results
self.calls: list[tuple[str, tuple[str, ...], dict[str, Any]]] = []
def _run_tool(self, tool: str, args, **kwargs) -> CompletedProcess:
self.calls.append((tool, args, kwargs))
full_command = shlex.join((tool, *tuple(args)))
for cmd, result in self._results.items():
if cmd in full_command:
return result
return CompletedProcess(
args=full_command,
returncode=0xFF,
stderr=f'I do not know how to `{full_command}`'.encode(),
stdout=b'Failed to execute command',
)
def git_ok(cmd: str, stdout: str) -> CompletedProcess:
return CompletedProcess(
args=cmd,
returncode=0,
stderr='',
stdout=stdout.encode(),
)
def git_err(cmd: str, stderr: str, returncode: int = 1) -> CompletedProcess:
return CompletedProcess(
args=cmd,
returncode=returncode,
stderr=stderr.encode(),
stdout='',
)
class TestGitRepo(unittest.TestCase):
"""Tests for git_repo.py"""
GIT_ROOT = Path("/dev/null/test").resolve()
SUBMODULES = [
Path("/dev/null/test/third_party/pigweed").resolve(),
Path("/dev/null/test/vendor/anycom/p1").resolve(),
Path("/dev/null/test/vendor/anycom/p2").resolve(),
]
GIT_SUBMODULES_OUT = "\n".join([str(x) for x in SUBMODULES])
EXPECTED_SUBMODULE_LIST_CMD = shlex.join(
(
'submodule',
'foreach',
'--quiet',
'--recursive',
'echo $toplevel/$sm_path',
)
)
def make_fake_git_repo(self, cmds):
return GitRepo(self.GIT_ROOT, FakeGitToolRunner(cmds))
def test_mock_root(self):
"""Ensure our mock works since so many of our tests depend upon it."""
cmds = {}
repo = self.make_fake_git_repo(cmds)
self.assertEqual(repo.root(), self.GIT_ROOT)
def test_list_submodules_1(self):
"""Ensures the root git repo appears in the submodule list."""
cmds = {
self.EXPECTED_SUBMODULE_LIST_CMD: git_ok(
self.EXPECTED_SUBMODULE_LIST_CMD, self.GIT_SUBMODULES_OUT
)
}
repo = self.make_fake_git_repo(cmds)
paths = repo.list_submodules()
self.assertNotIn(self.GIT_ROOT, paths)
def test_list_submodules_2(self):
cmds = {
self.EXPECTED_SUBMODULE_LIST_CMD: git_ok(
self.EXPECTED_SUBMODULE_LIST_CMD, self.GIT_SUBMODULES_OUT
)
}
repo = self.make_fake_git_repo(cmds)
paths = repo.list_submodules()
self.assertIn(self.SUBMODULES[2], paths)
def test_list_submodules_with_exclude_str(self):
cmds = {
self.EXPECTED_SUBMODULE_LIST_CMD: git_ok(
self.EXPECTED_SUBMODULE_LIST_CMD, self.GIT_SUBMODULES_OUT
)
}
repo = self.make_fake_git_repo(cmds)
paths = repo.list_submodules(
excluded_paths=(self.GIT_ROOT.as_posix(),),
)
self.assertNotIn(self.GIT_ROOT, paths)
def test_list_submodules_with_exclude_regex(self):
cmds = {
self.EXPECTED_SUBMODULE_LIST_CMD: git_ok(
self.EXPECTED_SUBMODULE_LIST_CMD, self.GIT_SUBMODULES_OUT
)
}
repo = self.make_fake_git_repo(cmds)
paths = repo.list_submodules(
excluded_paths=(re.compile("third_party/.*"),),
)
self.assertNotIn(self.SUBMODULES[0], paths)
def test_list_submodules_with_exclude_str_miss(self):
cmds = {
self.EXPECTED_SUBMODULE_LIST_CMD: git_ok(
self.EXPECTED_SUBMODULE_LIST_CMD, self.GIT_SUBMODULES_OUT
)
}
repo = self.make_fake_git_repo(cmds)
paths = repo.list_submodules(
excluded_paths=(re.compile("pigweed"),),
)
self.assertIn(self.SUBMODULES[-1], paths)
def test_list_submodules_with_exclude_regex_miss_1(self):
cmds = {
self.EXPECTED_SUBMODULE_LIST_CMD: git_ok(
self.EXPECTED_SUBMODULE_LIST_CMD, self.GIT_SUBMODULES_OUT
)
}
repo = self.make_fake_git_repo(cmds)
paths = repo.list_submodules(
excluded_paths=(re.compile("foo/.*"),),
)
self.assertNotIn(self.GIT_ROOT, paths)
for module in self.SUBMODULES:
self.assertIn(module, paths)
def test_list_submodules_with_exclude_regex_miss_2(self):
cmds = {
self.EXPECTED_SUBMODULE_LIST_CMD: git_ok(
self.EXPECTED_SUBMODULE_LIST_CMD, self.GIT_SUBMODULES_OUT
)
}
repo = self.make_fake_git_repo(cmds)
paths = repo.list_submodules(
excluded_paths=(re.compile("pigweed"),),
)
self.assertNotIn(self.GIT_ROOT, paths)
for module in self.SUBMODULES:
self.assertIn(module, paths)
def test_list_files_unknown_hash(self):
bad_cmd = "diff --name-only --diff-filter=d 'something' --"
good_cmd = 'ls-files --'
fake_path = 'path/to/foo.h'
cmds = {
bad_cmd: git_err(bad_cmd, "fatal: bad revision 'something'"),
good_cmd: git_ok(good_cmd, fake_path + '\n'),
}
expected_file_path = self.GIT_ROOT / Path(fake_path)
repo = self.make_fake_git_repo(cmds)
# This function needs to be mocked because it does a `is_file()` check
# on returned paths. Since we're not using real files, nothing will
# be yielded.
repo._ls_files = mock.MagicMock( # pylint: disable=protected-access
return_value=[expected_file_path]
)
paths = repo.list_files(commit='something')
self.assertIn(expected_file_path, paths)
def test_fake_uncommitted_changes(self):
index_update = 'update-index -q --refresh'
diff_index = 'diff-index --quiet HEAD --'
cmds = {
index_update: git_ok(index_update, ''),
diff_index: git_err(diff_index, '', returncode=1),
}
repo = self.make_fake_git_repo(cmds)
self.assertTrue(repo.has_uncommitted_changes())
def test_name_rev(self):
git_cmd = 'name-rev --no-undefined --name-only HEAD'
cmds = {
git_cmd: git_ok(git_cmd, 'main'),
}
repo = self.make_fake_git_repo(cmds)
self.assertEqual(repo.name_rev(), 'main')
def test_name_rev_undefined_fallback(self):
name_rev_cmd = 'name-rev --no-undefined --name-only HEAD'
rev_parse_cmd = 'rev-parse --short HEAD'
cmds = {
name_rev_cmd: git_err(
name_rev_cmd, 'fatal: cannot describe', returncode=128
),
rev_parse_cmd: git_ok(rev_parse_cmd, '1234567'),
}
repo = self.make_fake_git_repo(cmds)
self.assertEqual(repo.name_rev(), '1234567')
def test_commit_count(self):
git_cmd_1 = 'rev-list --count HEAD'
git_cmd_2 = "rev-list --count abc..def"
repo = self.make_fake_git_repo(
{
git_cmd_1: git_ok(git_cmd_1, '1'),
git_cmd_2: git_ok(git_cmd_2, '2'),
}
)
self.assertEqual(repo.commit_count('HEAD'), 1)
self.assertEqual(repo.commit_count('abc', 'def'), 2)
def test_is_in_rebase(self):
with tempfile.TemporaryDirectory() as tmp_dir:
tmp_path = Path(tmp_dir)
git_cmd = 'rev-parse --absolute-git-dir'
cmds = {
git_cmd: git_ok(git_cmd, str(tmp_path / '.git')),
}
repo = GitRepo(tmp_path, FakeGitToolRunner(cmds))
git_dir = tmp_path / '.git'
git_dir.mkdir()
rebase_merge = git_dir / 'rebase-merge'
rebase_apply = git_dir / 'rebase-apply'
# Test neither exists
self.assertFalse(repo.is_in_rebase())
# Test rebase-merge exists
rebase_merge.mkdir()
self.assertTrue(repo.is_in_rebase())
rebase_merge.rmdir()
# Test rebase-apply exists
rebase_apply.mkdir()
self.assertTrue(repo.is_in_rebase())
rebase_apply.rmdir()
def test_rebase_info(self):
with tempfile.TemporaryDirectory() as tmp_dir:
tmp_path = Path(tmp_dir)
git_cmd = 'rev-parse --absolute-git-dir'
cmds = {
git_cmd: git_ok(git_cmd, str(tmp_path / '.git')),
}
repo = GitRepo(tmp_path, FakeGitToolRunner(cmds))
git_dir = tmp_path / '.git'
git_dir.mkdir()
rebase_merge = git_dir / 'rebase-merge'
# Test not in rebase
self.assertIsNone(repo.rebase_info())
# Test in rebase
rebase_merge.mkdir()
(rebase_merge / 'onto').write_text('1234567890abcdef')
(rebase_merge / 'orig-head').write_text('abcdef1234567890')
info = repo.rebase_info()
self.assertIsNotNone(info)
self.assertIsInstance(info, RebaseInfo)
self.assertEqual(info.onto, '1234567890abcdef')
self.assertEqual(info.orig_head, 'abcdef1234567890')
def test_rebase_interactive(self):
"""Verify interactive rebase editor script."""
expected_cmd = 'rebase -i origin/main'
runner = FakeGitToolRunner({expected_cmd: git_ok(expected_cmd, '')})
repo = GitRepo(self.GIT_ROOT, runner)
repo.modify().rebase_interactive('origin/main')
self.assertEqual(len(runner.calls), 1)
tool, args, kwargs = runner.calls[0]
self.assertEqual(tool, 'git')
self.assertIn('rebase', args)
self.assertIn('-i', args)
self.assertIn('origin/main', args)
# Extract the script and run it to verify it works as expected.
editor_cmd = kwargs['env']['GIT_SEQUENCE_EDITOR']
script = editor_cmd.split("'", 1)[1].rstrip("'")
with tempfile.NamedTemporaryFile(mode='w+', delete=False) as f:
f.write("pick 1234 normal commit\n")
f.write("pick 5678 cherry-pick this\n")
f.write("pick 9012 pick in subject\n")
f.close()
with mock.patch.object(sys, 'argv', [sys.executable, f.name]):
exec(script, {}) # pylint: disable=exec-used
result = Path(f.name).read_text()
Path(f.name).unlink()
self.assertEqual(
result,
"edit 1234 normal commit\n"
"edit 5678 cherry-pick this\n"
"edit 9012 pick in subject\n",
)
def test_rebase_continue(self):
git_cmd = 'rebase --continue'
cmds = {
git_cmd: git_ok(git_cmd, ''),
}
repo = self.make_fake_git_repo(cmds)
repo.modify().rebase_continue()
def test_amend_commit_with_updated_files(self):
cmds = {
'add -u': git_ok('add -u', ''),
'commit --amend --no-edit': git_ok('commit --amend --no-edit', ''),
}
runner = FakeGitToolRunner(cmds)
repo = GitRepo(self.GIT_ROOT, runner)
repo.modify().amend_commit_with_updated_files()
self.assertEqual(len(runner.calls), 2)
tool1, args1, _ = runner.calls[0]
tool2, args2, _ = runner.calls[1]
self.assertEqual(tool1, 'git')
self.assertIn('add', args1)
self.assertIn('-u', args1)
self.assertEqual(tool2, 'git')
self.assertIn('commit', args2)
self.assertIn('--amend', args2)
self.assertIn('--no-edit', args2)
def _resolve(path: str) -> str:
"""Needed to make Windows happy.
Since resolved paths start with drive letters, any literal string
paths in these tests need to be resolved so they are prefixed with `C:`.
"""
# Avoid manipulation on other OSes since they don't strictly require it.
if os.name != 'nt':
return path
return str(Path(path).resolve())
class TestGitRepoFinder(fake_filesystem_unittest.TestCase):
"""Tests for GitRepoFinder."""
FAKE_ROOT = _resolve('/dev/null/fake/root')
FAKE_NESTED_REPO = _resolve('/dev/null/fake/root/third_party/bogus')
def setUp(self):
self.setUpPyfakefs()
self.fs.create_dir(self.FAKE_ROOT)
os.chdir(self.FAKE_ROOT)
def test_cwd_is_root(self):
"""Tests when cwd is the root of a repo."""
expected_repo_query = shlex.join(
(
'-C',
'.',
'rev-parse',
'--show-toplevel',
)
)
runner = FakeGitToolRunner(
{expected_repo_query: git_ok(expected_repo_query, self.FAKE_ROOT)}
)
finder = GitRepoFinder(runner)
path_to_search = '.'
maybe_repo = finder.find_git_repo(path_to_search)
self.assertNotEqual(
maybe_repo, None, f'Could not resolve {path_to_search}'
)
self.assertEqual(maybe_repo.root(), Path(self.FAKE_ROOT))
def test_cwd_is_not_repo(self):
"""Tests when cwd is not tracked by a repo."""
expected_repo_query = shlex.join(
(
'-C',
'.',
'rev-parse',
'--show-toplevel',
)
)
runner = FakeGitToolRunner(
{expected_repo_query: git_err(expected_repo_query, self.FAKE_ROOT)}
)
finder = GitRepoFinder(runner)
self.assertEqual(finder.find_git_repo('.'), None)
def test_file(self):
"""Tests a file at the root of a repo."""
expected_repo_query = shlex.join(
(
'-C',
'.',
'rev-parse',
'--show-toplevel',
)
)
runner = FakeGitToolRunner(
{expected_repo_query: git_ok(expected_repo_query, self.FAKE_ROOT)}
)
finder = GitRepoFinder(runner)
path_to_search = 'foo.txt'
self.fs.create_file(path_to_search)
maybe_repo = finder.find_git_repo(path_to_search)
self.assertNotEqual(
maybe_repo, None, f'Could not resolve {path_to_search}'
)
self.assertEqual(maybe_repo.root(), Path(self.FAKE_ROOT))
def test_parents_memoized(self):
"""Tests multiple queries that are optimized via memoization."""
expected_repo_query = shlex.join(
(
'-C',
str(Path('subdir/nested')),
'rev-parse',
'--show-toplevel',
)
)
runner = FakeGitToolRunner(
{expected_repo_query: git_ok(expected_repo_query, self.FAKE_ROOT)}
)
finder = GitRepoFinder(runner)
# Because of the ordering, only ONE call to git should be necessary.
paths = [
'subdir/nested/foo.txt',
'subdir/bar.txt',
'subdir/nested/baz.txt',
'bleh.txt',
]
for file_to_find in paths:
self.fs.create_file(file_to_find)
maybe_repo = finder.find_git_repo(file_to_find)
self.assertNotEqual(
maybe_repo, None, f'Could not resolve {file_to_find}'
)
self.assertEqual(maybe_repo.root(), Path(self.FAKE_ROOT))
def test_absolute_path(self):
"""Test that absolute paths hit memoized paths."""
expected_repo_query = shlex.join(
(
'-C',
str(Path('subdir/nested')),
'rev-parse',
'--show-toplevel',
)
)
runner = FakeGitToolRunner(
{expected_repo_query: git_ok(expected_repo_query, self.FAKE_ROOT)}
)
finder = GitRepoFinder(runner)
# Because of the ordering, only ONE call to git should be necessary.
paths = [
'subdir/nested/foo.txt',
_resolve(f'{self.FAKE_ROOT}/subdir/bar.txt'),
]
for file_to_find in paths:
self.fs.create_file(file_to_find)
maybe_repo = finder.find_git_repo(file_to_find)
self.assertNotEqual(
maybe_repo, None, f'Could not resolve {file_to_find}'
)
self.assertEqual(maybe_repo.root(), Path(self.FAKE_ROOT))
def test_subdir(self):
"""Test that querying a dir properly memoizes things."""
expected_repo_query = shlex.join(
(
'-C',
'subdir',
'rev-parse',
'--show-toplevel',
)
)
runner = FakeGitToolRunner(
{expected_repo_query: git_ok(expected_repo_query, self.FAKE_ROOT)}
)
finder = GitRepoFinder(runner)
dir_to_check = 'subdir'
self.fs.create_dir(dir_to_check)
maybe_repo = finder.find_git_repo(dir_to_check)
self.assertNotEqual(
maybe_repo, None, f'Could not resolve {dir_to_check}'
)
self.assertEqual(maybe_repo.root(), Path(self.FAKE_ROOT))
def test_nested_repo(self):
"""Test a nested repo works as expected."""
expected_inner_repo_query = shlex.join(
(
'-C',
str(Path('third_party/bogus/test')),
'rev-parse',
'--show-toplevel',
)
)
expected_outer_repo_query = shlex.join(
(
'-C',
'test',
'rev-parse',
'--show-toplevel',
)
)
runner = FakeGitToolRunner(
{
expected_inner_repo_query: git_ok(
expected_inner_repo_query, self.FAKE_NESTED_REPO
),
expected_outer_repo_query: git_ok(
expected_outer_repo_query, self.FAKE_ROOT
),
}
)
finder = GitRepoFinder(runner)
inner_repo_file = "third_party/bogus/test/baz.txt"
self.fs.create_file(inner_repo_file)
maybe_repo = finder.find_git_repo(inner_repo_file)
self.assertNotEqual(
maybe_repo, None, f'Could not resolve {inner_repo_file}'
)
self.assertEqual(maybe_repo.root(), Path(self.FAKE_NESTED_REPO))
outer_repo_file = "test/baz.txt"
self.fs.create_file(outer_repo_file)
maybe_repo = finder.find_git_repo(outer_repo_file)
self.assertNotEqual(
maybe_repo, None, f'Could not resolve {outer_repo_file}'
)
self.assertEqual(maybe_repo.root(), Path(self.FAKE_ROOT))
def test_absolute_repo_not_under_cwd(self):
"""Test an absolute path that isn't a subdir of cwd works."""
fake_parallel_repo = _resolve('/dev/null/fake/parallel')
expected_repo_query = shlex.join(
(
'-C',
_resolve('/dev/null/fake/parallel/yep'),
'rev-parse',
'--show-toplevel',
)
)
runner = FakeGitToolRunner(
{
expected_repo_query: git_ok(
expected_repo_query, fake_parallel_repo
)
}
)
finder = GitRepoFinder(runner)
path_to_search = _resolve('/dev/null/fake/parallel/yep/foo.txt')
self.fs.create_file(path_to_search)
maybe_repo = finder.find_git_repo(path_to_search)
self.assertNotEqual(
maybe_repo, None, f'Could not resolve {path_to_search}'
)
self.assertEqual(maybe_repo.root(), Path(fake_parallel_repo))
def test_absolute_not_under_cwd(self):
"""Test files not tracked by a repo."""
expected_repo_query = shlex.join(
(
'-C',
_resolve('/dev/null/fake/parallel/yep'),
'rev-parse',
'--show-toplevel',
)
)
runner = FakeGitToolRunner(
{expected_repo_query: git_err(expected_repo_query, '')}
)
finder = GitRepoFinder(runner)
# Because of the ordering, only ONE call to git should be necessary.
paths = [
_resolve('/dev/null/fake/parallel/yep/foo.txt'),
_resolve('/dev/null/fake/bar.txt'),
_resolve('/dev/null/fake/parallel/yep'),
]
for file_to_find in paths:
if file_to_find.endswith('.txt'):
self.fs.create_file(file_to_find)
self.assertEqual(finder.find_git_repo(file_to_find), None)
def test_make_pathspec_relative(self):
"""Tests that pathspec relativization works."""
expected_queries = (
(
shlex.join(
(
'-C',
str(Path('george/one')),
'rev-parse',
'--show-toplevel',
)
),
self.FAKE_ROOT,
),
(
shlex.join(
(
'-C',
str(Path('third_party/bogus')),
'rev-parse',
'--show-toplevel',
)
),
self.FAKE_NESTED_REPO,
),
(
shlex.join(
(
'-C',
str(Path('third_party/bogus/frob')),
'rev-parse',
'--show-toplevel',
)
),
self.FAKE_NESTED_REPO,
),
)
runner = FakeGitToolRunner(
{
expected_args: git_ok(expected_args, repo)
for expected_args, repo in expected_queries
}
)
finder = GitRepoFinder(runner)
files = [
'george/one/two.txt',
'third_party/bogus/sad.png',
]
for file_to_find in files:
self.fs.create_file(file_to_find)
self.fs.create_dir('third_party/bogus/frob')
pathspecs = {
'george/one/two.txt': str(Path('george/one/two.txt')),
'a/': 'a',
'third_party/bogus/sad.png': 'sad.png',
'third_party/bogus/': '.',
'third_party/bogus/frob/j*': str(Path('frob/j*')),
}
for pathspec, expected in pathspecs.items():
maybe_repo, relativized = finder.make_pathspec_relative(pathspec)
self.assertNotEqual(
maybe_repo, None, f'Could not resolve {pathspec}'
)
self.assertEqual(relativized, expected)
def test_make_pathspec_relative_untracked(self):
"""Tests that untracked files work with relativization."""
expected_repo_query = shlex.join(
(
'-C',
str(Path('subdir/nested')),
'rev-parse',
'--show-toplevel',
)
)
runner = FakeGitToolRunner(
{expected_repo_query: git_err(expected_repo_query, '')}
)
finder = GitRepoFinder(runner)
self.fs.create_file('george/one/two.txt')
pathspecs = {
'george/one/two.txt': 'george/one/two.txt',
}
for pathspec, expected in pathspecs.items():
maybe_repo, relativized = finder.make_pathspec_relative(pathspec)
self.assertEqual(
maybe_repo, None, f'Unexpectedly resolved {pathspec}'
)
self.assertEqual(relativized, expected)
def test_make_pathspec_relative_absolute(self):
"""Tests that absolute paths work with relativization."""
expected_repo_query = shlex.join(
(
'-C',
_resolve('/dev/null/fake/root/third_party/bogus/one'),
'rev-parse',
'--show-toplevel',
)
)
runner = FakeGitToolRunner(
{
expected_repo_query: git_ok(
expected_repo_query, self.FAKE_NESTED_REPO
)
}
)
finder = GitRepoFinder(runner)
self.fs.create_file('third_party/bogus/one/two.txt')
pathspecs = {
_resolve('/dev/null/fake/root/third_party/bogus/one/two.txt'): str(
Path('one/two.txt')
),
}
for pathspec, expected in pathspecs.items():
maybe_repo, relativized = finder.make_pathspec_relative(pathspec)
self.assertNotEqual(
maybe_repo, None, f'Could not resolve {pathspec}'
)
self.assertEqual(relativized, expected)
class TestCollectFiles(fake_filesystem_unittest.TestCase):
"""Tests for collect_files."""
def setUp(self) -> None:
self.setUpPyfakefs()
self.root = Path(_resolve('/repo'))
self.fs.create_dir(self.root)
os.chdir(self.root)
@mock.patch('pw_cli.git_repo.describe_git_pattern')
def test_collect_all_files(self, mock_describe: mock.MagicMock) -> None:
mock_describe.return_value = 'mocked description'
runner = FakeGitToolRunner(
{
'rev-parse': git_ok('rev-parse', '/repo'),
'ls-files': git_ok('ls-files', 'file1.txt\nfile2.py'),
}
)
self.fs.create_file(self.root / 'file1.txt')
self.fs.create_file(self.root / 'file2.py')
result = collect_files(
repos=[self.root],
pathspecs=[],
base=None,
tool_runner=runner,
)
self.assertEqual(len(result.paths), 2)
self.assertIn(self.root / 'file1.txt', result.paths)
self.assertIn(self.root / 'file2.py', result.paths)
self.assertEqual(result.paths, result.modified_paths)
@mock.patch('pw_cli.git_repo.describe_git_pattern')
def test_collect_modified_files(
self, mock_describe: mock.MagicMock
) -> None:
mock_describe.return_value = 'mocked description'
runner = FakeGitToolRunner(
{
'rev-parse': git_ok('rev-parse', '/repo'),
'ls-files': git_ok('ls-files', 'file1.txt\nfile2.py'),
'diff': git_ok('diff', 'file2.py'),
}
)
self.fs.create_file(self.root / 'file1.txt')
self.fs.create_file(self.root / 'file2.py')
result = collect_files(
repos=[self.root],
pathspecs=[],
base='HEAD~1',
tool_runner=runner,
)
self.assertEqual(len(result.paths), 2)
self.assertEqual(len(result.modified_paths), 1)
self.assertIn(self.root / 'file2.py', result.modified_paths)
@mock.patch('pw_cli.git_repo.describe_git_pattern')
def test_collect_with_filter(self, mock_describe: mock.MagicMock) -> None:
mock_describe.return_value = 'mocked description'
runner = FakeGitToolRunner(
{
'rev-parse': git_ok('rev-parse', '/repo'),
'ls-files': git_ok('ls-files', 'file1.txt\nfile2.py'),
}
)
self.fs.create_file(self.root / 'file1.txt')
self.fs.create_file(self.root / 'file2.py')
filt = FileFilter(exclude=('.py',))
result = collect_files(
repos=[self.root],
pathspecs=[],
base=None,
file_filter=filt,
tool_runner=runner,
)
self.assertEqual(len(result.paths), 1)
self.assertIn(self.root / 'file1.txt', result.paths)
self.assertNotIn(self.root / 'file2.py', result.paths)
if __name__ == '__main__':
unittest.main()