1# Copyright (C) 2009-2018 Jelmer Vernooij <jelmer@jelmer.uk>
2# Copyright (C) 2006-2009 Canonical Ltd
3
4# Authors: Robert Collins <robert.collins@canonical.com>
5#          Jelmer Vernooij <jelmer@jelmer.uk>
6#          John Carr <john.carr@unrouted.co.uk>
7#
8# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation; either version 2 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with this program; if not, write to the Free Software
20# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21
22
23"""A GIT branch and repository format implementation for bzr."""
24
25import os
26import sys
27
28dulwich_minimum_version = (0, 19, 11)
29
30from .. import (  # noqa: F401
31    __version__ as breezy_version,
32    errors as brz_errors,
33    trace,
34    urlutils,
35    version_info,
36    )
37
38from ..controldir import (
39    ControlDirFormat,
40    Prober,
41    format_registry,
42    network_format_registry as controldir_network_format_registry,
43    )
44
45from ..transport import (
46    register_lazy_transport,
47    register_transport_proto,
48    transport_server_registry,
49    )
50from ..commands import (
51    plugin_cmds,
52    )
53
54
55if getattr(sys, "frozen", None):
56    # allow import additional libs from ./_lib for bzr.exe only
57    sys.path.append(os.path.normpath(
58        os.path.join(os.path.dirname(__file__), '_lib')))
59
60
61def import_dulwich():
62    try:
63        from dulwich import __version__ as dulwich_version
64    except ImportError:
65        raise brz_errors.DependencyNotPresent(
66            "dulwich",
67            "bzr-git: Please install dulwich, https://www.dulwich.io/")
68    else:
69        if dulwich_version < dulwich_minimum_version:
70            raise brz_errors.DependencyNotPresent(
71                "dulwich",
72                "bzr-git: Dulwich is too old; at least %d.%d.%d is required" %
73                dulwich_minimum_version)
74
75
76_versions_checked = False
77
78
79def lazy_check_versions():
80    global _versions_checked
81    if _versions_checked:
82        return
83    import_dulwich()
84    _versions_checked = True
85
86
87format_registry.register_lazy(
88    'git', __name__ + ".dir", "LocalGitControlDirFormat",
89    help='GIT repository.', native=False, experimental=False)
90
91format_registry.register_lazy(
92    'git-bare', __name__ + ".dir", "BareLocalGitControlDirFormat",
93    help='Bare GIT repository (no working tree).', native=False,
94    experimental=False)
95
96from ..revisionspec import (RevisionSpec_dwim, revspec_registry)
97revspec_registry.register_lazy("git:", __name__ + ".revspec",
98                               "RevisionSpec_git")
99RevisionSpec_dwim.append_possible_lazy_revspec(
100    __name__ + ".revspec", "RevisionSpec_git")
101
102
103class LocalGitProber(Prober):
104
105    @classmethod
106    def priority(klass, transport):
107        return 10
108
109    def probe_transport(self, transport):
110        try:
111            external_url = transport.external_url()
112        except brz_errors.InProcessTransport:
113            raise brz_errors.NotBranchError(path=transport.base)
114        if (external_url.startswith("http:") or
115                external_url.startswith("https:")):
116            # Already handled by RemoteGitProber
117            raise brz_errors.NotBranchError(path=transport.base)
118        if urlutils.split(transport.base)[1] == ".git":
119            raise brz_errors.NotBranchError(path=transport.base)
120        if not transport.has_any(['objects', '.git/objects', '.git']):
121            raise brz_errors.NotBranchError(path=transport.base)
122        lazy_check_versions()
123        from .dir import (
124            BareLocalGitControlDirFormat,
125            LocalGitControlDirFormat,
126            )
127        if transport.has_any(['.git/objects', '.git']):
128            return LocalGitControlDirFormat()
129        if transport.has('info') and transport.has('objects'):
130            return BareLocalGitControlDirFormat()
131        raise brz_errors.NotBranchError(path=transport.base)
132
133    @classmethod
134    def known_formats(cls):
135        from .dir import (
136            BareLocalGitControlDirFormat,
137            LocalGitControlDirFormat,
138            )
139        return [BareLocalGitControlDirFormat(), LocalGitControlDirFormat()]
140
141
142def user_agent_for_github():
143    # GitHub requires we lie. https://github.com/dulwich/dulwich/issues/562
144    return "git/Breezy/%s" % breezy_version
145
146
147def is_github_url(url):
148    (scheme, user, password, host, port,
149     path) = urlutils.parse_url(url)
150    return host in ("github.com", "gopkg.in")
151
152
153class RemoteGitProber(Prober):
154
155    @classmethod
156    def priority(klass, transport):
157        # This is a surprisingly good heuristic to determine whether this
158        # prober is more likely to succeed than the Bazaar one.
159        if 'git' in transport.base:
160            return -15
161        return -10
162
163    def probe_http_transport(self, transport):
164        # This function intentionally doesn't use any of the support code under
165        # breezy.git, since it's called for every repository that's
166        # accessed over HTTP, whether it's Git, Bzr or something else.
167        # Importing Dulwich and the other support code adds unnecessray slowdowns.
168        base_url = urlutils.strip_segment_parameters(transport.external_url())
169        url = urlutils.URL.from_string(base_url)
170        url.user = url.quoted_user = None
171        url.password = url.quoted_password = None
172        host = url.host
173        url = urlutils.join(str(url), "info/refs") + "?service=git-upload-pack"
174        headers = {"Content-Type": "application/x-git-upload-pack-request",
175                   "Accept": "application/x-git-upload-pack-result",
176                   }
177        if is_github_url(url):
178            # GitHub requires we lie.
179            # https://github.com/dulwich/dulwich/issues/562
180            headers["User-Agent"] = user_agent_for_github()
181        resp = transport.request('GET', url, headers=headers)
182        if resp.status in (404, 405):
183            raise brz_errors.NotBranchError(transport.base)
184        elif resp.status == 400 and resp.reason == 'no such method: info':
185            # hgweb :(
186            raise brz_errors.NotBranchError(transport.base)
187        elif resp.status != 200:
188            raise brz_errors.UnexpectedHttpStatus(url, resp.status)
189
190        ct = resp.getheader("Content-Type")
191        if ct and ct.startswith("application/x-git"):
192            from .remote import RemoteGitControlDirFormat
193            return RemoteGitControlDirFormat()
194        elif not ct:
195            from .dir import (
196                BareLocalGitControlDirFormat,
197                )
198            ret = BareLocalGitControlDirFormat()
199            ret._refs_text = resp.read()
200            return ret
201        raise brz_errors.NotBranchError(transport.base)
202
203    def probe_transport(self, transport):
204        try:
205            external_url = transport.external_url()
206        except brz_errors.InProcessTransport:
207            raise brz_errors.NotBranchError(path=transport.base)
208
209        if (external_url.startswith("http:") or
210                external_url.startswith("https:")):
211            return self.probe_http_transport(transport)
212
213        if (not external_url.startswith("git://") and
214                not external_url.startswith("git+")):
215            raise brz_errors.NotBranchError(transport.base)
216
217        # little ugly, but works
218        from .remote import (
219            GitSmartTransport,
220            RemoteGitControlDirFormat,
221            )
222        if isinstance(transport, GitSmartTransport):
223            return RemoteGitControlDirFormat()
224        raise brz_errors.NotBranchError(path=transport.base)
225
226    @classmethod
227    def known_formats(cls):
228        from .remote import RemoteGitControlDirFormat
229        return [RemoteGitControlDirFormat()]
230
231
232ControlDirFormat.register_prober(LocalGitProber)
233ControlDirFormat.register_prober(RemoteGitProber)
234
235register_transport_proto(
236    'git://', help="Access using the Git smart server protocol.")
237register_transport_proto(
238    'git+ssh://',
239    help="Access using the Git smart server protocol over SSH.")
240
241register_lazy_transport("git://", __name__ + '.remote',
242                        'TCPGitSmartTransport')
243register_lazy_transport("git+ssh://", __name__ + '.remote',
244                        'SSHGitSmartTransport')
245
246
247plugin_cmds.register_lazy("cmd_git_import", [], __name__ + ".commands")
248plugin_cmds.register_lazy("cmd_git_object", ["git-objects", "git-cat"],
249                          __name__ + ".commands")
250plugin_cmds.register_lazy("cmd_git_refs", [], __name__ + ".commands")
251plugin_cmds.register_lazy("cmd_git_apply", [], __name__ + ".commands")
252plugin_cmds.register_lazy("cmd_git_push_pristine_tar_deltas",
253                          ['git-push-pristine-tar', 'git-push-pristine'],
254                          __name__ + ".commands")
255
256
257def extract_git_foreign_revid(rev):
258    try:
259        foreign_revid = rev.foreign_revid
260    except AttributeError:
261        from .mapping import mapping_registry
262        foreign_revid, mapping = \
263            mapping_registry.parse_revision_id(rev.revision_id)
264        return foreign_revid
265    else:
266        from .mapping import foreign_vcs_git
267        if rev.mapping.vcs == foreign_vcs_git:
268            return foreign_revid
269        else:
270            raise brz_errors.InvalidRevisionId(rev.revision_id, None)
271
272
273def update_stanza(rev, stanza):
274    try:
275        git_commit = extract_git_foreign_revid(rev)
276    except brz_errors.InvalidRevisionId:
277        pass
278    else:
279        stanza.add("git-commit", git_commit)
280
281
282from ..hooks import install_lazy_named_hook
283install_lazy_named_hook(
284    "breezy.version_info_formats.format_rio",
285    "RioVersionInfoBuilder.hooks", "revision", update_stanza,
286    "git commits")
287
288transport_server_registry.register_lazy(
289    'git', __name__ + '.server', 'serve_git',
290    'Git Smart server protocol over TCP. (default port: 9418)')
291
292transport_server_registry.register_lazy(
293    'git-receive-pack', __name__ + '.server',
294    'serve_git_receive_pack',
295    help='Git Smart server receive pack command. (inetd mode only)')
296transport_server_registry.register_lazy(
297    'git-upload-pack', __name__ + 'git.server',
298    'serve_git_upload_pack',
299    help='Git Smart server upload pack command. (inetd mode only)')
300
301from ..repository import (
302    format_registry as repository_format_registry,
303    network_format_registry as repository_network_format_registry,
304    )
305repository_network_format_registry.register_lazy(
306    b'git', __name__ + '.repository', 'GitRepositoryFormat')
307
308register_extra_lazy_repository_format = getattr(repository_format_registry,
309                                                "register_extra_lazy")
310register_extra_lazy_repository_format(__name__ + '.repository',
311                                      'GitRepositoryFormat')
312
313from ..branch import (
314    network_format_registry as branch_network_format_registry,
315    )
316branch_network_format_registry.register_lazy(
317    b'git', __name__ + '.branch', 'LocalGitBranchFormat')
318
319
320from ..branch import (
321    format_registry as branch_format_registry,
322    )
323branch_format_registry.register_extra_lazy(
324    __name__ + '.branch',
325    'LocalGitBranchFormat',
326    )
327branch_format_registry.register_extra_lazy(
328    __name__ + '.remote',
329    'RemoteGitBranchFormat',
330    )
331
332
333from ..workingtree import (
334    format_registry as workingtree_format_registry,
335    )
336workingtree_format_registry.register_extra_lazy(
337    __name__ + '.workingtree',
338    'GitWorkingTreeFormat',
339    )
340
341controldir_network_format_registry.register_lazy(
342    b'git', __name__ + ".dir", "GitControlDirFormat")
343
344
345from ..diff import format_registry as diff_format_registry
346diff_format_registry.register_lazy(
347    'git', __name__ + '.send',
348    'GitDiffTree', 'Git am-style diff format')
349
350from ..send import (
351    format_registry as send_format_registry,
352    )
353send_format_registry.register_lazy('git', __name__ + '.send',
354                                   'send_git', 'Git am-style diff format')
355
356from ..directory_service import directories
357directories.register_lazy('github:', __name__ + '.directory',
358                          'GitHubDirectory',
359                          'GitHub directory.')
360directories.register_lazy('git@github.com:', __name__ + '.directory',
361                          'GitHubDirectory',
362                          'GitHub directory.')
363
364from ..help_topics import (
365    topic_registry,
366    )
367topic_registry.register_lazy(
368    'git', __name__ + '.help', 'help_git', 'Using Bazaar with Git')
369
370from ..foreign import (
371    foreign_vcs_registry,
372    )
373foreign_vcs_registry.register_lazy(
374    "git", __name__ + ".mapping", "foreign_vcs_git", "Stupid content tracker")
375
376
377def update_git_cache(repository, revid):
378    """Update the git cache after a local commit."""
379    if getattr(repository, "_git", None) is not None:
380        return  # No need to update cache for git repositories
381
382    if not repository.control_transport.has("git"):
383        return  # No existing cache, don't bother updating
384    try:
385        lazy_check_versions()
386    except brz_errors.DependencyNotPresent as e:
387        # dulwich is probably missing. silently ignore
388        trace.mutter("not updating git map for %r: %s",
389                     repository, e)
390
391    from .object_store import BazaarObjectStore
392    store = BazaarObjectStore(repository)
393    with store.lock_write():
394        try:
395            parent_revisions = set(repository.get_parent_map([revid])[revid])
396        except KeyError:
397            # Isn't this a bit odd - how can a revision that was just committed
398            # be missing?
399            return
400        missing_revisions = store._missing_revisions(parent_revisions)
401        if not missing_revisions:
402            store._cache.idmap.start_write_group()
403            try:
404                # Only update if the cache was up to date previously
405                store._update_sha_map_revision(revid)
406            except BaseException:
407                store._cache.idmap.abort_write_group()
408                raise
409            else:
410                store._cache.idmap.commit_write_group()
411
412
413def post_commit_update_cache(local_branch, master_branch, old_revno, old_revid,
414                             new_revno, new_revid):
415    if local_branch is not None:
416        update_git_cache(local_branch.repository, new_revid)
417    update_git_cache(master_branch.repository, new_revid)
418
419
420def loggerhead_git_hook(branch_app, environ):
421    branch = branch_app.branch
422    config_stack = branch.get_config_stack()
423    if not config_stack.get('git.http'):
424        return None
425    from .server import git_http_hook
426    return git_http_hook(branch, environ['REQUEST_METHOD'],
427                         environ['PATH_INFO'])
428
429
430install_lazy_named_hook("breezy.branch",
431                        "Branch.hooks", "post_commit",
432                        post_commit_update_cache, "git cache")
433install_lazy_named_hook("breezy.plugins.loggerhead.apps.branch",
434                        "BranchWSGIApp.hooks", "controller",
435                        loggerhead_git_hook, "git support")
436
437
438from ..config import (
439    option_registry,
440    Option,
441    bool_from_store,
442    )
443
444option_registry.register(
445    Option('git.http',
446           default=None, from_unicode=bool_from_store, invalid='warning',
447           help='''\
448Allow fetching of Git packs over HTTP.
449
450This enables support for fetching Git packs over HTTP in Loggerhead.
451'''))
452
453
454def test_suite():
455    from . import tests
456    return tests.test_suite()
457