~ubuntu-branches/ubuntu/wily/dulwich/wily-proposed

« back to all changes in this revision

Viewing changes to dulwich/client.py

  • Committer: Package Import Robot
  • Author(s): Jelmer Vernooij
  • Date: 2015-09-13 18:15:59 UTC
  • mfrom: (1.5.9) (31.1.3 experimental)
  • Revision ID: package-import@ubuntu.com-20150913181559-qqjuqtjpr26xzkqs
Tags: 0.11.1-1
* New upstream release.
 + Fixes formatting in assertion. Closes: #789546
 + Fixes FTBFS on some architectures. Closes: #798565
* debian/copyright: Fix license header to be unique.
* debian/watch: Fix watch URL to use pypi redirector service.
* Add 01_refs_unicode: Support running on platforms that can't encode
  certain characters.

Show diffs side-by-side

added added

removed removed

Lines of Context:
38
38
 
39
39
__docformat__ = 'restructuredText'
40
40
 
41
 
from io import BytesIO
 
41
from contextlib import closing
 
42
from io import BytesIO, BufferedReader
42
43
import dulwich
43
44
import select
 
45
import shlex
44
46
import socket
45
47
import subprocess
46
48
import sys
60
62
    )
61
63
from dulwich.protocol import (
62
64
    _RBUFSIZE,
 
65
    CAPABILITY_DELETE_REFS,
 
66
    CAPABILITY_MULTI_ACK,
 
67
    CAPABILITY_MULTI_ACK_DETAILED,
 
68
    CAPABILITY_OFS_DELTA,
 
69
    CAPABILITY_REPORT_STATUS,
 
70
    CAPABILITY_SIDE_BAND_64K,
 
71
    CAPABILITY_THIN_PACK,
 
72
    COMMAND_DONE,
 
73
    COMMAND_HAVE,
 
74
    COMMAND_WANT,
 
75
    SIDE_BAND_CHANNEL_DATA,
 
76
    SIDE_BAND_CHANNEL_PROGRESS,
 
77
    SIDE_BAND_CHANNEL_FATAL,
63
78
    PktLineParser,
64
79
    Protocol,
65
80
    ProtocolFile,
79
94
    """Check if a file descriptor is readable."""
80
95
    return len(select.select([fileno], [], [], 0)[0]) > 0
81
96
 
82
 
COMMON_CAPABILITIES = ['ofs-delta', 'side-band-64k']
83
 
FETCH_CAPABILITIES = (['thin-pack', 'multi_ack', 'multi_ack_detailed'] +
 
97
COMMON_CAPABILITIES = [CAPABILITY_OFS_DELTA, CAPABILITY_SIDE_BAND_64K]
 
98
FETCH_CAPABILITIES = ([CAPABILITY_THIN_PACK, CAPABILITY_MULTI_ACK,
 
99
                       CAPABILITY_MULTI_ACK_DETAILED] +
84
100
                      COMMON_CAPABILITIES)
85
 
SEND_CAPABILITIES = ['report-status'] + COMMON_CAPABILITIES
 
101
SEND_CAPABILITIES = [CAPABILITY_REPORT_STATUS] + COMMON_CAPABILITIES
86
102
 
87
103
 
88
104
class ReportStatusParser(object):
101
117
        :raise SendPackError: Raised when the server could not unpack
102
118
        :raise UpdateRefsError: Raised when refs could not be updated
103
119
        """
104
 
        if self._pack_status not in ('unpack ok', None):
 
120
        if self._pack_status not in (b'unpack ok', None):
105
121
            raise SendPackError(self._pack_status)
106
122
        if not self._ref_status_ok:
107
123
            ref_status = {}
108
124
            ok = set()
109
125
            for status in self._ref_statuses:
110
 
                if ' ' not in status:
 
126
                if b' ' not in status:
111
127
                    # malformed response, move on to the next one
112
128
                    continue
113
 
                status, ref = status.split(' ', 1)
 
129
                status, ref = status.split(b' ', 1)
114
130
 
115
 
                if status == 'ng':
116
 
                    if ' ' in ref:
117
 
                        ref, status = ref.split(' ', 1)
 
131
                if status == b'ng':
 
132
                    if b' ' in ref:
 
133
                        ref, status = ref.split(b' ', 1)
118
134
                else:
119
135
                    ok.add(ref)
120
136
                ref_status[ref] = status
121
 
            raise UpdateRefsError('%s failed to update' %
122
 
                                  ', '.join([ref for ref in ref_status
123
 
                                             if ref not in ok]),
124
 
                                  ref_status=ref_status)
 
137
            # TODO(jelmer): don't assume encoding of refs is ascii.
 
138
            raise UpdateRefsError(', '.join([
 
139
                ref.decode('ascii') for ref in ref_status if ref not in ok]) +
 
140
                ' failed to update', ref_status=ref_status)
125
141
 
126
142
    def handle_packet(self, pkt):
127
143
        """Handle a packet.
139
155
        else:
140
156
            ref_status = pkt.strip()
141
157
            self._ref_statuses.append(ref_status)
142
 
            if not ref_status.startswith('ok '):
 
158
            if not ref_status.startswith(b'ok '):
143
159
                self._ref_status_ok = False
144
160
 
145
161
 
148
164
    refs = {}
149
165
    # Receive refs from server
150
166
    for pkt in proto.read_pkt_seq():
151
 
        (sha, ref) = pkt.rstrip('\n').split(None, 1)
152
 
        if sha == 'ERR':
 
167
        (sha, ref) = pkt.rstrip(b'\n').split(None, 1)
 
168
        if sha == b'ERR':
153
169
            raise GitProtocolError(ref)
154
170
        if server_capabilities is None:
155
171
            (ref, server_capabilities) = extract_capabilities(ref)
180
196
        self._fetch_capabilities = set(FETCH_CAPABILITIES)
181
197
        self._send_capabilities = set(SEND_CAPABILITIES)
182
198
        if not thin_packs:
183
 
            self._fetch_capabilities.remove('thin-pack')
 
199
            self._fetch_capabilities.remove(CAPABILITY_THIN_PACK)
184
200
 
185
201
    def send_pack(self, path, determine_wants, generate_pack_contents,
186
202
                  progress=None, write_pack=write_pack_objects):
187
203
        """Upload a pack to a remote repository.
188
204
 
189
 
        :param path: Repository path
 
205
        :param path: Repository path (as bytestring)
190
206
        :param generate_pack_contents: Function that can return a sequence of
191
207
            the shas of the objects to upload.
192
208
        :param progress: Optional progress function
202
218
    def fetch(self, path, target, determine_wants=None, progress=None):
203
219
        """Fetch into a target repository.
204
220
 
205
 
        :param path: Path to fetch from
 
221
        :param path: Path to fetch from (as bytestring)
206
222
        :param target: Target repository to fetch into
207
223
        :param determine_wants: Optional function to determine what refs
208
224
            to fetch
211
227
        """
212
228
        if determine_wants is None:
213
229
            determine_wants = target.object_store.determine_wants_all
 
230
 
214
231
        f, commit, abort = target.object_store.add_pack()
215
232
        try:
216
233
            result = self.fetch_pack(
234
251
        """
235
252
        raise NotImplementedError(self.fetch_pack)
236
253
 
 
254
    def get_refs(self, path):
 
255
        """Retrieve the current refs from a git smart server.
 
256
 
 
257
        :param path: Path to the repo to fetch from. (as bytestring)
 
258
        """
 
259
        raise NotImplementedError(self.get_refs)
 
260
 
237
261
    def _parse_status_report(self, proto):
238
262
        unpack = proto.read_pkt_line().strip()
239
 
        if unpack != 'unpack ok':
 
263
        if unpack != b'unpack ok':
240
264
            st = True
241
265
            # flush remaining error data
242
266
            while st is not None:
248
272
        while ref_status:
249
273
            ref_status = ref_status.strip()
250
274
            statuses.append(ref_status)
251
 
            if not ref_status.startswith('ok '):
 
275
            if not ref_status.startswith(b'ok '):
252
276
                errs = True
253
277
            ref_status = proto.read_pkt_line()
254
278
 
256
280
            ref_status = {}
257
281
            ok = set()
258
282
            for status in statuses:
259
 
                if ' ' not in status:
 
283
                if b' ' not in status:
260
284
                    # malformed response, move on to the next one
261
285
                    continue
262
 
                status, ref = status.split(' ', 1)
 
286
                status, ref = status.split(b' ', 1)
263
287
 
264
 
                if status == 'ng':
265
 
                    if ' ' in ref:
266
 
                        ref, status = ref.split(' ', 1)
 
288
                if status == b'ng':
 
289
                    if b' ' in ref:
 
290
                        ref, status = ref.split(b' ', 1)
267
291
                else:
268
292
                    ok.add(ref)
269
293
                ref_status[ref] = status
270
 
            raise UpdateRefsError('%s failed to update' %
271
 
                                  ', '.join([ref for ref in ref_status
272
 
                                             if ref not in ok]),
 
294
            raise UpdateRefsError(', '.join([ref for ref in ref_status
 
295
                                             if ref not in ok]) +
 
296
                                             b' failed to update',
273
297
                                  ref_status=ref_status)
274
298
 
275
299
    def _read_side_band64k_data(self, proto, channel_callbacks):
282
306
            handlers to use. None for a callback discards channel data.
283
307
        """
284
308
        for pkt in proto.read_pkt_seq():
285
 
            channel = ord(pkt[0])
 
309
            channel = ord(pkt[:1])
286
310
            pkt = pkt[1:]
287
311
            try:
288
312
                cb = channel_callbacks[channel]
306
330
        have = [x for x in old_refs.values() if not x == ZERO_SHA]
307
331
        sent_capabilities = False
308
332
 
309
 
        for refname in set(new_refs.keys() + old_refs.keys()):
 
333
        all_refs = set(new_refs.keys()).union(set(old_refs.keys()))
 
334
        for refname in all_refs:
310
335
            old_sha1 = old_refs.get(refname, ZERO_SHA)
311
336
            new_sha1 = new_refs.get(refname, ZERO_SHA)
312
337
 
313
338
            if old_sha1 != new_sha1:
314
339
                if sent_capabilities:
315
 
                    proto.write_pkt_line('%s %s %s' % (
316
 
                        old_sha1, new_sha1, refname))
 
340
                    proto.write_pkt_line(old_sha1 + b' ' + new_sha1 + b' ' + refname)
317
341
                else:
318
342
                    proto.write_pkt_line(
319
 
                        '%s %s %s\0%s' % (old_sha1, new_sha1, refname,
320
 
                                          ' '.join(capabilities)))
 
343
                        old_sha1 + b' ' + new_sha1 + b' ' + refname + b'\0' +
 
344
                        b' '.join(capabilities))
321
345
                    sent_capabilities = True
322
346
            if new_sha1 not in have and new_sha1 != ZERO_SHA:
323
347
                want.append(new_sha1)
331
355
        :param capabilities: List of negotiated capabilities
332
356
        :param progress: Optional progress reporting function
333
357
        """
334
 
        if "side-band-64k" in capabilities:
 
358
        if b"side-band-64k" in capabilities:
335
359
            if progress is None:
336
360
                progress = lambda x: None
337
361
            channel_callbacks = {2: progress}
338
 
            if 'report-status' in capabilities:
 
362
            if CAPABILITY_REPORT_STATUS in capabilities:
339
363
                channel_callbacks[1] = PktLineParser(
340
364
                    self._report_status_parser.handle_packet).parse
341
365
            self._read_side_band64k_data(proto, channel_callbacks)
342
366
        else:
343
 
            if 'report-status' in capabilities:
 
367
            if CAPABILITY_REPORT_STATUS in capabilities:
344
368
                for pkt in proto.read_pkt_seq():
345
369
                    self._report_status_parser.handle_packet(pkt)
346
370
        if self._report_status_parser is not None:
357
381
        :param can_read: function that returns a boolean that indicates
358
382
            whether there is extra graph data to read on proto
359
383
        """
360
 
        assert isinstance(wants, list) and isinstance(wants[0], str)
361
 
        proto.write_pkt_line('want %s %s\n' % (
362
 
            wants[0], ' '.join(capabilities)))
 
384
        assert isinstance(wants, list) and isinstance(wants[0], bytes)
 
385
        proto.write_pkt_line(COMMAND_WANT + b' ' + wants[0] + b' ' + b' '.join(capabilities) + b'\n')
363
386
        for want in wants[1:]:
364
 
            proto.write_pkt_line('want %s\n' % want)
 
387
            proto.write_pkt_line(COMMAND_WANT + b' ' + want + b'\n')
365
388
        proto.write_pkt_line(None)
366
389
        have = next(graph_walker)
367
390
        while have:
368
 
            proto.write_pkt_line('have %s\n' % have)
 
391
            proto.write_pkt_line(COMMAND_HAVE + b' ' + have + b'\n')
369
392
            if can_read():
370
393
                pkt = proto.read_pkt_line()
371
 
                parts = pkt.rstrip('\n').split(' ')
372
 
                if parts[0] == 'ACK':
 
394
                parts = pkt.rstrip(b'\n').split(b' ')
 
395
                if parts[0] == b'ACK':
373
396
                    graph_walker.ack(parts[1])
374
 
                    if parts[2] in ('continue', 'common'):
 
397
                    if parts[2] in (b'continue', b'common'):
375
398
                        pass
376
 
                    elif parts[2] == 'ready':
 
399
                    elif parts[2] == b'ready':
377
400
                        break
378
401
                    else:
379
402
                        raise AssertionError(
380
403
                            "%s not in ('continue', 'ready', 'common)" %
381
404
                            parts[2])
382
405
            have = next(graph_walker)
383
 
        proto.write_pkt_line('done\n')
 
406
        proto.write_pkt_line(COMMAND_DONE + b'\n')
384
407
 
385
408
    def _handle_upload_pack_tail(self, proto, capabilities, graph_walker,
386
409
                                 pack_data, progress=None, rbufsize=_RBUFSIZE):
395
418
        """
396
419
        pkt = proto.read_pkt_line()
397
420
        while pkt:
398
 
            parts = pkt.rstrip('\n').split(' ')
399
 
            if parts[0] == 'ACK':
 
421
            parts = pkt.rstrip(b'\n').split(b' ')
 
422
            if parts[0] == b'ACK':
400
423
                graph_walker.ack(parts[1])
401
424
            if len(parts) < 3 or parts[2] not in (
402
 
                    'ready', 'continue', 'common'):
 
425
                    b'ready', b'continue', b'common'):
403
426
                break
404
427
            pkt = proto.read_pkt_line()
405
 
        if "side-band-64k" in capabilities:
 
428
        if CAPABILITY_SIDE_BAND_64K in capabilities:
406
429
            if progress is None:
407
430
                # Just ignore progress data
408
431
                progress = lambda x: None
409
 
            self._read_side_band64k_data(proto, {1: pack_data, 2: progress})
 
432
            self._read_side_band64k_data(proto, {
 
433
                SIDE_BAND_CHANNEL_DATA: pack_data,
 
434
                SIDE_BAND_CHANNEL_PROGRESS: progress}
 
435
            )
410
436
        else:
411
437
            while True:
412
438
                data = proto.read(rbufsize)
413
 
                if data == "":
 
439
                if data == b"":
414
440
                    break
415
441
                pack_data(data)
416
442
 
428
454
        reads would block.
429
455
 
430
456
        :param cmd: The git service name to which we should connect.
431
 
        :param path: The path we should pass to the service.
 
457
        :param path: The path we should pass to the service. (as bytestirng)
432
458
        """
433
459
        raise NotImplementedError()
434
460
 
436
462
                  progress=None, write_pack=write_pack_objects):
437
463
        """Upload a pack to a remote repository.
438
464
 
439
 
        :param path: Repository path
 
465
        :param path: Repository path (as bytestring)
440
466
        :param generate_pack_contents: Function that can return a sequence of
441
467
            the shas of the objects to upload.
442
468
        :param progress: Optional callback called with progress updates
447
473
        :raises UpdateRefsError: if the server supports report-status
448
474
                                 and rejects ref updates
449
475
        """
450
 
        proto, unused_can_read = self._connect('receive-pack', path)
 
476
        proto, unused_can_read = self._connect(b'receive-pack', path)
451
477
        with proto:
452
478
            old_refs, server_capabilities = read_pkt_refs(proto)
453
479
            negotiated_capabilities = self._send_capabilities & server_capabilities
454
480
 
455
 
            if 'report-status' in negotiated_capabilities:
 
481
            if CAPABILITY_REPORT_STATUS in negotiated_capabilities:
456
482
                self._report_status_parser = ReportStatusParser()
457
483
            report_status_parser = self._report_status_parser
458
484
 
462
488
                proto.write_pkt_line(None)
463
489
                raise
464
490
 
465
 
            if not 'delete-refs' in server_capabilities:
 
491
            if not CAPABILITY_DELETE_REFS in server_capabilities:
466
492
                # Server does not support deletions. Fail later.
467
493
                new_refs = dict(orig_new_refs)
468
 
                for ref, sha in orig_new_refs.iteritems():
 
494
                for ref, sha in orig_new_refs.items():
469
495
                    if sha == ZERO_SHA:
470
 
                        if 'report-status' in negotiated_capabilities:
 
496
                        if CAPABILITY_REPORT_STATUS in negotiated_capabilities:
471
497
                            report_status_parser._ref_statuses.append(
472
 
                                'ng %s remote does not support deleting refs'
473
 
                                % sha)
 
498
                                b'ng ' + sha + b' remote does not support deleting refs')
474
499
                            report_status_parser._ref_status_ok = False
475
500
                        del new_refs[ref]
476
501
 
493
518
 
494
519
            dowrite = len(objects) > 0
495
520
            dowrite = dowrite or any(old_refs.get(ref) != sha
496
 
                                     for (ref, sha) in new_refs.iteritems()
 
521
                                     for (ref, sha) in new_refs.items()
497
522
                                     if sha != ZERO_SHA)
498
523
            if dowrite:
499
524
                write_pack(proto.write_file(), objects)
511
536
        :param pack_data: Callback called for each bit of data in the pack
512
537
        :param progress: Callback for progress reports (strings)
513
538
        """
514
 
        proto, can_read = self._connect('upload-pack', path)
 
539
        proto, can_read = self._connect(b'upload-pack', path)
515
540
        with proto:
516
541
            refs, server_capabilities = read_pkt_refs(proto)
517
542
            negotiated_capabilities = (
537
562
                proto, negotiated_capabilities, graph_walker, pack_data, progress)
538
563
            return refs
539
564
 
 
565
    def get_refs(self, path):
 
566
        """Retrieve the current refs from a git smart server."""
 
567
        # stock `git ls-remote` uses upload-pack
 
568
        proto, _ = self._connect(b'upload-pack', path)
 
569
        with proto:
 
570
            refs, _ = read_pkt_refs(proto)
 
571
            return refs
 
572
 
540
573
    def archive(self, path, committish, write_data, progress=None,
541
574
                write_error=None):
542
575
        proto, can_read = self._connect(b'upload-archive', path)
543
576
        with proto:
544
 
            proto.write_pkt_line("argument %s" % committish)
 
577
            proto.write_pkt_line(b"argument " + committish)
545
578
            proto.write_pkt_line(None)
546
579
            pkt = proto.read_pkt_line()
547
 
            if pkt == "NACK\n":
 
580
            if pkt == b"NACK\n":
548
581
                return
549
 
            elif pkt == "ACK\n":
 
582
            elif pkt == b"ACK\n":
550
583
                pass
551
 
            elif pkt.startswith("ERR "):
552
 
                raise GitProtocolError(pkt[4:].rstrip("\n"))
 
584
            elif pkt.startswith(b"ERR "):
 
585
                raise GitProtocolError(pkt[4:].rstrip(b"\n"))
553
586
            else:
554
587
                raise AssertionError("invalid response %r" % pkt)
555
588
            ret = proto.read_pkt_line()
556
589
            if ret is not None:
557
590
                raise AssertionError("expected pkt tail")
558
591
            self._read_side_band64k_data(proto, {
559
 
                1: write_data, 2: progress, 3: write_error})
 
592
                SIDE_BAND_CHANNEL_DATA: write_data,
 
593
                SIDE_BAND_CHANNEL_PROGRESS: progress,
 
594
                SIDE_BAND_CHANNEL_FATAL: write_error})
560
595
 
561
596
 
562
597
class TCPGitClient(TraditionalGitClient):
570
605
        TraditionalGitClient.__init__(self, *args, **kwargs)
571
606
 
572
607
    def _connect(self, cmd, path):
 
608
        if type(cmd) is not bytes:
 
609
            raise TypeError(path)
 
610
        if type(path) is not bytes:
 
611
            raise TypeError(path)
573
612
        sockaddrs = socket.getaddrinfo(
574
613
            self._host, self._port, socket.AF_UNSPEC, socket.SOCK_STREAM)
575
614
        s = None
597
636
 
598
637
        proto = Protocol(rfile.read, wfile.write, close,
599
638
                         report_activity=self._report_activity)
600
 
        if path.startswith("/~"):
 
639
        if path.startswith(b"/~"):
601
640
            path = path[1:]
602
 
        proto.send_cmd('git-%s' % cmd, path, 'host=%s' % self._host)
 
641
        # TODO(jelmer): Alternative to ascii?
 
642
        proto.send_cmd(b'git-' + cmd, path, b'host=' + self._host.encode('ascii'))
603
643
        return proto, lambda: _fileno_can_read(s)
604
644
 
605
645
 
608
648
 
609
649
    def __init__(self, proc):
610
650
        self.proc = proc
611
 
        self.read = proc.stdout.read
 
651
        if sys.version_info[0] == 2:
 
652
            self.read = proc.stdout.read
 
653
        else:
 
654
            self.read = BufferedReader(proc.stdout).read
612
655
        self.write = proc.stdin.write
613
656
 
614
657
    def can_read(self):
629
672
        self.proc.wait()
630
673
 
631
674
 
 
675
def find_git_command():
 
676
    """Find command to run for system Git (usually C Git).
 
677
    """
 
678
    if sys.platform == 'win32': # support .exe, .bat and .cmd
 
679
        try: # to avoid overhead
 
680
            import win32api
 
681
        except ImportError: # run through cmd.exe with some overhead
 
682
            return ['cmd', '/c', 'git']
 
683
        else:
 
684
            status, git = win32api.FindExecutable('git')
 
685
            return [git]
 
686
    else:
 
687
        return ['git']
 
688
 
 
689
 
632
690
class SubprocessGitClient(TraditionalGitClient):
633
691
    """Git client that talks to a server using a subprocess."""
634
692
 
640
698
            del kwargs['stderr']
641
699
        TraditionalGitClient.__init__(self, *args, **kwargs)
642
700
 
 
701
    git_command = None
 
702
 
643
703
    def _connect(self, service, path):
 
704
        if type(service) is not bytes:
 
705
            raise TypeError(path)
 
706
        if type(path) is not bytes:
 
707
            raise TypeError(path)
644
708
        import subprocess
645
 
        argv = ['git', service, path]
 
709
        if self.git_command is None:
 
710
            git_command = find_git_command()
 
711
        argv = git_command + [service.decode('ascii'), path]
646
712
        p = SubprocessWrapper(
647
713
            subprocess.Popen(argv, bufsize=0, stdin=subprocess.PIPE,
648
714
                             stdout=subprocess.PIPE,
657
723
    def __init__(self, thin_packs=True, report_activity=None):
658
724
        """Create a new LocalGitClient instance.
659
725
 
660
 
        :param path: Path to the local repository
661
726
        :param thin_packs: Whether or not thin packs should be retrieved
662
727
        :param report_activity: Optional callback for reporting transport
663
728
            activity.
669
734
                  progress=None, write_pack=write_pack_objects):
670
735
        """Upload a pack to a remote repository.
671
736
 
672
 
        :param path: Repository path
 
737
        :param path: Repository path (as bytestring)
673
738
        :param generate_pack_contents: Function that can return a sequence of
674
739
            the shas of the objects to upload.
675
740
        :param progress: Optional progress function
682
747
        """
683
748
        from dulwich.repo import Repo
684
749
 
685
 
        target = Repo(path)
686
 
        old_refs = target.get_refs()
687
 
        new_refs = determine_wants(old_refs)
688
 
 
689
 
        have = [sha1 for sha1 in old_refs.values() if sha1 != ZERO_SHA]
690
 
        want = []
691
 
        for refname in set(new_refs.keys() + old_refs.keys()):
692
 
            old_sha1 = old_refs.get(refname, ZERO_SHA)
693
 
            new_sha1 = new_refs.get(refname, ZERO_SHA)
694
 
            if new_sha1 not in have and new_sha1 != ZERO_SHA:
695
 
                want.append(new_sha1)
696
 
 
697
 
        if not want and old_refs == new_refs:
698
 
            return new_refs
699
 
 
700
 
        target.object_store.add_objects(generate_pack_contents(have, want))
701
 
 
702
 
        for name, sha in new_refs.iteritems():
703
 
            target.refs[name] = sha
 
750
        with closing(Repo(path)) as target:
 
751
            old_refs = target.get_refs()
 
752
            new_refs = determine_wants(dict(old_refs))
 
753
 
 
754
            have = [sha1 for sha1 in old_refs.values() if sha1 != ZERO_SHA]
 
755
            want = []
 
756
            all_refs = set(new_refs.keys()).union(set(old_refs.keys()))
 
757
            for refname in all_refs:
 
758
                old_sha1 = old_refs.get(refname, ZERO_SHA)
 
759
                new_sha1 = new_refs.get(refname, ZERO_SHA)
 
760
                if new_sha1 not in have and new_sha1 != ZERO_SHA:
 
761
                    want.append(new_sha1)
 
762
 
 
763
            if not want and old_refs == new_refs:
 
764
                return new_refs
 
765
 
 
766
            target.object_store.add_objects(generate_pack_contents(have, want))
 
767
 
 
768
            for name, sha in new_refs.items():
 
769
                target.refs[name] = sha
704
770
 
705
771
        return new_refs
706
772
 
707
773
    def fetch(self, path, target, determine_wants=None, progress=None):
708
774
        """Fetch into a target repository.
709
775
 
710
 
        :param path: Path to fetch from
 
776
        :param path: Path to fetch from (as bytestring)
711
777
        :param target: Target repository to fetch into
712
778
        :param determine_wants: Optional function to determine what refs
713
779
            to fetch
715
781
        :return: remote refs as dictionary
716
782
        """
717
783
        from dulwich.repo import Repo
718
 
        r = Repo(path)
719
 
        return r.fetch(target, determine_wants=determine_wants,
720
 
                       progress=progress)
 
784
        with closing(Repo(path)) as r:
 
785
            return r.fetch(target, determine_wants=determine_wants,
 
786
                           progress=progress)
721
787
 
722
788
    def fetch_pack(self, path, determine_wants, graph_walker, pack_data,
723
789
                   progress=None):
729
795
        :param progress: Callback for progress reports (strings)
730
796
        """
731
797
        from dulwich.repo import Repo
732
 
        r = Repo(path)
733
 
        objects_iter = r.fetch_objects(determine_wants, graph_walker, progress)
734
 
 
735
 
        # Did the process short-circuit (e.g. in a stateless RPC call)? Note
736
 
        # that the client still expects a 0-object pack in most cases.
737
 
        if objects_iter is None:
738
 
            return
739
 
        write_pack_objects(ProtocolFile(None, pack_data), objects_iter)
 
798
        with closing(Repo(path)) as r:
 
799
            objects_iter = r.fetch_objects(determine_wants, graph_walker, progress)
 
800
 
 
801
            # Did the process short-circuit (e.g. in a stateless RPC call)? Note
 
802
            # that the client still expects a 0-object pack in most cases.
 
803
            if objects_iter is None:
 
804
                return
 
805
            write_pack_objects(ProtocolFile(None, pack_data), objects_iter)
 
806
 
 
807
    def get_refs(self, path):
 
808
        """Retrieve the current refs from a git smart server."""
 
809
        from dulwich.repo import Repo
 
810
 
 
811
        with closing(Repo(path)) as target:
 
812
            return target.get_refs()
740
813
 
741
814
 
742
815
# What Git client to use for local access
743
 
default_local_git_client_cls = SubprocessGitClient
 
816
default_local_git_client_cls = LocalGitClient
744
817
 
745
818
 
746
819
class SSHVendor(object):
760
833
        with the remote command.
761
834
 
762
835
        :param host: Host name
763
 
        :param command: Command to run
 
836
        :param command: Command to run (as argv array)
764
837
        :param username: Optional ame of user to log in as
765
838
        :param port: Optional SSH port to use
766
839
        """
771
844
    """SSH vendor that shells out to the local 'ssh' command."""
772
845
 
773
846
    def run_command(self, host, command, username=None, port=None):
 
847
        if (type(command) is not list or
 
848
            not all([isinstance(b, bytes) for b in command])):
 
849
            raise TypeError(command)
 
850
 
774
851
        import subprocess
775
852
        #FIXME: This has no way to deal with passwords..
776
853
        args = ['ssh', '-x']
873
950
 
874
951
        def run_command(self, host, command, username=None, port=None,
875
952
                        progress_stderr=None):
876
 
 
 
953
            if (type(command) is not list or
 
954
                not all([isinstance(b, bytes) for b in command])):
 
955
                raise TypeError(command)
877
956
            # Paramiko needs an explicit port. None is not valid
878
957
            if port is None:
879
958
                port = 22
889
968
            channel = client.get_transport().open_session()
890
969
 
891
970
            # Run commands
892
 
            channel.exec_command(*command)
 
971
            channel.exec_command(subprocess.list2cmdline(command))
893
972
 
894
973
            return ParamikoWrapper(
895
974
                client, channel, progress_stderr=progress_stderr)
909
988
        self.alternative_paths = {}
910
989
 
911
990
    def _get_cmd_path(self, cmd):
912
 
        return self.alternative_paths.get(cmd, 'git-%s' % cmd)
 
991
        cmd = self.alternative_paths.get(cmd, b'git-' + cmd)
 
992
        assert isinstance(cmd, bytes)
 
993
        if sys.version_info[:2] <= (2, 6):
 
994
            return shlex.split(cmd)
 
995
        else:
 
996
            # TODO(jelmer): Don't decode/encode here
 
997
            return [x.encode('ascii') for x in shlex.split(cmd.decode('ascii'))]
913
998
 
914
999
    def _connect(self, cmd, path):
915
 
        if path.startswith("/~"):
 
1000
        if type(cmd) is not bytes:
 
1001
            raise TypeError(path)
 
1002
        if type(path) is not bytes:
 
1003
            raise TypeError(path)
 
1004
        if path.startswith(b"/~"):
916
1005
            path = path[1:]
 
1006
        argv = self._get_cmd_path(cmd) + [path]
917
1007
        con = get_ssh_vendor().run_command(
918
 
            self.host, ["%s '%s'" % (self._get_cmd_path(cmd), path)],
919
 
            port=self.port, username=self.username)
920
 
        return (Protocol(con.read, con.write, con.close, 
921
 
                         report_activity=self._report_activity), 
 
1008
            self.host, argv, port=self.port, username=self.username)
 
1009
        return (Protocol(con.read, con.write, con.close,
 
1010
                         report_activity=self._report_activity),
922
1011
                con.can_read)
923
1012
 
924
1013
 
957
1046
            self.opener = opener
958
1047
        GitClient.__init__(self, *args, **kwargs)
959
1048
 
 
1049
    def __repr__(self):
 
1050
        return "%s(%r, dumb=%r)" % (type(self).__name__, self.base_url, self.dumb)
 
1051
 
960
1052
    def _get_url(self, path):
961
1053
        return urlparse.urljoin(self.base_url, path).rstrip("/") + "/"
962
1054
 
1008
1100
                  progress=None, write_pack=write_pack_objects):
1009
1101
        """Upload a pack to a remote repository.
1010
1102
 
1011
 
        :param path: Repository path
 
1103
        :param path: Repository path (as bytestring)
1012
1104
        :param generate_pack_contents: Function that can return a sequence of
1013
1105
            the shas of the objects to upload.
1014
1106
        :param progress: Optional progress function
1021
1113
        """
1022
1114
        url = self._get_url(path)
1023
1115
        old_refs, server_capabilities = self._discover_references(
1024
 
            "git-receive-pack", url)
 
1116
            b"git-receive-pack", url)
1025
1117
        negotiated_capabilities = self._send_capabilities & server_capabilities
1026
1118
 
1027
 
        if 'report-status' in negotiated_capabilities:
 
1119
        if CAPABILITY_REPORT_STATUS in negotiated_capabilities:
1028
1120
            self._report_status_parser = ReportStatusParser()
1029
1121
 
1030
1122
        new_refs = determine_wants(dict(old_refs))
1041
1133
        objects = generate_pack_contents(have, want)
1042
1134
        if len(objects) > 0:
1043
1135
            write_pack(req_proto.write_file(), objects)
1044
 
        resp = self._smart_request("git-receive-pack", url,
 
1136
        resp = self._smart_request(b"git-receive-pack", url,
1045
1137
                                   data=req_data.getvalue())
1046
1138
        try:
1047
1139
            resp_proto = Protocol(resp.read, None)
1064
1156
        """
1065
1157
        url = self._get_url(path)
1066
1158
        refs, server_capabilities = self._discover_references(
1067
 
            "git-upload-pack", url)
 
1159
            b"git-upload-pack", url)
1068
1160
        negotiated_capabilities = self._fetch_capabilities & server_capabilities
1069
1161
        wants = determine_wants(refs)
1070
1162
        if wants is not None:
1079
1171
            req_proto, negotiated_capabilities, graph_walker, wants,
1080
1172
            lambda: False)
1081
1173
        resp = self._smart_request(
1082
 
            "git-upload-pack", url, data=req_data.getvalue())
 
1174
            b"git-upload-pack", url, data=req_data.getvalue())
1083
1175
        try:
1084
1176
            resp_proto = Protocol(resp.read, None)
1085
1177
            self._handle_upload_pack_tail(resp_proto, negotiated_capabilities,
1088
1180
        finally:
1089
1181
            resp.close()
1090
1182
 
 
1183
    def get_refs(self, path):
 
1184
        """Retrieve the current refs from a git smart server."""
 
1185
        url = self._get_url(path)
 
1186
        refs, _ = self._discover_references(
 
1187
            b"git-upload-pack", url)
 
1188
        return refs
 
1189
 
1091
1190
 
1092
1191
def get_transport_and_path_from_url(url, config=None, **kwargs):
1093
1192
    """Obtain a git client from a URL.
1094
1193
 
1095
 
    :param url: URL to open
 
1194
    :param url: URL to open (a unicode string)
1096
1195
    :param config: Optional config object
1097
1196
    :param thin_packs: Whether or not thin packs should be retrieved
1098
1197
    :param report_activity: Optional callback for reporting transport
1121
1220
def get_transport_and_path(location, **kwargs):
1122
1221
    """Obtain a git client from a URL.
1123
1222
 
1124
 
    :param location: URL or path
 
1223
    :param location: URL or path (a string)
1125
1224
    :param config: Optional config object
1126
1225
    :param thin_packs: Whether or not thin packs should be retrieved
1127
1226
    :param report_activity: Optional callback for reporting transport
1135
1234
        pass
1136
1235
 
1137
1236
    if (sys.platform == 'win32' and
1138
 
            location[0].isalpha() and location[1:2] == ':\\'):
 
1237
            location[0].isalpha() and location[1:3] == ':\\'):
1139
1238
        # Windows local path
1140
1239
        return default_local_git_client_cls(**kwargs), location
1141
1240