New upstream version 2.1.7
Yaroslav Halchenko
6 years ago
18 | 18 | -Timothy B. Hartman <tbhartman _at_ gmail.com> |
19 | 19 | -Konstantin Popov <konstantin.popov.89 _at_ yandex.ru> |
20 | 20 | -Peter Jones <pjones _at_ redhat.com> |
21 | -Anson Mansfield <anson.mansfield _at_ gmail.com> | |
22 | -Ken Odegard <ken.odegard _at_ gmail.com> | |
23 | -Alexis Horgix Chotard | |
21 | 24 | |
22 | 25 | Portions derived from other open source works and are clearly marked. |
0 | 0 | Metadata-Version: 1.1 |
1 | 1 | Name: GitPython |
2 | Version: 2.1.6 | |
2 | Version: 2.1.7 | |
3 | 3 | Summary: Python Git Library |
4 | 4 | Home-page: https://github.com/gitpython-developers/GitPython |
5 | 5 | Author: Sebastian Thiel, Michael Trier |
121 | 121 | git/test/fixtures/ls_tree_a |
122 | 122 | git/test/fixtures/ls_tree_b |
123 | 123 | git/test/fixtures/ls_tree_commit |
124 | git/test/fixtures/ls_tree_empty | |
124 | 125 | git/test/fixtures/reflog_HEAD |
125 | 126 | git/test/fixtures/reflog_invalid_date |
126 | 127 | git/test/fixtures/reflog_invalid_email |
0 | 0 | Metadata-Version: 1.1 |
1 | 1 | Name: GitPython |
2 | Version: 2.1.6 | |
2 | Version: 2.1.7 | |
3 | 3 | Summary: Python Git Library |
4 | 4 | Home-page: https://github.com/gitpython-developers/GitPython |
5 | 5 | Author: Sebastian Thiel, Michael Trier |
11 | 11 | import os.path as osp |
12 | 12 | |
13 | 13 | |
14 | __version__ = '2.1.6' | |
14 | __version__ = '2.1.7' | |
15 | 15 | |
16 | 16 | |
17 | 17 | #{ Initialization |
18 | 18 | def _init_externals(): |
19 | 19 | """Initialize external projects by putting them into the path""" |
20 | if __version__ == '2.1.6': | |
20 | if __version__ == '2.1.7': | |
21 | 21 | sys.path.insert(0, osp.join(osp.dirname(__file__), 'ext', 'gitdb')) |
22 | 22 | |
23 | 23 | try: |
34 | 34 | |
35 | 35 | #{ Imports |
36 | 36 | |
37 | from git.config import GitConfigParser # @NoMove @IgnorePep8 | |
38 | from git.objects import * # @NoMove @IgnorePep8 | |
39 | from git.refs import * # @NoMove @IgnorePep8 | |
40 | from git.diff import * # @NoMove @IgnorePep8 | |
41 | from git.exc import * # @NoMove @IgnorePep8 | |
42 | from git.db import * # @NoMove @IgnorePep8 | |
43 | from git.cmd import Git # @NoMove @IgnorePep8 | |
44 | from git.repo import Repo # @NoMove @IgnorePep8 | |
45 | from git.remote import * # @NoMove @IgnorePep8 | |
46 | from git.index import * # @NoMove @IgnorePep8 | |
47 | from git.util import ( # @NoMove @IgnorePep8 | |
48 | LockFile, | |
49 | BlockingLockFile, | |
50 | Stats, | |
51 | Actor, | |
52 | rmtree, | |
53 | ) | |
37 | from git.exc import * # @NoMove @IgnorePep8 | |
38 | try: | |
39 | from git.config import GitConfigParser # @NoMove @IgnorePep8 | |
40 | from git.objects import * # @NoMove @IgnorePep8 | |
41 | from git.refs import * # @NoMove @IgnorePep8 | |
42 | from git.diff import * # @NoMove @IgnorePep8 | |
43 | from git.db import * # @NoMove @IgnorePep8 | |
44 | from git.cmd import Git # @NoMove @IgnorePep8 | |
45 | from git.repo import Repo # @NoMove @IgnorePep8 | |
46 | from git.remote import * # @NoMove @IgnorePep8 | |
47 | from git.index import * # @NoMove @IgnorePep8 | |
48 | from git.util import ( # @NoMove @IgnorePep8 | |
49 | LockFile, | |
50 | BlockingLockFile, | |
51 | Stats, | |
52 | Actor, | |
53 | rmtree, | |
54 | ) | |
55 | except GitError as exc: | |
56 | raise ImportError('%s: %s' % (exc.__class__.__name__, exc)) | |
54 | 57 | |
55 | 58 | #} END imports |
56 | 59 | |
57 | 60 | __all__ = [name for name, obj in locals().items() |
58 | 61 | if not (name.startswith('_') or inspect.ismodule(obj))] |
62 | ||
63 | ||
64 | #{ Initialize git executable path | |
65 | GIT_OK = None | |
66 | ||
67 | def refresh(path=None): | |
68 | """Convenience method for setting the git executable path.""" | |
69 | global GIT_OK | |
70 | GIT_OK = False | |
71 | ||
72 | if not Git.refresh(path=path): | |
73 | return | |
74 | if not FetchInfo.refresh(): | |
75 | return | |
76 | ||
77 | GIT_OK = True | |
78 | #} END initialize git executable path | |
79 | ||
80 | ################# | |
81 | refresh() | |
82 | ################# |
16 | 16 | import subprocess |
17 | 17 | import sys |
18 | 18 | import threading |
19 | from textwrap import dedent | |
19 | 20 | |
20 | 21 | from git.compat import ( |
21 | 22 | string_types, |
30 | 31 | ) |
31 | 32 | from git.exc import CommandError |
32 | 33 | from git.odict import OrderedDict |
33 | from git.util import is_cygwin_git, cygpath | |
34 | from git.util import is_cygwin_git, cygpath, expand_path | |
34 | 35 | |
35 | 36 | from .exc import ( |
36 | 37 | GitCommandError, |
45 | 46 | execute_kwargs = set(('istream', 'with_extended_output', |
46 | 47 | 'with_exceptions', 'as_process', 'stdout_as_string', |
47 | 48 | 'output_stream', 'with_stdout', 'kill_after_timeout', |
48 | 'universal_newlines', 'shell')) | |
49 | 'universal_newlines', 'shell', 'env')) | |
49 | 50 | |
50 | 51 | log = logging.getLogger(__name__) |
51 | 52 | log.addHandler(logging.NullHandler()) |
181 | 182 | # Enables debugging of GitPython's git commands |
182 | 183 | GIT_PYTHON_TRACE = os.environ.get("GIT_PYTHON_TRACE", False) |
183 | 184 | |
184 | # Provide the full path to the git executable. Otherwise it assumes git is in the path | |
185 | _git_exec_env_var = "GIT_PYTHON_GIT_EXECUTABLE" | |
186 | GIT_PYTHON_GIT_EXECUTABLE = os.environ.get(_git_exec_env_var, git_exec_name) | |
187 | ||
188 | 185 | # If True, a shell will be used when executing git commands. |
189 | 186 | # This should only be desirable on Windows, see https://github.com/gitpython-developers/GitPython/pull/126 |
190 | 187 | # and check `git/test_repo.py:TestRepo.test_untracked_files()` TC for an example where it is required. |
191 | 188 | # Override this value using `Git.USE_SHELL = True` |
192 | 189 | USE_SHELL = False |
190 | ||
191 | # Provide the full path to the git executable. Otherwise it assumes git is in the path | |
192 | _git_exec_env_var = "GIT_PYTHON_GIT_EXECUTABLE" | |
193 | _refresh_env_var = "GIT_PYTHON_REFRESH" | |
194 | GIT_PYTHON_GIT_EXECUTABLE = None | |
195 | # note that the git executable is actually found during the refresh step in | |
196 | # the top level __init__ | |
197 | ||
198 | @classmethod | |
199 | def refresh(cls, path=None): | |
200 | """This gets called by the refresh function (see the top level | |
201 | __init__). | |
202 | """ | |
203 | # discern which path to refresh with | |
204 | if path is not None: | |
205 | new_git = os.path.expanduser(path) | |
206 | new_git = os.path.abspath(new_git) | |
207 | else: | |
208 | new_git = os.environ.get(cls._git_exec_env_var, cls.git_exec_name) | |
209 | ||
210 | # keep track of the old and new git executable path | |
211 | old_git = cls.GIT_PYTHON_GIT_EXECUTABLE | |
212 | cls.GIT_PYTHON_GIT_EXECUTABLE = new_git | |
213 | ||
214 | # test if the new git executable path is valid | |
215 | ||
216 | if sys.version_info < (3,): | |
217 | # - a GitCommandNotFound error is spawned by ourselves | |
218 | # - a OSError is spawned if the git executable provided | |
219 | # cannot be executed for whatever reason | |
220 | exceptions = (GitCommandNotFound, OSError) | |
221 | else: | |
222 | # - a GitCommandNotFound error is spawned by ourselves | |
223 | # - a PermissionError is spawned if the git executable provided | |
224 | # cannot be executed for whatever reason | |
225 | exceptions = (GitCommandNotFound, PermissionError) | |
226 | ||
227 | has_git = False | |
228 | try: | |
229 | cls().version() | |
230 | has_git = True | |
231 | except exceptions: | |
232 | pass | |
233 | ||
234 | # warn or raise exception if test failed | |
235 | if not has_git: | |
236 | err = dedent("""\ | |
237 | Bad git executable. | |
238 | The git executable must be specified in one of the following ways: | |
239 | - be included in your $PATH | |
240 | - be set via $%s | |
241 | - explicitly set via git.refresh() | |
242 | """) % cls._git_exec_env_var | |
243 | ||
244 | # revert to whatever the old_git was | |
245 | cls.GIT_PYTHON_GIT_EXECUTABLE = old_git | |
246 | ||
247 | if old_git is None: | |
248 | # on the first refresh (when GIT_PYTHON_GIT_EXECUTABLE is | |
249 | # None) we only are quiet, warn, or error depending on the | |
250 | # GIT_PYTHON_REFRESH value | |
251 | ||
252 | # determine what the user wants to happen during the initial | |
253 | # refresh we expect GIT_PYTHON_REFRESH to either be unset or | |
254 | # be one of the following values: | |
255 | # 0|q|quiet|s|silence | |
256 | # 1|w|warn|warning | |
257 | # 2|r|raise|e|error | |
258 | ||
259 | mode = os.environ.get(cls._refresh_env_var, "raise").lower() | |
260 | ||
261 | quiet = ["quiet", "q", "silence", "s", "none", "n", "0"] | |
262 | warn = ["warn", "w", "warning", "1"] | |
263 | error = ["error", "e", "raise", "r", "2"] | |
264 | ||
265 | if mode in quiet: | |
266 | pass | |
267 | elif mode in warn or mode in error: | |
268 | err = dedent("""\ | |
269 | %s | |
270 | All git commands will error until this is rectified. | |
271 | ||
272 | This initial warning can be silenced or aggravated in the future by setting the | |
273 | $%s environment variable. Use one of the following values: | |
274 | - %s: for no warning or exception | |
275 | - %s: for a printed warning | |
276 | - %s: for a raised exception | |
277 | ||
278 | Example: | |
279 | export %s=%s | |
280 | """) % ( | |
281 | err, | |
282 | cls._refresh_env_var, | |
283 | "|".join(quiet), | |
284 | "|".join(warn), | |
285 | "|".join(error), | |
286 | cls._refresh_env_var, | |
287 | quiet[0]) | |
288 | ||
289 | if mode in warn: | |
290 | print("WARNING: %s" % err) | |
291 | else: | |
292 | raise ImportError(err) | |
293 | else: | |
294 | err = dedent("""\ | |
295 | %s environment variable has been set but it has been set with an invalid value. | |
296 | ||
297 | Use only the following values: | |
298 | - %s: for no warning or exception | |
299 | - %s: for a printed warning | |
300 | - %s: for a raised exception | |
301 | """) % ( | |
302 | cls._refresh_env_var, | |
303 | "|".join(quiet), | |
304 | "|".join(warn), | |
305 | "|".join(error)) | |
306 | raise ImportError(err) | |
307 | ||
308 | # we get here if this was the init refresh and the refresh mode | |
309 | # was not error, go ahead and set the GIT_PYTHON_GIT_EXECUTABLE | |
310 | # such that we discern the difference between a first import | |
311 | # and a second import | |
312 | cls.GIT_PYTHON_GIT_EXECUTABLE = cls.git_exec_name | |
313 | else: | |
314 | # after the first refresh (when GIT_PYTHON_GIT_EXECUTABLE | |
315 | # is no longer None) we raise an exception | |
316 | raise GitCommandNotFound("git", err) | |
317 | ||
318 | return has_git | |
193 | 319 | |
194 | 320 | @classmethod |
195 | 321 | def is_cygwin(cls): |
404 | 530 | It is meant to be the working tree directory if available, or the |
405 | 531 | .git directory in case of bare repositories.""" |
406 | 532 | super(Git, self).__init__() |
407 | self._working_dir = working_dir | |
533 | self._working_dir = expand_path(working_dir) | |
408 | 534 | self._git_options = () |
409 | 535 | self._persistent_git_options = [] |
410 | 536 | |
470 | 596 | with_stdout=True, |
471 | 597 | universal_newlines=False, |
472 | 598 | shell=None, |
599 | env=None, | |
473 | 600 | **subprocess_kwargs |
474 | 601 | ): |
475 | 602 | """Handles executing the command on the shell and consumes and returns |
512 | 639 | if False, the commands standard output will be bytes. Otherwise, it will be |
513 | 640 | decoded into a string using the default encoding (usually utf-8). |
514 | 641 | The latter can fail, if the output contains binary data. |
642 | ||
643 | :param env: | |
644 | A dictionary of environment variables to be passed to `subprocess.Popen`. | |
515 | 645 | |
516 | 646 | :param subprocess_kwargs: |
517 | 647 | Keyword arguments to be passed to subprocess.Popen. Please note that |
558 | 688 | cwd = self._working_dir or os.getcwd() |
559 | 689 | |
560 | 690 | # Start the process |
691 | inline_env = env | |
561 | 692 | env = os.environ.copy() |
562 | 693 | # Attempt to force all output to plain ascii english, which is what some parsing code |
563 | 694 | # may expect. |
566 | 697 | env["LANGUAGE"] = "C" |
567 | 698 | env["LC_ALL"] = "C" |
568 | 699 | env.update(self._environment) |
700 | if inline_env is not None: | |
701 | env.update(inline_env) | |
569 | 702 | |
570 | 703 | if is_win: |
571 | 704 | cmd_not_found_exception = OSError |
827 | 960 | - "command options" to be converted by :meth:`transform_kwargs()`; |
828 | 961 | - the `'insert_kwargs_after'` key which its value must match one of ``*args``, |
829 | 962 | and any cmd-options will be appended after the matched arg. |
830 | ||
963 | ||
831 | 964 | Examples:: |
832 | ||
965 | ||
833 | 966 | git.rev_list('master', max_count=10, header=True) |
834 | ||
967 | ||
835 | 968 | turns into:: |
836 | ||
969 | ||
837 | 970 | git rev-list max-count 10 --header master |
838 | 971 | |
839 | 972 | :return: Same as ``execute``""" |
36 | 36 | # and delete remainders manually |
37 | 37 | for ref in refs: |
38 | 38 | try: |
39 | os.remove(osp.join(repo.common_dir, ref.path)) | |
40 | except OSError: | |
41 | pass | |
42 | try: | |
39 | 43 | os.remove(osp.join(repo.git_dir, ref.path)) |
40 | 44 | except OSError: |
41 | 45 | pass |
25 | 25 | __all__ = ["SymbolicReference"] |
26 | 26 | |
27 | 27 | |
28 | def _git_dir(repo, path): | |
29 | """ Find the git dir that's appropriate for the path""" | |
30 | name = "%s" % (path,) | |
31 | if name in ['HEAD', 'ORIG_HEAD', 'FETCH_HEAD', 'index', 'logs']: | |
32 | return repo.git_dir | |
33 | return repo.common_dir | |
34 | ||
35 | ||
28 | 36 | class SymbolicReference(object): |
29 | 37 | |
30 | 38 | """Represents a special case of a reference such that this reference is symbolic. |
70 | 78 | |
71 | 79 | @property |
72 | 80 | def abspath(self): |
73 | return join_path_native(self.repo.git_dir, self.path) | |
81 | return join_path_native(_git_dir(self.repo, self.path), self.path) | |
74 | 82 | |
75 | 83 | @classmethod |
76 | 84 | def _get_packed_refs_path(cls, repo): |
77 | try: | |
78 | commondir = open(osp.join(repo.git_dir, 'commondir'), 'rt').readlines()[0].strip() | |
79 | except (OSError, IOError): | |
80 | commondir = '.' | |
81 | repodir = osp.join(repo.git_dir, commondir) | |
82 | return osp.join(repodir, 'packed-refs') | |
85 | return osp.join(repo.common_dir, 'packed-refs') | |
83 | 86 | |
84 | 87 | @classmethod |
85 | 88 | def _iter_packed_refs(cls, repo): |
126 | 129 | # END recursive dereferencing |
127 | 130 | |
128 | 131 | @classmethod |
129 | def _get_ref_info_helper(cls, repo, repodir, ref_path): | |
132 | def _get_ref_info_helper(cls, repo, ref_path): | |
130 | 133 | """Return: (str(sha), str(target_ref_path)) if available, the sha the file at |
131 | 134 | rela_path points to, or None. target_ref_path is the reference we |
132 | 135 | point to, or None""" |
133 | 136 | tokens = None |
137 | repodir = _git_dir(repo, ref_path) | |
134 | 138 | try: |
135 | 139 | with open(osp.join(repodir, ref_path), 'rt') as fp: |
136 | 140 | value = fp.read().rstrip() |
168 | 172 | """Return: (str(sha), str(target_ref_path)) if available, the sha the file at |
169 | 173 | rela_path points to, or None. target_ref_path is the reference we |
170 | 174 | point to, or None""" |
171 | try: | |
172 | return cls._get_ref_info_helper(repo, repo.git_dir, ref_path) | |
173 | except ValueError: | |
174 | try: | |
175 | commondir = open(osp.join(repo.git_dir, 'commondir'), 'rt').readlines()[0].strip() | |
176 | except (OSError, IOError): | |
177 | commondir = '.' | |
178 | ||
179 | repodir = osp.join(repo.git_dir, commondir) | |
180 | return cls._get_ref_info_helper(repo, repodir, ref_path) | |
175 | return cls._get_ref_info_helper(repo, ref_path) | |
181 | 176 | |
182 | 177 | def _get_object(self): |
183 | 178 | """ |
432 | 427 | or just "myreference", hence 'refs/' is implied. |
433 | 428 | Alternatively the symbolic reference to be deleted""" |
434 | 429 | full_ref_path = cls.to_full_path(path) |
435 | abs_path = osp.join(repo.git_dir, full_ref_path) | |
430 | abs_path = osp.join(repo.common_dir, full_ref_path) | |
436 | 431 | if osp.exists(abs_path): |
437 | 432 | os.remove(abs_path) |
438 | 433 | else: |
483 | 478 | a proper symbolic reference. Otherwise it will be resolved to the |
484 | 479 | corresponding object and a detached symbolic reference will be created |
485 | 480 | instead""" |
481 | git_dir = _git_dir(repo, path) | |
486 | 482 | full_ref_path = cls.to_full_path(path) |
487 | abs_ref_path = osp.join(repo.git_dir, full_ref_path) | |
483 | abs_ref_path = osp.join(git_dir, full_ref_path) | |
488 | 484 | |
489 | 485 | # figure out target data |
490 | 486 | target = reference |
558 | 554 | if self.path == new_path: |
559 | 555 | return self |
560 | 556 | |
561 | new_abs_path = osp.join(self.repo.git_dir, new_path) | |
562 | cur_abs_path = osp.join(self.repo.git_dir, self.path) | |
557 | new_abs_path = osp.join(_git_dir(self.repo, new_path), new_path) | |
558 | cur_abs_path = osp.join(_git_dir(self.repo, self.path), self.path) | |
563 | 559 | if osp.isfile(new_abs_path): |
564 | 560 | if not force: |
565 | 561 | # if they point to the same file, its not an error |
593 | 589 | |
594 | 590 | # walk loose refs |
595 | 591 | # Currently we do not follow links |
596 | for root, dirs, files in os.walk(join_path_native(repo.git_dir, common_path)): | |
592 | for root, dirs, files in os.walk(join_path_native(repo.common_dir, common_path)): | |
597 | 593 | if 'refs' not in root.split(os.sep): # skip non-refs subfolders |
598 | 594 | refs_id = [d for d in dirs if d == 'refs'] |
599 | 595 | if refs_id: |
604 | 600 | if f == 'packed-refs': |
605 | 601 | continue |
606 | 602 | abs_path = to_native_path_linux(join_path(root, f)) |
607 | rela_paths.add(abs_path.replace(to_native_path_linux(repo.git_dir) + '/', "")) | |
603 | rela_paths.add(abs_path.replace(to_native_path_linux(repo.common_dir) + '/', "")) | |
608 | 604 | # END for each file in root directory |
609 | 605 | # END for each directory to walk |
610 | 606 |
208 | 208 | NEW_TAG, NEW_HEAD, HEAD_UPTODATE, TAG_UPDATE, REJECTED, FORCED_UPDATE, \ |
209 | 209 | FAST_FORWARD, ERROR = [1 << x for x in range(8)] |
210 | 210 | |
211 | re_fetch_result = re.compile(r'^\s*(.) (\[?[\w\s\.$@]+\]?)\s+(.+) -> ([^\s]+)( \(.*\)?$)?') | |
212 | ||
213 | _flag_map = {'!': ERROR, | |
214 | '+': FORCED_UPDATE, | |
215 | '*': 0, | |
216 | '=': HEAD_UPTODATE, | |
217 | ' ': FAST_FORWARD} | |
218 | ||
219 | v = Git().version_info[:2] | |
220 | if v >= (2, 10): | |
221 | _flag_map['t'] = TAG_UPDATE | |
222 | else: | |
223 | _flag_map['-'] = TAG_UPDATE | |
211 | _re_fetch_result = re.compile(r'^\s*(.) (\[?[\w\s\.$@]+\]?)\s+(.+) -> ([^\s]+)( \(.*\)?$)?') | |
212 | ||
213 | _flag_map = { | |
214 | '!': ERROR, | |
215 | '+': FORCED_UPDATE, | |
216 | '*': 0, | |
217 | '=': HEAD_UPTODATE, | |
218 | ' ': FAST_FORWARD, | |
219 | '-': TAG_UPDATE, | |
220 | } | |
221 | ||
222 | @classmethod | |
223 | def refresh(cls): | |
224 | """This gets called by the refresh function (see the top level | |
225 | __init__). | |
226 | """ | |
227 | # clear the old values in _flag_map | |
228 | try: | |
229 | del cls._flag_map["t"] | |
230 | except KeyError: | |
231 | pass | |
232 | ||
233 | try: | |
234 | del cls._flag_map["-"] | |
235 | except KeyError: | |
236 | pass | |
237 | ||
238 | # set the value given the git version | |
239 | if Git().version_info[:2] >= (2, 10): | |
240 | cls._flag_map["t"] = cls.TAG_UPDATE | |
241 | else: | |
242 | cls._flag_map["-"] = cls.TAG_UPDATE | |
243 | ||
244 | return True | |
224 | 245 | |
225 | 246 | def __init__(self, ref, flags, note='', old_commit=None, remote_ref_path=None): |
226 | 247 | """ |
263 | 284 | |
264 | 285 | fetch line is the corresponding line from FETCH_HEAD, like |
265 | 286 | acb0fa8b94ef421ad60c8507b634759a472cd56c not-for-merge branch '0.1.7RC' of /tmp/tmpya0vairemote_repo""" |
266 | match = cls.re_fetch_result.match(line) | |
287 | match = cls._re_fetch_result.match(line) | |
267 | 288 | if match is None: |
268 | 289 | raise ValueError("Failed to parse line: %r" % line) |
269 | 290 | |
652 | 673 | continue |
653 | 674 | |
654 | 675 | # read head information |
655 | with open(osp.join(self.repo.git_dir, 'FETCH_HEAD'), 'rb') as fp: | |
676 | with open(osp.join(self.repo.common_dir, 'FETCH_HEAD'), 'rb') as fp: | |
656 | 677 | fetch_head_info = [l.decode(defenc) for l in fp.readlines()] |
657 | 678 | |
658 | 679 | l_fil = len(fetch_info_lines) |
8 | 8 | import os |
9 | 9 | import re |
10 | 10 | import sys |
11 | import warnings | |
11 | 12 | |
12 | 13 | from git.cmd import ( |
13 | 14 | Git, |
28 | 29 | from git.objects import Submodule, RootModule, Commit |
29 | 30 | from git.refs import HEAD, Head, Reference, TagReference |
30 | 31 | from git.remote import Remote, add_progress, to_progress_instance |
31 | from git.util import Actor, finalize_process, decygpath, hex_to_bin | |
32 | from git.util import Actor, finalize_process, decygpath, hex_to_bin, expand_path | |
32 | 33 | import os.path as osp |
33 | 34 | |
34 | 35 | from .fun import rev_parse, is_git_dir, find_submodule_git_dir, touch, find_worktree_git_dir |
49 | 50 | __all__ = ('Repo',) |
50 | 51 | |
51 | 52 | |
52 | def _expand_path(p): | |
53 | return osp.normpath(osp.abspath(osp.expandvars(osp.expanduser(p)))) | |
54 | ||
55 | ||
56 | 53 | class Repo(object): |
57 | 54 | """Represents a git repository and allows you to query references, |
58 | 55 | gather commit information, generate diffs, create and clone repositories query |
73 | 70 | working_dir = None |
74 | 71 | _working_tree_dir = None |
75 | 72 | git_dir = None |
73 | _common_dir = None | |
76 | 74 | |
77 | 75 | # precompiled regex |
78 | 76 | re_whitespace = re.compile(r'\s+') |
89 | 87 | # Subclasses may easily bring in their own custom types by placing a constructor or type here |
90 | 88 | GitCommandWrapperType = Git |
91 | 89 | |
92 | def __init__(self, path=None, odbt=DefaultDBType, search_parent_directories=False): | |
90 | def __init__(self, path=None, odbt=DefaultDBType, search_parent_directories=False, expand_vars=True): | |
93 | 91 | """Create a new Repo instance |
94 | 92 | |
95 | 93 | :param path: |
115 | 113 | :raise InvalidGitRepositoryError: |
116 | 114 | :raise NoSuchPathError: |
117 | 115 | :return: git.Repo """ |
116 | ||
118 | 117 | epath = path or os.getenv('GIT_DIR') |
119 | 118 | if not epath: |
120 | 119 | epath = os.getcwd() |
121 | 120 | if Git.is_cygwin(): |
122 | 121 | epath = decygpath(epath) |
123 | epath = _expand_path(epath or path or os.getcwd()) | |
122 | ||
123 | epath = epath or path or os.getcwd() | |
124 | if expand_vars and ("%" in epath or "$" in epath): | |
125 | warnings.warn("The use of environment variables in paths is deprecated" + | |
126 | "\nfor security reasons and may be removed in the future!!") | |
127 | epath = expand_path(epath, expand_vars) | |
124 | 128 | if not os.path.exists(epath): |
125 | 129 | raise NoSuchPathError(epath) |
126 | 130 | |
147 | 151 | sm_gitpath = find_worktree_git_dir(dotgit) |
148 | 152 | |
149 | 153 | if sm_gitpath is not None: |
150 | self.git_dir = _expand_path(sm_gitpath) | |
154 | self.git_dir = expand_path(sm_gitpath, expand_vars) | |
151 | 155 | self._working_tree_dir = curpath |
152 | 156 | break |
153 | 157 | |
168 | 172 | # lets not assume the option exists, although it should |
169 | 173 | pass |
170 | 174 | |
175 | try: | |
176 | common_dir = open(osp.join(self.git_dir, 'commondir'), 'rt').readlines()[0].strip() | |
177 | self._common_dir = osp.join(self.git_dir, common_dir) | |
178 | except (OSError, IOError): | |
179 | self._common_dir = None | |
180 | ||
171 | 181 | # adjust the wd in case we are actually bare - we didn't know that |
172 | 182 | # in the first place |
173 | 183 | if self._bare: |
174 | 184 | self._working_tree_dir = None |
175 | 185 | # END working dir handling |
176 | 186 | |
177 | self.working_dir = self._working_tree_dir or self.git_dir | |
187 | self.working_dir = self._working_tree_dir or self.common_dir | |
178 | 188 | self.git = self.GitCommandWrapperType(self.working_dir) |
179 | 189 | |
180 | 190 | # special handling, in special times |
181 | args = [osp.join(self.git_dir, 'objects')] | |
191 | args = [osp.join(self.common_dir, 'objects')] | |
182 | 192 | if issubclass(odbt, GitCmdObjectDB): |
183 | 193 | args.append(self.git) |
184 | 194 | self.odb = odbt(*args) |
234 | 244 | """:return: The working tree directory of our git repository. If this is a bare repository, None is returned. |
235 | 245 | """ |
236 | 246 | return self._working_tree_dir |
247 | ||
248 | @property | |
249 | def common_dir(self): | |
250 | """:return: The git dir that holds everything except possibly HEAD, | |
251 | FETCH_HEAD, ORIG_HEAD, COMMIT_EDITMSG, index, and logs/ . | |
252 | """ | |
253 | return self._common_dir or self.git_dir | |
237 | 254 | |
238 | 255 | @property |
239 | 256 | def bare(self): |
573 | 590 | :note: |
574 | 591 | The method does not check for the existence of the paths in alts |
575 | 592 | as the caller is responsible.""" |
576 | alternates_path = osp.join(self.git_dir, 'objects', 'info', 'alternates') | |
593 | alternates_path = osp.join(self.common_dir, 'objects', 'info', 'alternates') | |
577 | 594 | if not alts: |
578 | 595 | if osp.isfile(alternates_path): |
579 | 596 | os.remove(alternates_path) |
843 | 860 | return blames |
844 | 861 | |
845 | 862 | @classmethod |
846 | def init(cls, path=None, mkdir=True, odbt=DefaultDBType, **kwargs): | |
863 | def init(cls, path=None, mkdir=True, odbt=DefaultDBType, expand_vars=True, **kwargs): | |
847 | 864 | """Initialize a git repository at the given path if specified |
848 | 865 | |
849 | 866 | :param path: |
861 | 878 | the directory containing the database objects, i.e. .git/objects. |
862 | 879 | It will be used to access all object data |
863 | 880 | |
881 | :param expand_vars: | |
882 | if specified, environment variables will not be escaped. This | |
883 | can lead to information disclosure, allowing attackers to | |
884 | access the contents of environment variables | |
885 | ||
864 | 886 | :parm kwargs: |
865 | 887 | keyword arguments serving as additional options to the git-init command |
866 | 888 | |
867 | 889 | :return: ``git.Repo`` (the newly created repo)""" |
868 | 890 | if path: |
869 | path = _expand_path(path) | |
891 | path = expand_path(path, expand_vars) | |
870 | 892 | if mkdir and path and not osp.exists(path): |
871 | 893 | os.makedirs(path, 0o755) |
872 | 894 | |
931 | 953 | * All remaining keyword arguments are given to the git-clone command |
932 | 954 | |
933 | 955 | :return: ``git.Repo`` (the newly cloned repo)""" |
934 | return self._clone(self.git, self.git_dir, path, type(self.odb), progress, **kwargs) | |
956 | return self._clone(self.git, self.common_dir, path, type(self.odb), progress, **kwargs) | |
935 | 957 | |
936 | 958 | @classmethod |
937 | 959 | def clone_from(cls, url, to_path, progress=None, env=None, **kwargs): |
288 | 288 | assert len(headcommit.hexsha) == 40 |
289 | 289 | assert len(headcommit.parents) > 0 |
290 | 290 | assert headcommit.tree.type == 'tree' |
291 | assert headcommit.author.name == 'Sebastian Thiel' | |
291 | assert len(headcommit.author.name) != 0 | |
292 | 292 | assert isinstance(headcommit.authored_date, int) |
293 | assert headcommit.committer.name == 'Sebastian Thiel' | |
293 | assert len(headcommit.committer.name) != 0 | |
294 | 294 | assert isinstance(headcommit.committed_date, int) |
295 | 295 | assert headcommit.message != '' |
296 | 296 | # ![14-test_references_and_objects] |
9 | 9 | |
10 | 10 | from git import ( |
11 | 11 | Git, |
12 | refresh, | |
12 | 13 | GitCommandError, |
13 | 14 | GitCommandNotFound, |
14 | 15 | Repo, |
104 | 105 | self.git.version(pass_this_kwarg=False) |
105 | 106 | assert_true("pass_this_kwarg" not in git.call_args[1]) |
106 | 107 | |
108 | def test_it_accepts_environment_variables(self): | |
109 | filename = fixture_path("ls_tree_empty") | |
110 | with open(filename, 'r') as fh: | |
111 | tree = self.git.mktree(istream=fh) | |
112 | env = { | |
113 | 'GIT_AUTHOR_NAME': 'Author Name', | |
114 | 'GIT_AUTHOR_EMAIL': 'author@example.com', | |
115 | 'GIT_AUTHOR_DATE': '1400000000+0000', | |
116 | 'GIT_COMMITTER_NAME': 'Committer Name', | |
117 | 'GIT_COMMITTER_EMAIL': 'committer@example.com', | |
118 | 'GIT_COMMITTER_DATE': '1500000000+0000', | |
119 | } | |
120 | commit = self.git.commit_tree(tree, m='message', env=env) | |
121 | assert_equal(commit, '4cfd6b0314682d5a58f80be39850bad1640e9241') | |
122 | ||
107 | 123 | def test_persistent_cat_file_command(self): |
108 | 124 | # read header only |
109 | 125 | import subprocess as sp |
155 | 171 | type(self.git).GIT_PYTHON_GIT_EXECUTABLE = prev_cmd |
156 | 172 | # END undo adjustment |
157 | 173 | |
174 | def test_refresh(self): | |
175 | # test a bad git path refresh | |
176 | self.assertRaises(GitCommandNotFound, refresh, "yada") | |
177 | ||
178 | # test a good path refresh | |
179 | path = os.popen("which git").read().strip() | |
180 | refresh(path) | |
181 | ||
158 | 182 | def test_options_are_passed_to_git(self): |
159 | 183 | # This work because any command after git --version is ignored |
160 | 184 | git_version = self.git(version=True).NoOp() |
934 | 934 | commit = repo.head.commit |
935 | 935 | self.assertIsInstance(commit, Object) |
936 | 936 | |
937 | self.assertIsInstance(repo.heads['aaaaaaaa'], Head) | |
938 | ||
937 | 939 | @with_rw_directory |
938 | 940 | def test_git_work_tree_env(self, rw_dir): |
939 | 941 | """Check that we yield to GIT_WORK_TREE""" |
338 | 338 | """Wait for the process (clone, fetch, pull or push) and handle its errors accordingly""" |
339 | 339 | ## TODO: No close proc-streams?? |
340 | 340 | proc.wait(**kwargs) |
341 | ||
342 | ||
343 | def expand_path(p, expand_vars=True): | |
344 | try: | |
345 | p = osp.expanduser(p) | |
346 | if expand_vars: | |
347 | p = osp.expandvars(p) | |
348 | return osp.normpath(osp.abspath(p)) | |
349 | except: | |
350 | return None | |
341 | 351 | |
342 | 352 | #} END utilities |
343 | 353 |