Mercurial > repos > guerler > springsuite
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) |