0
# VCS Integration
1
2
REUSE provides pluggable version control system support through a strategy pattern. The system supports Git, Mercurial, Jujutsu, and Pijul, with automatic VCS detection and root directory discovery.
3
4
## Capabilities
5
6
### Abstract VCS Strategy
7
8
Base class for all version control system implementations.
9
10
```python { .api }
11
class VCSStrategy(ABC):
12
"""
13
Abstract base class for VCS strategies using the strategy pattern.
14
15
Provides the interface for integrating with different version
16
control systems, handling ignore patterns, and discovering
17
project boundaries.
18
"""
19
20
EXE: Optional[str] = None # Path to VCS executable
21
22
def __init__(self, root: StrPath):
23
"""
24
Initialize VCS strategy.
25
26
Args:
27
root: Root directory of the project
28
"""
29
self.root = Path(root)
30
31
@abstractmethod
32
def is_ignored(self, path: StrPath) -> bool:
33
"""
34
Check if path is ignored by VCS.
35
36
Args:
37
path: File or directory path to check (accepts str or Path-like)
38
39
Returns:
40
True if path should be ignored by REUSE processing
41
"""
42
43
@abstractmethod
44
def is_submodule(self, path: StrPath) -> bool:
45
"""
46
Check if path is a VCS submodule.
47
48
Args:
49
path: Directory path to check (accepts str or Path-like)
50
51
Returns:
52
True if path is a submodule/subrepo
53
"""
54
55
@classmethod
56
@abstractmethod
57
def in_repo(cls, directory: StrPath) -> bool:
58
"""
59
Check if directory is inside of the VCS repository.
60
61
Args:
62
directory: Directory path to check
63
64
Returns:
65
True if directory is within a VCS repository
66
67
Raises:
68
NotADirectoryError: if directory is not a directory.
69
"""
70
71
@classmethod
72
@abstractmethod
73
def find_root(cls, cwd: Optional[StrPath] = None) -> Optional[Path]:
74
"""
75
Try to find the root of the project from cwd.
76
77
Args:
78
cwd: Directory to start search from (defaults to current directory)
79
80
Returns:
81
Root directory path if found, None otherwise
82
83
Raises:
84
NotADirectoryError: if directory is not a directory.
85
"""
86
```
87
88
### Git VCS Strategy
89
90
Implementation for Git version control system.
91
92
```python { .api }
93
class VCSStrategyGit(VCSStrategy):
94
"""
95
Git VCS support.
96
97
Provides Git-specific functionality including .gitignore processing,
98
submodule detection, and Git repository root discovery.
99
"""
100
101
def is_ignored(self, path: Path) -> bool:
102
"""
103
Check if path is ignored by Git (.gitignore).
104
105
Args:
106
path: Path to check against Git ignore patterns
107
108
Returns:
109
True if Git would ignore this path
110
"""
111
112
def is_submodule(self, path: Path) -> bool:
113
"""
114
Check if path is a Git submodule.
115
116
Args:
117
path: Directory path to check
118
119
Returns:
120
True if path is a Git submodule
121
"""
122
123
@classmethod
124
def find_root(cls, path: Path) -> Optional[Path]:
125
"""
126
Find Git repository root by looking for .git directory.
127
128
Args:
129
path: Starting path for search
130
131
Returns:
132
Git repository root, or None if not in Git repo
133
"""
134
```
135
136
**Usage Examples:**
137
138
```python
139
from reuse.vcs import VCSStrategyGit
140
from pathlib import Path
141
142
# Create Git strategy for project
143
git_strategy = VCSStrategyGit(Path("/path/to/git/project"))
144
145
# Check if files are ignored
146
test_files = [
147
Path("src/main.py"),
148
Path("build/output.o"),
149
Path(".env"),
150
Path("node_modules/package/index.js")
151
]
152
153
for file_path in test_files:
154
if git_strategy.is_ignored(file_path):
155
print(f"Ignored: {file_path}")
156
else:
157
print(f"Tracked: {file_path}")
158
159
# Check for submodules
160
subdirs = [Path("lib/external"), Path("src/core")]
161
for subdir in subdirs:
162
if git_strategy.is_submodule(subdir):
163
print(f"Submodule: {subdir}")
164
165
# Find Git root
166
git_root = VCSStrategyGit.find_root(Path("/path/to/git/project/src"))
167
print(f"Git root: {git_root}")
168
```
169
170
### Mercurial VCS Strategy
171
172
Implementation for Mercurial version control system.
173
174
```python { .api }
175
class VCSStrategyHg(VCSStrategy):
176
"""
177
Mercurial VCS support.
178
179
Provides Mercurial-specific functionality including .hgignore processing,
180
subrepo detection, and Mercurial repository root discovery.
181
"""
182
183
def is_ignored(self, path: Path) -> bool:
184
"""
185
Check if path is ignored by Mercurial (.hgignore).
186
187
Args:
188
path: Path to check against Mercurial ignore patterns
189
190
Returns:
191
True if Mercurial would ignore this path
192
"""
193
194
def is_submodule(self, path: Path) -> bool:
195
"""
196
Check if path is a Mercurial subrepo.
197
198
Args:
199
path: Directory path to check
200
201
Returns:
202
True if path is a Mercurial subrepo
203
"""
204
205
@classmethod
206
def find_root(cls, path: Path) -> Optional[Path]:
207
"""
208
Find Mercurial repository root by looking for .hg directory.
209
210
Args:
211
path: Starting path for search
212
213
Returns:
214
Mercurial repository root, or None if not in Hg repo
215
"""
216
```
217
218
### Jujutsu VCS Strategy
219
220
Implementation for Jujutsu version control system.
221
222
```python { .api }
223
class VCSStrategyJujutsu(VCSStrategy):
224
"""
225
Jujutsu VCS support.
226
227
Provides Jujutsu-specific functionality including ignore pattern processing
228
and repository root discovery for the modern Jujutsu VCS.
229
"""
230
231
def is_ignored(self, path: Path) -> bool:
232
"""
233
Check if path is ignored by Jujutsu.
234
235
Args:
236
path: Path to check against Jujutsu ignore patterns
237
238
Returns:
239
True if Jujutsu would ignore this path
240
"""
241
242
def is_submodule(self, path: Path) -> bool:
243
"""
244
Check if path is a Jujutsu workspace/submodule.
245
246
Args:
247
path: Directory path to check
248
249
Returns:
250
True if path is a Jujutsu submodule
251
"""
252
253
@classmethod
254
def find_root(cls, path: Path) -> Optional[Path]:
255
"""
256
Find Jujutsu repository root.
257
258
Args:
259
path: Starting path for search
260
261
Returns:
262
Jujutsu repository root, or None if not in Jujutsu repo
263
"""
264
```
265
266
### Pijul VCS Strategy
267
268
Implementation for Pijul version control system.
269
270
```python { .api }
271
class VCSStrategyPijul(VCSStrategy):
272
"""
273
Pijul VCS support.
274
275
Provides Pijul-specific functionality including ignore pattern processing
276
and repository root discovery for the Pijul patch-based VCS.
277
"""
278
279
def is_ignored(self, path: Path) -> bool:
280
"""
281
Check if path is ignored by Pijul.
282
283
Args:
284
path: Path to check against Pijul ignore patterns
285
286
Returns:
287
True if Pijul would ignore this path
288
"""
289
290
def is_submodule(self, path: Path) -> bool:
291
"""
292
Check if path is a Pijul subrepository.
293
294
Args:
295
path: Directory path to check
296
297
Returns:
298
True if path is a Pijul subrepository
299
"""
300
301
@classmethod
302
def find_root(cls, path: Path) -> Optional[Path]:
303
"""
304
Find Pijul repository root.
305
306
Args:
307
path: Starting path for search
308
309
Returns:
310
Pijul repository root, or None if not in Pijul repo
311
"""
312
```
313
314
### No VCS Strategy
315
316
Implementation for projects without version control.
317
318
```python { .api }
319
class VCSStrategyNone(VCSStrategy):
320
"""
321
No VCS support.
322
323
Used for projects that don't use version control or when
324
VCS detection fails. Provides minimal functionality with
325
no ignore pattern processing.
326
"""
327
328
def is_ignored(self, path: Path) -> bool:
329
"""
330
Always returns False (no ignore patterns).
331
332
Args:
333
path: Path to check (ignored)
334
335
Returns:
336
False (nothing is ignored without VCS)
337
"""
338
return False
339
340
def is_submodule(self, path: Path) -> bool:
341
"""
342
Always returns False (no submodules without VCS).
343
344
Args:
345
path: Directory path to check (ignored)
346
347
Returns:
348
False (no submodules without VCS)
349
"""
350
return False
351
352
@classmethod
353
def find_root(cls, path: Path) -> Optional[Path]:
354
"""
355
Returns None (no VCS root to find).
356
357
Args:
358
path: Starting path (ignored)
359
360
Returns:
361
None (no VCS root exists)
362
"""
363
return None
364
```
365
366
### VCS Discovery Functions
367
368
Utility functions for discovering and working with VCS systems.
369
370
```python { .api }
371
def all_vcs_strategies() -> Generator[Type[VCSStrategy], None, None]:
372
"""
373
Get all available VCS strategies.
374
375
Yields:
376
VCS strategy classes in order of preference
377
378
Note:
379
Yields concrete VCS strategy classes (Git, Mercurial, etc.)
380
but not the abstract base class or VCSStrategyNone.
381
"""
382
383
def find_root(cwd: Optional[StrPath] = None) -> Optional[Path]:
384
"""
385
Find VCS root directory starting from current working directory.
386
387
Args:
388
cwd: Starting directory (default: current working directory)
389
390
Returns:
391
VCS root directory, or None if no VCS found
392
393
Note:
394
Tries all available VCS strategies in order until one
395
successfully finds a repository root.
396
"""
397
```
398
399
**Usage Examples:**
400
401
```python
402
from reuse.vcs import all_vcs_strategies, find_root
403
from pathlib import Path
404
405
# List all available VCS strategies
406
print("Available VCS strategies:")
407
for strategy_class in all_vcs_strategies():
408
print(f" - {strategy_class.__name__}")
409
410
# Find VCS root automatically
411
project_root = find_root()
412
if project_root:
413
print(f"Found VCS root: {project_root}")
414
else:
415
print("No VCS repository found")
416
417
# Find VCS root from specific directory
418
specific_root = find_root("/path/to/project/subdir")
419
if specific_root:
420
print(f"VCS root: {specific_root}")
421
422
# Try each VCS strategy manually
423
test_path = Path("/path/to/project")
424
for strategy_class in all_vcs_strategies():
425
root = strategy_class.find_root(test_path)
426
if root:
427
print(f"Found {strategy_class.__name__} repo at: {root}")
428
break
429
```
430
431
## VCS Integration Examples
432
433
### Automatic VCS Strategy Selection
434
435
```python
436
from reuse.vcs import all_vcs_strategies, VCSStrategyNone
437
from pathlib import Path
438
439
def detect_vcs_strategy(project_path: Path) -> VCSStrategy:
440
"""Automatically detect and create appropriate VCS strategy."""
441
442
# Try each VCS strategy
443
for strategy_class in all_vcs_strategies():
444
root = strategy_class.find_root(project_path)
445
if root:
446
print(f"Detected {strategy_class.__name__} at {root}")
447
return strategy_class(root)
448
449
# Fallback to no VCS
450
print("No VCS detected, using VCSStrategyNone")
451
return VCSStrategyNone(project_path)
452
453
# Usage
454
project = Path("/path/to/project")
455
vcs_strategy = detect_vcs_strategy(project)
456
print(f"Using strategy: {type(vcs_strategy).__name__}")
457
```
458
459
### VCS-Aware File Processing
460
461
```python
462
from reuse.vcs import find_root, all_vcs_strategies
463
from pathlib import Path
464
from typing import Iterator
465
466
def get_vcs_tracked_files(project_path: Path) -> Iterator[Path]:
467
"""Get all files that should be processed by REUSE (not ignored by VCS)."""
468
469
# Detect VCS strategy
470
vcs_strategy = None
471
for strategy_class in all_vcs_strategies():
472
root = strategy_class.find_root(project_path)
473
if root:
474
vcs_strategy = strategy_class(root)
475
break
476
477
if not vcs_strategy:
478
# No VCS, process all files
479
for file_path in project_path.rglob("*"):
480
if file_path.is_file():
481
yield file_path
482
return
483
484
# Process files not ignored by VCS
485
for file_path in project_path.rglob("*"):
486
if file_path.is_file() and not vcs_strategy.is_ignored(file_path):
487
yield file_path
488
489
# Usage
490
project_files = list(get_vcs_tracked_files(Path("/path/to/project")))
491
print(f"Found {len(project_files)} VCS-tracked files")
492
```
493
494
### Submodule Handling
495
496
```python
497
from reuse.vcs import VCSStrategyGit
498
from pathlib import Path
499
500
def analyze_submodules(project_path: Path) -> dict:
501
"""Analyze Git submodules in a project."""
502
503
git_root = VCSStrategyGit.find_root(project_path)
504
if not git_root:
505
return {"error": "Not a Git repository"}
506
507
git_strategy = VCSStrategyGit(git_root)
508
509
analysis = {
510
"root": str(git_root),
511
"submodules": [],
512
"submodule_files": 0,
513
"main_repo_files": 0
514
}
515
516
# Check all directories
517
for dir_path in git_root.rglob("*"):
518
if dir_path.is_dir() and git_strategy.is_submodule(dir_path):
519
submodule_info = {
520
"path": str(dir_path.relative_to(git_root)),
521
"absolute_path": str(dir_path),
522
"file_count": sum(1 for f in dir_path.rglob("*") if f.is_file())
523
}
524
analysis["submodules"].append(submodule_info)
525
analysis["submodule_files"] += submodule_info["file_count"]
526
527
# Count main repository files
528
for file_path in git_root.rglob("*"):
529
if file_path.is_file():
530
# Check if file is in a submodule
531
in_submodule = any(
532
git_strategy.is_submodule(parent)
533
for parent in file_path.parents
534
)
535
if not in_submodule:
536
analysis["main_repo_files"] += 1
537
538
return analysis
539
540
# Usage
541
submodule_analysis = analyze_submodules(Path("/path/to/git/project"))
542
print(f"Found {len(submodule_analysis.get('submodules', []))} submodules")
543
for submod in submodule_analysis.get("submodules", []):
544
print(f" {submod['path']}: {submod['file_count']} files")
545
```
546
547
### Multi-VCS Project Detection
548
549
```python
550
from reuse.vcs import all_vcs_strategies
551
from pathlib import Path
552
from typing import Dict, List
553
554
def detect_all_vcs_repos(search_path: Path) -> Dict[str, List[Path]]:
555
"""Detect all VCS repositories under a search path."""
556
557
repos = {}
558
559
# Initialize results for each VCS type
560
for strategy_class in all_vcs_strategies():
561
vcs_name = strategy_class.__name__.replace("VCSStrategy", "").lower()
562
repos[vcs_name] = []
563
564
# Search for repositories
565
for dir_path in search_path.rglob("*"):
566
if dir_path.is_dir():
567
for strategy_class in all_vcs_strategies():
568
root = strategy_class.find_root(dir_path)
569
if root and root not in repos[strategy_class.__name__.replace("VCSStrategy", "").lower()]:
570
vcs_name = strategy_class.__name__.replace("VCSStrategy", "").lower()
571
repos[vcs_name].append(root)
572
573
return repos
574
575
# Usage
576
all_repos = detect_all_vcs_repos(Path("/workspace"))
577
for vcs_type, repo_list in all_repos.items():
578
if repo_list:
579
print(f"{vcs_type.capitalize()} repositories:")
580
for repo in repo_list:
581
print(f" - {repo}")
582
```
583
584
## Type Definitions
585
586
```python { .api }
587
# Type alias used in VCS functions
588
StrPath = Union[str, PathLike[str]] # Something that looks like a path
589
```
590
591
This type alias is used throughout the VCS integration system to accept both string paths and Path objects.