0
# Command-Line Interface
1
2
Complete CLI implementation providing Git command equivalents with argument parsing, progress reporting, and consistent behavior.
3
4
## Capabilities
5
6
### Main Entry Point
7
8
Primary entry point function for the dulwich command-line interface.
9
10
```python { .api }
11
def main(argv: List[str] = None) -> int:
12
"""
13
Main entry point for dulwich CLI.
14
15
Args:
16
argv: Command line arguments (defaults to sys.argv)
17
18
Returns:
19
Exit code (0 for success, non-zero for error)
20
"""
21
22
def signal_int(signal, frame) -> None:
23
"""
24
Signal handler for SIGINT (Ctrl+C).
25
26
Args:
27
signal: Signal number
28
frame: Current stack frame
29
"""
30
31
def signal_quit(signal, frame) -> None:
32
"""
33
Signal handler for SIGQUIT.
34
35
Args:
36
signal: Signal number
37
frame: Current stack frame
38
"""
39
```
40
41
### Command Base Classes
42
43
Base classes for implementing individual Git commands and command groups.
44
45
```python { .api }
46
class Command:
47
"""Base class for dulwich CLI commands."""
48
49
common_options: List[Tuple[str, str]] # Common command-line options
50
51
def run(self, args: List[str]) -> int:
52
"""
53
Run the command with given arguments.
54
55
Args:
56
args: Command-line arguments
57
58
Returns:
59
Exit code
60
"""
61
62
def add_parser_arguments(self, parser) -> None:
63
"""Add command-specific arguments to argument parser."""
64
65
def setup_logging(self, args) -> None:
66
"""Setup logging based on verbosity arguments."""
67
68
class SuperCommand(Command):
69
"""
70
Base class for commands that group multiple subcommands.
71
72
Examples include 'git remote', 'git submodule', 'git stash', etc.
73
"""
74
75
subcommands: ClassVar[dict[str, type[Command]]]
76
77
def run_subcommand(self, subcommand: str, args: List[str]) -> int:
78
"""
79
Run a specific subcommand.
80
81
Args:
82
subcommand: Name of subcommand to run
83
args: Arguments for the subcommand
84
85
Returns:
86
Exit code
87
"""
88
```
89
90
### Utility Functions
91
92
Helper functions used throughout the CLI implementation.
93
94
```python { .api }
95
def parse_relative_time(time_str: str) -> int:
96
"""
97
Parse relative time string like '2 weeks ago' into seconds.
98
99
Args:
100
time_str: String like '2 weeks ago' or 'now'
101
102
Returns:
103
Number of seconds
104
105
Raises:
106
ValueError: If time string cannot be parsed
107
"""
108
109
def format_bytes(bytes: int) -> str:
110
"""
111
Format bytes as human-readable string.
112
113
Args:
114
bytes: Number of bytes
115
116
Returns:
117
Human-readable string like "1.5 MB"
118
"""
119
120
def launch_editor(template_content: bytes = b"") -> bytes:
121
"""
122
Launch editor for user to enter text.
123
124
Args:
125
template_content: Initial content for editor
126
127
Returns:
128
Edited content as bytes
129
"""
130
```
131
132
### Paging System
133
134
Classes for handling paged output in CLI commands.
135
136
```python { .api }
137
class Pager:
138
"""
139
Text output pager for long command output.
140
141
Automatically pipes output through system pager
142
(like 'less') when output is longer than terminal.
143
"""
144
145
def write(self, text: str) -> int:
146
"""
147
Write text to pager.
148
149
Args:
150
text: Text to write
151
152
Returns:
153
Number of characters written
154
"""
155
156
def flush(self) -> None:
157
"""Flush pager output."""
158
159
def close(self) -> None:
160
"""Close pager and wait for completion."""
161
162
class PagerBuffer:
163
"""
164
Binary buffer wrapper for Pager to mimic sys.stdout.buffer.
165
166
Allows CLI commands to write binary data through the pager
167
by converting to text with appropriate encoding.
168
"""
169
170
def __init__(self, pager: Pager) -> None:
171
"""
172
Initialize buffer wrapper.
173
174
Args:
175
pager: Pager instance to wrap
176
"""
177
178
def write(self, data: bytes) -> int:
179
"""
180
Write bytes to pager.
181
182
Args:
183
data: Bytes to write
184
185
Returns:
186
Number of bytes written
187
"""
188
189
def flush(self) -> None:
190
"""Flush pager output."""
191
192
def writelines(self, lines: list[bytes]) -> None:
193
"""
194
Write multiple lines to pager.
195
196
Args:
197
lines: List of byte lines to write
198
"""
199
200
def close(self) -> None:
201
"""Close pager."""
202
```
203
204
### Available Commands
205
206
The dulwich CLI provides the following commands equivalent to Git operations:
207
208
#### Repository Commands
209
210
```python { .api }
211
class cmd_init(Command):
212
"""Initialize a new Git repository."""
213
214
class cmd_clone(Command):
215
"""Clone a Git repository."""
216
217
class cmd_status(Command):
218
"""Show working tree status."""
219
220
class cmd_log(Command):
221
"""Show commit history."""
222
223
class cmd_show(Command):
224
"""Show object contents."""
225
226
class cmd_describe(Command):
227
"""Describe a commit using the most recent tag."""
228
229
class cmd_reflog(Command):
230
"""Show reference logs."""
231
232
class cmd_count_objects(Command):
233
"""Count unpacked objects and display statistics."""
234
```
235
236
#### File Operations
237
238
```python { .api }
239
class cmd_add(Command):
240
"""Add files to the staging area."""
241
242
class cmd_rm(Command):
243
"""Remove files from repository."""
244
245
class cmd_mv(Command):
246
"""Move/rename files."""
247
248
class cmd_commit(Command):
249
"""Create a new commit."""
250
251
class cmd_reset(Command):
252
"""Reset repository state."""
253
254
class cmd_checkout(Command):
255
"""Checkout branches or files."""
256
257
class cmd_ls_files(Command):
258
"""Show information about files in the index and working tree."""
259
260
class cmd_check_ignore(Command):
261
"""Check if files are ignored by .gitignore rules."""
262
263
class cmd_check_mailmap(Command):
264
"""Check names and email addresses against mailmap."""
265
```
266
267
#### Branch Operations
268
269
```python { .api }
270
class cmd_branch(Command):
271
"""List, create, or delete branches."""
272
273
class cmd_checkout(Command):
274
"""Switch branches or restore working tree files."""
275
276
class cmd_merge(Command):
277
"""Merge branches."""
278
279
class cmd_merge_tree(Command):
280
"""Show three-way merge without touching index."""
281
282
class cmd_cherry_pick(Command):
283
"""Apply changes from existing commits."""
284
285
class cmd_revert(Command):
286
"""Revert commits by creating new commits."""
287
288
class cmd_rebase(Command):
289
"""Reapply commits on top of another base tip."""
290
```
291
292
#### Remote Operations
293
294
```python { .api }
295
class cmd_fetch(Command):
296
"""Fetch from remote repository."""
297
298
class cmd_fetch_pack(Command):
299
"""Fetch pack from remote repository (low-level)."""
300
301
class cmd_pull(Command):
302
"""Pull changes from remote repository."""
303
304
class cmd_push(Command):
305
"""Push changes to remote repository."""
306
307
class cmd_remote(SuperCommand):
308
"""Manage remote repositories."""
309
310
class cmd_remote_add(Command):
311
"""Add remote repository."""
312
313
class cmd_ls_remote(Command):
314
"""List remote references."""
315
```
316
317
#### Tag Operations
318
319
```python { .api }
320
class cmd_tag(Command):
321
"""Create, list, or delete tags."""
322
323
class cmd_for_each_ref(Command):
324
"""Iterate over all references matching pattern."""
325
```
326
327
#### Diff Operations
328
329
```python { .api }
330
class cmd_diff(Command):
331
"""Show differences between commits, trees, or files."""
332
333
class cmd_diff_tree(Command):
334
"""Show differences between trees."""
335
336
class cmd_format_patch(Command):
337
"""Generate patches in mbox format."""
338
```
339
340
#### Advanced Commands
341
342
```python { .api }
343
class cmd_stash(SuperCommand):
344
"""Stash working directory changes."""
345
346
class cmd_stash_list(Command):
347
"""List stash entries."""
348
349
class cmd_stash_push(Command):
350
"""Save local modifications to new stash entry."""
351
352
class cmd_stash_pop(Command):
353
"""Apply stash and remove from stash list."""
354
355
class cmd_archive(Command):
356
"""Create archive from repository."""
357
358
class cmd_gc(Command):
359
"""Cleanup and optimize repository."""
360
361
class cmd_fsck(Command):
362
"""Verify repository integrity."""
363
364
class cmd_pack_refs(Command):
365
"""Pack references for efficiency."""
366
367
class cmd_update_server_info(Command):
368
"""Update auxiliary server information."""
369
370
class cmd_prune(Command):
371
"""Prune unreachable objects from object database."""
372
373
class cmd_repack(Command):
374
"""Repack objects in repository."""
375
376
class cmd_filter_branch(Command):
377
"""Rewrite branches by applying filters."""
378
379
class cmd_bisect(SuperCommand):
380
"""Use binary search to find commit that introduced bug."""
381
```
382
383
#### Low-level Commands
384
385
```python { .api }
386
class cmd_rev_list(Command):
387
"""List commit objects in reverse chronological order."""
388
389
class cmd_ls_tree(Command):
390
"""List tree contents."""
391
392
class cmd_hash_object(Command):
393
"""Compute object ID and optionally store object."""
394
395
class cmd_symbolic_ref(Command):
396
"""Read and modify symbolic references."""
397
398
class cmd_write_tree(Command):
399
"""Create tree object from current index."""
400
401
class cmd_commit_tree(Command):
402
"""Create commit object."""
403
404
class cmd_pack_objects(Command):
405
"""Create packed archive of objects."""
406
407
class cmd_unpack_objects(Command):
408
"""Unpack objects from packed archive."""
409
410
class cmd_dump_pack(Command):
411
"""Dump contents of pack file."""
412
413
class cmd_dump_index(Command):
414
"""Dump contents of index file."""
415
```
416
417
#### Server Commands
418
419
```python { .api }
420
class cmd_daemon(Command):
421
"""Run Git protocol daemon."""
422
423
class cmd_web_daemon(Command):
424
"""Run web-based Git repository browser."""
425
426
class cmd_upload_pack(Command):
427
"""Send packed objects for fetch operations."""
428
429
class cmd_receive_pack(Command):
430
"""Receive packed objects for push operations."""
431
```
432
433
#### Submodule Commands
434
435
```python { .api }
436
class cmd_submodule(SuperCommand):
437
"""Initialize, update, or inspect submodules."""
438
439
class cmd_submodule_list(Command):
440
"""List submodules."""
441
442
class cmd_submodule_init(Command):
443
"""Initialize submodules."""
444
445
class cmd_submodule_add(Command):
446
"""Add new submodule."""
447
448
class cmd_submodule_update(Command):
449
"""Update existing submodules."""
450
```
451
452
#### Notes Commands
453
454
```python { .api }
455
class cmd_notes(SuperCommand):
456
"""Add, show, or manipulate object notes."""
457
458
class cmd_notes_add(Command):
459
"""Add notes to objects."""
460
461
class cmd_notes_show(Command):
462
"""Show notes for objects."""
463
464
class cmd_notes_remove(Command):
465
"""Remove notes from objects."""
466
467
class cmd_notes_list(Command):
468
"""List notes objects."""
469
```
470
471
#### Worktree Commands
472
473
```python { .api }
474
class cmd_worktree(SuperCommand):
475
"""Manage multiple working trees."""
476
477
class cmd_worktree_add(Command):
478
"""Add new working tree."""
479
480
class cmd_worktree_list(Command):
481
"""List working trees."""
482
483
class cmd_worktree_remove(Command):
484
"""Remove working tree."""
485
486
class cmd_worktree_prune(Command):
487
"""Prune working tree information."""
488
489
class cmd_worktree_lock(Command):
490
"""Lock working tree."""
491
492
class cmd_worktree_unlock(Command):
493
"""Unlock working tree."""
494
495
class cmd_worktree_move(Command):
496
"""Move working tree."""
497
```
498
499
#### LFS Commands
500
501
```python { .api }
502
class cmd_lfs(Command):
503
"""Git Large File Storage operations."""
504
```
505
506
#### Bundle Commands
507
508
```python { .api }
509
class cmd_bundle(Command):
510
"""Create, verify, list, and unbundle Git bundles."""
511
```
512
513
#### Utility Commands
514
515
```python { .api }
516
class cmd_help(Command):
517
"""Show help information."""
518
519
class cmd_config(Command):
520
"""Get and set repository or global options."""
521
522
class cmd_annotate(Command):
523
"""Annotate file lines with commit information."""
524
525
class cmd_blame(Command):
526
"""Show what revision and author last modified each line."""
527
```
528
529
#### Exception Classes
530
531
```python { .api }
532
class CommitMessageError(Exception):
533
"""Raised when there's an issue with commit message."""
534
```
535
536
## Usage Examples
537
538
### Running Commands Programmatically
539
540
```python
541
from dulwich.cli import main, parse_relative_time, format_bytes
542
import sys
543
544
# Run dulwich commands programmatically
545
exit_code = main(['status'])
546
if exit_code != 0:
547
print("Command failed", file=sys.stderr)
548
549
# Clone a repository
550
exit_code = main(['clone', 'https://github.com/user/repo.git', 'local-dir'])
551
552
# Show commit log with time filtering
553
since_time = parse_relative_time("2 weeks ago")
554
exit_code = main(['log', f'--since={since_time}', '--oneline'])
555
556
# Format repository size information
557
repo_size = 1073741824 # 1GB in bytes
558
print(f"Repository size: {format_bytes(repo_size)}")
559
```
560
561
### Custom Command Implementation
562
563
```python
564
from dulwich.cli import Command, SuperCommand, launch_editor
565
from dulwich.repo import Repo
566
import argparse
567
568
class CustomCommand(Command):
569
"""Example custom command."""
570
571
def add_parser_arguments(self, parser):
572
parser.add_argument('--verbose', '-v', action='store_true',
573
help='Enable verbose output')
574
parser.add_argument('--edit', action='store_true',
575
help='Launch editor for input')
576
parser.add_argument('path', help='Repository path')
577
578
def run(self, args):
579
repo = Repo(args.path)
580
581
if args.verbose:
582
print(f"Repository at: {repo.path}")
583
584
if args.edit:
585
content = launch_editor(b"Enter your text here:\n")
586
print(f"You entered: {content.decode()}")
587
588
# Perform custom operation
589
refs = list(repo.refs.keys())
590
print(f"Found {len(refs)} references")
591
592
return 0
593
594
class CustomSuperCommand(SuperCommand):
595
"""Example super command with subcommands."""
596
597
subcommands = {
598
'list': CustomCommand,
599
'show': CustomCommand,
600
}
601
602
def run(self, args):
603
if not args or args[0] not in self.subcommands:
604
print("Available subcommands:", list(self.subcommands.keys()))
605
return 1
606
607
return self.run_subcommand(args[0], args[1:])
608
```
609
610
### Command-Line Interface Usage
611
612
```bash
613
# Basic repository operations
614
dulwich init my-repo
615
dulwich clone https://github.com/user/repo.git
616
dulwich status
617
dulwich add file.txt
618
dulwich commit -m "Add file"
619
620
# Branch operations
621
dulwich branch feature-branch
622
dulwich checkout feature-branch
623
dulwich merge main
624
dulwich cherry-pick abc123
625
dulwich revert def456
626
627
# Remote operations
628
dulwich remote add origin https://github.com/user/repo.git
629
dulwich push origin main
630
dulwich pull origin main
631
dulwich fetch origin
632
dulwich ls-remote origin
633
634
# Stashing operations
635
dulwich stash push -m "Save work"
636
dulwich stash list
637
dulwich stash pop
638
639
# Information commands
640
dulwich log --oneline -10
641
dulwich show HEAD
642
dulwich diff HEAD~1..HEAD
643
dulwich describe HEAD
644
dulwich blame file.txt
645
646
# Submodule operations
647
dulwich submodule add https://github.com/user/lib.git lib
648
dulwich submodule update
649
dulwich submodule list
650
651
# Notes operations
652
dulwich notes add -m "Important note" abc123
653
dulwich notes show abc123
654
dulwich notes list
655
656
# Worktree operations
657
dulwich worktree add ../feature-work feature-branch
658
dulwich worktree list
659
dulwich worktree remove ../feature-work
660
661
# Advanced operations
662
dulwich archive HEAD --format=tar.gz
663
dulwich gc --aggressive
664
dulwich fsck --full
665
dulwich filter-branch --tree-filter 'rm -f passwords.txt'
666
dulwich bisect start
667
dulwich bundle create backup.bundle HEAD
668
669
# Low-level operations
670
dulwich hash-object file.txt
671
dulwich ls-tree HEAD
672
dulwich pack-objects .git/objects/pack/pack
673
dulwich unpack-objects < pack-file
674
```
675
676
### Error Handling
677
678
```python
679
from dulwich.cli import main
680
from dulwich.errors import NotGitRepository
681
682
try:
683
exit_code = main(['status'])
684
if exit_code != 0:
685
print("Command completed with errors")
686
except NotGitRepository:
687
print("Not in a Git repository")
688
except Exception as e:
689
print(f"Unexpected error: {e}")
690
```