/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-03-22 01:35:14 UTC
  • mfrom: (7490.7.6 work)
  • mto: This revision was merged to the branch mainline in revision 7499.
  • Revision ID: jelmer@jelmer.uk-20200322013514-7vw1ntwho04rcuj3
merge lp:brz/3.1.

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
            if lossy:
 
610
                generate_pack_data = source_store.generate_lossy_pack_data
 
611
            else:
 
612
                generate_pack_data = source_store.generate_pack_data
 
613
            new_refs = self.send_pack(get_changed_refs, generate_pack_data)
 
614
        push_result.new_revid = repo.lookup_foreign_revision_id(
 
615
            new_refs[actual_refname])
 
616
        if old_sha is not None:
 
617
            push_result.old_revid = repo.lookup_foreign_revision_id(old_sha)
 
618
        else:
 
619
            push_result.old_revid = NULL_REVISION
 
620
        if self._refs is not None:
 
621
            update_refs_container(self._refs, new_refs)
 
622
        push_result.target_branch = self.open_branch(name)
 
623
        if old_sha is not None:
 
624
            push_result.branch_push_result = GitBranchPushResult()
 
625
            push_result.branch_push_result.source_branch = source
 
626
            push_result.branch_push_result.target_branch = (
 
627
                push_result.target_branch)
 
628
            push_result.branch_push_result.local_branch = None
 
629
            push_result.branch_push_result.master_branch = (
 
630
                push_result.target_branch)
 
631
            push_result.branch_push_result.old_revid = push_result.old_revid
 
632
            push_result.branch_push_result.new_revid = push_result.new_revid
 
633
            push_result.branch_push_result.new_original_revid = (
 
634
                push_result.new_original_revid)
 
635
        if source.get_push_location() is None or remember:
 
636
            source.set_push_location(push_result.target_branch.base)
 
637
        return push_result
 
638
 
 
639
    def _find_commondir(self):
 
640
        # There is no way to find the commondir, if there is any.
 
641
        return self
 
642
 
 
643
 
 
644
class EmptyObjectStoreIterator(dict):
 
645
 
 
646
    def iterobjects(self):
 
647
        return []
 
648
 
 
649
 
 
650
class TemporaryPackIterator(Pack):
 
651
 
 
652
    def __init__(self, path, resolve_ext_ref):
 
653
        super(TemporaryPackIterator, self).__init__(
 
654
            path, resolve_ext_ref=resolve_ext_ref)
 
655
        self._idx_load = lambda: self._idx_load_or_generate(self._idx_path)
 
656
 
 
657
    def _idx_load_or_generate(self, path):
 
658
        if not os.path.exists(path):
 
659
            with ui.ui_factory.nested_progress_bar() as pb:
 
660
                def report_progress(cur, total):
 
661
                    pb.update("generating index", cur, total)
 
662
                self.data.create_index(path, progress=report_progress)
 
663
        return load_pack_index(path)
 
664
 
 
665
    def __del__(self):
 
666
        if self._idx is not None:
 
667
            self._idx.close()
 
668
            os.remove(self._idx_path)
 
669
        if self._data is not None:
 
670
            self._data.close()
 
671
            os.remove(self._data_path)
 
672
 
 
673
 
 
674
class BzrGitHttpClient(dulwich.client.HttpGitClient):
 
675
 
 
676
    def __init__(self, transport, *args, **kwargs):
 
677
        self.transport = transport
 
678
        url = urlutils.URL.from_string(transport.external_url())
 
679
        url.user = url.quoted_user = None
 
680
        url.password = url.quoted_password = None
 
681
        url = urlutils.strip_segment_parameters(str(url))
 
682
        super(BzrGitHttpClient, self).__init__(url, *args, **kwargs)
 
683
 
 
684
    def _http_request(self, url, headers=None, data=None,
 
685
                      allow_compression=False):
 
686
        """Perform HTTP request.
 
687
 
 
688
        :param url: Request URL.
 
689
        :param headers: Optional custom headers to override defaults.
 
690
        :param data: Request data.
 
691
        :param allow_compression: Allow GZipped communication.
 
692
        :return: Tuple (`response`, `read`), where response is an `urllib3`
 
693
            response object with additional `content_type` and
 
694
            `redirect_location` properties, and `read` is a consumable read
 
695
            method for the response data.
 
696
        """
 
697
        if is_github_url(url):
 
698
            headers['User-agent'] = user_agent_for_github()
 
699
        headers["Pragma"] = "no-cache"
 
700
        if allow_compression:
 
701
            headers["Accept-Encoding"] = "gzip"
 
702
        else:
 
703
            headers["Accept-Encoding"] = "identity"
 
704
 
 
705
        response = self.transport.request(
 
706
            ('GET' if data is None else 'POST'),
 
707
            url,
 
708
            body=data,
 
709
            headers=headers, retries=8)
 
710
 
 
711
        if response.status == 404:
 
712
            raise NotGitRepository()
 
713
        elif response.status != 200:
 
714
            raise GitProtocolError("unexpected http resp %d for %s" %
 
715
                                   (response.code, url))
 
716
 
 
717
        # TODO: Optimization available by adding `preload_content=False` to the
 
718
        # request and just passing the `read` method on instead of going via
 
719
        # `BytesIO`, if we can guarantee that the entire response is consumed
 
720
        # before issuing the next to still allow for connection reuse from the
 
721
        # pool.
 
722
        if response.getheader("Content-Encoding") == "gzip":
 
723
            read = gzip.GzipFile(fileobj=BytesIO(response.read())).read
 
724
        else:
 
725
            read = response.read
 
726
 
 
727
        class WrapResponse(object):
 
728
 
 
729
            def __init__(self, response):
 
730
                self._response = response
 
731
                self.status = response.status
 
732
                self.content_type = response.getheader("Content-Type")
 
733
                self.redirect_location = response._actual.geturl()
 
734
 
 
735
            def readlines(self):
 
736
                return self._response.readlines()
 
737
 
 
738
            def close(self):
 
739
                pass
 
740
 
 
741
        return WrapResponse(response), read
 
742
 
 
743
 
 
744
def _git_url_and_path_from_transport(external_url):
 
745
    url = urlutils.strip_segment_parameters(external_url)
 
746
    return urlparse.urlsplit(url)
 
747
 
 
748
 
 
749
class RemoteGitControlDirFormat(GitControlDirFormat):
 
750
    """The .git directory control format."""
 
751
 
 
752
    supports_workingtrees = False
 
753
 
 
754
    @classmethod
 
755
    def _known_formats(self):
 
756
        return set([RemoteGitControlDirFormat()])
 
757
 
 
758
    def get_branch_format(self):
 
759
        return RemoteGitBranchFormat()
 
760
 
 
761
    @property
 
762
    def repository_format(self):
 
763
        return GitRepositoryFormat()
 
764
 
 
765
    def is_initializable(self):
 
766
        return False
 
767
 
 
768
    def is_supported(self):
 
769
        return True
 
770
 
 
771
    def open(self, transport, _found=None):
 
772
        """Open this directory.
 
773
 
 
774
        """
 
775
        split_url = _git_url_and_path_from_transport(transport.external_url())
 
776
        if isinstance(transport, GitSmartTransport):
 
777
            client = transport._get_client()
 
778
        elif split_url.scheme in ("http", "https"):
 
779
            client = BzrGitHttpClient(transport)
 
780
        elif split_url.scheme in ('file', ):
 
781
            client = dulwich.client.LocalGitClient()
 
782
        else:
 
783
            raise NotBranchError(transport.base)
 
784
        if not _found:
 
785
            pass  # TODO(jelmer): Actually probe for something
 
786
        return RemoteGitDir(transport, self, client, split_url.path)
 
787
 
 
788
    def get_format_description(self):
 
789
        return "Remote Git Repository"
 
790
 
 
791
    def initialize_on_transport(self, transport):
 
792
        raise UninitializableFormat(self)
 
793
 
 
794
    def supports_transport(self, transport):
 
795
        try:
 
796
            external_url = transport.external_url()
 
797
        except InProcessTransport:
 
798
            raise NotBranchError(path=transport.base)
 
799
        return (external_url.startswith("http:")
 
800
                or external_url.startswith("https:")
 
801
                or external_url.startswith("git+")
 
802
                or external_url.startswith("git:"))
 
803
 
 
804
 
 
805
class GitRemoteRevisionTree(RevisionTree):
 
806
 
 
807
    def archive(self, format, name, root=None, subdir=None, force_mtime=None):
 
808
        """Create an archive of this tree.
 
809
 
 
810
        :param format: Format name (e.g. 'tar')
 
811
        :param name: target file name
 
812
        :param root: Root directory name (or None)
 
813
        :param subdir: Subdirectory to export (or None)
 
814
        :return: Iterator over archive chunks
 
815
        """
 
816
        commit = self._repository.lookup_bzr_revision_id(
 
817
            self.get_revision_id())[0]
 
818
        f = tempfile.SpooledTemporaryFile()
 
819
        # git-upload-archive(1) generaly only supports refs. So let's see if we
 
820
        # can find one.
 
821
        reverse_refs = {
 
822
            v: k for (k, v) in
 
823
            self._repository.controldir.get_refs_container().as_dict().items()}
 
824
        try:
 
825
            committish = reverse_refs[commit]
 
826
        except KeyError:
 
827
            # No? Maybe the user has uploadArchive.allowUnreachable enabled.
 
828
            # Let's hope for the best.
 
829
            committish = commit
 
830
        self._repository.archive(
 
831
            format, committish, f.write,
 
832
            subdirs=([subdir] if subdir else None),
 
833
            prefix=(root + '/') if root else '')
 
834
        f.seek(0)
 
835
        return osutils.file_iterator(f)
 
836
 
 
837
    def is_versioned(self, path):
 
838
        raise GitSmartRemoteNotSupported(self.is_versioned, self)
 
839
 
 
840
    def has_filename(self, path):
 
841
        raise GitSmartRemoteNotSupported(self.has_filename, self)
 
842
 
 
843
    def get_file_text(self, path):
 
844
        raise GitSmartRemoteNotSupported(self.get_file_text, self)
 
845
 
 
846
    def list_files(self, include_root=False, from_dir=None, recursive=True):
 
847
        raise GitSmartRemoteNotSupported(self.list_files, self)
 
848
 
101
849
 
102
850
class RemoteGitRepository(GitRepository):
103
851
 
104
 
    def __init__(self, gitdir, lockfiles):
105
 
        GitRepository.__init__(self, gitdir, lockfiles)
106
 
 
107
 
    def fetch_pack(self, determine_wants, graph_walker, pack_data, 
 
852
    supports_random_access = False
 
853
 
 
854
    @property
 
855
    def user_url(self):
 
856
        return self.control_url
 
857
 
 
858
    def get_parent_map(self, revids):
 
859
        raise GitSmartRemoteNotSupported(self.get_parent_map, self)
 
860
 
 
861
    def archive(self, *args, **kwargs):
 
862
        return self.controldir.archive(*args, **kwargs)
 
863
 
 
864
    def fetch_pack(self, determine_wants, graph_walker, pack_data,
108
865
                   progress=None):
109
 
        self._transport.fetch_pack(determine_wants, graph_walker, pack_data, 
110
 
            progress)
 
866
        return self.controldir.fetch_pack(
 
867
            determine_wants, graph_walker, pack_data, progress)
 
868
 
 
869
    def send_pack(self, get_changed_refs, generate_pack_data):
 
870
        return self.controldir.send_pack(get_changed_refs, generate_pack_data)
 
871
 
 
872
    def fetch_objects(self, determine_wants, graph_walker, resolve_ext_ref,
 
873
                      progress=None):
 
874
        fd, path = tempfile.mkstemp(suffix=".pack")
 
875
        try:
 
876
            self.fetch_pack(determine_wants, graph_walker,
 
877
                            lambda x: os.write(fd, x), progress)
 
878
        finally:
 
879
            os.close(fd)
 
880
        if os.path.getsize(path) == 0:
 
881
            return EmptyObjectStoreIterator()
 
882
        return TemporaryPackIterator(path[:-len(".pack")], resolve_ext_ref)
 
883
 
 
884
    def lookup_bzr_revision_id(self, bzr_revid, mapping=None):
 
885
        # This won't work for any round-tripped bzr revisions, but it's a
 
886
        # start..
 
887
        try:
 
888
            return mapping_registry.revision_id_bzr_to_foreign(bzr_revid)
 
889
        except InvalidRevisionId:
 
890
            raise NoSuchRevision(self, bzr_revid)
 
891
 
 
892
    def lookup_foreign_revision_id(self, foreign_revid, mapping=None):
 
893
        """Lookup a revision id.
 
894
 
 
895
        """
 
896
        if mapping is None:
 
897
            mapping = self.get_mapping()
 
898
        # Not really an easy way to parse foreign revids here..
 
899
        return mapping.revision_id_foreign_to_bzr(foreign_revid)
 
900
 
 
901
    def revision_tree(self, revid):
 
902
        return GitRemoteRevisionTree(self, revid)
 
903
 
 
904
    def get_revisions(self, revids):
 
905
        raise GitSmartRemoteNotSupported(self.get_revisions, self)
 
906
 
 
907
    def has_revisions(self, revids):
 
908
        raise GitSmartRemoteNotSupported(self.get_revisions, self)
 
909
 
 
910
 
 
911
class RemoteGitTagDict(GitTags):
 
912
 
 
913
    def set_tag(self, name, revid):
 
914
        sha = self.branch.lookup_bzr_revision_id(revid)[0]
 
915
        self._set_ref(name, sha)
 
916
 
 
917
    def delete_tag(self, name):
 
918
        self._set_ref(name, dulwich.client.ZERO_SHA)
 
919
 
 
920
    def _set_ref(self, name, sha):
 
921
        ref = tag_name_to_ref(name)
 
922
 
 
923
        def get_changed_refs(old_refs):
 
924
            ret = {}
 
925
            if sha == dulwich.client.ZERO_SHA and ref not in old_refs:
 
926
                raise NoSuchTag(name)
 
927
            ret[ref] = sha
 
928
            return ret
 
929
 
 
930
        def generate_pack_data(have, want, ofs_delta=False):
 
931
            return pack_objects_to_data([])
 
932
        self.repository.send_pack(get_changed_refs, generate_pack_data)
111
933
 
112
934
 
113
935
class RemoteGitBranch(GitBranch):
114
936
 
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)
 
937
    def __init__(self, controldir, repository, name):
 
938
        self._sha = None
 
939
        super(RemoteGitBranch, self).__init__(controldir, repository, name,
 
940
                                              RemoteGitBranchFormat())
 
941
 
 
942
    def last_revision_info(self):
 
943
        raise GitSmartRemoteNotSupported(self.last_revision_info, self)
 
944
 
 
945
    @property
 
946
    def user_url(self):
 
947
        return self.control_url
 
948
 
 
949
    @property
 
950
    def control_url(self):
 
951
        return self.base
 
952
 
 
953
    def revision_id_to_revno(self, revision_id):
 
954
        raise GitSmartRemoteNotSupported(self.revision_id_to_revno, self)
121
955
 
122
956
    def last_revision(self):
123
 
        return self.mapping.revision_id_foreign_to_bzr(self._ref)
124
 
 
 
957
        return self.lookup_foreign_revision_id(self.head)
 
958
 
 
959
    @property
 
960
    def head(self):
 
961
        if self._sha is not None:
 
962
            return self._sha
 
963
        refs = self.controldir.get_refs_container()
 
964
        name = branch_name_to_ref(self.name)
 
965
        try:
 
966
            self._sha = refs[name]
 
967
        except KeyError:
 
968
            raise NoSuchRef(name, self.repository.user_url, refs)
 
969
        return self._sha
 
970
 
 
971
    def _synchronize_history(self, destination, revision_id):
 
972
        """See Branch._synchronize_history()."""
 
973
        if revision_id is None:
 
974
            revision_id = self.last_revision()
 
975
        destination.generate_revision_history(revision_id)
 
976
 
 
977
    def _get_parent_location(self):
 
978
        return None
 
979
 
 
980
    def get_push_location(self):
 
981
        return None
 
982
 
 
983
    def set_push_location(self, url):
 
984
        pass
 
985
 
 
986
    def _iter_tag_refs(self):
 
987
        """Iterate over the tag refs.
 
988
 
 
989
        :param refs: Refs dictionary (name -> git sha1)
 
990
        :return: iterator over (ref_name, tag_name, peeled_sha1, unpeeled_sha1)
 
991
        """
 
992
        refs = self.controldir.get_refs_container()
 
993
        for ref_name, unpeeled in refs.as_dict().items():
 
994
            try:
 
995
                tag_name = ref_to_tag_name(ref_name)
 
996
            except (ValueError, UnicodeDecodeError):
 
997
                continue
 
998
            peeled = refs.get_peeled(ref_name)
 
999
            if peeled is None:
 
1000
                # Let's just hope it's a commit
 
1001
                peeled = unpeeled
 
1002
            if not isinstance(tag_name, str):
 
1003
                raise TypeError(tag_name)
 
1004
            yield (ref_name, tag_name, peeled, unpeeled)
 
1005
 
 
1006
    def set_last_revision_info(self, revno, revid):
 
1007
        self.generate_revision_history(revid)
 
1008
 
 
1009
    def generate_revision_history(self, revision_id, last_rev=None,
 
1010
                                  other_branch=None):
 
1011
        sha = self.lookup_bzr_revision_id(revision_id)[0]
 
1012
        def get_changed_refs(old_refs):
 
1013
            return {self.ref: sha}
 
1014
        def generate_pack_data(have, want, ofs_delta=False):
 
1015
            return pack_objects_to_data([])
 
1016
        self.repository.send_pack(get_changed_refs, generate_pack_data)
 
1017
        self._sha = sha
 
1018
 
 
1019
 
 
1020
def remote_refs_dict_to_container(refs_dict, symrefs_dict={}):
 
1021
    base = {}
 
1022
    peeled = {}
 
1023
    for k, v in refs_dict.items():
 
1024
        if is_peeled(k):
 
1025
            peeled[k[:-3]] = v
 
1026
        else:
 
1027
            base[k] = v
 
1028
    for name, target in symrefs_dict.items():
 
1029
        base[name] = SYMREF + target
 
1030
    ret = DictRefsContainer(base)
 
1031
    ret._peeled = peeled
 
1032
    return ret
 
1033
 
 
1034
 
 
1035
def update_refs_container(container, refs_dict):
 
1036
    peeled = {}
 
1037
    base = {}
 
1038
    for k, v in refs_dict.items():
 
1039
        if is_peeled(k):
 
1040
            peeled[k[:-3]] = v
 
1041
        else:
 
1042
            base[k] = v
 
1043
    container._peeled = peeled
 
1044
    container._refs.update(base)