mirror of
https://github.com/SCST-project/scst.git
synced 2026-10-22 06:02:33 +00:00
Passing the base directly to git diff allows an option such as --quiet to be interpreted as a flag. On a clean worktree it can succeed without selecting any sources, instead of rejecting the invalid revision. Resolve the base with rev-parse --verify --end-of-options and require a commit before building the diff command. Reuse the resulting object ID so options, ranges, tree objects and unknown revisions cannot turn an invalid request into an empty selection. Add a fixture covering rejected bases on a clean tree and a valid tag before and after a source change. Expose the resolver for future patch consumers as well.
67 lines
2.8 KiB
Python
67 lines
2.8 KiB
Python
"""Shared, tracked C source scope for SCST analyzers."""
|
|
|
|
import os
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
SOURCE_DIRS = ("scst", "iscsi-scst", "qla2x00t-32gbit/qla2x00-target",
|
|
"srpt", "fcst", "scst_local", "usr")
|
|
# Compiler capability probes are intentionally not ordinary product code.
|
|
EXCLUDE_DIRS = ("iscsi-scst/conftest", "srpt/conftest")
|
|
|
|
|
|
def in_scope(path: Path, root: Path) -> bool:
|
|
try:
|
|
# Neither an alias nor its destination may widen the source scope.
|
|
names = (Path(os.path.abspath(path)).relative_to(Path(os.path.abspath(root))).as_posix(),
|
|
path.resolve().relative_to(root.resolve()).as_posix())
|
|
except ValueError:
|
|
return False
|
|
return all(name.startswith(tuple(directory + "/" for directory in SOURCE_DIRS))
|
|
and Path(name).suffix in {".c", ".h"} and not name.endswith(".mod.c")
|
|
and not any(name.startswith(directory + "/") for directory in EXCLUDE_DIRS)
|
|
for name in names)
|
|
|
|
|
|
def git_pathspecs() -> list[str]:
|
|
return [*(f":(top){directory}/*.[ch]" for directory in SOURCE_DIRS),
|
|
":(top,exclude)*.mod.c",
|
|
*(f":(top,exclude){directory}/**" for directory in EXCLUDE_DIRS)]
|
|
|
|
|
|
def git_names(root: Path, *arguments: str) -> list[str]:
|
|
output = subprocess.check_output(["git", "-C", str(root), *arguments])
|
|
return [os.fsdecode(name) for name in output.split(b"\0") if name]
|
|
|
|
|
|
def tracked_sources(root: Path) -> list[Path]:
|
|
names = git_names(root, "ls-files", "--cached", "-z", "--", *SOURCE_DIRS)
|
|
return sorted({root / name for name in names
|
|
if in_scope(root / name, root) and (root / name).is_file()})
|
|
|
|
|
|
def resolve_base(root: Path, base: str) -> str:
|
|
"""Require one commit before passing a user-supplied base to Git diff."""
|
|
return subprocess.check_output(
|
|
["git", "-C", str(root), "rev-parse", "--verify", "--end-of-options",
|
|
f"{base}^{{commit}}"], text=True).strip()
|
|
|
|
|
|
def changed_sources(root: Path, base: str) -> list[Path]:
|
|
revision = resolve_base(root, base)
|
|
names = git_names(root, "diff", "--no-renames", "--name-only", "-z",
|
|
"--diff-filter=d", revision, "--", *git_pathspecs())
|
|
return sorted({root / name for name in names
|
|
if in_scope(root / name, root) and (root / name).is_file()})
|
|
|
|
|
|
def select_sources(root: Path, files: list[Path], base: str | None) -> list[Path]:
|
|
if files and base:
|
|
raise ValueError("select files or a Git base, not both")
|
|
if files:
|
|
for path in files:
|
|
if not path.is_file() or not in_scope(path, root):
|
|
raise ValueError(f"not a project C source/header: {path}")
|
|
return sorted({path.resolve() for path in files})
|
|
return changed_sources(root, base) if base else tracked_sources(root)
|