Statistics
| Revision:

gvsig-scripting / org.gvsig.scripting / trunk / org.gvsig.scripting / org.gvsig.scripting.app / org.gvsig.scripting.app.mainplugin / src / main / resources-plugin / scripting / lib / dulwich / client.py @ 959

History | View | Annotate | Download (48.5 KB)

1
# client.py -- Implementation of the client side git protocols
2
# Copyright (C) 2008-2013 Jelmer Vernooij <jelmer@samba.org>
3
#
4
# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
5
# General Public License as public by the Free Software Foundation; version 2.0
6
# or (at your option) any later version. You can redistribute it and/or
7
# modify it under the terms of either of these two licenses.
8
#
9
# Unless required by applicable law or agreed to in writing, software
10
# distributed under the License is distributed on an "AS IS" BASIS,
11
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
# See the License for the specific language governing permissions and
13
# limitations under the License.
14
#
15
# You should have received a copy of the licenses; if not, see
16
# <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
17
# and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
18
# License, Version 2.0.
19
#
20

    
21
"""Client side support for the Git protocol.
22

23
The Dulwich client supports the following capabilities:
24

25
 * thin-pack
26
 * multi_ack_detailed
27
 * multi_ack
28
 * side-band-64k
29
 * ofs-delta
30
 * quiet
31
 * report-status
32
 * delete-refs
33

34
Known capabilities that are not supported:
35

36
 * shallow
37
 * no-progress
38
 * include-tag
39
"""
40

    
41
__docformat__ = 'restructuredText'
42

    
43
from contextlib import closing
44
from io import BytesIO, BufferedReader
45
import dulwich
46
import select
47
import socket
48
import subprocess
49
import sys
50

    
51
try:
52
    from urllib import quote as urlquote
53
    from urllib import unquote as urlunquote
54
except ImportError:
55
    from urllib.parse import quote as urlquote
56
    from urllib.parse import unquote as urlunquote
57

    
58
try:
59
    import urllib2
60
    import urlparse
61
except ImportError:
62
    import urllib.request as urllib2
63
    import urllib.parse as urlparse
64

    
65
from dulwich.errors import (
66
    GitProtocolError,
67
    NotGitRepository,
68
    SendPackError,
69
    UpdateRefsError,
70
    )
71
from dulwich.protocol import (
72
    _RBUFSIZE,
73
    capability_agent,
74
    CAPABILITY_DELETE_REFS,
75
    CAPABILITY_MULTI_ACK,
76
    CAPABILITY_MULTI_ACK_DETAILED,
77
    CAPABILITY_OFS_DELTA,
78
    CAPABILITY_QUIET,
79
    CAPABILITY_REPORT_STATUS,
80
    CAPABILITY_SIDE_BAND_64K,
81
    CAPABILITY_THIN_PACK,
82
    CAPABILITIES_REF,
83
    COMMAND_DONE,
84
    COMMAND_HAVE,
85
    COMMAND_WANT,
86
    SIDE_BAND_CHANNEL_DATA,
87
    SIDE_BAND_CHANNEL_PROGRESS,
88
    SIDE_BAND_CHANNEL_FATAL,
89
    PktLineParser,
90
    Protocol,
91
    ProtocolFile,
92
    TCP_GIT_PORT,
93
    ZERO_SHA,
94
    extract_capabilities,
95
    )
96
from dulwich.pack import (
97
    write_pack_objects,
98
    )
99
from dulwich.refs import (
100
    read_info_refs,
101
    )
102

    
103

    
104
def _fileno_can_read(fileno):
105
    """Check if a file descriptor is readable."""
106
    return len(select.select([fileno], [], [], 0)[0]) > 0
107

    
108

    
109
COMMON_CAPABILITIES = [CAPABILITY_OFS_DELTA, CAPABILITY_SIDE_BAND_64K]
110
FETCH_CAPABILITIES = ([CAPABILITY_THIN_PACK, CAPABILITY_MULTI_ACK,
111
                       CAPABILITY_MULTI_ACK_DETAILED] +
112
                      COMMON_CAPABILITIES)
113
SEND_CAPABILITIES = [CAPABILITY_REPORT_STATUS] + COMMON_CAPABILITIES
114

    
115

    
116
class ReportStatusParser(object):
117
    """Handle status as reported by servers with 'report-status' capability.
118
    """
119

    
120
    def __init__(self):
121
        self._done = False
122
        self._pack_status = None
123
        self._ref_status_ok = True
124
        self._ref_statuses = []
125

    
126
    def check(self):
127
        """Check if there were any errors and, if so, raise exceptions.
128

129
        :raise SendPackError: Raised when the server could not unpack
130
        :raise UpdateRefsError: Raised when refs could not be updated
131
        """
132
        if self._pack_status not in (b'unpack ok', None):
133
            raise SendPackError(self._pack_status)
134
        if not self._ref_status_ok:
135
            ref_status = {}
136
            ok = set()
137
            for status in self._ref_statuses:
138
                if b' ' not in status:
139
                    # malformed response, move on to the next one
140
                    continue
141
                status, ref = status.split(b' ', 1)
142

    
143
                if status == b'ng':
144
                    if b' ' in ref:
145
                        ref, status = ref.split(b' ', 1)
146
                else:
147
                    ok.add(ref)
148
                ref_status[ref] = status
149
            # TODO(jelmer): don't assume encoding of refs is ascii.
150
            raise UpdateRefsError(', '.join([
151
                ref.decode('ascii') for ref in ref_status if ref not in ok]) +
152
                ' failed to update', ref_status=ref_status)
153

    
154
    def handle_packet(self, pkt):
155
        """Handle a packet.
156

157
        :raise GitProtocolError: Raised when packets are received after a
158
            flush packet.
159
        """
160
        if self._done:
161
            raise GitProtocolError("received more data after status report")
162
        if pkt is None:
163
            self._done = True
164
            return
165
        if self._pack_status is None:
166
            self._pack_status = pkt.strip()
167
        else:
168
            ref_status = pkt.strip()
169
            self._ref_statuses.append(ref_status)
170
            if not ref_status.startswith(b'ok '):
171
                self._ref_status_ok = False
172

    
173

    
174
def read_pkt_refs(proto):
175
    server_capabilities = None
176
    refs = {}
177
    # Receive refs from server
178
    for pkt in proto.read_pkt_seq():
179
        (sha, ref) = pkt.rstrip(b'\n').split(None, 1)
180
        if sha == b'ERR':
181
            raise GitProtocolError(ref)
182
        if server_capabilities is None:
183
            (ref, server_capabilities) = extract_capabilities(ref)
184
        refs[ref] = sha
185

    
186
    if len(refs) == 0:
187
        return None, set([])
188
    if refs == {CAPABILITIES_REF: ZERO_SHA}:
189
        refs = {}
190
    return refs, set(server_capabilities)
191

    
192

    
193
# TODO(durin42): this doesn't correctly degrade if the server doesn't
194
# support some capabilities. This should work properly with servers
195
# that don't support multi_ack.
196
class GitClient(object):
197
    """Git smart server client.
198

199
    """
200

    
201
    def __init__(self, thin_packs=True, report_activity=None, quiet=False):
202
        """Create a new GitClient instance.
203

204
        :param thin_packs: Whether or not thin packs should be retrieved
205
        :param report_activity: Optional callback for reporting transport
206
            activity.
207
        """
208
        self._report_activity = report_activity
209
        self._report_status_parser = None
210
        self._fetch_capabilities = set(FETCH_CAPABILITIES)
211
        self._fetch_capabilities.add(capability_agent())
212
        self._send_capabilities = set(SEND_CAPABILITIES)
213
        self._send_capabilities.add(capability_agent())
214
        if quiet:
215
            self._send_capabilities.add(CAPABILITY_QUIET)
216
        if not thin_packs:
217
            self._fetch_capabilities.remove(CAPABILITY_THIN_PACK)
218

    
219
    def get_url(self, path):
220
        """Retrieves full url to given path.
221

222
        :param path: Repository path (as string)
223
        :return: Url to path (as string)
224
        """
225
        raise NotImplementedError(self.get_url)
226

    
227
    @classmethod
228
    def from_parsedurl(cls, parsedurl, **kwargs):
229
        """Create an instance of this client from a urlparse.parsed object.
230

231
        :param parsedurl: Result of urlparse.urlparse()
232
        :return: A `GitClient` object
233
        """
234
        raise NotImplementedError(cls.from_parsedurl)
235

    
236
    def send_pack(self, path, determine_wants, generate_pack_contents,
237
                  progress=None, write_pack=write_pack_objects):
238
        """Upload a pack to a remote repository.
239

240
        :param path: Repository path (as bytestring)
241
        :param generate_pack_contents: Function that can return a sequence of
242
            the shas of the objects to upload.
243
        :param progress: Optional progress function
244
        :param write_pack: Function called with (file, iterable of objects) to
245
            write the objects returned by generate_pack_contents to the server.
246

247
        :raises SendPackError: if server rejects the pack data
248
        :raises UpdateRefsError: if the server supports report-status
249
                                 and rejects ref updates
250
        :return: new_refs dictionary containing the changes that were made
251
            {refname: new_ref}, including deleted refs.
252
        """
253
        raise NotImplementedError(self.send_pack)
254

    
255
    def fetch(self, path, target, determine_wants=None, progress=None):
256
        """Fetch into a target repository.
257

258
        :param path: Path to fetch from (as bytestring)
259
        :param target: Target repository to fetch into
260
        :param determine_wants: Optional function to determine what refs
261
            to fetch
262
        :param progress: Optional progress function
263
        :return: Dictionary with all remote refs (not just those fetched)
264
        """
265
        if determine_wants is None:
266
            determine_wants = target.object_store.determine_wants_all
267
        if CAPABILITY_THIN_PACK in self._fetch_capabilities:
268
            # TODO(jelmer): Avoid reading entire file into memory and
269
            # only processing it after the whole file has been fetched.
270
            f = BytesIO()
271
            def commit():
272
                if f.tell():
273
                    f.seek(0)
274
                    target.object_store.add_thin_pack(f.read, None)
275
            def abort():
276
                pass
277
        else:
278
            f, commit, abort = target.object_store.add_pack()
279
        try:
280
            result = self.fetch_pack(
281
                path, determine_wants, target.get_graph_walker(), f.write,
282
                progress)
283
        except:
284
            abort()
285
            raise
286
        else:
287
            commit()
288
        return result
289

    
290
    def fetch_pack(self, path, determine_wants, graph_walker, pack_data,
291
                   progress=None):
292
        """Retrieve a pack from a git smart server.
293

294
        :param determine_wants: Callback that returns list of commits to fetch
295
        :param graph_walker: Object with next() and ack().
296
        :param pack_data: Callback called for each bit of data in the pack
297
        :param progress: Callback for progress reports (strings)
298
        :return: Dictionary with all remote refs (not just those fetched)
299
        """
300
        raise NotImplementedError(self.fetch_pack)
301

    
302
    def get_refs(self, path):
303
        """Retrieve the current refs from a git smart server.
304

305
        :param path: Path to the repo to fetch from. (as bytestring)
306
        """
307
        raise NotImplementedError(self.get_refs)
308

    
309
    def _parse_status_report(self, proto):
310
        unpack = proto.read_pkt_line().strip()
311
        if unpack != b'unpack ok':
312
            st = True
313
            # flush remaining error data
314
            while st is not None:
315
                st = proto.read_pkt_line()
316
            raise SendPackError(unpack)
317
        statuses = []
318
        errs = False
319
        ref_status = proto.read_pkt_line()
320
        while ref_status:
321
            ref_status = ref_status.strip()
322
            statuses.append(ref_status)
323
            if not ref_status.startswith(b'ok '):
324
                errs = True
325
            ref_status = proto.read_pkt_line()
326

    
327
        if errs:
328
            ref_status = {}
329
            ok = set()
330
            for status in statuses:
331
                if b' ' not in status:
332
                    # malformed response, move on to the next one
333
                    continue
334
                status, ref = status.split(b' ', 1)
335

    
336
                if status == b'ng':
337
                    if b' ' in ref:
338
                        ref, status = ref.split(b' ', 1)
339
                else:
340
                    ok.add(ref)
341
                ref_status[ref] = status
342
            raise UpdateRefsError(', '.join([ref for ref in ref_status
343
                                             if ref not in ok]) +
344
                                             b' failed to update',
345
                                  ref_status=ref_status)
346

    
347
    def _read_side_band64k_data(self, proto, channel_callbacks):
348
        """Read per-channel data.
349

350
        This requires the side-band-64k capability.
351

352
        :param proto: Protocol object to read from
353
        :param channel_callbacks: Dictionary mapping channels to packet
354
            handlers to use. None for a callback discards channel data.
355
        """
356
        for pkt in proto.read_pkt_seq():
357
            channel = ord(pkt[:1])
358
            pkt = pkt[1:]
359
            try:
360
                cb = channel_callbacks[channel]
361
            except KeyError:
362
                raise AssertionError('Invalid sideband channel %d' % channel)
363
            else:
364
                if cb is not None:
365
                    cb(pkt)
366

    
367
    def _handle_receive_pack_head(self, proto, capabilities, old_refs,
368
                                  new_refs):
369
        """Handle the head of a 'git-receive-pack' request.
370

371
        :param proto: Protocol object to read from
372
        :param capabilities: List of negotiated capabilities
373
        :param old_refs: Old refs, as received from the server
374
        :param new_refs: Refs to change
375
        :return: (have, want) tuple
376
        """
377
        want = []
378
        have = [x for x in old_refs.values() if not x == ZERO_SHA]
379
        sent_capabilities = False
380

    
381
        for refname in new_refs:
382
            if not isinstance(refname, bytes):
383
                raise TypeError('refname is not a bytestring: %r' % refname)
384
            old_sha1 = old_refs.get(refname, ZERO_SHA)
385
            if not isinstance(old_sha1, bytes):
386
                raise TypeError('old sha1 for %s is not a bytestring: %r' %
387
                        (refname, old_sha1))
388
            new_sha1 = new_refs.get(refname, ZERO_SHA)
389
            if not isinstance(new_sha1, bytes):
390
                raise TypeError('old sha1 for %s is not a bytestring %r' %
391
                        (refname, new_sha1))
392

    
393
            if old_sha1 != new_sha1:
394
                if sent_capabilities:
395
                    proto.write_pkt_line(old_sha1 + b' ' + new_sha1 + b' ' + refname)
396
                else:
397
                    proto.write_pkt_line(
398
                        old_sha1 + b' ' + new_sha1 + b' ' + refname + b'\0' +
399
                        b' '.join(capabilities))
400
                    sent_capabilities = True
401
            if new_sha1 not in have and new_sha1 != ZERO_SHA:
402
                want.append(new_sha1)
403
        proto.write_pkt_line(None)
404
        return (have, want)
405

    
406
    def _handle_receive_pack_tail(self, proto, capabilities, progress=None):
407
        """Handle the tail of a 'git-receive-pack' request.
408

409
        :param proto: Protocol object to read from
410
        :param capabilities: List of negotiated capabilities
411
        :param progress: Optional progress reporting function
412
        """
413
        if b"side-band-64k" in capabilities:
414
            if progress is None:
415
                progress = lambda x: None
416
            channel_callbacks = {2: progress}
417
            if CAPABILITY_REPORT_STATUS in capabilities:
418
                channel_callbacks[1] = PktLineParser(
419
                    self._report_status_parser.handle_packet).parse
420
            self._read_side_band64k_data(proto, channel_callbacks)
421
        else:
422
            if CAPABILITY_REPORT_STATUS in capabilities:
423
                for pkt in proto.read_pkt_seq():
424
                    self._report_status_parser.handle_packet(pkt)
425
        if self._report_status_parser is not None:
426
            self._report_status_parser.check()
427

    
428
    def _handle_upload_pack_head(self, proto, capabilities, graph_walker,
429
                                 wants, can_read):
430
        """Handle the head of a 'git-upload-pack' request.
431

432
        :param proto: Protocol object to read from
433
        :param capabilities: List of negotiated capabilities
434
        :param graph_walker: GraphWalker instance to call .ack() on
435
        :param wants: List of commits to fetch
436
        :param can_read: function that returns a boolean that indicates
437
            whether there is extra graph data to read on proto
438
        """
439
        assert isinstance(wants, list) and isinstance(wants[0], bytes)
440
        proto.write_pkt_line(COMMAND_WANT + b' ' + wants[0] + b' ' + b' '.join(capabilities) + b'\n')
441
        for want in wants[1:]:
442
            proto.write_pkt_line(COMMAND_WANT + b' ' + want + b'\n')
443
        proto.write_pkt_line(None)
444
        have = next(graph_walker)
445
        while have:
446
            proto.write_pkt_line(COMMAND_HAVE + b' ' + have + b'\n')
447
            if can_read():
448
                pkt = proto.read_pkt_line()
449
                parts = pkt.rstrip(b'\n').split(b' ')
450
                if parts[0] == b'ACK':
451
                    graph_walker.ack(parts[1])
452
                    if parts[2] in (b'continue', b'common'):
453
                        pass
454
                    elif parts[2] == b'ready':
455
                        break
456
                    else:
457
                        raise AssertionError(
458
                            "%s not in ('continue', 'ready', 'common)" %
459
                            parts[2])
460
            have = next(graph_walker)
461
        proto.write_pkt_line(COMMAND_DONE + b'\n')
462

    
463
    def _handle_upload_pack_tail(self, proto, capabilities, graph_walker,
464
                                 pack_data, progress=None, rbufsize=_RBUFSIZE):
465
        """Handle the tail of a 'git-upload-pack' request.
466

467
        :param proto: Protocol object to read from
468
        :param capabilities: List of negotiated capabilities
469
        :param graph_walker: GraphWalker instance to call .ack() on
470
        :param pack_data: Function to call with pack data
471
        :param progress: Optional progress reporting function
472
        :param rbufsize: Read buffer size
473
        """
474
        pkt = proto.read_pkt_line()
475
        while pkt:
476
            parts = pkt.rstrip(b'\n').split(b' ')
477
            if parts[0] == b'ACK':
478
                graph_walker.ack(parts[1])
479
            if len(parts) < 3 or parts[2] not in (
480
                    b'ready', b'continue', b'common'):
481
                break
482
            pkt = proto.read_pkt_line()
483
        if CAPABILITY_SIDE_BAND_64K in capabilities:
484
            if progress is None:
485
                # Just ignore progress data
486
                progress = lambda x: None
487
            self._read_side_band64k_data(proto, {
488
                SIDE_BAND_CHANNEL_DATA: pack_data,
489
                SIDE_BAND_CHANNEL_PROGRESS: progress}
490
            )
491
        else:
492
            while True:
493
                data = proto.read(rbufsize)
494
                if data == b"":
495
                    break
496
                pack_data(data)
497

    
498

    
499
class TraditionalGitClient(GitClient):
500
    """Traditional Git client."""
501

    
502
    DEFAULT_ENCODING = 'utf-8'
503

    
504
    def __init__(self, path_encoding=DEFAULT_ENCODING, **kwargs):
505
        self._remote_path_encoding = path_encoding
506
        super(TraditionalGitClient, self).__init__(**kwargs)
507

    
508
    def _connect(self, cmd, path):
509
        """Create a connection to the server.
510

511
        This method is abstract - concrete implementations should
512
        implement their own variant which connects to the server and
513
        returns an initialized Protocol object with the service ready
514
        for use and a can_read function which may be used to see if
515
        reads would block.
516

517
        :param cmd: The git service name to which we should connect.
518
        :param path: The path we should pass to the service. (as bytestirng)
519
        """
520
        raise NotImplementedError()
521

    
522
    def send_pack(self, path, determine_wants, generate_pack_contents,
523
                  progress=None, write_pack=write_pack_objects):
524
        """Upload a pack to a remote repository.
525

526
        :param path: Repository path (as bytestring)
527
        :param generate_pack_contents: Function that can return a sequence of
528
            the shas of the objects to upload.
529
        :param progress: Optional callback called with progress updates
530
        :param write_pack: Function called with (file, iterable of objects) to
531
            write the objects returned by generate_pack_contents to the server.
532

533
        :raises SendPackError: if server rejects the pack data
534
        :raises UpdateRefsError: if the server supports report-status
535
                                 and rejects ref updates
536
        :return: new_refs dictionary containing the changes that were made
537
            {refname: new_ref}, including deleted refs.
538
        """
539
        proto, unused_can_read = self._connect(b'receive-pack', path)
540
        with proto:
541
            old_refs, server_capabilities = read_pkt_refs(proto)
542
            negotiated_capabilities = self._send_capabilities & server_capabilities
543

    
544
            if CAPABILITY_REPORT_STATUS in negotiated_capabilities:
545
                self._report_status_parser = ReportStatusParser()
546
            report_status_parser = self._report_status_parser
547

    
548
            try:
549
                new_refs = orig_new_refs = determine_wants(dict(old_refs))
550
            except:
551
                proto.write_pkt_line(None)
552
                raise
553

    
554
            if not CAPABILITY_DELETE_REFS in server_capabilities:
555
                # Server does not support deletions. Fail later.
556
                new_refs = dict(orig_new_refs)
557
                for ref, sha in orig_new_refs.items():
558
                    if sha == ZERO_SHA:
559
                        if CAPABILITY_REPORT_STATUS in negotiated_capabilities:
560
                            report_status_parser._ref_statuses.append(
561
                                b'ng ' + sha + b' remote does not support deleting refs')
562
                            report_status_parser._ref_status_ok = False
563
                        del new_refs[ref]
564

    
565
            if new_refs is None:
566
                proto.write_pkt_line(None)
567
                return old_refs
568

    
569
            if len(new_refs) == 0 and len(orig_new_refs):
570
                # NOOP - Original new refs filtered out by policy
571
                proto.write_pkt_line(None)
572
                if report_status_parser is not None:
573
                    report_status_parser.check()
574
                return old_refs
575

    
576
            (have, want) = self._handle_receive_pack_head(
577
                proto, negotiated_capabilities, old_refs, new_refs)
578
            if not want and set(new_refs.items()).issubset(set(old_refs.items())):
579
                return new_refs
580
            objects = generate_pack_contents(have, want)
581

    
582
            dowrite = len(objects) > 0
583
            dowrite = dowrite or any(old_refs.get(ref) != sha
584
                                     for (ref, sha) in new_refs.items()
585
                                     if sha != ZERO_SHA)
586
            if dowrite:
587
                write_pack(proto.write_file(), objects)
588

    
589
            self._handle_receive_pack_tail(
590
                proto, negotiated_capabilities, progress)
591
            return new_refs
592

    
593
    def fetch_pack(self, path, determine_wants, graph_walker, pack_data,
594
                   progress=None):
595
        """Retrieve a pack from a git smart server.
596

597
        :param determine_wants: Callback that returns list of commits to fetch
598
        :param graph_walker: Object with next() and ack().
599
        :param pack_data: Callback called for each bit of data in the pack
600
        :param progress: Callback for progress reports (strings)
601
        :return: Dictionary with all remote refs (not just those fetched)
602
        """
603
        proto, can_read = self._connect(b'upload-pack', path)
604
        with proto:
605
            refs, server_capabilities = read_pkt_refs(proto)
606
            negotiated_capabilities = (
607
                self._fetch_capabilities & server_capabilities)
608

    
609
            if refs is None:
610
                proto.write_pkt_line(None)
611
                return refs
612

    
613
            try:
614
                wants = determine_wants(refs)
615
            except:
616
                proto.write_pkt_line(None)
617
                raise
618
            if wants is not None:
619
                wants = [cid for cid in wants if cid != ZERO_SHA]
620
            if not wants:
621
                proto.write_pkt_line(None)
622
                return refs
623
            self._handle_upload_pack_head(
624
                proto, negotiated_capabilities, graph_walker, wants, can_read)
625
            self._handle_upload_pack_tail(
626
                proto, negotiated_capabilities, graph_walker, pack_data, progress)
627
            return refs
628

    
629
    def get_refs(self, path):
630
        """Retrieve the current refs from a git smart server."""
631
        # stock `git ls-remote` uses upload-pack
632
        proto, _ = self._connect(b'upload-pack', path)
633
        with proto:
634
            refs, _ = read_pkt_refs(proto)
635
            return refs
636

    
637
    def archive(self, path, committish, write_data, progress=None,
638
                write_error=None):
639
        proto, can_read = self._connect(b'upload-archive', path)
640
        with proto:
641
            proto.write_pkt_line(b"argument " + committish)
642
            proto.write_pkt_line(None)
643
            pkt = proto.read_pkt_line()
644
            if pkt == b"NACK\n":
645
                return
646
            elif pkt == b"ACK\n":
647
                pass
648
            elif pkt.startswith(b"ERR "):
649
                raise GitProtocolError(pkt[4:].rstrip(b"\n"))
650
            else:
651
                raise AssertionError("invalid response %r" % pkt)
652
            ret = proto.read_pkt_line()
653
            if ret is not None:
654
                raise AssertionError("expected pkt tail")
655
            self._read_side_band64k_data(proto, {
656
                SIDE_BAND_CHANNEL_DATA: write_data,
657
                SIDE_BAND_CHANNEL_PROGRESS: progress,
658
                SIDE_BAND_CHANNEL_FATAL: write_error})
659

    
660

    
661
class TCPGitClient(TraditionalGitClient):
662
    """A Git Client that works over TCP directly (i.e. git://)."""
663

    
664
    def __init__(self, host, port=None, **kwargs):
665
        if port is None:
666
            port = TCP_GIT_PORT
667
        self._host = host
668
        self._port = port
669
        super(TCPGitClient, self).__init__(**kwargs)
670

    
671
    @classmethod
672
    def from_parsedurl(cls, parsedurl, **kwargs):
673
        return cls(parsedurl.hostname, port=parsedurl.port, **kwargs)
674

    
675
    def get_url(self, path):
676
        netloc = self._host
677
        if self._port is not None and self._port != TCP_GIT_PORT:
678
            netloc += ":%d" % self._port
679
        return urlparse.urlunsplit(("git", netloc, path, '', ''))
680

    
681
    def _connect(self, cmd, path):
682
        if type(cmd) is not bytes:
683
            raise TypeError(cmd)
684
        if type(path) is not bytes:
685
            path = path.encode(self._remote_path_encoding)
686
        sockaddrs = socket.getaddrinfo(
687
            self._host, self._port, socket.AF_UNSPEC, socket.SOCK_STREAM)
688
        s = None
689
        err = socket.error("no address found for %s" % self._host)
690
        for (family, socktype, proto, canonname, sockaddr) in sockaddrs:
691
            s = socket.socket(family, socktype, proto)
692
            s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
693
            try:
694
                s.connect(sockaddr)
695
                break
696
            except socket.error as err:
697
                if s is not None:
698
                    s.close()
699
                s = None
700
        if s is None:
701
            raise err
702
        # -1 means system default buffering
703
        rfile = s.makefile('rb', -1)
704
        # 0 means unbuffered
705
        wfile = s.makefile('wb', 0)
706
        def close():
707
            rfile.close()
708
            wfile.close()
709
            s.close()
710

    
711
        proto = Protocol(rfile.read, wfile.write, close,
712
                         report_activity=self._report_activity)
713
        if path.startswith(b"/~"):
714
            path = path[1:]
715
        # TODO(jelmer): Alternative to ascii?
716
        proto.send_cmd(b'git-' + cmd, path, b'host=' + self._host.encode('ascii'))
717
        return proto, lambda: _fileno_can_read(s)
718

    
719

    
720
class SubprocessWrapper(object):
721
    """A socket-like object that talks to a subprocess via pipes."""
722

    
723
    def __init__(self, proc):
724
        self.proc = proc
725
        if sys.version_info[0] == 2:
726
            self.read = proc.stdout.read
727
        else:
728
            self.read = BufferedReader(proc.stdout).read
729
        self.write = proc.stdin.write
730

    
731
    def can_read(self):
732
        if sys.platform == 'win32':
733
            from msvcrt import get_osfhandle
734
            from win32pipe import PeekNamedPipe
735
            handle = get_osfhandle(self.proc.stdout.fileno())
736
            data, total_bytes_avail, msg_bytes_left = PeekNamedPipe(handle, 0)
737
            return total_bytes_avail != 0
738
        else:
739
            return _fileno_can_read(self.proc.stdout.fileno())
740

    
741
    def close(self):
742
        self.proc.stdin.close()
743
        self.proc.stdout.close()
744
        if self.proc.stderr:
745
            self.proc.stderr.close()
746
        self.proc.wait()
747

    
748

    
749
def find_git_command():
750
    """Find command to run for system Git (usually C Git).
751
    """
752
    if sys.platform == 'win32': # support .exe, .bat and .cmd
753
        try: # to avoid overhead
754
            import win32api
755
        except ImportError: # run through cmd.exe with some overhead
756
            return ['cmd', '/c', 'git']
757
        else:
758
            status, git = win32api.FindExecutable('git')
759
            return [git]
760
    else:
761
        return ['git']
762

    
763

    
764
class SubprocessGitClient(TraditionalGitClient):
765
    """Git client that talks to a server using a subprocess."""
766

    
767
    def __init__(self, **kwargs):
768
        self._connection = None
769
        self._stderr = None
770
        self._stderr = kwargs.get('stderr')
771
        if 'stderr' in kwargs:
772
            del kwargs['stderr']
773
        super(SubprocessGitClient, self).__init__(**kwargs)
774

    
775
    @classmethod
776
    def from_parsedurl(cls, parsedurl, **kwargs):
777
        return cls(**kwargs)
778

    
779
    git_command = None
780

    
781
    def _connect(self, service, path):
782
        if type(service) is not bytes:
783
            raise TypeError(service)
784
        if type(path) is not bytes:
785
            path = path.encode(self._remote_path_encoding)
786
        if self.git_command is None:
787
            git_command = find_git_command()
788
        argv = git_command + [service.decode('ascii'), path]
789
        p = SubprocessWrapper(
790
            subprocess.Popen(argv, bufsize=0, stdin=subprocess.PIPE,
791
                             stdout=subprocess.PIPE,
792
                             stderr=self._stderr))
793
        return Protocol(p.read, p.write, p.close,
794
                        report_activity=self._report_activity), p.can_read
795

    
796

    
797
class LocalGitClient(GitClient):
798
    """Git Client that just uses a local Repo."""
799

    
800
    def __init__(self, thin_packs=True, report_activity=None):
801
        """Create a new LocalGitClient instance.
802

803
        :param thin_packs: Whether or not thin packs should be retrieved
804
        :param report_activity: Optional callback for reporting transport
805
            activity.
806
        """
807
        self._report_activity = report_activity
808
        # Ignore the thin_packs argument
809

    
810
    def get_url(self, path):
811
        return urlparse.urlunsplit(('file', '', path, '', ''))
812

    
813
    @classmethod
814
    def from_parsedurl(cls, parsedurl, **kwargs):
815
        return cls(**kwargs)
816

    
817
    @classmethod
818
    def _open_repo(cls, path):
819
        from dulwich.repo import Repo
820
        if not isinstance(path, str):
821
            path = path.decode(sys.getfilesystemencoding())
822
        return closing(Repo(path))
823

    
824
    def send_pack(self, path, determine_wants, generate_pack_contents,
825
                  progress=None, write_pack=write_pack_objects):
826
        """Upload a pack to a remote repository.
827

828
        :param path: Repository path (as bytestring)
829
        :param generate_pack_contents: Function that can return a sequence of
830
            the shas of the objects to upload.
831
        :param progress: Optional progress function
832
        :param write_pack: Function called with (file, iterable of objects) to
833
            write the objects returned by generate_pack_contents to the server.
834

835
        :raises SendPackError: if server rejects the pack data
836
        :raises UpdateRefsError: if the server supports report-status
837
                                 and rejects ref updates
838
        :return: new_refs dictionary containing the changes that were made
839
            {refname: new_ref}, including deleted refs.
840
        """
841
        if not progress:
842
            progress = lambda x: None
843

    
844
        with self._open_repo(path)  as target:
845
            old_refs = target.get_refs()
846
            new_refs = determine_wants(dict(old_refs))
847

    
848
            have = [sha1 for sha1 in old_refs.values() if sha1 != ZERO_SHA]
849
            want = []
850
            for refname, new_sha1 in new_refs.items():
851
                if new_sha1 not in have and not new_sha1 in want and new_sha1 != ZERO_SHA:
852
                    want.append(new_sha1)
853

    
854
            if not want and set(new_refs.items()).issubset(set(old_refs.items())):
855
                return new_refs
856

    
857
            target.object_store.add_objects(generate_pack_contents(have, want))
858

    
859
            for refname, new_sha1 in new_refs.items():
860
                old_sha1 = old_refs.get(refname, ZERO_SHA)
861
                if new_sha1 != ZERO_SHA:
862
                    if not target.refs.set_if_equals(refname, old_sha1, new_sha1):
863
                        progress('unable to set %s to %s' % (refname, new_sha1))
864
                else:
865
                    if not target.refs.remove_if_equals(refname, old_sha1):
866
                        progress('unable to remove %s' % refname)
867

    
868
        return new_refs
869

    
870
    def fetch(self, path, target, determine_wants=None, progress=None):
871
        """Fetch into a target repository.
872

873
        :param path: Path to fetch from (as bytestring)
874
        :param target: Target repository to fetch into
875
        :param determine_wants: Optional function to determine what refs
876
            to fetch
877
        :param progress: Optional progress function
878
        :return: Dictionary with all remote refs (not just those fetched)
879
        """
880
        with self._open_repo(path) as r:
881
            return r.fetch(target, determine_wants=determine_wants,
882
                           progress=progress)
883

    
884
    def fetch_pack(self, path, determine_wants, graph_walker, pack_data,
885
                   progress=None):
886
        """Retrieve a pack from a git smart server.
887

888
        :param determine_wants: Callback that returns list of commits to fetch
889
        :param graph_walker: Object with next() and ack().
890
        :param pack_data: Callback called for each bit of data in the pack
891
        :param progress: Callback for progress reports (strings)
892
        :return: Dictionary with all remote refs (not just those fetched)
893
        """
894
        with self._open_repo(path) as r:
895
            objects_iter = r.fetch_objects(determine_wants, graph_walker, progress)
896

    
897
            # Did the process short-circuit (e.g. in a stateless RPC call)? Note
898
            # that the client still expects a 0-object pack in most cases.
899
            if objects_iter is None:
900
                return
901
            write_pack_objects(ProtocolFile(None, pack_data), objects_iter)
902
            return r.get_refs()
903

    
904
    def get_refs(self, path):
905
        """Retrieve the current refs from a git smart server."""
906

    
907
        with self._open_repo(path) as target:
908
            return target.get_refs()
909

    
910

    
911
# What Git client to use for local access
912
default_local_git_client_cls = LocalGitClient
913

    
914

    
915
class SSHVendor(object):
916
    """A client side SSH implementation."""
917

    
918
    def connect_ssh(self, host, command, username=None, port=None):
919
        # This function was deprecated in 0.9.1
920
        import warnings
921
        warnings.warn(
922
            "SSHVendor.connect_ssh has been renamed to SSHVendor.run_command",
923
            DeprecationWarning)
924
        return self.run_command(host, command, username=username, port=port)
925

    
926
    def run_command(self, host, command, username=None, port=None):
927
        """Connect to an SSH server.
928

929
        Run a command remotely and return a file-like object for interaction
930
        with the remote command.
931

932
        :param host: Host name
933
        :param command: Command to run (as argv array)
934
        :param username: Optional ame of user to log in as
935
        :param port: Optional SSH port to use
936
        """
937
        raise NotImplementedError(self.run_command)
938

    
939

    
940
class SubprocessSSHVendor(SSHVendor):
941
    """SSH vendor that shells out to the local 'ssh' command."""
942

    
943
    def run_command(self, host, command, username=None, port=None):
944
        if not isinstance(command, bytes):
945
            raise TypeError(command)
946

    
947
        #FIXME: This has no way to deal with passwords..
948
        args = ['ssh', '-x']
949
        if port is not None:
950
            args.extend(['-p', str(port)])
951
        if username is not None:
952
            host = '%s@%s' % (username, host)
953
        args.append(host)
954
        proc = subprocess.Popen(args + [command],
955
                                stdin=subprocess.PIPE,
956
                                stdout=subprocess.PIPE)
957
        return SubprocessWrapper(proc)
958

    
959

    
960
def ParamikoSSHVendor(**kwargs):
961
    import warnings
962
    warnings.warn(
963
        "ParamikoSSHVendor has been moved to dulwich.contrib.paramiko_vendor.",
964
        DeprecationWarning)
965
    from dulwich.contrib.paramiko_vendor import ParamikoSSHVendor
966
    return ParamikoSSHVendor(**kwargs)
967

    
968

    
969
# Can be overridden by users
970
get_ssh_vendor = SubprocessSSHVendor
971

    
972

    
973
class SSHGitClient(TraditionalGitClient):
974

    
975
    def __init__(self, host, port=None, username=None, vendor=None, **kwargs):
976
        self.host = host
977
        self.port = port
978
        self.username = username
979
        super(SSHGitClient, self).__init__(**kwargs)
980
        self.alternative_paths = {}
981
        if vendor is not None:
982
            self.ssh_vendor = vendor
983
        else:
984
            self.ssh_vendor = get_ssh_vendor()
985

    
986
    def get_url(self, path):
987
        netloc = self.host
988
        if self.port is not None:
989
            netloc += ":%d" % self.port
990

    
991
        if self.username is not None:
992
            netloc = urlquote(self.username, '@/:') + "@" + netloc
993

    
994
        return urlparse.urlunsplit(('ssh', netloc, path, '', ''))
995

    
996
    @classmethod
997
    def from_parsedurl(cls, parsedurl, **kwargs):
998
        return cls(host=parsedurl.hostname, port=parsedurl.port,
999
                   username=parsedurl.username, **kwargs)
1000

    
1001
    def _get_cmd_path(self, cmd):
1002
        cmd = self.alternative_paths.get(cmd, b'git-' + cmd)
1003
        assert isinstance(cmd, bytes)
1004
        return cmd
1005

    
1006
    def _connect(self, cmd, path):
1007
        if type(cmd) is not bytes:
1008
            raise TypeError(cmd)
1009
        if type(path) is not bytes:
1010
            path = path.encode(self._remote_path_encoding)
1011
        if path.startswith(b"/~"):
1012
            path = path[1:]
1013
        argv = self._get_cmd_path(cmd) + b" '" + path + b"'"
1014
        con = self.ssh_vendor.run_command(
1015
            self.host, argv, port=self.port, username=self.username)
1016
        return (Protocol(con.read, con.write, con.close,
1017
                         report_activity=self._report_activity),
1018
                con.can_read)
1019

    
1020

    
1021
def default_user_agent_string():
1022
    return "dulwich/%s" % ".".join([str(x) for x in dulwich.__version__])
1023

    
1024

    
1025
def default_urllib2_opener(config):
1026
    if config is not None:
1027
        proxy_server = config.get("http", "proxy")
1028
    else:
1029
        proxy_server = None
1030
    handlers = []
1031
    if proxy_server is not None:
1032
        handlers.append(urllib2.ProxyHandler({"http": proxy_server}))
1033
    opener = urllib2.build_opener(*handlers)
1034
    if config is not None:
1035
        user_agent = config.get("http", "useragent")
1036
    else:
1037
        user_agent = None
1038
    if user_agent is None:
1039
        user_agent = default_user_agent_string()
1040
    opener.addheaders = [('User-agent', user_agent)]
1041
    return opener
1042

    
1043

    
1044
class HttpGitClient(GitClient):
1045

    
1046
    def __init__(self, base_url, dumb=None, opener=None, config=None,
1047
                 username=None, password=None, **kwargs):
1048
        self._base_url = base_url.rstrip("/") + "/"
1049
        self._username = username
1050
        self._password = password
1051
        self.dumb = dumb
1052
        if opener is None:
1053
            self.opener = default_urllib2_opener(config)
1054
        else:
1055
            self.opener = opener
1056
        if username is not None:
1057
            pass_man = urllib2.HTTPPasswordMgrWithDefaultRealm()
1058
            pass_man.add_password(None, base_url, username, password)
1059
            self.opener.add_handler(urllib2.HTTPBasicAuthHandler(pass_man))
1060
        GitClient.__init__(self, **kwargs)
1061

    
1062
    def get_url(self, path):
1063
        return self._get_url(path).rstrip("/")
1064

    
1065
    @classmethod
1066
    def from_parsedurl(cls, parsedurl, **kwargs):
1067
        auth, host = urllib2.splituser(parsedurl.netloc)
1068
        password = parsedurl.password
1069
        if password is not None:
1070
            password = urlunquote(password)
1071
        username = parsedurl.username
1072
        if username is not None:
1073
            username = urlunquote(username)
1074
        # TODO(jelmer): This also strips the username
1075
        parsedurl = parsedurl._replace(netloc=host)
1076
        return cls(urlparse.urlunparse(parsedurl),
1077
                   password=password, username=username, **kwargs)
1078

    
1079
    def __repr__(self):
1080
        return "%s(%r, dumb=%r)" % (type(self).__name__, self._base_url, self.dumb)
1081

    
1082
    def _get_url(self, path):
1083
        return urlparse.urljoin(self._base_url, path).rstrip("/") + "/"
1084

    
1085
    def _http_request(self, url, headers={}, data=None):
1086
        req = urllib2.Request(url, headers=headers, data=data)
1087
        try:
1088
            resp = self.opener.open(req)
1089
        except urllib2.HTTPError as e:
1090
            if e.code == 404:
1091
                raise NotGitRepository()
1092
            if e.code != 200:
1093
                raise GitProtocolError("unexpected http response %d" % e.code)
1094
        return resp
1095

    
1096
    def _discover_references(self, service, url):
1097
        assert url[-1] == "/"
1098
        url = urlparse.urljoin(url, "info/refs")
1099
        headers = {}
1100
        if self.dumb is not False:
1101
            url += "?service=%s" % service.decode('ascii')
1102
            headers["Content-Type"] = "application/x-%s-request" % (
1103
                service.decode('ascii'))
1104
        resp = self._http_request(url, headers)
1105
        try:
1106
            content_type = resp.info().gettype()
1107
        except AttributeError:
1108
            content_type = resp.info().get_content_type()
1109
        try:
1110
            self.dumb = (not content_type.startswith("application/x-git-"))
1111
            if not self.dumb:
1112
                proto = Protocol(resp.read, None)
1113
                # The first line should mention the service
1114
                try:
1115
                    [pkt] = list(proto.read_pkt_seq())
1116
                except ValueError:
1117
                    raise GitProtocolError(
1118
                        "unexpected number of packets received")
1119
                if pkt.rstrip(b'\n') != (b'# service=' + service):
1120
                    raise GitProtocolError(
1121
                        "unexpected first line %r from smart server" % pkt)
1122
                return read_pkt_refs(proto)
1123
            else:
1124
                return read_info_refs(resp), set()
1125
        finally:
1126
            resp.close()
1127

    
1128
    def _smart_request(self, service, url, data):
1129
        assert url[-1] == "/"
1130
        url = urlparse.urljoin(url, service)
1131
        headers = {
1132
            "Content-Type": "application/x-%s-request" % service
1133
        }
1134
        resp = self._http_request(url, headers, data)
1135
        try:
1136
            content_type = resp.info().gettype()
1137
        except AttributeError:
1138
            content_type = resp.info().get_content_type()
1139
        if content_type != (
1140
                "application/x-%s-result" % service):
1141
            raise GitProtocolError("Invalid content-type from server: %s"
1142
                % content_type)
1143
        return resp
1144

    
1145
    def send_pack(self, path, determine_wants, generate_pack_contents,
1146
                  progress=None, write_pack=write_pack_objects):
1147
        """Upload a pack to a remote repository.
1148

1149
        :param path: Repository path (as bytestring)
1150
        :param generate_pack_contents: Function that can return a sequence of
1151
            the shas of the objects to upload.
1152
        :param progress: Optional progress function
1153
        :param write_pack: Function called with (file, iterable of objects) to
1154
            write the objects returned by generate_pack_contents to the server.
1155

1156
        :raises SendPackError: if server rejects the pack data
1157
        :raises UpdateRefsError: if the server supports report-status
1158
                                 and rejects ref updates
1159
        :return: new_refs dictionary containing the changes that were made
1160
            {refname: new_ref}, including deleted refs.
1161
        """
1162
        url = self._get_url(path)
1163
        old_refs, server_capabilities = self._discover_references(
1164
            b"git-receive-pack", url)
1165
        negotiated_capabilities = self._send_capabilities & server_capabilities
1166

    
1167
        if CAPABILITY_REPORT_STATUS in negotiated_capabilities:
1168
            self._report_status_parser = ReportStatusParser()
1169

    
1170
        new_refs = determine_wants(dict(old_refs))
1171
        if new_refs is None:
1172
            # Determine wants function is aborting the push.
1173
            return old_refs
1174
        if self.dumb:
1175
            raise NotImplementedError(self.fetch_pack)
1176
        req_data = BytesIO()
1177
        req_proto = Protocol(None, req_data.write)
1178
        (have, want) = self._handle_receive_pack_head(
1179
            req_proto, negotiated_capabilities, old_refs, new_refs)
1180
        if not want and set(new_refs.items()).issubset(set(old_refs.items())):
1181
            return new_refs
1182
        objects = generate_pack_contents(have, want)
1183
        if len(objects) > 0:
1184
            write_pack(req_proto.write_file(), objects)
1185
        resp = self._smart_request("git-receive-pack", url,
1186
                                   data=req_data.getvalue())
1187
        try:
1188
            resp_proto = Protocol(resp.read, None)
1189
            self._handle_receive_pack_tail(resp_proto, negotiated_capabilities,
1190
                progress)
1191
            return new_refs
1192
        finally:
1193
            resp.close()
1194

    
1195

    
1196
    def fetch_pack(self, path, determine_wants, graph_walker, pack_data,
1197
                   progress=None):
1198
        """Retrieve a pack from a git smart server.
1199

1200
        :param determine_wants: Callback that returns list of commits to fetch
1201
        :param graph_walker: Object with next() and ack().
1202
        :param pack_data: Callback called for each bit of data in the pack
1203
        :param progress: Callback for progress reports (strings)
1204
        :return: Dictionary with all remote refs (not just those fetched)
1205
        """
1206
        url = self._get_url(path)
1207
        refs, server_capabilities = self._discover_references(
1208
            b"git-upload-pack", url)
1209
        negotiated_capabilities = self._fetch_capabilities & server_capabilities
1210
        wants = determine_wants(refs)
1211
        if wants is not None:
1212
            wants = [cid for cid in wants if cid != ZERO_SHA]
1213
        if not wants:
1214
            return refs
1215
        if self.dumb:
1216
            raise NotImplementedError(self.send_pack)
1217
        req_data = BytesIO()
1218
        req_proto = Protocol(None, req_data.write)
1219
        self._handle_upload_pack_head(
1220
            req_proto, negotiated_capabilities, graph_walker, wants,
1221
            lambda: False)
1222
        resp = self._smart_request(
1223
            "git-upload-pack", url, data=req_data.getvalue())
1224
        try:
1225
            resp_proto = Protocol(resp.read, None)
1226
            self._handle_upload_pack_tail(resp_proto, negotiated_capabilities,
1227
                graph_walker, pack_data, progress)
1228
            return refs
1229
        finally:
1230
            resp.close()
1231

    
1232
    def get_refs(self, path):
1233
        """Retrieve the current refs from a git smart server."""
1234
        url = self._get_url(path)
1235
        refs, _ = self._discover_references(
1236
            b"git-upload-pack", url)
1237
        return refs
1238

    
1239

    
1240
def get_transport_and_path_from_url(url, config=None, **kwargs):
1241
    """Obtain a git client from a URL.
1242

1243
    :param url: URL to open (a unicode string)
1244
    :param config: Optional config object
1245
    :param thin_packs: Whether or not thin packs should be retrieved
1246
    :param report_activity: Optional callback for reporting transport
1247
        activity.
1248
    :return: Tuple with client instance and relative path.
1249
    """
1250
    parsed = urlparse.urlparse(url)
1251
    if parsed.scheme == 'git':
1252
        return (TCPGitClient.from_parsedurl(parsed, **kwargs),
1253
                parsed.path)
1254
    elif parsed.scheme in ('git+ssh', 'ssh'):
1255
        path = parsed.path
1256
        if path.startswith('/'):
1257
            path = parsed.path[1:]
1258
        return SSHGitClient.from_parsedurl(parsed, **kwargs), path
1259
    elif parsed.scheme in ('http', 'https'):
1260
        return HttpGitClient.from_parsedurl(
1261
            parsed, config=config, **kwargs), parsed.path
1262
    elif parsed.scheme == 'file':
1263
        return default_local_git_client_cls.from_parsedurl(
1264
            parsed, **kwargs), parsed.path
1265

    
1266
    raise ValueError("unknown scheme '%s'" % parsed.scheme)
1267

    
1268

    
1269
def get_transport_and_path(location, **kwargs):
1270
    """Obtain a git client from a URL.
1271

1272
    :param location: URL or path (a string)
1273
    :param config: Optional config object
1274
    :param thin_packs: Whether or not thin packs should be retrieved
1275
    :param report_activity: Optional callback for reporting transport
1276
        activity.
1277
    :return: Tuple with client instance and relative path.
1278
    """
1279
    # First, try to parse it as a URL
1280
    try:
1281
        return get_transport_and_path_from_url(location, **kwargs)
1282
    except ValueError:
1283
        pass
1284

    
1285
    if (sys.platform == 'win32' and
1286
            location[0].isalpha() and location[1:3] == ':\\'):
1287
        # Windows local path
1288
        return default_local_git_client_cls(**kwargs), location
1289

    
1290
    if ':' in location and not '@' in location:
1291
        # SSH with no user@, zero or one leading slash.
1292
        (hostname, path) = location.split(':', 1)
1293
        return SSHGitClient(hostname, **kwargs), path
1294
    elif ':' in location:
1295
        # SSH with user@host:foo.
1296
        user_host, path = location.split(':', 1)
1297
        if '@' in user_host:
1298
            user, host = user_host.rsplit('@', 1)
1299
        else:
1300
            user = None
1301
            host = user_host
1302
        return SSHGitClient(host, username=user, **kwargs), path
1303

    
1304
    # Otherwise, assume it's a local path.
1305
    return default_local_git_client_cls(**kwargs), location