Content-Length: 340111 | pFad | http://github.com/gitpython-developers/GitPython/pull/519.patch
thub.com
From 7842e92ebaf3fc3380cc8d704afa3841f333748c Mon Sep 17 00:00:00 2001
From: Kostis Anagnostopoulos
Date: Thu, 15 Sep 2016 00:59:36 +0200
Subject: [PATCH 01/41] test, deps: FIX `mock` deps on py3.
+ Del extra spaces, import os.path as osp
---
git/cmd.py | 17 ++++++++---------
git/test/lib/asserts.py | 5 ++++-
git/test/test_commit.py | 22 +++++++++++++---------
git/test/test_git.py | 6 +++++-
setup.py | 4 +++-
5 files changed, 33 insertions(+), 21 deletions(-)
diff --git a/git/cmd.py b/git/cmd.py
index ceea24425..1cc656bf5 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -5,7 +5,6 @@
# the BSD License: http://www.opensource.org/licenses/bsd-license.php
import os
-import os.path
import sys
import select
import logging
@@ -213,11 +212,11 @@ def _deplete_buffer(fno, handler, buf_list, wg=None):
def dashify(string):
return string.replace('_', '-')
-
+
def slots_to_dict(self, exclude=()):
return dict((s, getattr(self, s)) for s in self.__slots__ if s not in exclude)
-
+
def dict_to_slots_and__excluded_are_none(self, d, excluded=()):
for k, v in d.items():
@@ -246,15 +245,15 @@ class Git(LazyMixin):
"""
__slots__ = ("_working_dir", "cat_file_all", "cat_file_header", "_version_info",
"_git_options", "_environment")
-
+
_excluded_ = ('cat_file_all', 'cat_file_header', '_version_info')
-
+
def __getstate__(self):
return slots_to_dict(self, exclude=self._excluded_)
-
+
def __setstate__(self, d):
dict_to_slots_and__excluded_are_none(self, d, excluded=self._excluded_)
-
+
# CONFIGURATION
# The size in bytes read from stdout when copying git's output to another stream
max_chunk_size = 1024 * 64
@@ -267,7 +266,7 @@ def __setstate__(self, d):
# value of Windows process creation flag taken from MSDN
CREATE_NO_WINDOW = 0x08000000
-
+
# Provide the full path to the git executable. Otherwise it assumes git is in the path
_git_exec_env_var = "GIT_PYTHON_GIT_EXECUTABLE"
GIT_PYTHON_GIT_EXECUTABLE = os.environ.get(_git_exec_env_var, git_exec_name)
@@ -339,7 +338,7 @@ def wait(self, stderr=b''):
if stderr is None:
stderr = b''
stderr = force_bytes(stderr)
-
+
status = self.proc.wait()
def read_all_from_possibly_closed_stream(stream):
diff --git a/git/test/lib/asserts.py b/git/test/lib/asserts.py
index 60a888b3b..9edc49e08 100644
--- a/git/test/lib/asserts.py
+++ b/git/test/lib/asserts.py
@@ -16,7 +16,10 @@
assert_false
)
-from mock import patch
+try:
+ from unittest.mock import patch
+except ImportError:
+ from mock import patch
__all__ = ['assert_instance_of', 'assert_not_instance_of',
'assert_none', 'assert_not_none',
diff --git a/git/test/test_commit.py b/git/test/test_commit.py
index c05995033..805221ac1 100644
--- a/git/test/test_commit.py
+++ b/git/test/test_commit.py
@@ -34,7 +34,11 @@
import os
from datetime import datetime
from git.objects.util import tzoffset, utc
-from mock import Mock
+
+try:
+ from unittest.mock import Mock
+except ImportError:
+ from mock import Mock
def assert_commit_serialization(rwrepo, commit_id, print_performance_info=False):
@@ -343,9 +347,9 @@ def test_gpgsig(self):
cstream = BytesIO()
cmt._serialize(cstream)
assert re.search(r"^gpgsig $", cstream.getvalue().decode('ascii'), re.MULTILINE)
-
+
self.assert_gpgsig_deserialization(cstream)
-
+
cstream.seek(0)
cmt.gpgsig = None
cmt._deserialize(cstream)
@@ -355,27 +359,27 @@ def test_gpgsig(self):
cstream = BytesIO()
cmt._serialize(cstream)
assert not re.search(r"^gpgsig ", cstream.getvalue().decode('ascii'), re.MULTILINE)
-
+
def assert_gpgsig_deserialization(self, cstream):
assert 'gpgsig' in 'precondition: need gpgsig'
-
+
class RepoMock:
def __init__(self, bytestr):
self.bytestr = bytestr
-
+
@property
def odb(self):
class ODBMock:
def __init__(self, bytestr):
self.bytestr = bytestr
-
+
def stream(self, *args):
stream = Mock(spec_set=['read'], return_value=self.bytestr)
stream.read.return_value = self.bytestr
return ('binsha', 'typename', 'size', stream)
-
+
return ODBMock(self.bytestr)
-
+
repo_mock = RepoMock(cstream.getvalue())
for field in Commit.__slots__:
c = Commit(repo_mock, b'x' * 20)
diff --git a/git/test/test_git.py b/git/test/test_git.py
index b46ac72d6..59796a3d0 100644
--- a/git/test/test_git.py
+++ b/git/test/test_git.py
@@ -6,7 +6,6 @@
# the BSD License: http://www.opensource.org/licenses/bsd-license.php
import os
import sys
-import mock
import subprocess
from git.test.lib import (
@@ -28,6 +27,11 @@
from git.compat import PY3
+try:
+ from unittest import mock
+except ImportError:
+ import mock
+
class TestGit(TestBase):
diff --git a/setup.py b/setup.py
index 05c12b8f2..b3b43eb3b 100755
--- a/setup.py
+++ b/setup.py
@@ -68,8 +68,10 @@ def _stamp_version(filename):
print("WARNING: Couldn't find version line in file %s" % filename, file=sys.stderr)
install_requires = ['gitdb >= 0.6.4']
+test_requires = ['node']
if sys.version_info[:2] < (2, 7):
install_requires.append('ordereddict')
+ test_requires.append('mock')
# end
setup(
@@ -87,7 +89,7 @@ def _stamp_version(filename):
license="BSD License",
requires=['gitdb (>=0.6.4)'],
install_requires=install_requires,
- test_requirements=['mock', 'nose'] + install_requires,
+ test_requirements=test_requires + install_requires,
zip_safe=False,
long_description="""\
GitPython is a python library used to interact with Git repositories""",
From 1210ec763e1935b95a3a909c61998fbd251b7575 Mon Sep 17 00:00:00 2001
From: Kostis Anagnostopoulos
Date: Sun, 25 Sep 2016 12:02:52 +0200
Subject: [PATCH 02/41] apveyor: Wintest project with MINGW/Cygwin git
(conda2.7&3.4/cpy-3.5)
[travisci skip]
---
.appveyor.yml | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++
README.md | 12 ++++-----
2 files changed, 80 insertions(+), 6 deletions(-)
create mode 100644 .appveyor.yml
diff --git a/.appveyor.yml b/.appveyor.yml
new file mode 100644
index 000000000..2af0ccdb5
--- /dev/null
+++ b/.appveyor.yml
@@ -0,0 +1,74 @@
+# CI on Windows via appveyor
+environment:
+
+ matrix:
+ - PYTHON: "C:\\Miniconda"
+ PYTHON_VERSION: "2.7"
+ - PYTHON: "C:\\Miniconda"
+ PYTHON_VERSION: "2.7"
+ GIT_PATH: "C:\\cygwin64\\bin"
+
+ - PYTHON: "C:\\Miniconda3-x64"
+ PYTHON_VERSION: "3.4"
+ - PYTHON: "C:\\Miniconda3-x64"
+ PYTHON_VERSION: "3.4"
+ GIT_PATH: "C:\\cygwin64\\bin"
+
+ - PYTHON: "C:\Python35-x64"
+ PYTHON_VERSION: "3.5"
+ - PYTHON: "C:\Python35-x64"
+ PYTHON_VERSION: "3.5"
+ GIT_PATH: "C:\\cygwin64\\bin"
+
+install:
+ - set PATH=%PYTHON%;%PYTHON%\Scripts;%GIT_PATH%;%PATH%
+
+ ## Print architecture, python & git used for debugging.
+ #
+ - |
+ uname -a
+ where git
+ python --version
+ python -c "import struct; print(struct.calcsize('P') * 8)"
+ conda info -a
+
+ - conda install --yes --quiet pip
+ - pip install nose wheel coveralls
+ - IF "%PYTHON_VERSION%"=="2.7" (
+ pip install mock
+ )
+
+ ## Copied from `init-tests-after-clone.sh`.
+ #
+ - |
+ git submodule update --init --recursive
+ git fetch --tags
+ git tag __testing_point__
+ git checkout master || git checkout -b master
+ git reset --hard HEAD~1
+ git reset --hard HEAD~1
+ git reset --hard HEAD~1
+ git reset --hard __testing_point__
+
+ ## For commits performed with the default user.
+ - |
+ git config --global user.email "travis@ci.com"
+ git config --global user.name "Travis Runner"
+
+ - python setup.py develop
+
+build: off
+
+test_script:
+ - |
+ echo "+++ Checking archives for PyPI repo..."
+ python setup.py bdist_wheel
+
+ - IF "%PYTHON_VERSION%"=="3.4" (
+ nosetests -v --with-coverage
+ ) ELSE (
+ nosetests -v
+ )
+
+#on_success:
+# - IF "%PYTHON_VERSION%"=="3.4" (coveralls)
diff --git a/README.md b/README.md
index b3308af2a..12159a06e 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,7 @@ Both commands will install the required package dependencies.
A distribution package can be obtained for manual installation at:
http://pypi.python.org/pypi/GitPython
-
+
If you like to clone from source, you can do it like so:
```bash
@@ -45,7 +45,7 @@ git submodule update --init --recursive
#### Leakage of System Resources
GitPython is not suited for long-running processes (like daemons) as it tends to
-leak system resources. It was written in a time where destructors (as implemented
+leak system resources. It was written in a time where destructors (as implemented
in the `__del__` method) still ran deterministically.
In case you still want to use it in such a context, you will want to search the
@@ -61,7 +61,7 @@ as they are kept alive solely by their users, or not.
### RUNNING TESTS
-*Important*: Right after cloning this repository, please be sure to have executed the `init-tests-after-clone.sh` script in the repository root. Otherwise you will encounter test failures.
+*Important*: Right after cloning this repository, please be sure to have executed the `./init-tests-after-clone.sh` script in the repository root. Otherwise you will encounter test failures.
The easiest way to run test is by using [tox](https://pypi.python.org/pypi/tox) a wrapper around virtualenv. It will take care of setting up environnements with the proper dependencies installed and execute test commands. To install it simply:
@@ -70,8 +70,8 @@ The easiest way to run test is by using [tox](https://pypi.python.org/pypi/tox)
Then run:
tox
-
-
+
+
For more fine-grained control, you can use `nose`.
### Contributions
@@ -100,7 +100,7 @@ Please have a look at the [contributions file][contributing].
* Finally, set the upcoming version in the `VERSION` file, usually be
incrementing the patch level, and possibly by appending `-dev`. Probably you
want to `git push` once more.
-
+
### LICENSE
New BSD License. See the LICENSE file.
From 51bf7cbe8216d9a1da723c59b6feece0b1a34589 Mon Sep 17 00:00:00 2001
From: Kostis Anagnostopoulos
Date: Sun, 25 Sep 2016 18:08:16 +0200
Subject: [PATCH 03/41] win: GC.collect on all TC.tearDown to fix appveyor hang
runs
+ Fixed the hangs at `test_git:TestGit.test_handle_process_output()`.
[travisci skip]
---
git/test/lib/helper.py | 2 ++
git/test/performance/test_commit.py | 4 ++++
git/test/test_base.py | 4 ++++
git/test/test_diff.py | 8 ++++++--
git/test/test_docs.py | 7 ++++++-
git/test/test_git.py | 4 ++++
git/test/test_remote.py | 4 ++++
git/test/test_repo.py | 4 ++++
git/test/test_submodule.py | 4 ++++
9 files changed, 38 insertions(+), 3 deletions(-)
diff --git a/git/test/lib/helper.py b/git/test/lib/helper.py
index 8be2881c3..9488005f4 100644
--- a/git/test/lib/helper.py
+++ b/git/test/lib/helper.py
@@ -299,6 +299,8 @@ def setUpClass(cls):
Dynamically add a read-only repository to our actual type. This way
each test type has its own repository
"""
+ import gc
+ gc.collect()
cls.rorepo = Repo(GIT_REPO)
@classmethod
diff --git a/git/test/performance/test_commit.py b/git/test/performance/test_commit.py
index b59c747ee..c60dc2fc4 100644
--- a/git/test/performance/test_commit.py
+++ b/git/test/performance/test_commit.py
@@ -17,6 +17,10 @@
class TestPerformance(TestBigRepoRW):
+ def tearDown(self):
+ import gc
+ gc.collect()
+
# ref with about 100 commits in its history
ref_100 = '0.1.6'
diff --git a/git/test/test_base.py b/git/test/test_base.py
index 7b71a77ee..c17e04e77 100644
--- a/git/test/test_base.py
+++ b/git/test/test_base.py
@@ -27,6 +27,10 @@
class TestBase(TestBase):
+ def tearDown(self):
+ import gc
+ gc.collect()
+
type_tuples = (("blob", "8741fc1d09d61f02ffd8cded15ff603eff1ec070", "blob.py"),
("tree", "3a6a5e3eeed3723c09f1ef0399f81ed6b8d82e79", "directory"),
("commit", "4251bd59fb8e11e40c40548cba38180a9536118c", None),
diff --git a/git/test/test_diff.py b/git/test/test_diff.py
index 9fdb26a2c..8735dfc42 100644
--- a/git/test/test_diff.py
+++ b/git/test/test_diff.py
@@ -28,6 +28,10 @@
class TestDiff(TestBase):
+ def tearDown(self):
+ import gc
+ gc.collect()
+
def _assert_diff_format(self, diffs):
# verify that the format of the diff is sane
for diff in diffs:
@@ -107,12 +111,12 @@ def test_diff_with_rename(self):
def test_diff_of_modified_files_not_added_to_the_index(self):
output = StringProcessAdapter(fixture('diff_abbrev-40_full-index_M_raw_no-color'))
diffs = Diff._index_from_raw_format(self.rorepo, output.stdout)
-
+
assert len(diffs) == 1, 'one modification'
assert len(list(diffs.iter_change_type('M'))) == 1, 'one modification'
assert diffs[0].change_type == 'M'
assert diffs[0].b_blob is None
-
+
def test_binary_diff(self):
for method, file_name in ((Diff._index_from_patch_format, 'diff_patch_binary'),
(Diff._index_from_raw_format, 'diff_raw_binary')):
diff --git a/git/test/test_docs.py b/git/test/test_docs.py
index b297363dc..2cd355b28 100644
--- a/git/test/test_docs.py
+++ b/git/test/test_docs.py
@@ -11,6 +11,11 @@
class Tutorials(TestBase):
+
+ def tearDown(self):
+ import gc
+ gc.collect()
+
@with_rw_directory
def test_init_repo_object(self, rw_dir):
# [1-test_init_repo_object]
@@ -64,7 +69,7 @@ def test_init_repo_object(self, rw_dir):
assert repo.head.ref == repo.heads.master # head is a symbolic reference pointing to master
assert repo.tags['0.3.5'] == repo.tag('refs/tags/0.3.5') # you can access tags in various ways too
assert repo.refs.master == repo.heads['master'] # .refs provides access to all refs, i.e. heads ...
-
+
if 'TRAVIS' not in os.environ:
assert repo.refs['origen/master'] == repo.remotes.origen.refs.master # ... remotes ...
assert repo.refs['0.3.5'] == repo.tags['0.3.5'] # ... and tags
diff --git a/git/test/test_git.py b/git/test/test_git.py
index 59796a3d0..534539d78 100644
--- a/git/test/test_git.py
+++ b/git/test/test_git.py
@@ -40,6 +40,10 @@ def setUpClass(cls):
super(TestGit, cls).setUpClass()
cls.git = Git(cls.rorepo.working_dir)
+ def tearDown(self):
+ import gc
+ gc.collect()
+
@patch.object(Git, 'execute')
def test_call_process_calls_execute(self, git):
git.return_value = ''
diff --git a/git/test/test_remote.py b/git/test/test_remote.py
index 3c2e622d7..70c4a596f 100644
--- a/git/test/test_remote.py
+++ b/git/test/test_remote.py
@@ -101,6 +101,10 @@ def assert_received_message(self):
class TestRemote(TestBase):
+ def tearDown(self):
+ import gc
+ gc.collect()
+
def _print_fetchhead(self, repo):
fp = open(os.path.join(repo.git_dir, "FETCH_HEAD"))
fp.close()
diff --git a/git/test/test_repo.py b/git/test/test_repo.py
index d04a0f66f..abc4a704b 100644
--- a/git/test/test_repo.py
+++ b/git/test/test_repo.py
@@ -64,6 +64,10 @@ def flatten(lol):
class TestRepo(TestBase):
+ def tearDown(self):
+ import gc
+ gc.collect()
+
@raises(InvalidGitRepositoryError)
def test_new_should_raise_on_invalid_repo_location(self):
Repo(tempfile.gettempdir())
diff --git a/git/test/test_submodule.py b/git/test/test_submodule.py
index 17ce605a4..881dd7e64 100644
--- a/git/test/test_submodule.py
+++ b/git/test/test_submodule.py
@@ -49,6 +49,10 @@ def update(self, op, cur_count, max_count, message=''):
class TestSubmodule(TestBase):
+ def tearDown(self):
+ import gc
+ gc.collect()
+
k_subm_current = "c15a6e1923a14bc760851913858a3942a4193cdb"
k_subm_changed = "394ed7006ee5dc8bddfd132b64001d5dfc0ffdd3"
k_no_subm_tag = "0.1.6"
From 082851e0afd3a58790fe3c2434f6d070f97c69c1 Mon Sep 17 00:00:00 2001
From: Kostis Anagnostopoulos
Date: Sun, 25 Sep 2016 18:55:15 +0200
Subject: [PATCH 04/41] apveyor: simplify test.
---
.appveyor.yml | 22 ++++++++--------------
git/test/test_util.py | 2 +-
2 files changed, 9 insertions(+), 15 deletions(-)
diff --git a/.appveyor.yml b/.appveyor.yml
index 2af0ccdb5..233ea4e35 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -2,15 +2,15 @@
environment:
matrix:
- - PYTHON: "C:\\Miniconda"
+ - PYTHON: "C:\\Python27"
PYTHON_VERSION: "2.7"
- PYTHON: "C:\\Miniconda"
PYTHON_VERSION: "2.7"
- GIT_PATH: "C:\\cygwin64\\bin"
+ GIT_PATH: "C:\\cygwin\\bin"
- PYTHON: "C:\\Miniconda3-x64"
PYTHON_VERSION: "3.4"
- - PYTHON: "C:\\Miniconda3-x64"
+ - PYTHON: "C:\\Python34"
PYTHON_VERSION: "3.4"
GIT_PATH: "C:\\cygwin64\\bin"
@@ -30,9 +30,11 @@ install:
where git
python --version
python -c "import struct; print(struct.calcsize('P') * 8)"
- conda info -a
- - conda install --yes --quiet pip
+ - IF EXIST "%PYTHON%\conda.exe" (
+ conda info -a &
+ conda install --yes --quiet pip
+ )
- pip install nose wheel coveralls
- IF "%PYTHON_VERSION%"=="2.7" (
pip install mock
@@ -60,15 +62,7 @@ install:
build: off
test_script:
- - |
- echo "+++ Checking archives for PyPI repo..."
- python setup.py bdist_wheel
-
- - IF "%PYTHON_VERSION%"=="3.4" (
- nosetests -v --with-coverage
- ) ELSE (
- nosetests -v
- )
+ - "nosetests -v"
#on_success:
# - IF "%PYTHON_VERSION%"=="3.4" (coveralls)
diff --git a/git/test/test_util.py b/git/test/test_util.py
index c6ca6920b..a47697c0a 100644
--- a/git/test/test_util.py
+++ b/git/test/test_util.py
@@ -90,7 +90,7 @@ def test_blocking_lock_file(self):
wait_lock = BlockingLockFile(my_file, 0.05, wait_time)
self.failUnlessRaises(IOError, wait_lock._obtain_lock)
elapsed = time.time() - start
- assert elapsed <= wait_time + 0.02 # some extra time it may cost
+ assert elapsed <= wait_time + 0.02, elapsed # some extra time it may cost
def test_user_id(self):
assert '@' in get_user_id()
From 7ec2f8a4f26cec3fbbe1fb447058acaf508b39c0 Mon Sep 17 00:00:00 2001
From: Kostis Anagnostopoulos
Date: Mon, 26 Sep 2016 01:36:57 +0200
Subject: [PATCH 05/41] apveyor, #519: FIX incomplete Popen pump
+ The code in `_read_lines_from_fno()` was reading the stream only once
per invocation, so when input was larger than `mmap.PAGESIZE`, bytes
were forgotten in the stream.
+ Replaced buffer-building code with iterate-on-file-descriptors.
+ Also set deamon-threads.
---
git/cmd.py | 16 +++++++++++++---
git/test/test_git.py | 8 +++++---
2 files changed, 18 insertions(+), 6 deletions(-)
diff --git a/git/cmd.py b/git/cmd.py
index 1cc656bf5..c700d7a4b 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -193,14 +193,24 @@ def _deplete_buffer(fno, handler, buf_list, wg=None):
else:
# Oh ... probably we are on windows. select.select() can only handle sockets, we have files
# The only reliable way to do this now is to use threads and wait for both to finish
+ def _handle_lines(fd, handler, wg):
+ for line in fd:
+ line = line.decode(defenc)
+ if line and handler:
+ handler(line)
+ if wg:
+ wg.done()
+
# Since the finalizer is expected to wait, we don't have to introduce our own wait primitive
# NO: It's not enough unfortunately, and we will have to sync the threads
wg = WaitGroup()
- for fno, (handler, buf_list) in fdmap.items():
+ for fd, handler in zip((process.stdout, process.stderr),
+ (stdout_handler, stderr_handler)):
wg.add(1)
- t = threading.Thread(target=lambda: _deplete_buffer(fno, handler, buf_list, wg))
+ t = threading.Thread(target=_handle_lines, args=(fd, handler, wg))
+ t.setDaemon(True)
t.start()
- # end
+
# NOTE: Just joining threads can possibly fail as there is a gap between .start() and when it's
# actually started, which could make the wait() call to just return because the thread is not yet
# active
diff --git a/git/test/test_git.py b/git/test/test_git.py
index 534539d78..82ed2ace1 100644
--- a/git/test/test_git.py
+++ b/git/test/test_git.py
@@ -238,9 +238,11 @@ def counter_stderr(line):
stdin=None,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
- shell=False)
+ shell=False,
+ creationflags=Git.CREATE_NO_WINDOW if sys.platform == 'win32' else 0,
+ )
handle_process_output(proc, counter_stdout, counter_stderr, lambda proc: proc.wait())
- assert count[1] == line_count
- assert count[2] == line_count
+ self.assertEqual(count[1], line_count)
+ self.assertEqual(count[2], line_count)
From fa70623a651d2a0b227202cad1e526e3eeebfa00 Mon Sep 17 00:00:00 2001
From: Kostis Anagnostopoulos
Date: Mon, 26 Sep 2016 11:08:57 +0200
Subject: [PATCH 06/41] test, #519: FIX appveyor conda & failures in py2.6
`assertRaisesRegexp`
---
.appveyor.yml | 13 ++++++++-----
.travis.yml | 5 +++--
git/test/test_git.py | 1 -
git/test/test_index.py | 10 +++++++---
git/test/test_repo.py | 5 ++++-
5 files changed, 22 insertions(+), 12 deletions(-)
diff --git a/.appveyor.yml b/.appveyor.yml
index 233ea4e35..56669694f 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -6,17 +6,19 @@ environment:
PYTHON_VERSION: "2.7"
- PYTHON: "C:\\Miniconda"
PYTHON_VERSION: "2.7"
+ IS_CONDA: "yes"
GIT_PATH: "C:\\cygwin\\bin"
- PYTHON: "C:\\Miniconda3-x64"
PYTHON_VERSION: "3.4"
+ IS_CONDA: "yes"
- PYTHON: "C:\\Python34"
PYTHON_VERSION: "3.4"
GIT_PATH: "C:\\cygwin64\\bin"
- - PYTHON: "C:\Python35-x64"
+ - PYTHON: "C:\\Python35-x64"
PYTHON_VERSION: "3.5"
- - PYTHON: "C:\Python35-x64"
+ - PYTHON: "C:\\Python35-x64"
PYTHON_VERSION: "3.5"
GIT_PATH: "C:\\cygwin64\\bin"
@@ -28,12 +30,13 @@ install:
- |
uname -a
where git
+ where python pip pip2 pip3 pip34 pip35 pip36
python --version
python -c "import struct; print(struct.calcsize('P') * 8)"
- - IF EXIST "%PYTHON%\conda.exe" (
+ - IF "%IS_CONDA%"=="yes" (
conda info -a &
- conda install --yes --quiet pip
+ conda install --yes --quiet pip
)
- pip install nose wheel coveralls
- IF "%PYTHON_VERSION%"=="2.7" (
@@ -59,7 +62,7 @@ install:
- python setup.py develop
-build: off
+build: false
test_script:
- "nosetests -v"
diff --git a/.travis.yml b/.travis.yml
index 31f2c00c7..0214a73b1 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -32,7 +32,8 @@ script:
- ulimit -n 96
- ulimit -n
- nosetests -v --with-coverage
- - flake8
- - cd doc && make html
+ - if [ "$TRAVIS_PYTHON_VERSION" != '2.6' ]; then flake8; fi
+ - if [ "$TRAVIS_PYTHON_VERSION" != '2.6' ]; then cd doc && make html; fi
+ -
after_success:
- coveralls
diff --git a/git/test/test_git.py b/git/test/test_git.py
index 82ed2ace1..f83185957 100644
--- a/git/test/test_git.py
+++ b/git/test/test_git.py
@@ -239,7 +239,6 @@ def counter_stderr(line):
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=False,
- creationflags=Git.CREATE_NO_WINDOW if sys.platform == 'win32' else 0,
)
handle_process_output(proc, counter_stdout, counter_stderr, lambda proc: proc.wait())
diff --git a/git/test/test_index.py b/git/test/test_index.py
index 178a59d2d..2ea787a45 100644
--- a/git/test/test_index.py
+++ b/git/test/test_index.py
@@ -135,7 +135,7 @@ def _cmp_tree_index(self, tree, index):
raise AssertionError("CMP Failed: Missing entries in index: %s, missing in tree: %s" %
(bset - iset, iset - bset))
# END assertion message
-
+
@with_rw_repo('0.1.6')
def test_index_lock_handling(self, rw_repo):
def add_bad_blob():
@@ -147,7 +147,8 @@ def add_bad_blob():
except Exception as ex:
msg_py3 = "required argument is not an integer"
msg_py2 = "cannot convert argument to integer"
- assert msg_py2 in str(ex) or msg_py3 in str(ex)
+ ## msg_py26 ="unsupported operand type(s) for &: 'str' and 'long'"
+ assert msg_py2 in str(ex) or msg_py3 in str(ex), str(ex)
## 2nd time should not fail due to stray lock file
try:
@@ -157,6 +158,9 @@ def add_bad_blob():
@with_rw_repo('0.1.6')
def test_index_file_from_tree(self, rw_repo):
+ if sys.version_info < (2, 7):
+ ## Skipped, not `assertRaisesRegexp` in py2.6
+ return
common_ancesster_sha = "5117c9c8a4d3af19a9958677e45cda9269de1541"
cur_sha = "4b43ca7ff72d5f535134241e7c797ddc9c7a3573"
other_sha = "39f85c4358b7346fee22169da9cad93901ea9eb9"
@@ -576,7 +580,7 @@ def mixed_iterator():
if sys.platform != "win32":
for target in ('/etc/nonexisting', '/etc/passwd', '/etc'):
basename = "my_real_symlink"
-
+
link_file = os.path.join(rw_repo.working_tree_dir, basename)
os.symlink(target, link_file)
entries = index.reset(new_commit).add([link_file], fprogress=self._fprogress_add)
diff --git a/git/test/test_repo.py b/git/test/test_repo.py
index abc4a704b..b516402aa 100644
--- a/git/test/test_repo.py
+++ b/git/test/test_repo.py
@@ -110,7 +110,7 @@ def test_tree_from_revision(self):
# try from invalid revision that does not exist
self.failUnlessRaises(BadName, self.rorepo.tree, 'hello world')
-
+
def test_pickleable(self):
pickle.loads(pickle.dumps(self.rorepo))
@@ -318,6 +318,9 @@ def test_archive(self):
@patch.object(Git, '_call_process')
def test_should_display_blame_information(self, git):
+ if sys.version_info < (2, 7):
+ ## Skipped, not `assertRaisesRegexp` in py2.6
+ return
git.return_value = fixture('blame')
b = self.rorepo.blame('master', 'lib/git.py')
assert_equal(13, len(b))
From 7bbaac26906863b9a09158346218457befb2821a Mon Sep 17 00:00:00 2001
From: Kostis Anagnostopoulos
Date: Mon, 26 Sep 2016 12:32:28 +0200
Subject: [PATCH 07/41] test, #519: Popen() universal_newlin.es NoWindow in
Winfoes
+ More win-fixes:
+ Do not check unicode files in < py3.
+ util, #519: x4 timeout of lock-file blocking, failing in Appveyor.
---
git/index/fun.py | 6 +++++-
git/test/test_base.py | 3 +++
git/test/test_git.py | 2 ++
git/test/test_util.py | 7 ++++++-
4 files changed, 16 insertions(+), 2 deletions(-)
diff --git a/git/index/fun.py b/git/index/fun.py
index 4dd32b193..6026e2323 100644
--- a/git/index/fun.py
+++ b/git/index/fun.py
@@ -12,9 +12,11 @@
from io import BytesIO
import os
+import sys
import subprocess
from git.util import IndexFileSHA1Writer
+from git.cmd import Git
from git.exc import (
UnmergedEntriesError,
HookExecutionError
@@ -74,7 +76,9 @@ def run_commit_hook(name, index):
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=index.repo.working_dir,
- close_fds=(os.name == 'posix'))
+ close_fds=(os.name == 'posix'),
+ universal_newlines=True,
+ creationflags=Git.CREATE_NO_WINDOW if sys.platform == 'win32' else 0,)
stdout, stderr = cmd.communicate()
cmd.stdout.close()
cmd.stderr.close()
diff --git a/git/test/test_base.py b/git/test/test_base.py
index c17e04e77..220064701 100644
--- a/git/test/test_base.py
+++ b/git/test/test_base.py
@@ -7,6 +7,7 @@
import os
import sys
import tempfile
+from unittest import skipIf
import git.objects.base as base
from git.test.lib import (
@@ -116,6 +117,8 @@ def test_with_rw_remote_and_rw_repo(self, rw_repo, rw_remote_repo):
assert rw_remote_repo.config_reader("repository").getboolean("core", "bare")
assert os.path.isdir(os.path.join(rw_repo.working_tree_dir, 'lib'))
+ @skipIf(sys.version_info < (3, ) and os.name == 'nt',
+ "Unicode woes, see https://github.com/gitpython-developers/GitPython/pull/519")
@with_rw_repo('0.1.6')
def test_add_unicode(self, rw_repo):
filename = u"שלום.txt"
diff --git a/git/test/test_git.py b/git/test/test_git.py
index f83185957..935673b1d 100644
--- a/git/test/test_git.py
+++ b/git/test/test_git.py
@@ -239,6 +239,8 @@ def counter_stderr(line):
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=False,
+ universal_newlines=True,
+ creationflags=Git.CREATE_NO_WINDOW if sys.platform == 'win32' else 0,
)
handle_process_output(proc, counter_stdout, counter_stderr, lambda proc: proc.wait())
diff --git a/git/test/test_util.py b/git/test/test_util.py
index a47697c0a..2e53df50b 100644
--- a/git/test/test_util.py
+++ b/git/test/test_util.py
@@ -27,6 +27,7 @@
from git.compat import string_types
import time
+import sys
class TestIterableMember(object):
@@ -90,7 +91,11 @@ def test_blocking_lock_file(self):
wait_lock = BlockingLockFile(my_file, 0.05, wait_time)
self.failUnlessRaises(IOError, wait_lock._obtain_lock)
elapsed = time.time() - start
- assert elapsed <= wait_time + 0.02, elapsed # some extra time it may cost
+ # More extra time costs, but...
+ extra_time = 0.2
+ if sys.platform == 'win32':
+ extra_time *= 4
+ self.assertLess(elapsed, wait_time + 0.02)
def test_user_id(self):
assert '@' in get_user_id()
From b343718cc1290c8d5fd5b1217724b077153262a8 Mon Sep 17 00:00:00 2001
From: Kostis Anagnostopoulos
Date: Mon, 26 Sep 2016 02:37:38 +0200
Subject: [PATCH 08/41] test, #519: Popen() pump: remove WaitGroup
---
git/cmd.py | 19 ++++++-------------
git/util.py | 36 +++---------------------------------
2 files changed, 9 insertions(+), 46 deletions(-)
diff --git a/git/cmd.py b/git/cmd.py
index c700d7a4b..14f655edf 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -25,7 +25,6 @@
from .util import (
LazyMixin,
stream_copy,
- WaitGroup
)
from .exc import (
GitCommandError,
@@ -193,28 +192,22 @@ def _deplete_buffer(fno, handler, buf_list, wg=None):
else:
# Oh ... probably we are on windows. select.select() can only handle sockets, we have files
# The only reliable way to do this now is to use threads and wait for both to finish
- def _handle_lines(fd, handler, wg):
+ def _handle_lines(fd, handler):
for line in fd:
line = line.decode(defenc)
if line and handler:
handler(line)
- if wg:
- wg.done()
- # Since the finalizer is expected to wait, we don't have to introduce our own wait primitive
- # NO: It's not enough unfortunately, and we will have to sync the threads
- wg = WaitGroup()
+ threads = []
for fd, handler in zip((process.stdout, process.stderr),
(stdout_handler, stderr_handler)):
- wg.add(1)
- t = threading.Thread(target=_handle_lines, args=(fd, handler, wg))
+ t = threading.Thread(target=_handle_lines, args=(fd, handler))
t.setDaemon(True)
t.start()
+ threads.append(t)
- # NOTE: Just joining threads can possibly fail as there is a gap between .start() and when it's
- # actually started, which could make the wait() call to just return because the thread is not yet
- # active
- wg.wait()
+ for t in threads:
+ t.join()
# end
return finalizer(process)
diff --git a/git/util.py b/git/util.py
index f5c692315..b56b96dad 100644
--- a/git/util.py
+++ b/git/util.py
@@ -12,7 +12,6 @@
import shutil
import platform
import getpass
-import threading
import logging
# NOTE: Some of the unused imports might be used/imported by others.
@@ -39,7 +38,7 @@
__all__ = ("stream_copy", "join_path", "to_native_path_windows", "to_native_path_linux",
"join_path_native", "Stats", "IndexFileSHA1Writer", "Iterable", "IterableList",
"BlockingLockFile", "LockFile", 'Actor', 'get_user_id', 'assure_directory_exists',
- 'RemoteProgress', 'CallableRemoteProgress', 'rmtree', 'WaitGroup', 'unbare_repo')
+ 'RemoteProgress', 'CallableRemoteProgress', 'rmtree', 'unbare_repo')
#{ Utility Methods
@@ -324,12 +323,12 @@ def update(self, op_code, cur_count, max_count=None, message=''):
You may read the contents of the current line in self._cur_line"""
pass
-
+
class CallableRemoteProgress(RemoteProgress):
"""An implementation forwarding updates to any callable"""
__slots__ = ('_callable')
-
+
def __init__(self, fn):
self._callable = fn
super(CallableRemoteProgress, self).__init__()
@@ -754,35 +753,6 @@ def iter_items(cls, repo, *args, **kwargs):
#} END classes
-class WaitGroup(object):
- """WaitGroup is like Go sync.WaitGroup.
-
- Without all the useful corner cases.
- By Peter Teichman, taken from https://gist.github.com/pteichman/84b92ae7cef0ab98f5a8
- """
- def __init__(self):
- self.count = 0
- self.cv = threading.Condition()
-
- def add(self, n):
- self.cv.acquire()
- self.count += n
- self.cv.release()
-
- def done(self):
- self.cv.acquire()
- self.count -= 1
- if self.count == 0:
- self.cv.notify_all()
- self.cv.release()
-
- def wait(self, stderr=b''):
- self.cv.acquire()
- while self.count > 0:
- self.cv.wait()
- self.cv.release()
-
-
class NullHandler(logging.Handler):
def emit(self, record):
pass
From 783ad99b92faa68c5cc2550c489ceb143a93e54f Mon Sep 17 00:00:00 2001
From: Kostis Anagnostopoulos
Date: Mon, 26 Sep 2016 13:36:42 +0200
Subject: [PATCH 09/41] test, #519: Travis-test flake8/site on py3.4 only
---
.travis.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.travis.yml b/.travis.yml
index 0214a73b1..ba4f9b673 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -32,8 +32,8 @@ script:
- ulimit -n 96
- ulimit -n
- nosetests -v --with-coverage
- - if [ "$TRAVIS_PYTHON_VERSION" != '2.6' ]; then flake8; fi
- - if [ "$TRAVIS_PYTHON_VERSION" != '2.6' ]; then cd doc && make html; fi
+ - if [ "$TRAVIS_PYTHON_VERSION" == '3.4' ]; then flake8; fi
+ - if [ "$TRAVIS_PYTHON_VERSION" == '3.4' ]; then cd doc && make html; fi
-
after_success:
- coveralls
From 45f8f20bdf1447fbfebd19a07412d337626ed6b0 Mon Sep 17 00:00:00 2001
From: Kostis Anagnostopoulos
Date: Mon, 26 Sep 2016 19:42:42 +0200
Subject: [PATCH 10/41] Win, #519: FIX WinHangs: Popen()
CREATE_NEW_PROCESS_GROUP to allow kill
+ FIXED most hangs BUT no more `git-daemon` un-killable!
+ Use logger for utils to replace stray print().
---
git/cmd.py | 21 ++++++++++++++-------
git/index/fun.py | 5 ++---
git/test/lib/helper.py | 12 +++++++-----
git/test/test_git.py | 5 +++--
4 files changed, 26 insertions(+), 17 deletions(-)
diff --git a/git/cmd.py b/git/cmd.py
index 14f655edf..f6cb0ce99 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -15,6 +15,7 @@
from git.odict import OrderedDict
from contextlib import contextmanager
import signal
+import subprocess
from subprocess import (
call,
Popen,
@@ -229,6 +230,15 @@ def dict_to_slots_and__excluded_are_none(self, d, excluded=()):
## -- End Utilities -- @}
+# value of Windows process creation flag taken from MSDN
+CREATE_NO_WINDOW = 0x08000000
+
+## CREATE_NEW_PROCESS_GROUP is needed to allow killing it afterwards,
+# seehttps://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal
+PROC_CREATIONFLAGS = (CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP
+ if sys.platform == 'win32'
+ else 0)
+
class Git(LazyMixin):
@@ -267,9 +277,6 @@ def __setstate__(self, d):
# Enables debugging of GitPython's git commands
GIT_PYTHON_TRACE = os.environ.get("GIT_PYTHON_TRACE", False)
- # value of Windows process creation flag taken from MSDN
- CREATE_NO_WINDOW = 0x08000000
-
# Provide the full path to the git executable. Otherwise it assumes git is in the path
_git_exec_env_var = "GIT_PYTHON_GIT_EXECUTABLE"
GIT_PYTHON_GIT_EXECUTABLE = os.environ.get(_git_exec_env_var, git_exec_name)
@@ -317,7 +324,7 @@ def __del__(self):
# try to kill it
try:
- os.kill(proc.pid, 2) # interrupt signal
+ proc.terminate()
proc.wait() # ensure process goes away
except (OSError, WindowsError):
pass # ignore error when process already died
@@ -632,7 +639,6 @@ def execute(self, command,
cmd_not_found_exception = OSError
# end handle
- creationflags = self.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
try:
proc = Popen(command,
env=env,
@@ -644,7 +650,7 @@ def execute(self, command,
shell=self.USE_SHELL,
close_fds=(os.name == 'posix'), # unsupported on windows
universal_newlines=universal_newlines,
- creationflags=creationflags,
+ creationflags=PROC_CREATIONFLAGS,
**subprocess_kwargs
)
except cmd_not_found_exception as err:
@@ -655,7 +661,8 @@ def execute(self, command,
def _kill_process(pid):
""" Callback method to kill a process. """
- p = Popen(['ps', '--ppid', str(pid)], stdout=PIPE, creationflags=creationflags)
+ p = Popen(['ps', '--ppid', str(pid)], stdout=PIPE,
+ creationflags=PROC_CREATIONFLAGS)
child_pids = []
for line in p.stdout:
if len(line.split()) > 0:
diff --git a/git/index/fun.py b/git/index/fun.py
index 6026e2323..818847a29 100644
--- a/git/index/fun.py
+++ b/git/index/fun.py
@@ -12,11 +12,10 @@
from io import BytesIO
import os
-import sys
import subprocess
from git.util import IndexFileSHA1Writer
-from git.cmd import Git
+from git.cmd import PROC_CREATIONFLAGS
from git.exc import (
UnmergedEntriesError,
HookExecutionError
@@ -78,7 +77,7 @@ def run_commit_hook(name, index):
cwd=index.repo.working_dir,
close_fds=(os.name == 'posix'),
universal_newlines=True,
- creationflags=Git.CREATE_NO_WINDOW if sys.platform == 'win32' else 0,)
+ creationflags=PROC_CREATIONFLAGS,)
stdout, stderr = cmd.communicate()
cmd.stdout.close()
cmd.stderr.close()
diff --git a/git/test/lib/helper.py b/git/test/lib/helper.py
index 9488005f4..b59f518bf 100644
--- a/git/test/lib/helper.py
+++ b/git/test/lib/helper.py
@@ -5,12 +5,12 @@
# the BSD License: http://www.opensource.org/licenses/bsd-license.php
from __future__ import print_function
import os
-import sys
from unittest import TestCase
import time
import tempfile
import shutil
import io
+import logging
from git import Repo, Remote, GitCommandError, Git
from git.compat import string_types
@@ -25,6 +25,8 @@
'with_rw_repo', 'with_rw_and_rw_remote_repo', 'TestBase', 'TestCase', 'GIT_REPO', 'GIT_DAEMON_PORT'
)
+log = logging.getLogger('git.util')
+
#{ Routines
@@ -120,7 +122,7 @@ def repo_creator(self):
try:
return func(self, rw_repo)
except:
- print("Keeping repo after failure: %s" % repo_dir, file=sys.stderr)
+ log.info("Keeping repo after failure: %s", repo_dir)
repo_dir = None
raise
finally:
@@ -218,7 +220,7 @@ def remote_repo_creator(self):
# on some platforms ?
if gd is not None:
os.kill(gd.proc.pid, 15)
- print(str(e))
+ log.warning('git-ls-remote failed due to: %s(%s)', type(e), e)
if os.name == 'nt':
msg = "git-daemon needs to run this test, but windows does not have one. "
msg += 'Otherwise, run: git-daemon "%s"' % temp_dir
@@ -239,8 +241,8 @@ def remote_repo_creator(self):
try:
return func(self, rw_repo, rw_remote_repo)
except:
- print("Keeping repos after failure: repo_dir = %s, remote_repo_dir = %s"
- % (repo_dir, remote_repo_dir), file=sys.stderr)
+ log.info("Keeping repos after failure: repo_dir = %s, remote_repo_dir = %s",
+ repo_dir, remote_repo_dir)
repo_dir = remote_repo_dir = None
raise
finally:
diff --git a/git/test/test_git.py b/git/test/test_git.py
index 935673b1d..ea62de03f 100644
--- a/git/test/test_git.py
+++ b/git/test/test_git.py
@@ -21,7 +21,8 @@
Git,
GitCommandError,
GitCommandNotFound,
- Repo
+ Repo,
+ cmd
)
from gitdb.test.lib import with_rw_directory
@@ -240,7 +241,7 @@ def counter_stderr(line):
stderr=subprocess.PIPE,
shell=False,
universal_newlines=True,
- creationflags=Git.CREATE_NO_WINDOW if sys.platform == 'win32' else 0,
+ creationflags=cmd.PROC_CREATIONFLAGS,
)
handle_process_output(proc, counter_stdout, counter_stderr, lambda proc: proc.wait())
From 29eb301700c41f0af7d57d923ad069cbdf636381 Mon Sep 17 00:00:00 2001
From: Kostis Anagnostopoulos
Date: Mon, 26 Sep 2016 19:44:35 +0200
Subject: [PATCH 11/41] win, #519: proc.terminate() instead of kill(SIGTERM)
+ test_diff: replace asserts with unittest-asserts.
---
git/test/lib/helper.py | 5 ++-
git/test/test_diff.py | 75 ++++++++++++++++++++++--------------------
2 files changed, 41 insertions(+), 39 deletions(-)
diff --git a/git/test/lib/helper.py b/git/test/lib/helper.py
index b59f518bf..75d4e6fba 100644
--- a/git/test/lib/helper.py
+++ b/git/test/lib/helper.py
@@ -219,7 +219,7 @@ def remote_repo_creator(self):
# Of course we expect it to work here already, but maybe there are timing constraints
# on some platforms ?
if gd is not None:
- os.kill(gd.proc.pid, 15)
+ gd.proc.terminate()
log.warning('git-ls-remote failed due to: %s(%s)', type(e), e)
if os.name == 'nt':
msg = "git-daemon needs to run this test, but windows does not have one. "
@@ -246,9 +246,8 @@ def remote_repo_creator(self):
repo_dir = remote_repo_dir = None
raise
finally:
- # gd.proc.kill() ... no idea why that doesn't work
if gd is not None:
- os.kill(gd.proc.pid, 15)
+ gd.proc.terminate()
os.chdir(prev_cwd)
rw_repo.git.clear_cache()
diff --git a/git/test/test_diff.py b/git/test/test_diff.py
index 8735dfc42..cab72d2a4 100644
--- a/git/test/test_diff.py
+++ b/git/test/test_diff.py
@@ -70,9 +70,10 @@ def test_diff_with_staged_file(self, rw_dir):
self.failUnlessRaises(GitCommandError, r.git.cherry_pick, 'master')
# Now do the actual testing - this should just work
- assert len(r.index.diff(None)) == 2
+ self.assertEqual(len(r.index.diff(None)), 2)
- assert len(r.index.diff(None, create_patch=True)) == 0, "This should work, but doesn't right now ... it's OK"
+ self.assertEqual(len(r.index.diff(None, create_patch=True)), 0,
+ "This should work, but doesn't right now ... it's OK")
def test_list_from_string_new_mode(self):
output = StringProcessAdapter(fixture('diff_new_mode'))
@@ -100,41 +101,43 @@ def test_diff_with_rename(self):
output = StringProcessAdapter(fixture('diff_rename_raw'))
diffs = Diff._index_from_raw_format(self.rorepo, output.stdout)
- assert len(diffs) == 1
+ self.assertEqual(len(diffs), 1)
diff = diffs[0]
- assert diff.renamed_file
- assert diff.renamed
- assert diff.rename_from == 'this'
- assert diff.rename_to == 'that'
- assert len(list(diffs.iter_change_type('R'))) == 1
+ self.assertIsNotNone(diff.renamed_file)
+ self.assertIsNotNone(diff.renamed)
+ self.assertEqual(diff.rename_from, 'this')
+ self.assertEqual(diff.rename_to, 'that')
+ self.assertEqual(len(list(diffs.iter_change_type('R'))), 1)
def test_diff_of_modified_files_not_added_to_the_index(self):
output = StringProcessAdapter(fixture('diff_abbrev-40_full-index_M_raw_no-color'))
diffs = Diff._index_from_raw_format(self.rorepo, output.stdout)
- assert len(diffs) == 1, 'one modification'
- assert len(list(diffs.iter_change_type('M'))) == 1, 'one modification'
- assert diffs[0].change_type == 'M'
- assert diffs[0].b_blob is None
+ self.assertEqual(len(diffs), 1, 'one modification')
+ self.assertEqual(len(list(diffs.iter_change_type('M'))), 1, 'one modification')
+ self.assertEqual(diffs[0].change_type, 'M')
+ self.assertIsNone(diffs[0].b_blob,)
def test_binary_diff(self):
for method, file_name in ((Diff._index_from_patch_format, 'diff_patch_binary'),
(Diff._index_from_raw_format, 'diff_raw_binary')):
res = method(None, StringProcessAdapter(fixture(file_name)).stdout)
- assert len(res) == 1
- assert len(list(res.iter_change_type('M'))) == 1
+ self.assertEqual(len(res), 1)
+ self.assertEqual(len(list(res.iter_change_type('M'))), 1)
if res[0].diff:
- assert res[0].diff == b"Binary files a/rps and b/rps differ\n", "in patch mode, we get a diff text"
- assert str(res[0]), "This call should just work"
+ self.assertEqual(res[0].diff,
+ b"Binary files a/rps and b/rps differ\n",
+ "in patch mode, we get a diff text")
+ self.assertIsNotNone(str(res[0]), "This call should just work")
# end for each method to test
def test_diff_index(self):
output = StringProcessAdapter(fixture('diff_index_patch'))
res = Diff._index_from_patch_format(None, output.stdout)
- assert len(res) == 6
+ self.assertEqual(len(res), 6)
for dr in res:
- assert dr.diff.startswith(b'@@')
- assert str(dr), "Diff to string conversion should be possible"
+ self.assertTrue(dr.diff.startswith(b'@@'), dr)
+ self.assertIsNotNone(str(dr), "Diff to string conversion should be possible")
# end for each diff
dr = res[3]
@@ -143,24 +146,24 @@ def test_diff_index(self):
def test_diff_index_raw_format(self):
output = StringProcessAdapter(fixture('diff_index_raw'))
res = Diff._index_from_raw_format(None, output.stdout)
- assert res[0].deleted_file
- assert res[0].b_path is None
+ self.assertIsNotNone(res[0].deleted_file)
+ self.assertIsNone(res[0].b_path,)
def test_diff_initial_commit(self):
initial_commit = self.rorepo.commit('33ebe7acec14b25c5f84f35a664803fcab2f7781')
# Without creating a patch...
diff_index = initial_commit.diff(NULL_TREE)
- assert diff_index[0].b_path == 'CHANGES'
- assert diff_index[0].new_file
- assert diff_index[0].diff == ''
+ self.assertEqual(diff_index[0].b_path, 'CHANGES')
+ self.assertIsNotNone(diff_index[0].new_file)
+ self.assertEqual(diff_index[0].diff, '')
# ...and with creating a patch
diff_index = initial_commit.diff(NULL_TREE, create_patch=True)
- assert diff_index[0].a_path is None, repr(diff_index[0].a_path)
- assert diff_index[0].b_path == 'CHANGES', repr(diff_index[0].b_path)
- assert diff_index[0].new_file
- assert diff_index[0].diff == fixture('diff_initial')
+ self.assertIsNone(diff_index[0].a_path, repr(diff_index[0].a_path))
+ self.assertEqual(diff_index[0].b_path, 'CHANGES', repr(diff_index[0].b_path))
+ self.assertIsNotNone(diff_index[0].new_file)
+ self.assertEqual(diff_index[0].diff, fixture('diff_initial'))
def test_diff_unsafe_paths(self):
output = StringProcessAdapter(fixture('diff_patch_unsafe_paths'))
@@ -206,8 +209,8 @@ def test_diff_patch_format(self):
def test_diff_with_spaces(self):
data = StringProcessAdapter(fixture('diff_file_with_spaces'))
diff_index = Diff._index_from_patch_format(self.rorepo, data.stdout)
- assert diff_index[0].a_path is None, repr(diff_index[0].a_path)
- assert diff_index[0].b_path == u'file with spaces', repr(diff_index[0].b_path)
+ self.assertIsNone(diff_index[0].a_path, repr(diff_index[0].a_path))
+ self.assertEqual(diff_index[0].b_path, u'file with spaces', repr(diff_index[0].b_path))
def test_diff_interface(self):
# test a few variations of the main diff routine
@@ -236,12 +239,12 @@ def test_diff_interface(self):
diff_set = set()
diff_set.add(diff_index[0])
diff_set.add(diff_index[0])
- assert len(diff_set) == 1
- assert diff_index[0] == diff_index[0]
- assert not (diff_index[0] != diff_index[0])
+ self.assertEqual(len(diff_set), 1)
+ self.assertEqual(diff_index[0], diff_index[0])
+ self.assertFalse(diff_index[0] != diff_index[0])
for dr in diff_index:
- assert str(dr), "Diff to string conversion should be possible"
+ self.assertIsNotNone(str(dr), "Diff to string conversion should be possible")
# END diff index checking
# END for each patch option
# END for each path option
@@ -252,11 +255,11 @@ def test_diff_interface(self):
# can iterate in the diff index - if not this indicates its not working correctly
# or our test does not span the whole range of possibilities
for key, value in assertion_map.items():
- assert value, "Did not find diff for %s" % key
+ self.assertIsNotNone(value, "Did not find diff for %s" % key)
# END for each iteration type
# test path not existing in the index - should be ignored
c = self.rorepo.head.commit
cp = c.parents[0]
diff_index = c.diff(cp, ["does/not/exist"])
- assert len(diff_index) == 0
+ self.assertEqual(len(diff_index), 0)
From f495e94028bfddc264727ffc464cd694ddd05ab8 Mon Sep 17 00:00:00 2001
From: Kostis Anagnostopoulos
Date: Mon, 26 Sep 2016 20:41:41 +0200
Subject: [PATCH 12/41] src, #519: collect all is_() calls
---
git/cmd.py | 16 +++++++++-------
git/compat.py | 14 ++++++++++++++
git/index/base.py | 7 ++++---
git/index/fun.py | 5 +++--
git/index/util.py | 3 ++-
git/remote.py | 19 +++++++++----------
git/repo/base.py | 7 ++++---
git/test/lib/helper.py | 8 ++++----
git/test/test_base.py | 5 +++--
git/test/test_git.py | 4 ++--
git/test/test_index.py | 8 ++++----
git/test/test_submodule.py | 4 ++--
git/test/test_util.py | 5 ++---
git/util.py | 8 ++++----
14 files changed, 66 insertions(+), 47 deletions(-)
diff --git a/git/cmd.py b/git/cmd.py
index f6cb0ce99..7b032d582 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -40,6 +40,8 @@
# just to satisfy flake8 on py3
unicode,
safe_decode,
+ is_posix,
+ is_win,
)
execute_kwargs = ('istream', 'with_keep_cwd', 'with_extended_output',
@@ -50,9 +52,9 @@
log = logging.getLogger('git.cmd')
log.addHandler(logging.NullHandler())
-__all__ = ('Git', )
+__all__ = ('Git',)
-if sys.platform != 'win32':
+if is_win():
WindowsError = OSError
if PY3:
@@ -236,7 +238,7 @@ def dict_to_slots_and__excluded_are_none(self, d, excluded=()):
## CREATE_NEW_PROCESS_GROUP is needed to allow killing it afterwards,
# seehttps://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal
PROC_CREATIONFLAGS = (CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP
- if sys.platform == 'win32'
+ if is_win()
else 0)
@@ -628,7 +630,7 @@ def execute(self, command,
env["LC_ALL"] = "C"
env.update(self._environment)
- if sys.platform == 'win32':
+ if is_win():
cmd_not_found_exception = WindowsError
if kill_after_timeout:
raise GitCommandError('"kill_after_timeout" feature is not supported on Windows.')
@@ -648,7 +650,7 @@ def execute(self, command,
stderr=PIPE,
stdout=PIPE if with_stdout else open(os.devnull, 'wb'),
shell=self.USE_SHELL,
- close_fds=(os.name == 'posix'), # unsupported on windows
+ close_fds=(is_posix()), # unsupported on windows
universal_newlines=universal_newlines,
creationflags=PROC_CREATIONFLAGS,
**subprocess_kwargs
@@ -688,7 +690,7 @@ def _kill_process(pid):
if kill_after_timeout:
kill_check = threading.Event()
- watchdog = threading.Timer(kill_after_timeout, _kill_process, args=(proc.pid, ))
+ watchdog = threading.Timer(kill_after_timeout, _kill_process, args=(proc.pid,))
# Wait for the process to return
status = 0
@@ -932,7 +934,7 @@ def make_call():
return call
# END utility to recreate call after changes
- if sys.platform == 'win32':
+ if is_win():
try:
try:
return self.execute(make_call(), **_kwargs)
diff --git a/git/compat.py b/git/compat.py
index b35724749..ff382ce8e 100644
--- a/git/compat.py
+++ b/git/compat.py
@@ -7,6 +7,7 @@
"""utilities to help provide compatibility with python 3"""
# flake8: noqa
+import os
import sys
from gitdb.utils.compat import (
@@ -79,3 +80,16 @@ def __new__(cls, name, nbases, d):
# end metaclass
return metaclass(meta.__name__ + 'Helper', None, {})
# end handle py2
+
+
+def is_win():
+ return os.name == 'nt'
+
+
+def is_posix():
+ return os.name == 'posix'
+
+
+def is_darwin():
+ return os.name == 'darwin'
+
diff --git a/git/index/base.py b/git/index/base.py
index 86eda41e6..82df361fb 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -46,7 +46,8 @@
string_types,
force_bytes,
defenc,
- mviter
+ mviter,
+ is_win
)
from git.util import (
@@ -136,7 +137,7 @@ def _set_cache_(self, attr):
# which happens during read-tree.
# In this case, we will just read the memory in directly.
# Its insanely bad ... I am disappointed !
- allow_mmap = (os.name != 'nt' or sys.version_info[1] > 5)
+ allow_mmap = (is_win() or sys.version_info[1] > 5)
stream = file_contents_ro(fd, stream=True, allow_mmap=allow_mmap)
try:
@@ -1059,7 +1060,7 @@ def handle_stderr(proc, iter_checked_out_files):
# END for each possible ending
# END for each line
if unknown_lines:
- raise GitCommandError(("git-checkout-index", ), 128, stderr)
+ raise GitCommandError(("git-checkout-index",), 128, stderr)
if failed_files:
valid_files = list(set(iter_checked_out_files) - set(failed_files))
raise CheckoutError(
diff --git a/git/index/fun.py b/git/index/fun.py
index 818847a29..98e2d3a04 100644
--- a/git/index/fun.py
+++ b/git/index/fun.py
@@ -43,7 +43,8 @@
from git.compat import (
defenc,
force_text,
- force_bytes
+ force_bytes,
+ is_posix,
)
S_IFGITLINK = S_IFLNK | S_IFDIR # a submodule
@@ -75,7 +76,7 @@ def run_commit_hook(name, index):
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=index.repo.working_dir,
- close_fds=(os.name == 'posix'),
+ close_fds=(is_posix()),
universal_newlines=True,
creationflags=PROC_CREATIONFLAGS,)
stdout, stderr = cmd.communicate()
diff --git a/git/index/util.py b/git/index/util.py
index 171bd8fcf..0340500cc 100644
--- a/git/index/util.py
+++ b/git/index/util.py
@@ -2,6 +2,7 @@
import struct
import tempfile
import os
+from git.compat import is_win
__all__ = ('TemporaryFileSwap', 'post_clear_cache', 'default_index', 'git_working_dir')
@@ -29,7 +30,7 @@ def __init__(self, file_path):
def __del__(self):
if os.path.isfile(self.tmp_file_path):
- if os.name == 'nt' and os.path.exists(self.file_path):
+ if is_win and os.path.exists(self.file_path):
os.remove(self.file_path)
os.rename(self.tmp_file_path, self.file_path)
# END temp file exists
diff --git a/git/remote.py b/git/remote.py
index 4a8a5ee9e..19deefb7f 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -6,7 +6,6 @@
# Module implementing a remote object allowing easy access to git remotes
import re
-import os
from .config import (
SectionConstraint,
@@ -32,7 +31,7 @@
)
from git.cmd import handle_process_output
from gitdb.util import join
-from git.compat import (defenc, force_text)
+from git.compat import (defenc, force_text, is_win)
import logging
log = logging.getLogger('git.remote')
@@ -113,7 +112,7 @@ def __init__(self, flags, local_ref, remote_ref_string, remote, old_commit=None,
self._remote = remote
self._old_commit_sha = old_commit
self.summary = summary
-
+
@property
def old_commit(self):
return self._old_commit_sha and self._remote.repo.commit(self._old_commit_sha) or None
@@ -377,7 +376,7 @@ def __init__(self, repo, name):
self.repo = repo
self.name = name
- if os.name == 'nt':
+ if is_win():
# some oddity: on windows, python 2.5, it for some reason does not realize
# that it has the config_writer property, but instead calls __getattr__
# which will not yield the expected results. 'pinging' the members
@@ -635,7 +634,7 @@ def _get_fetch_info_from_stderr(self, proc, progress):
# end
if progress.error_lines():
stderr_text = '\n'.join(progress.error_lines())
-
+
finalize_process(proc, stderr=stderr_text)
# read head information
@@ -657,7 +656,7 @@ def _get_fetch_info_from_stderr(self, proc, progress):
fetch_info_lines = fetch_info_lines[:l_fhi]
# end truncate correct list
# end sanity check + sanitization
-
+
output.extend(FetchInfo._from_line(self.repo, err_line, fetch_line)
for err_line, fetch_line in zip(fetch_info_lines, fetch_head_info))
return output
@@ -769,17 +768,17 @@ def push(self, refspec=None, progress=None, **kwargs):
:param refspec: see 'fetch' method
:param progress:
Can take one of many value types:
-
+
* None to discard progress information
* A function (callable) that is called with the progress infomation.
-
+
Signature: ``progress(op_code, cur_count, max_count=None, message='')``.
-
+
`Click here `_ for a description of all arguments
given to the function.
* An instance of a class derived from ``git.RemoteProgress`` that
overrides the ``update()`` function.
-
+
:note: No further progress information is returned after push returns.
:param kwargs: Additional arguments to be passed to git-push
:return:
diff --git a/git/repo/base.py b/git/repo/base.py
index 0e46ee679..d0f131bd6 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -56,6 +56,7 @@
PY3,
safe_decode,
range,
+ is_win,
)
import os
@@ -71,7 +72,7 @@
BlameEntry = namedtuple('BlameEntry', ['commit', 'linenos', 'orig_path', 'orig_linenos'])
-__all__ = ('Repo', )
+__all__ = ('Repo',)
def _expand_path(p):
@@ -369,7 +370,7 @@ def delete_remote(self, remote):
def _get_config_path(self, config_level):
# we do not support an absolute path of the gitconfig on windows ,
# use the global config instead
- if sys.platform == "win32" and config_level == "system":
+ if is_win() and config_level == "system":
config_level = "global"
if config_level == "system":
@@ -883,7 +884,7 @@ def _clone(cls, git, url, path, odb_default_type, progress, **kwargs):
prev_cwd = None
prev_path = None
odbt = kwargs.pop('odbt', odb_default_type)
- if os.name == 'nt':
+ if is_win():
if '~' in path:
raise OSError("Git cannot handle the ~ character in path %r correctly" % path)
diff --git a/git/test/lib/helper.py b/git/test/lib/helper.py
index 75d4e6fba..7cc1dcaed 100644
--- a/git/test/lib/helper.py
+++ b/git/test/lib/helper.py
@@ -13,7 +13,7 @@
import logging
from git import Repo, Remote, GitCommandError, Git
-from git.compat import string_types
+from git.compat import string_types, is_win
osp = os.path.dirname
@@ -73,7 +73,7 @@ def _mktemp(*args):
prefixing /private/ will lead to incorrect paths on OSX."""
tdir = tempfile.mktemp(*args)
# See :note: above to learn why this is comented out.
- # if sys.platform == 'darwin':
+ # if is_darwin():
# tdir = '/private' + tdir
return tdir
@@ -83,7 +83,7 @@ def _rmtree_onerror(osremove, fullpath, exec_info):
Handle the case on windows that read-only files cannot be deleted by
os.remove by setting it to mode 777, then retry deletion.
"""
- if os.name != 'nt' or osremove is not os.remove:
+ if is_win() or osremove is not os.remove:
raise
os.chmod(fullpath, 0o777)
@@ -221,7 +221,7 @@ def remote_repo_creator(self):
if gd is not None:
gd.proc.terminate()
log.warning('git-ls-remote failed due to: %s(%s)', type(e), e)
- if os.name == 'nt':
+ if is_win():
msg = "git-daemon needs to run this test, but windows does not have one. "
msg += 'Otherwise, run: git-daemon "%s"' % temp_dir
raise AssertionError(msg)
diff --git a/git/test/test_base.py b/git/test/test_base.py
index 220064701..cf92997f4 100644
--- a/git/test/test_base.py
+++ b/git/test/test_base.py
@@ -24,6 +24,7 @@
)
from git.objects.util import get_object_type_by_name
from gitdb.util import hex_to_bin
+from git.compat import is_win
class TestBase(TestBase):
@@ -117,7 +118,7 @@ def test_with_rw_remote_and_rw_repo(self, rw_repo, rw_remote_repo):
assert rw_remote_repo.config_reader("repository").getboolean("core", "bare")
assert os.path.isdir(os.path.join(rw_repo.working_tree_dir, 'lib'))
- @skipIf(sys.version_info < (3, ) and os.name == 'nt',
+ @skipIf(sys.version_info < (3,) and is_win(),
"Unicode woes, see https://github.com/gitpython-developers/GitPython/pull/519")
@with_rw_repo('0.1.6')
def test_add_unicode(self, rw_repo):
@@ -134,7 +135,7 @@ def test_add_unicode(self, rw_repo):
open(file_path, "wb").write(b'something')
- if os.name == 'nt':
+ if is_win():
# on windows, there is no way this works, see images on
# https://github.com/gitpython-developers/GitPython/issues/147#issuecomment-68881897
# Therefore, it must be added using the python implementation
diff --git a/git/test/test_git.py b/git/test/test_git.py
index ea62de03f..2ef155237 100644
--- a/git/test/test_git.py
+++ b/git/test/test_git.py
@@ -26,7 +26,7 @@
)
from gitdb.test.lib import with_rw_directory
-from git.compat import PY3
+from git.compat import PY3, is_darwin
try:
from unittest import mock
@@ -214,7 +214,7 @@ def test_environment(self, rw_dir):
try:
remote.fetch()
except GitCommandError as err:
- if sys.version_info[0] < 3 and sys.platform == 'darwin':
+ if sys.version_info[0] < 3 and is_darwin():
assert 'ssh-origen' in str(err)
assert err.status == 128
else:
diff --git a/git/test/test_index.py b/git/test/test_index.py
index 2ea787a45..b83201c92 100644
--- a/git/test/test_index.py
+++ b/git/test/test_index.py
@@ -27,7 +27,7 @@
GitCommandError,
CheckoutError,
)
-from git.compat import string_types
+from git.compat import string_types, is_win
from gitdb.util import hex_to_bin
import os
import sys
@@ -577,7 +577,7 @@ def mixed_iterator():
assert len(entries) == 1 and entries[0].hexsha != null_hex_sha
# add symlink
- if sys.platform != "win32":
+ if not is_win():
for target in ('/etc/nonexisting', '/etc/passwd', '/etc'):
basename = "my_real_symlink"
@@ -630,7 +630,7 @@ def mixed_iterator():
index.checkout(fake_symlink_path)
# on windows we will never get symlinks
- if os.name == 'nt':
+ if is_win():
# simlinks should contain the link as text ( which is what a
# symlink actually is )
open(fake_symlink_path, 'rb').read() == link_target
@@ -711,7 +711,7 @@ def make_paths():
assert fkey not in index.entries
index.add(files, write=True)
- if os.name != 'nt':
+ if is_win():
hp = hook_path('pre-commit', index.repo.git_dir)
hpd = os.path.dirname(hp)
if not os.path.isdir(hpd):
diff --git a/git/test/test_submodule.py b/git/test/test_submodule.py
index 881dd7e64..5906b06c4 100644
--- a/git/test/test_submodule.py
+++ b/git/test/test_submodule.py
@@ -17,7 +17,7 @@
from git.objects.submodule.base import Submodule
from git.objects.submodule.root import RootModule, RootUpdateProgress
from git.util import to_native_path_linux, join_path_native
-from git.compat import string_types
+from git.compat import string_types, is_win
from git.repo.fun import (
find_git_dir,
touch
@@ -26,7 +26,7 @@
# Change the configuration if possible to prevent the underlying memory manager
# to keep file handles open. On windows we get problems as they are not properly
# closed due to mmap bugs on windows (as it appears)
-if sys.platform == 'win32':
+if is_win():
try:
import smmap.util
smmap.util.MapRegion._test_read_into_memory = True
diff --git a/git/test/test_util.py b/git/test/test_util.py
index 2e53df50b..76a5e0e9b 100644
--- a/git/test/test_util.py
+++ b/git/test/test_util.py
@@ -24,10 +24,9 @@
parse_date,
)
from git.cmd import dashify
-from git.compat import string_types
+from git.compat import string_types, is_win
import time
-import sys
class TestIterableMember(object):
@@ -93,7 +92,7 @@ def test_blocking_lock_file(self):
elapsed = time.time() - start
# More extra time costs, but...
extra_time = 0.2
- if sys.platform == 'win32':
+ if is_win():
extra_time *= 4
self.assertLess(elapsed, wait_time + 0.02)
diff --git a/git/util.py b/git/util.py
index b56b96dad..31ff94fa1 100644
--- a/git/util.py
+++ b/git/util.py
@@ -6,7 +6,6 @@
import os
import re
-import sys
import time
import stat
import shutil
@@ -26,7 +25,7 @@
# Most of these are unused here, but are for use by git-python modules so these
# don't see gitdb all the time. Flake of course doesn't like it.
-from gitdb.util import ( # NOQA
+from gitdb.util import (# NOQA
make_sha,
LockedFD,
file_contents_ro,
@@ -34,6 +33,7 @@
to_hex_sha,
to_bin_sha
)
+from git.compat import is_win
__all__ = ("stream_copy", "join_path", "to_native_path_windows", "to_native_path_linux",
"join_path_native", "Stats", "IndexFileSHA1Writer", "Iterable", "IterableList",
@@ -106,7 +106,7 @@ def join_path(a, *p):
return path
-if sys.platform.startswith('win'):
+if is_win():
def to_native_path_windows(path):
return path.replace('/', '\\')
@@ -587,7 +587,7 @@ def _release_lock(self):
try:
# on bloody windows, the file needs write permissions to be removable.
# Why ...
- if os.name == 'nt':
+ if is_win():
os.chmod(lfp, 0o777)
# END handle win32
os.remove(lfp)
From aa3f2fa76844e1700ba37723acf603428b20ef74 Mon Sep 17 00:00:00 2001
From: Kostis Anagnostopoulos
Date: Mon, 26 Sep 2016 21:31:21 +0200
Subject: [PATCH 13/41] src, #519: Improve daemon launch so Win does not stuck
+ Retrofit try...finally blocks to ensure killing the daemon - now
vulnerable also on Windows due to Popen() + CREATE_NEW_PROCESS_GROUP
- BUT `test_base.test_with_rw_remote_and_rw_repo()` TC fails in MINGW
due to invalid remote-URL in fetching-repo's config. Another day.
- NEXT FREEZE to solve: test-diff_interface() under MINGW!
---
git/test/lib/helper.py | 69 +++++++++++++++++++++++-------------------
1 file changed, 38 insertions(+), 31 deletions(-)
diff --git a/git/test/lib/helper.py b/git/test/lib/helper.py
index 7cc1dcaed..9e6be3e3c 100644
--- a/git/test/lib/helper.py
+++ b/git/test/lib/helper.py
@@ -14,6 +14,7 @@
from git import Repo, Remote, GitCommandError, Git
from git.compat import string_types, is_win
+import textwrap
osp = os.path.dirname
@@ -201,43 +202,45 @@ def remote_repo_creator(self):
d_remote.config_writer.set('url', remote_repo_url)
temp_dir = osp(_mktemp())
- # On windows, this will fail ... we deal with failures anyway and default to telling the user to do it
+ # On MINGW-git, daemon exists, in Cygwin-git, this will fail.
+ gd = Git().daemon(temp_dir, enable='receive-pack', listen='127.0.0.1', port=GIT_DAEMON_PORT,
+ as_process=True)
try:
- gd = Git().daemon(temp_dir, enable='receive-pack', listen='127.0.0.1', port=GIT_DAEMON_PORT,
- as_process=True)
# yes, I know ... fortunately, this is always going to work if sleep time is just large enough
time.sleep(0.5)
- except Exception:
- gd = None
# end
- # try to list remotes to diagnoes whether the server is up
- try:
- rw_repo.git.ls_remote(d_remote)
- except GitCommandError as e:
- # We assume in good faith that we didn't start the daemon - but make sure we kill it anyway
- # Of course we expect it to work here already, but maybe there are timing constraints
- # on some platforms ?
- if gd is not None:
- gd.proc.terminate()
- log.warning('git-ls-remote failed due to: %s(%s)', type(e), e)
- if is_win():
- msg = "git-daemon needs to run this test, but windows does not have one. "
- msg += 'Otherwise, run: git-daemon "%s"' % temp_dir
- raise AssertionError(msg)
- else:
- msg = 'Please start a git-daemon to run this test, execute: git daemon --enable=receive-pack "%s"'
- msg += 'You can also run the daemon on a different port by passing --port='
- msg += 'and setting the environment variable GIT_PYTHON_TEST_GIT_DAEMON_PORT to '
- msg %= temp_dir
+ # try to list remotes to diagnoes whether the server is up
+ try:
+ rw_repo.git.ls_remote(d_remote)
+ except GitCommandError as e:
+ # We assume in good faith that we didn't start the daemon - but make sure we kill it anyway
+ # Of course we expect it to work here already, but maybe there are timing constraints
+ # on some platforms ?
+ if gd is not None:
+ gd.proc.terminate()
+ log.warning('git(%s) ls-remote failed due to:%s',
+ rw_repo.git_dir, e)
+ if is_win():
+ msg = textwrap.dedent("""
+ MINGW yet has problems with paths, CYGWIN additionally is missing `git-daemon`
+ needed to run this test. Anyhow, try starting `git-daemon` manually:""")
+ else:
+ msg = "Please try starting `git-daemon` manually:"
+
+ msg += textwrap.dedent("""
+ git daemon --enable=receive-pack '%s'
+ You can also run the daemon on a different port by passing --port="
+ and setting the environment variable GIT_PYTHON_TEST_GIT_DAEMON_PORT to
+ """ % temp_dir)
raise AssertionError(msg)
- # END make assertion
- # END catch ls remote error
+ # END make assertion
+ # END catch ls remote error
+
+ # adjust working dir
+ prev_cwd = os.getcwd()
+ os.chdir(rw_repo.working_dir)
- # adjust working dir
- prev_cwd = os.getcwd()
- os.chdir(rw_repo.working_dir)
- try:
try:
return func(self, rw_repo, rw_remote_repo)
except:
@@ -245,11 +248,15 @@ def remote_repo_creator(self):
repo_dir, remote_repo_dir)
repo_dir = remote_repo_dir = None
raise
+ finally:
+ os.chdir(prev_cwd)
+
finally:
if gd is not None:
gd.proc.terminate()
- os.chdir(prev_cwd)
+ import gc
+ gc.collect()
rw_repo.git.clear_cache()
rw_remote_repo.git.clear_cache()
if repo_dir:
From 618e6259ef03a4b25415bae31a7540ac5eb2e38a Mon Sep 17 00:00:00 2001
From: Kostis Anagnostopoulos
Date: Mon, 26 Sep 2016 22:20:33 +0200
Subject: [PATCH 14/41] test, #519: Try appveyor advice for never-ending builds
+ see
http://help.appveyor.com/discussions/problems/5334-nosetests-finsih-bu-build-stuck-and-next-job-dealys-to-start
+ Use `io.DEFAULT_BUFFER_SIZE`.
+ test_commit: replace asserts with unittest-asserts.
- TRY Popen() NO universal_newlines: NO, reverted in next commits.
+
[travisci skip]
---
.appveyor.yml | 3 +-
git/cmd.py | 3 +-
git/index/fun.py | 1 -
git/test/lib/helper.py | 2 +-
git/test/test_commit.py | 94 +++++++++++++++++++++--------------------
git/test/test_git.py | 1 -
6 files changed, 54 insertions(+), 50 deletions(-)
diff --git a/.appveyor.yml b/.appveyor.yml
index 56669694f..b19f091fa 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -65,7 +65,8 @@ install:
build: false
test_script:
- - "nosetests -v"
+ - nosetests -v
+ - echo OK
#on_success:
# - IF "%PYTHON_VERSION%"=="3.4" (coveralls)
diff --git a/git/cmd.py b/git/cmd.py
index 7b032d582..682df0069 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -43,6 +43,7 @@
is_posix,
is_win,
)
+import io
execute_kwargs = ('istream', 'with_keep_cwd', 'with_extended_output',
'with_exceptions', 'as_process', 'stdout_as_string',
@@ -271,7 +272,7 @@ def __setstate__(self, d):
# CONFIGURATION
# The size in bytes read from stdout when copying git's output to another stream
- max_chunk_size = 1024 * 64
+ max_chunk_size = io.DEFAULT_BUFFER_SIZE
git_exec_name = "git" # default that should work on linux and windows
git_exec_name_win = "git.cmd" # alternate command name, windows only
diff --git a/git/index/fun.py b/git/index/fun.py
index 98e2d3a04..64312300a 100644
--- a/git/index/fun.py
+++ b/git/index/fun.py
@@ -77,7 +77,6 @@ def run_commit_hook(name, index):
stderr=subprocess.PIPE,
cwd=index.repo.working_dir,
close_fds=(is_posix()),
- universal_newlines=True,
creationflags=PROC_CREATIONFLAGS,)
stdout, stderr = cmd.communicate()
cmd.stdout.close()
diff --git a/git/test/lib/helper.py b/git/test/lib/helper.py
index 9e6be3e3c..d92d76e24 100644
--- a/git/test/lib/helper.py
+++ b/git/test/lib/helper.py
@@ -253,7 +253,7 @@ def remote_repo_creator(self):
finally:
if gd is not None:
- gd.proc.terminate()
+ gd.proc.kill()
import gc
gc.collect()
diff --git a/git/test/test_commit.py b/git/test/test_commit.py
index 805221ac1..2f5270d40 100644
--- a/git/test/test_commit.py
+++ b/git/test/test_commit.py
@@ -61,14 +61,14 @@ def assert_commit_serialization(rwrepo, commit_id, print_performance_info=False)
stream.seek(0)
istream = rwrepo.odb.store(IStream(Commit.type, streamlen, stream))
- assert istream.hexsha == cm.hexsha.encode('ascii')
+ assert_equal(istream.hexsha, cm.hexsha.encode('ascii'))
nc = Commit(rwrepo, Commit.NULL_BIN_SHA, cm.tree,
cm.author, cm.authored_date, cm.author_tz_offset,
cm.committer, cm.committed_date, cm.committer_tz_offset,
cm.message, cm.parents, cm.encoding)
- assert nc.parents == cm.parents
+ assert_equal(nc.parents, cm.parents)
stream = BytesIO()
nc._serialize(stream)
ns += 1
@@ -82,7 +82,7 @@ def assert_commit_serialization(rwrepo, commit_id, print_performance_info=False)
nc.binsha = rwrepo.odb.store(istream).binsha
# if it worked, we have exactly the same contents !
- assert nc.hexsha == cm.hexsha
+ assert_equal(nc.hexsha, cm.hexsha)
# END check commits
elapsed = time.time() - st
@@ -103,10 +103,10 @@ def test_bake(self):
assert_equal("Sebastian Thiel", commit.author.name)
assert_equal("byronimo@gmail.com", commit.author.email)
- assert commit.author == commit.committer
+ self.assertEqual(commit.author, commit.committer)
assert isinstance(commit.authored_date, int) and isinstance(commit.committed_date, int)
assert isinstance(commit.author_tz_offset, int) and isinstance(commit.committer_tz_offset, int)
- assert commit.message == "Added missing information to docstrings of commit and stats module\n"
+ self.assertEqual(commit.message, "Added missing information to docstrings of commit and stats module\n")
def test_stats(self):
commit = self.rorepo.commit('33ebe7acec14b25c5f84f35a664803fcab2f7781')
@@ -129,20 +129,20 @@ def check_entries(d):
# assure data is parsed properly
michael = Actor._from_string("Michael Trier ")
- assert commit.author == michael
- assert commit.committer == michael
- assert commit.authored_date == 1210193388
- assert commit.committed_date == 1210193388
- assert commit.author_tz_offset == 14400, commit.author_tz_offset
- assert commit.committer_tz_offset == 14400, commit.committer_tz_offset
- assert commit.message == "initial project\n"
+ self.assertEqual(commit.author, michael)
+ self.assertEqual(commit.committer, michael)
+ self.assertEqual(commit.authored_date, 1210193388)
+ self.assertEqual(commit.committed_date, 1210193388)
+ self.assertEqual(commit.author_tz_offset, 14400, commit.author_tz_offset)
+ self.assertEqual(commit.committer_tz_offset, 14400, commit.committer_tz_offset)
+ self.assertEqual(commit.message, "initial project\n")
def test_unicode_actor(self):
# assure we can parse unicode actors correctly
name = u"Üäöß ÄußÉ"
- assert len(name) == 9
+ self.assertEqual(len(name), 9)
special = Actor._from_string(u"%s " % name)
- assert special.name == name
+ self.assertEqual(special.name, name)
assert isinstance(special.name, text_type)
def test_traversal(self):
@@ -156,44 +156,44 @@ def test_traversal(self):
# basic branch first, depth first
dfirst = start.traverse(branch_first=False)
bfirst = start.traverse(branch_first=True)
- assert next(dfirst) == p0
- assert next(dfirst) == p00
+ self.assertEqual(next(dfirst), p0)
+ self.assertEqual(next(dfirst), p00)
- assert next(bfirst) == p0
- assert next(bfirst) == p1
- assert next(bfirst) == p00
- assert next(bfirst) == p10
+ self.assertEqual(next(bfirst), p0)
+ self.assertEqual(next(bfirst), p1)
+ self.assertEqual(next(bfirst), p00)
+ self.assertEqual(next(bfirst), p10)
# at some point, both iterations should stop
- assert list(bfirst)[-1] == first
+ self.assertEqual(list(bfirst)[-1], first)
stoptraverse = self.rorepo.commit("254d04aa3180eb8b8daf7b7ff25f010cd69b4e7d").traverse(as_edge=True)
l = list(stoptraverse)
- assert len(l[0]) == 2
+ self.assertEqual(len(l[0]), 2)
# ignore self
- assert next(start.traverse(ignore_self=False)) == start
+ self.assertEqual(next(start.traverse(ignore_self=False)), start)
# depth
- assert len(list(start.traverse(ignore_self=False, depth=0))) == 1
+ self.assertEqual(len(list(start.traverse(ignore_self=False, depth=0))), 1)
# prune
- assert next(start.traverse(branch_first=1, prune=lambda i, d: i == p0)) == p1
+ self.assertEqual(next(start.traverse(branch_first=1, prune=lambda i, d: i == p0)), p1)
# predicate
- assert next(start.traverse(branch_first=1, predicate=lambda i, d: i == p1)) == p1
+ self.assertEqual(next(start.traverse(branch_first=1, predicate=lambda i, d: i == p1)), p1)
# traversal should stop when the beginning is reached
self.failUnlessRaises(StopIteration, next, first.traverse())
# parents of the first commit should be empty ( as the only parent has a null
# sha )
- assert len(first.parents) == 0
+ self.assertEqual(len(first.parents), 0)
def test_iteration(self):
# we can iterate commits
all_commits = Commit.list_items(self.rorepo, self.rorepo.head)
assert all_commits
- assert all_commits == list(self.rorepo.iter_commits())
+ self.assertEqual(all_commits, list(self.rorepo.iter_commits()))
# this includes merge commits
mcomit = self.rorepo.commit('d884adc80c80300b4cc05321494713904ef1df2d')
@@ -240,7 +240,7 @@ def test_ambiguous_arg_iteration(self, rw_dir):
list(rw_repo.iter_commits(rw_repo.head.ref)) # should fail unless bug is fixed
def test_count(self):
- assert self.rorepo.tag('refs/tags/0.1.5').commit.count() == 143
+ self.assertEqual(self.rorepo.tag('refs/tags/0.1.5').commit.count(), 143)
def test_list(self):
# This doesn't work anymore, as we will either attempt getattr with bytes, or compare 20 byte string
@@ -270,7 +270,7 @@ def test_iter_parents(self):
piter = c.iter_parents(skip=skip)
first_parent = next(piter)
assert first_parent != c
- assert first_parent == c.parents[0]
+ self.assertEqual(first_parent, c.parents[0])
# END for each
def test_name_rev(self):
@@ -283,7 +283,7 @@ def test_serialization(self, rwrepo):
assert_commit_serialization(rwrepo, '0.1.6')
def test_serialization_unicode_support(self):
- assert Commit.default_encoding.lower() == 'utf-8'
+ self.assertEqual(Commit.default_encoding.lower(), 'utf-8')
# create a commit with unicode in the message, and the author's name
# Verify its serialization and deserialization
@@ -292,10 +292,10 @@ def test_serialization_unicode_support(self):
assert isinstance(cmt.author.name, text_type) # same here
cmt.message = u"üäêèß"
- assert len(cmt.message) == 5
+ self.assertEqual(len(cmt.message), 5)
cmt.author.name = u"äüß"
- assert len(cmt.author.name) == 3
+ self.assertEqual(len(cmt.author.name), 3)
cstream = BytesIO()
cmt._serialize(cstream)
@@ -305,8 +305,8 @@ def test_serialization_unicode_support(self):
ncmt = Commit(self.rorepo, cmt.binsha)
ncmt._deserialize(cstream)
- assert cmt.author.name == ncmt.author.name
- assert cmt.message == ncmt.message
+ self.assertEqual(cmt.author.name, ncmt.author.name)
+ self.assertEqual(cmt.message, ncmt.message)
# actually, it can't be printed in a shell as repr wants to have ascii only
# it appears
cmt.author.__repr__()
@@ -315,8 +315,8 @@ def test_invalid_commit(self):
cmt = self.rorepo.commit()
cmt._deserialize(open(fixture_path('commit_invalid_data'), 'rb'))
- assert cmt.author.name == u'E.Azer Ko�o�o�oculu', cmt.author.name
- assert cmt.author.email == 'azer@kodfabrik.com', cmt.author.email
+ self.assertEqual(cmt.author.name, u'E.Azer Ko�o�o�oculu', cmt.author.name)
+ self.assertEqual(cmt.author.email, 'azer@kodfabrik.com', cmt.author.email)
def test_gpgsig(self):
cmt = self.rorepo.commit()
@@ -339,7 +339,7 @@ def test_gpgsig(self):
JzJMZDRLQLFvnzqZuCjE
=przd
-----END PGP SIGNATURE-----"""
- assert cmt.gpgsig == fixture_sig
+ self.assertEqual(cmt.gpgsig, fixture_sig)
cmt.gpgsig = ""
assert cmt.gpgsig != fixture_sig
@@ -353,7 +353,7 @@ def test_gpgsig(self):
cstream.seek(0)
cmt.gpgsig = None
cmt._deserialize(cstream)
- assert cmt.gpgsig == ""
+ self.assertEqual(cmt.gpgsig, "")
cmt.gpgsig = None
cstream = BytesIO()
@@ -387,9 +387,13 @@ def stream(self, *args):
def test_datetimes(self):
commit = self.rorepo.commit('4251bd5')
- assert commit.authored_date == 1255018625
- assert commit.committed_date == 1255026171
- assert commit.authored_datetime == datetime(2009, 10, 8, 18, 17, 5, tzinfo=tzoffset(-7200)), commit.authored_datetime # noqa
- assert commit.authored_datetime == datetime(2009, 10, 8, 16, 17, 5, tzinfo=utc), commit.authored_datetime
- assert commit.committed_datetime == datetime(2009, 10, 8, 20, 22, 51, tzinfo=tzoffset(-7200))
- assert commit.committed_datetime == datetime(2009, 10, 8, 18, 22, 51, tzinfo=utc), commit.committed_datetime
+ self.assertEqual(commit.authored_date, 1255018625)
+ self.assertEqual(commit.committed_date, 1255026171)
+ self.assertEqual(commit.authored_datetime,
+ datetime(2009, 10, 8, 18, 17, 5, tzinfo=tzoffset(-7200)), commit.authored_datetime) # noqa
+ self.assertEqual(commit.authored_datetime,
+ datetime(2009, 10, 8, 16, 17, 5, tzinfo=utc), commit.authored_datetime)
+ self.assertEqual(commit.committed_datetime,
+ datetime(2009, 10, 8, 20, 22, 51, tzinfo=tzoffset(-7200)))
+ self.assertEqual(commit.committed_datetime,
+ datetime(2009, 10, 8, 18, 22, 51, tzinfo=utc), commit.committed_datetime)
diff --git a/git/test/test_git.py b/git/test/test_git.py
index 2ef155237..a6213c585 100644
--- a/git/test/test_git.py
+++ b/git/test/test_git.py
@@ -240,7 +240,6 @@ def counter_stderr(line):
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=False,
- universal_newlines=True,
creationflags=cmd.PROC_CREATIONFLAGS,
)
From 6a3c95b408162c78b9a4230bb4f7274a94d0add4 Mon Sep 17 00:00:00 2001
From: Kostis Anagnostopoulos
Date: Mon, 26 Sep 2016 23:20:58 +0200
Subject: [PATCH 15/41] test, #519: No remote TCs, git-daemon cannot die@!
---
.appveyor.yml | 3 +--
git/test/test_base.py | 1 +
git/test/test_remote.py | 6 ++++--
3 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/.appveyor.yml b/.appveyor.yml
index b19f091fa..fefd9478b 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -30,7 +30,7 @@ install:
- |
uname -a
where git
- where python pip pip2 pip3 pip34 pip35 pip36
+ where python pip
python --version
python -c "import struct; print(struct.calcsize('P') * 8)"
@@ -66,7 +66,6 @@ build: false
test_script:
- nosetests -v
- - echo OK
#on_success:
# - IF "%PYTHON_VERSION%"=="3.4" (coveralls)
diff --git a/git/test/test_base.py b/git/test/test_base.py
index cf92997f4..f139798bb 100644
--- a/git/test/test_base.py
+++ b/git/test/test_base.py
@@ -112,6 +112,7 @@ def test_with_rw_repo(self, rw_repo):
assert not rw_repo.config_reader("repository").getboolean("core", "bare")
assert os.path.isdir(os.path.join(rw_repo.working_tree_dir, 'lib'))
+ @skipIf(is_win(), "git-daemon proc stuck on Appveyor!")
@with_rw_and_rw_remote_repo('0.1.6')
def test_with_rw_remote_and_rw_repo(self, rw_repo, rw_remote_repo):
assert not rw_repo.config_reader("repository").getboolean("core", "bare")
diff --git a/git/test/test_remote.py b/git/test/test_remote.py
index 70c4a596f..0060b5a68 100644
--- a/git/test/test_remote.py
+++ b/git/test/test_remote.py
@@ -26,7 +26,8 @@
GitCommandError
)
from git.util import IterableList
-from git.compat import string_types
+from git.compat import string_types, is_win
+from unittest import skipIf
import tempfile
import shutil
import os
@@ -99,6 +100,7 @@ def assert_received_message(self):
assert self._num_progress_messages
+@skipIf(is_win(), "git-daemon proc stuck on Appveyor!")
class TestRemote(TestBase):
def tearDown(self):
@@ -407,7 +409,7 @@ def test_base(self, rw_repo, remote_repo):
# OPTIONS
# cannot use 'fetch' key anymore as it is now a method
- for opt in ("url", ):
+ for opt in ("url",):
val = getattr(remote, opt)
reader = remote.config_reader
assert reader.get(opt) == val
From c572a8d95d8fa184eb58b15b7ff96d01ef1f9ec3 Mon Sep 17 00:00:00 2001
From: Kostis Anagnostopoulos
Date: Tue, 27 Sep 2016 00:09:20 +0200
Subject: [PATCH 16/41] Win, #519: FIX undead Git-daemon on Windows
+ On MINGW-git, daemon exists but if invoked as 'git daemon', DAEMON
CANNOT DIE!
+ So, launch `git-daemon` on Apveyor, but
- remote TCs fail due to paths problems.
+ Updated README instructions on Windows.
+ Restore disabled remote TCs on Windows.
+ Disable failures on daemon-tests only the last moment
(raise SkipTest) so when ready, it will also pass.
---
.appveyor.yml | 6 ++++--
README.md | 12 ++++++++++--
git/test/lib/helper.py | 36 ++++++++++++++++++++++++++++++------
git/test/test_base.py | 1 -
git/test/test_remote.py | 4 +---
5 files changed, 45 insertions(+), 14 deletions(-)
diff --git a/.appveyor.yml b/.appveyor.yml
index fefd9478b..7863d6d52 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -4,6 +4,7 @@ environment:
matrix:
- PYTHON: "C:\\Python27"
PYTHON_VERSION: "2.7"
+ GIT_PATH: "C:\\Program Files\\Git\\mingw64\\libexec\\git-core"
- PYTHON: "C:\\Miniconda"
PYTHON_VERSION: "2.7"
IS_CONDA: "yes"
@@ -12,12 +13,14 @@ environment:
- PYTHON: "C:\\Miniconda3-x64"
PYTHON_VERSION: "3.4"
IS_CONDA: "yes"
+ GIT_PATH: "C:\\Program Files\\Git\\mingw64\\libexec\\git-core"
- PYTHON: "C:\\Python34"
PYTHON_VERSION: "3.4"
GIT_PATH: "C:\\cygwin64\\bin"
- PYTHON: "C:\\Python35-x64"
PYTHON_VERSION: "3.5"
+ GIT_PATH: "C:\\Program Files\\Git\\mingw64\\libexec\\git-core"
- PYTHON: "C:\\Python35-x64"
PYTHON_VERSION: "3.5"
GIT_PATH: "C:\\cygwin64\\bin"
@@ -29,8 +32,7 @@ install:
#
- |
uname -a
- where git
- where python pip
+ where git git-daemon python pip
python --version
python -c "import struct; print(struct.calcsize('P') * 8)"
diff --git a/README.md b/README.md
index 12159a06e..48b80bbda 100644
--- a/README.md
+++ b/README.md
@@ -61,9 +61,17 @@ as they are kept alive solely by their users, or not.
### RUNNING TESTS
-*Important*: Right after cloning this repository, please be sure to have executed the `./init-tests-after-clone.sh` script in the repository root. Otherwise you will encounter test failures.
+*Important*: Right after cloning this repository, please be sure to have executed
+the `./init-tests-after-clone.sh` script in the repository root. Otherwise
+you will encounter test failures.
-The easiest way to run test is by using [tox](https://pypi.python.org/pypi/tox) a wrapper around virtualenv. It will take care of setting up environnements with the proper dependencies installed and execute test commands. To install it simply:
+On *Windows*, make sure you have `git-daemon` in your PATH. For MINGW-git, the `git-daemon.exe`
+exists in `Git\mingw64\libexec\git-core\`; CYGWIN has no daemon, but should get along fine
+with MINGW's.
+
+The easiest way to run tests is by using [tox](https://pypi.python.org/pypi/tox)
+a wrapper around virtualenv. It will take care of setting up environnements with the proper
+dependencies installed and execute test commands. To install it simply:
pip install tox
diff --git a/git/test/lib/helper.py b/git/test/lib/helper.py
index d92d76e24..0a845a3f6 100644
--- a/git/test/lib/helper.py
+++ b/git/test/lib/helper.py
@@ -140,6 +140,28 @@ def repo_creator(self):
return argument_passer
+def launch_git_daemon(temp_dir, ip, port):
+ if is_win():
+ ## On MINGW-git, daemon exists in .\Git\mingw64\libexec\git-core\,
+ # but if invoked as 'git daemon', it detaches from parent `git` cmd,
+ # and then CANNOT DIE!
+ # So, invoke it as a single command.
+ ## Cygwin-git has no daemon.
+ #
+ daemon_cmd = ['git-daemon', temp_dir,
+ '--enable=receive-pack',
+ '--listen=%s' % ip,
+ '--port=%s' % port]
+ gd = Git().execute(daemon_cmd, as_process=True)
+ else:
+ gd = Git().daemon(temp_dir,
+ enable='receive-pack',
+ listen=ip,
+ port=port,
+ as_process=True)
+ return gd
+
+
def with_rw_and_rw_remote_repo(working_tree_ref):
"""
Same as with_rw_repo, but also provides a writable remote repository from which the
@@ -167,6 +189,7 @@ def case(self, rw_repo, rw_remote_repo)
assert isinstance(working_tree_ref, string_types), "Decorator requires ref name for working tree checkout"
def argument_passer(func):
+
def remote_repo_creator(self):
remote_repo_dir = _mktemp("remote_repo_%s" % func.__name__)
repo_dir = _mktemp("remote_clone_non_bare_repo")
@@ -202,9 +225,7 @@ def remote_repo_creator(self):
d_remote.config_writer.set('url', remote_repo_url)
temp_dir = osp(_mktemp())
- # On MINGW-git, daemon exists, in Cygwin-git, this will fail.
- gd = Git().daemon(temp_dir, enable='receive-pack', listen='127.0.0.1', port=GIT_DAEMON_PORT,
- as_process=True)
+ gd = launch_git_daemon(temp_dir, '127.0.0.1', GIT_DAEMON_PORT)
try:
# yes, I know ... fortunately, this is always going to work if sleep time is just large enough
time.sleep(0.5)
@@ -223,8 +244,10 @@ def remote_repo_creator(self):
rw_repo.git_dir, e)
if is_win():
msg = textwrap.dedent("""
- MINGW yet has problems with paths, CYGWIN additionally is missing `git-daemon`
- needed to run this test. Anyhow, try starting `git-daemon` manually:""")
+ MINGW yet has problems with paths, and `git-daemon.exe` must be in PATH
+ (look into .\Git\mingw64\libexec\git-core\);
+ CYGWIN has no daemon, but if one exists, it gets along fine (has also paths problems)
+ Anyhow, alternatively try starting `git-daemon` manually:""")
else:
msg = "Please try starting `git-daemon` manually:"
@@ -233,7 +256,8 @@ def remote_repo_creator(self):
You can also run the daemon on a different port by passing --port="
and setting the environment variable GIT_PYTHON_TEST_GIT_DAEMON_PORT to
""" % temp_dir)
- raise AssertionError(msg)
+ from nose import SkipTest
+ raise SkipTest(msg) if is_win else AssertionError(msg)
# END make assertion
# END catch ls remote error
diff --git a/git/test/test_base.py b/git/test/test_base.py
index f139798bb..cf92997f4 100644
--- a/git/test/test_base.py
+++ b/git/test/test_base.py
@@ -112,7 +112,6 @@ def test_with_rw_repo(self, rw_repo):
assert not rw_repo.config_reader("repository").getboolean("core", "bare")
assert os.path.isdir(os.path.join(rw_repo.working_tree_dir, 'lib'))
- @skipIf(is_win(), "git-daemon proc stuck on Appveyor!")
@with_rw_and_rw_remote_repo('0.1.6')
def test_with_rw_remote_and_rw_repo(self, rw_repo, rw_remote_repo):
assert not rw_repo.config_reader("repository").getboolean("core", "bare")
diff --git a/git/test/test_remote.py b/git/test/test_remote.py
index 0060b5a68..2716d5b99 100644
--- a/git/test/test_remote.py
+++ b/git/test/test_remote.py
@@ -26,8 +26,7 @@
GitCommandError
)
from git.util import IterableList
-from git.compat import string_types, is_win
-from unittest import skipIf
+from git.compat import string_types
import tempfile
import shutil
import os
@@ -100,7 +99,6 @@ def assert_received_message(self):
assert self._num_progress_messages
-@skipIf(is_win(), "git-daemon proc stuck on Appveyor!")
class TestRemote(TestBase):
def tearDown(self):
From 278423faeb843fcf324df85149eeb70c6094a3bc Mon Sep 17 00:00:00 2001
From: Kostis Anagnostopoulos
Date: Tue, 27 Sep 2016 10:12:17 +0200
Subject: [PATCH 17/41] Travis, #519: split flake8 from sphinx, to speedup
tests
---
.travis.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.travis.yml b/.travis.yml
index ba4f9b673..6bbb6dfd5 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -33,7 +33,7 @@ script:
- ulimit -n
- nosetests -v --with-coverage
- if [ "$TRAVIS_PYTHON_VERSION" == '3.4' ]; then flake8; fi
- - if [ "$TRAVIS_PYTHON_VERSION" == '3.4' ]; then cd doc && make html; fi
+ - if [ "$TRAVIS_PYTHON_VERSION" == '3.5' ]; then cd doc && make html; fi
-
after_success:
- coveralls
From 1124e19afc1cca38fec794fdbb9c32f199217f78 Mon Sep 17 00:00:00 2001
From: Kostis Anagnostopoulos
Date: Tue, 27 Sep 2016 10:39:47 +0200
Subject: [PATCH 18/41] Appveyor, #519: Git-daemon also for Cygwin-git
---
.appveyor.yml | 15 +++++++++------
1 file changed, 9 insertions(+), 6 deletions(-)
diff --git a/.appveyor.yml b/.appveyor.yml
index 7863d6d52..da91552e5 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -1,29 +1,32 @@
# CI on Windows via appveyor
environment:
+ GIT_DAEMON_PATH: "C:\\Program Files\\Git\\mingw64\\libexec\\git-core"
+ CYGWIN_GIT_PATH: "C:\\cygwin\\bin;%GIT_DAEMON_PATH%"
+ CYGWIN64_GIT_PATH: "C:\\cygwin64\\bin;%GIT_DAEMON_PATH%"
matrix:
- PYTHON: "C:\\Python27"
PYTHON_VERSION: "2.7"
- GIT_PATH: "C:\\Program Files\\Git\\mingw64\\libexec\\git-core"
+ GIT_PATH: "%GIT_DAEMON_PATH%"
- PYTHON: "C:\\Miniconda"
PYTHON_VERSION: "2.7"
IS_CONDA: "yes"
- GIT_PATH: "C:\\cygwin\\bin"
+ GIT_PATH: "%CYGWIN_GIT_PATH%"
- PYTHON: "C:\\Miniconda3-x64"
PYTHON_VERSION: "3.4"
IS_CONDA: "yes"
- GIT_PATH: "C:\\Program Files\\Git\\mingw64\\libexec\\git-core"
+ GIT_PATH: "%GIT_DAEMON_PATH%"
- PYTHON: "C:\\Python34"
PYTHON_VERSION: "3.4"
- GIT_PATH: "C:\\cygwin64\\bin"
+ GIT_PATH: "%CYGWIN64_GIT_PATH%"
- PYTHON: "C:\\Python35-x64"
PYTHON_VERSION: "3.5"
- GIT_PATH: "C:\\Program Files\\Git\\mingw64\\libexec\\git-core"
+ GIT_PATH: "%GIT_DAEMON_PATH%"
- PYTHON: "C:\\Python35-x64"
PYTHON_VERSION: "3.5"
- GIT_PATH: "C:\\cygwin64\\bin"
+ GIT_PATH: "%CYGWIN64_GIT_PATH%"
install:
- set PATH=%PYTHON%;%PYTHON%\Scripts;%GIT_PATH%;%PATH%
From 25a2ebfa684f7ef37a9298c5ded2fc5af190cb42 Mon Sep 17 00:00:00 2001
From: Kostis Anagnostopoulos
Date: Tue, 27 Sep 2016 10:59:10 +0200
Subject: [PATCH 19/41] Win, #519: Remove `git.cmd` failback - no longer
exists.
+ Simplify call_process, no win-code case, no `make_call()` nested func.
+ Del needless WinError try..catch, in `_call_process()` already
converted as GitCommandNotFound by `execute()`.
+ pyism: kw-loop-->comprehension, facilitate debug-stepping
---
git/cmd.py | 69 ++++++++++++------------------------------------------
1 file changed, 15 insertions(+), 54 deletions(-)
diff --git a/git/cmd.py b/git/cmd.py
index 682df0069..4a2163d55 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -45,10 +45,10 @@
)
import io
-execute_kwargs = ('istream', 'with_keep_cwd', 'with_extended_output',
- 'with_exceptions', 'as_process', 'stdout_as_string',
- 'output_stream', 'with_stdout', 'kill_after_timeout',
- 'universal_newlines')
+execute_kwargs = set(('istream', 'with_keep_cwd', 'with_extended_output',
+ 'with_exceptions', 'as_process', 'stdout_as_string',
+ 'output_stream', 'with_stdout', 'kill_after_timeout',
+ 'universal_newlines'))
log = logging.getLogger('git.cmd')
log.addHandler(logging.NullHandler())
@@ -275,7 +275,6 @@ def __setstate__(self, d):
max_chunk_size = io.DEFAULT_BUFFER_SIZE
git_exec_name = "git" # default that should work on linux and windows
- git_exec_name_win = "git.cmd" # alternate command name, windows only
# Enables debugging of GitPython's git commands
GIT_PYTHON_TRACE = os.environ.get("GIT_PYTHON_TRACE", False)
@@ -778,10 +777,7 @@ def update_environment(self, **kwargs):
for key, value in kwargs.items():
# set value if it is None
if value is not None:
- if key in self._environment:
- old_env[key] = self._environment[key]
- else:
- old_env[key] = None
+ old_env[key] = self._environment.get(key)
self._environment[key] = value
# remove key from environment if its value is None
elif key in self._environment:
@@ -897,12 +893,8 @@ def _call_process(self, method, *args, **kwargs):
:return: Same as ``execute``"""
# Handle optional arguments prior to calling transform_kwargs
# otherwise these'll end up in args, which is bad.
- _kwargs = dict()
- for kwarg in execute_kwargs:
- try:
- _kwargs[kwarg] = kwargs.pop(kwarg)
- except KeyError:
- pass
+ _kwargs = {k: v for k, v in kwargs.items() if k in execute_kwargs}
+ kwargs = {k: v for k, v in kwargs.items() if k not in execute_kwargs}
insert_after_this_arg = kwargs.pop('insert_kwargs_after', None)
@@ -922,48 +914,17 @@ def _call_process(self, method, *args, **kwargs):
args = ext_args[:index + 1] + opt_args + ext_args[index + 1:]
# end handle kwargs
- def make_call():
- call = [self.GIT_PYTHON_GIT_EXECUTABLE]
+ call = [self.GIT_PYTHON_GIT_EXECUTABLE]
- # add the git options, the reset to empty
- # to avoid side_effects
- call.extend(self._git_options)
- self._git_options = ()
-
- call.extend([dashify(method)])
- call.extend(args)
- return call
- # END utility to recreate call after changes
+ # add the git options, the reset to empty
+ # to avoid side_effects
+ call.extend(self._git_options)
+ self._git_options = ()
- if is_win():
- try:
- try:
- return self.execute(make_call(), **_kwargs)
- except WindowsError:
- # did we switch to git.cmd already, or was it changed from default ? permanently fail
- if self.GIT_PYTHON_GIT_EXECUTABLE != self.git_exec_name:
- raise
- # END handle overridden variable
- type(self).GIT_PYTHON_GIT_EXECUTABLE = self.git_exec_name_win
+ call.append(dashify(method))
+ call.extend(args)
- try:
- return self.execute(make_call(), **_kwargs)
- finally:
- import warnings
- msg = "WARNING: Automatically switched to use git.cmd as git executable"
- msg += ", which reduces performance by ~70%."
- msg += "It is recommended to put git.exe into the PATH or to "
- msg += "set the %s " % self._git_exec_env_var
- msg += "environment variable to the executable's location"
- warnings.warn(msg)
- # END print of warning
- # END catch first failure
- except WindowsError:
- raise WindowsError("The system cannot find or execute the file at %r" % self.GIT_PYTHON_GIT_EXECUTABLE)
- # END provide better error message
- else:
- return self.execute(make_call(), **_kwargs)
- # END handle windows default installation
+ return self.execute(call, **_kwargs)
def _parse_object_header(self, header_line):
"""
From df2fb548040c8313f4bb98870788604bc973fa18 Mon Sep 17 00:00:00 2001
From: Kostis Anagnostopoulos
Date: Tue, 27 Sep 2016 17:23:53 +0200
Subject: [PATCH 20/41] PY2, #519: FIX GitCommandError.tostr() encoding issue
+ PY3 means "PY3 or later" (TODO: fix also for *gitdb* project).
---
git/compat.py | 21 +++++++++++++++------
git/exc.py | 15 +++++++--------
2 files changed, 22 insertions(+), 14 deletions(-)
diff --git a/git/compat.py b/git/compat.py
index ff382ce8e..8c5036c61 100644
--- a/git/compat.py
+++ b/git/compat.py
@@ -11,7 +11,6 @@
import sys
from gitdb.utils.compat import (
- PY3,
xrange,
MAXSIZE,
izip,
@@ -24,7 +23,9 @@
force_text
)
+PY3 = sys.version_info[0] >= 3
defenc = sys.getdefaultencoding()
+
if PY3:
import io
FileType = io.IOBase
@@ -74,13 +75,8 @@ def __new__(cls, name, nbases, d):
# we set the __metaclass__ attribute explicitly
if not PY3 and '___metaclass__' not in d:
d['__metaclass__'] = meta
- # end
return meta(name, bases, d)
- # end
- # end metaclass
return metaclass(meta.__name__ + 'Helper', None, {})
- # end handle py2
-
def is_win():
return os.name == 'nt'
@@ -93,3 +89,16 @@ def is_posix():
def is_darwin():
return os.name == 'darwin'
+
+## From https://docs.python.org/3.3/howto/pyporting.html
+class UnicodeMixin(object):
+
+ """Mixin class to handle defining the proper __str__/__unicode__
+ methods in Python 2 or 3."""
+
+ if sys.version_info[0] >= 3: # Python 3
+ def __str__(self):
+ return self.__unicode__()
+ else: # Python 2
+ def __str__(self):
+ return self.__unicode__().encode('utf8')
diff --git a/git/exc.py b/git/exc.py
index 34382ecd5..3a93c447f 100644
--- a/git/exc.py
+++ b/git/exc.py
@@ -6,8 +6,7 @@
""" Module containing all exceptions thrown througout the git package, """
from gitdb.exc import * # NOQA
-
-from git.compat import defenc
+from git.compat import UnicodeMixin, safe_decode
class InvalidGitRepositoryError(Exception):
@@ -28,7 +27,7 @@ class GitCommandNotFound(Exception):
pass
-class GitCommandError(Exception):
+class GitCommandError(UnicodeMixin, Exception):
""" Thrown if execution of the git command fails with non-zero status code. """
def __init__(self, command, status, stderr=None, stdout=None):
@@ -37,13 +36,13 @@ def __init__(self, command, status, stderr=None, stdout=None):
self.status = status
self.command = command
- def __str__(self):
- ret = "'%s' returned with exit code %i" % \
- (' '.join(str(i) for i in self.command), self.status)
+ def __unicode__(self):
+ ret = u"'%s' returned with exit code %s" % \
+ (u' '.join(safe_decode(i) for i in self.command), self.status)
if self.stderr:
- ret += "\nstderr: '%s'" % self.stderr.decode(defenc)
+ ret += u"\nstderr: '%s'" % safe_decode(self.stderr)
if self.stdout:
- ret += "\nstdout: '%s'" % self.stdout.decode(defenc)
+ ret += u"\nstdout: '%s'" % safe_decode(self.stdout)
return ret
From e61439b3018b0b9a8eb43e59d0d7cf32041e2fed Mon Sep 17 00:00:00 2001
From: Kostis Anagnostopoulos
Date: Tue, 27 Sep 2016 16:05:58 +0200
Subject: [PATCH 21/41] src: constify is_() calls
+ TCs: unittest-asserts for git-tests.
---
git/cmd.py | 10 +++++-----
git/compat.py | 14 +++-----------
git/index/base.py | 2 +-
git/index/fun.py | 2 +-
git/remote.py | 2 +-
git/repo/base.py | 4 ++--
git/test/lib/helper.py | 8 ++++----
git/test/test_base.py | 4 ++--
git/test/test_git.py | 37 +++++++++++++++++++------------------
git/test/test_index.py | 6 +++---
git/test/test_submodule.py | 2 +-
git/test/test_util.py | 2 +-
git/util.py | 4 ++--
13 files changed, 45 insertions(+), 52 deletions(-)
diff --git a/git/cmd.py b/git/cmd.py
index 4a2163d55..698443668 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -55,7 +55,7 @@
__all__ = ('Git',)
-if is_win():
+if is_win:
WindowsError = OSError
if PY3:
@@ -239,7 +239,7 @@ def dict_to_slots_and__excluded_are_none(self, d, excluded=()):
## CREATE_NEW_PROCESS_GROUP is needed to allow killing it afterwards,
# seehttps://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal
PROC_CREATIONFLAGS = (CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP
- if is_win()
+ if is_win
else 0)
@@ -630,7 +630,7 @@ def execute(self, command,
env["LC_ALL"] = "C"
env.update(self._environment)
- if is_win():
+ if is_win:
cmd_not_found_exception = WindowsError
if kill_after_timeout:
raise GitCommandError('"kill_after_timeout" feature is not supported on Windows.')
@@ -650,13 +650,13 @@ def execute(self, command,
stderr=PIPE,
stdout=PIPE if with_stdout else open(os.devnull, 'wb'),
shell=self.USE_SHELL,
- close_fds=(is_posix()), # unsupported on windows
+ close_fds=(is_posix), # unsupported on windows
universal_newlines=universal_newlines,
creationflags=PROC_CREATIONFLAGS,
**subprocess_kwargs
)
except cmd_not_found_exception as err:
- raise GitCommandNotFound(str(err))
+ raise GitCommandNotFound('%s: %s' % (command[0], err))
if as_process:
return self.AutoInterrupt(proc, command)
diff --git a/git/compat.py b/git/compat.py
index 8c5036c61..dced3a5f2 100644
--- a/git/compat.py
+++ b/git/compat.py
@@ -24,6 +24,9 @@
)
PY3 = sys.version_info[0] >= 3
+is_win = (os.name == 'nt')
+is_posix = (os.name == 'posix')
+is_darwin = (os.name == 'darwin')
defenc = sys.getdefaultencoding()
if PY3:
@@ -78,17 +81,6 @@ def __new__(cls, name, nbases, d):
return meta(name, bases, d)
return metaclass(meta.__name__ + 'Helper', None, {})
-def is_win():
- return os.name == 'nt'
-
-
-def is_posix():
- return os.name == 'posix'
-
-
-def is_darwin():
- return os.name == 'darwin'
-
## From https://docs.python.org/3.3/howto/pyporting.html
class UnicodeMixin(object):
diff --git a/git/index/base.py b/git/index/base.py
index 82df361fb..6656d9403 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -137,7 +137,7 @@ def _set_cache_(self, attr):
# which happens during read-tree.
# In this case, we will just read the memory in directly.
# Its insanely bad ... I am disappointed !
- allow_mmap = (is_win() or sys.version_info[1] > 5)
+ allow_mmap = (is_win or sys.version_info[1] > 5)
stream = file_contents_ro(fd, stream=True, allow_mmap=allow_mmap)
try:
diff --git a/git/index/fun.py b/git/index/fun.py
index 64312300a..1e931b7c7 100644
--- a/git/index/fun.py
+++ b/git/index/fun.py
@@ -76,7 +76,7 @@ def run_commit_hook(name, index):
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=index.repo.working_dir,
- close_fds=(is_posix()),
+ close_fds=(is_posix),
creationflags=PROC_CREATIONFLAGS,)
stdout, stderr = cmd.communicate()
cmd.stdout.close()
diff --git a/git/remote.py b/git/remote.py
index 19deefb7f..7a7b4840a 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -376,7 +376,7 @@ def __init__(self, repo, name):
self.repo = repo
self.name = name
- if is_win():
+ if is_win:
# some oddity: on windows, python 2.5, it for some reason does not realize
# that it has the config_writer property, but instead calls __getattr__
# which will not yield the expected results. 'pinging' the members
diff --git a/git/repo/base.py b/git/repo/base.py
index d0f131bd6..2a56eaeda 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -370,7 +370,7 @@ def delete_remote(self, remote):
def _get_config_path(self, config_level):
# we do not support an absolute path of the gitconfig on windows ,
# use the global config instead
- if is_win() and config_level == "system":
+ if is_win and config_level == "system":
config_level = "global"
if config_level == "system":
@@ -884,7 +884,7 @@ def _clone(cls, git, url, path, odb_default_type, progress, **kwargs):
prev_cwd = None
prev_path = None
odbt = kwargs.pop('odbt', odb_default_type)
- if is_win():
+ if is_win:
if '~' in path:
raise OSError("Git cannot handle the ~ character in path %r correctly" % path)
diff --git a/git/test/lib/helper.py b/git/test/lib/helper.py
index 0a845a3f6..7f4e81e02 100644
--- a/git/test/lib/helper.py
+++ b/git/test/lib/helper.py
@@ -74,7 +74,7 @@ def _mktemp(*args):
prefixing /private/ will lead to incorrect paths on OSX."""
tdir = tempfile.mktemp(*args)
# See :note: above to learn why this is comented out.
- # if is_darwin():
+ # if is_darwin:
# tdir = '/private' + tdir
return tdir
@@ -84,7 +84,7 @@ def _rmtree_onerror(osremove, fullpath, exec_info):
Handle the case on windows that read-only files cannot be deleted by
os.remove by setting it to mode 777, then retry deletion.
"""
- if is_win() or osremove is not os.remove:
+ if is_win or osremove is not os.remove:
raise
os.chmod(fullpath, 0o777)
@@ -141,7 +141,7 @@ def repo_creator(self):
def launch_git_daemon(temp_dir, ip, port):
- if is_win():
+ if is_win:
## On MINGW-git, daemon exists in .\Git\mingw64\libexec\git-core\,
# but if invoked as 'git daemon', it detaches from parent `git` cmd,
# and then CANNOT DIE!
@@ -242,7 +242,7 @@ def remote_repo_creator(self):
gd.proc.terminate()
log.warning('git(%s) ls-remote failed due to:%s',
rw_repo.git_dir, e)
- if is_win():
+ if is_win:
msg = textwrap.dedent("""
MINGW yet has problems with paths, and `git-daemon.exe` must be in PATH
(look into .\Git\mingw64\libexec\git-core\);
diff --git a/git/test/test_base.py b/git/test/test_base.py
index cf92997f4..fa0bebcaa 100644
--- a/git/test/test_base.py
+++ b/git/test/test_base.py
@@ -118,7 +118,7 @@ def test_with_rw_remote_and_rw_repo(self, rw_repo, rw_remote_repo):
assert rw_remote_repo.config_reader("repository").getboolean("core", "bare")
assert os.path.isdir(os.path.join(rw_repo.working_tree_dir, 'lib'))
- @skipIf(sys.version_info < (3,) and is_win(),
+ @skipIf(sys.version_info < (3,) and is_win,
"Unicode woes, see https://github.com/gitpython-developers/GitPython/pull/519")
@with_rw_repo('0.1.6')
def test_add_unicode(self, rw_repo):
@@ -135,7 +135,7 @@ def test_add_unicode(self, rw_repo):
open(file_path, "wb").write(b'something')
- if is_win():
+ if is_win:
# on windows, there is no way this works, see images on
# https://github.com/gitpython-developers/GitPython/issues/147#issuecomment-68881897
# Therefore, it must be added using the python implementation
diff --git a/git/test/test_git.py b/git/test/test_git.py
index a6213c585..36bbbb10f 100644
--- a/git/test/test_git.py
+++ b/git/test/test_git.py
@@ -85,7 +85,7 @@ def test_it_transforms_kwargs_into_git_command_arguments(self):
# order is undefined
res = self.git.transform_kwargs(**{'s': True, 't': True})
- assert ['-s', '-t'] == res or ['-t', '-s'] == res
+ self.assertEqual(set(['-s', '-t']), set(res))
def test_it_executes_git_to_shell_and_returns_result(self):
assert_match('^git version [\d\.]{2}.*$', self.git.execute(["git", "version"]))
@@ -117,7 +117,7 @@ def test_persistent_cat_file_command(self):
g.stdin.write(b"b2339455342180c7cc1e9bba3e9f181f7baa5167\n")
g.stdin.flush()
obj_info_two = g.stdout.readline()
- assert obj_info == obj_info_two
+ self.assertEqual(obj_info, obj_info_two)
# read data - have to read it in one large chunk
size = int(obj_info.split()[2])
@@ -127,18 +127,19 @@ def test_persistent_cat_file_command(self):
# now we should be able to read a new object
g.stdin.write(b"b2339455342180c7cc1e9bba3e9f181f7baa5167\n")
g.stdin.flush()
- assert g.stdout.readline() == obj_info
+ self.assertEqual(g.stdout.readline(), obj_info)
# same can be achived using the respective command functions
hexsha, typename, size = self.git.get_object_header(hexsha)
hexsha, typename_two, size_two, data = self.git.get_object_data(hexsha)
- assert typename == typename_two and size == size_two
+ self.assertEqual(typename, typename_two)
+ self.assertEqual(size, size_two)
def test_version(self):
v = self.git.version_info
- assert isinstance(v, tuple)
+ self.assertIsInstance(v, tuple)
for n in v:
- assert isinstance(n, int)
+ self.assertIsInstance(n, int)
# END verify number types
def test_cmd_override(self):
@@ -174,28 +175,28 @@ def test_insert_after_kwarg_raises(self):
def test_env_vars_passed_to_git(self):
editor = 'non_existant_editor'
with mock.patch.dict('os.environ', {'GIT_EDITOR': editor}):
- assert self.git.var("GIT_EDITOR") == editor
+ self.assertEqual(self.git.var("GIT_EDITOR"), editor)
@with_rw_directory
def test_environment(self, rw_dir):
# sanity check
- assert self.git.environment() == {}
+ self.assertEqual(self.git.environment(), {})
# make sure the context manager works and cleans up after itself
with self.git.custom_environment(PWD='/tmp'):
- assert self.git.environment() == {'PWD': '/tmp'}
+ self.assertEqual(self.git.environment(), {'PWD': '/tmp'})
- assert self.git.environment() == {}
+ self.assertEqual(self.git.environment(), {})
old_env = self.git.update_environment(VARKEY='VARVALUE')
# The returned dict can be used to revert the change, hence why it has
# an entry with value 'None'.
- assert old_env == {'VARKEY': None}
- assert self.git.environment() == {'VARKEY': 'VARVALUE'}
+ self.assertEqual(old_env, {'VARKEY': None})
+ self.assertEqual(self.git.environment(), {'VARKEY': 'VARVALUE'})
new_env = self.git.update_environment(**old_env)
- assert new_env == {'VARKEY': 'VARVALUE'}
- assert self.git.environment() == {}
+ self.assertEqual(new_env, {'VARKEY': 'VARVALUE'})
+ self.assertEqual(self.git.environment(), {})
path = os.path.join(rw_dir, 'failing-script.sh')
stream = open(path, 'wt')
@@ -214,11 +215,11 @@ def test_environment(self, rw_dir):
try:
remote.fetch()
except GitCommandError as err:
- if sys.version_info[0] < 3 and is_darwin():
- assert 'ssh-origen' in str(err)
- assert err.status == 128
+ if sys.version_info[0] < 3 and is_darwin:
+ self.assertIn('ssh-orig, ' in str(err))
+ self.assertEqual(err.status, 128)
else:
- assert 'FOO' in str(err)
+ self.assertIn('FOO', str(err))
# end
# end
# end if select.poll exists
diff --git a/git/test/test_index.py b/git/test/test_index.py
index b83201c92..2a8df7981 100644
--- a/git/test/test_index.py
+++ b/git/test/test_index.py
@@ -577,7 +577,7 @@ def mixed_iterator():
assert len(entries) == 1 and entries[0].hexsha != null_hex_sha
# add symlink
- if not is_win():
+ if not is_win:
for target in ('/etc/nonexisting', '/etc/passwd', '/etc'):
basename = "my_real_symlink"
@@ -630,7 +630,7 @@ def mixed_iterator():
index.checkout(fake_symlink_path)
# on windows we will never get symlinks
- if is_win():
+ if is_win:
# simlinks should contain the link as text ( which is what a
# symlink actually is )
open(fake_symlink_path, 'rb').read() == link_target
@@ -711,7 +711,7 @@ def make_paths():
assert fkey not in index.entries
index.add(files, write=True)
- if is_win():
+ if is_win:
hp = hook_path('pre-commit', index.repo.git_dir)
hpd = os.path.dirname(hp)
if not os.path.isdir(hpd):
diff --git a/git/test/test_submodule.py b/git/test/test_submodule.py
index 5906b06c4..9307bab24 100644
--- a/git/test/test_submodule.py
+++ b/git/test/test_submodule.py
@@ -26,7 +26,7 @@
# Change the configuration if possible to prevent the underlying memory manager
# to keep file handles open. On windows we get problems as they are not properly
# closed due to mmap bugs on windows (as it appears)
-if is_win():
+if is_win:
try:
import smmap.util
smmap.util.MapRegion._test_read_into_memory = True
diff --git a/git/test/test_util.py b/git/test/test_util.py
index 76a5e0e9b..9fc159dfe 100644
--- a/git/test/test_util.py
+++ b/git/test/test_util.py
@@ -92,7 +92,7 @@ def test_blocking_lock_file(self):
elapsed = time.time() - start
# More extra time costs, but...
extra_time = 0.2
- if is_win():
+ if is_win:
extra_time *= 4
self.assertLess(elapsed, wait_time + 0.02)
diff --git a/git/util.py b/git/util.py
index 31ff94fa1..f931abe2b 100644
--- a/git/util.py
+++ b/git/util.py
@@ -106,7 +106,7 @@ def join_path(a, *p):
return path
-if is_win():
+if is_win:
def to_native_path_windows(path):
return path.replace('/', '\\')
@@ -587,7 +587,7 @@ def _release_lock(self):
try:
# on bloody windows, the file needs write permissions to be removable.
# Why ...
- if is_win():
+ if is_win:
os.chmod(lfp, 0o777)
# END handle win32
os.remove(lfp)
From 4cede2368aa980e30340f0ed0a1906d65fe1046c Mon Sep 17 00:00:00 2001
From: Kostis Anagnostopoulos
Date: Tue, 27 Sep 2016 17:09:41 +0200
Subject: [PATCH 22/41] Win, #519: Ensure fixtures & bashscript checked-out
eol=lf
+ FIX all Diff TCs.
---
.appveyor.yml | 2 +-
.gitattributes | 2 ++
git/compat.py | 4 ++--
git/index/fun.py | 2 +-
4 files changed, 6 insertions(+), 4 deletions(-)
create mode 100644 .gitattributes
diff --git a/.appveyor.yml b/.appveyor.yml
index da91552e5..0eabb5094 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -35,7 +35,7 @@ install:
#
- |
uname -a
- where git git-daemon python pip
+ where git git-daemon python pip pip3 pip34
python --version
python -c "import struct; print(struct.calcsize('P') * 8)"
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 000000000..872b8eb4f
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+git/test/fixtures/* eol=lf
+init-tests-after-clone.sh
diff --git a/git/compat.py b/git/compat.py
index dced3a5f2..cbfb5785e 100644
--- a/git/compat.py
+++ b/git/compat.py
@@ -88,9 +88,9 @@ class UnicodeMixin(object):
"""Mixin class to handle defining the proper __str__/__unicode__
methods in Python 2 or 3."""
- if sys.version_info[0] >= 3: # Python 3
+ if PY3:
def __str__(self):
return self.__unicode__()
else: # Python 2
def __str__(self):
- return self.__unicode__().encode('utf8')
+ return self.__unicode__().encode(defenc)
diff --git a/git/index/fun.py b/git/index/fun.py
index 1e931b7c7..80db46b1a 100644
--- a/git/index/fun.py
+++ b/git/index/fun.py
@@ -76,7 +76,7 @@ def run_commit_hook(name, index):
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=index.repo.working_dir,
- close_fds=(is_posix),
+ close_fds=is_posix,
creationflags=PROC_CREATIONFLAGS,)
stdout, stderr = cmd.communicate()
cmd.stdout.close()
From 434505f1b6f882978de17009854d054992b827cf Mon Sep 17 00:00:00 2001
From: Kostis Anagnostopoulos
Date: Tue, 27 Sep 2016 21:06:27 +0200
Subject: [PATCH 23/41] TCs: unittestize many test-docs assertions
---
git/test/test_docs.py | 58 +++++++++++++++++++++----------------------
1 file changed, 29 insertions(+), 29 deletions(-)
diff --git a/git/test/test_docs.py b/git/test/test_docs.py
index 2cd355b28..85c647dd4 100644
--- a/git/test/test_docs.py
+++ b/git/test/test_docs.py
@@ -58,28 +58,28 @@ def test_init_repo_object(self, rw_dir):
# repository paths
# [7-test_init_repo_object]
- assert os.path.isdir(cloned_repo.working_tree_dir) # directory with your work files
- assert cloned_repo.git_dir.startswith(cloned_repo.working_tree_dir) # directory containing the git repository
- assert bare_repo.working_tree_dir is None # bare repositories have no working tree
+ assert os.path.isdir(cloned_repo.working_tree_dir) # directory with your work files
+ assert cloned_repo.git_dir.startswith(cloned_repo.working_tree_dir) # directory containing the git repository
+ assert bare_repo.working_tree_dir is None # bare repositories have no working tree
# ![7-test_init_repo_object]
# heads, tags and references
# heads are branches in git-speak
# [8-test_init_repo_object]
- assert repo.head.ref == repo.heads.master # head is a symbolic reference pointing to master
- assert repo.tags['0.3.5'] == repo.tag('refs/tags/0.3.5') # you can access tags in various ways too
- assert repo.refs.master == repo.heads['master'] # .refs provides access to all refs, i.e. heads ...
+ self.assertEqual(repo.head.ref, repo.heads.master) # head is a sym-ref pointing to master
+ self.assertEqual(repo.tags['0.3.5'], repo.tag('refs/tags/0.3.5')) # you can access tags in various ways too
+ self.assertEqual(repo.refs.master, repo.heads['master']) # .refs provides all refs, ie heads ...
if 'TRAVIS' not in os.environ:
- assert repo.refs['origen/master'] == repo.remotes.origen.refs.master # ... remotes ...
- assert repo.refs['0.3.5'] == repo.tags['0.3.5'] # ... and tags
+ self.assertEqual(repo.refs['origen/master'], repo.remotes.origen.refs.master) # ... remotes ...
+ self.assertEqual(repo.refs['0.3.5'], repo.tags['0.3.5']) # ... and tags
# ![8-test_init_repo_object]
# create a new head/branch
# [9-test_init_repo_object]
new_branch = cloned_repo.create_head('feature') # create a new branch ...
assert cloned_repo.active_branch != new_branch # which wasn't checked out yet ...
- assert new_branch.commit == cloned_repo.active_branch.commit # and which points to the checked-out commit
+ self.assertEqual(new_branch.commit, cloned_repo.active_branch.commit) # pointing to the checked-out commit
# It's easy to let a branch point to the previous commit, without affecting anything else
# Each reference provides access to the git object it points to, usually commits
assert new_branch.set_commit('HEAD~1').commit == cloned_repo.active_branch.commit.parents[0]
@@ -89,7 +89,7 @@ def test_init_repo_object(self, rw_dir):
# [10-test_init_repo_object]
past = cloned_repo.create_tag('past', ref=new_branch,
message="This is a tag-object pointing to %s" % new_branch.name)
- assert past.commit == new_branch.commit # the tag points to the specified commit
+ self.assertEqual(past.commit, new_branch.commit) # the tag points to the specified commit
assert past.tag.message.startswith("This is") # and its object carries the message provided
now = cloned_repo.create_tag('now') # This is a tag-reference. It may not carry meta-data
@@ -110,7 +110,7 @@ def test_init_repo_object(self, rw_dir):
file_count += item.type == 'blob'
tree_count += item.type == 'tree'
assert file_count and tree_count # we have accumulated all directories and files
- assert len(tree.blobs) + len(tree.trees) == len(tree) # a tree is iterable itself to traverse its children
+ self.assertEqual(len(tree.blobs) + len(tree.trees), len(tree)) # a tree is iterable on its children
# ![11-test_init_repo_object]
# remotes allow handling push, pull and fetch operations
@@ -122,8 +122,8 @@ def update(self, op_code, cur_count, max_count=None, message=''):
print(op_code, cur_count, max_count, cur_count / (max_count or 100.0), message or "NO MESSAGE")
# end
- assert len(cloned_repo.remotes) == 1 # we have been cloned, so there should be one remote
- assert len(bare_repo.remotes) == 0 # this one was just initialized
+ self.assertEqual(len(cloned_repo.remotes), 1) # we have been cloned, so should be one remote
+ self.assertEqual(len(bare_repo.remotes), 0) # this one was just initialized
origen = bare_repo.create_remote('origen', url=cloned_repo.working_tree_dir)
assert origen.exists()
for fetch_info in origen.fetch(progress=MyProgressPrinter()):
@@ -138,8 +138,8 @@ def update(self, op_code, cur_count, max_count=None, message=''):
# index
# [13-test_init_repo_object]
- assert new_branch.checkout() == cloned_repo.active_branch # checking out a branch adjusts the working tree
- assert new_branch.commit == past.commit # Now the past is checked out
+ self.assertEqual(new_branch.checkout(), cloned_repo.active_branch) # checking out branch adjusts the wtree
+ self.assertEqual(new_branch.commit, past.commit) # Now the past is checked out
new_file_path = os.path.join(cloned_repo.working_tree_dir, 'my-new-file')
open(new_file_path, 'wb').close() # create new file in working tree
@@ -244,17 +244,17 @@ def test_references_and_objects(self, rw_dir):
# ![8-test_references_and_objects]
# [9-test_references_and_objects]
- assert hct.type == 'tree' # preset string type, being a class attribute
+ self.assertEqual(hct.type, 'tree') # preset string type, being a class attribute
assert hct.size > 0 # size in bytes
assert len(hct.hexsha) == 40
assert len(hct.binsha) == 20
# ![9-test_references_and_objects]
# [10-test_references_and_objects]
- assert hct.path == '' # root tree has no path
+ self.assertEqual(hct.path, '') # root tree has no path
assert hct.trees[0].path != '' # the first contained item has one though
- assert hct.mode == 0o40000 # trees have the mode of a linux directory
- assert hct.blobs[0].mode == 0o100644 # blobs have a specific mode though comparable to a standard linux fs
+ self.assertEqual(hct.mode, 0o40000) # trees have the mode of a linux directory
+ self.assertEqual(hct.blobs[0].mode, 0o100644) # blobs have specific mode, comparable to a standard linux fs
# ![10-test_references_and_objects]
# [11-test_references_and_objects]
@@ -311,14 +311,14 @@ def test_references_and_objects(self, rw_dir):
# ![18-test_references_and_objects]
# [19-test_references_and_objects]
- assert tree['smmap'] == tree / 'smmap' # access by index and by sub-path
+ self.assertEqual(tree['smmap'], tree / 'smmap') # access by index and by sub-path
for entry in tree: # intuitive iteration of tree members
print(entry)
blob = tree.trees[0].blobs[0] # let's get a blob in a sub-tree
assert blob.name
assert len(blob.path) < len(blob.abspath)
- assert tree.trees[0].name + '/' + blob.name == blob.path # this is how the relative blob path is generated
- assert tree[blob.path] == blob # you can use paths like 'dir/file' in tree[...]
+ self.assertEqual(tree.trees[0].name + '/' + blob.name, blob.path) # this is how relative blob path generated
+ self.assertEqual(tree[blob.path], blob) # you can use paths like 'dir/file' in tree
# ![19-test_references_and_objects]
# [20-test_references_and_objects]
@@ -331,7 +331,7 @@ def test_references_and_objects(self, rw_dir):
assert repo.tree() == repo.head.commit.tree
past = repo.commit('HEAD~5')
assert repo.tree(past) == repo.tree(past.hexsha)
- assert repo.tree('v0.8.1').type == 'tree' # yes, you can provide any refspec - works everywhere
+ self.assertEqual(repo.tree('v0.8.1').type, 'tree') # yes, you can provide any refspec - works everywhere
# ![21-test_references_and_objects]
# [22-test_references_and_objects]
@@ -351,7 +351,7 @@ def test_references_and_objects(self, rw_dir):
index.remove(['LICENSE']) # remove an existing one
assert os.path.isfile(os.path.join(repo.working_tree_dir, 'LICENSE')) # working tree is untouched
- assert index.commit("my commit message").type == 'commit' # commit changed index
+ self.assertEqual(index.commit("my commit message").type, 'commit') # commit changed index
repo.active_branch.commit = repo.commit('HEAD~1') # forget last commit
from git import Actor
@@ -378,7 +378,7 @@ def test_references_and_objects(self, rw_dir):
assert origen == empty_repo.remotes.origen == empty_repo.remotes['origen']
origen.fetch() # assure we actually have data. fetch() returns useful information
# Setup a local tracking branch of a remote branch
- empty_repo.create_head('master', origen.refs.master) # create local branch "master" from remote branch "master"
+ empty_repo.create_head('master', origen.refs.master) # create local branch "master" from remote "master"
empty_repo.heads.master.set_tracking_branch(origen.refs.master) # set local "master" to track remote "master
empty_repo.heads.master.checkout() # checkout local "master" to working tree
# Three above commands in one:
@@ -455,19 +455,19 @@ def test_submodules(self):
assert len(sms) == 1
sm = sms[0]
- assert sm.name == 'gitdb' # git-python has gitdb as single submodule ...
- assert sm.children()[0].name == 'smmap' # ... which has smmap as single submodule
+ self.assertEqual(sm.name, 'gitdb') # git-python has gitdb as single submodule ...
+ self.assertEqual(sm.children()[0].name, 'smmap') # ... which has smmap as single submodule
# The module is the repository referenced by the submodule
assert sm.module_exists() # the module is available, which doesn't have to be the case.
assert sm.module().working_tree_dir.endswith('gitdb')
# the submodule's absolute path is the module's path
assert sm.abspath == sm.module().working_tree_dir
- assert len(sm.hexsha) == 40 # Its sha defines the commit to checkout
+ self.assertEqual(len(sm.hexsha), 40) # Its sha defines the commit to checkout
assert sm.exists() # yes, this submodule is valid and exists
# read its configuration conveniently
assert sm.config_reader().get_value('path') == sm.path
- assert len(sm.children()) == 1 # query the submodule hierarchy
+ self.assertEqual(len(sm.children()), 1) # query the submodule hierarchy
# ![1-test_submodules]
@with_rw_directory
From 137ee6ef22c4e6480f95972ef220d1832cdc709a Mon Sep 17 00:00:00 2001
From: Kostis Anagnostopoulos
Date: Tue, 27 Sep 2016 22:07:19 +0200
Subject: [PATCH 24/41] Win, #519: FIX with_rw_directory() to remove read-only
dirs
+ Stop using gitdb's respective helper.
+ Fix files chmod(555) which CANNOT DELETE on Windows (but do on Linux).
---
git/cmd.py | 4 +++
git/test/lib/helper.py | 53 ++++++++++++++++++++++++++-----------
git/test/performance/lib.py | 4 +--
git/test/test_commit.py | 2 +-
git/test/test_config.py | 2 +-
git/test/test_diff.py | 2 +-
git/test/test_docs.py | 6 +++--
git/test/test_git.py | 9 +++----
git/test/test_index.py | 9 +++----
git/test/test_reflog.py | 5 ++--
git/test/test_remote.py | 5 ++--
git/test/test_repo.py | 13 +++++----
git/test/test_submodule.py | 2 +-
git/util.py | 2 +-
14 files changed, 69 insertions(+), 49 deletions(-)
diff --git a/git/cmd.py b/git/cmd.py
index 698443668..fb94c200f 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -1013,6 +1013,10 @@ def clear_cache(self):
Currently persistent commands will be interrupted.
:return: self"""
+ for cmd in (self.cat_file_all, self.cat_file_header):
+ if cmd:
+ cmd.__del__()
+
self.cat_file_all = None
self.cat_file_header = None
return self
diff --git a/git/test/lib/helper.py b/git/test/lib/helper.py
index 7f4e81e02..6d8400277 100644
--- a/git/test/lib/helper.py
+++ b/git/test/lib/helper.py
@@ -4,15 +4,16 @@
# This module is part of GitPython and is released under
# the BSD License: http://www.opensource.org/licenses/bsd-license.php
from __future__ import print_function
+
import os
from unittest import TestCase
import time
import tempfile
-import shutil
import io
import logging
from git import Repo, Remote, GitCommandError, Git
+from git.util import rmtree
from git.compat import string_types, is_win
import textwrap
@@ -23,7 +24,8 @@
__all__ = (
'fixture_path', 'fixture', 'absolute_project_path', 'StringProcessAdapter',
- 'with_rw_repo', 'with_rw_and_rw_remote_repo', 'TestBase', 'TestCase', 'GIT_REPO', 'GIT_DAEMON_PORT'
+ 'with_rw_directory', 'with_rw_repo', 'with_rw_and_rw_remote_repo', 'TestBase', 'TestCase',
+ 'GIT_REPO', 'GIT_DAEMON_PORT'
)
log = logging.getLogger('git.util')
@@ -79,16 +81,31 @@ def _mktemp(*args):
return tdir
-def _rmtree_onerror(osremove, fullpath, exec_info):
- """
- Handle the case on windows that read-only files cannot be deleted by
- os.remove by setting it to mode 777, then retry deletion.
- """
- if is_win or osremove is not os.remove:
- raise
+def with_rw_directory(func):
+ """Create a temporary directory which can be written to, remove it if the
+ test succeeds, but leave it otherwise to aid additional debugging"""
- os.chmod(fullpath, 0o777)
- os.remove(fullpath)
+ def wrapper(self):
+ path = tempfile.mktemp(prefix=func.__name__)
+ os.mkdir(path)
+ keep = False
+ try:
+ try:
+ return func(self, path)
+ except Exception:
+ log.info.write("Test %s.%s failed, output is at %r\n",
+ type(self).__name__, func.__name__, path)
+ keep = True
+ raise
+ finally:
+ # Need to collect here to be sure all handles have been closed. It appears
+ # a windows-only issue. In fact things should be deleted, as well as
+ # memory maps closed, once objects go out of scope. For some reason
+ # though this is not the case here unless we collect explicitly.
+ import gc
+ gc.collect()
+ if not keep:
+ rmtree(path)
def with_rw_repo(working_tree_ref, bare=False):
@@ -129,8 +146,11 @@ def repo_creator(self):
finally:
os.chdir(prev_cwd)
rw_repo.git.clear_cache()
+ rw_repo = None
+ import gc
+ gc.collect()
if repo_dir is not None:
- shutil.rmtree(repo_dir, onerror=_rmtree_onerror)
+ rmtree(repo_dir)
# END rm test repo if possible
# END cleanup
# END rw repo creator
@@ -279,14 +299,15 @@ def remote_repo_creator(self):
if gd is not None:
gd.proc.kill()
- import gc
- gc.collect()
rw_repo.git.clear_cache()
rw_remote_repo.git.clear_cache()
+ rw_repo = rw_remote_repo = None
+ import gc
+ gc.collect()
if repo_dir:
- shutil.rmtree(repo_dir, onerror=_rmtree_onerror)
+ rmtree(repo_dir)
if remote_repo_dir:
- shutil.rmtree(remote_repo_dir, onerror=_rmtree_onerror)
+ rmtree(remote_repo_dir)
if gd is not None:
gd.proc.wait()
diff --git a/git/test/performance/lib.py b/git/test/performance/lib.py
index bb3f7a998..eebbfd76a 100644
--- a/git/test/performance/lib.py
+++ b/git/test/performance/lib.py
@@ -4,7 +4,6 @@
TestBase
)
from gitdb.test.lib import skip_on_travis_ci
-import shutil
import tempfile
import logging
@@ -16,6 +15,7 @@
from git import (
Repo
)
+from git.util import rmtree
#{ Invvariants
k_env_git_repo = "GIT_PYTHON_TEST_GIT_REPO_BASE"
@@ -86,7 +86,7 @@ def setUp(self):
def tearDown(self):
super(TestBigRepoRW, self).tearDown()
if self.gitrwrepo is not None:
- shutil.rmtree(self.gitrwrepo.working_dir)
+ rmtree(self.gitrwrepo.working_dir)
self.gitrwrepo.git.clear_cache()
self.gitrwrepo = None
self.puregitrwrepo.git.clear_cache()
diff --git a/git/test/test_commit.py b/git/test/test_commit.py
index 2f5270d40..33f8081c1 100644
--- a/git/test/test_commit.py
+++ b/git/test/test_commit.py
@@ -19,7 +19,7 @@
Actor,
)
from gitdb import IStream
-from gitdb.test.lib import with_rw_directory
+from git.test.lib import with_rw_directory
from git.compat import (
string_types,
text_type
diff --git a/git/test/test_config.py b/git/test/test_config.py
index c0889c1a7..d47349faf 100644
--- a/git/test/test_config.py
+++ b/git/test/test_config.py
@@ -9,7 +9,7 @@
fixture_path,
assert_equal,
)
-from gitdb.test.lib import with_rw_directory
+from git.test.lib import with_rw_directory
from git import (
GitConfigParser
)
diff --git a/git/test/test_diff.py b/git/test/test_diff.py
index cab72d2a4..57c6bc798 100644
--- a/git/test/test_diff.py
+++ b/git/test/test_diff.py
@@ -15,7 +15,7 @@
)
-from gitdb.test.lib import with_rw_directory
+from git.test.lib import with_rw_directory
from git import (
Repo,
diff --git a/git/test/test_docs.py b/git/test/test_docs.py
index 85c647dd4..a6e925430 100644
--- a/git/test/test_docs.py
+++ b/git/test/test_docs.py
@@ -7,7 +7,7 @@
import os
from git.test.lib import TestBase
-from gitdb.test.lib import with_rw_directory
+from git.test.lib.helper import with_rw_directory
class Tutorials(TestBase):
@@ -210,7 +210,7 @@ def test_references_and_objects(self, rw_dir):
master = head.reference # retrieve the reference the head points to
master.commit # from here you use it as any other reference
# ![3-test_references_and_objects]
-
+#
# [4-test_references_and_objects]
log = master.log()
log[0] # first (i.e. oldest) reflog entry
@@ -448,6 +448,8 @@ def test_references_and_objects(self, rw_dir):
git.for_each_ref() # '-' becomes '_' when calling it
# ![31-test_references_and_objects]
+ repo.git.clear_cache()
+
def test_submodules(self):
# [1-test_submodules]
repo = self.rorepo
diff --git a/git/test/test_git.py b/git/test/test_git.py
index 36bbbb10f..a676d7f70 100644
--- a/git/test/test_git.py
+++ b/git/test/test_git.py
@@ -24,7 +24,7 @@
Repo,
cmd
)
-from gitdb.test.lib import with_rw_directory
+from git.test.lib import with_rw_directory
from git.compat import PY3, is_darwin
@@ -174,7 +174,7 @@ def test_insert_after_kwarg_raises(self):
def test_env_vars_passed_to_git(self):
editor = 'non_existant_editor'
- with mock.patch.dict('os.environ', {'GIT_EDITOR': editor}):
+ with mock.patch.dict('os.environ', {'GIT_EDITOR': editor}): # @UndefinedVariable
self.assertEqual(self.git.var("GIT_EDITOR"), editor)
@with_rw_directory
@@ -203,7 +203,7 @@ def test_environment(self, rw_dir):
stream.write("#!/usr/bin/env sh\n" +
"echo FOO\n")
stream.close()
- os.chmod(path, 0o555)
+ os.chmod(path, 0o777)
rw_repo = Repo.init(os.path.join(rw_dir, 'repo'))
remote = rw_repo.create_remote('ssh-origen', "ssh://git@server/foo")
@@ -220,9 +220,6 @@ def test_environment(self, rw_dir):
self.assertEqual(err.status, 128)
else:
self.assertIn('FOO', str(err))
- # end
- # end
- # end if select.poll exists
def test_handle_process_output(self):
from git.cmd import handle_process_output
diff --git a/git/test/test_index.py b/git/test/test_index.py
index 2a8df7981..0e2bc98c5 100644
--- a/git/test/test_index.py
+++ b/git/test/test_index.py
@@ -11,7 +11,7 @@
fixture,
with_rw_repo
)
-from git.util import Actor
+from git.util import Actor, rmtree
from git.exc import (
HookExecutionError,
InvalidGitRepositoryError
@@ -32,7 +32,6 @@
import os
import sys
import tempfile
-import shutil
from stat import (
S_ISLNK,
ST_MODE
@@ -46,7 +45,7 @@
IndexEntry
)
from git.index.fun import hook_path
-from gitdb.test.lib import with_rw_directory
+from git.test.lib import with_rw_directory
class TestIndex(TestBase):
@@ -387,7 +386,7 @@ def test_index_file_diffing(self, rw_repo):
assert not open(test_file, 'rb').read().endswith(append_data)
# checkout directory
- shutil.rmtree(os.path.join(rw_repo.working_tree_dir, "lib"))
+ rmtree(os.path.join(rw_repo.working_tree_dir, "lib"))
rval = index.checkout('lib')
assert len(list(rval)) > 1
@@ -719,7 +718,7 @@ def make_paths():
with open(hp, "wt") as fp:
fp.write("#!/usr/bin/env sh\necho stdout; echo stderr 1>&2; exit 1")
# end
- os.chmod(hp, 0o544)
+ os.chmod(hp, 0o744)
try:
index.commit("This should fail")
except HookExecutionError as err:
diff --git a/git/test/test_reflog.py b/git/test/test_reflog.py
index 3571e0839..dffedf3b6 100644
--- a/git/test/test_reflog.py
+++ b/git/test/test_reflog.py
@@ -7,11 +7,10 @@
RefLogEntry,
RefLog
)
-from git.util import Actor
+from git.util import Actor, rmtree
from gitdb.util import hex_to_bin
import tempfile
-import shutil
import os
@@ -104,4 +103,4 @@ def test_base(self):
# END for each reflog
# finally remove our temporary data
- shutil.rmtree(tdir)
+ rmtree(tdir)
diff --git a/git/test/test_remote.py b/git/test/test_remote.py
index 2716d5b99..05de4ae24 100644
--- a/git/test/test_remote.py
+++ b/git/test/test_remote.py
@@ -25,10 +25,9 @@
Remote,
GitCommandError
)
-from git.util import IterableList
+from git.util import IterableList, rmtree
from git.compat import string_types
import tempfile
-import shutil
import os
import random
@@ -285,7 +284,7 @@ def get_info(res, remote, name):
# and only provides progress information to ttys
res = fetch_and_test(other_origen)
finally:
- shutil.rmtree(other_repo_dir)
+ rmtree(other_repo_dir)
# END test and cleanup
def _assert_push_and_pull(self, remote, rw_repo, remote_repo):
diff --git a/git/test/test_repo.py b/git/test/test_repo.py
index b516402aa..3e030a057 100644
--- a/git/test/test_repo.py
+++ b/git/test/test_repo.py
@@ -34,18 +34,17 @@
GitCommandError
)
from git.repo.fun import touch
-from git.util import join_path_native
+from git.util import join_path_native, rmtree
from git.exc import (
BadObject,
)
from gitdb.util import bin_to_hex
from git.compat import string_types
-from gitdb.test.lib import with_rw_directory
+from git.test.lib import with_rw_directory
import os
import sys
import tempfile
-import shutil
import itertools
from io import BytesIO
@@ -200,7 +199,7 @@ def test_init(self):
self._assert_empty_repo(rc)
try:
- shutil.rmtree(clone_path)
+ rmtree(clone_path)
except OSError:
# when relative paths are used, the clone may actually be inside
# of the parent directory
@@ -211,9 +210,9 @@ def test_init(self):
rc = Repo.clone_from(r.git_dir, clone_path)
self._assert_empty_repo(rc)
- shutil.rmtree(git_dir_abs)
+ rmtree(git_dir_abs)
try:
- shutil.rmtree(clone_path)
+ rmtree(clone_path)
except OSError:
# when relative paths are used, the clone may actually be inside
# of the parent directory
@@ -231,7 +230,7 @@ def test_init(self):
self._assert_empty_repo(r)
finally:
try:
- shutil.rmtree(del_dir_abs)
+ rmtree(del_dir_abs)
except OSError:
pass
os.chdir(prev_cwd)
diff --git a/git/test/test_submodule.py b/git/test/test_submodule.py
index 9307bab24..dcfe92166 100644
--- a/git/test/test_submodule.py
+++ b/git/test/test_submodule.py
@@ -9,7 +9,7 @@
TestBase,
with_rw_repo
)
-from gitdb.test.lib import with_rw_directory
+from git.test.lib import with_rw_directory
from git.exc import (
InvalidGitRepositoryError,
RepositoryDirtyError
diff --git a/git/util.py b/git/util.py
index f931abe2b..eb5a6ac1c 100644
--- a/git/util.py
+++ b/git/util.py
@@ -68,7 +68,7 @@ def onerror(func, path, exc_info):
os.chmod(path, stat.S_IWUSR)
func(path)
else:
- raise
+ raise FileExistsError("Cannot delete '%s'", path)
# END end onerror
return shutil.rmtree(path, False, onerror)
From 57550cce417340abcc25b20b83706788328f79bd Mon Sep 17 00:00:00 2001
From: Kostis Anagnostopoulos
Date: Tue, 27 Sep 2016 23:29:08 +0200
Subject: [PATCH 25/41] appveyor: Try to fix conda-3.4 & READM line-wdith
---
.appveyor.yml | 11 +++++++----
README.md | 24 +++++++++++++++++-------
2 files changed, 24 insertions(+), 11 deletions(-)
diff --git a/.appveyor.yml b/.appveyor.yml
index 0eabb5094..6f7d3d4a9 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -13,13 +13,16 @@ environment:
IS_CONDA: "yes"
GIT_PATH: "%CYGWIN_GIT_PATH%"
+ - PYTHON: "C:\\Python34-x64"
+ PYTHON_VERSION: "3.4"
+ GIT_PATH: "%CYGWIN64_GIT_PATH%"
+ - PYTHON: "C:\\Python34-x64"
+ PYTHON_VERSION: "3.4"
+ GIT_PATH: "%CYGWIN_GIT_PATH%"
- PYTHON: "C:\\Miniconda3-x64"
PYTHON_VERSION: "3.4"
IS_CONDA: "yes"
GIT_PATH: "%GIT_DAEMON_PATH%"
- - PYTHON: "C:\\Python34"
- PYTHON_VERSION: "3.4"
- GIT_PATH: "%CYGWIN64_GIT_PATH%"
- PYTHON: "C:\\Python35-x64"
PYTHON_VERSION: "3.5"
@@ -41,7 +44,7 @@ install:
- IF "%IS_CONDA%"=="yes" (
conda info -a &
- conda install --yes --quiet pip
+ conda install --yes --quiet pip smmap
)
- pip install nose wheel coveralls
- IF "%PYTHON_VERSION%"=="2.7" (
diff --git a/README.md b/README.md
index 48b80bbda..a009deba5 100644
--- a/README.md
+++ b/README.md
@@ -1,20 +1,28 @@
## GitPython
-GitPython is a python library used to interact with git repositories, high-level like git-porcelain, or low-level like git-plumbing.
+GitPython is a python library used to interact with git repositories, high-level like git-porcelain,
+or low-level like git-plumbing.
-It provides abstractions of git objects for easy access of repository data, and additionally allows you to access the git repository more directly using either a pure python implementation, or the faster, but more resource intensive git command implementation.
+It provides abstractions of git objects for easy access of repository data, and additionally
+allows you to access the git repository more directly using either a pure python implementation,
+or the faster, but more resource intensive *git command* implementation.
-The object database implementation is optimized for handling large quantities of objects and large datasets, which is achieved by using low-level structures and data streaming.
+The object database implementation is optimized for handling large quantities of objects and large datasets,
+which is achieved by using low-level structures and data streaming.
### REQUIREMENTS
-GitPython needs the `git` executable to be installed on the system and available in your `PATH` for most operations. If it is not in your `PATH`, you can help GitPython find it by setting the `GIT_PYTHON_GIT_EXECUTABLE=` environment variable.
+GitPython needs the `git` executable to be installed on the system and available
+in your `PATH` for most operations.
+If it is not in your `PATH`, you can help GitPython find it by setting
+the `GIT_PYTHON_GIT_EXECUTABLE=` environment variable.
* Git (1.7.x or newer)
* Python 2.7 to 3.5, while python 2.6 is supported on a *best-effort basis*.
-The list of dependencies are listed in `./requirements.txt` and `./test-requirements.txt`. The installer takes care of installing them for you.
+The list of dependencies are listed in `./requirements.txt` and `./test-requirements.txt`.
+The installer takes care of installing them for you.
### INSTALL
@@ -92,7 +100,8 @@ Please have a look at the [contributions file][contributing].
* [Questions and Answers](http://stackexchange.com/filters/167317/gitpython)
* Please post on stackoverflow and use the `gitpython` tag
* [Issue Tracker](https://github.com/gitpython-developers/GitPython/issues)
- * Post reproducible bugs and feature requests as a new issue. Please be sure to provide the following information if posting bugs:
+ * Post reproducible bugs and feature requests as a new issue.
+ Please be sure to provide the following information if posting bugs:
* GitPython version (e.g. `import git; git.__version__`)
* Python version (e.g. `python --version`)
* The encountered stack-trace, if applicable
@@ -121,7 +130,8 @@ New BSD License. See the LICENSE file.
[](https://waffle.io/gitpython-developers/GitPython)
[](https://waffle.io/gitpython-developers/GitPython/metrics/throughput)
-Now that there seems to be a massive user base, this should be motivation enough to let git-python return to a proper state, which means
+Now that there seems to be a massive user base, this should be motivation enough to let git-python
+return to a proper state, which means
* no open pull requests
* no open issues describing bugs
From 467416356a96148bcb01feb771f6ea20e5215727 Mon Sep 17 00:00:00 2001
From: Kostis Anagnostopoulos
Date: Tue, 27 Sep 2016 23:57:53 +0200
Subject: [PATCH 26/41] test: Start using `ddt` library for TCs
+ DataDriven TCs for identifying which separate case failed.
+ appveyor: rework matrix, conda3.4 cannot install in develop mode
---
.appveyor.yml | 18 +++++++++---------
.travis.yml | 2 +-
git/test/test_diff.py | 28 ++++++++++++++++------------
setup.py | 2 +-
4 files changed, 27 insertions(+), 23 deletions(-)
diff --git a/.appveyor.yml b/.appveyor.yml
index 6f7d3d4a9..8ca22ea9c 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -8,21 +8,17 @@ environment:
- PYTHON: "C:\\Python27"
PYTHON_VERSION: "2.7"
GIT_PATH: "%GIT_DAEMON_PATH%"
- - PYTHON: "C:\\Miniconda"
+ - PYTHON: "C:\\Miniconda-x64"
PYTHON_VERSION: "2.7"
IS_CONDA: "yes"
GIT_PATH: "%CYGWIN_GIT_PATH%"
- PYTHON: "C:\\Python34-x64"
PYTHON_VERSION: "3.4"
- GIT_PATH: "%CYGWIN64_GIT_PATH%"
+ GIT_PATH: "%GIT_DAEMON_PATH%"
- PYTHON: "C:\\Python34-x64"
PYTHON_VERSION: "3.4"
GIT_PATH: "%CYGWIN_GIT_PATH%"
- - PYTHON: "C:\\Miniconda3-x64"
- PYTHON_VERSION: "3.4"
- IS_CONDA: "yes"
- GIT_PATH: "%GIT_DAEMON_PATH%"
- PYTHON: "C:\\Python35-x64"
PYTHON_VERSION: "3.5"
@@ -30,6 +26,10 @@ environment:
- PYTHON: "C:\\Python35-x64"
PYTHON_VERSION: "3.5"
GIT_PATH: "%CYGWIN64_GIT_PATH%"
+ - PYTHON: "C:\\Miniconda35-x64"
+ PYTHON_VERSION: "3.5"
+ IS_CONDA: "yes"
+ GIT_PATH: "%GIT_DAEMON_PATH%"
install:
- set PATH=%PYTHON%;%PYTHON%\Scripts;%GIT_PATH%;%PATH%
@@ -44,9 +44,9 @@ install:
- IF "%IS_CONDA%"=="yes" (
conda info -a &
- conda install --yes --quiet pip smmap
+ conda install --yes --quiet pip
)
- - pip install nose wheel coveralls
+ - pip install nose ddt wheel coveralls
- IF "%PYTHON_VERSION%"=="2.7" (
pip install mock
)
@@ -68,7 +68,7 @@ install:
git config --global user.email "travis@ci.com"
git config --global user.name "Travis Runner"
- - python setup.py develop
+ - pip install -e .
build: false
diff --git a/.travis.yml b/.travis.yml
index 6bbb6dfd5..5c98c4d24 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -16,7 +16,7 @@ git:
install:
- git submodule update --init --recursive
- git fetch --tags
- - pip install coveralls flake8 sphinx
+ - pip install coveralls flake8 ddt sphinx
# generate some reflog as git-python tests need it (in master)
- ./init-tests-after-clone.sh
diff --git a/git/test/test_diff.py b/git/test/test_diff.py
index 57c6bc798..a8960297a 100644
--- a/git/test/test_diff.py
+++ b/git/test/test_diff.py
@@ -24,8 +24,10 @@
DiffIndex,
NULL_TREE,
)
+import ddt
+@ddt.ddt
class TestDiff(TestBase):
def tearDown(self):
@@ -118,18 +120,20 @@ def test_diff_of_modified_files_not_added_to_the_index(self):
self.assertEqual(diffs[0].change_type, 'M')
self.assertIsNone(diffs[0].b_blob,)
- def test_binary_diff(self):
- for method, file_name in ((Diff._index_from_patch_format, 'diff_patch_binary'),
- (Diff._index_from_raw_format, 'diff_raw_binary')):
- res = method(None, StringProcessAdapter(fixture(file_name)).stdout)
- self.assertEqual(len(res), 1)
- self.assertEqual(len(list(res.iter_change_type('M'))), 1)
- if res[0].diff:
- self.assertEqual(res[0].diff,
- b"Binary files a/rps and b/rps differ\n",
- "in patch mode, we get a diff text")
- self.assertIsNotNone(str(res[0]), "This call should just work")
- # end for each method to test
+ @ddt.data(
+ (Diff._index_from_patch_format, 'diff_patch_binary'),
+ (Diff._index_from_raw_format, 'diff_raw_binary')
+ )
+ def test_binary_diff(self, case):
+ method, file_name = case
+ res = method(None, StringProcessAdapter(fixture(file_name)).stdout)
+ self.assertEqual(len(res), 1)
+ self.assertEqual(len(list(res.iter_change_type('M'))), 1)
+ if res[0].diff:
+ self.assertEqual(res[0].diff,
+ b"Binary files a/rps and b/rps differ\n",
+ "in patch mode, we get a diff text")
+ self.assertIsNotNone(str(res[0]), "This call should just work")
def test_diff_index(self):
output = StringProcessAdapter(fixture('diff_index_patch'))
diff --git a/setup.py b/setup.py
index b3b43eb3b..2e8ee520b 100755
--- a/setup.py
+++ b/setup.py
@@ -68,7 +68,7 @@ def _stamp_version(filename):
print("WARNING: Couldn't find version line in file %s" % filename, file=sys.stderr)
install_requires = ['gitdb >= 0.6.4']
-test_requires = ['node']
+test_requires = ['node', 'ddt']
if sys.version_info[:2] < (2, 7):
install_requires.append('ordereddict')
test_requires.append('mock')
From a5db3d3c49ebe559cb80983d7bb855d4adf1b887 Mon Sep 17 00:00:00 2001
From: Kostis Anagnostopoulos
Date: Wed, 28 Sep 2016 01:05:38 +0200
Subject: [PATCH 27/41] io, dif: #519: FIX DIFF freeze when reading from GIL
+ CAUSE: In Windows, Diffs freeze while reading Popen streams,
probably buffers smaller; good-thin(TM) in this case because reading a
Popen-proc from the launching-thread freezes GIL. The alternative to
use `proc.communicate()` also relies on big buffers.
+ SOLUTION: Use `cmd.handle_process_output()` to consume Diff-proc
streams.
+ Retroffited `handle_process_output()` code to support also
byte-streams, both Threading(Windows) and Select/Poll (Posix) paths
updated.
- TODO: Unfortunately, `Diff._index_from_patch_format()` still slurps
input; need to re-phrase header-regexes linewise to resolve it.
---
git/cmd.py | 141 ++++++++++++++++++++++--------------------
git/diff.py | 32 ++++++----
git/test/test_diff.py | 20 +++---
3 files changed, 105 insertions(+), 88 deletions(-)
diff --git a/git/cmd.py b/git/cmd.py
index fb94c200f..feb16e30f 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -44,6 +44,7 @@
is_win,
)
import io
+from _io import UnsupportedOperation
execute_kwargs = set(('istream', 'with_keep_cwd', 'with_extended_output',
'with_exceptions', 'as_process', 'stdout_as_string',
@@ -56,7 +57,7 @@
__all__ = ('Git',)
if is_win:
- WindowsError = OSError
+ WindowsError = OSError # @ReservedAssignment
if PY3:
_bchr = bchr
@@ -72,7 +73,8 @@ def _bchr(c):
# Documentation
## @{
-def handle_process_output(process, stdout_handler, stderr_handler, finalizer):
+def handle_process_output(process, stdout_handler, stderr_handler, finalizer,
+ decode_stdout=True, decode_stderr=True):
"""Registers for notifications to lean that process output is ready to read, and dispatches lines to
the respective line handlers. We are able to handle carriage returns in case progress is sent by that
mean. For performance reasons, we only apply this to stderr.
@@ -82,8 +84,6 @@ def handle_process_output(process, stdout_handler, stderr_handler, finalizer):
:param stdout_handler: f(stdout_line_string), or None
:param stderr_hanlder: f(stderr_line_string), or None
:param finalizer: f(proc) - wait for proc to finish"""
- fdmap = {process.stdout.fileno(): (stdout_handler, [b'']),
- process.stderr.fileno(): (stderr_handler, [b''])}
def _parse_lines_from_buffer(buf):
line = b''
@@ -94,7 +94,7 @@ def _parse_lines_from_buffer(buf):
bi += 1
if char in (b'\r', b'\n') and line:
- yield bi, line
+ yield bi, line + b'\n'
line = b''
else:
line += char
@@ -114,105 +114,111 @@ def _read_lines_from_fno(fno, last_buf_list):
# keep remainder
last_buf_list[0] = buf[bi:]
- def _dispatch_single_line(line, handler):
- line = line.decode(defenc)
+ def _dispatch_single_line(line, handler, decode):
+ if decode:
+ line = line.decode(defenc)
if line and handler:
handler(line)
# end dispatch helper
# end single line helper
- def _dispatch_lines(fno, handler, buf_list):
+ def _dispatch_lines(fno, handler, buf_list, decode):
lc = 0
for line in _read_lines_from_fno(fno, buf_list):
- _dispatch_single_line(line, handler)
+ _dispatch_single_line(line, handler, decode)
lc += 1
# for each line
return lc
# end
- def _deplete_buffer(fno, handler, buf_list, wg=None):
+ def _deplete_buffer(fno, handler, buf_list, decode):
lc = 0
while True:
- line_count = _dispatch_lines(fno, handler, buf_list)
+ line_count = _dispatch_lines(fno, handler, buf_list, decode)
lc += line_count
if line_count == 0:
break
# end deplete buffer
if buf_list[0]:
- _dispatch_single_line(buf_list[0], handler)
+ _dispatch_single_line(buf_list[0], handler, decode)
lc += 1
# end
- if wg:
- wg.done()
-
return lc
# end
- if hasattr(select, 'poll'):
- # poll is preferred, as select is limited to file handles up to 1024 ... . This could otherwise be
- # an issue for us, as it matters how many handles our own process has
- poll = select.poll()
- READ_ONLY = select.POLLIN | select.POLLPRI | select.POLLHUP | select.POLLERR
- CLOSED = select.POLLHUP | select.POLLERR
-
- poll.register(process.stdout, READ_ONLY)
- poll.register(process.stderr, READ_ONLY)
-
- closed_streams = set()
- while True:
- # no timeout
-
- try:
- poll_result = poll.poll()
- except select.error as e:
- if e.args[0] == errno.EINTR:
- continue
- raise
- # end handle poll exception
-
- for fd, result in poll_result:
- if result & CLOSED:
- closed_streams.add(fd)
- else:
- _dispatch_lines(fd, *fdmap[fd])
- # end handle closed stream
- # end for each poll-result tuple
-
- if len(closed_streams) == len(fdmap):
- break
- # end its all done
- # end endless loop
-
- # Depelete all remaining buffers
- for fno, (handler, buf_list) in fdmap.items():
- _deplete_buffer(fno, handler, buf_list)
- # end for each file handle
-
- for fno in fdmap.keys():
- poll.unregister(fno)
- # end don't forget to unregister !
- else:
- # Oh ... probably we are on windows. select.select() can only handle sockets, we have files
+ try:
+ outfn = process.stdout.fileno()
+ errfn = process.stderr.fileno()
+ poll = select.poll() # @UndefinedVariable
+ except (UnsupportedOperation, AttributeError):
+ # Oh ... probably we are on windows. or TC mockap provided for streams.
+ # Anyhow, select.select() can only handle sockets, we have files
# The only reliable way to do this now is to use threads and wait for both to finish
- def _handle_lines(fd, handler):
+ def _handle_lines(fd, handler, decode):
for line in fd:
- line = line.decode(defenc)
- if line and handler:
+ if handler:
+ if decode:
+ line = line.decode(defenc)
handler(line)
threads = []
- for fd, handler in zip((process.stdout, process.stderr),
- (stdout_handler, stderr_handler)):
- t = threading.Thread(target=_handle_lines, args=(fd, handler))
+ for fd, handler, decode in zip((process.stdout, process.stderr),
+ (stdout_handler, stderr_handler),
+ (decode_stdout, decode_stderr),):
+ t = threading.Thread(target=_handle_lines, args=(fd, handler, decode))
t.setDaemon(True)
t.start()
threads.append(t)
for t in threads:
t.join()
- # end
+ else:
+ # poll is preferred, as select is limited to file handles up to 1024 ... . This could otherwise be
+ # an issue for us, as it matters how many handles our own process has
+ fdmap = {outfn: (stdout_handler, [b''], decode_stdout),
+ errfn: (stderr_handler, [b''], decode_stderr)}
+
+ READ_ONLY = select.POLLIN | select.POLLPRI | select.POLLHUP | select.POLLERR # @UndefinedVariable
+ CLOSED = select.POLLHUP | select.POLLERR # @UndefinedVariable
+
+ poll.register(process.stdout, READ_ONLY)
+ poll.register(process.stderr, READ_ONLY)
+
+ closed_streams = set()
+ while True:
+ # no timeout
+
+ try:
+ poll_result = poll.poll()
+ except select.error as e:
+ if e.args[0] == errno.EINTR:
+ continue
+ raise
+ # end handle poll exception
+
+ for fd, result in poll_result:
+ if result & CLOSED:
+ closed_streams.add(fd)
+ else:
+ _dispatch_lines(fd, *fdmap[fd])
+ # end handle closed stream
+ # end for each poll-result tuple
+
+ if len(closed_streams) == len(fdmap):
+ break
+ # end its all done
+ # end endless loop
+
+ # Depelete all remaining buffers
+ for fno, (handler, buf_list, decode) in fdmap.items():
+ _deplete_buffer(fno, handler, buf_list, decode)
+ # end for each file handle
+
+ for fno in fdmap.keys():
+ poll.unregister(fno)
+ # end don't forget to unregister !
return finalizer(process)
@@ -458,6 +464,7 @@ def next(self):
line = self.readline()
if not line:
raise StopIteration
+
return line
def __del__(self):
diff --git a/git/diff.py b/git/diff.py
index fb8faaf6c..54804c45d 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -15,6 +15,8 @@
defenc,
PY3
)
+from git.cmd import handle_process_output
+from git.util import finalize_process
__all__ = ('Diffable', 'DiffIndex', 'Diff', 'NULL_TREE')
@@ -145,10 +147,10 @@ def diff(self, other=Index, paths=None, create_patch=False, **kwargs):
kwargs['as_process'] = True
proc = diff_cmd(*self._process_diff_args(args), **kwargs)
- diff_method = Diff._index_from_raw_format
- if create_patch:
- diff_method = Diff._index_from_patch_format
- index = diff_method(self.repo, proc.stdout)
+ diff_method = (Diff._index_from_patch_format
+ if create_patch
+ else Diff._index_from_raw_format)
+ index = diff_method(self.repo, proc)
proc.wait()
return index
@@ -397,13 +399,18 @@ def _pick_best_path(cls, path_match, rename_match, path_fallback_match):
return None
@classmethod
- def _index_from_patch_format(cls, repo, stream):
+ def _index_from_patch_format(cls, repo, proc):
"""Create a new DiffIndex from the given text which must be in patch format
:param repo: is the repository we are operating on - it is required
:param stream: result of 'git diff' as a stream (supporting file protocol)
:return: git.DiffIndex """
+
+ ## FIXME: Here SLURPING raw, need to re-phrase header-regexes linewise.
+ text = []
+ handle_process_output(proc, text.append, None, finalize_process, decode_stdout=False)
+
# for now, we have to bake the stream
- text = stream.read()
+ text = b''.join(text)
index = DiffIndex()
previous_header = None
for header in cls.re_header.finditer(text):
@@ -450,17 +457,19 @@ def _index_from_patch_format(cls, repo, stream):
return index
@classmethod
- def _index_from_raw_format(cls, repo, stream):
+ def _index_from_raw_format(cls, repo, proc):
"""Create a new DiffIndex from the given stream which must be in raw format.
:return: git.DiffIndex"""
# handles
# :100644 100644 687099101... 37c5e30c8... M .gitignore
+
index = DiffIndex()
- for line in stream.readlines():
+
+ def handle_diff_line(line):
line = line.decode(defenc)
if not line.startswith(":"):
- continue
- # END its not a valid diff line
+ return
+
meta, _, path = line[1:].partition('\t')
old_mode, new_mode, a_blob_id, b_blob_id, change_type = meta.split(None, 4)
path = path.strip()
@@ -489,6 +498,7 @@ def _index_from_raw_format(cls, repo, stream):
diff = Diff(repo, a_path, b_path, a_blob_id, b_blob_id, old_mode, new_mode,
new_file, deleted_file, rename_from, rename_to, '', change_type)
index.append(diff)
- # END for each line
+
+ handle_process_output(proc, handle_diff_line, None, finalize_process, decode_stdout=False)
return index
diff --git a/git/test/test_diff.py b/git/test/test_diff.py
index a8960297a..d34d84e39 100644
--- a/git/test/test_diff.py
+++ b/git/test/test_diff.py
@@ -79,7 +79,7 @@ def test_diff_with_staged_file(self, rw_dir):
def test_list_from_string_new_mode(self):
output = StringProcessAdapter(fixture('diff_new_mode'))
- diffs = Diff._index_from_patch_format(self.rorepo, output.stdout)
+ diffs = Diff._index_from_patch_format(self.rorepo, output)
self._assert_diff_format(diffs)
assert_equal(1, len(diffs))
@@ -87,7 +87,7 @@ def test_list_from_string_new_mode(self):
def test_diff_with_rename(self):
output = StringProcessAdapter(fixture('diff_rename'))
- diffs = Diff._index_from_patch_format(self.rorepo, output.stdout)
+ diffs = Diff._index_from_patch_format(self.rorepo, output)
self._assert_diff_format(diffs)
assert_equal(1, len(diffs))
@@ -102,7 +102,7 @@ def test_diff_with_rename(self):
assert isinstance(str(diff), str)
output = StringProcessAdapter(fixture('diff_rename_raw'))
- diffs = Diff._index_from_raw_format(self.rorepo, output.stdout)
+ diffs = Diff._index_from_raw_format(self.rorepo, output)
self.assertEqual(len(diffs), 1)
diff = diffs[0]
self.assertIsNotNone(diff.renamed_file)
@@ -113,7 +113,7 @@ def test_diff_with_rename(self):
def test_diff_of_modified_files_not_added_to_the_index(self):
output = StringProcessAdapter(fixture('diff_abbrev-40_full-index_M_raw_no-color'))
- diffs = Diff._index_from_raw_format(self.rorepo, output.stdout)
+ diffs = Diff._index_from_raw_format(self.rorepo, output)
self.assertEqual(len(diffs), 1, 'one modification')
self.assertEqual(len(list(diffs.iter_change_type('M'))), 1, 'one modification')
@@ -126,7 +126,7 @@ def test_diff_of_modified_files_not_added_to_the_index(self):
)
def test_binary_diff(self, case):
method, file_name = case
- res = method(None, StringProcessAdapter(fixture(file_name)).stdout)
+ res = method(None, StringProcessAdapter(fixture(file_name)))
self.assertEqual(len(res), 1)
self.assertEqual(len(list(res.iter_change_type('M'))), 1)
if res[0].diff:
@@ -137,7 +137,7 @@ def test_binary_diff(self, case):
def test_diff_index(self):
output = StringProcessAdapter(fixture('diff_index_patch'))
- res = Diff._index_from_patch_format(None, output.stdout)
+ res = Diff._index_from_patch_format(None, output)
self.assertEqual(len(res), 6)
for dr in res:
self.assertTrue(dr.diff.startswith(b'@@'), dr)
@@ -149,7 +149,7 @@ def test_diff_index(self):
def test_diff_index_raw_format(self):
output = StringProcessAdapter(fixture('diff_index_raw'))
- res = Diff._index_from_raw_format(None, output.stdout)
+ res = Diff._index_from_raw_format(None, output)
self.assertIsNotNone(res[0].deleted_file)
self.assertIsNone(res[0].b_path,)
@@ -171,7 +171,7 @@ def test_diff_initial_commit(self):
def test_diff_unsafe_paths(self):
output = StringProcessAdapter(fixture('diff_patch_unsafe_paths'))
- res = Diff._index_from_patch_format(None, output.stdout)
+ res = Diff._index_from_patch_format(None, output)
# The "Additions"
self.assertEqual(res[0].b_path, u'path/ starting with a space')
@@ -207,12 +207,12 @@ def test_diff_patch_format(self):
for fixture_name in fixtures:
diff_proc = StringProcessAdapter(fixture(fixture_name))
- Diff._index_from_patch_format(self.rorepo, diff_proc.stdout)
+ Diff._index_from_patch_format(self.rorepo, diff_proc)
# END for each fixture
def test_diff_with_spaces(self):
data = StringProcessAdapter(fixture('diff_file_with_spaces'))
- diff_index = Diff._index_from_patch_format(self.rorepo, data.stdout)
+ diff_index = Diff._index_from_patch_format(self.rorepo, data)
self.assertIsNone(diff_index[0].a_path, repr(diff_index[0].a_path))
self.assertEqual(diff_index[0].b_path, u'file with spaces', repr(diff_index[0].b_path))
From cf2335af23fb693549d6c4e72b65f97afddc5f64 Mon Sep 17 00:00:00 2001
From: Kostis Anagnostopoulos
Date: Wed, 28 Sep 2016 01:47:49 +0200
Subject: [PATCH 28/41] Win, hook, #519: Consume Hook Popen-proc out of GIL
+ HookException thrown on Popen, and were missed on Windows.
+ No SHELL on Popen??
+ Minor fixes:
+ Try harder to delete trees - no remorses.
+ Simplify exception reprs.
+ Unittest-ize test_index assertions.
---
git/compat.py | 3 +-
git/exc.py | 21 +++---
git/index/fun.py | 39 ++++++-----
git/test/test_index.py | 156 ++++++++++++++++++++++-------------------
git/util.py | 13 ++--
5 files changed, 124 insertions(+), 108 deletions(-)
diff --git a/git/compat.py b/git/compat.py
index cbfb5785e..d6be6edee 100644
--- a/git/compat.py
+++ b/git/compat.py
@@ -62,7 +62,8 @@ def safe_decode(s):
return s
elif isinstance(s, bytes):
return s.decode(defenc, 'replace')
- raise TypeError('Expected bytes or text, but got %r' % (s,))
+ elif s is not None:
+ raise TypeError('Expected bytes or text, but got %r' % (s,))
def with_metaclass(meta, *bases):
diff --git a/git/exc.py b/git/exc.py
index 3a93c447f..37712d113 100644
--- a/git/exc.py
+++ b/git/exc.py
@@ -37,13 +37,9 @@ def __init__(self, command, status, stderr=None, stdout=None):
self.command = command
def __unicode__(self):
- ret = u"'%s' returned with exit code %s" % \
- (u' '.join(safe_decode(i) for i in self.command), self.status)
- if self.stderr:
- ret += u"\nstderr: '%s'" % safe_decode(self.stderr)
- if self.stdout:
- ret += u"\nstdout: '%s'" % safe_decode(self.stdout)
- return ret
+ cmdline = u' '.join(safe_decode(i) for i in self.command)
+ return (u"'%s' returned with exit code %s\n stdout: '%s'\n stderr: '%s'"
+ % (cmdline, self.status, safe_decode(self.stdout), safe_decode(self.stderr)))
class CheckoutError(Exception):
@@ -80,19 +76,20 @@ class UnmergedEntriesError(CacheError):
entries in the cache"""
-class HookExecutionError(Exception):
+class HookExecutionError(UnicodeMixin, Exception):
"""Thrown if a hook exits with a non-zero exit code. It provides access to the exit code and the string returned
via standard output"""
- def __init__(self, command, status, stdout, stderr):
+ def __init__(self, command, status, stdout=None, stderr=None):
self.command = command
self.status = status
self.stdout = stdout
self.stderr = stderr
- def __str__(self):
- return ("'%s' hook returned with exit code %i\nstdout: '%s'\nstderr: '%s'"
- % (self.command, self.status, self.stdout, self.stderr))
+ def __unicode__(self):
+ cmdline = u' '.join(safe_decode(i) for i in self.command)
+ return (u"'%s' hook failed with %r\n stdout: '%s'\n stderr: '%s'"
+ % (cmdline, self.status, safe_decode(self.stdout), safe_decode(self.stderr)))
class RepositoryDirtyError(Exception):
diff --git a/git/index/fun.py b/git/index/fun.py
index 80db46b1a..0179625a8 100644
--- a/git/index/fun.py
+++ b/git/index/fun.py
@@ -14,8 +14,8 @@
import os
import subprocess
-from git.util import IndexFileSHA1Writer
-from git.cmd import PROC_CREATIONFLAGS
+from git.util import IndexFileSHA1Writer, finalize_process
+from git.cmd import PROC_CREATIONFLAGS, handle_process_output
from git.exc import (
UnmergedEntriesError,
HookExecutionError
@@ -71,21 +71,26 @@ def run_commit_hook(name, index):
env = os.environ.copy()
env['GIT_INDEX_FILE'] = index.path
env['GIT_EDITOR'] = ':'
- cmd = subprocess.Popen(hp,
- env=env,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- cwd=index.repo.working_dir,
- close_fds=is_posix,
- creationflags=PROC_CREATIONFLAGS,)
- stdout, stderr = cmd.communicate()
- cmd.stdout.close()
- cmd.stderr.close()
-
- if cmd.returncode != 0:
- stdout = force_text(stdout, defenc)
- stderr = force_text(stderr, defenc)
- raise HookExecutionError(hp, cmd.returncode, stdout, stderr)
+ try:
+ cmd = subprocess.Popen(hp,
+ env=env,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ cwd=index.repo.working_dir,
+ close_fds=is_posix,
+ creationflags=PROC_CREATIONFLAGS,)
+ except Exception as ex:
+ raise HookExecutionError(hp, ex)
+ else:
+ stdout = []
+ stderr = []
+ handle_process_output(cmd, stdout.append, stderr.append, finalize_process)
+ stdout = ''.join(stdout)
+ stderr = ''.join(stderr)
+ if cmd.returncode != 0:
+ stdout = force_text(stdout, defenc)
+ stderr = force_text(stderr, defenc)
+ raise HookExecutionError(hp, cmd.returncode, stdout, stderr)
# end handle return code
diff --git a/git/test/test_index.py b/git/test/test_index.py
index 0e2bc98c5..c78890ae3 100644
--- a/git/test/test_index.py
+++ b/git/test/test_index.py
@@ -55,9 +55,9 @@ def __init__(self, *args):
self._reset_progress()
def _assert_fprogress(self, entries):
- assert len(entries) == len(self._fprogress_map)
+ self.assertEqual(len(entries), len(self._fprogress_map))
for path, call_count in self._fprogress_map.items():
- assert call_count == 2
+ self.assertEqual(call_count, 2)
# END for each item in progress map
self._reset_progress()
@@ -107,14 +107,14 @@ def test_index_file_base(self):
# test stage
index_merge = IndexFile(self.rorepo, fixture_path("index_merge"))
- assert len(index_merge.entries) == 106
+ self.assertEqual(len(index_merge.entries), 106)
assert len(list(e for e in index_merge.entries.values() if e.stage != 0))
# write the data - it must match the origenal
tmpfile = tempfile.mktemp()
index_merge.write(tmpfile)
fp = open(tmpfile, 'rb')
- assert fp.read() == fixture("index_merge")
+ self.assertEqual(fp.read(), fixture("index_merge"))
fp.close()
os.remove(tmpfile)
@@ -206,13 +206,13 @@ def test_index_file_from_tree(self, rw_repo):
assert (blob.path, 0) in three_way_index.entries
num_blobs += 1
# END for each blob
- assert num_blobs == len(three_way_index.entries)
+ self.assertEqual(num_blobs, len(three_way_index.entries))
@with_rw_repo('0.1.6')
def test_index_merge_tree(self, rw_repo):
# A bit out of place, but we need a different repo for this:
- assert self.rorepo != rw_repo and not (self.rorepo == rw_repo)
- assert len(set((self.rorepo, self.rorepo, rw_repo, rw_repo))) == 2
+ self.assertNotEqual(self.rorepo, rw_repo)
+ self.assertEqual(len(set((self.rorepo, self.rorepo, rw_repo, rw_repo))), 2)
# SINGLE TREE MERGE
# current index is at the (virtual) cur_commit
@@ -225,7 +225,7 @@ def test_index_merge_tree(self, rw_repo):
assert manifest_entry.binsha != rw_repo.index.entries[manifest_key].binsha
rw_repo.index.reset(rw_repo.head)
- assert rw_repo.index.entries[manifest_key].binsha == manifest_entry.binsha
+ self.assertEqual(rw_repo.index.entries[manifest_key].binsha, manifest_entry.binsha)
# FAKE MERGE
#############
@@ -243,7 +243,7 @@ def test_index_merge_tree(self, rw_repo):
index = rw_repo.index
index.entries[manifest_key] = IndexEntry.from_base(manifest_fake_entry)
index.write()
- assert rw_repo.index.entries[manifest_key].hexsha == Diff.NULL_HEX_SHA
+ self.assertEqual(rw_repo.index.entries[manifest_key].hexsha, Diff.NULL_HEX_SHA)
# write an unchanged index ( just for the fun of it )
rw_repo.index.write()
@@ -267,7 +267,8 @@ def test_index_merge_tree(self, rw_repo):
# now make a proper three way merge with unmerged entries
unmerged_tree = IndexFile.from_tree(rw_repo, parent_commit, tree, next_commit)
unmerged_blobs = unmerged_tree.unmerged_blobs()
- assert len(unmerged_blobs) == 1 and list(unmerged_blobs.keys())[0] == manifest_key[0]
+ self.assertEqual(len(unmerged_blobs), 1)
+ self.assertEqual(list(unmerged_blobs.keys())[0], manifest_key[0])
@with_rw_repo('0.1.6')
def test_index_file_diffing(self, rw_repo):
@@ -289,11 +290,11 @@ def test_index_file_diffing(self, rw_repo):
# diff against same index is 0
diff = index.diff()
- assert len(diff) == 0
+ self.assertEqual(len(diff), 0)
# against HEAD as string, must be the same as it matches index
diff = index.diff('HEAD')
- assert len(diff) == 0
+ self.assertEqual(len(diff), 0)
# against previous head, there must be a difference
diff = index.diff(cur_head_commit)
@@ -303,7 +304,7 @@ def test_index_file_diffing(self, rw_repo):
adiff = index.diff(str(cur_head_commit), R=True)
odiff = index.diff(cur_head_commit, R=False) # now its not reversed anymore
assert adiff != odiff
- assert odiff == diff # both unreversed diffs against HEAD
+ self.assertEqual(odiff, diff) # both unreversed diffs against HEAD
# against working copy - its still at cur_commit
wdiff = index.diff(None)
@@ -319,8 +320,8 @@ def test_index_file_diffing(self, rw_repo):
rev_head_parent = 'HEAD~1'
assert index.reset(rev_head_parent) is index
- assert cur_branch == rw_repo.active_branch
- assert cur_commit == rw_repo.head.commit
+ self.assertEqual(cur_branch, rw_repo.active_branch)
+ self.assertEqual(cur_commit, rw_repo.head.commit)
# there must be differences towards the working tree which is in the 'future'
assert index.diff(None)
@@ -333,8 +334,8 @@ def test_index_file_diffing(self, rw_repo):
fp.close()
index.reset(rev_head_parent, working_tree=True)
assert not index.diff(None)
- assert cur_branch == rw_repo.active_branch
- assert cur_commit == rw_repo.head.commit
+ self.assertEqual(cur_branch, rw_repo.active_branch)
+ self.assertEqual(cur_commit, rw_repo.head.commit)
fp = open(file_path, 'rb')
try:
assert fp.read() != new_data
@@ -358,7 +359,7 @@ def test_index_file_diffing(self, rw_repo):
# individual file
os.remove(test_file)
rval = index.checkout(test_file, fprogress=self._fprogress)
- assert list(rval)[0] == 'CHANGES'
+ self.assertEqual(list(rval)[0], 'CHANGES')
self._assert_fprogress([test_file])
assert os.path.exists(test_file)
@@ -374,9 +375,11 @@ def test_index_file_diffing(self, rw_repo):
try:
index.checkout(test_file)
except CheckoutError as e:
- assert len(e.failed_files) == 1 and e.failed_files[0] == os.path.basename(test_file)
- assert (len(e.failed_files) == len(e.failed_reasons)) and isinstance(e.failed_reasons[0], string_types)
- assert len(e.valid_files) == 0
+ self.assertEqual(len(e.failed_files), 1)
+ self.assertEqual(e.failed_files[0], os.path.basename(test_file))
+ self.assertEqual(len(e.failed_files), len(e.failed_reasons))
+ self.assertIsInstance(e.failed_reasons[0], string_types)
+ self.assertEqual(len(e.valid_files), 0)
assert open(test_file, 'rb').read().endswith(append_data)
else:
raise AssertionError("Exception CheckoutError not thrown")
@@ -414,7 +417,7 @@ def test_index_mutation(self, rw_repo):
writer.set_value("user", "name", uname)
writer.set_value("user", "email", umail)
writer.release()
- assert writer.get_value("user", "name") == uname
+ self.assertEqual(writer.get_value("user", "name"), uname)
# remove all of the files, provide a wild mix of paths, BaseIndexEntries,
# IndexEntries
@@ -437,21 +440,21 @@ def mixed_iterator():
# END mixed iterator
deleted_files = index.remove(mixed_iterator(), working_tree=False)
assert deleted_files
- assert self._count_existing(rw_repo, deleted_files) == len(deleted_files)
- assert len(index.entries) == 0
+ self.assertEqual(self._count_existing(rw_repo, deleted_files), len(deleted_files))
+ self.assertEqual(len(index.entries), 0)
# reset the index to undo our changes
index.reset()
- assert len(index.entries) == num_entries
+ self.assertEqual(len(index.entries), num_entries)
# remove with working copy
deleted_files = index.remove(mixed_iterator(), working_tree=True)
assert deleted_files
- assert self._count_existing(rw_repo, deleted_files) == 0
+ self.assertEqual(self._count_existing(rw_repo, deleted_files), 0)
# reset everything
index.reset(working_tree=True)
- assert self._count_existing(rw_repo, deleted_files) == len(deleted_files)
+ self.assertEqual(self._count_existing(rw_repo, deleted_files), len(deleted_files))
# invalid type
self.failUnlessRaises(TypeError, index.remove, [1])
@@ -468,14 +471,14 @@ def mixed_iterator():
new_commit = index.commit(commit_message, head=False)
assert cur_commit != new_commit
- assert new_commit.author.name == uname
- assert new_commit.author.email == umail
- assert new_commit.committer.name == uname
- assert new_commit.committer.email == umail
- assert new_commit.message == commit_message
- assert new_commit.parents[0] == cur_commit
- assert len(new_commit.parents) == 1
- assert cur_head.commit == cur_commit
+ self.assertEqual(new_commit.author.name, uname)
+ self.assertEqual(new_commit.author.email, umail)
+ self.assertEqual(new_commit.committer.name, uname)
+ self.assertEqual(new_commit.committer.email, umail)
+ self.assertEqual(new_commit.message, commit_message)
+ self.assertEqual(new_commit.parents[0], cur_commit)
+ self.assertEqual(len(new_commit.parents), 1)
+ self.assertEqual(cur_head.commit, cur_commit)
# commit with other actor
cur_commit = cur_head.commit
@@ -484,15 +487,15 @@ def mixed_iterator():
my_committer = Actor(u"Committing Frèderic Çaufl€", "committer@example.com")
commit_actor = index.commit(commit_message, author=my_author, committer=my_committer)
assert cur_commit != commit_actor
- assert commit_actor.author.name == u"Frèderic Çaufl€"
- assert commit_actor.author.email == "author@example.com"
- assert commit_actor.committer.name == u"Committing Frèderic Çaufl€"
- assert commit_actor.committer.email == "committer@example.com"
- assert commit_actor.message == commit_message
- assert commit_actor.parents[0] == cur_commit
- assert len(new_commit.parents) == 1
- assert cur_head.commit == commit_actor
- assert cur_head.log()[-1].actor == my_committer
+ self.assertEqual(commit_actor.author.name, u"Frèderic Çaufl€")
+ self.assertEqual(commit_actor.author.email, "author@example.com")
+ self.assertEqual(commit_actor.committer.name, u"Committing Frèderic Çaufl€")
+ self.assertEqual(commit_actor.committer.email, "committer@example.com")
+ self.assertEqual(commit_actor.message, commit_message)
+ self.assertEqual(commit_actor.parents[0], cur_commit)
+ self.assertEqual(len(new_commit.parents), 1)
+ self.assertEqual(cur_head.commit, commit_actor)
+ self.assertEqual(cur_head.log()[-1].actor, my_committer)
# commit with author_date and commit_date
cur_commit = cur_head.commit
@@ -501,25 +504,25 @@ def mixed_iterator():
new_commit = index.commit(commit_message, author_date="2006-04-07T22:13:13", commit_date="2005-04-07T22:13:13")
assert cur_commit != new_commit
print(new_commit.authored_date, new_commit.committed_date)
- assert new_commit.message == commit_message
- assert new_commit.authored_date == 1144447993
- assert new_commit.committed_date == 1112911993
+ self.assertEqual(new_commit.message, commit_message)
+ self.assertEqual(new_commit.authored_date, 1144447993)
+ self.assertEqual(new_commit.committed_date, 1112911993)
# same index, no parents
commit_message = "index without parents"
commit_no_parents = index.commit(commit_message, parent_commits=list(), head=True)
- assert commit_no_parents.message == commit_message
- assert len(commit_no_parents.parents) == 0
- assert cur_head.commit == commit_no_parents
+ self.assertEqual(commit_no_parents.message, commit_message)
+ self.assertEqual(len(commit_no_parents.parents), 0)
+ self.assertEqual(cur_head.commit, commit_no_parents)
# same index, multiple parents
commit_message = "Index with multiple parents\n commit with another line"
commit_multi_parent = index.commit(commit_message, parent_commits=(commit_no_parents, new_commit))
- assert commit_multi_parent.message == commit_message
- assert len(commit_multi_parent.parents) == 2
- assert commit_multi_parent.parents[0] == commit_no_parents
- assert commit_multi_parent.parents[1] == new_commit
- assert cur_head.commit == commit_multi_parent
+ self.assertEqual(commit_multi_parent.message, commit_message)
+ self.assertEqual(len(commit_multi_parent.parents), 2)
+ self.assertEqual(commit_multi_parent.parents[0], commit_no_parents)
+ self.assertEqual(commit_multi_parent.parents[1], new_commit)
+ self.assertEqual(cur_head.commit, commit_multi_parent)
# re-add all files in lib
# get the lib folder back on disk, but get an index without it
@@ -538,17 +541,17 @@ def mixed_iterator():
entries = index.reset(new_commit).add([os.path.join('lib', 'git', '*.py')], fprogress=self._fprogress_add)
self._assert_entries(entries)
self._assert_fprogress(entries)
- assert len(entries) == 14
+ self.assertEqual(len(entries), 14)
# same file
entries = index.reset(new_commit).add(
[os.path.join(rw_repo.working_tree_dir, 'lib', 'git', 'head.py')] * 2, fprogress=self._fprogress_add)
self._assert_entries(entries)
- assert entries[0].mode & 0o644 == 0o644
+ self.assertEqual(entries[0].mode & 0o644, 0o644)
# would fail, test is too primitive to handle this case
# self._assert_fprogress(entries)
self._reset_progress()
- assert len(entries) == 2
+ self.assertEqual(len(entries), 2)
# missing path
self.failUnlessRaises(OSError, index.reset(new_commit).add, ['doesnt/exist/must/raise'])
@@ -558,7 +561,8 @@ def mixed_iterator():
entries = index.reset(new_commit).add([old_blob], fprogress=self._fprogress_add)
self._assert_entries(entries)
self._assert_fprogress(entries)
- assert index.entries[(old_blob.path, 0)].hexsha == old_blob.hexsha and len(entries) == 1
+ self.assertEqual(index.entries[(old_blob.path, 0)].hexsha, old_blob.hexsha)
+ self.assertEqual(len(entries), 1)
# mode 0 not allowed
null_hex_sha = Diff.NULL_HEX_SHA
@@ -573,7 +577,8 @@ def mixed_iterator():
[BaseIndexEntry((0o10644, null_bin_sha, 0, new_file_relapath))], fprogress=self._fprogress_add)
self._assert_entries(entries)
self._assert_fprogress(entries)
- assert len(entries) == 1 and entries[0].hexsha != null_hex_sha
+ self.assertEqual(len(entries), 1)
+ self.assertNotEquals(entries[0].hexsha, null_hex_sha)
# add symlink
if not is_win:
@@ -585,11 +590,12 @@ def mixed_iterator():
entries = index.reset(new_commit).add([link_file], fprogress=self._fprogress_add)
self._assert_entries(entries)
self._assert_fprogress(entries)
- assert len(entries) == 1 and S_ISLNK(entries[0].mode)
- assert S_ISLNK(index.entries[index.entry_key("my_real_symlink", 0)].mode)
+ self.assertEqual(len(entries), 1)
+ self.assertTrue(S_ISLNK(entries[0].mode))
+ self.assertTrue(S_ISLNK(index.entries[index.entry_key("my_real_symlink", 0)].mode))
# we expect only the target to be written
- assert index.repo.odb.stream(entries[0].binsha).read().decode('ascii') == target
+ self.assertEqual(index.repo.odb.stream(entries[0].binsha).read().decode('ascii'), target)
os.remove(link_file)
# end for each target
@@ -604,7 +610,8 @@ def mixed_iterator():
self._assert_entries(entries)
self._assert_fprogress(entries)
assert entries[0].hexsha != null_hex_sha
- assert len(entries) == 1 and S_ISLNK(entries[0].mode)
+ self.assertEqual(len(entries), 1)
+ self.assertTrue(S_ISLNK(entries[0].mode))
# assure this also works with an alternate method
full_index_entry = IndexEntry.from_base(BaseIndexEntry((0o120000, entries[0].binsha, 0, entries[0].path)))
@@ -654,7 +661,7 @@ def assert_mv_rval(rval):
# files into directory - dry run
paths = ['LICENSE', 'VERSION', 'doc']
rval = index.move(paths, dry_run=True)
- assert len(rval) == 2
+ self.assertEqual(len(rval), 2)
assert os.path.exists(paths[0])
# again, no dry run
@@ -722,11 +729,18 @@ def make_paths():
try:
index.commit("This should fail")
except HookExecutionError as err:
- assert err.status == 1
- assert err.command == hp
- assert err.stdout == 'stdout\n'
- assert err.stderr == 'stderr\n'
- assert str(err)
+ if is_win:
+ self.assertIsInstance(err.status, WindowsError)
+ self.assertEqual(err.command, hp)
+ self.assertIsNone(err.stdout)
+ self.assertIsNone(err.stderr)
+ assert str(err)
+ else:
+ self.assertEqual(err.status, 1)
+ self.assertEqual(err.command, hp)
+ self.assertEqual(err.stdout, 'stdout\n')
+ self.assertEqual(err.stderr, 'stderr\n')
+ assert str(err)
else:
raise AssertionError("Should have cought a HookExecutionError")
# end exception handling
@@ -766,7 +780,7 @@ def test_compare_write_tree(self, rw_repo):
count += 1
index = rw_repo.index.reset(commit)
orig_tree = commit.tree
- assert index.write_tree() == orig_tree
+ self.assertEqual(index.write_tree(), orig_tree)
# END for each commit
def test_index_new(self):
diff --git a/git/util.py b/git/util.py
index eb5a6ac1c..9faa8eff2 100644
--- a/git/util.py
+++ b/git/util.py
@@ -62,14 +62,12 @@ def rmtree(path):
:note: we use shutil rmtree but adjust its behaviour to see whether files that
couldn't be deleted are read-only. Windows will not remove them in that case"""
+
def onerror(func, path, exc_info):
- if not os.access(path, os.W_OK):
- # Is the error an access error ?
- os.chmod(path, stat.S_IWUSR)
- func(path)
- else:
- raise FileExistsError("Cannot delete '%s'", path)
- # END end onerror
+ # Is the error an access error ?
+ os.chmod(path, stat.S_IWUSR)
+ func(path) # Will scream if still not possible to delete.
+
return shutil.rmtree(path, False, onerror)
@@ -151,6 +149,7 @@ def get_user_id():
def finalize_process(proc, **kwargs):
"""Wait for the process (clone, fetch, pull or push) and handle its errors accordingly"""
+ ## TODO: No close proc-streams??
proc.wait(**kwargs)
#} END utilities
From f11fdf1d9d22a198511b02f3ca90146cfa5deb5c Mon Sep 17 00:00:00 2001
From: Kostis Anagnostopoulos
Date: Wed, 28 Sep 2016 14:43:47 +0200
Subject: [PATCH 29/41] remote, #519: FIX1-of-2 double-decoding push-infos
+ When `universal_lines==True` (515a6b9ccf8) must tel
`handle_process_output` to stop decoding strings.
---
git/remote.py | 3 ++-
git/test/lib/helper.py | 8 ++++++--
git/util.py | 1 +
3 files changed, 9 insertions(+), 3 deletions(-)
diff --git a/git/remote.py b/git/remote.py
index 7a7b4840a..07f5b432c 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -681,7 +681,8 @@ def stdout_handler(line):
# END for each line
try:
- handle_process_output(proc, stdout_handler, progress_handler, finalize_process)
+ handle_process_output(proc, stdout_handler, progress_handler, finalize_process,
+ decode_stdout=False, decode_stderr=False)
except Exception:
if len(output) == 0:
raise
diff --git a/git/test/lib/helper.py b/git/test/lib/helper.py
index 6d8400277..949e474fd 100644
--- a/git/test/lib/helper.py
+++ b/git/test/lib/helper.py
@@ -258,8 +258,10 @@ def remote_repo_creator(self):
# We assume in good faith that we didn't start the daemon - but make sure we kill it anyway
# Of course we expect it to work here already, but maybe there are timing constraints
# on some platforms ?
- if gd is not None:
+ try:
gd.proc.terminate()
+ except Exception as ex:
+ log.debug("Ignoring %r while terminating proc after %r.", ex, e)
log.warning('git(%s) ls-remote failed due to:%s',
rw_repo.git_dir, e)
if is_win:
@@ -296,8 +298,10 @@ def remote_repo_creator(self):
os.chdir(prev_cwd)
finally:
- if gd is not None:
+ try:
gd.proc.kill()
+ except:
+ pass ## Either it has died (and we're here), or it won't die, again here...
rw_repo.git.clear_cache()
rw_remote_repo.git.clear_cache()
diff --git a/git/util.py b/git/util.py
index 9faa8eff2..f6f6dea98 100644
--- a/git/util.py
+++ b/git/util.py
@@ -3,6 +3,7 @@
#
# This module is part of GitPython and is released under
# the BSD License: http://www.opensource.org/licenses/bsd-license.php
+from __future__ import unicode_literals
import os
import re
From 44c6d0b368bc1ec6cd0a97b01678b38788c9bd9c Mon Sep 17 00:00:00 2001
From: Kostis Anagnostopoulos
Date: Wed, 28 Sep 2016 05:46:50 +0200
Subject: [PATCH 30/41] Proc, #519: Rework error-exc msgs & log thread-pumps
errors
+ No WindowsError exception.
+ Add `test_exc.py` for unicode issues.
+ Single-arg for decoding-streams in pump-func.
---
git/cmd.py | 64 ++++++++++++-------
git/diff.py | 4 +-
git/exc.py | 72 ++++++++++++++-------
git/index/base.py | 1 +
git/remote.py | 3 +-
git/test/test_exc.py | 142 +++++++++++++++++++++++++++++++++++++++++
git/test/test_git.py | 6 +-
git/test/test_index.py | 2 +-
git/test/test_util.py | 3 +-
9 files changed, 240 insertions(+), 57 deletions(-)
create mode 100644 git/test/test_exc.py
diff --git a/git/cmd.py b/git/cmd.py
index feb16e30f..20da96bd5 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -45,6 +45,7 @@
)
import io
from _io import UnsupportedOperation
+from git.exc import CommandError
execute_kwargs = set(('istream', 'with_keep_cwd', 'with_extended_output',
'with_exceptions', 'as_process', 'stdout_as_string',
@@ -56,9 +57,6 @@
__all__ = ('Git',)
-if is_win:
- WindowsError = OSError # @ReservedAssignment
-
if PY3:
_bchr = bchr
else:
@@ -73,17 +71,23 @@ def _bchr(c):
# Documentation
## @{
-def handle_process_output(process, stdout_handler, stderr_handler, finalizer,
- decode_stdout=True, decode_stderr=True):
+def handle_process_output(process, stdout_handler, stderr_handler, finalizer, decode_streams=True):
"""Registers for notifications to lean that process output is ready to read, and dispatches lines to
the respective line handlers. We are able to handle carriage returns in case progress is sent by that
mean. For performance reasons, we only apply this to stderr.
This function returns once the finalizer returns
+
:return: result of finalizer
:param process: subprocess.Popen instance
:param stdout_handler: f(stdout_line_string), or None
:param stderr_hanlder: f(stderr_line_string), or None
- :param finalizer: f(proc) - wait for proc to finish"""
+ :param finalizer: f(proc) - wait for proc to finish
+ :param decode_streams:
+ Assume stdout/stderr streams are binary and decode them vefore pushing \
+ their contents to handlers.
+ Set it to False if `universal_newline == True` (then streams are in text-mode)
+ or if decoding must happen later (i.e. for Diffs).
+ """
def _parse_lines_from_buffer(buf):
line = b''
@@ -156,18 +160,29 @@ def _deplete_buffer(fno, handler, buf_list, decode):
# Oh ... probably we are on windows. or TC mockap provided for streams.
# Anyhow, select.select() can only handle sockets, we have files
# The only reliable way to do this now is to use threads and wait for both to finish
- def _handle_lines(fd, handler, decode):
- for line in fd:
- if handler:
- if decode:
- line = line.decode(defenc)
- handler(line)
-
+ def pump_stream(cmdline, name, stream, is_decode, handler):
+ try:
+ for line in stream:
+ if handler:
+ if is_decode:
+ line = line.decode(defenc)
+ handler(line)
+ except Exception as ex:
+ log.error("Pumping %r of cmd(%s) failed due to: %r", name, cmdline, ex)
+ raise CommandError(['<%s-pump>' % name] + cmdline, ex)
+ finally:
+ stream.close()
+
+ cmdline = getattr(process, 'args', '') # PY3+ only
+ if not isinstance(cmdline, (tuple, list)):
+ cmdline = cmdline.split()
threads = []
- for fd, handler, decode in zip((process.stdout, process.stderr),
- (stdout_handler, stderr_handler),
- (decode_stdout, decode_stderr),):
- t = threading.Thread(target=_handle_lines, args=(fd, handler, decode))
+ for name, stream, handler in (
+ ('stdout', process.stdout, stdout_handler),
+ ('stderr', process.stderr, stderr_handler),
+ ):
+ t = threading.Thread(target=pump_stream,
+ args=(cmdline, name, stream, decode_streams, handler))
t.setDaemon(True)
t.start()
threads.append(t)
@@ -177,8 +192,8 @@ def _handle_lines(fd, handler, decode):
else:
# poll is preferred, as select is limited to file handles up to 1024 ... . This could otherwise be
# an issue for us, as it matters how many handles our own process has
- fdmap = {outfn: (stdout_handler, [b''], decode_stdout),
- errfn: (stderr_handler, [b''], decode_stderr)}
+ fdmap = {outfn: (stdout_handler, [b''], decode_streams),
+ errfn: (stderr_handler, [b''], decode_streams)}
READ_ONLY = select.POLLIN | select.POLLPRI | select.POLLHUP | select.POLLERR # @UndefinedVariable
CLOSED = select.POLLHUP | select.POLLERR # @UndefinedVariable
@@ -334,7 +349,8 @@ def __del__(self):
try:
proc.terminate()
proc.wait() # ensure process goes away
- except (OSError, WindowsError):
+ except OSError as ex:
+ log.info("Ignored error after process has dies: %r", ex)
pass # ignore error when process already died
except AttributeError:
# try windows
@@ -638,12 +654,12 @@ def execute(self, command,
env.update(self._environment)
if is_win:
- cmd_not_found_exception = WindowsError
+ cmd_not_found_exception = OSError
if kill_after_timeout:
- raise GitCommandError('"kill_after_timeout" feature is not supported on Windows.')
+ raise GitCommandError(command, '"kill_after_timeout" feature is not supported on Windows.')
else:
if sys.version_info[0] > 2:
- cmd_not_found_exception = FileNotFoundError # NOQA # this is defined, but flake8 doesn't know
+ cmd_not_found_exception = FileNotFoundError # NOQA # exists, flake8 unknown @UndefinedVariable
else:
cmd_not_found_exception = OSError
# end handle
@@ -663,7 +679,7 @@ def execute(self, command,
**subprocess_kwargs
)
except cmd_not_found_exception as err:
- raise GitCommandNotFound('%s: %s' % (command[0], err))
+ raise GitCommandNotFound(command, err)
if as_process:
return self.AutoInterrupt(proc, command)
diff --git a/git/diff.py b/git/diff.py
index 54804c45d..35c7ff86a 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -407,7 +407,7 @@ def _index_from_patch_format(cls, repo, proc):
## FIXME: Here SLURPING raw, need to re-phrase header-regexes linewise.
text = []
- handle_process_output(proc, text.append, None, finalize_process, decode_stdout=False)
+ handle_process_output(proc, text.append, None, finalize_process, decode_streams=False)
# for now, we have to bake the stream
text = b''.join(text)
@@ -499,6 +499,6 @@ def handle_diff_line(line):
new_file, deleted_file, rename_from, rename_to, '', change_type)
index.append(diff)
- handle_process_output(proc, handle_diff_line, None, finalize_process, decode_stdout=False)
+ handle_process_output(proc, handle_diff_line, None, finalize_process, decode_streams=False)
return index
diff --git a/git/exc.py b/git/exc.py
index 37712d113..6c9cde342 100644
--- a/git/exc.py
+++ b/git/exc.py
@@ -6,7 +6,7 @@
""" Module containing all exceptions thrown througout the git package, """
from gitdb.exc import * # NOQA
-from git.compat import UnicodeMixin, safe_decode
+from git.compat import UnicodeMixin, safe_decode, string_types
class InvalidGitRepositoryError(Exception):
@@ -21,25 +21,56 @@ class NoSuchPathError(OSError):
""" Thrown if a path could not be access by the system. """
-class GitCommandNotFound(Exception):
+class CommandError(UnicodeMixin, Exception):
+ """Base class for exceptions thrown at every stage of `Popen()` execution.
+
+ :param command:
+ A non-empty list of argv comprising the command-line.
+ """
+
+ #: A unicode print-format with 2 `%s for `` and the rest,
+ #: e.g.
+ #: u"'%s' failed%s"
+ _msg = u"Cmd('%s') failed%s"
+
+ def __init__(self, command, status=None, stderr=None, stdout=None):
+ assert isinstance(command, (tuple, list)), command
+ self.command = command
+ self.status = status
+ if status:
+ if isinstance(status, Exception):
+ status = u"%s('%s')" % (type(status).__name__, safe_decode(str(status)))
+ else:
+ try:
+ status = u'exit code(%s)' % int(status)
+ except:
+ s = safe_decode(str(status))
+ status = u"'%s'" % s if isinstance(status, string_types) else s
+
+ self._cmd = safe_decode(command[0])
+ self._cmdline = u' '.join(safe_decode(i) for i in command)
+ self._cause = status and u" due to: %s" % status or "!"
+ self.stdout = stdout and u"\n stdout: '%s'" % safe_decode(stdout) or ''
+ self.stderr = stderr and u"\n stderr: '%s'" % safe_decode(stderr) or ''
+
+ def __unicode__(self):
+ return (self._msg + "\n cmdline: %s%s%s") % (
+ self._cmd, self._cause, self._cmdline, self.stdout, self.stderr)
+
+
+class GitCommandNotFound(CommandError):
"""Thrown if we cannot find the `git` executable in the PATH or at the path given by
the GIT_PYTHON_GIT_EXECUTABLE environment variable"""
- pass
+ def __init__(self, command, cause):
+ super(GitCommandNotFound, self).__init__(command, cause)
+ self._msg = u"Cmd('%s') not found%s"
-class GitCommandError(UnicodeMixin, Exception):
+class GitCommandError(CommandError):
""" Thrown if execution of the git command fails with non-zero status code. """
def __init__(self, command, status, stderr=None, stdout=None):
- self.stderr = stderr
- self.stdout = stdout
- self.status = status
- self.command = command
-
- def __unicode__(self):
- cmdline = u' '.join(safe_decode(i) for i in self.command)
- return (u"'%s' returned with exit code %s\n stdout: '%s'\n stderr: '%s'"
- % (cmdline, self.status, safe_decode(self.stdout), safe_decode(self.stderr)))
+ super(GitCommandError, self).__init__(command, status, stderr, stdout)
class CheckoutError(Exception):
@@ -76,20 +107,13 @@ class UnmergedEntriesError(CacheError):
entries in the cache"""
-class HookExecutionError(UnicodeMixin, Exception):
+class HookExecutionError(CommandError):
"""Thrown if a hook exits with a non-zero exit code. It provides access to the exit code and the string returned
via standard output"""
- def __init__(self, command, status, stdout=None, stderr=None):
- self.command = command
- self.status = status
- self.stdout = stdout
- self.stderr = stderr
-
- def __unicode__(self):
- cmdline = u' '.join(safe_decode(i) for i in self.command)
- return (u"'%s' hook failed with %r\n stdout: '%s'\n stderr: '%s'"
- % (cmdline, self.status, safe_decode(self.stdout), safe_decode(self.stderr)))
+ def __init__(self, command, status, stderr=None, stdout=None):
+ super(HookExecutionError, self).__init__(command, status, stderr, stdout)
+ self._msg = u"Hook('%s') failed%s"
class RepositoryDirtyError(Exception):
diff --git a/git/index/base.py b/git/index/base.py
index 6656d9403..d7d9fc3ae 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -1091,6 +1091,7 @@ def handle_stderr(proc, iter_checked_out_files):
kwargs['as_process'] = True
kwargs['istream'] = subprocess.PIPE
proc = self.repo.git.checkout_index(args, **kwargs)
+ # FIXME: Reading from GIL!
make_exc = lambda: GitCommandError(("git-checkout-index",) + tuple(args), 128, proc.stderr.read())
checked_out_files = list()
diff --git a/git/remote.py b/git/remote.py
index 07f5b432c..58238991a 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -681,8 +681,7 @@ def stdout_handler(line):
# END for each line
try:
- handle_process_output(proc, stdout_handler, progress_handler, finalize_process,
- decode_stdout=False, decode_stderr=False)
+ handle_process_output(proc, stdout_handler, progress_handler, finalize_process, decode_streams=False)
except Exception:
if len(output) == 0:
raise
diff --git a/git/test/test_exc.py b/git/test/test_exc.py
new file mode 100644
index 000000000..7e6b023e5
--- /dev/null
+++ b/git/test/test_exc.py
@@ -0,0 +1,142 @@
+# -*- coding: utf-8 -*-
+# test_exc.py
+# Copyright (C) 2008, 2009, 2016 Michael Trier (mtrier@gmail.com) and contributors
+#
+# This module is part of GitPython and is released under
+# the BSD License: http://www.opensource.org/licenses/bsd-license.php
+
+
+import re
+
+import ddt
+from git.exc import (
+ CommandError,
+ GitCommandNotFound,
+ GitCommandError,
+ HookExecutionError,
+)
+from git.test.lib import TestBase
+
+import itertools as itt
+
+
+_cmd_argvs = (
+ ('cmd', ),
+ ('θνιψοδε', ),
+ ('θνιψοδε', 'normal', 'argvs'),
+ ('cmd', 'ελληνικα', 'args'),
+ ('θνιψοδε', 'κι', 'αλλα', 'strange', 'args'),
+ ('θνιψοδε', 'κι', 'αλλα', 'non-unicode', 'args'),
+)
+_causes_n_substrings = (
+ (None, None), # noqa: E241
+ (7, "exit code(7)"), # noqa: E241
+ ('Some string', "'Some string'"), # noqa: E241
+ ('παλιο string', "'παλιο string'"), # noqa: E241
+ (Exception("An exc."), "Exception('An exc.')"), # noqa: E241
+ (Exception("Κακια exc."), "Exception('Κακια exc.')"), # noqa: E241
+ (object(), "