Apply Black formatting only in regions changed since last commit
—
Git repository interaction, revision comparison, and modified file discovery for determining which files and regions need formatting.
Functions for detecting and validating Git repositories.
def git_is_repository(path: Path) -> bool:
"""
Check if the given path is inside a Git working tree.
Parameters:
- path: Path to check for Git repository
Returns:
True if path is within a Git repository, False otherwise
"""
def get_path_in_repo(path: Path) -> str:
"""
Get relative path within repository, handling VSCode temp files.
Converts absolute paths to repository-relative paths and handles
special cases like VSCode temporary files.
Parameters:
- path: Absolute path to convert
Returns:
Repository-relative path as string
"""Functions for finding modified Python files using Git.
def git_get_modified_python_files(
paths: Collection[str],
revrange: RevisionRange,
cwd: Path,
) -> Set[str]:
"""
Ask Git for modified *.py files in the given revision range.
Parameters:
- paths: Collection of file/directory paths to check
- revrange: Git revision range for comparison
- cwd: Current working directory for Git operations
Returns:
Set of repository-relative paths to modified Python files
"""
def should_reformat_file(path: Path) -> bool:
"""
Check if the given path is an existing *.py file that should be reformatted.
Parameters:
- path: File path to check
Returns:
True if file exists and is a Python file, False otherwise
"""
def get_missing_at_revision(paths: Set[str], revrange: RevisionRange, cwd: Path) -> Set[str]:
"""
Return paths that are missing (don't exist) in the given revision.
Parameters:
- paths: Set of paths to check
- revrange: Git revision range
- cwd: Current working directory
Returns:
Set of paths that don't exist in the specified revision
"""Class for detecting changed lines between Git revisions.
class EditedLinenumsDiffer:
"""
Find changed lines between Git revisions for selective formatting.
This class compares file content between revisions to identify
exactly which lines have been modified, enabling precise formatting
of only the changed regions.
"""
def __init__(self, root: Path, revrange: RevisionRange):
"""
Initialize the differ with root path and revision range.
Parameters:
- root: Common root directory for relative paths
- revrange: Git revision range for comparison
"""
def revision_vs_lines(
self,
path_in_repo: Path,
content: TextDocument,
context_lines: int
) -> List[int]:
"""
Return changed line numbers between revision and current content.
Parameters:
- path_in_repo: Repository-relative path to the file
- content: Current content of the file
- context_lines: Number of context lines to include around changes
Returns:
List of line numbers that have changed in the current content
"""from darker.git import git_is_repository, get_path_in_repo
from pathlib import Path
# Check if current directory is a Git repository
if git_is_repository(Path.cwd()):
print("Current directory is in a Git repository")
# Get repository-relative path
abs_path = Path.cwd() / "src" / "module.py"
repo_path = get_path_in_repo(abs_path)
print(f"Repository path: {repo_path}")
else:
print("Not in a Git repository")from darker.git import git_get_modified_python_files, should_reformat_file
from darkgraylib.git import RevisionRange
from pathlib import Path
# Set up revision range
revrange = RevisionRange.parse_with_common_ancestor("HEAD~1", ":WORKTREE:")
cwd = Path.cwd()
# Find modified Python files
modified_files = git_get_modified_python_files(
paths=["src/", "tests/"],
revrange=revrange,
cwd=cwd
)
print("Modified Python files:")
for file_path in modified_files:
full_path = cwd / file_path
if should_reformat_file(full_path):
print(f" {file_path}")from darker.git import EditedLinenumsDiffer
from darkgraylib.git import RevisionRange
from darkgraylib.utils import TextDocument
from pathlib import Path
# Set up differ
revrange = RevisionRange.parse_with_common_ancestor("HEAD", ":WORKTREE:")
differ = EditedLinenumsDiffer(Path.cwd(), revrange)
# Read current file content
file_path = "src/module.py"
with open(file_path) as f:
current_content = TextDocument.from_str(f.read())
# Find changed lines
changed_lines = differ.revision_vs_lines(
Path(file_path),
current_content,
context_lines=3
)
print(f"Changed lines in {file_path}: {changed_lines}")
# Use line information for selective formatting
if changed_lines:
print("File has changes and needs formatting")
else:
print("No changes detected, skipping formatting")from darker.git import EditedLinenumsDiffer, git_get_modified_python_files
from darker.chooser import choose_lines
from darkgraylib.git import RevisionRange
from darkgraylib.utils import TextDocument
from pathlib import Path
def format_only_changed_lines(file_path: str, formatter_func):
"""Example of using Git integration for selective formatting."""
# Set up Git integration
revrange = RevisionRange.parse_with_common_ancestor("HEAD", ":WORKTREE:")
differ = EditedLinenumsDiffer(revrange, Path.cwd())
# Read current content
with open(file_path) as f:
current_content = TextDocument.from_str(f.read())
# Find changed lines
baseline_lines, edited_lines = differ.revision_vs_lines(
file_path,
current_content
)
if not edited_lines:
print(f"No changes in {file_path}, skipping")
return current_content
# Apply formatter to entire content
formatted_content = formatter_func(current_content)
# Select only changed regions from formatted version
result = choose_lines(
edited_lines,
current_content,
formatted_content
)
return result
# Usage
formatted = format_only_changed_lines(
"src/module.py",
lambda content: run_black_formatter(content)
)from darker.git import get_missing_at_revision
from darkgraylib.git import RevisionRange
from pathlib import Path
# Check for files that exist now but not in the baseline
revrange = RevisionRange.parse_with_common_ancestor("HEAD~5", ":WORKTREE:")
current_files = {"src/new_module.py", "src/existing.py", "tests/test_new.py"}
missing_in_baseline = get_missing_at_revision(
current_files,
revrange,
Path.cwd()
)
print("New files (missing in baseline):")
for new_file in missing_in_baseline:
print(f" {new_file}")
# These files should be fully formatted since they're entirely new
existing_files = current_files - missing_in_baseline
print("Existing files (partial formatting):")
for existing_file in existing_files:
print(f" {existing_file}")from darker.git import git_get_modified_python_files
from darkgraylib.git import RevisionRange
from pathlib import Path
# Different revision range patterns
examples = [
"HEAD~1", # Compare with previous commit
"main", # Compare with main branch
"origin/main...", # Compare with remote main branch
":PRE-COMMIT:", # Pre-commit hook mode
":STDIN:", # Standard input mode
]
for rev_str in examples:
try:
revrange = RevisionRange.parse_with_common_ancestor(rev_str, ":WORKTREE:")
modified = git_get_modified_python_files(
paths=["src/"],
revrange=revrange,
cwd=Path.cwd()
)
print(f"{rev_str}: {len(modified)} modified files")
except Exception as e:
print(f"{rev_str}: Error - {e}")Install with Tessl CLI
npx tessl i tessl/pypi-darker