PageRenderTime 54ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 0ms

/Lib/site-packages/pip/_internal/vcs/git.py

https://gitlab.com/phongphans61/machine-learning-tictactoe
Python | 450 lines | 424 code | 14 blank | 12 comment | 7 complexity | 9913e51a03a8b3e442ac6d8ab4bf6f2e MD5 | raw file
  1. import logging
  2. import os.path
  3. import re
  4. import urllib.parse
  5. import urllib.request
  6. from typing import List, Optional, Tuple
  7. from pip._vendor.packaging.version import _BaseVersion
  8. from pip._vendor.packaging.version import parse as parse_version
  9. from pip._internal.exceptions import BadCommand, InstallationError
  10. from pip._internal.utils.misc import HiddenText, display_path, hide_url
  11. from pip._internal.utils.subprocess import make_command
  12. from pip._internal.vcs.versioncontrol import (
  13. AuthInfo,
  14. RemoteNotFoundError,
  15. RevOptions,
  16. VersionControl,
  17. find_path_to_setup_from_repo_root,
  18. vcs,
  19. )
  20. urlsplit = urllib.parse.urlsplit
  21. urlunsplit = urllib.parse.urlunsplit
  22. logger = logging.getLogger(__name__)
  23. HASH_REGEX = re.compile('^[a-fA-F0-9]{40}$')
  24. def looks_like_hash(sha):
  25. # type: (str) -> bool
  26. return bool(HASH_REGEX.match(sha))
  27. class Git(VersionControl):
  28. name = 'git'
  29. dirname = '.git'
  30. repo_name = 'clone'
  31. schemes = (
  32. 'git+http', 'git+https', 'git+ssh', 'git+git', 'git+file',
  33. )
  34. # Prevent the user's environment variables from interfering with pip:
  35. # https://github.com/pypa/pip/issues/1130
  36. unset_environ = ('GIT_DIR', 'GIT_WORK_TREE')
  37. default_arg_rev = 'HEAD'
  38. @staticmethod
  39. def get_base_rev_args(rev):
  40. # type: (str) -> List[str]
  41. return [rev]
  42. def is_immutable_rev_checkout(self, url, dest):
  43. # type: (str, str) -> bool
  44. _, rev_options = self.get_url_rev_options(hide_url(url))
  45. if not rev_options.rev:
  46. return False
  47. if not self.is_commit_id_equal(dest, rev_options.rev):
  48. # the current commit is different from rev,
  49. # which means rev was something else than a commit hash
  50. return False
  51. # return False in the rare case rev is both a commit hash
  52. # and a tag or a branch; we don't want to cache in that case
  53. # because that branch/tag could point to something else in the future
  54. is_tag_or_branch = bool(
  55. self.get_revision_sha(dest, rev_options.rev)[0]
  56. )
  57. return not is_tag_or_branch
  58. def get_git_version(self):
  59. # type: () -> _BaseVersion
  60. VERSION_PFX = 'git version '
  61. version = self.run_command(
  62. ['version'], show_stdout=False, stdout_only=True
  63. )
  64. if version.startswith(VERSION_PFX):
  65. version = version[len(VERSION_PFX):].split()[0]
  66. else:
  67. version = ''
  68. # get first 3 positions of the git version because
  69. # on windows it is x.y.z.windows.t, and this parses as
  70. # LegacyVersion which always smaller than a Version.
  71. version = '.'.join(version.split('.')[:3])
  72. return parse_version(version)
  73. @classmethod
  74. def get_current_branch(cls, location):
  75. # type: (str) -> Optional[str]
  76. """
  77. Return the current branch, or None if HEAD isn't at a branch
  78. (e.g. detached HEAD).
  79. """
  80. # git-symbolic-ref exits with empty stdout if "HEAD" is a detached
  81. # HEAD rather than a symbolic ref. In addition, the -q causes the
  82. # command to exit with status code 1 instead of 128 in this case
  83. # and to suppress the message to stderr.
  84. args = ['symbolic-ref', '-q', 'HEAD']
  85. output = cls.run_command(
  86. args,
  87. extra_ok_returncodes=(1, ),
  88. show_stdout=False,
  89. stdout_only=True,
  90. cwd=location,
  91. )
  92. ref = output.strip()
  93. if ref.startswith('refs/heads/'):
  94. return ref[len('refs/heads/'):]
  95. return None
  96. @classmethod
  97. def get_revision_sha(cls, dest, rev):
  98. # type: (str, str) -> Tuple[Optional[str], bool]
  99. """
  100. Return (sha_or_none, is_branch), where sha_or_none is a commit hash
  101. if the revision names a remote branch or tag, otherwise None.
  102. Args:
  103. dest: the repository directory.
  104. rev: the revision name.
  105. """
  106. # Pass rev to pre-filter the list.
  107. output = cls.run_command(
  108. ['show-ref', rev],
  109. cwd=dest,
  110. show_stdout=False,
  111. stdout_only=True,
  112. on_returncode='ignore',
  113. )
  114. refs = {}
  115. # NOTE: We do not use splitlines here since that would split on other
  116. # unicode separators, which can be maliciously used to install a
  117. # different revision.
  118. for line in output.strip().split("\n"):
  119. line = line.rstrip("\r")
  120. if not line:
  121. continue
  122. try:
  123. ref_sha, ref_name = line.split(" ", maxsplit=2)
  124. except ValueError:
  125. # Include the offending line to simplify troubleshooting if
  126. # this error ever occurs.
  127. raise ValueError(f'unexpected show-ref line: {line!r}')
  128. refs[ref_name] = ref_sha
  129. branch_ref = f'refs/remotes/origin/{rev}'
  130. tag_ref = f'refs/tags/{rev}'
  131. sha = refs.get(branch_ref)
  132. if sha is not None:
  133. return (sha, True)
  134. sha = refs.get(tag_ref)
  135. return (sha, False)
  136. @classmethod
  137. def _should_fetch(cls, dest, rev):
  138. # type: (str, str) -> bool
  139. """
  140. Return true if rev is a ref or is a commit that we don't have locally.
  141. Branches and tags are not considered in this method because they are
  142. assumed to be always available locally (which is a normal outcome of
  143. ``git clone`` and ``git fetch --tags``).
  144. """
  145. if rev.startswith("refs/"):
  146. # Always fetch remote refs.
  147. return True
  148. if not looks_like_hash(rev):
  149. # Git fetch would fail with abbreviated commits.
  150. return False
  151. if cls.has_commit(dest, rev):
  152. # Don't fetch if we have the commit locally.
  153. return False
  154. return True
  155. @classmethod
  156. def resolve_revision(cls, dest, url, rev_options):
  157. # type: (str, HiddenText, RevOptions) -> RevOptions
  158. """
  159. Resolve a revision to a new RevOptions object with the SHA1 of the
  160. branch, tag, or ref if found.
  161. Args:
  162. rev_options: a RevOptions object.
  163. """
  164. rev = rev_options.arg_rev
  165. # The arg_rev property's implementation for Git ensures that the
  166. # rev return value is always non-None.
  167. assert rev is not None
  168. sha, is_branch = cls.get_revision_sha(dest, rev)
  169. if sha is not None:
  170. rev_options = rev_options.make_new(sha)
  171. rev_options.branch_name = rev if is_branch else None
  172. return rev_options
  173. # Do not show a warning for the common case of something that has
  174. # the form of a Git commit hash.
  175. if not looks_like_hash(rev):
  176. logger.warning(
  177. "Did not find branch or tag '%s', assuming revision or ref.",
  178. rev,
  179. )
  180. if not cls._should_fetch(dest, rev):
  181. return rev_options
  182. # fetch the requested revision
  183. cls.run_command(
  184. make_command('fetch', '-q', url, rev_options.to_args()),
  185. cwd=dest,
  186. )
  187. # Change the revision to the SHA of the ref we fetched
  188. sha = cls.get_revision(dest, rev='FETCH_HEAD')
  189. rev_options = rev_options.make_new(sha)
  190. return rev_options
  191. @classmethod
  192. def is_commit_id_equal(cls, dest, name):
  193. # type: (str, Optional[str]) -> bool
  194. """
  195. Return whether the current commit hash equals the given name.
  196. Args:
  197. dest: the repository directory.
  198. name: a string name.
  199. """
  200. if not name:
  201. # Then avoid an unnecessary subprocess call.
  202. return False
  203. return cls.get_revision(dest) == name
  204. def fetch_new(self, dest, url, rev_options):
  205. # type: (str, HiddenText, RevOptions) -> None
  206. rev_display = rev_options.to_display()
  207. logger.info('Cloning %s%s to %s', url, rev_display, display_path(dest))
  208. self.run_command(make_command('clone', '-q', url, dest))
  209. if rev_options.rev:
  210. # Then a specific revision was requested.
  211. rev_options = self.resolve_revision(dest, url, rev_options)
  212. branch_name = getattr(rev_options, 'branch_name', None)
  213. if branch_name is None:
  214. # Only do a checkout if the current commit id doesn't match
  215. # the requested revision.
  216. if not self.is_commit_id_equal(dest, rev_options.rev):
  217. cmd_args = make_command(
  218. 'checkout', '-q', rev_options.to_args(),
  219. )
  220. self.run_command(cmd_args, cwd=dest)
  221. elif self.get_current_branch(dest) != branch_name:
  222. # Then a specific branch was requested, and that branch
  223. # is not yet checked out.
  224. track_branch = f'origin/{branch_name}'
  225. cmd_args = [
  226. 'checkout', '-b', branch_name, '--track', track_branch,
  227. ]
  228. self.run_command(cmd_args, cwd=dest)
  229. #: repo may contain submodules
  230. self.update_submodules(dest)
  231. def switch(self, dest, url, rev_options):
  232. # type: (str, HiddenText, RevOptions) -> None
  233. self.run_command(
  234. make_command('config', 'remote.origin.url', url),
  235. cwd=dest,
  236. )
  237. cmd_args = make_command('checkout', '-q', rev_options.to_args())
  238. self.run_command(cmd_args, cwd=dest)
  239. self.update_submodules(dest)
  240. def update(self, dest, url, rev_options):
  241. # type: (str, HiddenText, RevOptions) -> None
  242. # First fetch changes from the default remote
  243. if self.get_git_version() >= parse_version('1.9.0'):
  244. # fetch tags in addition to everything else
  245. self.run_command(['fetch', '-q', '--tags'], cwd=dest)
  246. else:
  247. self.run_command(['fetch', '-q'], cwd=dest)
  248. # Then reset to wanted revision (maybe even origin/master)
  249. rev_options = self.resolve_revision(dest, url, rev_options)
  250. cmd_args = make_command('reset', '--hard', '-q', rev_options.to_args())
  251. self.run_command(cmd_args, cwd=dest)
  252. #: update submodules
  253. self.update_submodules(dest)
  254. @classmethod
  255. def get_remote_url(cls, location):
  256. # type: (str) -> str
  257. """
  258. Return URL of the first remote encountered.
  259. Raises RemoteNotFoundError if the repository does not have a remote
  260. url configured.
  261. """
  262. # We need to pass 1 for extra_ok_returncodes since the command
  263. # exits with return code 1 if there are no matching lines.
  264. stdout = cls.run_command(
  265. ['config', '--get-regexp', r'remote\..*\.url'],
  266. extra_ok_returncodes=(1, ),
  267. show_stdout=False,
  268. stdout_only=True,
  269. cwd=location,
  270. )
  271. remotes = stdout.splitlines()
  272. try:
  273. found_remote = remotes[0]
  274. except IndexError:
  275. raise RemoteNotFoundError
  276. for remote in remotes:
  277. if remote.startswith('remote.origin.url '):
  278. found_remote = remote
  279. break
  280. url = found_remote.split(' ')[1]
  281. return url.strip()
  282. @classmethod
  283. def has_commit(cls, location, rev):
  284. # type: (str, str) -> bool
  285. """
  286. Check if rev is a commit that is available in the local repository.
  287. """
  288. try:
  289. cls.run_command(
  290. ['rev-parse', '-q', '--verify', "sha^" + rev],
  291. cwd=location,
  292. log_failed_cmd=False,
  293. )
  294. except InstallationError:
  295. return False
  296. else:
  297. return True
  298. @classmethod
  299. def get_revision(cls, location, rev=None):
  300. # type: (str, Optional[str]) -> str
  301. if rev is None:
  302. rev = 'HEAD'
  303. current_rev = cls.run_command(
  304. ['rev-parse', rev],
  305. show_stdout=False,
  306. stdout_only=True,
  307. cwd=location,
  308. )
  309. return current_rev.strip()
  310. @classmethod
  311. def get_subdirectory(cls, location):
  312. # type: (str) -> Optional[str]
  313. """
  314. Return the path to setup.py, relative to the repo root.
  315. Return None if setup.py is in the repo root.
  316. """
  317. # find the repo root
  318. git_dir = cls.run_command(
  319. ['rev-parse', '--git-dir'],
  320. show_stdout=False,
  321. stdout_only=True,
  322. cwd=location,
  323. ).strip()
  324. if not os.path.isabs(git_dir):
  325. git_dir = os.path.join(location, git_dir)
  326. repo_root = os.path.abspath(os.path.join(git_dir, '..'))
  327. return find_path_to_setup_from_repo_root(location, repo_root)
  328. @classmethod
  329. def get_url_rev_and_auth(cls, url):
  330. # type: (str) -> Tuple[str, Optional[str], AuthInfo]
  331. """
  332. Prefixes stub URLs like 'user@hostname:user/repo.git' with 'ssh://'.
  333. That's required because although they use SSH they sometimes don't
  334. work with a ssh:// scheme (e.g. GitHub). But we need a scheme for
  335. parsing. Hence we remove it again afterwards and return it as a stub.
  336. """
  337. # Works around an apparent Git bug
  338. # (see https://article.gmane.org/gmane.comp.version-control.git/146500)
  339. scheme, netloc, path, query, fragment = urlsplit(url)
  340. if scheme.endswith('file'):
  341. initial_slashes = path[:-len(path.lstrip('/'))]
  342. newpath = (
  343. initial_slashes +
  344. urllib.request.url2pathname(path)
  345. .replace('\\', '/').lstrip('/')
  346. )
  347. after_plus = scheme.find('+') + 1
  348. url = scheme[:after_plus] + urlunsplit(
  349. (scheme[after_plus:], netloc, newpath, query, fragment),
  350. )
  351. if '://' not in url:
  352. assert 'file:' not in url
  353. url = url.replace('git+', 'git+ssh://')
  354. url, rev, user_pass = super().get_url_rev_and_auth(url)
  355. url = url.replace('ssh://', '')
  356. else:
  357. url, rev, user_pass = super().get_url_rev_and_auth(url)
  358. return url, rev, user_pass
  359. @classmethod
  360. def update_submodules(cls, location):
  361. # type: (str) -> None
  362. if not os.path.exists(os.path.join(location, '.gitmodules')):
  363. return
  364. cls.run_command(
  365. ['submodule', 'update', '--init', '--recursive', '-q'],
  366. cwd=location,
  367. )
  368. @classmethod
  369. def get_repository_root(cls, location):
  370. # type: (str) -> Optional[str]
  371. loc = super().get_repository_root(location)
  372. if loc:
  373. return loc
  374. try:
  375. r = cls.run_command(
  376. ['rev-parse', '--show-toplevel'],
  377. cwd=location,
  378. show_stdout=False,
  379. stdout_only=True,
  380. on_returncode='raise',
  381. log_failed_cmd=False,
  382. )
  383. except BadCommand:
  384. logger.debug("could not determine if %s is under git control "
  385. "because git is not available", location)
  386. return None
  387. except InstallationError:
  388. return None
  389. return os.path.normpath(r.rstrip('\r\n'))
  390. vcs.register(Git)