Skip to content

Commit

Permalink
Read --stdin-filename=<path> content from stdin
Browse files Browse the repository at this point in the history
  • Loading branch information
akaihola committed Jan 10, 2023
1 parent ca36698 commit ec4ee44
Show file tree
Hide file tree
Showing 7 changed files with 311 additions and 88 deletions.
46 changes: 30 additions & 16 deletions src/darker/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from darker.exceptions import DependencyError, MissingPackageError
from darker.git import (
PRE_COMMIT_FROM_TO_REFS,
STDIN,
WORKTREE,
EditedLinenumsDiffer,
RevisionRange,
Expand Down Expand Up @@ -127,9 +128,12 @@ def _isort_and_blacken_single_file( # pylint: disable=too-many-arguments
# With VSCode, `relative_path_in_rev2` may be a `.py.<HASH>.tmp` file in the
# working tree instead of a `.py` file.
absolute_path_in_rev2 = root / relative_path_in_rev2
rev2_content = git_get_content_at_revision(
relative_path_in_rev2, revrange.rev2, root
)
if revrange.rev2 == STDIN:
rev2_content = TextDocument.from_bytes(sys.stdin.buffer.read())
else:
rev2_content = git_get_content_at_revision(
relative_path_in_rev2, revrange.rev2, root
)
# 1. run isort
rev2_isorted = apply_isort(
rev2_content,
Expand Down Expand Up @@ -394,10 +398,16 @@ def main( # pylint: disable=too-many-locals,too-many-branches,too-many-statemen
if args.skip_magic_trailing_comma is not None:
black_config["skip_magic_trailing_comma"] = args.skip_magic_trailing_comma

paths = {Path(p) for p in args.src}
stdin_mode = args.stdin_filename is not None
if stdin_mode:
paths = {Path(args.stdin_filename)}
# `parse_command_line` guarantees that `args.src` is empty
else:
paths = {Path(p) for p in args.src}
# `parse_command_line` guarantees that `args.stdin_filename` is `None`
root = get_common_root(paths)

revrange = RevisionRange.parse_with_common_ancestor(args.revision, root)
revrange = RevisionRange.parse_with_common_ancestor(args.revision, root, stdin_mode)
output_mode = OutputMode.from_args(args)
write_modified_files = not args.check and output_mode == OutputMode.NOTHING
if write_modified_files:
Expand All @@ -414,21 +424,25 @@ def main( # pylint: disable=too-many-locals,too-many-branches,too-many-statemen
" Either --diff or --check must be used.",
)

missing = get_missing_at_revision(paths, revrange.rev2, root)
if missing:
missing_reprs = " ".join(repr(str(path)) for path in missing)
rev2_repr = "the working tree" if revrange.rev2 == WORKTREE else revrange.rev2
raise ArgumentError(
Action(["PATH"], "path"),
f"Error: Path(s) {missing_reprs} do not exist in {rev2_repr}",
)
if revrange.rev2 != STDIN:
missing = get_missing_at_revision(paths, revrange.rev2, root)
if missing:
missing_reprs = " ".join(repr(str(path)) for path in missing)
rev2_repr = (
"the working tree" if revrange.rev2 == WORKTREE else revrange.rev2
)
raise ArgumentError(
Action(["PATH"], "path"),
f"Error: Path(s) {missing_reprs} do not exist in {rev2_repr}",
)

# These are absolute paths:
files_to_process = filter_python_files(paths, root, {})
files_to_blacken = filter_python_files(paths, root, black_config)
if output_mode == OutputMode.CONTENT:
# With `-d` / `--stdout`, process the file whether modified or not. Paths have
# previously been validated to contain exactly one existing file.
if output_mode == OutputMode.CONTENT or revrange.rev2 == STDIN:
# With `-d` / `--stdout` and `--stdin-filename`, process the file whether
# modified or not. Paths have previously been validated to contain exactly one
# existing file.
changed_files_to_process = {
p.resolve().relative_to(root) for p in files_to_process
}
Expand Down
53 changes: 43 additions & 10 deletions src/darker/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
# - referring to the `PRE_COMMIT_FROM_REF` and `PRE_COMMIT_TO_REF` environment variables
# for determining the revision range
WORKTREE = ":WORKTREE:"
STDIN = ":STDIN:"
PRE_COMMIT_FROM_TO_REFS = ":PRE-COMMIT:"


Expand Down Expand Up @@ -149,34 +150,55 @@ class RevisionRange:

@classmethod
def parse_with_common_ancestor(
cls, revision_range: str, cwd: Path
cls, revision_range: str, cwd: Path, stdin_mode: bool
) -> "RevisionRange":
"""Convert a range expression to a ``RevisionRange`` object
If the expression contains triple dots (e.g. ``master...HEAD``), finds the
common ancestor of the two revisions and uses that as the first revision.
:param revision_range: The revision range as a string to parse
:param cwd: The working directory to use if invoking Git
:param stdin_mode: If `True`, the default for ``rev2`` is ``:STDIN:``
:return: The range parsed into a `RevisionRange` object
"""
rev1, rev2, use_common_ancestor = cls._parse(revision_range)
rev1, rev2, use_common_ancestor = cls._parse(revision_range, stdin_mode)
if use_common_ancestor:
return cls._with_common_ancestor(rev1, rev2, cwd)
return cls(rev1, rev2)

@staticmethod
def _parse(revision_range: str) -> Tuple[str, str, bool]:
def _parse(revision_range: str, stdin_mode: bool) -> Tuple[str, str, bool]:
"""Convert a range expression to revisions, using common ancestor if appropriate
>>> RevisionRange._parse("a..b")
A `ValueError` is raised if ``--stdin-filename`` is used by the revision range
is ``:PRE-COMMIT:`` or the end of the range is not ``:STDIN:``.
:param revision_range: The revision range as a string to parse
:param stdin_mode: If `True`, the default for ``rev2`` is ``:STDIN:``
:return: The range parsed into a `RevisionRange` object
:raise: ValueError
>>> RevisionRange._parse("a..b", stdin_mode=False)
('a', 'b', False)
>>> RevisionRange._parse("a...b")
>>> RevisionRange._parse("a...b", stdin_mode=False)
('a', 'b', True)
>>> RevisionRange._parse("a..")
>>> RevisionRange._parse("a..", stdin_mode=False)
('a', ':WORKTREE:', False)
>>> RevisionRange._parse("a...")
>>> RevisionRange._parse("a...", stdin_mode=False)
('a', ':WORKTREE:', True)
>>> RevisionRange._parse("a..", stdin_mode=True)
('a', ':STDIN:', False)
>>> RevisionRange._parse("a...", stdin_mode=True)
('a', ':STDIN:', True)
"""
if revision_range == PRE_COMMIT_FROM_TO_REFS:
if stdin_mode:
raise ValueError(
f"With --stdin-filename, revision {revision_range!r} is not allowed"
)
try:
return (
os.environ["PRE_COMMIT_FROM_REF"],
Expand All @@ -187,16 +209,27 @@ def _parse(revision_range: str) -> Tuple[str, str, bool]:
# Fallback to running against HEAD
revision_range = "HEAD"
match = COMMIT_RANGE_RE.match(revision_range)
default_rev2 = STDIN if stdin_mode else WORKTREE
if match:
rev1, range_dots, rev2 = match.groups()
use_common_ancestor = range_dots == "..."
return (rev1 or "HEAD", rev2 or WORKTREE, use_common_ancestor)
return (revision_range or "HEAD", WORKTREE, revision_range not in ["", "HEAD"])
effective_rev2 = rev2 or default_rev2
if stdin_mode and effective_rev2 != STDIN:
raise ValueError(
f"With --stdin-filename, rev2 in {revision_range} must be"
f" {STDIN!r}, not {effective_rev2!r}"
)
return (rev1 or "HEAD", rev2 or default_rev2, use_common_ancestor)
return (
revision_range or "HEAD",
default_rev2,
revision_range not in ["", "HEAD"],
)

@classmethod
def _with_common_ancestor(cls, rev1: str, rev2: str, cwd: Path) -> "RevisionRange":
"""Find common ancestor for revisions and return a ``RevisionRange`` object"""
rev2_for_merge_base = "HEAD" if rev2 == WORKTREE else rev2
rev2_for_merge_base = "HEAD" if rev2 in [WORKTREE, STDIN] else rev2
merge_base_cmd = ["merge-base", rev1, rev2_for_merge_base]
common_ancestor = _git_check_output_lines(merge_base_cmd, cwd)[0]
rev1_hash = _git_check_output_lines(["show", "-s", "--pretty=%H", rev1], cwd)[0]
Expand Down
6 changes: 5 additions & 1 deletion src/darker/linting.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from subprocess import PIPE, Popen # nosec
from typing import IO, Generator, List, Set, Tuple

from darker.git import WORKTREE, EditedLinenumsDiffer, RevisionRange
from darker.git import STDIN, WORKTREE, EditedLinenumsDiffer, RevisionRange
from darker.highlighting import colorize

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -232,6 +232,10 @@ def run_linters(
:return: Total number of linting errors found on modified lines
"""
if linter_cmdlines and revrange.rev2 == STDIN:
raise NotImplementedError(
"The -l/--lint option isn't yet available with --stdin-filename"
)
# 10. run linter subprocesses for all edited files (10.-13. optional)
# 11. diff the given revision and worktree (after isort and Black reformatting)
# for each file reported by a linter
Expand Down
Loading

0 comments on commit ec4ee44

Please sign in to comment.