/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-05-06 02:13:25 UTC
  • mfrom: (7490.7.21 work)
  • mto: This revision was merged to the branch mainline in revision 7501.
  • Revision ID: jelmer@jelmer.uk-20200506021325-awbmmqu1zyorz7sj
Merge 3.1 branch.

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