0
# Git Protocol Clients
1
2
Network protocol implementations for communicating with Git servers over HTTP, SSH, and Git protocols with authentication and progress tracking.
3
4
## Capabilities
5
6
### Base Client Classes
7
8
Abstract base classes and common functionality for Git protocol clients.
9
10
```python { .api }
11
class GitClient:
12
"""Abstract base class for Git protocol clients."""
13
14
def fetch_pack(
15
self,
16
path: str,
17
determine_wants: Callable,
18
graph_walker: object,
19
pack_data: Callable,
20
progress: Optional[Callable] = None,
21
depth: Optional[int] = None
22
) -> FetchPackResult:
23
"""
24
Fetch a pack from the remote repository.
25
26
Args:
27
path: Repository path on remote
28
determine_wants: Function to determine wanted objects
29
graph_walker: Object graph walker
30
pack_data: Callback for pack data
31
progress: Optional progress callback
32
depth: Optional shallow clone depth
33
34
Returns:
35
FetchPackResult with refs and symrefs
36
"""
37
38
def send_pack(
39
self,
40
path: str,
41
determine_wants: Callable,
42
generate_pack_data: Callable,
43
progress: Optional[Callable] = None
44
) -> SendPackResult:
45
"""
46
Send a pack to the remote repository.
47
48
Args:
49
path: Repository path on remote
50
determine_wants: Function to determine wanted objects
51
generate_pack_data: Function to generate pack data
52
progress: Optional progress callback
53
54
Returns:
55
SendPackResult with reference update status
56
"""
57
58
def get_refs(self, path: str) -> Dict[bytes, bytes]:
59
"""
60
Get references from remote repository.
61
62
Args:
63
path: Repository path on remote
64
65
Returns:
66
Dictionary mapping ref names to SHAs
67
"""
68
69
def archive(
70
self,
71
path: str,
72
committish: Optional[str] = None,
73
write_data: Optional[Callable] = None,
74
progress: Optional[Callable] = None,
75
write_error: Optional[Callable] = None,
76
) -> None:
77
"""
78
Retrieve an archive from remote repository.
79
80
Args:
81
path: Repository path on remote
82
committish: Commit-ish to archive
83
write_data: Function to write archive data
84
progress: Optional progress callback
85
write_error: Function to write error messages
86
"""
87
88
def close(self) -> None:
89
"""
90
Close the client and clean up resources.
91
"""
92
93
class TraditionalGitClient(GitClient):
94
"""Traditional Git protocol client implementation.
95
96
Provides common functionality for Git protocol clients
97
that use traditional Git wire protocol.
98
"""
99
100
def __init__(
101
self,
102
can_read: Optional[Callable] = None,
103
read: Optional[Callable] = None,
104
write: Optional[Callable] = None,
105
**kwargs
106
) -> None:
107
"""
108
Initialize traditional Git client.
109
110
Args:
111
can_read: Function to check if data can be read
112
read: Function to read data
113
write: Function to write data
114
"""
115
```
116
117
### Protocol-Specific Clients
118
119
Client implementations for different network protocols.
120
121
```python { .api }
122
class TCPGitClient(TraditionalGitClient):
123
"""TCP-based Git protocol client.
124
125
Connects to Git daemon over TCP socket on specified
126
host and port.
127
"""
128
129
def __init__(
130
self,
131
host: str,
132
port: Optional[int] = None,
133
**kwargs
134
) -> None:
135
"""
136
Initialize TCP Git client.
137
138
Args:
139
host: Git server hostname
140
port: Git server port (default: 9418)
141
"""
142
143
class SubprocessGitClient(TraditionalGitClient):
144
"""Subprocess-based Git client using local git command.
145
146
Executes local git commands in subprocess to communicate
147
with remote repositories.
148
"""
149
150
def __init__(self, **kwargs) -> None:
151
"""
152
Initialize subprocess Git client.
153
"""
154
155
class LocalGitClient(GitClient):
156
"""Local filesystem Git client.
157
158
Accesses Git repositories directly on the local filesystem
159
without network communication.
160
"""
161
162
def __init__(self, **kwargs) -> None:
163
"""
164
Initialize local Git client.
165
"""
166
167
class SSHGitClient(TraditionalGitClient):
168
"""SSH-based Git protocol client.
169
170
Connects to Git repositories over SSH using configurable
171
SSH vendor implementations.
172
"""
173
174
def __init__(
175
self,
176
host: str,
177
port: Optional[int] = None,
178
username: Optional[str] = None,
179
vendor: Optional["SSHVendor"] = None,
180
**kwargs
181
) -> None:
182
"""
183
Initialize SSH Git client.
184
185
Args:
186
host: SSH server hostname
187
port: SSH server port (default: 22)
188
username: SSH username
189
vendor: SSH vendor implementation
190
"""
191
192
class BundleClient(GitClient):
193
"""Git bundle client for reading/writing Git bundles.
194
195
Handles Git bundle format files that contain packaged
196
Git objects and references.
197
"""
198
199
def __init__(self, bundle_file: Union[str, IO]) -> None:
200
"""
201
Initialize bundle client.
202
203
Args:
204
bundle_file: Path to bundle file or file-like object
205
"""
206
```
207
208
### HTTP Clients
209
210
HTTP-based Git protocol clients with authentication support.
211
212
```python { .api }
213
class AbstractHttpGitClient(GitClient):
214
"""Abstract HTTP Git protocol client.
215
216
Base class for HTTP-based Git protocol implementations
217
supporting smart HTTP protocol.
218
"""
219
220
def __init__(
221
self,
222
base_url: str,
223
dumb: Optional[bool] = None,
224
**kwargs
225
) -> None:
226
"""
227
Initialize HTTP Git client.
228
229
Args:
230
base_url: Base URL for Git repository
231
dumb: Force dumb HTTP protocol
232
"""
233
234
def _http_request(
235
self,
236
url: str,
237
headers: Optional[Dict[str, str]] = None,
238
data: Optional[bytes] = None
239
) -> object:
240
"""
241
Make HTTP request.
242
243
Args:
244
url: Request URL
245
headers: Optional HTTP headers
246
data: Optional request body data
247
248
Returns:
249
HTTP response object
250
"""
251
252
def _discover_references(
253
self,
254
service: str,
255
base_url: str
256
) -> tuple[Dict[bytes, bytes], Dict[bytes, bytes]]:
257
"""
258
Discover references from HTTP endpoint.
259
260
Args:
261
service: Git service name
262
base_url: Repository base URL
263
264
Returns:
265
Tuple of (refs, symrefs)
266
"""
267
268
class Urllib3HttpGitClient(AbstractHttpGitClient):
269
"""HTTP Git client using urllib3 library.
270
271
Provides HTTP transport using the urllib3 library
272
with connection pooling and retry logic.
273
"""
274
275
def __init__(
276
self,
277
base_url: str,
278
config: Optional[object] = None,
279
pool_manager: Optional["urllib3.PoolManager"] = None,
280
**kwargs
281
) -> None:
282
"""
283
Initialize urllib3 HTTP client.
284
285
Args:
286
base_url: Base URL for Git repository
287
config: Git configuration object
288
pool_manager: Optional urllib3 PoolManager
289
"""
290
```
291
292
### Transport Functions
293
294
Utility functions for determining appropriate transport clients.
295
296
```python { .api }
297
def get_transport_and_path(uri: str) -> Tuple[GitClient, str]:
298
"""
299
Get appropriate transport client and path for URI.
300
301
Args:
302
uri: Git repository URI
303
304
Returns:
305
Tuple of (client, path)
306
"""
307
308
def get_transport_and_path_from_url(
309
url: str,
310
config: Optional[object] = None,
311
**kwargs
312
) -> Tuple[GitClient, str]:
313
"""
314
Get transport client and path from URL with options.
315
316
Args:
317
url: Repository URL
318
config: Optional Git configuration
319
**kwargs: Additional client options
320
321
Returns:
322
Tuple of (client, path)
323
"""
324
325
def parse_rsync_url(url: str) -> Tuple[Optional[str], str, Optional[str]]:
326
"""
327
Parse rsync-style URL into components.
328
329
Args:
330
url: rsync-style URL (user@host:path)
331
332
Returns:
333
Tuple of (username, host, path)
334
"""
335
336
def default_local_git_client_cls() -> type[GitClient]:
337
"""
338
Get default local Git client class.
339
340
Returns:
341
Git client class for local operations
342
"""
343
344
def default_user_agent_string() -> str:
345
"""
346
Get default user agent string for HTTP clients.
347
348
Returns:
349
User agent string identifying Dulwich
350
"""
351
```
352
353
### SSH Vendor Classes
354
355
SSH implementation backends for SSH-based Git clients.
356
357
```python { .api }
358
class SSHVendor:
359
"""Abstract SSH vendor interface.
360
361
Defines interface for different SSH implementations
362
used by SSH Git clients.
363
"""
364
365
def run_command(
366
self,
367
host: str,
368
command: str,
369
username: Optional[str] = None,
370
port: Optional[int] = None,
371
ssh_key: Optional[str] = None,
372
**kwargs
373
) -> "SubprocessWrapper":
374
"""
375
Run command on remote host via SSH.
376
377
Args:
378
host: Remote hostname
379
command: Command to execute
380
username: SSH username
381
port: SSH port number
382
ssh_key: Path to SSH private key
383
384
Returns:
385
SubprocessWrapper for the SSH connection
386
"""
387
388
class SubprocessSSHVendor(SSHVendor):
389
"""SSH vendor using subprocess to call ssh command.
390
391
Uses the system 'ssh' command to create SSH connections.
392
"""
393
394
def __init__(self, ssh_command: Optional[str] = None) -> None:
395
"""
396
Initialize subprocess SSH vendor.
397
398
Args:
399
ssh_command: Custom SSH command (default: 'ssh')
400
"""
401
402
class PLinkSSHVendor(SSHVendor):
403
"""SSH vendor using PuTTY's plink command.
404
405
Windows-specific SSH vendor that uses PuTTY's plink
406
command for SSH connections.
407
"""
408
409
def __init__(self, plink_command: Optional[str] = None) -> None:
410
"""
411
Initialize PLink SSH vendor.
412
413
Args:
414
plink_command: Custom plink command (default: 'plink')
415
"""
416
417
class SubprocessWrapper:
418
"""
419
Wrapper for subprocess providing Git protocol interface.
420
421
Manages stdin, stdout, stderr streams for Git protocol
422
communication over SSH or local subprocesses.
423
"""
424
425
def __init__(self, proc: subprocess.Popen) -> None:
426
"""
427
Initialize subprocess wrapper.
428
429
Args:
430
proc: Popen subprocess instance
431
"""
432
433
def can_read(self) -> bool:
434
"""
435
Check if data can be read from subprocess.
436
437
Returns:
438
True if data is available to read
439
"""
440
441
def write(self, data: bytes) -> int:
442
"""
443
Write data to subprocess stdin.
444
445
Args:
446
data: Data to write
447
448
Returns:
449
Number of bytes written
450
"""
451
452
def read(self, size: int = -1) -> bytes:
453
"""
454
Read data from subprocess stdout.
455
456
Args:
457
size: Number of bytes to read
458
459
Returns:
460
Data read from subprocess
461
"""
462
463
def close(self) -> None:
464
"""
465
Close subprocess and wait for termination.
466
"""
467
```
468
469
### Result Classes
470
471
Data classes containing results from Git protocol operations.
472
473
```python { .api }
474
class FetchPackResult:
475
"""Result from fetch_pack operation.
476
477
Contains information about refs and capabilities
478
returned by the remote during fetch.
479
"""
480
481
refs: Dict[bytes, bytes]
482
symrefs: Dict[bytes, bytes]
483
agent: Optional[bytes]
484
shallow: Optional[set[bytes]]
485
new_shallow: Optional[set[bytes]]
486
new_unshallow: Optional[set[bytes]]
487
488
def __init__(
489
self,
490
refs: Dict[bytes, bytes],
491
symrefs: Optional[Dict[bytes, bytes]] = None,
492
agent: Optional[bytes] = None,
493
shallow: Optional[set[bytes]] = None,
494
new_shallow: Optional[set[bytes]] = None,
495
new_unshallow: Optional[set[bytes]] = None,
496
) -> None:
497
"""
498
Initialize fetch pack result.
499
500
Args:
501
refs: Dictionary of references
502
symrefs: Dictionary of symbolic references
503
agent: Remote agent string
504
shallow: Set of shallow commit SHAs
505
new_shallow: Set of newly shallow commits
506
new_unshallow: Set of newly unshallow commits
507
"""
508
509
class SendPackResult:
510
"""Result from send_pack operation.
511
512
Contains status of reference updates sent to remote.
513
"""
514
515
ref_status: Dict[bytes, str]
516
agent: Optional[bytes]
517
pack_sent: bool
518
519
def __init__(
520
self,
521
ref_status: Dict[bytes, str],
522
agent: Optional[bytes] = None,
523
pack_sent: bool = False,
524
) -> None:
525
"""
526
Initialize send pack result.
527
528
Args:
529
ref_status: Status of each reference update
530
agent: Remote agent string
531
pack_sent: Whether pack data was sent
532
"""
533
534
class LsRemoteResult:
535
"""Result from ls_remote operation.
536
537
Contains references and symbolic references from remote.
538
"""
539
540
refs: Dict[bytes, bytes]
541
symrefs: Dict[bytes, bytes]
542
agent: Optional[bytes]
543
544
def __init__(
545
self,
546
refs: Dict[bytes, bytes],
547
symrefs: Optional[Dict[bytes, bytes]] = None,
548
agent: Optional[bytes] = None,
549
) -> None:
550
"""
551
Initialize ls-remote result.
552
553
Args:
554
refs: Dictionary of references
555
symrefs: Dictionary of symbolic references
556
agent: Remote agent string
557
"""
558
559
class ReportStatusParser:
560
"""Parser for Git report-status capability responses.
561
562
Parses status reports from remote during push operations
563
to determine success/failure of reference updates.
564
"""
565
566
def __init__(self) -> None:
567
"""
568
Initialize report status parser.
569
"""
570
571
def check(self) -> Dict[bytes, str]:
572
"""
573
Check status reports and return results.
574
575
Returns:
576
Dictionary mapping refs to status messages
577
"""
578
```
579
580
### Validation Functions
581
582
Functions for validating protocol requests and responses.
583
584
```python { .api }
585
def check_wants(wants: List[bytes], refs: Dict[bytes, bytes]) -> None:
586
"""
587
Validate that wanted objects exist in refs.
588
589
Args:
590
wants: List of wanted object SHAs
591
refs: Available references
592
593
Raises:
594
InvalidWants: If wanted objects are not available
595
"""
596
597
def _fileno_can_read(fileno: int) -> bool:
598
"""
599
Check if file descriptor is ready for reading.
600
601
Args:
602
fileno: File descriptor number
603
604
Returns:
605
True if data can be read without blocking
606
"""
607
608
def _win32_peek_avail(handle: int) -> int:
609
"""
610
Check available bytes in Windows named pipe.
611
612
Args:
613
handle: Windows pipe handle
614
615
Returns:
616
Number of bytes available to read
617
"""
618
```
619
620
## Exception Classes
621
622
```python { .api }
623
class InvalidWants(Exception):
624
"""Exception raised when client wants unavailable objects.
625
626
Occurs when fetch operation requests objects that don't
627
exist in the remote repository.
628
"""
629
630
def __init__(self, wants: List[bytes]) -> None:
631
"""
632
Initialize exception.
633
634
Args:
635
wants: List of unavailable object SHAs
636
"""
637
638
class HTTPUnauthorized(Exception):
639
"""Exception raised for HTTP authentication failures.
640
641
Occurs when HTTP Git operations fail due to insufficient
642
authentication credentials.
643
"""
644
645
def __init__(self, www_authenticate: str, url: str) -> None:
646
"""
647
Initialize exception.
648
649
Args:
650
www_authenticate: WWW-Authenticate header value
651
url: URL that failed authentication
652
"""
653
654
class HTTPProxyUnauthorized(Exception):
655
"""Exception raised for HTTP proxy authentication failures.
656
657
Occurs when HTTP proxy requires authentication that
658
was not provided or was invalid.
659
"""
660
661
def __init__(self, proxy_authenticate: str, url: str) -> None:
662
"""
663
Initialize exception.
664
665
Args:
666
proxy_authenticate: Proxy-Authenticate header value
667
url: URL that failed proxy authentication
668
"""
669
670
class StrangeHostname(Exception):
671
"""Exception raised for malformed hostnames.
672
673
Occurs when hostname in Git URL cannot be parsed
674
or contains invalid characters.
675
"""
676
```
677
678
## Usage Examples
679
680
### Basic Client Usage
681
682
```python
683
from dulwich.client import (
684
get_transport_and_path,
685
get_transport_and_path_from_url,
686
InvalidWants,
687
HTTPUnauthorized
688
)
689
from dulwich.repo import Repo
690
691
# Get appropriate client for URL
692
client, path = get_transport_and_path('https://github.com/user/repo.git')
693
694
# Fetch references
695
try:
696
refs = client.get_refs(path)
697
for ref_name, sha in refs.items():
698
print(f"{ref_name.decode()}: {sha.hex()}")
699
except HTTPUnauthorized as e:
700
print(f"Authentication failed for {e.url}")
701
except InvalidWants as e:
702
print(f"Invalid objects requested: {e}")
703
finally:
704
client.close()
705
706
# Advanced client usage with configuration
707
repo = Repo('/path/to/repo')
708
config = repo.get_config()
709
client, path = get_transport_and_path_from_url(
710
'https://github.com/user/repo.git',
711
config=config
712
)
713
714
# Fetch pack with progress
715
def progress_callback(message):
716
print(f"Progress: {message.decode()}")
717
718
def determine_wants(refs, depth=None):
719
# Return list of SHAs to fetch
720
return [refs[b'refs/heads/main']]
721
722
def graph_walker_func():
723
# Return graph walker for the repository
724
return repo.get_graph_walker()
725
726
def pack_data_callback(data):
727
# Process incoming pack data
728
pass
729
730
result = client.fetch_pack(
731
path,
732
determine_wants,
733
graph_walker_func(),
734
pack_data_callback,
735
progress=progress_callback
736
)
737
738
print(f"Fetched {len(result.refs)} references")
739
print(f"Remote agent: {result.agent}")
740
client.close()
741
```
742
743
### Custom SSH Configuration
744
745
```python
746
from dulwich.client import (
747
SSHGitClient,
748
SubprocessSSHVendor,
749
PLinkSSHVendor
750
)
751
import sys
752
753
# Create SSH client with custom vendor
754
if sys.platform == 'win32':
755
ssh_vendor = PLinkSSHVendor()
756
else:
757
ssh_vendor = SubprocessSSHVendor()
758
759
client = SSHGitClient(
760
'git.example.com',
761
username='git',
762
port=2222,
763
vendor=ssh_vendor
764
)
765
766
# Use client for operations
767
try:
768
refs = client.get_refs('/path/to/repo.git')
769
for ref_name, sha in refs.items():
770
print(f"{ref_name.decode()}: {sha.hex()}")
771
772
# Clone repository using SSH
773
def determine_wants(refs, depth=None):
774
return list(refs.values())
775
776
# Fetch all references
777
result = client.fetch_pack(
778
'/path/to/repo.git',
779
determine_wants,
780
None, # graph_walker
781
lambda data: None, # pack_data
782
)
783
print(f"SSH fetch completed: {len(result.refs)} refs")
784
785
finally:
786
client.close()
787
```
788
789
### HTTP Client with Authentication
790
791
```python
792
from dulwich.client import Urllib3HttpGitClient, HTTPUnauthorized
793
import urllib3
794
795
# Create HTTP client with custom pool manager
796
pool_manager = urllib3.PoolManager(
797
cert_reqs='CERT_REQUIRED',
798
ca_certs='/path/to/ca-certificates.crt'
799
)
800
801
client = Urllib3HttpGitClient(
802
'https://github.com/user/repo.git',
803
pool_manager=pool_manager
804
)
805
806
# Configure authentication if needed
807
# HTTP clients support various authentication methods:
808
# - Basic auth via URL: https://user:pass@github.com/user/repo.git
809
# - Token auth via environment or config
810
811
try:
812
# Fetch references
813
refs = client.get_refs('')
814
for ref_name, sha in refs.items():
815
print(f"{ref_name.decode()}: {sha.hex()}")
816
817
# Smart HTTP operations
818
def determine_wants(refs, depth=None):
819
# Only fetch main branch
820
if b'refs/heads/main' in refs:
821
return [refs[b'refs/heads/main']]
822
return []
823
824
result = client.fetch_pack(
825
'', # Empty path for base URL
826
determine_wants,
827
None, # graph_walker
828
lambda data: print(f"Received {len(data)} bytes"),
829
)
830
831
print(f"HTTP fetch completed")
832
print(f"Agent: {result.agent}")
833
834
except HTTPUnauthorized as e:
835
print(f"Authentication required: {e.www_authenticate}")
836
print(f"Failed URL: {e.url}")
837
finally:
838
client.close()
839
840
# Bundle client example
841
from dulwich.client import BundleClient
842
843
# Read from bundle file
844
with open('backup.bundle', 'rb') as bundle_file:
845
bundle_client = BundleClient(bundle_file)
846
refs = bundle_client.get_refs('')
847
print(f"Bundle contains {len(refs)} references")
848
```