0
# Advanced Operations
1
2
Advanced Git operations including blame, stashing, submodules, filtering, object database access, and specialized repository operations. These features provide fine-grained control over Git functionality for complex workflows.
3
4
## Capabilities
5
6
### Blame Operations
7
8
Track the origin of each line in a file with detailed authorship information.
9
10
```python { .api }
11
class Blame:
12
@property
13
def hunks(self) -> list[BlameHunk]:
14
"""List of blame hunks"""
15
16
def __len__(self) -> int:
17
"""Number of blame hunks"""
18
19
def __getitem__(self, index: int) -> BlameHunk:
20
"""Get blame hunk by index"""
21
22
def __iter__(self):
23
"""Iterate over blame hunks"""
24
25
def for_line(self, line_no: int) -> BlameHunk:
26
"""Get blame hunk for specific line"""
27
28
class BlameHunk:
29
@property
30
def lines_in_hunk(self) -> int:
31
"""Number of lines in hunk"""
32
33
@property
34
def final_commit_id(self) -> Oid:
35
"""Commit that last modified these lines"""
36
37
@property
38
def final_start_line_number(self) -> int:
39
"""Starting line number in final file"""
40
41
@property
42
def final_signature(self) -> Signature:
43
"""Author signature from final commit"""
44
45
@property
46
def orig_commit_id(self) -> Oid:
47
"""Original commit that introduced these lines"""
48
49
@property
50
def orig_path(self) -> str:
51
"""Original file path"""
52
53
@property
54
def orig_start_line_number(self) -> int:
55
"""Starting line number in original file"""
56
57
@property
58
def orig_signature(self) -> Signature:
59
"""Author signature from original commit"""
60
61
@property
62
def boundary(self) -> bool:
63
"""True if hunk is at boundary of blame range"""
64
65
# Repository blame method
66
class Repository:
67
def blame(
68
self,
69
path: str,
70
flags: int = None,
71
min_match_characters: int = None,
72
newest_commit: Oid = None,
73
oldest_commit: Oid = None,
74
min_line: int = None,
75
max_line: int = None
76
) -> Blame:
77
"""
78
Generate blame information for file.
79
80
Parameters:
81
- path: File path to blame
82
- flags: Blame option flags
83
- min_match_characters: Minimum characters for copy detection
84
- newest_commit: Most recent commit to consider
85
- oldest_commit: Oldest commit to consider
86
- min_line: First line to blame (1-based)
87
- max_line: Last line to blame (1-based)
88
89
Returns:
90
Blame object
91
"""
92
93
# Blame Flag Constants
94
GIT_BLAME_NORMAL: int # Normal blame
95
GIT_BLAME_TRACK_COPIES_SAME_FILE: int # Track copies within file
96
GIT_BLAME_TRACK_COPIES_SAME_COMMIT_MOVES: int # Track moves in same commit
97
GIT_BLAME_TRACK_COPIES_SAME_COMMIT_COPIES: int # Track copies in same commit
98
GIT_BLAME_TRACK_COPIES_ANY_COMMIT_COPIES: int # Track copies across commits
99
GIT_BLAME_FIRST_PARENT: int # Follow only first parent
100
GIT_BLAME_USE_MAILMAP: int # Use mailmap for mapping
101
GIT_BLAME_IGNORE_WHITESPACE: int # Ignore whitespace changes
102
```
103
104
### Stash Operations
105
106
Save and restore working directory changes temporarily.
107
108
```python { .api }
109
class Stash:
110
def save(
111
self,
112
stasher: Signature,
113
message: str,
114
flags: int = None,
115
paths: list[str] = None
116
) -> Oid:
117
"""
118
Save working directory to stash.
119
120
Parameters:
121
- stasher: Signature of person creating stash
122
- message: Stash message
123
- flags: Stash creation flags
124
- paths: Specific paths to stash (None = all)
125
126
Returns:
127
Stash commit OID
128
"""
129
130
def apply(
131
self,
132
index: int,
133
callbacks: 'StashApplyCallbacks' = None,
134
reinstate_index: bool = False
135
):
136
"""Apply stash to working directory"""
137
138
def pop(
139
self,
140
index: int,
141
callbacks: 'StashApplyCallbacks' = None,
142
reinstate_index: bool = False
143
):
144
"""Apply and remove stash"""
145
146
def drop(self, index: int):
147
"""Remove stash without applying"""
148
149
def __len__(self) -> int:
150
"""Number of stashes"""
151
152
def __getitem__(self, index: int) -> tuple[str, Oid]:
153
"""Get stash by index (message, oid)"""
154
155
def __iter__(self):
156
"""Iterate over stashes"""
157
158
# Repository stash access
159
class Repository:
160
@property
161
def stash(self) -> Stash:
162
"""Repository stash collection"""
163
164
# Stash Flag Constants
165
GIT_STASH_DEFAULT: int # Default behavior
166
GIT_STASH_KEEP_INDEX: int # Keep staged changes
167
GIT_STASH_INCLUDE_UNTRACKED: int # Include untracked files
168
GIT_STASH_INCLUDE_IGNORED: int # Include ignored files
169
GIT_STASH_KEEP_ALL: int # Keep all changes
170
171
# Stash Apply Flag Constants
172
GIT_STASH_APPLY_DEFAULT: int # Default apply
173
GIT_STASH_APPLY_REINSTATE_INDEX: int # Restore staged changes
174
175
class StashApplyCallbacks:
176
def apply_progress(self, progress: int) -> int:
177
"""Report apply progress"""
178
return 0
179
180
def checkout_notify(
181
self,
182
why: int,
183
path: str,
184
baseline: DiffFile,
185
target: DiffFile,
186
workdir: DiffFile
187
) -> int:
188
"""Checkout notification callback"""
189
return 0
190
```
191
192
### Submodule Operations
193
194
Manage Git submodules for nested repository structures.
195
196
```python { .api }
197
class Submodule:
198
@property
199
def name(self) -> str:
200
"""Submodule name"""
201
202
@property
203
def path(self) -> str:
204
"""Submodule path in repository"""
205
206
@property
207
def url(self) -> str:
208
"""Submodule remote URL"""
209
210
@property
211
def branch(self) -> str:
212
"""Submodule tracking branch"""
213
214
@property
215
def head_id(self) -> Oid:
216
"""OID of submodule HEAD"""
217
218
@property
219
def index_id(self) -> Oid:
220
"""OID of submodule in index"""
221
222
@property
223
def workdir_id(self) -> Oid:
224
"""OID of submodule in working directory"""
225
226
@property
227
def ignore_rule(self) -> int:
228
"""Submodule ignore rule"""
229
230
@property
231
def update_rule(self) -> int:
232
"""Submodule update rule"""
233
234
def init(self, overwrite: bool = False):
235
"""Initialize submodule"""
236
237
def clone(self, callbacks: 'RemoteCallbacks' = None) -> Repository:
238
"""Clone submodule repository"""
239
240
def update(
241
self,
242
init: bool = False,
243
callbacks: 'RemoteCallbacks' = None
244
):
245
"""Update submodule"""
246
247
def sync(self):
248
"""Sync submodule URL"""
249
250
def reload(self, force: bool = False):
251
"""Reload submodule from config"""
252
253
def status(self) -> int:
254
"""Get submodule status flags"""
255
256
def location(self) -> int:
257
"""Get submodule location flags"""
258
259
class SubmoduleCollection:
260
def add(
261
self,
262
url: str,
263
path: str,
264
use_gitlink: bool = True
265
) -> Submodule:
266
"""Add new submodule"""
267
268
def __getitem__(self, name: str) -> Submodule:
269
"""Get submodule by name or path"""
270
271
def __contains__(self, name: str) -> bool:
272
"""Check if submodule exists"""
273
274
def __iter__(self):
275
"""Iterate over submodules"""
276
277
def __len__(self) -> int:
278
"""Number of submodules"""
279
280
# Repository submodule access
281
class Repository:
282
@property
283
def submodules(self) -> SubmoduleCollection:
284
"""Repository submodules"""
285
286
# Submodule Status Constants
287
GIT_SUBMODULE_STATUS_IN_HEAD: int # Submodule in HEAD
288
GIT_SUBMODULE_STATUS_IN_INDEX: int # Submodule in index
289
GIT_SUBMODULE_STATUS_IN_CONFIG: int # Submodule in config
290
GIT_SUBMODULE_STATUS_IN_WD: int # Submodule in working directory
291
GIT_SUBMODULE_STATUS_INDEX_ADDED: int # Submodule added to index
292
GIT_SUBMODULE_STATUS_INDEX_DELETED: int # Submodule deleted from index
293
GIT_SUBMODULE_STATUS_INDEX_MODIFIED: int # Submodule modified in index
294
GIT_SUBMODULE_STATUS_WD_UNINITIALIZED: int # Submodule not initialized
295
GIT_SUBMODULE_STATUS_WD_ADDED: int # Submodule added to workdir
296
GIT_SUBMODULE_STATUS_WD_DELETED: int # Submodule deleted from workdir
297
GIT_SUBMODULE_STATUS_WD_MODIFIED: int # Submodule modified in workdir
298
299
# Submodule Ignore Constants
300
GIT_SUBMODULE_IGNORE_UNSPECIFIED: int # Use config setting
301
GIT_SUBMODULE_IGNORE_NONE: int # Don't ignore changes
302
GIT_SUBMODULE_IGNORE_UNTRACKED: int # Ignore untracked files
303
GIT_SUBMODULE_IGNORE_DIRTY: int # Ignore dirty working directory
304
GIT_SUBMODULE_IGNORE_ALL: int # Ignore all changes
305
```
306
307
### Filter Operations
308
309
Custom content filtering for clean/smudge operations.
310
311
```python { .api }
312
class Filter:
313
def initialize(self):
314
"""Initialize filter"""
315
return 0
316
317
def shutdown(self):
318
"""Shutdown filter"""
319
320
def check(self, source: 'FilterSource', attr_values: list[str]) -> int:
321
"""Check if filter should apply"""
322
return 0
323
324
def apply(
325
self,
326
source: 'FilterSource',
327
to: 'FilterSource',
328
input_data: bytes
329
) -> bytes:
330
"""Apply filter transformation"""
331
return input_data
332
333
def stream(self, source: 'FilterSource', to: 'FilterSource'):
334
"""Stream-based filter application"""
335
pass
336
337
class FilterSource:
338
@property
339
def repository(self) -> Repository:
340
"""Source repository"""
341
342
@property
343
def path(self) -> str:
344
"""File path"""
345
346
@property
347
def file_mode(self) -> int:
348
"""File mode"""
349
350
@property
351
def oid(self) -> Oid:
352
"""File object ID"""
353
354
@property
355
def flags(self) -> int:
356
"""Filter flags"""
357
358
def filter_register(name: str, filter_obj: Filter, priority: int):
359
"""Register custom filter"""
360
361
def filter_unregister(name: str):
362
"""Unregister custom filter"""
363
364
# Filter Mode Constants
365
GIT_FILTER_TO_WORKTREE: int # Smudge (ODB -> workdir)
366
GIT_FILTER_TO_ODB: int # Clean (workdir -> ODB)
367
368
# Filter Flag Constants
369
GIT_FILTER_DEFAULT: int # Default behavior
370
GIT_FILTER_ALLOW_UNSAFE: int # Allow unsafe filters
371
```
372
373
### Object Database Operations
374
375
Low-level access to Git's object database.
376
377
```python { .api }
378
class Odb:
379
def read(self, oid: Oid) -> tuple[int, bytes]:
380
"""
381
Read object from database.
382
383
Returns:
384
Tuple of (object_type, data)
385
"""
386
387
def write(self, data: bytes, object_type: int) -> Oid:
388
"""Write object to database"""
389
390
def exists(self, oid: Oid) -> bool:
391
"""Check if object exists"""
392
393
def refresh(self):
394
"""Refresh object database"""
395
396
def add_backend(self, backend: 'OdbBackend', priority: int):
397
"""Add storage backend"""
398
399
def add_alternate(self, path: str, priority: int):
400
"""Add alternate object database"""
401
402
def __contains__(self, oid: Oid) -> bool:
403
"""Check if object exists"""
404
405
class OdbBackend:
406
"""Base class for ODB backends"""
407
pass
408
409
class OdbBackendLoose(OdbBackend):
410
def __init__(self, objects_dir: str, compression_level: int = -1):
411
"""Loose object backend"""
412
413
class OdbBackendPack(OdbBackend):
414
def __init__(self, objects_dir: str):
415
"""Pack file backend"""
416
417
# Repository ODB access
418
class Repository:
419
@property
420
def odb(self) -> Odb:
421
"""Object database"""
422
423
def init_file_backend(objects_dir: str) -> OdbBackend:
424
"""Initialize file-based ODB backend"""
425
```
426
427
### Pack File Operations
428
429
Build and manage Git pack files for efficient storage.
430
431
```python { .api }
432
class PackBuilder:
433
def __init__(self, repo: Repository):
434
"""Create pack builder"""
435
436
def insert(self, oid: Oid, name: str = None) -> int:
437
"""Insert object into pack"""
438
439
def insert_tree(self, oid: Oid) -> int:
440
"""Insert tree and all referenced objects"""
441
442
def insert_commit(self, oid: Oid) -> int:
443
"""Insert commit and all referenced objects"""
444
445
def insert_walk(self, walk: Walker) -> int:
446
"""Insert objects from walker"""
447
448
def write(self, path: str = None) -> bytes:
449
"""Write pack to file or return pack data"""
450
451
def set_threads(self, threads: int):
452
"""Set number of threads for packing"""
453
454
@property
455
def written_objects_count(self) -> int:
456
"""Number of objects written"""
457
```
458
459
### Advanced Repository Operations
460
461
Specialized repository operations for complex workflows.
462
463
```python { .api }
464
class Repository:
465
# Cherry-pick and Revert
466
def cherry_pick(
467
self,
468
commit: Oid,
469
callbacks: 'CheckoutCallbacks' = None,
470
mainline: int = 0
471
):
472
"""Cherry-pick commit"""
473
474
def revert(
475
self,
476
commit: Oid,
477
callbacks: 'CheckoutCallbacks' = None,
478
mainline: int = 0
479
):
480
"""Revert commit"""
481
482
# Apply Operations
483
def apply(
484
self,
485
diff: Diff,
486
location: int = None,
487
callbacks: 'CheckoutCallbacks' = None
488
):
489
"""Apply diff/patch to repository"""
490
491
# History Operations
492
def descendant_of(self, commit: Oid, ancestor: Oid) -> bool:
493
"""Check if commit is descendant of ancestor"""
494
495
def ahead_behind(self, local: Oid, upstream: Oid) -> tuple[int, int]:
496
"""Count commits ahead/behind between branches"""
497
498
def merge_base(self, one: Oid, two: Oid) -> Oid:
499
"""Find merge base between commits"""
500
501
def merge_base_many(self, oids: list[Oid]) -> Oid:
502
"""Find merge base among multiple commits"""
503
504
# Worktree Operations
505
def create_worktree(
506
self,
507
name: str,
508
path: str,
509
reference: Reference = None
510
) -> Worktree:
511
"""Create new worktree"""
512
513
def worktrees(self) -> list[str]:
514
"""List worktree names"""
515
516
def lookup_worktree(self, name: str) -> Worktree:
517
"""Get worktree by name"""
518
519
class Worktree:
520
@property
521
def name(self) -> str:
522
"""Worktree name"""
523
524
@property
525
def path(self) -> str:
526
"""Worktree path"""
527
528
@property
529
def is_bare(self) -> bool:
530
"""True if worktree is bare"""
531
532
@property
533
def is_detached(self) -> bool:
534
"""True if HEAD is detached"""
535
536
@property
537
def is_locked(self) -> bool:
538
"""True if worktree is locked"""
539
540
def prune(self, flags: int = 0):
541
"""Prune worktree"""
542
543
def lock(self, reason: str = None):
544
"""Lock worktree"""
545
546
def unlock(self):
547
"""Unlock worktree"""
548
```
549
550
### Usage Examples
551
552
#### Blame Analysis
553
554
```python
555
import pygit2
556
557
repo = pygit2.Repository('/path/to/repo')
558
559
# Generate blame for file
560
blame = repo.blame('src/main.py')
561
562
print(f"Blame has {len(blame)} hunks")
563
564
# Analyze each line
565
with open('src/main.py', 'r') as f:
566
lines = f.readlines()
567
568
line_no = 1
569
for hunk in blame:
570
for i in range(hunk.lines_in_hunk):
571
if line_no <= len(lines):
572
line_content = lines[line_no - 1].rstrip()
573
commit = repo[hunk.final_commit_id]
574
author = hunk.final_signature
575
576
print(f"{hunk.final_commit_id.hex[:8]} {author.name:<20} "
577
f"{line_no:4d}: {line_content}")
578
line_no += 1
579
580
# Blame specific line range
581
blame = repo.blame('README.md', min_line=10, max_line=20)
582
583
# Blame with copy detection
584
blame = repo.blame(
585
'renamed_file.py',
586
flags=pygit2.GIT_BLAME_TRACK_COPIES_ANY_COMMIT_COPIES
587
)
588
```
589
590
#### Stash Operations
591
592
```python
593
# Save working directory to stash
594
signature = pygit2.Signature('User', 'user@example.com')
595
stash_id = repo.stash.save(signature, 'Work in progress')
596
print(f"Created stash: {stash_id}")
597
598
# List stashes
599
print("Stashes:")
600
for i, (message, oid) in enumerate(repo.stash):
601
print(f" {i}: {message} ({oid.hex[:8]})")
602
603
# Apply most recent stash
604
if len(repo.stash) > 0:
605
repo.stash.apply(0)
606
607
# Pop stash (apply and remove)
608
if len(repo.stash) > 0:
609
repo.stash.pop(0)
610
611
# Stash with specific options
612
stash_id = repo.stash.save(
613
signature,
614
'Partial stash',
615
flags=pygit2.GIT_STASH_INCLUDE_UNTRACKED | pygit2.GIT_STASH_KEEP_INDEX,
616
paths=['src/*.py']
617
)
618
```
619
620
#### Submodule Management
621
622
```python
623
# Add submodule
624
submodule = repo.submodules.add(
625
'https://github.com/library/repo.git',
626
'lib/external'
627
)
628
629
# Initialize and clone submodule
630
submodule.init()
631
sub_repo = submodule.clone()
632
633
# Update submodule
634
submodule.update()
635
636
# Check submodule status
637
status = submodule.status()
638
if status & pygit2.GIT_SUBMODULE_STATUS_WD_MODIFIED:
639
print("Submodule has uncommitted changes")
640
641
# Iterate over submodules
642
for name, submodule in repo.submodules:
643
print(f"Submodule {name}: {submodule.url} -> {submodule.path}")
644
print(f" Status: {submodule.status()}")
645
```
646
647
#### Custom Filters
648
649
```python
650
class UppercaseFilter(pygit2.Filter):
651
def apply(self, source, to, input_data):
652
# Convert to uppercase when checking out
653
if to == pygit2.GIT_FILTER_TO_WORKTREE:
654
return input_data.upper()
655
# Convert to lowercase when checking in
656
else:
657
return input_data.lower()
658
659
# Register filter
660
pygit2.filter_register('uppercase', UppercaseFilter(), 100)
661
662
# Filter is applied based on .gitattributes:
663
# *.txt filter=uppercase
664
```
665
666
#### Object Database Operations
667
668
```python
669
# Direct object access
670
odb = repo.odb
671
672
# Check if object exists
673
if odb.exists(some_oid):
674
obj_type, data = odb.read(some_oid)
675
print(f"Object type: {obj_type}, size: {len(data)}")
676
677
# Write object directly
678
blob_data = b"Hello, World!"
679
blob_oid = odb.write(blob_data, pygit2.GIT_OBJECT_BLOB)
680
681
# Add alternate object database
682
odb.add_alternate('/path/to/other/repo/.git/objects', 1)
683
```
684
685
#### Pack File Creation
686
687
```python
688
# Create pack file
689
pack_builder = pygit2.PackBuilder(repo)
690
691
# Add objects to pack
692
for commit in repo.walk(repo.head.target):
693
pack_builder.insert(commit.oid)
694
pack_builder.insert_tree(commit.tree_id)
695
696
# Write pack
697
pack_data = pack_builder.write()
698
with open('custom.pack', 'wb') as f:
699
f.write(pack_data)
700
701
print(f"Packed {pack_builder.written_objects_count} objects")
702
```
703
704
#### Advanced Repository Analysis
705
706
```python
707
# Find merge base
708
main_oid = repo.branches['main'].target
709
feature_oid = repo.branches['feature'].target
710
merge_base = repo.merge_base(main_oid, feature_oid)
711
print(f"Merge base: {merge_base}")
712
713
# Count commits ahead/behind
714
ahead, behind = repo.ahead_behind(feature_oid, main_oid)
715
print(f"Feature is {ahead} commits ahead, {behind} commits behind main")
716
717
# Check ancestry
718
if repo.descendant_of(feature_oid, main_oid):
719
print("Feature branch is descendant of main")
720
721
# Cherry-pick commit
722
commit_to_pick = repo.revparse_single('feature~3')
723
repo.cherry_pick(commit_to_pick.oid)
724
725
# Create worktree
726
worktree = repo.create_worktree('feature-test', '/tmp/feature-test')
727
print(f"Created worktree: {worktree.name} at {worktree.path}")
728
```