git.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. import tempfile
  2. import re
  3. import os.path
  4. from pip.util import call_subprocess
  5. from pip.util import display_path, rmtree
  6. from pip.vcs import vcs, VersionControl
  7. from pip.log import logger
  8. from pip.backwardcompat import url2pathname, urlparse
  9. urlsplit = urlparse.urlsplit
  10. urlunsplit = urlparse.urlunsplit
  11. class Git(VersionControl):
  12. name = 'git'
  13. dirname = '.git'
  14. repo_name = 'clone'
  15. schemes = ('git', 'git+http', 'git+https', 'git+ssh', 'git+git', 'git+file')
  16. bundle_file = 'git-clone.txt'
  17. guide = ('# This was a Git repo; to make it a repo again run:\n'
  18. 'git init\ngit remote add origin %(url)s -f\ngit checkout %(rev)s\n')
  19. def __init__(self, url=None, *args, **kwargs):
  20. # Works around an apparent Git bug
  21. # (see http://article.gmane.org/gmane.comp.version-control.git/146500)
  22. if url:
  23. scheme, netloc, path, query, fragment = urlsplit(url)
  24. if scheme.endswith('file'):
  25. initial_slashes = path[:-len(path.lstrip('/'))]
  26. newpath = initial_slashes + url2pathname(path).replace('\\', '/').lstrip('/')
  27. url = urlunsplit((scheme, netloc, newpath, query, fragment))
  28. after_plus = scheme.find('+') + 1
  29. url = scheme[:after_plus] + urlunsplit((scheme[after_plus:], netloc, newpath, query, fragment))
  30. super(Git, self).__init__(url, *args, **kwargs)
  31. def parse_vcs_bundle_file(self, content):
  32. url = rev = None
  33. for line in content.splitlines():
  34. if not line.strip() or line.strip().startswith('#'):
  35. continue
  36. url_match = re.search(r'git\s*remote\s*add\s*origin(.*)\s*-f', line)
  37. if url_match:
  38. url = url_match.group(1).strip()
  39. rev_match = re.search(r'^git\s*checkout\s*-q\s*(.*)\s*', line)
  40. if rev_match:
  41. rev = rev_match.group(1).strip()
  42. if url and rev:
  43. return url, rev
  44. return None, None
  45. def export(self, location):
  46. """Export the Git repository at the url to the destination location"""
  47. temp_dir = tempfile.mkdtemp('-export', 'pip-')
  48. self.unpack(temp_dir)
  49. try:
  50. if not location.endswith('/'):
  51. location = location + '/'
  52. call_subprocess(
  53. [self.cmd, 'checkout-index', '-a', '-f', '--prefix', location],
  54. filter_stdout=self._filter, show_stdout=False, cwd=temp_dir)
  55. finally:
  56. rmtree(temp_dir)
  57. def check_rev_options(self, rev, dest, rev_options):
  58. """Check the revision options before checkout to compensate that tags
  59. and branches may need origin/ as a prefix.
  60. Returns the SHA1 of the branch or tag if found.
  61. """
  62. revisions = self.get_refs(dest)
  63. origin_rev = 'origin/%s' % rev
  64. if origin_rev in revisions:
  65. # remote branch
  66. return [revisions[origin_rev]]
  67. elif rev in revisions:
  68. # a local tag or branch name
  69. return [revisions[rev]]
  70. else:
  71. logger.warn("Could not find a tag or branch '%s', assuming commit." % rev)
  72. return rev_options
  73. def switch(self, dest, url, rev_options):
  74. call_subprocess(
  75. [self.cmd, 'config', 'remote.origin.url', url], cwd=dest)
  76. call_subprocess(
  77. [self.cmd, 'checkout', '-q'] + rev_options, cwd=dest)
  78. self.update_submodules(dest)
  79. def update(self, dest, rev_options):
  80. # First fetch changes from the default remote
  81. call_subprocess([self.cmd, 'fetch', '-q'], cwd=dest)
  82. # Then reset to wanted revision (maby even origin/master)
  83. if rev_options:
  84. rev_options = self.check_rev_options(rev_options[0], dest, rev_options)
  85. call_subprocess([self.cmd, 'reset', '--hard', '-q'] + rev_options, cwd=dest)
  86. #: update submodules
  87. self.update_submodules(dest)
  88. def obtain(self, dest):
  89. url, rev = self.get_url_rev()
  90. if rev:
  91. rev_options = [rev]
  92. rev_display = ' (to %s)' % rev
  93. else:
  94. rev_options = ['origin/master']
  95. rev_display = ''
  96. if self.check_destination(dest, url, rev_options, rev_display):
  97. logger.notify('Cloning %s%s to %s' % (url, rev_display, display_path(dest)))
  98. call_subprocess([self.cmd, 'clone', '-q', url, dest])
  99. #: repo may contain submodules
  100. self.update_submodules(dest)
  101. if rev:
  102. rev_options = self.check_rev_options(rev, dest, rev_options)
  103. # Only do a checkout if rev_options differs from HEAD
  104. if not self.get_revision(dest).startswith(rev_options[0]):
  105. call_subprocess([self.cmd, 'checkout', '-q'] + rev_options, cwd=dest)
  106. def get_url(self, location):
  107. url = call_subprocess(
  108. [self.cmd, 'config', 'remote.origin.url'],
  109. show_stdout=False, cwd=location)
  110. return url.strip()
  111. def get_revision(self, location):
  112. current_rev = call_subprocess(
  113. [self.cmd, 'rev-parse', 'HEAD'], show_stdout=False, cwd=location)
  114. return current_rev.strip()
  115. def get_refs(self, location):
  116. """Return map of named refs (branches or tags) to commit hashes."""
  117. output = call_subprocess([self.cmd, 'show-ref'],
  118. show_stdout=False, cwd=location)
  119. rv = {}
  120. for line in output.strip().splitlines():
  121. commit, ref = line.split(' ', 1)
  122. ref = ref.strip()
  123. ref_name = None
  124. if ref.startswith('refs/remotes/'):
  125. ref_name = ref[len('refs/remotes/'):]
  126. elif ref.startswith('refs/heads/'):
  127. ref_name = ref[len('refs/heads/'):]
  128. elif ref.startswith('refs/tags/'):
  129. ref_name = ref[len('refs/tags/'):]
  130. if ref_name is not None:
  131. rv[ref_name] = commit.strip()
  132. return rv
  133. def get_src_requirement(self, dist, location, find_tags):
  134. repo = self.get_url(location)
  135. if not repo.lower().startswith('git:'):
  136. repo = 'git+' + repo
  137. egg_project_name = dist.egg_name().split('-', 1)[0]
  138. if not repo:
  139. return None
  140. current_rev = self.get_revision(location)
  141. refs = self.get_refs(location)
  142. # refs maps names to commit hashes; we need the inverse
  143. # if multiple names map to a single commit, this arbitrarily picks one
  144. names_by_commit = dict((commit, ref) for ref, commit in refs.items())
  145. if current_rev in names_by_commit:
  146. # It's a tag
  147. full_egg_name = '%s-%s' % (egg_project_name, names_by_commit[current_rev])
  148. else:
  149. full_egg_name = '%s-dev' % egg_project_name
  150. return '%s@%s#egg=%s' % (repo, current_rev, full_egg_name)
  151. def get_url_rev(self):
  152. """
  153. Prefixes stub URLs like 'user@hostname:user/repo.git' with 'ssh://'.
  154. That's required because although they use SSH they sometimes doesn't
  155. work with a ssh:// scheme (e.g. Github). But we need a scheme for
  156. parsing. Hence we remove it again afterwards and return it as a stub.
  157. """
  158. if not '://' in self.url:
  159. assert not 'file:' in self.url
  160. self.url = self.url.replace('git+', 'git+ssh://')
  161. url, rev = super(Git, self).get_url_rev()
  162. url = url.replace('ssh://', '')
  163. else:
  164. url, rev = super(Git, self).get_url_rev()
  165. return url, rev
  166. def update_submodules(self, location):
  167. if not os.path.exists(os.path.join(location, '.gitmodules')):
  168. return
  169. call_subprocess([self.cmd, 'submodule', 'update', '--init', '--recursive', '-q'],
  170. cwd=location)
  171. vcs.register(Git)