comparison planemo/lib/python3.7/site-packages/pip/_internal/vcs/git.py @ 1:56ad4e20f292 draft

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