0
# Git Integration
1
2
Git repository interaction, revision comparison, and modified file discovery for determining which files and regions need formatting.
3
4
## Capabilities
5
6
### Repository Detection
7
8
Functions for detecting and validating Git repositories.
9
10
```python { .api }
11
def git_is_repository(path: Path) -> bool:
12
"""
13
Check if the given path is inside a Git working tree.
14
15
Parameters:
16
- path: Path to check for Git repository
17
18
Returns:
19
True if path is within a Git repository, False otherwise
20
"""
21
22
def get_path_in_repo(path: Path) -> str:
23
"""
24
Get relative path within repository, handling VSCode temp files.
25
26
Converts absolute paths to repository-relative paths and handles
27
special cases like VSCode temporary files.
28
29
Parameters:
30
- path: Absolute path to convert
31
32
Returns:
33
Repository-relative path as string
34
"""
35
```
36
37
### Modified File Discovery
38
39
Functions for finding modified Python files using Git.
40
41
```python { .api }
42
def git_get_modified_python_files(
43
paths: Collection[str],
44
revrange: RevisionRange,
45
cwd: Path,
46
) -> Set[str]:
47
"""
48
Ask Git for modified *.py files in the given revision range.
49
50
Parameters:
51
- paths: Collection of file/directory paths to check
52
- revrange: Git revision range for comparison
53
- cwd: Current working directory for Git operations
54
55
Returns:
56
Set of repository-relative paths to modified Python files
57
"""
58
59
def should_reformat_file(path: Path) -> bool:
60
"""
61
Check if the given path is an existing *.py file that should be reformatted.
62
63
Parameters:
64
- path: File path to check
65
66
Returns:
67
True if file exists and is a Python file, False otherwise
68
"""
69
70
def get_missing_at_revision(paths: Set[str], revrange: RevisionRange, cwd: Path) -> Set[str]:
71
"""
72
Return paths that are missing (don't exist) in the given revision.
73
74
Parameters:
75
- paths: Set of paths to check
76
- revrange: Git revision range
77
- cwd: Current working directory
78
79
Returns:
80
Set of paths that don't exist in the specified revision
81
"""
82
```
83
84
### Line-Level Change Detection
85
86
Class for detecting changed lines between Git revisions.
87
88
```python { .api }
89
class EditedLinenumsDiffer:
90
"""
91
Find changed lines between Git revisions for selective formatting.
92
93
This class compares file content between revisions to identify
94
exactly which lines have been modified, enabling precise formatting
95
of only the changed regions.
96
"""
97
98
def __init__(self, root: Path, revrange: RevisionRange):
99
"""
100
Initialize the differ with root path and revision range.
101
102
Parameters:
103
- root: Common root directory for relative paths
104
- revrange: Git revision range for comparison
105
"""
106
107
def revision_vs_lines(
108
self,
109
path_in_repo: Path,
110
content: TextDocument,
111
context_lines: int
112
) -> List[int]:
113
"""
114
Return changed line numbers between revision and current content.
115
116
Parameters:
117
- path_in_repo: Repository-relative path to the file
118
- content: Current content of the file
119
- context_lines: Number of context lines to include around changes
120
121
Returns:
122
List of line numbers that have changed in the current content
123
"""
124
```
125
126
## Usage Examples
127
128
### Basic Repository Operations
129
130
```python
131
from darker.git import git_is_repository, get_path_in_repo
132
from pathlib import Path
133
134
# Check if current directory is a Git repository
135
if git_is_repository(Path.cwd()):
136
print("Current directory is in a Git repository")
137
138
# Get repository-relative path
139
abs_path = Path.cwd() / "src" / "module.py"
140
repo_path = get_path_in_repo(abs_path)
141
print(f"Repository path: {repo_path}")
142
else:
143
print("Not in a Git repository")
144
```
145
146
### Finding Modified Files
147
148
```python
149
from darker.git import git_get_modified_python_files, should_reformat_file
150
from darkgraylib.git import RevisionRange
151
from pathlib import Path
152
153
# Set up revision range
154
revrange = RevisionRange.parse_with_common_ancestor("HEAD~1", ":WORKTREE:")
155
cwd = Path.cwd()
156
157
# Find modified Python files
158
modified_files = git_get_modified_python_files(
159
paths=["src/", "tests/"],
160
revrange=revrange,
161
cwd=cwd
162
)
163
164
print("Modified Python files:")
165
for file_path in modified_files:
166
full_path = cwd / file_path
167
if should_reformat_file(full_path):
168
print(f" {file_path}")
169
```
170
171
### Line-Level Change Detection
172
173
```python
174
from darker.git import EditedLinenumsDiffer
175
from darkgraylib.git import RevisionRange
176
from darkgraylib.utils import TextDocument
177
from pathlib import Path
178
179
# Set up differ
180
revrange = RevisionRange.parse_with_common_ancestor("HEAD", ":WORKTREE:")
181
differ = EditedLinenumsDiffer(Path.cwd(), revrange)
182
183
# Read current file content
184
file_path = "src/module.py"
185
with open(file_path) as f:
186
current_content = TextDocument.from_str(f.read())
187
188
# Find changed lines
189
changed_lines = differ.revision_vs_lines(
190
Path(file_path),
191
current_content,
192
context_lines=3
193
)
194
195
print(f"Changed lines in {file_path}: {changed_lines}")
196
197
# Use line information for selective formatting
198
if changed_lines:
199
print("File has changes and needs formatting")
200
else:
201
print("No changes detected, skipping formatting")
202
```
203
204
### Integration with Main Workflow
205
206
```python
207
from darker.git import EditedLinenumsDiffer, git_get_modified_python_files
208
from darker.chooser import choose_lines
209
from darkgraylib.git import RevisionRange
210
from darkgraylib.utils import TextDocument
211
from pathlib import Path
212
213
def format_only_changed_lines(file_path: str, formatter_func):
214
"""Example of using Git integration for selective formatting."""
215
216
# Set up Git integration
217
revrange = RevisionRange.parse_with_common_ancestor("HEAD", ":WORKTREE:")
218
differ = EditedLinenumsDiffer(revrange, Path.cwd())
219
220
# Read current content
221
with open(file_path) as f:
222
current_content = TextDocument.from_str(f.read())
223
224
# Find changed lines
225
baseline_lines, edited_lines = differ.revision_vs_lines(
226
file_path,
227
current_content
228
)
229
230
if not edited_lines:
231
print(f"No changes in {file_path}, skipping")
232
return current_content
233
234
# Apply formatter to entire content
235
formatted_content = formatter_func(current_content)
236
237
# Select only changed regions from formatted version
238
result = choose_lines(
239
edited_lines,
240
current_content,
241
formatted_content
242
)
243
244
return result
245
246
# Usage
247
formatted = format_only_changed_lines(
248
"src/module.py",
249
lambda content: run_black_formatter(content)
250
)
251
```
252
253
### Working with Missing Files
254
255
```python
256
from darker.git import get_missing_at_revision
257
from darkgraylib.git import RevisionRange
258
from pathlib import Path
259
260
# Check for files that exist now but not in the baseline
261
revrange = RevisionRange.parse_with_common_ancestor("HEAD~5", ":WORKTREE:")
262
current_files = {"src/new_module.py", "src/existing.py", "tests/test_new.py"}
263
264
missing_in_baseline = get_missing_at_revision(
265
current_files,
266
revrange,
267
Path.cwd()
268
)
269
270
print("New files (missing in baseline):")
271
for new_file in missing_in_baseline:
272
print(f" {new_file}")
273
274
# These files should be fully formatted since they're entirely new
275
existing_files = current_files - missing_in_baseline
276
print("Existing files (partial formatting):")
277
for existing_file in existing_files:
278
print(f" {existing_file}")
279
```
280
281
### Custom Revision Ranges
282
283
```python
284
from darker.git import git_get_modified_python_files
285
from darkgraylib.git import RevisionRange
286
from pathlib import Path
287
288
# Different revision range patterns
289
examples = [
290
"HEAD~1", # Compare with previous commit
291
"main", # Compare with main branch
292
"origin/main...", # Compare with remote main branch
293
":PRE-COMMIT:", # Pre-commit hook mode
294
":STDIN:", # Standard input mode
295
]
296
297
for rev_str in examples:
298
try:
299
revrange = RevisionRange.parse_with_common_ancestor(rev_str, ":WORKTREE:")
300
modified = git_get_modified_python_files(
301
paths=["src/"],
302
revrange=revrange,
303
cwd=Path.cwd()
304
)
305
print(f"{rev_str}: {len(modified)} modified files")
306
except Exception as e:
307
print(f"{rev_str}: Error - {e}")
308
```