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