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