/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/git/remote.py

  • Committer: Jelmer Vernooij
  • Date: 2020-02-07 02:14:30 UTC
  • mto: This revision was merged to the branch mainline in revision 7492.
  • Revision ID: jelmer@jelmer.uk-20200207021430-m49iq3x4x8xlib6x
Drop python2 support.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2007-2008 Canonical Ltd
 
1
# Copyright (C) 2007-2018 Jelmer Vernooij <jelmer@jelmer.uk>
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
 
 
17
 
import bzrlib
18
 
from bzrlib import urlutils
19
 
from bzrlib.bzrdir import BzrDir, BzrDirFormat
20
 
from bzrlib.errors import NoSuchFile, NotLocalUrl
21
 
from bzrlib.lockable_files import TransportLock
22
 
from bzrlib.repository import Repository
23
 
from bzrlib.trace import info
24
 
from bzrlib.transport import Transport
25
 
 
26
 
from bzrlib.plugins.git import lazy_check_versions
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
"""Remote dirs, repositories and branches."""
 
18
 
 
19
from __future__ import absolute_import
 
20
 
 
21
import gzip
 
22
from io import BytesIO
 
23
import re
 
24
 
 
25
from .. import (
 
26
    config,
 
27
    debug,
 
28
    errors,
 
29
    osutils,
 
30
    trace,
 
31
    ui,
 
32
    urlutils,
 
33
    )
 
34
from ..push import (
 
35
    PushResult,
 
36
    )
 
37
from ..errors import (
 
38
    AlreadyBranchError,
 
39
    BzrError,
 
40
    DivergedBranches,
 
41
    InProcessTransport,
 
42
    InvalidRevisionId,
 
43
    NoSuchFile,
 
44
    NoSuchRevision,
 
45
    NoSuchTag,
 
46
    NotBranchError,
 
47
    NotLocalUrl,
 
48
    PermissionDenied,
 
49
    UninitializableFormat,
 
50
    )
 
51
from ..revisiontree import RevisionTree
 
52
from ..transport import (
 
53
    Transport,
 
54
    register_urlparse_netloc_protocol,
 
55
    )
 
56
 
 
57
from . import (
 
58
    lazy_check_versions,
 
59
    is_github_url,
 
60
    user_agent_for_github,
 
61
    )
27
62
lazy_check_versions()
28
63
 
29
 
from bzrlib.plugins.git.branch import GitBranch
30
 
from bzrlib.plugins.git.errors import NoSuchRef
31
 
from bzrlib.plugins.git.dir import GitDir
32
 
from bzrlib.plugins.git.foreign import ForeignBranch
33
 
from bzrlib.plugins.git.repository import GitFormat, GitRepository
 
64
from .branch import (
 
65
    GitBranch,
 
66
    GitBranchFormat,
 
67
    GitBranchPushResult,
 
68
    GitTags,
 
69
    _quick_lookup_revno,
 
70
    )
 
71
from .dir import (
 
72
    GitControlDirFormat,
 
73
    GitDir,
 
74
    )
 
75
from .errors import (
 
76
    GitSmartRemoteNotSupported,
 
77
    NoSuchRef,
 
78
    )
 
79
from .mapping import (
 
80
    mapping_registry,
 
81
    )
 
82
from .object_store import (
 
83
    get_object_store,
 
84
    )
 
85
from .push import (
 
86
    remote_divergence,
 
87
    )
 
88
from .repository import (
 
89
    GitRepository,
 
90
    GitRepositoryFormat,
 
91
    )
 
92
from .refs import (
 
93
    branch_name_to_ref,
 
94
    is_peeled,
 
95
    ref_to_tag_name,
 
96
    tag_name_to_ref,
 
97
    )
34
98
 
 
99
import dulwich
 
100
import dulwich.client
 
101
from dulwich.errors import (
 
102
    GitProtocolError,
 
103
    HangupException,
 
104
    )
 
105
from dulwich.pack import (
 
106
    Pack,
 
107
    pack_objects_to_data,
 
108
    )
 
109
from dulwich.protocol import ZERO_SHA
 
110
from dulwich.refs import (
 
111
    DictRefsContainer,
 
112
    SYMREF,
 
113
    )
 
114
from dulwich.repo import (
 
115
    NotGitRepository,
 
116
    )
35
117
import os
 
118
import select
36
119
import tempfile
37
 
import urllib
38
 
import urlparse
39
 
 
40
 
import dulwich as git
41
 
from dulwich.pack import PackData, Pack, PackIndex
42
 
 
43
 
# Don't run any tests on GitSmartTransport as it is not intended to be 
 
120
 
 
121
import urllib.parse as urlparse
 
122
from urllib.parse import splituser
 
123
 
 
124
# urlparse only supports a limited number of schemes by default
 
125
register_urlparse_netloc_protocol('git')
 
126
register_urlparse_netloc_protocol('git+ssh')
 
127
 
 
128
from dulwich.pack import load_pack_index
 
129
 
 
130
 
 
131
class GitPushResult(PushResult):
 
132
 
 
133
    def _lookup_revno(self, revid):
 
134
        try:
 
135
            return _quick_lookup_revno(self.source_branch, self.target_branch,
 
136
                                       revid)
 
137
        except GitSmartRemoteNotSupported:
 
138
            return None
 
139
 
 
140
    @property
 
141
    def old_revno(self):
 
142
        return self._lookup_revno(self.old_revid)
 
143
 
 
144
    @property
 
145
    def new_revno(self):
 
146
        return self._lookup_revno(self.new_revid)
 
147
 
 
148
 
 
149
# Don't run any tests on GitSmartTransport as it is not intended to be
44
150
# a full implementation of Transport
45
151
def get_test_permutations():
46
152
    return []
47
153
 
48
154
 
 
155
def split_git_url(url):
 
156
    """Split a Git URL.
 
157
 
 
158
    :param url: Git URL
 
159
    :return: Tuple with host, port, username, path.
 
160
    """
 
161
    parsed_url = urlparse.urlparse(url)
 
162
    path = urlparse.unquote(parsed_url.path)
 
163
    if path.startswith("/~"):
 
164
        path = path[1:]
 
165
    return ((parsed_url.hostname or '', parsed_url.port, parsed_url.username, path))
 
166
 
 
167
 
 
168
class RemoteGitError(BzrError):
 
169
 
 
170
    _fmt = "Remote server error: %(msg)s"
 
171
 
 
172
 
 
173
class HeadUpdateFailed(BzrError):
 
174
 
 
175
    _fmt = ("Unable to update remote HEAD branch. To update the master "
 
176
            "branch, specify the URL %(base_url)s,branch=master.")
 
177
 
 
178
    def __init__(self, base_url):
 
179
        super(HeadUpdateFailed, self).__init__()
 
180
        self.base_url = base_url
 
181
 
 
182
 
 
183
def parse_git_error(url, message):
 
184
    """Parse a remote git server error and return a bzr exception.
 
185
 
 
186
    :param url: URL of the remote repository
 
187
    :param message: Message sent by the remote git server
 
188
    """
 
189
    message = str(message).strip()
 
190
    if (message.startswith("Could not find Repository ")
 
191
        or message == 'Repository not found.'
 
192
            or (message.startswith('Repository ') and
 
193
                message.endswith(' not found.'))):
 
194
        return NotBranchError(url, message)
 
195
    if message == "HEAD failed to update":
 
196
        base_url = urlutils.strip_segment_parameters(url)
 
197
        return HeadUpdateFailed(base_url)
 
198
    if message.startswith('access denied or repository not exported:'):
 
199
        extra, path = message.split(':', 1)
 
200
        return PermissionDenied(path.strip(), extra)
 
201
    if message.endswith('You are not allowed to push code to this project.'):
 
202
        return PermissionDenied(url, message)
 
203
    if message.endswith(' does not appear to be a git repository'):
 
204
        return NotBranchError(url, message)
 
205
    if re.match('(.+) is not a valid repository name',
 
206
                message.splitlines()[0]):
 
207
        return NotBranchError(url, message)
 
208
    m = re.match(r'Permission to ([^ ]+) denied to ([^ ]+)\.', message)
 
209
    if m:
 
210
        return PermissionDenied(m.group(1), 'denied to %s' % m.group(2))
 
211
    # Don't know, just return it to the user as-is
 
212
    return RemoteGitError(message)
 
213
 
 
214
 
49
215
class GitSmartTransport(Transport):
50
216
 
51
217
    def __init__(self, url, _client=None):
52
218
        Transport.__init__(self, url)
53
 
        (scheme, _, loc, _, _) = urlparse.urlsplit(url)
54
 
        assert scheme == "git"
55
 
        hostport, self._path = urllib.splithost(loc)
56
 
        (self._host, self._port) = urllib.splitnport(hostport, git.protocol.TCP_GIT_PORT)
 
219
        (self._host, self._port, self._username, self._path) = \
 
220
            split_git_url(url)
 
221
        if 'transport' in debug.debug_flags:
 
222
            trace.mutter('host: %r, user: %r, port: %r, path: %r',
 
223
                         self._host, self._username, self._port, self._path)
57
224
        self._client = _client
 
225
        self._stripped_path = self._path.rsplit(",", 1)[0]
 
226
 
 
227
    def external_url(self):
 
228
        return self.base
 
229
 
 
230
    def has(self, relpath):
 
231
        return False
58
232
 
59
233
    def _get_client(self):
60
 
        if self._client is not None:
61
 
            ret = self._client
62
 
            self._client = None
63
 
            return ret
64
 
        return git.client.TCPGitClient(self._host, self._port, 
65
 
            capabilities=["multi_ack", "side-band-64k", "ofs-delta", "side-band"])
 
234
        raise NotImplementedError(self._get_client)
66
235
 
67
 
    def fetch_pack(self, determine_wants, graph_walker, pack_data, progress=None):
68
 
        if progress is None:
69
 
            def progress(text):
70
 
                info("git: %s" % text)
71
 
        self._get_client().fetch_pack(self._path, determine_wants, 
72
 
            graph_walker, pack_data, progress)
 
236
    def _get_path(self):
 
237
        return self._stripped_path
73
238
 
74
239
    def get(self, path):
75
240
        raise NoSuchFile(path)
84
249
        else:
85
250
            newurl = urlutils.join(self.base, offset)
86
251
 
87
 
        return GitSmartTransport(newurl, self._client)
 
252
        return self.__class__(newurl, self._client)
 
253
 
 
254
 
 
255
class TCPGitSmartTransport(GitSmartTransport):
 
256
 
 
257
    _scheme = 'git'
 
258
 
 
259
    def _get_client(self):
 
260
        if self._client is not None:
 
261
            ret = self._client
 
262
            self._client = None
 
263
            return ret
 
264
        if self._host == '':
 
265
            # return dulwich.client.LocalGitClient()
 
266
            return dulwich.client.SubprocessGitClient()
 
267
        return dulwich.client.TCPGitClient(
 
268
            self._host, self._port, report_activity=self._report_activity)
 
269
 
 
270
 
 
271
class SSHSocketWrapper(object):
 
272
 
 
273
    def __init__(self, sock):
 
274
        self.sock = sock
 
275
 
 
276
    def read(self, len=None):
 
277
        return self.sock.recv(len)
 
278
 
 
279
    def write(self, data):
 
280
        return self.sock.write(data)
 
281
 
 
282
    def can_read(self):
 
283
        return len(select.select([self.sock.fileno()], [], [], 0)[0]) > 0
 
284
 
 
285
 
 
286
class DulwichSSHVendor(dulwich.client.SSHVendor):
 
287
 
 
288
    def __init__(self):
 
289
        from ..transport import ssh
 
290
        self.bzr_ssh_vendor = ssh._get_ssh_vendor()
 
291
 
 
292
    def run_command(self, host, command, username=None, port=None):
 
293
        connection = self.bzr_ssh_vendor.connect_ssh(
 
294
            username=username, password=None, port=port, host=host,
 
295
            command=command)
 
296
        (kind, io_object) = connection.get_sock_or_pipes()
 
297
        if kind == 'socket':
 
298
            return SSHSocketWrapper(io_object)
 
299
        else:
 
300
            raise AssertionError("Unknown io object kind %r'" % kind)
 
301
 
 
302
 
 
303
# dulwich.client.get_ssh_vendor = DulwichSSHVendor
 
304
 
 
305
 
 
306
class SSHGitSmartTransport(GitSmartTransport):
 
307
 
 
308
    _scheme = 'git+ssh'
 
309
 
 
310
    def _get_path(self):
 
311
        path = self._stripped_path
 
312
        if path.startswith("/~/"):
 
313
            return path[3:]
 
314
        return path
 
315
 
 
316
    def _get_client(self):
 
317
        if self._client is not None:
 
318
            ret = self._client
 
319
            self._client = None
 
320
            return ret
 
321
        location_config = config.LocationConfig(self.base)
 
322
        client = dulwich.client.SSHGitClient(
 
323
            self._host, self._port, self._username,
 
324
            report_activity=self._report_activity)
 
325
        # Set up alternate pack program paths
 
326
        upload_pack = location_config.get_user_option('git_upload_pack')
 
327
        if upload_pack:
 
328
            client.alternative_paths["upload-pack"] = upload_pack
 
329
        receive_pack = location_config.get_user_option('git_receive_pack')
 
330
        if receive_pack:
 
331
            client.alternative_paths["receive-pack"] = receive_pack
 
332
        return client
 
333
 
 
334
 
 
335
class RemoteGitBranchFormat(GitBranchFormat):
 
336
 
 
337
    def get_format_description(self):
 
338
        return 'Remote Git Branch'
 
339
 
 
340
    @property
 
341
    def _matchingcontroldir(self):
 
342
        return RemoteGitControlDirFormat()
 
343
 
 
344
    def initialize(self, a_controldir, name=None, repository=None,
 
345
                   append_revisions_only=None):
 
346
        raise UninitializableFormat(self)
 
347
 
 
348
 
 
349
class DefaultProgressReporter(object):
 
350
 
 
351
    _GIT_PROGRESS_PARTIAL_RE = re.compile(r"(.*?): +(\d+)% \((\d+)/(\d+)\)")
 
352
    _GIT_PROGRESS_TOTAL_RE = re.compile(r"(.*?): (\d+)")
 
353
 
 
354
    def __init__(self, pb):
 
355
        self.pb = pb
 
356
 
 
357
    def progress(self, text):
 
358
        text = text.rstrip(b"\r\n")
 
359
        text = text.decode('utf-8')
 
360
        if text.lower().startswith('error: '):
 
361
            trace.show_error('git: %s', text[len(b'error: '):])
 
362
        else:
 
363
            trace.mutter("git: %s", text)
 
364
            g = self._GIT_PROGRESS_PARTIAL_RE.match(text)
 
365
            if g is not None:
 
366
                (text, pct, current, total) = g.groups()
 
367
                self.pb.update(text, int(current), int(total))
 
368
            else:
 
369
                g = self._GIT_PROGRESS_TOTAL_RE.match(text)
 
370
                if g is not None:
 
371
                    (text, total) = g.groups()
 
372
                    self.pb.update(text, None, int(total))
 
373
                else:
 
374
                    trace.note("%s", text)
88
375
 
89
376
 
90
377
class RemoteGitDir(GitDir):
91
378
 
92
 
    def __init__(self, transport, lockfiles, format):
 
379
    def __init__(self, transport, format, client, client_path):
93
380
        self._format = format
94
381
        self.root_transport = transport
95
382
        self.transport = transport
96
 
        self._lockfiles = lockfiles
 
383
        self._mode_check_done = None
 
384
        self._client = client
 
385
        self._client_path = client_path
 
386
        self.base = self.root_transport.base
 
387
        self._refs = None
 
388
 
 
389
    @property
 
390
    def _gitrepository_class(self):
 
391
        return RemoteGitRepository
 
392
 
 
393
    def archive(self, format, committish, write_data, progress=None,
 
394
                write_error=None, subdirs=None, prefix=None):
 
395
        if progress is None:
 
396
            pb = ui.ui_factory.nested_progress_bar()
 
397
            progress = DefaultProgressReporter(pb).progress
 
398
        else:
 
399
            pb = None
 
400
        def progress_wrapper(message):
 
401
            if message.startswith(b"fatal: Unknown archive format \'"):
 
402
                format = message.strip()[len(b"fatal: Unknown archive format '"):-1]
 
403
                raise errors.NoSuchExportFormat(format.decode('ascii'))
 
404
            return progress(message)
 
405
        try:
 
406
            self._client.archive(
 
407
                self._client_path, committish, write_data, progress_wrapper,
 
408
                write_error,
 
409
                format=(format.encode('ascii') if format else None),
 
410
                subdirs=subdirs,
 
411
                prefix=(prefix.encode('utf-8') if prefix else None))
 
412
        except GitProtocolError as e:
 
413
            raise parse_git_error(self.transport.external_url(), e)
 
414
        finally:
 
415
            if pb is not None:
 
416
                pb.finished()
 
417
 
 
418
    def fetch_pack(self, determine_wants, graph_walker, pack_data,
 
419
                   progress=None):
 
420
        if progress is None:
 
421
            pb = ui.ui_factory.nested_progress_bar()
 
422
            progress = DefaultProgressReporter(pb).progress
 
423
        else:
 
424
            pb = None
 
425
        try:
 
426
            result = self._client.fetch_pack(
 
427
                self._client_path, determine_wants, graph_walker, pack_data,
 
428
                progress)
 
429
            if result.refs is None:
 
430
                result.refs = {}
 
431
            self._refs = remote_refs_dict_to_container(
 
432
                result.refs, result.symrefs)
 
433
            return result
 
434
        except GitProtocolError as e:
 
435
            raise parse_git_error(self.transport.external_url(), e)
 
436
        finally:
 
437
            if pb is not None:
 
438
                pb.finished()
 
439
 
 
440
    def send_pack(self, get_changed_refs, generate_pack_data, progress=None):
 
441
        if progress is None:
 
442
            pb = ui.ui_factory.nested_progress_bar()
 
443
            progress = DefaultProgressReporter(pb).progress
 
444
        else:
 
445
            pb = None
 
446
 
 
447
        def get_changed_refs_wrapper(refs):
 
448
            # TODO(jelmer): This drops symref information
 
449
            self._refs = remote_refs_dict_to_container(refs)
 
450
            return get_changed_refs(refs)
 
451
        try:
 
452
            return self._client.send_pack(
 
453
                self._client_path, get_changed_refs_wrapper,
 
454
                generate_pack_data, progress)
 
455
        except GitProtocolError as e:
 
456
            raise parse_git_error(self.transport.external_url(), e)
 
457
        finally:
 
458
            if pb is not None:
 
459
                pb.finished()
 
460
 
 
461
    def create_branch(self, name=None, repository=None,
 
462
                      append_revisions_only=None, ref=None):
 
463
        refname = self._get_selected_ref(name, ref)
 
464
        if refname != b'HEAD' and refname in self.get_refs_container():
 
465
            raise AlreadyBranchError(self.user_url)
 
466
        if refname in self.get_refs_container():
 
467
            ref_chain, unused_sha = self.get_refs_container().follow(
 
468
                self._get_selected_ref(None))
 
469
            if ref_chain[0] == b'HEAD':
 
470
                refname = ref_chain[1]
 
471
        repo = self.open_repository()
 
472
        return RemoteGitBranch(self, repo, refname)
 
473
 
 
474
    def destroy_branch(self, name=None):
 
475
        refname = self._get_selected_ref(name)
 
476
 
 
477
        def get_changed_refs(old_refs):
 
478
            ret = {}
 
479
            if refname not in old_refs:
 
480
                raise NotBranchError(self.user_url)
 
481
            ret[refname] = dulwich.client.ZERO_SHA
 
482
            return ret
 
483
 
 
484
        def generate_pack_data(have, want, ofs_delta=False):
 
485
            return pack_objects_to_data([])
 
486
        self.send_pack(get_changed_refs, generate_pack_data)
 
487
 
 
488
    @property
 
489
    def user_url(self):
 
490
        return self.control_url
 
491
 
 
492
    @property
 
493
    def user_transport(self):
 
494
        return self.root_transport
 
495
 
 
496
    @property
 
497
    def control_url(self):
 
498
        return self.control_transport.base
 
499
 
 
500
    @property
 
501
    def control_transport(self):
 
502
        return self.root_transport
97
503
 
98
504
    def open_repository(self):
99
 
        return RemoteGitRepository(self, self._lockfiles)
100
 
 
101
 
    def open_branch(self, _unsupported=False):
 
505
        return RemoteGitRepository(self)
 
506
 
 
507
    def get_branch_reference(self, name=None):
 
508
        ref = branch_name_to_ref(name)
 
509
        val = self.get_refs_container().read_ref(ref)
 
510
        if val.startswith(SYMREF):
 
511
            return val[len(SYMREF):]
 
512
        return None
 
513
 
 
514
    def open_branch(self, name=None, unsupported=False,
 
515
                    ignore_fallbacks=False, ref=None, possible_transports=None,
 
516
                    nascent_ok=False):
102
517
        repo = self.open_repository()
103
 
        # TODO: Support for multiple branches in one bzrdir in bzrlib!
104
 
        return RemoteGitBranch(self, repo, "HEAD", self._lockfiles)
 
518
        ref = self._get_selected_ref(name, ref)
 
519
        try:
 
520
            if not nascent_ok and ref not in self.get_refs_container():
 
521
                raise NotBranchError(
 
522
                    self.root_transport.base, controldir=self)
 
523
        except NotGitRepository:
 
524
            raise NotBranchError(self.root_transport.base,
 
525
                                 controldir=self)
 
526
        ref_chain, unused_sha = self.get_refs_container().follow(ref)
 
527
        return RemoteGitBranch(self, repo, ref_chain[-1])
105
528
 
106
 
    def open_workingtree(self):
 
529
    def open_workingtree(self, recommend_upgrade=False):
107
530
        raise NotLocalUrl(self.transport.base)
108
531
 
 
532
    def has_workingtree(self):
 
533
        return False
 
534
 
 
535
    def get_peeled(self, name):
 
536
        return self.get_refs_container().get_peeled(name)
 
537
 
 
538
    def get_refs_container(self):
 
539
        if self._refs is not None:
 
540
            return self._refs
 
541
        result = self.fetch_pack(lambda x: None, None,
 
542
                                 lambda x: None,
 
543
                                 lambda x: trace.mutter("git: %s" % x))
 
544
        self._refs = remote_refs_dict_to_container(
 
545
            result.refs, result.symrefs)
 
546
        return self._refs
 
547
 
 
548
    def push_branch(self, source, revision_id=None, overwrite=False,
 
549
                    remember=False, create_prefix=False, lossy=False,
 
550
                    name=None):
 
551
        """Push the source branch into this ControlDir."""
 
552
        if revision_id is None:
 
553
            # No revision supplied by the user, default to the branch
 
554
            # revision
 
555
            revision_id = source.last_revision()
 
556
 
 
557
        push_result = GitPushResult()
 
558
        push_result.workingtree_updated = None
 
559
        push_result.master_branch = None
 
560
        push_result.source_branch = source
 
561
        push_result.stacked_on = None
 
562
        push_result.branch_push_result = None
 
563
        repo = self.find_repository()
 
564
        refname = self._get_selected_ref(name)
 
565
        if isinstance(source, GitBranch) and lossy:
 
566
            raise errors.LossyPushToSameVCS(source.controldir, self)
 
567
        source_store = get_object_store(source.repository)
 
568
        fetch_tags = source.get_config_stack().get('branch.fetch_tags')
 
569
        def get_changed_refs(refs):
 
570
            self._refs = remote_refs_dict_to_container(refs)
 
571
            ret = {}
 
572
            # TODO(jelmer): Unpeel if necessary
 
573
            push_result.new_original_revid = revision_id
 
574
            if lossy:
 
575
                new_sha = source_store._lookup_revision_sha1(revision_id)
 
576
            else:
 
577
                try:
 
578
                    new_sha = repo.lookup_bzr_revision_id(revision_id)[0]
 
579
                except errors.NoSuchRevision:
 
580
                    raise errors.NoRoundtrippingSupport(
 
581
                        source, self.open_branch(name=name, nascent_ok=True))
 
582
            if not overwrite:
 
583
                if remote_divergence(ret.get(refname), new_sha,
 
584
                                     source_store):
 
585
                    raise DivergedBranches(
 
586
                        source, self.open_branch(name, nascent_ok=True))
 
587
            ret[refname] = new_sha
 
588
            if fetch_tags:
 
589
                for tagname, revid in source.tags.get_tag_dict().items():
 
590
                    if lossy:
 
591
                        new_sha = source_store._lookup_revision_sha1(revid)
 
592
                    else:
 
593
                        try:
 
594
                            new_sha = repo.lookup_bzr_revision_id(revid)[0]
 
595
                        except errors.NoSuchRevision:
 
596
                            continue
 
597
                    ret[tag_name_to_ref(tagname)] = new_sha
 
598
            return ret
 
599
        with source_store.lock_read():
 
600
            if lossy:
 
601
                generate_pack_data = source_store.generate_lossy_pack_data
 
602
            else:
 
603
                generate_pack_data = source_store.generate_pack_data
 
604
            new_refs = self.send_pack(get_changed_refs, generate_pack_data)
 
605
        push_result.new_revid = repo.lookup_foreign_revision_id(
 
606
            new_refs[refname])
 
607
        try:
 
608
            old_remote = self._refs[refname]
 
609
        except KeyError:
 
610
            old_remote = ZERO_SHA
 
611
        push_result.old_revid = repo.lookup_foreign_revision_id(old_remote)
 
612
        self._refs = remote_refs_dict_to_container(new_refs)
 
613
        push_result.target_branch = self.open_branch(name)
 
614
        if old_remote != ZERO_SHA:
 
615
            push_result.branch_push_result = GitBranchPushResult()
 
616
            push_result.branch_push_result.source_branch = source
 
617
            push_result.branch_push_result.target_branch = (
 
618
                push_result.target_branch)
 
619
            push_result.branch_push_result.local_branch = None
 
620
            push_result.branch_push_result.master_branch = (
 
621
                push_result.target_branch)
 
622
            push_result.branch_push_result.old_revid = push_result.old_revid
 
623
            push_result.branch_push_result.new_revid = push_result.new_revid
 
624
            push_result.branch_push_result.new_original_revid = (
 
625
                push_result.new_original_revid)
 
626
        if source.get_push_location() is None or remember:
 
627
            source.set_push_location(push_result.target_branch.base)
 
628
        return push_result
 
629
 
 
630
    def _find_commondir(self):
 
631
        # There is no way to find the commondir, if there is any.
 
632
        return self
 
633
 
109
634
 
110
635
class EmptyObjectStoreIterator(dict):
111
636
 
116
641
class TemporaryPackIterator(Pack):
117
642
 
118
643
    def __init__(self, path, resolve_ext_ref):
119
 
        self.resolve_ext_ref = resolve_ext_ref
120
 
        super(TemporaryPackIterator, self).__init__(path)
 
644
        super(TemporaryPackIterator, self).__init__(
 
645
            path, resolve_ext_ref=resolve_ext_ref)
 
646
        self._idx_load = lambda: self._idx_load_or_generate(self._idx_path)
121
647
 
122
 
    @property
123
 
    def idx(self):
124
 
        if self._idx is None:
125
 
            self._data.create_index_v2(self._idx_path, self.resolve_ext_ref)
126
 
            self._idx = PackIndex(self._idx_path)
127
 
        return self._idx
 
648
    def _idx_load_or_generate(self, path):
 
649
        if not os.path.exists(path):
 
650
            with ui.ui_factory.nested_progress_bar() as pb:
 
651
                def report_progress(cur, total):
 
652
                    pb.update("generating index", cur, total)
 
653
                self.data.create_index(path, progress=report_progress)
 
654
        return load_pack_index(path)
128
655
 
129
656
    def __del__(self):
130
 
        os.remove(self._data_path)
131
 
        os.remove(self._idx_path)
 
657
        if self._idx is not None:
 
658
            self._idx.close()
 
659
            os.remove(self._idx_path)
 
660
        if self._data is not None:
 
661
            self._data.close()
 
662
            os.remove(self._data_path)
 
663
 
 
664
 
 
665
class BzrGitHttpClient(dulwich.client.HttpGitClient):
 
666
 
 
667
    def __init__(self, transport, *args, **kwargs):
 
668
        self.transport = transport
 
669
        url = urlutils.URL.from_string(transport.external_url())
 
670
        url.user = url.quoted_user = None
 
671
        url.password = url.quoted_password = None
 
672
        url = urlutils.strip_segment_parameters(str(url))
 
673
        super(BzrGitHttpClient, self).__init__(url, *args, **kwargs)
 
674
 
 
675
    def _http_request(self, url, headers=None, data=None,
 
676
                      allow_compression=False):
 
677
        """Perform HTTP request.
 
678
 
 
679
        :param url: Request URL.
 
680
        :param headers: Optional custom headers to override defaults.
 
681
        :param data: Request data.
 
682
        :param allow_compression: Allow GZipped communication.
 
683
        :return: Tuple (`response`, `read`), where response is an `urllib3`
 
684
            response object with additional `content_type` and
 
685
            `redirect_location` properties, and `read` is a consumable read
 
686
            method for the response data.
 
687
        """
 
688
        if is_github_url(url):
 
689
            headers['User-agent'] = user_agent_for_github()
 
690
        headers["Pragma"] = "no-cache"
 
691
        if allow_compression:
 
692
            headers["Accept-Encoding"] = "gzip"
 
693
        else:
 
694
            headers["Accept-Encoding"] = "identity"
 
695
 
 
696
        response = self.transport.request(
 
697
            ('GET' if data is None else 'POST'),
 
698
            url,
 
699
            body=data,
 
700
            headers=headers, retries=8)
 
701
 
 
702
        if response.status == 404:
 
703
            raise NotGitRepository()
 
704
        elif response.status != 200:
 
705
            raise GitProtocolError("unexpected http resp %d for %s" %
 
706
                                   (response.code, url))
 
707
 
 
708
        # TODO: Optimization available by adding `preload_content=False` to the
 
709
        # request and just passing the `read` method on instead of going via
 
710
        # `BytesIO`, if we can guarantee that the entire response is consumed
 
711
        # before issuing the next to still allow for connection reuse from the
 
712
        # pool.
 
713
        if response.getheader("Content-Encoding") == "gzip":
 
714
            read = gzip.GzipFile(fileobj=BytesIO(response.read())).read
 
715
        else:
 
716
            read = response.read
 
717
 
 
718
        class WrapResponse(object):
 
719
 
 
720
            def __init__(self, response):
 
721
                self._response = response
 
722
                self.status = response.status
 
723
                self.content_type = response.getheader("Content-Type")
 
724
                self.redirect_location = response._actual.geturl()
 
725
 
 
726
            def readlines(self):
 
727
                return self._response.readlines()
 
728
 
 
729
            def close(self):
 
730
                pass
 
731
 
 
732
        return WrapResponse(response), read
 
733
 
 
734
 
 
735
def _git_url_and_path_from_transport(external_url):
 
736
    url = urlutils.strip_segment_parameters(external_url)
 
737
    return urlparse.urlsplit(url)
 
738
 
 
739
 
 
740
class RemoteGitControlDirFormat(GitControlDirFormat):
 
741
    """The .git directory control format."""
 
742
 
 
743
    supports_workingtrees = False
 
744
 
 
745
    @classmethod
 
746
    def _known_formats(self):
 
747
        return set([RemoteGitControlDirFormat()])
 
748
 
 
749
    def get_branch_format(self):
 
750
        return RemoteGitBranchFormat()
 
751
 
 
752
    @property
 
753
    def repository_format(self):
 
754
        return GitRepositoryFormat()
 
755
 
 
756
    def is_initializable(self):
 
757
        return False
 
758
 
 
759
    def is_supported(self):
 
760
        return True
 
761
 
 
762
    def open(self, transport, _found=None):
 
763
        """Open this directory.
 
764
 
 
765
        """
 
766
        split_url = _git_url_and_path_from_transport(transport.external_url())
 
767
        if isinstance(transport, GitSmartTransport):
 
768
            client = transport._get_client()
 
769
        elif split_url.scheme in ("http", "https"):
 
770
            client = BzrGitHttpClient(transport)
 
771
        elif split_url.scheme in ('file', ):
 
772
            client = dulwich.client.LocalGitClient()
 
773
        else:
 
774
            raise NotBranchError(transport.base)
 
775
        if not _found:
 
776
            pass  # TODO(jelmer): Actually probe for something
 
777
        return RemoteGitDir(transport, self, client, split_url.path)
 
778
 
 
779
    def get_format_description(self):
 
780
        return "Remote Git Repository"
 
781
 
 
782
    def initialize_on_transport(self, transport):
 
783
        raise UninitializableFormat(self)
 
784
 
 
785
    def supports_transport(self, transport):
 
786
        try:
 
787
            external_url = transport.external_url()
 
788
        except InProcessTransport:
 
789
            raise NotBranchError(path=transport.base)
 
790
        return (external_url.startswith("http:")
 
791
                or external_url.startswith("https:")
 
792
                or external_url.startswith("git+")
 
793
                or external_url.startswith("git:"))
 
794
 
 
795
 
 
796
class GitRemoteRevisionTree(RevisionTree):
 
797
 
 
798
    def archive(self, format, name, root=None, subdir=None, force_mtime=None):
 
799
        """Create an archive of this tree.
 
800
 
 
801
        :param format: Format name (e.g. 'tar')
 
802
        :param name: target file name
 
803
        :param root: Root directory name (or None)
 
804
        :param subdir: Subdirectory to export (or None)
 
805
        :return: Iterator over archive chunks
 
806
        """
 
807
        commit = self._repository.lookup_bzr_revision_id(
 
808
            self.get_revision_id())[0]
 
809
        f = tempfile.SpooledTemporaryFile()
 
810
        # git-upload-archive(1) generaly only supports refs. So let's see if we
 
811
        # can find one.
 
812
        reverse_refs = {
 
813
            v: k for (k, v) in
 
814
            self._repository.controldir.get_refs_container().as_dict().items()}
 
815
        try:
 
816
            committish = reverse_refs[commit]
 
817
        except KeyError:
 
818
            # No? Maybe the user has uploadArchive.allowUnreachable enabled.
 
819
            # Let's hope for the best.
 
820
            committish = commit
 
821
        self._repository.archive(
 
822
            format, committish, f.write,
 
823
            subdirs=([subdir] if subdir else None),
 
824
            prefix=(root + '/') if root else '')
 
825
        f.seek(0)
 
826
        return osutils.file_iterator(f)
 
827
 
 
828
    def is_versioned(self, path):
 
829
        raise GitSmartRemoteNotSupported(self.is_versioned, self)
 
830
 
 
831
    def has_filename(self, path):
 
832
        raise GitSmartRemoteNotSupported(self.has_filename, self)
 
833
 
 
834
    def get_file_text(self, path):
 
835
        raise GitSmartRemoteNotSupported(self.get_file_text, self)
 
836
 
 
837
    def list_files(self, include_root=False, from_dir=None, recursive=True):
 
838
        raise GitSmartRemoteNotSupported(self.list_files, self)
132
839
 
133
840
 
134
841
class RemoteGitRepository(GitRepository):
135
842
 
136
 
    def __init__(self, gitdir, lockfiles):
137
 
        GitRepository.__init__(self, gitdir, lockfiles)
138
 
 
139
 
    def fetch_pack(self, determine_wants, graph_walker, pack_data, 
 
843
    supports_random_access = False
 
844
 
 
845
    @property
 
846
    def user_url(self):
 
847
        return self.control_url
 
848
 
 
849
    def get_parent_map(self, revids):
 
850
        raise GitSmartRemoteNotSupported(self.get_parent_map, self)
 
851
 
 
852
    def archive(self, *args, **kwargs):
 
853
        return self.controldir.archive(*args, **kwargs)
 
854
 
 
855
    def fetch_pack(self, determine_wants, graph_walker, pack_data,
140
856
                   progress=None):
141
 
        self._transport.fetch_pack(determine_wants, graph_walker, pack_data, 
142
 
            progress)
143
 
 
144
 
    def fetch_objects(self, determine_wants, graph_walker, resolve_ext_ref, progress=None):
 
857
        return self.controldir.fetch_pack(
 
858
            determine_wants, graph_walker, pack_data, progress)
 
859
 
 
860
    def send_pack(self, get_changed_refs, generate_pack_data):
 
861
        return self.controldir.send_pack(get_changed_refs, generate_pack_data)
 
862
 
 
863
    def fetch_objects(self, determine_wants, graph_walker, resolve_ext_ref,
 
864
                      progress=None):
145
865
        fd, path = tempfile.mkstemp(suffix=".pack")
146
 
        self.fetch_pack(determine_wants, graph_walker, lambda x: os.write(fd, x), progress)
147
 
        os.close(fd)
 
866
        try:
 
867
            self.fetch_pack(determine_wants, graph_walker,
 
868
                            lambda x: os.write(fd, x), progress)
 
869
        finally:
 
870
            os.close(fd)
148
871
        if os.path.getsize(path) == 0:
149
872
            return EmptyObjectStoreIterator()
150
873
        return TemporaryPackIterator(path[:-len(".pack")], resolve_ext_ref)
151
874
 
 
875
    def lookup_bzr_revision_id(self, bzr_revid, mapping=None):
 
876
        # This won't work for any round-tripped bzr revisions, but it's a
 
877
        # start..
 
878
        try:
 
879
            return mapping_registry.revision_id_bzr_to_foreign(bzr_revid)
 
880
        except InvalidRevisionId:
 
881
            raise NoSuchRevision(self, bzr_revid)
 
882
 
 
883
    def lookup_foreign_revision_id(self, foreign_revid, mapping=None):
 
884
        """Lookup a revision id.
 
885
 
 
886
        """
 
887
        if mapping is None:
 
888
            mapping = self.get_mapping()
 
889
        # Not really an easy way to parse foreign revids here..
 
890
        return mapping.revision_id_foreign_to_bzr(foreign_revid)
 
891
 
 
892
    def revision_tree(self, revid):
 
893
        return GitRemoteRevisionTree(self, revid)
 
894
 
 
895
    def get_revisions(self, revids):
 
896
        raise GitSmartRemoteNotSupported(self.get_revisions, self)
 
897
 
 
898
    def has_revisions(self, revids):
 
899
        raise GitSmartRemoteNotSupported(self.get_revisions, self)
 
900
 
 
901
 
 
902
class RemoteGitTagDict(GitTags):
 
903
 
 
904
    def set_tag(self, name, revid):
 
905
        sha = self.branch.lookup_bzr_revision_id(revid)[0]
 
906
        self._set_ref(name, sha)
 
907
 
 
908
    def delete_tag(self, name):
 
909
        self._set_ref(name, dulwich.client.ZERO_SHA)
 
910
 
 
911
    def _set_ref(self, name, sha):
 
912
        ref = tag_name_to_ref(name)
 
913
 
 
914
        def get_changed_refs(old_refs):
 
915
            ret = {}
 
916
            if sha == dulwich.client.ZERO_SHA and ref not in old_refs:
 
917
                raise NoSuchTag(name)
 
918
            ret[ref] = sha
 
919
            return ret
 
920
 
 
921
        def generate_pack_data(have, want, ofs_delta=False):
 
922
            return pack_objects_to_data([])
 
923
        self.repository.send_pack(get_changed_refs, generate_pack_data)
 
924
 
152
925
 
153
926
class RemoteGitBranch(GitBranch):
154
927
 
155
 
    def __init__(self, bzrdir, repository, name, lockfiles):
156
 
        def determine_wants(heads):
157
 
            if not name in heads:
158
 
                raise NoSuchRef(name)
159
 
            self._ref = heads[name]
160
 
        bzrdir.root_transport.fetch_pack(determine_wants, None, lambda x: None, 
161
 
                             lambda x: mutter("git: %s" % x))
162
 
        super(RemoteGitBranch, self).__init__(bzrdir, repository, name, self._ref, lockfiles)
 
928
    def __init__(self, controldir, repository, name):
 
929
        self._sha = None
 
930
        super(RemoteGitBranch, self).__init__(controldir, repository, name,
 
931
                                              RemoteGitBranchFormat())
 
932
 
 
933
    def last_revision_info(self):
 
934
        raise GitSmartRemoteNotSupported(self.last_revision_info, self)
 
935
 
 
936
    @property
 
937
    def user_url(self):
 
938
        return self.control_url
 
939
 
 
940
    @property
 
941
    def control_url(self):
 
942
        return self.base
 
943
 
 
944
    def revision_id_to_revno(self, revision_id):
 
945
        raise GitSmartRemoteNotSupported(self.revision_id_to_revno, self)
163
946
 
164
947
    def last_revision(self):
165
 
        return self.mapping.revision_id_foreign_to_bzr(self._ref)
 
948
        return self.lookup_foreign_revision_id(self.head)
 
949
 
 
950
    @property
 
951
    def head(self):
 
952
        if self._sha is not None:
 
953
            return self._sha
 
954
        refs = self.controldir.get_refs_container()
 
955
        name = branch_name_to_ref(self.name)
 
956
        try:
 
957
            self._sha = refs[name]
 
958
        except KeyError:
 
959
            raise NoSuchRef(name, self.repository.user_url, refs)
 
960
        return self._sha
166
961
 
167
962
    def _synchronize_history(self, destination, revision_id):
168
963
        """See Branch._synchronize_history()."""
169
 
        destination.generate_revision_history(self.last_revision())
170
 
 
 
964
        if revision_id is None:
 
965
            revision_id = self.last_revision()
 
966
        destination.generate_revision_history(revision_id)
 
967
 
 
968
    def _get_parent_location(self):
 
969
        return None
 
970
 
 
971
    def get_push_location(self):
 
972
        return None
 
973
 
 
974
    def set_push_location(self, url):
 
975
        pass
 
976
 
 
977
    def _iter_tag_refs(self):
 
978
        """Iterate over the tag refs.
 
979
 
 
980
        :param refs: Refs dictionary (name -> git sha1)
 
981
        :return: iterator over (ref_name, tag_name, peeled_sha1, unpeeled_sha1)
 
982
        """
 
983
        refs = self.controldir.get_refs_container()
 
984
        for ref_name, unpeeled in refs.as_dict().items():
 
985
            try:
 
986
                tag_name = ref_to_tag_name(ref_name)
 
987
            except (ValueError, UnicodeDecodeError):
 
988
                continue
 
989
            peeled = refs.get_peeled(ref_name)
 
990
            if peeled is None:
 
991
                # Let's just hope it's a commit
 
992
                peeled = unpeeled
 
993
            if not isinstance(tag_name, str):
 
994
                raise TypeError(tag_name)
 
995
            yield (ref_name, tag_name, peeled, unpeeled)
 
996
 
 
997
    def set_last_revision_info(self, revno, revid):
 
998
        self.generate_revision_history(revid)
 
999
 
 
1000
    def generate_revision_history(self, revision_id, last_rev=None,
 
1001
                                  other_branch=None):
 
1002
        sha = self.lookup_bzr_revision_id(revision_id)[0]
 
1003
        def get_changed_refs(old_refs):
 
1004
            return {self.ref: sha}
 
1005
        def generate_pack_data(have, want, ofs_delta=False):
 
1006
            return pack_objects_to_data([])
 
1007
        self.repository.send_pack(get_changed_refs, generate_pack_data)
 
1008
        self._sha = sha
 
1009
 
 
1010
 
 
1011
def remote_refs_dict_to_container(refs_dict, symrefs_dict={}):
 
1012
    base = {}
 
1013
    peeled = {}
 
1014
    for k, v in refs_dict.items():
 
1015
        if is_peeled(k):
 
1016
            peeled[k[:-3]] = v
 
1017
        else:
 
1018
            base[k] = v
 
1019
    for name, target in symrefs_dict.items():
 
1020
        base[name] = SYMREF + target
 
1021
    ret = DictRefsContainer(base)
 
1022
    ret._peeled = peeled
 
1023
    return ret