summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniele Esposti <daniele.esposti@gmail.com>2016-10-06 18:55:42 +0100
committerDaniele Esposti <daniele.esposti@gmail.com>2016-10-06 18:55:42 +0100
commit26f9b9f40144b73d75e739ef5e6552007b6376f8 (patch)
tree957cc9dbcafad0e7887df835bb5f85eadeb67288
parentf0fcc072b37d6e773e18ea561ebce8ccba2ba19c (diff)
parent833ac6ec4c9f185fd40af7852b6878326f44a0b3 (diff)
downloadgitpython-26f9b9f40144b73d75e739ef5e6552007b6376f8.tar.gz
Merge remote-tracking branch 'upstream/master'
Conflicts: git/util.py
-rw-r--r--.appveyor.yml84
-rw-r--r--.gitattributes2
-rw-r--r--.travis.yml9
-rw-r--r--README.md61
-rw-r--r--doc/source/conf.py3
-rw-r--r--doc/source/index.rst1
-rw-r--r--doc/source/intro.rst8
-rw-r--r--doc/source/tutorial.rst124
-rw-r--r--doc/source/whatsnew.rst25
-rw-r--r--git/__init__.py24
-rw-r--r--git/cmd.py363
-rw-r--r--git/compat.py74
-rw-r--r--git/config.py53
-rw-r--r--git/db.py4
-rw-r--r--git/diff.py32
-rw-r--r--git/exc.py79
-rw-r--r--git/index/base.py48
-rw-r--r--git/index/fun.py47
-rw-r--r--git/index/util.py14
-rw-r--r--git/objects/__init__.py16
-rw-r--r--git/objects/base.py2
-rw-r--r--git/objects/commit.py4
-rw-r--r--git/objects/fun.py4
-rw-r--r--git/objects/submodule/base.py115
-rw-r--r--git/objects/tag.py6
-rw-r--r--git/refs/head.py21
-rw-r--r--git/refs/reference.py2
-rw-r--r--git/refs/symbolic.py86
-rw-r--r--git/remote.py36
-rw-r--r--git/repo/base.py33
-rw-r--r--git/repo/fun.py6
-rw-r--r--git/test/fixtures/cat_file.py7
-rw-r--r--git/test/lib/asserts.py17
-rw-r--r--git/test/lib/helper.py220
-rw-r--r--git/test/performance/lib.py5
-rw-r--r--git/test/performance/test_commit.py4
-rw-r--r--git/test/performance/test_odb.py9
-rw-r--r--git/test/performance/test_streams.py7
-rw-r--r--git/test/test_base.py23
-rw-r--r--git/test/test_commit.py126
-rw-r--r--git/test/test_config.py194
-rw-r--r--git/test/test_diff.py119
-rw-r--r--git/test/test_docs.py95
-rw-r--r--git/test/test_exc.py142
-rw-r--r--git/test/test_git.py93
-rw-r--r--git/test/test_index.py290
-rw-r--r--git/test/test_reflog.py5
-rw-r--r--git/test/test_refs.py20
-rw-r--r--git/test/test_remote.py37
-rw-r--r--git/test/test_repo.py332
-rw-r--r--git/test/test_submodule.py154
-rw-r--r--git/test/test_tree.py19
-rw-r--r--git/test/test_util.py130
-rw-r--r--git/util.py122
-rw-r--r--requirements.txt2
-rwxr-xr-xsetup.py52
56 files changed, 2068 insertions, 1542 deletions
diff --git a/.appveyor.yml b/.appveyor.yml
new file mode 100644
index 00000000..9b87d962
--- /dev/null
+++ b/.appveyor.yml
@@ -0,0 +1,84 @@
+# 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:
+ ## MINGW
+ #
+ - PYTHON: "C:\\Python27"
+ PYTHON_VERSION: "2.7"
+ GIT_PATH: "%GIT_DAEMON_PATH%"
+ - PYTHON: "C:\\Python34-x64"
+ PYTHON_VERSION: "3.4"
+ GIT_PATH: "%GIT_DAEMON_PATH%"
+ - PYTHON: "C:\\Python35-x64"
+ PYTHON_VERSION: "3.5"
+ GIT_PATH: "%GIT_DAEMON_PATH%"
+ - PYTHON: "C:\\Miniconda35-x64"
+ PYTHON_VERSION: "3.5"
+ IS_CONDA: "yes"
+ GIT_PATH: "%GIT_DAEMON_PATH%"
+
+ # ## Cygwin
+ # #
+ # - 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: "%CYGWIN_GIT_PATH%"
+ # - PYTHON: "C:\\Python35-x64"
+ # PYTHON_VERSION: "3.5"
+ # GIT_PATH: "%CYGWIN64_GIT_PATH%"
+
+
+install:
+ - set PATH=%PYTHON%;%PYTHON%\Scripts;%GIT_PATH%;%PATH%
+
+ ## Print configuration for debugging.
+ #
+ - |
+ echo %PATH%
+ uname -a
+ where git git-daemon python pip pip3 pip34
+ python --version
+ python -c "import struct; print(struct.calcsize('P') * 8)"
+
+ - IF "%IS_CONDA%"=="yes" (
+ conda info -a &
+ conda install --yes --quiet pip
+ )
+ - pip install nose ddt 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"
+
+ - pip install -e .
+
+build: false
+
+test_script:
+ - nosetests -v --with-coverage
+
+#on_success:
+# - IF "%PYTHON_VERSION%"=="3.4" (coveralls)
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..872b8eb4
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+git/test/fixtures/* eol=lf
+init-tests-after-clone.sh
diff --git a/.travis.yml b/.travis.yml
index 31f2c00c..0a1b79ff 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
@@ -29,10 +29,11 @@ install:
- cat git/test/fixtures/.gitconfig >> ~/.gitconfig
script:
# Make sure we limit open handles to see if we are leaking them
- - ulimit -n 96
+ - ulimit -n 110
- ulimit -n
- nosetests -v --with-coverage
- - flake8
- - cd doc && make html
+ - if [ "$TRAVIS_PYTHON_VERSION" == '3.4' ]; then flake8; fi
+ - if [ "$TRAVIS_PYTHON_VERSION" == '3.5' ]; then cd doc && make html; fi
+ -
after_success:
- coveralls
diff --git a/README.md b/README.md
index c999dcaa..42000af5 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=<path/to/git>` 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=<path/to/git>` 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
@@ -31,7 +39,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 +53,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
@@ -54,19 +62,37 @@ codebase for `__del__` implementations and call these yourself when you see fit.
Another way assure proper cleanup of resources is to factor out GitPython into a
separate process which can be dropped periodically.
+#### Windows support
+
+For *Windows*, we do regularly test it on [Appveyor CI](https://www.appveyor.com/)
+but not all test-cases pass - you may help improve them by exploring
+[Issue #525](https://github.com/gitpython-developers/GitPython/issues/525).
+
+#### Python 2.6
+
+Python 2.6 is supported on best-effort basis; which means that it is likely to deteriorate over time.
+
### 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
Then run:
tox
-
-
+
+
For more fine-grained control, you can use `nose`.
### Contributions
@@ -79,7 +105,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
@@ -95,7 +122,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.
@@ -103,12 +130,16 @@ New BSD License. See the LICENSE file.
### DEVELOPMENT STATUS
[![Build Status](https://travis-ci.org/gitpython-developers/GitPython.svg)](https://travis-ci.org/gitpython-developers/GitPython)
+[![Build status](https://ci.appveyor.com/api/projects/status/0f3pi3c00hajlrsd/branch/master?svg=true&passingText=windows%20OK&failingText=windows%20failed)](https://ci.appveyor.com/project/Byron/gitpython/branch/master)
[![Code Climate](https://codeclimate.com/github/gitpython-developers/GitPython/badges/gpa.svg)](https://codeclimate.com/github/gitpython-developers/GitPython)
[![Documentation Status](https://readthedocs.org/projects/gitpython/badge/?version=stable)](https://readthedocs.org/projects/gitpython/?badge=stable)
+[![Stories in Ready](https://badge.waffle.io/gitpython-developers/GitPython.png?label=ready&title=Ready)](https://waffle.io/gitpython-developers/GitPython)
+[![Throughput Graph](https://graphs.waffle.io/gitpython-developers/GitPython/throughput.svg)](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
-[contributing]: https://github.com/gitpython-developers/GitPython/blob/master/README.md
+[contributing]: https://github.com/gitpython-developers/GitPython/blob/master/CONTRIBUTING.md
diff --git a/doc/source/conf.py b/doc/source/conf.py
index add686d3..2df3bbb6 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -50,7 +50,8 @@ copyright = u'Copyright (C) 2008, 2009 Michael Trier and contributors, 2010-2015
# built documents.
#
# The short X.Y version.
-VERSION = open(os.path.join(os.path.dirname(__file__),"..", "..", 'VERSION')).readline().strip()
+with open(os.path.join(os.path.dirname(__file__),"..", "..", 'VERSION')) as fd:
+ VERSION = fd.readline().strip()
version = VERSION
# The full version, including alpha/beta/rc tags.
release = VERSION
diff --git a/doc/source/index.rst b/doc/source/index.rst
index 1079c5c7..69fb573a 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -9,7 +9,6 @@ GitPython Documentation
:maxdepth: 2
intro
- whatsnew
tutorial
reference
roadmap
diff --git a/doc/source/intro.rst b/doc/source/intro.rst
index 1c1b0d1b..1766f8ae 100644
--- a/doc/source/intro.rst
+++ b/doc/source/intro.rst
@@ -15,7 +15,7 @@ Requirements
* `Python`_ 2.7 or newer
Since GitPython 2.0.0. Please note that python 2.6 is still reasonably well supported, but might
- deteriorate over time.
+ deteriorate over time. Support is provided on a best-effort basis only.
* `Git`_ 1.7.0 or newer
It should also work with older versions, but it may be that some operations
involving remotes will not work as expected.
@@ -75,6 +75,12 @@ codebase for `__del__` implementations and call these yourself when you see fit.
Another way assure proper cleanup of resources is to factor out GitPython into a
separate process which can be dropped periodically.
+Best-effort for Python 2.6 and Windows support
+----------------------------------------------
+
+This means that support for these platforms is likely to worsen over time
+as they are kept alive solely by their users, or not.
+
Getting Started
===============
diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst
index 92020975..7ac2eeea 100644
--- a/doc/source/tutorial.rst
+++ b/doc/source/tutorial.rst
@@ -28,28 +28,28 @@ In the above example, the directory ``self.rorepo.working_tree_dir`` equals ``/U
:language: python
:start-after: # [2-test_init_repo_object]
:end-before: # ![2-test_init_repo_object]
-
+
A repo object provides high-level access to your data, it allows you to create and delete heads, tags and remotes and access the configuration of the repository.
-
+
.. literalinclude:: ../../git/test/test_docs.py
:language: python
:start-after: # [3-test_init_repo_object]
:end-before: # ![3-test_init_repo_object]
Query the active branch, query untracked files or whether the repository data has been modified.
-
+
.. literalinclude:: ../../git/test/test_docs.py
:language: python
:start-after: # [4-test_init_repo_object]
:end-before: # ![4-test_init_repo_object]
-
+
Clone from existing repositories or initialize new empty ones.
.. literalinclude:: ../../git/test/test_docs.py
:language: python
:start-after: # [5-test_init_repo_object]
:end-before: # ![5-test_init_repo_object]
-
+
Archive the repository contents to a tar file.
.. literalinclude:: ../../git/test/test_docs.py
@@ -62,7 +62,7 @@ Advanced Repo Usage
And of course, there is much more you can do with this type, most of the following will be explained in greater detail in specific tutorials. Don't worry if you don't understand some of these examples right away, as they may require a thorough understanding of gits inner workings.
-Query relevant repository paths ...
+Query relevant repository paths ...
.. literalinclude:: ../../git/test/test_docs.py
:language: python
@@ -83,7 +83,7 @@ You can also create new heads ...
:start-after: # [9-test_init_repo_object]
:end-before: # ![9-test_init_repo_object]
-... and tags ...
+... and tags ...
.. literalinclude:: ../../git/test/test_docs.py
:language: python
@@ -118,7 +118,7 @@ The :class:`index <git.index.base.IndexFile>` is also called stage in git-speak.
:start-after: # [14-test_init_repo_object]
:end-before: # ![14-test_init_repo_object]
-
+
Examining References
********************
@@ -128,28 +128,28 @@ Examining References
:language: python
:start-after: # [1-test_references_and_objects]
:end-before: # ![1-test_references_and_objects]
-
+
:class:`Tags <git.refs.tag.TagReference>` are (usually immutable) references to a commit and/or a tag object.
.. literalinclude:: ../../git/test/test_docs.py
:language: python
:start-after: # [2-test_references_and_objects]
:end-before: # ![2-test_references_and_objects]
-
+
A :class:`symbolic reference <git.refs.symbolic.SymbolicReference>` is a special case of a reference as it points to another reference instead of a commit.
.. literalinclude:: ../../git/test/test_docs.py
:language: python
:start-after: # [3-test_references_and_objects]
:end-before: # ![3-test_references_and_objects]
-
+
Access the :class:`reflog <git.refs.log.RefLog>` easily.
-
+
.. literalinclude:: ../../git/test/test_docs.py
:language: python
:start-after: # [4-test_references_and_objects]
:end-before: # ![4-test_references_and_objects]
-
+
Modifying References
********************
You can easily create and delete :class:`reference types <git.refs.reference.Reference>` or modify where they point to.
@@ -165,7 +165,7 @@ Create or delete :class:`tags <git.refs.tag.TagReference>` the same way except y
:language: python
:start-after: # [6-test_references_and_objects]
:end-before: # ![6-test_references_and_objects]
-
+
Change the :class:`symbolic reference <git.refs.symbolic.SymbolicReference>` to switch branches cheaply (without adjusting the index or the working tree).
.. literalinclude:: ../../git/test/test_docs.py
@@ -185,29 +185,29 @@ In GitPython, all objects can be accessed through their common base, can be comp
:language: python
:start-after: # [8-test_references_and_objects]
:end-before: # ![8-test_references_and_objects]
-
-Common fields are ...
+
+Common fields are ...
.. literalinclude:: ../../git/test/test_docs.py
:language: python
:start-after: # [9-test_references_and_objects]
:end-before: # ![9-test_references_and_objects]
-
+
:class:`Index objects <git.objects.base.IndexObject>` are objects that can be put into git's index. These objects are trees, blobs and submodules which additionally know about their path in the file system as well as their mode.
-
+
.. literalinclude:: ../../git/test/test_docs.py
:language: python
:start-after: # [10-test_references_and_objects]
:end-before: # ![10-test_references_and_objects]
-
+
Access :class:`blob <git.objects.blob.Blob>` data (or any object data) using streams.
-
+
.. literalinclude:: ../../git/test/test_docs.py
:language: python
:start-after: # [11-test_references_and_objects]
:end-before: # ![11-test_references_and_objects]
-
-
+
+
The Commit object
*****************
@@ -218,35 +218,35 @@ Obtain commits at the specified revision
.. literalinclude:: ../../git/test/test_docs.py
:language: python
:start-after: # [12-test_references_and_objects]
- :end-before: # ![12-test_references_and_objects]
+ :end-before: # ![12-test_references_and_objects]
Iterate 50 commits, and if you need paging, you can specify a number of commits to skip.
.. literalinclude:: ../../git/test/test_docs.py
:language: python
:start-after: # [13-test_references_and_objects]
- :end-before: # ![13-test_references_and_objects]
+ :end-before: # ![13-test_references_and_objects]
A commit object carries all sorts of meta-data
.. literalinclude:: ../../git/test/test_docs.py
:language: python
:start-after: # [14-test_references_and_objects]
- :end-before: # ![14-test_references_and_objects]
+ :end-before: # ![14-test_references_and_objects]
Note: date time is represented in a ``seconds since epoch`` format. Conversion to human readable form can be accomplished with the various `time module <http://docs.python.org/library/time.html>`_ methods.
.. literalinclude:: ../../git/test/test_docs.py
:language: python
:start-after: # [15-test_references_and_objects]
- :end-before: # ![15-test_references_and_objects]
+ :end-before: # ![15-test_references_and_objects]
You can traverse a commit's ancestry by chaining calls to ``parents``
.. literalinclude:: ../../git/test/test_docs.py
:language: python
:start-after: # [16-test_references_and_objects]
- :end-before: # ![16-test_references_and_objects]
+ :end-before: # ![16-test_references_and_objects]
The above corresponds to ``master^^^`` or ``master~3`` in git parlance.
@@ -258,62 +258,62 @@ A :class:`tree <git.objects.tree.Tree>` records pointers to the contents of a di
.. literalinclude:: ../../git/test/test_docs.py
:language: python
:start-after: # [17-test_references_and_objects]
- :end-before: # ![17-test_references_and_objects]
+ :end-before: # ![17-test_references_and_objects]
Once you have a tree, you can get its contents
.. literalinclude:: ../../git/test/test_docs.py
:language: python
:start-after: # [18-test_references_and_objects]
- :end-before: # ![18-test_references_and_objects]
+ :end-before: # ![18-test_references_and_objects]
It is useful to know that a tree behaves like a list with the ability to query entries by name
.. literalinclude:: ../../git/test/test_docs.py
:language: python
:start-after: # [19-test_references_and_objects]
- :end-before: # ![19-test_references_and_objects]
+ :end-before: # ![19-test_references_and_objects]
There is a convenience method that allows you to get a named sub-object from a tree with a syntax similar to how paths are written in a posix system
.. literalinclude:: ../../git/test/test_docs.py
:language: python
:start-after: # [20-test_references_and_objects]
- :end-before: # ![20-test_references_and_objects]
+ :end-before: # ![20-test_references_and_objects]
You can also get a commit's root tree directly from the repository
.. literalinclude:: ../../git/test/test_docs.py
:language: python
:start-after: # [21-test_references_and_objects]
- :end-before: # ![21-test_references_and_objects]
-
+ :end-before: # ![21-test_references_and_objects]
+
As trees allow direct access to their intermediate child entries only, use the traverse method to obtain an iterator to retrieve entries recursively
.. literalinclude:: ../../git/test/test_docs.py
:language: python
:start-after: # [22-test_references_and_objects]
- :end-before: # ![22-test_references_and_objects]
-
+ :end-before: # ![22-test_references_and_objects]
+
.. note:: If trees return Submodule objects, they will assume that they exist at the current head's commit. The tree it originated from may be rooted at another commit though, that it doesn't know. That is why the caller would have to set the submodule's owning or parent commit using the ``set_parent_commit(my_commit)`` method.
-
+
The Index Object
****************
The git index is the stage containing changes to be written with the next commit or where merges finally have to take place. You may freely access and manipulate this information using the :class:`IndexFile <git.index.base.IndexFile>` object.
Modify the index with ease
-
+
.. literalinclude:: ../../git/test/test_docs.py
:language: python
:start-after: # [23-test_references_and_objects]
- :end-before: # ![23-test_references_and_objects]
-
+ :end-before: # ![23-test_references_and_objects]
+
Create new indices from other trees or as result of a merge. Write that result to a new index file for later inspection.
.. literalinclude:: ../../git/test/test_docs.py
:language: python
:start-after: # [24-test_references_and_objects]
- :end-before: # ![24-test_references_and_objects]
-
+ :end-before: # ![24-test_references_and_objects]
+
Handling Remotes
****************
@@ -322,10 +322,10 @@ Handling Remotes
.. literalinclude:: ../../git/test/test_docs.py
:language: python
:start-after: # [25-test_references_and_objects]
- :end-before: # ![25-test_references_and_objects]
+ :end-before: # ![25-test_references_and_objects]
You can easily access configuration information for a remote by accessing options as if they where attributes. The modification of remote configuration is more explicit though.
-
+
.. literalinclude:: ../../git/test/test_docs.py
:language: python
:start-after: # [26-test_references_and_objects]
@@ -352,7 +352,7 @@ Here's an example executable that can be used in place of the `ssh_executable` a
Please note that the script must be executable (i.e. `chomd +x script.sh`). `StrictHostKeyChecking=no` is used to avoid prompts asking to save the hosts key to `~/.ssh/known_hosts`, which happens in case you run this as daemon.
You might also have a look at `Git.update_environment(...)` in case you want to setup a changed environment more permanently.
-
+
Submodule Handling
******************
:class:`Submodules <git.objects.submodule.base.Submodule>` can be conveniently handled using the methods provided by GitPython, and as an added benefit, GitPython provides functionality which behave smarter and less error prone than its original c-git implementation, that is GitPython tries hard to keep your repository consistent when updating submodules recursively or adjusting the existing configuration.
@@ -360,15 +360,19 @@ Submodule Handling
.. literalinclude:: ../../git/test/test_docs.py
:language: python
:start-after: # [1-test_submodules]
- :end-before: # ![1-test_submodules]
+ :end-before: # ![1-test_submodules]
-In addition to the query functionality, you can move the submodule's repository to a different path <``move(...)``>, write its configuration <``config_writer().set_value(...).release()``>, update its working tree <``update(...)``>, and remove or add them <``remove(...)``, ``add(...)``>.
+In addition to the query functionality, you can move the submodule's repository to a different path <``move(...)``>,
+write its configuration <``config_writer().set_value(...).release()``>, update its working tree <``update(...)``>,
+and remove or add them <``remove(...)``, ``add(...)``>.
-If you obtained your submodule object by traversing a tree object which is not rooted at the head's commit, you have to inform the submodule about its actual commit to retrieve the data from by using the ``set_parent_commit(...)`` method.
+If you obtained your submodule object by traversing a tree object which is not rooted at the head's commit,
+you have to inform the submodule about its actual commit to retrieve the data from
+by using the ``set_parent_commit(...)`` method.
The special :class:`RootModule <git.objects.submodule.root.RootModule>` type allows you to treat your master repository as root of a hierarchy of submodules, which allows very convenient submodule handling. Its ``update(...)`` method is reimplemented to provide an advanced way of updating submodules as they change their values over time. The update method will track changes and make sure your working tree and submodule checkouts stay consistent, which is very useful in case submodules get deleted or added to name just two of the handled cases.
-Additionally, GitPython adds functionality to track a specific branch, instead of just a commit. Supported by customized update methods, you are able to automatically update submodules to the latest revision available in the remote repository, as well as to keep track of changes and movements of these submodules. To use it, set the name of the branch you want to track to the ``submodule.$name.branch`` option of the *.gitmodules* file, and use GitPython update methods on the resulting repository with the ``to_latest_revision`` parameter turned on. In the latter case, the sha of your submodule will be ignored, instead a local tracking branch will be updated to the respective remote branch automatically, provided there are no local changes. The resulting behaviour is much like the one of svn::externals, which can be useful in times.
+Additionally, GitPython adds functionality to track a specific branch, instead of just a commit. Supported by customized update methods, you are able to automatically update submodules to the latest revision available in the remote repository, as well as to keep track of changes and movements of these submodules. To use it, set the name of the branch you want to track to the ``submodule.$name.branch`` option of the *.gitmodules* file, and use GitPython update methods on the resulting repository with the ``to_latest_revision`` parameter turned on. In the latter case, the sha of your submodule will be ignored, instead a local tracking branch will be updated to the respective remote branch automatically, provided there are no local changes. The resulting behaviour is much like the one of svn::externals, which can be useful in times.
Obtaining Diff Information
**************************
@@ -380,7 +384,7 @@ Diffs can be made between the Index and Trees, Index and the working tree, trees
.. literalinclude:: ../../git/test/test_docs.py
:language: python
:start-after: # [27-test_references_and_objects]
- :end-before: # ![27-test_references_and_objects]
+ :end-before: # ![27-test_references_and_objects]
The item returned is a DiffIndex which is essentially a list of Diff objects. It provides additional filtering to ease finding what you might be looking for.
@@ -392,15 +396,15 @@ The item returned is a DiffIndex which is essentially a list of Diff objects. It
Use the diff framework if you want to implement git-status like functionality.
* A diff between the index and the commit's tree your HEAD points to
-
+
* use ``repo.index.diff(repo.head.commit)``
-
+
* A diff between the index and the working tree
-
+
* use ``repo.index.diff(None)``
-
+
* A list of untracked files
-
+
* use ``repo.untracked_files``
Switching Branches
@@ -411,7 +415,7 @@ To switch between branches similar to ``git checkout``, you effectively need to
:language: python
:start-after: # [29-test_references_and_objects]
:end-before: # ![29-test_references_and_objects]
-
+
The previous approach would brutally overwrite the user's changes in the working copy and index though and is less sophisticated than a ``git-checkout``. The latter will generally prevent you from destroying your work. Use the safer approach as follows.
.. literalinclude:: ../../git/test/test_docs.py
@@ -439,7 +443,7 @@ In case you are missing functionality as it has not been wrapped, you may conven
:language: python
:start-after: # [31-test_references_and_objects]
:end-before: # ![31-test_references_and_objects]
-
+
The return value will by default be a string of the standard output channel produced by the command.
Keyword arguments translate to short and long keyword arguments on the command-line.
@@ -457,14 +461,14 @@ The type of the database determines certain performance characteristics, such as
GitDB
=====
The GitDB is a pure-python implementation of the git object database. It is the default database to use in GitPython 0.3. Its uses less memory when handling huge files, but will be 2 to 5 times slower when extracting large quantities small of objects from densely packed repositories::
-
+
repo = Repo("path/to/repo", odbt=GitDB)
GitCmdObjectDB
==============
The git command database uses persistent git-cat-file instances to read repository information. These operate very fast under all conditions, but will consume additional memory for the process itself. When extracting large files, memory usage will be much higher than the one of the ``GitDB``::
-
+
repo = Repo("path/to/repo", odbt=GitCmdObjectDB)
Git Command Debugging and Customization
@@ -478,10 +482,10 @@ Using environment variables, you can further adjust the behaviour of the git com
* If set to *full*, the executed git command _and_ its entire output on stdout and stderr will be shown as they happen
**NOTE**: All logging is outputted using a Python logger, so make sure your program is configured to show INFO-level messages. If this is not the case, try adding the following to your program::
-
+
import logging
logging.basicConfig(level=logging.INFO)
-
+
* **GIT_PYTHON_GIT_EXECUTABLE**
* If set, it should contain the full path to the git executable, e.g. *c:\\Program Files (x86)\\Git\\bin\\git.exe* on windows or */usr/bin/git* on linux.
diff --git a/doc/source/whatsnew.rst b/doc/source/whatsnew.rst
deleted file mode 100644
index e0d39b09..00000000
--- a/doc/source/whatsnew.rst
+++ /dev/null
@@ -1,25 +0,0 @@
-
-################
-Whats New in 0.3
-################
-GitPython 0.3 is the first step in creating a hybrid which uses a pure python implementations for all simple git features which can be implemented without significant performance penalties. Everything else is still performed using the git command, which is nicely integrated and easy to use.
-
-Its biggest strength, being the support for all git features through the git command itself, is a weakness as well considering the possibly vast amount of times the git command is being started up. Depending on the actual command being performed, the git repository will be initialized on many of these invocations, causing additional overhead for possibly tiny operations.
-
-Keeping as many major operations in the python world will result in improved caching benefits as certain data structures just have to be initialized once and can be reused multiple times. This mode of operation may improve performance when altering the git database on a low level, and is clearly beneficial on operating systems where command invocations are very slow.
-
-****************
-Object Databases
-****************
-An object database provides a simple interface to query object information or to write new object data. Objects are generally identified by their 20 byte binary sha1 value during query.
-
-GitPython uses the ``gitdb`` project to provide a pure-python implementation of the git database, which includes reading and writing loose objects, reading pack files and handling alternate repositories.
-
-The great thing about this is that ``Repo`` objects can use any object database, hence it easily supports different implementations with different performance characteristics. If you are thinking in extremes, you can implement your own database representation, which may be more efficient for what you want to do specifically, like handling big files more efficiently.
-
-************************
-Reduced Memory Footprint
-************************
-Objects, such as commits, tags, trees and blobs now use 20 byte sha1 signatures internally, reducing their memory demands by 20 bytes per object, allowing you to keep more objects in memory at the same time.
-
-The internal caches of tree objects were improved to use less memory as well.
diff --git a/git/__init__.py b/git/__init__.py
index e8dae272..58e4e7b6 100644
--- a/git/__init__.py
+++ b/git/__init__.py
@@ -4,7 +4,7 @@
# This module is part of GitPython and is released under
# the BSD License: http://www.opensource.org/licenses/bsd-license.php
# flake8: noqa
-
+#@PydevCodeAnalysisIgnore
import os
import sys
import inspect
@@ -32,17 +32,17 @@ _init_externals()
#{ Imports
-from git.config import GitConfigParser
-from git.objects import *
-from git.refs import *
-from git.diff import *
-from git.exc import *
-from git.db import *
-from git.cmd import Git
-from git.repo import Repo
-from git.remote import *
-from git.index import *
-from git.util import (
+from git.config import GitConfigParser # @NoMove @IgnorePep8
+from git.objects import * # @NoMove @IgnorePep8
+from git.refs import * # @NoMove @IgnorePep8
+from git.diff import * # @NoMove @IgnorePep8
+from git.exc import * # @NoMove @IgnorePep8
+from git.db import * # @NoMove @IgnorePep8
+from git.cmd import Git # @NoMove @IgnorePep8
+from git.repo import Repo # @NoMove @IgnorePep8
+from git.remote import * # @NoMove @IgnorePep8
+from git.index import * # @NoMove @IgnorePep8
+from git.util import ( # @NoMove @IgnorePep8
LockFile,
BlockingLockFile,
Stats,
diff --git a/git/cmd.py b/git/cmd.py
index ceea2442..88d62aa4 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -4,64 +4,53 @@
# This module is part of GitPython and is released under
# the BSD License: http://www.opensource.org/licenses/bsd-license.php
-import os
-import os.path
-import sys
-import select
-import logging
-import threading
-import errno
-import mmap
-
-from git.odict import OrderedDict
from contextlib import contextmanager
+import io
+import logging
+import os
import signal
from subprocess import (
call,
Popen,
PIPE
)
+import subprocess
+import sys
+import threading
-
-from .util import (
- LazyMixin,
- stream_copy,
- WaitGroup
-)
-from .exc import (
- GitCommandError,
- GitCommandNotFound
-)
from git.compat import (
string_types,
defenc,
force_bytes,
PY3,
- bchr,
# just to satisfy flake8 on py3
unicode,
safe_decode,
+ is_posix,
+ is_win,
)
+from git.exc import CommandError
+from git.odict import OrderedDict
-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')
+from .exc import (
+ GitCommandError,
+ GitCommandNotFound
+)
+from .util import (
+ LazyMixin,
+ stream_copy,
+)
-log = logging.getLogger('git.cmd')
-log.addHandler(logging.NullHandler())
-__all__ = ('Git', )
+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', 'shell'))
-if sys.platform != 'win32':
- WindowsError = OSError
+log = logging.getLogger('git.cmd')
+log.addHandler(logging.NullHandler())
-if PY3:
- _bchr = bchr
-else:
- def _bchr(c):
- return c
-# get custom byte character handling
+__all__ = ('Git',)
# ==============================================================================
@@ -70,154 +59,63 @@ else:
# Documentation
## @{
-def handle_process_output(process, stdout_handler, stderr_handler, finalizer):
+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.
+ the respective line handlers.
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"""
- fdmap = {process.stdout.fileno(): (stdout_handler, [b'']),
- process.stderr.fileno(): (stderr_handler, [b''])}
-
- def _parse_lines_from_buffer(buf):
- line = b''
- bi = 0
- lb = len(buf)
- while bi < lb:
- char = _bchr(buf[bi])
- bi += 1
-
- if char in (b'\r', b'\n') and line:
- yield bi, line
- line = b''
- else:
- line += char
- # END process parsed line
- # END while file is not done reading
- # end
-
- def _read_lines_from_fno(fno, last_buf_list):
- buf = os.read(fno, mmap.PAGESIZE)
- buf = last_buf_list[0] + buf
-
- bi = 0
- for bi, line in _parse_lines_from_buffer(buf):
- yield line
- # for each line to parse from the buffer
-
- # keep remainder
- last_buf_list[0] = buf[bi:]
-
- def _dispatch_single_line(line, handler):
- line = line.decode(defenc)
- if line and handler:
- handler(line)
- # end dispatch helper
- # end single line helper
-
- def _dispatch_lines(fno, handler, buf_list):
- lc = 0
- for line in _read_lines_from_fno(fno, buf_list):
- _dispatch_single_line(line, handler)
- lc += 1
- # for each line
- return lc
- # end
-
- def _deplete_buffer(fno, handler, buf_list, wg=None):
- lc = 0
- while True:
- line_count = _dispatch_lines(fno, handler, buf_list)
- lc += line_count
- if line_count == 0:
- break
- # end deplete buffer
-
- if buf_list[0]:
- _dispatch_single_line(buf_list[0], handler)
- 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
- # The only reliable way to do this now is to use threads and wait for both to finish
- # 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():
- wg.add(1)
- t = threading.Thread(target=lambda: _deplete_buffer(fno, handler, buf_list, wg))
- 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
- wg.wait()
- # end
+ :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).
+ """
+ # Use 2 "pupm" threads and wait for both to finish.
+ 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 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)
+
+ for t in threads:
+ t.join()
return finalizer(process)
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():
@@ -227,6 +125,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 is_win
+ else 0)
+
class Git(LazyMixin):
@@ -246,35 +153,31 @@ 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
+ 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)
- # 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)
# If True, a shell will be used when executing git commands.
- # This should only be desirable on windows, see https://github.com/gitpython-developers/GitPython/pull/126
- # for more information
+ # This should only be desirable on Windows, see https://github.com/gitpython-developers/GitPython/pull/126
+ # and check `git/test_repo.py:TestRepo.test_untracked_files()` TC for an example where it is required.
# Override this value using `Git.USE_SHELL = True`
USE_SHELL = False
@@ -315,9 +218,10 @@ class Git(LazyMixin):
# try to kill it
try:
- os.kill(proc.pid, 2) # interrupt signal
+ 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
@@ -339,7 +243,7 @@ class Git(LazyMixin):
if stderr is None:
stderr = b''
stderr = force_bytes(stderr)
-
+
status = self.proc.wait()
def read_all_from_possibly_closed_stream(stream):
@@ -447,6 +351,7 @@ class Git(LazyMixin):
line = self.readline()
if not line:
raise StopIteration
+
return line
def __del__(self):
@@ -517,6 +422,7 @@ class Git(LazyMixin):
kill_after_timeout=None,
with_stdout=True,
universal_newlines=False,
+ shell=None,
**subprocess_kwargs
):
"""Handles executing the command on the shell and consumes and returns
@@ -574,6 +480,9 @@ class Git(LazyMixin):
:param universal_newlines:
if True, pipes will be opened as text, and lines are split at
all known line endings.
+ :param shell:
+ Whether to invoke commands through a shell (see `Popen(..., shell=True)`).
+ It overrides :attr:`USE_SHELL` if it is not `None`.
:param kill_after_timeout:
To specify a timeout in seconds for the git command, after which the process
should be killed. This will have no effect if as_process is set to True. It is
@@ -619,18 +528,19 @@ class Git(LazyMixin):
env["LC_ALL"] = "C"
env.update(self._environment)
- if sys.platform == 'win32':
- cmd_not_found_exception = WindowsError
+ if is_win:
+ 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
- creationflags = self.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
+ log.debug("Popen(%s, cwd=%s, universal_newlines=%s, shell=%s)",
+ command, cwd, universal_newlines, shell)
try:
proc = Popen(command,
env=env,
@@ -639,21 +549,22 @@ class Git(LazyMixin):
stdin=istream,
stderr=PIPE,
stdout=PIPE if with_stdout else open(os.devnull, 'wb'),
- shell=self.USE_SHELL,
- close_fds=(os.name == 'posix'), # unsupported on windows
+ shell=shell is not None and shell or self.USE_SHELL,
+ close_fds=(is_posix), # unsupported on windows
universal_newlines=universal_newlines,
- creationflags=creationflags,
+ creationflags=PROC_CREATIONFLAGS,
**subprocess_kwargs
)
except cmd_not_found_exception as err:
- raise GitCommandNotFound(str(err))
+ raise GitCommandNotFound(command, err)
if as_process:
return self.AutoInterrupt(proc, 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:
@@ -679,7 +590,7 @@ class Git(LazyMixin):
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
@@ -766,10 +677,7 @@ class Git(LazyMixin):
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:
@@ -885,12 +793,8 @@ class Git(LazyMixin):
: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)
@@ -910,48 +814,17 @@ class Git(LazyMixin):
args = ext_args[:index + 1] + opt_args + ext_args[index + 1:]
# end handle kwargs
- def make_call():
- 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 = [self.GIT_PYTHON_GIT_EXECUTABLE]
- 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 sys.platform == 'win32':
- 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):
"""
@@ -1040,6 +913,10 @@ class Git(LazyMixin):
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/compat.py b/git/compat.py
index b3572474..e7243e25 100644
--- a/git/compat.py
+++ b/git/compat.py
@@ -7,37 +7,47 @@
"""utilities to help provide compatibility with python 3"""
# flake8: noqa
+import locale
+import os
import sys
from gitdb.utils.compat import (
- PY3,
xrange,
- MAXSIZE,
- izip,
+ MAXSIZE, # @UnusedImport
+ izip, # @UnusedImport
)
-
from gitdb.utils.encoding import (
- string_types,
- text_type,
- force_bytes,
- force_text
+ string_types, # @UnusedImport
+ text_type, # @UnusedImport
+ force_bytes, # @UnusedImport
+ force_text # @UnusedImport
)
+
+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:
import io
FileType = io.IOBase
+
def byte_ord(b):
return b
+
def bchr(n):
return bytes([n])
+
def mviter(d):
return d.values()
- range = xrange
+
+ range = xrange # @ReservedAssignment
unicode = str
binary_type = bytes
else:
- FileType = file
+ FileType = file # @UndefinedVariable on PY3
# usually, this is just ascii, which might not enough for our encoding needs
# Unless it's set specifically, we override it to be utf-8
if defenc == 'ascii':
@@ -46,7 +56,8 @@ else:
bchr = chr
unicode = unicode
binary_type = str
- range = xrange
+ range = xrange # @ReservedAssignment
+
def mviter(d):
return d.itervalues()
@@ -57,7 +68,28 @@ 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 safe_encode(s):
+ """Safely decodes a binary string to unicode"""
+ if isinstance(s, unicode):
+ return s.encode(defenc)
+ elif isinstance(s, bytes):
+ return s
+ elif s is not None:
+ raise TypeError('Expected bytes or text, but got %r' % (s,))
+
+
+def win_encode(s):
+ """Encode unicodes for process arguments on Windows."""
+ if isinstance(s, unicode):
+ return s.encode(locale.getpreferredencoding(False))
+ elif isinstance(s, bytes):
+ return s
+ elif s is not None:
+ raise TypeError('Expected bytes or text, but got %r' % (s,))
def with_metaclass(meta, *bases):
@@ -73,9 +105,19 @@ def with_metaclass(meta, *bases):
# 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
+
+
+## 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 PY3:
+ def __str__(self):
+ return self.__unicode__()
+ else: # Python 2
+ def __str__(self):
+ return self.__unicode__().encode(defenc)
diff --git a/git/config.py b/git/config.py
index 5bd10975..eddfac15 100644
--- a/git/config.py
+++ b/git/config.py
@@ -17,6 +17,8 @@ import logging
import abc
import os
+from functools import wraps
+
from git.odict import OrderedDict
from git.util import LockFile
from git.compat import (
@@ -38,7 +40,7 @@ log.addHandler(logging.NullHandler())
class MetaParserBuilder(abc.ABCMeta):
"""Utlity class wrapping base-class methods into decorators that assure read-only properties"""
- def __new__(metacls, name, bases, clsdict):
+ def __new__(cls, name, bases, clsdict):
"""
Equip all base-class methods with a needs_values decorator, and all non-const methods
with a set_dirty_and_flush_changes decorator in addition to that."""
@@ -60,18 +62,18 @@ class MetaParserBuilder(abc.ABCMeta):
# END for each base
# END if mutating methods configuration is set
- new_type = super(MetaParserBuilder, metacls).__new__(metacls, name, bases, clsdict)
+ new_type = super(MetaParserBuilder, cls).__new__(cls, name, bases, clsdict)
return new_type
def needs_values(func):
"""Returns method assuring we read values (on demand) before we try to access them"""
+ @wraps(func)
def assure_data_present(self, *args, **kwargs):
self.read()
return func(self, *args, **kwargs)
# END wrapper method
- assure_data_present.__name__ = func.__name__
return assure_data_present
@@ -388,23 +390,18 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
while files_to_read:
file_path = files_to_read.pop(0)
fp = file_path
- close_fp = False
+ file_ok = False
- # assume a path if it is not a file-object
- if not hasattr(fp, "seek"):
+ if hasattr(fp, "seek"):
+ self._read(fp, fp.name)
+ else:
+ # assume a path if it is not a file-object
try:
- fp = open(file_path, 'rb')
- close_fp = True
+ with open(file_path, 'rb') as fp:
+ file_ok = True
+ self._read(fp, fp.name)
except IOError:
continue
- # END fp handling
-
- try:
- self._read(fp, fp.name)
- finally:
- if close_fp:
- fp.close()
- # END read-handling
# Read includes and append those that we didn't handle yet
# We expect all paths to be normalized and absolute (and will assure that is the case)
@@ -413,7 +410,7 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
if include_path.startswith('~'):
include_path = os.path.expanduser(include_path)
if not os.path.isabs(include_path):
- if not close_fp:
+ if not file_ok:
continue
# end ignore relative paths if we don't know the configuration file path
assert os.path.isabs(file_path), "Need absolute paths to be sure our cycle checks will work"
@@ -477,34 +474,20 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
# end
fp = self._file_or_files
- close_fp = False
# we have a physical file on disk, so get a lock
- if isinstance(fp, string_types + (FileType, )):
+ is_file_lock = isinstance(fp, string_types + (FileType, ))
+ if is_file_lock:
self._lock._obtain_lock()
- # END get lock for physical files
-
if not hasattr(fp, "seek"):
- fp = open(self._file_or_files, "wb")
- close_fp = True
+ with open(self._file_or_files, "wb") as fp:
+ self._write(fp)
else:
fp.seek(0)
# make sure we do not overwrite into an existing file
if hasattr(fp, 'truncate'):
fp.truncate()
- # END
- # END handle stream or file
-
- # WRITE DATA
- try:
self._write(fp)
- finally:
- if close_fp:
- fp.close()
- # END data writing
-
- # we do not release the lock - it will be done automatically once the
- # instance vanishes
def _assure_writable(self, method_name):
if self.read_only:
diff --git a/git/db.py b/git/db.py
index c4e19858..39b9872a 100644
--- a/git/db.py
+++ b/git/db.py
@@ -7,7 +7,7 @@ from gitdb.util import (
bin_to_hex,
hex_to_bin
)
-from gitdb.db import GitDB
+from gitdb.db import GitDB # @UnusedImport
from gitdb.db import LooseObjectDB
from .exc import (
@@ -54,7 +54,7 @@ class GitCmdObjectDB(LooseObjectDB):
:note: currently we only raise BadObject as git does not communicate
AmbiguousObjects separately"""
try:
- hexsha, typename, size = self._git.get_object_header(partial_hexsha)
+ hexsha, typename, size = self._git.get_object_header(partial_hexsha) # @UnusedVariable
return hex_to_bin(hexsha)
except (GitCommandError, ValueError):
raise BadObject(partial_hexsha)
diff --git a/git/diff.py b/git/diff.py
index fb8faaf6..35c7ff86 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -15,6 +15,8 @@ from git.compat import (
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 @@ class Diffable(object):
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 @@ class Diff(object):
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_streams=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 @@ class Diff(object):
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 @@ class Diff(object):
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_streams=False)
return index
diff --git a/git/exc.py b/git/exc.py
index 34382ecd..eb7c3c0e 100644
--- a/git/exc.py
+++ b/git/exc.py
@@ -5,9 +5,8 @@
# the BSD License: http://www.opensource.org/licenses/bsd-license.php
""" Module containing all exceptions thrown througout the git package, """
-from gitdb.exc import * # NOQA
-
-from git.compat import defenc
+from gitdb.exc import * # NOQA @UnusedWildImport
+from git.compat import UnicodeMixin, safe_decode, string_types
class InvalidGitRepositoryError(Exception):
@@ -22,29 +21,57 @@ 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 `<cmdline>` 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):
+ if not isinstance(command, (tuple, list)):
+ command = command.split()
+ 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(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 __str__(self):
- ret = "'%s' returned with exit code %i" % \
- (' '.join(str(i) for i in self.command), self.status)
- if self.stderr:
- ret += "\nstderr: '%s'" % self.stderr.decode(defenc)
- if self.stdout:
- ret += "\nstdout: '%s'" % self.stdout.decode(defenc)
- return ret
+ super(GitCommandError, self).__init__(command, status, stderr, stdout)
class CheckoutError(Exception):
@@ -81,19 +108,13 @@ class UnmergedEntriesError(CacheError):
entries in the cache"""
-class HookExecutionError(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, stderr):
- 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 __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 524b4568..ac2d3019 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -46,7 +46,8 @@ from git.compat import (
string_types,
force_bytes,
defenc,
- mviter
+ mviter,
+ is_win
)
from git.util import (
@@ -118,13 +119,17 @@ class IndexFile(LazyMixin, diff.Diffable, Serializable):
# read the current index
# try memory map for speed
lfd = LockedFD(self._file_path)
+ ok = False
try:
fd = lfd.open(write=False, stream=False)
+ ok = True
except OSError:
- lfd.rollback()
# in new repositories, there may be no index, which means we are empty
self.entries = dict()
return
+ finally:
+ if not ok:
+ lfd.rollback()
# END exception handling
# Here it comes: on windows in python 2.5, memory maps aren't closed properly
@@ -132,7 +137,7 @@ class IndexFile(LazyMixin, diff.Diffable, Serializable):
# 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:
@@ -165,7 +170,7 @@ class IndexFile(LazyMixin, diff.Diffable, Serializable):
def _deserialize(self, stream):
"""Initialize this instance with index values read from the given stream"""
- self.version, self.entries, self._extension_data, conten_sha = read_cache(stream)
+ self.version, self.entries, self._extension_data, conten_sha = read_cache(stream) # @UnusedVariable
return self
def _entries_sorted(self):
@@ -210,7 +215,13 @@ class IndexFile(LazyMixin, diff.Diffable, Serializable):
lfd = LockedFD(file_path or self._file_path)
stream = lfd.open(write=True, stream=True)
- self._serialize(stream, ignore_extension_data)
+ ok = False
+ try:
+ self._serialize(stream, ignore_extension_data)
+ ok = True
+ finally:
+ if not ok:
+ lfd.rollback()
lfd.commit()
@@ -393,7 +404,7 @@ class IndexFile(LazyMixin, diff.Diffable, Serializable):
continue
# END glob handling
try:
- for root, dirs, files in os.walk(abs_path, onerror=raise_exc):
+ for root, dirs, files in os.walk(abs_path, onerror=raise_exc): # @UnusedVariable
for rela_file in files:
# add relative paths only
yield os.path.join(root.replace(rs, ''), rela_file)
@@ -588,17 +599,15 @@ class IndexFile(LazyMixin, diff.Diffable, Serializable):
"""Store file at filepath in the database and return the base index entry
Needs the git_working_dir decorator active ! This must be assured in the calling code"""
st = os.lstat(filepath) # handles non-symlinks as well
- stream = None
if S_ISLNK(st.st_mode):
# in PY3, readlink is string, but we need bytes. In PY2, it's just OS encoded bytes, we assume UTF-8
- stream = BytesIO(force_bytes(os.readlink(filepath), encoding=defenc))
+ open_stream = lambda: BytesIO(force_bytes(os.readlink(filepath), encoding=defenc))
else:
- stream = open(filepath, 'rb')
- # END handle stream
- fprogress(filepath, False, filepath)
- istream = self.repo.odb.store(IStream(Blob.type, st.st_size, stream))
- fprogress(filepath, True, filepath)
- stream.close()
+ open_stream = lambda: open(filepath, 'rb')
+ with open_stream() as stream:
+ fprogress(filepath, False, filepath)
+ istream = self.repo.odb.store(IStream(Blob.type, st.st_size, stream))
+ fprogress(filepath, True, filepath)
return BaseIndexEntry((stat_mode_to_index_mode(st.st_mode),
istream.binsha, 0, to_native_path_linux(filepath)))
@@ -1049,7 +1058,7 @@ class IndexFile(LazyMixin, diff.Diffable, Serializable):
# 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(
@@ -1080,6 +1089,7 @@ class IndexFile(LazyMixin, diff.Diffable, Serializable):
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()
@@ -1091,11 +1101,11 @@ class IndexFile(LazyMixin, diff.Diffable, Serializable):
try:
self.entries[(co_path, 0)]
except KeyError:
- dir = co_path
- if not dir.endswith('/'):
- dir += '/'
+ folder = co_path
+ if not folder.endswith('/'):
+ folder += '/'
for entry in mviter(self.entries):
- if entry.path.startswith(dir):
+ if entry.path.startswith(folder):
p = entry.path
self._write_path_to_stdin(proc, p, p, make_exc,
fprogress, read_from_stdout=False)
diff --git a/git/index/fun.py b/git/index/fun.py
index 4dd32b19..7a7593fe 100644
--- a/git/index/fun.py
+++ b/git/index/fun.py
@@ -14,7 +14,8 @@ from io import BytesIO
import os
import subprocess
-from git.util import IndexFileSHA1Writer
+from git.util import IndexFileSHA1Writer, finalize_process
+from git.cmd import PROC_CREATIONFLAGS, handle_process_output
from git.exc import (
UnmergedEntriesError,
HookExecutionError
@@ -40,9 +41,13 @@ from .util import (
from gitdb.base import IStream
from gitdb.typ import str_tree_type
from git.compat import (
+ PY3,
defenc,
force_text,
- force_bytes
+ force_bytes,
+ is_posix,
+ safe_encode,
+ safe_decode,
)
S_IFGITLINK = S_IFLNK | S_IFDIR # a submodule
@@ -67,22 +72,28 @@ def run_commit_hook(name, index):
return
env = os.environ.copy()
- env['GIT_INDEX_FILE'] = index.path
+ env['GIT_INDEX_FILE'] = safe_decode(index.path) if PY3 else safe_encode(index.path)
env['GIT_EDITOR'] = ':'
- cmd = subprocess.Popen(hp,
- env=env,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- cwd=index.repo.working_dir,
- close_fds=(os.name == 'posix'))
- 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
@@ -253,7 +264,7 @@ def write_tree_from_cache(entries, odb, sl, si=0):
# enter recursion
# ci - 1 as we want to count our current item as well
- sha, tree_entry_list = write_tree_from_cache(entries, odb, slice(ci - 1, xi), rbound + 1)
+ sha, tree_entry_list = write_tree_from_cache(entries, odb, slice(ci - 1, xi), rbound + 1) # @UnusedVariable
tree_items_append((sha, S_IFDIR, base))
# skip ahead
diff --git a/git/index/util.py b/git/index/util.py
index 171bd8fc..ce798851 100644
--- a/git/index/util.py
+++ b/git/index/util.py
@@ -3,6 +3,10 @@ import struct
import tempfile
import os
+from functools import wraps
+
+from git.compat import is_win
+
__all__ = ('TemporaryFileSwap', 'post_clear_cache', 'default_index', 'git_working_dir')
#{ Aliases
@@ -29,7 +33,7 @@ class TemporaryFileSwap(object):
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
@@ -47,13 +51,13 @@ def post_clear_cache(func):
natively which in fact is possible, but probably not feasible performance wise.
"""
+ @wraps(func)
def post_clear_cache_if_not_raised(self, *args, **kwargs):
rval = func(self, *args, **kwargs)
self._delete_entries_cache()
return rval
-
# END wrapper method
- post_clear_cache_if_not_raised.__name__ = func.__name__
+
return post_clear_cache_if_not_raised
@@ -62,6 +66,7 @@ def default_index(func):
repository index. This is as we rely on git commands that operate
on that index only. """
+ @wraps(func)
def check_default_index(self, *args, **kwargs):
if self._file_path != self._index_path():
raise AssertionError(
@@ -69,7 +74,6 @@ def default_index(func):
return func(self, *args, **kwargs)
# END wrpaper method
- check_default_index.__name__ = func.__name__
return check_default_index
@@ -77,6 +81,7 @@ def git_working_dir(func):
"""Decorator which changes the current working dir to the one of the git
repository in order to assure relative paths are handled correctly"""
+ @wraps(func)
def set_git_working_dir(self, *args, **kwargs):
cur_wd = os.getcwd()
os.chdir(self.repo.working_tree_dir)
@@ -87,7 +92,6 @@ def git_working_dir(func):
# END handle working dir
# END wrapper
- set_git_working_dir.__name__ = func.__name__
return set_git_working_dir
#} END decorators
diff --git a/git/objects/__init__.py b/git/objects/__init__.py
index ee642876..23b2416a 100644
--- a/git/objects/__init__.py
+++ b/git/objects/__init__.py
@@ -3,22 +3,24 @@ Import all submodules main classes into the package space
"""
# flake8: noqa
from __future__ import absolute_import
+
import inspect
+
from .base import *
+from .blob import *
+from .commit import *
+from .submodule import util as smutil
+from .submodule.base import *
+from .submodule.root import *
+from .tag import *
+from .tree import *
# Fix import dependency - add IndexObject to the util module, so that it can be
# imported by the submodule.base
-from .submodule import util as smutil
smutil.IndexObject = IndexObject
smutil.Object = Object
del(smutil)
-from .submodule.base import *
-from .submodule.root import *
# must come after submodule was made available
-from .tag import *
-from .blob import *
-from .commit import *
-from .tree import *
__all__ = [name for name, obj in locals().items()
if not (name.startswith('_') or inspect.ismodule(obj))]
diff --git a/git/objects/base.py b/git/objects/base.py
index 77d0ed63..0b849960 100644
--- a/git/objects/base.py
+++ b/git/objects/base.py
@@ -40,7 +40,7 @@ class Object(LazyMixin):
assert len(binsha) == 20, "Require 20 byte binary sha, got %r, len = %i" % (binsha, len(binsha))
@classmethod
- def new(cls, repo, id):
+ def new(cls, repo, id): # @ReservedAssignment
"""
:return: New Object instance of a type appropriate to the object type behind
id. The id of the newly created object will be a binsha even though
diff --git a/git/objects/commit.py b/git/objects/commit.py
index 000ab3d0..1534c552 100644
--- a/git/objects/commit.py
+++ b/git/objects/commit.py
@@ -140,7 +140,7 @@ class Commit(base.Object, Iterable, Diffable, Traversable, Serializable):
def _set_cache_(self, attr):
if attr in Commit.__slots__:
# read the data in a chunk, its faster - then provide a file wrapper
- binsha, typename, self.size, stream = self.repo.odb.stream(self.binsha)
+ binsha, typename, self.size, stream = self.repo.odb.stream(self.binsha) # @UnusedVariable
self._deserialize(BytesIO(stream.read()))
else:
super(Commit, self)._set_cache_(attr)
@@ -267,7 +267,7 @@ class Commit(base.Object, Iterable, Diffable, Traversable, Serializable):
hexsha = line.strip()
if len(hexsha) > 40:
# split additional information, as returned by bisect for instance
- hexsha, rest = line.split(None, 1)
+ hexsha, _ = line.split(None, 1)
# END handle extra info
assert len(hexsha) == 40, "Invalid line: %s" % hexsha
diff --git a/git/objects/fun.py b/git/objects/fun.py
index c04f80b5..5c0f4819 100644
--- a/git/objects/fun.py
+++ b/git/objects/fun.py
@@ -157,9 +157,9 @@ def traverse_trees_recursive(odb, tree_shas, path_prefix):
if not item:
continue
# END skip already done items
- entries = [None for n in range(nt)]
+ entries = [None for _ in range(nt)]
entries[ti] = item
- sha, mode, name = item # its faster to unpack
+ sha, mode, name = item # its faster to unpack @UnusedVariable
is_dir = S_ISDIR(mode) # type mode bits
# find this item in all other tree data items
diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index eea091f8..28802b35 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -1,4 +1,3 @@
-from . import util
from .util import (
mkhead,
sm_name,
@@ -29,7 +28,8 @@ from git.exc import (
)
from git.compat import (
string_types,
- defenc
+ defenc,
+ is_win,
)
import stat
@@ -38,6 +38,9 @@ import git
import os
import logging
import uuid
+from unittest.case import SkipTest
+from git.test.lib.helper import HIDE_WINDOWS_KNOWN_ERRORS
+from git.objects.base import IndexObject, Object
__all__ = ["Submodule", "UpdateProgress"]
@@ -66,7 +69,7 @@ UPDWKTREE = UpdateProgress.UPDWKTREE
# IndexObject comes via util module, its a 'hacky' fix thanks to pythons import
# mechanism which cause plenty of trouble of the only reason for packages and
# modules is refactoring - subpackages shoudn't depend on parent packages
-class Submodule(util.IndexObject, Iterable, Traversable):
+class Submodule(IndexObject, Iterable, Traversable):
"""Implements access to a git submodule. They are special in that their sha
represents a commit in the submodule's repository which is to be checked out
@@ -289,14 +292,16 @@ class Submodule(util.IndexObject, Iterable, Traversable):
"""
git_file = os.path.join(working_tree_dir, '.git')
rela_path = os.path.relpath(module_abspath, start=working_tree_dir)
- fp = open(git_file, 'wb')
- fp.write(("gitdir: %s" % rela_path).encode(defenc))
- fp.close()
+ if is_win:
+ if os.path.isfile(git_file):
+ os.remove(git_file)
+ with open(git_file, 'wb') as fp:
+ fp.write(("gitdir: %s" % rela_path).encode(defenc))
- writer = GitConfigParser(os.path.join(module_abspath, 'config'), read_only=False, merge_includes=False)
- writer.set_value('core', 'worktree',
- to_native_path_linux(os.path.relpath(working_tree_dir, start=module_abspath)))
- writer.release()
+ with GitConfigParser(os.path.join(module_abspath, 'config'),
+ read_only=False, merge_includes=False) as writer:
+ writer.set_value('core', 'worktree',
+ to_native_path_linux(os.path.relpath(working_tree_dir, start=module_abspath)))
#{ Edit Interface
@@ -393,24 +398,20 @@ class Submodule(util.IndexObject, Iterable, Traversable):
# otherwise there is a '-' character in front of the submodule listing
# a38efa84daef914e4de58d1905a500d8d14aaf45 mymodule (v0.9.0-1-ga38efa8)
# -a38efa84daef914e4de58d1905a500d8d14aaf45 submodules/intermediate/one
- writer = sm.repo.config_writer()
- writer.set_value(sm_section(name), 'url', url)
- writer.release()
+ with sm.repo.config_writer() as writer:
+ writer.set_value(sm_section(name), 'url', url)
# update configuration and index
index = sm.repo.index
- writer = sm.config_writer(index=index, write=False)
- writer.set_value('url', url)
- writer.set_value('path', path)
-
- sm._url = url
- if not branch_is_default:
- # store full path
- writer.set_value(cls.k_head_option, br.path)
- sm._branch_path = br.path
- # END handle path
- writer.release()
- del(writer)
+ with sm.config_writer(index=index, write=False) as writer:
+ writer.set_value('url', url)
+ writer.set_value('path', path)
+
+ sm._url = url
+ if not branch_is_default:
+ # store full path
+ writer.set_value(cls.k_head_option, br.path)
+ sm._branch_path = br.path
# we deliberatly assume that our head matches our index !
sm.binsha = mrepo.head.commit.binsha
@@ -523,7 +524,7 @@ class Submodule(util.IndexObject, Iterable, Traversable):
# have a valid branch, but no checkout - make sure we can figure
# that out by marking the commit with a null_sha
- local_branch.set_object(util.Object(mrepo, self.NULL_BIN_SHA))
+ local_branch.set_object(Object(mrepo, self.NULL_BIN_SHA))
# END initial checkout + branch creation
# make sure HEAD is not detached
@@ -537,9 +538,8 @@ class Submodule(util.IndexObject, Iterable, Traversable):
# the default implementation will be offended and not update the repository
# Maybe this is a good way to assure it doesn't get into our way, but
# we want to stay backwards compatible too ... . Its so redundant !
- writer = self.repo.config_writer()
- writer.set_value(sm_section(self.name), 'url', self.url)
- writer.release()
+ with self.repo.config_writer() as writer:
+ writer.set_value(sm_section(self.name), 'url', self.url)
# END handle dry_run
# END handle initalization
@@ -726,11 +726,9 @@ class Submodule(util.IndexObject, Iterable, Traversable):
# END handle submodule doesn't exist
# update configuration
- writer = self.config_writer(index=index) # auto-write
- writer.set_value('path', module_checkout_path)
- self.path = module_checkout_path
- writer.release()
- del(writer)
+ with self.config_writer(index=index) as writer: # auto-write
+ writer.set_value('path', module_checkout_path)
+ self.path = module_checkout_path
# END handle configuration flag
except Exception:
if renamed_module:
@@ -833,7 +831,7 @@ class Submodule(util.IndexObject, Iterable, Traversable):
num_branches_with_new_commits += len(mod.git.cherry(rref)) != 0
# END for each remote ref
# not a single remote branch contained all our commits
- if num_branches_with_new_commits == len(rrefs):
+ if len(rrefs) and num_branches_with_new_commits == len(rrefs):
raise InvalidGitRepositoryError(
"Cannot delete module at %s as there are new commits" % mod.working_tree_dir)
# END handle new commits
@@ -848,14 +846,30 @@ class Submodule(util.IndexObject, Iterable, Traversable):
# finally delete our own submodule
if not dry_run:
+ self._clear_cache()
wtd = mod.working_tree_dir
del(mod) # release file-handles (windows)
- rmtree(wtd)
+ import gc
+ gc.collect()
+ try:
+ rmtree(wtd)
+ except Exception as ex:
+ if HIDE_WINDOWS_KNOWN_ERRORS:
+ raise SkipTest("FIXME: fails with: PermissionError\n %s", ex)
+ else:
+ raise
# END delete tree if possible
# END handle force
if not dry_run and os.path.isdir(git_dir):
- rmtree(git_dir)
+ self._clear_cache()
+ try:
+ rmtree(git_dir)
+ except Exception as ex:
+ if HIDE_WINDOWS_KNOWN_ERRORS:
+ raise SkipTest("FIXME: fails with: PermissionError\n %s", ex)
+ else:
+ raise
# end handle separate bare repository
# END handle module deletion
@@ -877,13 +891,11 @@ class Submodule(util.IndexObject, Iterable, Traversable):
# now git config - need the config intact, otherwise we can't query
# information anymore
- writer = self.repo.config_writer()
- writer.remove_section(sm_section(self.name))
- writer.release()
+ with self.repo.config_writer() as writer:
+ writer.remove_section(sm_section(self.name))
- writer = self.config_writer()
- writer.remove_section()
- writer.release()
+ with self.config_writer() as writer:
+ writer.remove_section()
# END delete configuration
return self
@@ -974,18 +986,15 @@ class Submodule(util.IndexObject, Iterable, Traversable):
return self
# .git/config
- pw = self.repo.config_writer()
- # As we ourselves didn't write anything about submodules into the parent .git/config, we will not require
- # it to exist, and just ignore missing entries
- if pw.has_section(sm_section(self.name)):
- pw.rename_section(sm_section(self.name), sm_section(new_name))
- # end
- pw.release()
+ with self.repo.config_writer() as pw:
+ # As we ourselves didn't write anything about submodules into the parent .git/config,
+ # we will not require it to exist, and just ignore missing entries.
+ if pw.has_section(sm_section(self.name)):
+ pw.rename_section(sm_section(self.name), sm_section(new_name))
# .gitmodules
- cw = self.config_writer(write=True).config
- cw.rename_section(sm_section(self.name), sm_section(new_name))
- cw.release()
+ with self.config_writer(write=True).config as cw:
+ cw.rename_section(sm_section(self.name), sm_section(new_name))
self._name = new_name
diff --git a/git/objects/tag.py b/git/objects/tag.py
index c8684447..cefff083 100644
--- a/git/objects/tag.py
+++ b/git/objects/tag.py
@@ -21,7 +21,7 @@ class TagObject(base.Object):
type = "tag"
__slots__ = ("object", "tag", "tagger", "tagged_date", "tagger_tz_offset", "message")
- def __init__(self, repo, binsha, object=None, tag=None,
+ def __init__(self, repo, binsha, object=None, tag=None, # @ReservedAssignment
tagger=None, tagged_date=None, tagger_tz_offset=None, message=None):
"""Initialize a tag object with additional data
@@ -55,8 +55,8 @@ class TagObject(base.Object):
ostream = self.repo.odb.stream(self.binsha)
lines = ostream.read().decode(defenc).splitlines()
- obj, hexsha = lines[0].split(" ") # object <hexsha>
- type_token, type_name = lines[1].split(" ") # type <type_name>
+ obj, hexsha = lines[0].split(" ") # object <hexsha> @UnusedVariable
+ type_token, type_name = lines[1].split(" ") # type <type_name> @UnusedVariable
self.object = \
get_object_type_by_name(type_name.encode('ascii'))(self.repo, hex_to_bin(hexsha))
diff --git a/git/refs/head.py b/git/refs/head.py
index fe820b10..a1d8ab46 100644
--- a/git/refs/head.py
+++ b/git/refs/head.py
@@ -133,18 +133,15 @@ class Head(Reference):
raise ValueError("Incorrect parameter type: %r" % remote_reference)
# END handle type
- writer = self.config_writer()
- if remote_reference is None:
- writer.remove_option(self.k_config_remote)
- writer.remove_option(self.k_config_remote_ref)
- if len(writer.options()) == 0:
- writer.remove_section()
- # END handle remove section
- else:
- writer.set_value(self.k_config_remote, remote_reference.remote_name)
- writer.set_value(self.k_config_remote_ref, Head.to_full_path(remote_reference.remote_head))
- # END handle ref value
- writer.release()
+ with self.config_writer() as writer:
+ if remote_reference is None:
+ writer.remove_option(self.k_config_remote)
+ writer.remove_option(self.k_config_remote_ref)
+ if len(writer.options()) == 0:
+ writer.remove_section()
+ else:
+ writer.set_value(self.k_config_remote, remote_reference.remote_name)
+ writer.set_value(self.k_config_remote_ref, Head.to_full_path(remote_reference.remote_head))
return self
diff --git a/git/refs/reference.py b/git/refs/reference.py
index 3e132aef..cc99dc26 100644
--- a/git/refs/reference.py
+++ b/git/refs/reference.py
@@ -50,7 +50,7 @@ class Reference(SymbolicReference, LazyMixin, Iterable):
#{ Interface
- def set_object(self, object, logmsg=None):
+ def set_object(self, object, logmsg=None): # @ReservedAssignment
"""Special version which checks if the head-log needs an update as well
:return: self"""
oldbinsha = None
diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py
index ec2944c6..ebaff8ca 100644
--- a/git/refs/symbolic.py
+++ b/git/refs/symbolic.py
@@ -134,9 +134,8 @@ class SymbolicReference(object):
point to, or None"""
tokens = None
try:
- fp = open(join(repo.git_dir, ref_path), 'rt')
- value = fp.read().rstrip()
- fp.close()
+ with open(join(repo.git_dir, ref_path), 'rt') as fp:
+ value = fp.read().rstrip()
# Don't only split on spaces, but on whitespace, which allows to parse lines like
# 60b64ef992065e2600bfef6187a97f92398a9144 branch 'master' of git-server:/path/to/repo
tokens = value.split()
@@ -219,7 +218,7 @@ class SymbolicReference(object):
return self
- def set_object(self, object, logmsg=None):
+ def set_object(self, object, logmsg=None): # @ReservedAssignment
"""Set the object we point to, possibly dereference our symbolic reference first.
If the reference does not exist, it will be created
@@ -230,7 +229,7 @@ class SymbolicReference(object):
:note: plain SymbolicReferences may not actually point to objects by convention
:return: self"""
if isinstance(object, SymbolicReference):
- object = object.object
+ object = object.object # @ReservedAssignment
# END resolve references
is_detached = True
@@ -313,13 +312,17 @@ class SymbolicReference(object):
lfd = LockedFD(fpath)
fd = lfd.open(write=True, stream=True)
- fd.write(write_value.encode('ascii') + b'\n')
- lfd.commit()
-
+ ok = True
+ try:
+ fd.write(write_value.encode('ascii') + b'\n')
+ lfd.commit()
+ ok = True
+ finally:
+ if not ok:
+ lfd.rollback()
# Adjust the reflog
if logmsg is not None:
self.log_append(oldbinsha, logmsg)
- # END handle reflog
return self
@@ -422,40 +425,36 @@ class SymbolicReference(object):
# check packed refs
pack_file_path = cls._get_packed_refs_path(repo)
try:
- reader = open(pack_file_path, 'rb')
- except (OSError, IOError):
- pass # it didnt exist at all
- else:
- new_lines = list()
- made_change = False
- dropped_last_line = False
- for line in reader:
- # keep line if it is a comment or if the ref to delete is not
- # in the line
- # If we deleted the last line and this one is a tag-reference object,
- # we drop it as well
- line = line.decode(defenc)
- if (line.startswith('#') or full_ref_path not in line) and \
- (not dropped_last_line or dropped_last_line and not line.startswith('^')):
- new_lines.append(line)
- dropped_last_line = False
- continue
- # END skip comments and lines without our path
-
- # drop this line
- made_change = True
- dropped_last_line = True
- # END for each line in packed refs
- reader.close()
+ with open(pack_file_path, 'rb') as reader:
+ new_lines = list()
+ made_change = False
+ dropped_last_line = False
+ for line in reader:
+ # keep line if it is a comment or if the ref to delete is not
+ # in the line
+ # If we deleted the last line and this one is a tag-reference object,
+ # we drop it as well
+ line = line.decode(defenc)
+ if (line.startswith('#') or full_ref_path not in line) and \
+ (not dropped_last_line or dropped_last_line and not line.startswith('^')):
+ new_lines.append(line)
+ dropped_last_line = False
+ continue
+ # END skip comments and lines without our path
+
+ # drop this line
+ made_change = True
+ dropped_last_line = True
# write the new lines
if made_change:
# write-binary is required, otherwise windows will
# open the file in text mode and change LF to CRLF !
- open(pack_file_path, 'wb').writelines(l.encode(defenc) for l in new_lines)
- # END write out file
- # END open exception handling
- # END handle deletion
+ with open(pack_file_path, 'wb') as fd:
+ fd.writelines(l.encode(defenc) for l in new_lines)
+
+ except (OSError, IOError):
+ pass # it didnt exist at all
# delete the reflog
reflog_path = RefLog.path(cls(repo, full_ref_path))
@@ -484,7 +483,8 @@ class SymbolicReference(object):
target_data = target.path
if not resolve:
target_data = "ref: " + target_data
- existing_data = open(abs_ref_path, 'rb').read().decode(defenc).strip()
+ with open(abs_ref_path, 'rb') as fd:
+ existing_data = fd.read().decode(defenc).strip()
if existing_data != target_data:
raise OSError("Reference at %r does already exist, pointing to %r, requested was %r" %
(full_ref_path, existing_data, target_data))
@@ -549,7 +549,11 @@ class SymbolicReference(object):
if isfile(new_abs_path):
if not force:
# if they point to the same file, its not an error
- if open(new_abs_path, 'rb').read().strip() != open(cur_abs_path, 'rb').read().strip():
+ with open(new_abs_path, 'rb') as fd1:
+ f1 = fd1.read().strip()
+ with open(cur_abs_path, 'rb') as fd2:
+ f2 = fd2.read().strip()
+ if f1 != f2:
raise OSError("File at path %r already exists" % new_abs_path)
# else: we could remove ourselves and use the otherone, but
# but clarity we just continue as usual
@@ -591,7 +595,7 @@ class SymbolicReference(object):
# END for each directory to walk
# read packed refs
- for sha, rela_path in cls._iter_packed_refs(repo):
+ for sha, rela_path in cls._iter_packed_refs(repo): # @UnusedVariable
if rela_path.startswith(common_path):
rela_paths.add(rela_path)
# END relative path matches common path
diff --git a/git/remote.py b/git/remote.py
index 12129460..d35e1fad 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.util import (
)
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 @@ class PushInfo(object):
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
@@ -177,7 +176,7 @@ class PushInfo(object):
split_token = "..."
if control_character == " ":
split_token = ".."
- old_sha, new_sha = summary.split(' ')[0].split(split_token)
+ old_sha, new_sha = summary.split(' ')[0].split(split_token) # @UnusedVariable
# have to use constructor here as the sha usually is abbreviated
old_commit = old_sha
# END message handling
@@ -263,7 +262,7 @@ class FetchInfo(object):
# parse lines
control_character, operation, local_remote_ref, remote_local_ref, note = match.groups()
try:
- new_hex_sha, fetch_operation, fetch_note = fetch_line.split("\t")
+ new_hex_sha, fetch_operation, fetch_note = fetch_line.split("\t") # @UnusedVariable
ref_type_name, fetch_note = fetch_note.split(' ', 1)
except ValueError: # unpack error
raise ValueError("Failed to parse FETCH_HEAD line: %r" % fetch_line)
@@ -377,7 +376,7 @@ class Remote(LazyMixin, Iterable):
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
@@ -445,7 +444,7 @@ class Remote(LazyMixin, Iterable):
def iter_items(cls, repo):
""":return: Iterator yielding Remote objects of the given repository"""
for section in repo.config_reader("repository").sections():
- if not section.startswith('remote'):
+ if not section.startswith('remote '):
continue
lbound = section.find('"')
rbound = section.rfind('"')
@@ -626,8 +625,8 @@ class Remote(LazyMixin, Iterable):
for pline in progress_handler(line):
# END handle special messages
for cmd in cmds:
- if len(line) > 1 and line[0] == ' ' and line[1] == cmd:
- fetch_info_lines.append(line)
+ if len(pline) > 1 and pline[0] == ' ' and pline[1] == cmd:
+ fetch_info_lines.append(pline)
continue
# end find command code
# end for each comand code we know
@@ -635,13 +634,12 @@ class Remote(LazyMixin, Iterable):
# end
if progress.error_lines():
stderr_text = '\n'.join(progress.error_lines())
-
+
finalize_process(proc, stderr=stderr_text)
# read head information
- fp = open(join(self.repo.git_dir, 'FETCH_HEAD'), 'rb')
- fetch_head_info = [l.decode(defenc) for l in fp.readlines()]
- fp.close()
+ with open(join(self.repo.git_dir, 'FETCH_HEAD'), 'rb') as fp:
+ fetch_head_info = [l.decode(defenc) for l in fp.readlines()]
l_fil = len(fetch_info_lines)
l_fhi = len(fetch_head_info)
@@ -657,7 +655,7 @@ class Remote(LazyMixin, Iterable):
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
@@ -682,7 +680,7 @@ class Remote(LazyMixin, Iterable):
# 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_streams=False)
except Exception:
if len(output) == 0:
raise
@@ -769,17 +767,17 @@ class Remote(LazyMixin, Iterable):
: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 <http://goo.gl/NPa7st>`_ 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 0e46ee67..8b68b5ff 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -56,6 +56,7 @@ from git.compat import (
PY3,
safe_decode,
range,
+ is_win,
)
import os
@@ -71,7 +72,7 @@ if sys.version_info[:2] < (2, 5): # python 2.4 compatiblity
BlameEntry = namedtuple('BlameEntry', ['commit', 'linenos', 'orig_path', 'orig_linenos'])
-__all__ = ('Repo', )
+__all__ = ('Repo',)
def _expand_path(p):
@@ -209,11 +210,13 @@ class Repo(object):
# Description property
def _get_description(self):
filename = join(self.git_dir, 'description')
- return open(filename, 'rb').read().rstrip().decode(defenc)
+ with open(filename, 'rb') as fp:
+ return fp.read().rstrip().decode(defenc)
def _set_description(self, descr):
filename = join(self.git_dir, 'description')
- open(filename, 'wb').write((descr + '\n').encode(defenc))
+ with open(filename, 'wb') as fp:
+ fp.write((descr + '\n').encode(defenc))
description = property(_get_description, _set_description,
doc="the project's description")
@@ -369,7 +372,7 @@ class Repo(object):
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":
@@ -547,11 +550,8 @@ class Repo(object):
alternates_path = join(self.git_dir, 'objects', 'info', 'alternates')
if os.path.exists(alternates_path):
- try:
- f = open(alternates_path, 'rb')
+ with open(alternates_path, 'rb') as f:
alts = f.read().decode(defenc)
- finally:
- f.close()
return alts.strip().splitlines()
else:
return list()
@@ -572,13 +572,8 @@ class Repo(object):
if isfile(alternates_path):
os.remove(alternates_path)
else:
- try:
- f = open(alternates_path, 'wb')
+ with open(alternates_path, 'wb') as f:
f.write("\n".join(alts).encode(defenc))
- finally:
- f.close()
- # END file handling
- # END alts handling
alternates = property(_get_alternates, _set_alternates,
doc="Retrieve a list of alternates paths or set a list paths to be used as alternates")
@@ -883,7 +878,7 @@ class Repo(object):
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)
@@ -907,7 +902,7 @@ class Repo(object):
if progress:
handle_process_output(proc, None, progress.new_message_handler(), finalize_process)
else:
- (stdout, stderr) = proc.communicate()
+ (stdout, stderr) = proc.communicate() # FIXME: Will block of outputs are big!
finalize_process(proc, stderr=stderr)
# end handle progress
finally:
@@ -929,10 +924,8 @@ class Repo(object):
# sure
repo = cls(os.path.abspath(path), odbt=odbt)
if repo.remotes:
- writer = repo.remotes[0].config_writer
- writer.set_value('url', repo.remotes[0].url.replace("\\\\", "\\").replace("\\", "/"))
- # PY3: be sure cleanup is performed and lock is released
- writer.release()
+ with repo.remotes[0].config_writer as writer:
+ writer.set_value('url', repo.remotes[0].url.replace("\\\\", "\\").replace("\\", "/"))
# END handle remote repo
return repo
diff --git a/git/repo/fun.py b/git/repo/fun.py
index 6b06663a..320eb1c8 100644
--- a/git/repo/fun.py
+++ b/git/repo/fun.py
@@ -25,8 +25,8 @@ __all__ = ('rev_parse', 'is_git_dir', 'touch', 'find_git_dir', 'name_to_object',
def touch(filename):
- fp = open(filename, "ab")
- fp.close()
+ with open(filename, "ab"):
+ pass
return filename
@@ -284,7 +284,7 @@ def rev_parse(repo, rev):
try:
if token == "~":
obj = to_commit(obj)
- for item in xrange(num):
+ for _ in xrange(num):
obj = obj.parents[0]
# END for each history item to walk
elif token == "^":
diff --git a/git/test/fixtures/cat_file.py b/git/test/fixtures/cat_file.py
index 2f1b915a..5480e628 100644
--- a/git/test/fixtures/cat_file.py
+++ b/git/test/fixtures/cat_file.py
@@ -1,5 +1,6 @@
import sys
-for line in open(sys.argv[1]).readlines():
- sys.stdout.write(line)
- sys.stderr.write(line)
+with open(sys.argv[1]) as fd:
+ for line in fd.readlines():
+ sys.stdout.write(line)
+ sys.stderr.write(line)
diff --git a/git/test/lib/asserts.py b/git/test/lib/asserts.py
index 60a888b3..6f5ba714 100644
--- a/git/test/lib/asserts.py
+++ b/git/test/lib/asserts.py
@@ -8,15 +8,18 @@ import re
import stat
from nose.tools import (
- assert_equal,
- assert_not_equal,
- assert_raises,
- raises,
- assert_true,
- assert_false
+ assert_equal, # @UnusedImport
+ assert_not_equal, # @UnusedImport
+ assert_raises, # @UnusedImport
+ raises, # @UnusedImport
+ assert_true, # @UnusedImport
+ assert_false # @UnusedImport
)
-from mock import patch
+try:
+ from unittest.mock import patch
+except ImportError:
+ from mock import patch # @NoMove @UnusedImport
__all__ = ['assert_instance_of', 'assert_not_instance_of',
'assert_none', 'assert_not_none',
diff --git a/git/test/lib/helper.py b/git/test/lib/helper.py
index 8be2881c..e92ce8b4 100644
--- a/git/test/lib/helper.py
+++ b/git/test/lib/helper.py
@@ -4,16 +4,19 @@
# 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
-import sys
from unittest import TestCase
import time
import tempfile
-import shutil
import io
+import logging
+
+from functools import wraps
-from git import Repo, Remote, GitCommandError, Git
-from git.compat import string_types
+from git.util import rmtree
+from git.compat import string_types, is_win
+import textwrap
osp = os.path.dirname
@@ -22,9 +25,17 @@ GIT_DAEMON_PORT = os.environ.get("GIT_PYTHON_TEST_GIT_DAEMON_PORT", "9418")
__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')
+
+#: We need an easy way to see if Appveyor TCs start failing,
+#: so the errors marked with this var are considered "acknowledged" ones, awaiting remedy,
+#: till then, we wish to hide them.
+HIDE_WINDOWS_KNOWN_ERRORS = is_win and os.environ.get('HIDE_WINDOWS_KNOWN_ERRORS', True)
+
#{ Routines
@@ -34,7 +45,8 @@ def fixture_path(name):
def fixture(name):
- return open(fixture_path(name), 'rb').read()
+ with open(fixture_path(name), 'rb') as fd:
+ return fd.read()
def absolute_project_path():
@@ -71,21 +83,39 @@ 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
-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:
- 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)
+ @wraps(func)
+ def wrapper(self):
+ path = tempfile.mktemp(prefix=func.__name__)
+ os.mkdir(path)
+ keep = False
+ try:
+ try:
+ return func(self, path)
+ except Exception:
+ log.info("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)
+
+ return wrapper
def with_rw_repo(working_tree_ref, bare=False):
@@ -101,6 +131,7 @@ def with_rw_repo(working_tree_ref, bare=False):
assert isinstance(working_tree_ref, string_types), "Decorator requires ref name for working tree checkout"
def argument_passer(func):
+ @wraps(func)
def repo_creator(self):
prefix = 'non_'
if bare:
@@ -120,23 +151,48 @@ def with_rw_repo(working_tree_ref, bare=False):
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:
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
- repo_creator.__name__ = func.__name__
return repo_creator
# END argument passer
return argument_passer
+def launch_git_daemon(temp_dir, ip, port):
+ from git import Git
+ 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
@@ -161,9 +217,12 @@ def with_rw_and_rw_remote_repo(working_tree_ref):
See working dir info in with_rw_repo
:note: We attempt to launch our own invocation of git-daemon, which will be shutdown at the end of the test.
"""
+ from git import Remote, GitCommandError
assert isinstance(working_tree_ref, string_types), "Decorator requires ref name for working tree checkout"
def argument_passer(func):
+
+ @wraps(func)
def remote_repo_creator(self):
remote_repo_dir = _mktemp("remote_repo_%s" % func.__name__)
repo_dir = _mktemp("remote_clone_non_bare_repo")
@@ -178,16 +237,13 @@ def with_rw_and_rw_remote_repo(working_tree_ref):
rw_remote_repo.daemon_export = True
# this thing is just annoying !
- crw = rw_remote_repo.config_writer()
- section = "daemon"
- try:
- crw.add_section(section)
- except Exception:
- pass
- crw.set(section, "receivepack", True)
- # release lock
- crw.release()
- del(crw)
+ with rw_remote_repo.config_writer() as crw:
+ section = "daemon"
+ try:
+ crw.add_section(section)
+ except Exception:
+ pass
+ crw.set(section, "receivepack", True)
# initialize the remote - first do it as local remote and pull, then
# we change the url to point to the daemon. The daemon should be started
@@ -196,74 +252,86 @@ def with_rw_and_rw_remote_repo(working_tree_ref):
d_remote.fetch()
remote_repo_url = "git://localhost:%s%s" % (GIT_DAEMON_PORT, remote_repo_dir)
- d_remote.config_writer.set('url', remote_repo_url)
+ with d_remote.config_writer as cw:
+ cw.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
+ gd = launch_git_daemon(temp_dir, '127.0.0.1', GIT_DAEMON_PORT)
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:
- os.kill(gd.proc.pid, 15)
- print(str(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
- 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=<port>'
- msg += 'and setting the environment variable GIT_PYTHON_TEST_GIT_DAEMON_PORT to <port>'
- msg %= temp_dir
- raise AssertionError(msg)
- # END make assertion
- # END catch ls remote error
-
- # adjust working dir
- prev_cwd = os.getcwd()
- os.chdir(rw_repo.working_dir)
- try:
+ # 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 ?
+ 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:
+ msg = textwrap.dedent("""
+ 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:"
+
+ msg += textwrap.dedent("""
+ git daemon --enable=receive-pack '%s'
+ You can also run the daemon on a different port by passing --port=<port>"
+ and setting the environment variable GIT_PYTHON_TEST_GIT_DAEMON_PORT to <port>
+ """ % temp_dir)
+ from nose import SkipTest
+ raise SkipTest(msg) if is_win else AssertionError(msg)
+ # END make assertion
+ # END catch ls remote error
+
+ # adjust working dir
+ prev_cwd = os.getcwd()
+ os.chdir(rw_repo.working_dir)
+
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:
+ os.chdir(prev_cwd)
+
finally:
- # gd.proc.kill() ... no idea why that doesn't work
- if gd is not None:
- os.kill(gd.proc.pid, 15)
+ try:
+ gd.proc.kill()
+ except:
+ ## Either it has died (and we're here), or it won't die, again here...
+ pass
- os.chdir(prev_cwd)
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()
# END cleanup
# END bare repo creator
- remote_repo_creator.__name__ = func.__name__
return remote_repo_creator
# END remote repo creator
- # END argument parsser
+ # END argument parser
return argument_passer
@@ -299,6 +367,9 @@ class TestBase(TestCase):
Dynamically add a read-only repository to our actual type. This way
each test type has its own repository
"""
+ from git import Repo
+ import gc
+ gc.collect()
cls.rorepo = Repo(GIT_REPO)
@classmethod
@@ -313,7 +384,6 @@ class TestBase(TestCase):
"""
repo = repo or self.rorepo
abs_path = os.path.join(repo.working_tree_dir, rela_path)
- fp = open(abs_path, "w")
- fp.write(data)
- fp.close()
+ with open(abs_path, "w") as fp:
+ fp.write(data)
return abs_path
diff --git a/git/test/performance/lib.py b/git/test/performance/lib.py
index bb3f7a99..0c4c20a4 100644
--- a/git/test/performance/lib.py
+++ b/git/test/performance/lib.py
@@ -4,7 +4,6 @@ from git.test.lib import (
TestBase
)
from gitdb.test.lib import skip_on_travis_ci
-import shutil
import tempfile
import logging
@@ -16,9 +15,11 @@ from git.db import (
from git import (
Repo
)
+from git.util import rmtree
#{ Invvariants
k_env_git_repo = "GIT_PYTHON_TEST_GIT_REPO_BASE"
+
#} END invariants
@@ -86,7 +87,7 @@ class TestBigRepoRW(TestBigRepoR):
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/performance/test_commit.py b/git/test/performance/test_commit.py
index b59c747e..c60dc2fc 100644
--- a/git/test/performance/test_commit.py
+++ b/git/test/performance/test_commit.py
@@ -17,6 +17,10 @@ from git.test.test_commit import assert_commit_serialization
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/performance/test_odb.py b/git/test/performance/test_odb.py
index b14e6db0..6f07a615 100644
--- a/git/test/performance/test_odb.py
+++ b/git/test/performance/test_odb.py
@@ -1,7 +1,12 @@
"""Performance tests for object store"""
from __future__ import print_function
-from time import time
+
import sys
+from time import time
+from unittest.case import skipIf
+
+from git.compat import PY3
+from git.test.lib.helper import HIDE_WINDOWS_KNOWN_ERRORS
from .lib import (
TestBigRepoR
@@ -10,6 +15,8 @@ from .lib import (
class TestObjDBPerformance(TestBigRepoR):
+ @skipIf(HIDE_WINDOWS_KNOWN_ERRORS and PY3,
+ "FIXME: smmp fails with: TypeError: Can't convert 'bytes' object to str implicitly")
def test_random_access(self):
results = [["Iterate Commits"], ["Iterate Blobs"], ["Retrieve Blob Data"]]
for repo in (self.gitrorepo, self.puregitrorepo):
diff --git a/git/test/performance/test_streams.py b/git/test/performance/test_streams.py
index 4b1738cd..42cbade5 100644
--- a/git/test/performance/test_streams.py
+++ b/git/test/performance/test_streams.py
@@ -87,6 +87,9 @@ class TestObjDBPerformance(TestBigRepoR):
% (size_kib, desc, cs_kib, elapsed_readchunks, size_kib / elapsed_readchunks), file=sys.stderr)
# del db file so git has something to do
+ ostream = None
+ import gc
+ gc.collect()
os.remove(db_file)
# VS. CGIT
@@ -117,7 +120,7 @@ class TestObjDBPerformance(TestBigRepoR):
# read all
st = time()
- s, t, size, data = rwrepo.git.get_object_data(gitsha)
+ hexsha, typename, size, data = rwrepo.git.get_object_data(gitsha) # @UnusedVariable
gelapsed_readall = time() - st
print("Read %i KiB of %s data at once using git-cat-file in %f s ( %f Read KiB / s)"
% (size_kib, desc, gelapsed_readall, size_kib / gelapsed_readall), file=sys.stderr)
@@ -128,7 +131,7 @@ class TestObjDBPerformance(TestBigRepoR):
# read chunks
st = time()
- s, t, size, stream = rwrepo.git.stream_object_data(gitsha)
+ hexsha, typename, size, stream = rwrepo.git.stream_object_data(gitsha) # @UnusedVariable
while True:
data = stream.read(cs)
if len(data) < cs:
diff --git a/git/test/test_base.py b/git/test/test_base.py
index 7b71a77e..e5e8f173 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 (
@@ -23,10 +24,15 @@ from git import (
)
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):
+ def tearDown(self):
+ import gc
+ gc.collect()
+
type_tuples = (("blob", "8741fc1d09d61f02ffd8cded15ff603eff1ec070", "blob.py"),
("tree", "3a6a5e3eeed3723c09f1ef0399f81ed6b8d82e79", "directory"),
("commit", "4251bd59fb8e11e40c40548cba38180a9536118c", None),
@@ -71,13 +77,11 @@ class TestBase(TestBase):
assert data
tmpfilename = tempfile.mktemp(suffix='test-stream')
- tmpfile = open(tmpfilename, 'wb+')
- assert item == item.stream_data(tmpfile)
- tmpfile.seek(0)
- assert tmpfile.read() == data
- tmpfile.close()
+ with open(tmpfilename, 'wb+') as tmpfile:
+ assert item == item.stream_data(tmpfile)
+ tmpfile.seek(0)
+ assert tmpfile.read() == data
os.remove(tmpfilename)
- # END stream to file directly
# END for each object type to create
# each has a unique sha
@@ -112,6 +116,8 @@ class TestBase(TestBase):
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,
+ "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"
@@ -125,9 +131,10 @@ class TestBase(TestBase):
from nose import SkipTest
raise SkipTest("Environment doesn't support unicode filenames")
- open(file_path, "wb").write(b'something')
+ with open(file_path, "wb") as fp:
+ fp.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_commit.py b/git/test/test_commit.py
index c0599503..fd9777fb 100644
--- a/git/test/test_commit.py
+++ b/git/test/test_commit.py
@@ -19,7 +19,7 @@ from git import (
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
@@ -34,7 +34,11 @@ import re
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):
@@ -57,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
@@ -78,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
@@ -99,10 +103,10 @@ class TestCommit(TestBase):
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')
@@ -119,26 +123,26 @@ class TestCommit(TestBase):
check_entries(stats.total)
assert "files" in stats.total
- for filepath, d in stats.files.items():
+ for filepath, d in stats.files.items(): # @UnusedVariable
check_entries(d)
# END for each stated file
# assure data is parsed properly
michael = Actor._from_string("Michael Trier <mtrier@gmail.com>")
- 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 <something@this.com>" % name)
- assert special.name == name
+ self.assertEqual(special.name, name)
assert isinstance(special.name, text_type)
def test_traversal(self):
@@ -152,44 +156,44 @@ class TestCommit(TestBase):
# 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')
@@ -236,7 +240,7 @@ class TestCommit(TestBase):
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
@@ -266,7 +270,7 @@ class TestCommit(TestBase):
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):
@@ -279,7 +283,7 @@ class TestCommit(TestBase):
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
@@ -288,10 +292,10 @@ class TestCommit(TestBase):
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)
@@ -301,22 +305,24 @@ class TestCommit(TestBase):
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__()
def test_invalid_commit(self):
cmt = self.rorepo.commit()
- cmt._deserialize(open(fixture_path('commit_invalid_data'), 'rb'))
+ with open(fixture_path('commit_invalid_data'), 'rb') as fd:
+ cmt._deserialize(fd)
- 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()
- cmt._deserialize(open(fixture_path('commit_with_gpgsig'), 'rb'))
+ with open(fixture_path('commit_with_gpgsig'), 'rb') as fd:
+ cmt._deserialize(fd)
fixture_sig = """-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.11 (GNU/Linux)
@@ -335,7 +341,7 @@ BX/otlTa8pNE3fWYBxURvfHnMY4i3HQT7Bc1QjImAhMnyo2vJk4ORBJIZ1FTNIhJ
JzJMZDRLQLFvnzqZuCjE
=przd
-----END PGP SIGNATURE-----"""
- assert cmt.gpgsig == fixture_sig
+ self.assertEqual(cmt.gpgsig, fixture_sig)
cmt.gpgsig = "<test\ndummy\nsig>"
assert cmt.gpgsig != fixture_sig
@@ -343,39 +349,39 @@ JzJMZDRLQLFvnzqZuCjE
cstream = BytesIO()
cmt._serialize(cstream)
assert re.search(r"^gpgsig <test\n dummy\n sig>$", cstream.getvalue().decode('ascii'), re.MULTILINE)
-
+
self.assert_gpgsig_deserialization(cstream)
-
+
cstream.seek(0)
cmt.gpgsig = None
cmt._deserialize(cstream)
- assert cmt.gpgsig == "<test\ndummy\nsig>"
+ self.assertEqual(cmt.gpgsig, "<test\ndummy\nsig>")
cmt.gpgsig = None
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)
@@ -383,9 +389,13 @@ JzJMZDRLQLFvnzqZuCjE
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_config.py b/git/test/test_config.py
index c0889c1a..32873f24 100644
--- a/git/test/test_config.py
+++ b/git/test/test_config.py
@@ -4,28 +4,45 @@
# This module is part of GitPython and is released under
# the BSD License: http://www.opensource.org/licenses/bsd-license.php
-from git.test.lib import (
- TestCase,
- fixture_path,
- assert_equal,
-)
-from gitdb.test.lib import with_rw_directory
+import glob
+import io
+import os
+
from git import (
GitConfigParser
)
-from git.compat import (
- string_types,
-)
-import io
-import os
+from git.compat import string_types
from git.config import cp
+from git.test.lib import (
+ TestCase,
+ fixture_path,
+)
+from git.test.lib import with_rw_directory
+
+import os.path as osp
+from git.util import rmfile
+
+
+_tc_lock_fpaths = osp.join(osp.dirname(__file__), 'fixtures/*.lock')
+
+
+def _rm_lock_files():
+ for lfp in glob.glob(_tc_lock_fpaths):
+ rmfile(lfp)
class TestBase(TestCase):
+ def setUp(self):
+ _rm_lock_files()
+
+ def tearDown(self):
+ for lfp in glob.glob(_tc_lock_fpaths):
+ if osp.isfile(lfp):
+ raise AssertionError('Previous TC left hanging git-lock file: %s', lfp)
def _to_memcache(self, file_path):
- fp = open(file_path, "rb")
- sio = io.BytesIO(fp.read())
+ with open(file_path, "rb") as fp:
+ sio = io.BytesIO(fp.read())
sio.name = file_path
return sio
@@ -33,43 +50,43 @@ class TestBase(TestCase):
# writer must create the exact same file as the one read before
for filename in ("git_config", "git_config_global"):
file_obj = self._to_memcache(fixture_path(filename))
- w_config = GitConfigParser(file_obj, read_only=False)
- w_config.read() # enforce reading
- assert w_config._sections
- w_config.write() # enforce writing
-
- # we stripped lines when reading, so the results differ
- assert file_obj.getvalue()
- self.assertEqual(file_obj.getvalue(), self._to_memcache(fixture_path(filename)).getvalue())
-
- # creating an additional config writer must fail due to exclusive access
- self.failUnlessRaises(IOError, GitConfigParser, file_obj, read_only=False)
-
- # should still have a lock and be able to make changes
- assert w_config._lock._has_lock()
-
- # changes should be written right away
- sname = "my_section"
- oname = "mykey"
- val = "myvalue"
- w_config.add_section(sname)
- assert w_config.has_section(sname)
- w_config.set(sname, oname, val)
- assert w_config.has_option(sname, oname)
- assert w_config.get(sname, oname) == val
-
- sname_new = "new_section"
- oname_new = "new_key"
- ival = 10
- w_config.set_value(sname_new, oname_new, ival)
- assert w_config.get_value(sname_new, oname_new) == ival
-
- file_obj.seek(0)
- r_config = GitConfigParser(file_obj, read_only=True)
- assert r_config.has_section(sname)
- assert r_config.has_option(sname, oname)
- assert r_config.get(sname, oname) == val
- w_config.release()
+ with GitConfigParser(file_obj, read_only=False) as w_config:
+ w_config.read() # enforce reading
+ assert w_config._sections
+ w_config.write() # enforce writing
+
+ # we stripped lines when reading, so the results differ
+ assert file_obj.getvalue()
+ self.assertEqual(file_obj.getvalue(), self._to_memcache(fixture_path(filename)).getvalue())
+
+ # creating an additional config writer must fail due to exclusive access
+ with self.assertRaises(IOError):
+ GitConfigParser(file_obj, read_only=False)
+
+ # should still have a lock and be able to make changes
+ assert w_config._lock._has_lock()
+
+ # changes should be written right away
+ sname = "my_section"
+ oname = "mykey"
+ val = "myvalue"
+ w_config.add_section(sname)
+ assert w_config.has_section(sname)
+ w_config.set(sname, oname, val)
+ assert w_config.has_option(sname, oname)
+ assert w_config.get(sname, oname) == val
+
+ sname_new = "new_section"
+ oname_new = "new_key"
+ ival = 10
+ w_config.set_value(sname_new, oname_new, ival)
+ assert w_config.get_value(sname_new, oname_new) == ival
+
+ file_obj.seek(0)
+ r_config = GitConfigParser(file_obj, read_only=True)
+ assert r_config.has_section(sname)
+ assert r_config.has_option(sname, oname)
+ assert r_config.get(sname, oname) == val
# END for each filename
@with_rw_directory
@@ -82,30 +99,32 @@ class TestBase(TestCase):
with gcp as cw:
cw.set_value('include', 'some_other_value', 'b')
# ...so creating an additional config writer must fail due to exclusive access
- self.failUnlessRaises(IOError, GitConfigParser, fpl, read_only=False)
+ with self.assertRaises(IOError):
+ GitConfigParser(fpl, read_only=False)
# but work when the lock is removed
with GitConfigParser(fpl, read_only=False):
assert os.path.exists(fpl)
# reentering with an existing lock must fail due to exclusive access
- self.failUnlessRaises(IOError, gcp.__enter__)
+ with self.assertRaises(IOError):
+ gcp.__enter__()
def test_multi_line_config(self):
file_obj = self._to_memcache(fixture_path("git_config_with_comments"))
- config = GitConfigParser(file_obj, read_only=False)
- ev = "ruby -e '\n"
- ev += " system %(git), %(merge-file), %(--marker-size=%L), %(%A), %(%O), %(%B)\n"
- ev += " b = File.read(%(%A))\n"
- ev += " b.sub!(/^<+ .*\\nActiveRecord::Schema\\.define.:version => (\\d+). do\\n=+\\nActiveRecord::Schema\\."
- ev += "define.:version => (\\d+). do\\n>+ .*/) do\n"
- ev += " %(ActiveRecord::Schema.define(:version => #{[$1, $2].max}) do)\n"
- ev += " end\n"
- ev += " File.open(%(%A), %(w)) {|f| f.write(b)}\n"
- ev += " exit 1 if b.include?(%(<)*%L)'"
- assert_equal(config.get('merge "railsschema"', 'driver'), ev)
- assert_equal(config.get('alias', 'lg'),
- "log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr)%Creset'"
- " --abbrev-commit --date=relative")
- assert len(config.sections()) == 23
+ with GitConfigParser(file_obj, read_only=False) as config:
+ ev = "ruby -e '\n"
+ ev += " system %(git), %(merge-file), %(--marker-size=%L), %(%A), %(%O), %(%B)\n"
+ ev += " b = File.read(%(%A))\n"
+ ev += " b.sub!(/^<+ .*\\nActiveRecord::Schema\\.define.:version => (\\d+). do\\n=+\\nActiveRecord::Schema\\." # noqa E501
+ ev += "define.:version => (\\d+). do\\n>+ .*/) do\n"
+ ev += " %(ActiveRecord::Schema.define(:version => #{[$1, $2].max}) do)\n"
+ ev += " end\n"
+ ev += " File.open(%(%A), %(w)) {|f| f.write(b)}\n"
+ ev += " exit 1 if b.include?(%(<)*%L)'"
+ self.assertEqual(config.get('merge "railsschema"', 'driver'), ev)
+ self.assertEqual(config.get('alias', 'lg'),
+ "log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr)%Creset'"
+ " --abbrev-commit --date=relative")
+ self.assertEqual(len(config.sections()), 23)
def test_base(self):
path_repo = fixture_path("git_config")
@@ -129,10 +148,13 @@ class TestBase(TestCase):
assert "\n" not in val
# writing must fail
- self.failUnlessRaises(IOError, r_config.set, section, option, None)
- self.failUnlessRaises(IOError, r_config.remove_option, section, option)
+ with self.assertRaises(IOError):
+ r_config.set(section, option, None)
+ with self.assertRaises(IOError):
+ r_config.remove_option(section, option)
# END for each option
- self.failUnlessRaises(IOError, r_config.remove_section, section)
+ with self.assertRaises(IOError):
+ r_config.remove_section(section)
# END for each section
assert num_sections and num_options
assert r_config._is_initialized is True
@@ -142,7 +164,8 @@ class TestBase(TestCase):
assert r_config.get_value("doesnt", "exist", default) == default
# it raises if there is no default though
- self.failUnlessRaises(cp.NoSectionError, r_config.get_value, "doesnt", "exist")
+ with self.assertRaises(cp.NoSectionError):
+ r_config.get_value("doesnt", "exist")
@with_rw_directory
def test_config_include(self, rw_dir):
@@ -191,7 +214,8 @@ class TestBase(TestCase):
write_test_value(cw, tv)
with GitConfigParser(fpa, read_only=True) as cr:
- self.failUnlessRaises(cp.NoSectionError, check_test_value, cr, tv)
+ with self.assertRaises(cp.NoSectionError):
+ check_test_value(cr, tv)
# But can make it skip includes alltogether, and thus allow write-backs
with GitConfigParser(fpa, read_only=False, merge_includes=False) as cw:
@@ -202,22 +226,21 @@ class TestBase(TestCase):
def test_rename(self):
file_obj = self._to_memcache(fixture_path('git_config'))
- cw = GitConfigParser(file_obj, read_only=False, merge_includes=False)
-
- self.failUnlessRaises(ValueError, cw.rename_section, "doesntexist", "foo")
- self.failUnlessRaises(ValueError, cw.rename_section, "core", "include")
+ with GitConfigParser(file_obj, read_only=False, merge_includes=False) as cw:
+ with self.assertRaises(ValueError):
+ cw.rename_section("doesntexist", "foo")
+ with self.assertRaises(ValueError):
+ cw.rename_section("core", "include")
- nn = "bee"
- assert cw.rename_section('core', nn) is cw
- assert not cw.has_section('core')
- assert len(cw.items(nn)) == 4
- cw.release()
+ nn = "bee"
+ assert cw.rename_section('core', nn) is cw
+ assert not cw.has_section('core')
+ assert len(cw.items(nn)) == 4
def test_complex_aliases(self):
file_obj = self._to_memcache(fixture_path('.gitconfig'))
- w_config = GitConfigParser(file_obj, read_only=False)
- self.assertEqual(w_config.get('alias', 'rbi'), '"!g() { git rebase -i origin/${1:-master} ; } ; g"')
- w_config.release()
+ with GitConfigParser(file_obj, read_only=False) as w_config:
+ self.assertEqual(w_config.get('alias', 'rbi'), '"!g() { git rebase -i origin/${1:-master} ; } ; g"')
self.assertEqual(file_obj.getvalue(), self._to_memcache(fixture_path('.gitconfig')).getvalue())
def test_empty_config_value(self):
@@ -225,4 +248,5 @@ class TestBase(TestCase):
assert cr.get_value('core', 'filemode'), "Should read keys with values"
- self.failUnlessRaises(cp.NoOptionError, cr.get_value, 'color', 'ui')
+ with self.assertRaises(cp.NoOptionError):
+ cr.get_value('color', 'ui')
diff --git a/git/test/test_diff.py b/git/test/test_diff.py
index 9fdb26a2..d34d84e3 100644
--- a/git/test/test_diff.py
+++ b/git/test/test_diff.py
@@ -15,7 +15,7 @@ from git.test.lib import (
)
-from gitdb.test.lib import with_rw_directory
+from git.test.lib import with_rw_directory
from git import (
Repo,
@@ -24,10 +24,16 @@ from git import (
DiffIndex,
NULL_TREE,
)
+import ddt
+@ddt.ddt
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:
@@ -66,13 +72,14 @@ class TestDiff(TestBase):
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'))
- 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))
@@ -80,7 +87,7 @@ class TestDiff(TestBase):
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))
@@ -95,42 +102,46 @@ class TestDiff(TestBase):
assert isinstance(str(diff), str)
output = StringProcessAdapter(fixture('diff_rename_raw'))
- diffs = Diff._index_from_raw_format(self.rorepo, output.stdout)
- assert len(diffs) == 1
+ diffs = Diff._index_from_raw_format(self.rorepo, output)
+ 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
-
- 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
- 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"
- # end for each method to test
+ 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')
+ self.assertEqual(diffs[0].change_type, 'M')
+ self.assertIsNone(diffs[0].b_blob,)
+
+ @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)))
+ 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'))
- res = Diff._index_from_patch_format(None, output.stdout)
- assert len(res) == 6
+ res = Diff._index_from_patch_format(None, output)
+ 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]
@@ -138,29 +149,29 @@ class TestDiff(TestBase):
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
+ res = Diff._index_from_raw_format(None, output)
+ 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'))
- 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')
@@ -196,14 +207,14 @@ class TestDiff(TestBase):
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)
- 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)
+ 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))
def test_diff_interface(self):
# test a few variations of the main diff routine
@@ -232,12 +243,12 @@ class TestDiff(TestBase):
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
@@ -248,11 +259,11 @@ class TestDiff(TestBase):
# 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)
diff --git a/git/test/test_docs.py b/git/test/test_docs.py
index b297363d..e2bfcb21 100644
--- a/git/test/test_docs.py
+++ b/git/test/test_docs.py
@@ -7,10 +7,18 @@
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):
+
+ def tearDown(self):
+ import gc
+ gc.collect()
+
+ # @skipIf(HIDE_WINDOWS_KNOWN_ERRORS,
+ # "FIXME: helper.wrapper fails with: PermissionError: [WinError 5] Access is denied: "
+ # "'C:\\Users\\appveyor\\AppData\\Local\\Temp\\1\\test_work_tree_unsupportedryfa60di\\master_repo\\.git\\objects\\pack\\pack-bc9e0787aef9f69e1591ef38ea0a6f566ec66fe3.idx") # noqa E501
@with_rw_directory
def test_init_repo_object(self, rw_dir):
# [1-test_init_repo_object]
@@ -31,8 +39,8 @@ class Tutorials(TestBase):
# [3-test_init_repo_object]
repo.config_reader() # get a config reader for read-only access
- cw = repo.config_writer() # get a config writer to change configuration
- cw.release() # call release() to be sure changes are written and locks are released
+ with repo.config_writer(): # get a config writer to change configuration
+ pass # call release() to be sure changes are written and locks are released
# ![3-test_init_repo_object]
# [4-test_init_repo_object]
@@ -48,33 +56,35 @@ class Tutorials(TestBase):
# ![5-test_init_repo_object]
# [6-test_init_repo_object]
- repo.archive(open(join(rw_dir, 'repo.tar'), 'wb'))
+ with open(join(rw_dir, 'repo.tar'), 'wb') as fp:
+ repo.archive(fp)
# ![6-test_init_repo_object]
# 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
+ "It's ok if TC not running from `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['origin/master'] == repo.remotes.origin.refs.master # ... remotes ...
- assert repo.refs['0.3.5'] == repo.tags['0.3.5'] # ... and tags
+ self.assertEqual(repo.refs['origin/master'], repo.remotes.origin.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]
@@ -84,7 +94,7 @@ class Tutorials(TestBase):
# [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
@@ -105,7 +115,7 @@ class Tutorials(TestBase):
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
@@ -117,8 +127,8 @@ class Tutorials(TestBase):
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
origin = bare_repo.create_remote('origin', url=cloned_repo.working_tree_dir)
assert origin.exists()
for fetch_info in origin.fetch(progress=MyProgressPrinter()):
@@ -133,8 +143,8 @@ class Tutorials(TestBase):
# 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
@@ -205,7 +215,7 @@ class Tutorials(TestBase):
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
@@ -233,23 +243,23 @@ class Tutorials(TestBase):
# [8-test_references_and_objects]
hc = repo.head.commit
hct = hc.tree
- hc != hct
- hc != repo.tags[0]
- hc == repo.head.reference.commit
+ hc != hct # @NoEffect
+ hc != repo.tags[0] # @NoEffect
+ hc == repo.head.reference.commit # @NoEffect
# ![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]
@@ -306,14 +316,14 @@ class Tutorials(TestBase):
# ![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]
@@ -326,7 +336,7 @@ class Tutorials(TestBase):
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]
@@ -338,7 +348,7 @@ class Tutorials(TestBase):
# The index contains all blobs in a flat list
assert len(list(index.iter_blobs())) == len([o for o in repo.head.commit.tree.traverse() if o.type == 'blob'])
# Access blob objects
- for (path, stage), entry in index.entries.items():
+ for (path, stage), entry in index.entries.items(): # @UnusedVariable
pass
new_file_path = os.path.join(repo.working_tree_dir, 'new-file-name')
open(new_file_path, 'w').close()
@@ -346,7 +356,7 @@ class Tutorials(TestBase):
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
@@ -373,7 +383,7 @@ class Tutorials(TestBase):
assert origin == empty_repo.remotes.origin == empty_repo.remotes['origin']
origin.fetch() # assure we actually have data. fetch() returns useful information
# Setup a local tracking branch of a remote branch
- empty_repo.create_head('master', origin.refs.master) # create local branch "master" from remote branch "master"
+ empty_repo.create_head('master', origin.refs.master) # create local branch "master" from remote "master"
empty_repo.heads.master.set_tracking_branch(origin.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:
@@ -388,9 +398,8 @@ class Tutorials(TestBase):
# [26-test_references_and_objects]
assert origin.url == repo.remotes.origin.url
- cw = origin.config_writer
- cw.set("pushurl", "other_url")
- cw.release()
+ with origin.config_writer as cw:
+ cw.set("pushurl", "other_url")
# Please note that in python 2, writing origin.config_writer.set(...) is totally safe.
# In py3 __del__ calls can be delayed, thus not writing changes in time.
@@ -443,6 +452,8 @@ class Tutorials(TestBase):
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
@@ -450,19 +461,19 @@ class Tutorials(TestBase):
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
diff --git a/git/test/test_exc.py b/git/test/test_exc.py
new file mode 100644
index 00000000..33f44034
--- /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 @IgnorePep8
+ (7, "exit code(7)"), # noqa: E241 @IgnorePep8
+ ('Some string', "'Some string'"), # noqa: E241 @IgnorePep8
+ ('παλιο string', "'παλιο string'"), # noqa: E241 @IgnorePep8
+ (Exception("An exc."), "Exception('An exc.')"), # noqa: E241 @IgnorePep8
+ (Exception("Κακια exc."), "Exception('Κακια exc.')"), # noqa: E241 @IgnorePep8
+ (object(), "<object object at "), # noqa: E241 @IgnorePep8
+)
+
+_streams_n_substrings = (None, 'steram', 'ομορφο stream', )
+
+
+@ddt.ddt
+class TExc(TestBase):
+
+ @ddt.data(*list(itt.product(_cmd_argvs, _causes_n_substrings, _streams_n_substrings)))
+ def test_CommandError_unicode(self, case):
+ argv, (cause, subs), stream = case
+ cls = CommandError
+ c = cls(argv, cause)
+ s = str(c)
+
+ self.assertIsNotNone(c._msg)
+ self.assertIn(' cmdline: ', s)
+
+ for a in argv:
+ self.assertIn(a, s)
+
+ if not cause:
+ self.assertIn("failed!", s)
+ else:
+ self.assertIn(" failed due to:", s)
+
+ if subs is not None:
+ # Substrings (must) already contain opening `'`.
+ subs = "(?<!')%s(?!')" % re.escape(subs)
+ self.assertRegexpMatches(s, subs)
+
+ if not stream:
+ c = cls(argv, cause)
+ s = str(c)
+ self.assertNotIn(" stdout:", s)
+ self.assertNotIn(" stderr:", s)
+ else:
+ c = cls(argv, cause, stream)
+ s = str(c)
+ self.assertIn(" stderr:", s)
+ self.assertIn(stream, s)
+
+ c = cls(argv, cause, None, stream)
+ s = str(c)
+ self.assertIn(" stdout:", s)
+ self.assertIn(stream, s)
+
+ c = cls(argv, cause, stream, stream + 'no2')
+ s = str(c)
+ self.assertIn(" stderr:", s)
+ self.assertIn(stream, s)
+ self.assertIn(" stdout:", s)
+ self.assertIn(stream + 'no2', s)
+
+ @ddt.data(
+ (['cmd1'], None),
+ (['cmd1'], "some cause"),
+ (['cmd1'], Exception()),
+ )
+ def test_GitCommandNotFound(self, init_args):
+ argv, cause = init_args
+ c = GitCommandNotFound(argv, cause)
+ s = str(c)
+
+ self.assertIn(argv[0], s)
+ if cause:
+ self.assertIn(' not found due to: ', s)
+ self.assertIn(str(cause), s)
+ else:
+ self.assertIn(' not found!', s)
+
+ @ddt.data(
+ (['cmd1'], None),
+ (['cmd1'], "some cause"),
+ (['cmd1'], Exception()),
+ )
+ def test_GitCommandError(self, init_args):
+ argv, cause = init_args
+ c = GitCommandError(argv, cause)
+ s = str(c)
+
+ self.assertIn(argv[0], s)
+ if cause:
+ self.assertIn(' failed due to: ', s)
+ self.assertIn(str(cause), s)
+ else:
+ self.assertIn(' failed!', s)
+
+ @ddt.data(
+ (['cmd1'], None),
+ (['cmd1'], "some cause"),
+ (['cmd1'], Exception()),
+ )
+ def test_HookExecutionError(self, init_args):
+ argv, cause = init_args
+ c = HookExecutionError(argv, cause)
+ s = str(c)
+
+ self.assertIn(argv[0], s)
+ if cause:
+ self.assertTrue(s.startswith('Hook('), s)
+ self.assertIn(str(cause), s)
+ else:
+ self.assertIn(' failed!', s)
diff --git a/git/test/test_git.py b/git/test/test_git.py
index b46ac72d..58ee8e9c 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 (
@@ -22,11 +21,18 @@ from git import (
Git,
GitCommandError,
GitCommandNotFound,
- Repo
+ Repo,
+ cmd
)
-from gitdb.test.lib import with_rw_directory
+from git.test.lib import with_rw_directory
-from git.compat import PY3
+from git.compat import PY3, is_darwin
+from git.util import finalize_process
+
+try:
+ from unittest import mock
+except ImportError:
+ import mock
class TestGit(TestBase):
@@ -36,6 +42,10 @@ class TestGit(TestBase):
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 = ''
@@ -76,17 +86,16 @@ class TestGit(TestBase):
# 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"]))
def test_it_accepts_stdin(self):
filename = fixture_path("cat_file_blob")
- fh = open(filename, 'r')
- assert_equal("70c379b63ffa0795fdbfbc128e5a2818397b7ef8",
- self.git.hash_object(istream=fh, stdin=True))
- fh.close()
+ with open(filename, 'r') as fh:
+ assert_equal("70c379b63ffa0795fdbfbc128e5a2818397b7ef8",
+ self.git.hash_object(istream=fh, stdin=True))
@patch.object(Git, 'execute')
def test_it_ignores_false_kwargs(self, git):
@@ -108,28 +117,29 @@ class TestGit(TestBase):
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])
- data = g.stdout.read(size)
+ g.stdout.read(size)
g.stdout.read(1)
# 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
+ hexsha, typename_two, size_two, data = self.git.get_object_data(hexsha) # @UnusedVariable
+ 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):
@@ -164,36 +174,35 @@ class TestGit(TestBase):
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
+ with mock.patch.dict('os.environ', {'GIT_EDITOR': editor}): # @UndefinedVariable
+ 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')
- stream.write("#!/usr/bin/env sh\n" +
- "echo FOO\n")
- stream.close()
- os.chmod(path, 0o555)
+ with open(path, 'wt') as stream:
+ stream.write("#!/usr/bin/env sh\n"
+ "echo FOO\n")
+ os.chmod(path, 0o777)
rw_repo = Repo.init(os.path.join(rw_dir, 'repo'))
remote = rw_repo.create_remote('ssh-origin', "ssh://git@server/foo")
@@ -205,14 +214,11 @@ class TestGit(TestBase):
try:
remote.fetch()
except GitCommandError as err:
- if sys.version_info[0] < 3 and sys.platform == 'darwin':
- assert 'ssh-origin' 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)
- # end
- # end
- # end if select.poll exists
+ self.assertIn('FOO', str(err))
def test_handle_process_output(self):
from git.cmd import handle_process_output
@@ -226,13 +232,16 @@ class TestGit(TestBase):
def counter_stderr(line):
count[2] += 1
- proc = subprocess.Popen([sys.executable, fixture_path('cat_file.py'), str(fixture_path('issue-301_stderr'))],
+ cmdline = [sys.executable, fixture_path('cat_file.py'), str(fixture_path('issue-301_stderr'))]
+ proc = subprocess.Popen(cmdline,
stdin=None,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
- shell=False)
+ shell=False,
+ creationflags=cmd.PROC_CREATIONFLAGS,
+ )
- handle_process_output(proc, counter_stdout, counter_stderr, lambda proc: proc.wait())
+ handle_process_output(proc, counter_stdout, counter_stderr, finalize_process)
- assert count[1] == line_count
- assert count[2] == line_count
+ self.assertEqual(count[1], line_count)
+ self.assertEqual(count[2], line_count)
diff --git a/git/test/test_index.py b/git/test/test_index.py
index ca877838..34014064 100644
--- a/git/test/test_index.py
+++ b/git/test/test_index.py
@@ -5,17 +5,16 @@
# This module is part of GitPython and is released under
# the BSD License: http://www.opensource.org/licenses/bsd-license.php
-from git.test.lib import (
- TestBase,
- fixture_path,
- fixture,
- with_rw_repo
-)
-from git.util import Actor
-from git.exc import (
- HookExecutionError,
- InvalidGitRepositoryError
+from io import BytesIO
+import os
+from stat import (
+ S_ISLNK,
+ ST_MODE
)
+import sys
+import tempfile
+from unittest.case import skipIf
+
from git import (
IndexFile,
Repo,
@@ -27,26 +26,28 @@ from git import (
GitCommandError,
CheckoutError,
)
-from git.compat import string_types
-from gitdb.util import hex_to_bin
-import os
-import sys
-import tempfile
-import shutil
-from stat import (
- S_ISLNK,
- ST_MODE
+from git.compat import string_types, is_win
+from git.exc import (
+ HookExecutionError,
+ InvalidGitRepositoryError
)
-
-from io import BytesIO
-from gitdb.base import IStream
-from git.objects import Blob
+from git.index.fun import hook_path
from git.index.typ import (
BaseIndexEntry,
IndexEntry
)
-from git.index.fun import hook_path
-from gitdb.test.lib import with_rw_directory
+from git.objects import Blob
+from git.test.lib import (
+ TestBase,
+ fixture_path,
+ fixture,
+ with_rw_repo
+)
+from git.test.lib.helper import HIDE_WINDOWS_KNOWN_ERRORS
+from git.test.lib import with_rw_directory
+from git.util import Actor, rmtree
+from gitdb.base import IStream
+from gitdb.util import hex_to_bin
class TestIndex(TestBase):
@@ -56,9 +57,9 @@ class TestIndex(TestBase):
self._reset_progress()
def _assert_fprogress(self, entries):
- assert len(entries) == len(self._fprogress_map)
- for path, call_count in self._fprogress_map.items():
- assert call_count == 2
+ self.assertEqual(len(entries), len(self._fprogress_map))
+ for path, call_count in self._fprogress_map.items(): # @UnusedVariable
+ self.assertEqual(call_count, 2)
# END for each item in progress map
self._reset_progress()
@@ -108,15 +109,14 @@ class TestIndex(TestBase):
# 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 original
tmpfile = tempfile.mktemp()
index_merge.write(tmpfile)
- fp = open(tmpfile, 'rb')
- assert fp.read() == fixture("index_merge")
- fp.close()
+ with open(tmpfile, 'rb') as fp:
+ self.assertEqual(fp.read(), fixture("index_merge"))
os.remove(tmpfile)
def _cmp_tree_index(self, tree, index):
@@ -137,7 +137,30 @@ class TestIndex(TestBase):
# END assertion message
@with_rw_repo('0.1.6')
+ def test_index_lock_handling(self, rw_repo):
+ def add_bad_blob():
+ rw_repo.index.add([Blob(rw_repo, b'f' * 20, 'bad-permissions', 'foo')])
+
+ try:
+ ## 1st fail on purpose adding into index.
+ add_bad_blob()
+ except Exception as ex:
+ msg_py3 = "required argument is not an integer"
+ msg_py2 = "cannot convert argument to integer"
+ ## 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:
+ add_bad_blob()
+ except Exception as ex:
+ assert "index.lock' could not be obtained" not in str(ex)
+
+ @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_ancestor_sha = "5117c9c8a4d3af19a9958677e45cda9269de1541"
cur_sha = "4b43ca7ff72d5f535134241e7c797ddc9c7a3573"
other_sha = "39f85c4358b7346fee22169da9cad93901ea9eb9"
@@ -165,7 +188,7 @@ class TestIndex(TestBase):
# test BlobFilter
prefix = 'lib/git'
- for stage, blob in base_index.iter_blobs(BlobFilter([prefix])):
+ for stage, blob in base_index.iter_blobs(BlobFilter([prefix])): # @UnusedVariable
assert blob.path.startswith(prefix)
# writing a tree should fail with an unmerged index
@@ -184,13 +207,13 @@ class TestIndex(TestBase):
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
@@ -203,7 +226,7 @@ class TestIndex(TestBase):
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
#############
@@ -221,7 +244,7 @@ class TestIndex(TestBase):
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()
@@ -245,7 +268,8 @@ class TestIndex(TestBase):
# 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):
@@ -267,11 +291,11 @@ class TestIndex(TestBase):
# 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)
@@ -281,7 +305,7 @@ class TestIndex(TestBase):
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)
@@ -297,8 +321,8 @@ class TestIndex(TestBase):
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)
@@ -306,22 +330,19 @@ class TestIndex(TestBase):
# reset the working copy as well to current head,to pull 'back' as well
new_data = b"will be reverted"
file_path = os.path.join(rw_repo.working_tree_dir, "CHANGES")
- fp = open(file_path, "wb")
- fp.write(new_data)
- fp.close()
+ with open(file_path, "wb") as fp:
+ fp.write(new_data)
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
- fp = open(file_path, 'rb')
- try:
+ self.assertEqual(cur_branch, rw_repo.active_branch)
+ self.assertEqual(cur_commit, rw_repo.head.commit)
+ with open(file_path, 'rb') as fp:
assert fp.read() != new_data
- finally:
- fp.close()
# test full checkout
test_file = os.path.join(rw_repo.working_tree_dir, "CHANGES")
- open(test_file, 'ab').write(b"some data")
+ with open(test_file, 'ab') as fd:
+ fd.write(b"some data")
rval = index.checkout(None, force=True, fprogress=self._fprogress)
assert 'CHANGES' in list(rval)
self._assert_fprogress([None])
@@ -336,7 +357,7 @@ class TestIndex(TestBase):
# 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)
@@ -346,16 +367,19 @@ class TestIndex(TestBase):
# checkout file with modifications
append_data = b"hello"
- fp = open(test_file, "ab")
- fp.write(append_data)
- fp.close()
+ with open(test_file, "ab") as fp:
+ fp.write(append_data)
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
- assert open(test_file, 'rb').read().endswith(append_data)
+ 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)
+ with open(test_file, 'rb') as fd:
+ s = fd.read()
+ self.assertTrue(s.endswith(append_data), s)
else:
raise AssertionError("Exception CheckoutError not thrown")
@@ -364,7 +388,7 @@ class TestIndex(TestBase):
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
@@ -388,11 +412,10 @@ class TestIndex(TestBase):
uname = u"Thomas Müller"
umail = "sd@company.com"
- writer = rw_repo.config_writer()
- writer.set_value("user", "name", uname)
- writer.set_value("user", "email", umail)
- writer.release()
- assert writer.get_value("user", "name") == uname
+ with rw_repo.config_writer() as writer:
+ writer.set_value("user", "name", uname)
+ writer.set_value("user", "email", umail)
+ self.assertEqual(writer.get_value("user", "name"), uname)
# remove all of the files, provide a wild mix of paths, BaseIndexEntries,
# IndexEntries
@@ -415,21 +438,21 @@ class TestIndex(TestBase):
# 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])
@@ -446,14 +469,14 @@ class TestIndex(TestBase):
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
@@ -462,15 +485,15 @@ class TestIndex(TestBase):
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
@@ -479,25 +502,25 @@ class TestIndex(TestBase):
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
@@ -516,17 +539,17 @@ class TestIndex(TestBase):
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'])
@@ -536,7 +559,8 @@ class TestIndex(TestBase):
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
@@ -551,23 +575,25 @@ class TestIndex(TestBase):
[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 sys.platform != "win32":
+ if not is_win:
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)
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
@@ -582,7 +608,8 @@ class TestIndex(TestBase):
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)))
@@ -607,12 +634,13 @@ class TestIndex(TestBase):
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
+ with open(fake_symlink_path, 'rt') as fd:
+ self.assertEqual(fd.read(), link_target)
else:
- assert S_ISLNK(os.lstat(fake_symlink_path)[ST_MODE])
+ self.assertTrue(S_ISLNK(os.lstat(fake_symlink_path)[ST_MODE]))
# TEST RENAMING
def assert_mv_rval(rval):
@@ -632,7 +660,7 @@ class TestIndex(TestBase):
# 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
@@ -662,7 +690,8 @@ class TestIndex(TestBase):
for fid in range(3):
fname = 'newfile%i' % fid
- open(fname, 'wb').write(b"abcd")
+ with open(fname, 'wb') as fd:
+ fd.write(b"abcd")
yield Blob(rw_repo, Blob.NULL_BIN_SHA, 0o100644, fname)
# END for each new file
# END path producer
@@ -688,7 +717,7 @@ class TestIndex(TestBase):
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):
@@ -696,15 +725,22 @@ class TestIndex(TestBase):
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:
- 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, OSError)
+ self.assertEqual(err.command, [hp])
+ self.assertEqual(err.stdout, '')
+ self.assertEqual(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
@@ -744,7 +780,7 @@ class TestIndex(TestBase):
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):
@@ -786,6 +822,10 @@ class TestIndex(TestBase):
asserted = True
assert asserted, "Adding using a filename is not correctly asserted."
+ @skipIf(HIDE_WINDOWS_KNOWN_ERRORS and sys.version_info[:2] == (2, 7), r"""
+ FIXME: File "C:\projects\gitpython\git\util.py", line 125, in to_native_path_linux
+ return path.replace('\\', '/')
+ UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 0: ordinal not in range(128)""")
@with_rw_directory
def test_add_utf8P_path(self, rw_dir):
# NOTE: fp is not a Unicode object in python 2 (which is the source of the problem)
diff --git a/git/test/test_reflog.py b/git/test/test_reflog.py
index 3571e083..dffedf3b 100644
--- a/git/test/test_reflog.py
+++ b/git/test/test_reflog.py
@@ -7,11 +7,10 @@ from git.refs import (
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 @@ class TestRefLog(TestBase):
# END for each reflog
# finally remove our temporary data
- shutil.rmtree(tdir)
+ rmtree(tdir)
diff --git a/git/test/test_refs.py b/git/test/test_refs.py
index 9816fb50..43f1dcc7 100644
--- a/git/test/test_refs.py
+++ b/git/test/test_refs.py
@@ -101,15 +101,13 @@ class TestRefs(TestBase):
assert prev_object == cur_object # represent the same git object
assert prev_object is not cur_object # but are different instances
- writer = head.config_writer()
- tv = "testopt"
- writer.set_value(tv, 1)
- assert writer.get_value(tv) == 1
- writer.release()
+ with head.config_writer() as writer:
+ tv = "testopt"
+ writer.set_value(tv, 1)
+ assert writer.get_value(tv) == 1
assert head.config_reader().get_value(tv) == 1
- writer = head.config_writer()
- writer.remove_option(tv)
- writer.release()
+ with head.config_writer() as writer:
+ writer.remove_option(tv)
# after the clone, we might still have a tracking branch setup
head.set_tracking_branch(None)
@@ -175,7 +173,7 @@ class TestRefs(TestBase):
def test_orig_head(self):
assert type(self.rorepo.head.orig_head()) == SymbolicReference
-
+
@with_rw_repo('0.1.6')
def test_head_checkout_detached_head(self, rw_repo):
res = rw_repo.remotes.origin.refs.master.checkout()
@@ -282,7 +280,7 @@ class TestRefs(TestBase):
# tag ref
tag_name = "5.0.2"
- light_tag = TagReference.create(rw_repo, tag_name)
+ TagReference.create(rw_repo, tag_name)
self.failUnlessRaises(GitCommandError, TagReference.create, rw_repo, tag_name)
light_tag = TagReference.create(rw_repo, tag_name, "HEAD~1", force=True)
assert isinstance(light_tag, TagReference)
@@ -442,7 +440,7 @@ class TestRefs(TestBase):
self.failUnlessRaises(OSError, SymbolicReference.create, rw_repo, symref_path, cur_head.reference.commit)
# it works if the new ref points to the same reference
- SymbolicReference.create(rw_repo, symref.path, symref.reference).path == symref.path
+ SymbolicReference.create(rw_repo, symref.path, symref.reference).path == symref.path # @NoEffect
SymbolicReference.delete(rw_repo, symref)
# would raise if the symref wouldn't have been deletedpbl
symref = SymbolicReference.create(rw_repo, symref_path, cur_head.reference)
diff --git a/git/test/test_remote.py b/git/test/test_remote.py
index 3c2e622d..7b52ccce 100644
--- a/git/test/test_remote.py
+++ b/git/test/test_remote.py
@@ -25,10 +25,9 @@ from git import (
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
@@ -91,7 +90,7 @@ class TestRemoteProgress(RemoteProgress):
assert self._stages_per_op
# must have seen all stages
- for op, stages in self._stages_per_op.items():
+ for op, stages in self._stages_per_op.items(): # @UnusedVariable
assert stages & self.STAGE_MASK == self.STAGE_MASK
# END for each op/stage
@@ -101,9 +100,13 @@ class TestRemoteProgress(RemoteProgress):
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()
+ with open(os.path.join(repo.git_dir, "FETCH_HEAD")):
+ pass
def _do_test_fetch_result(self, results, remote):
# self._print_fetchhead(remote.repo)
@@ -264,7 +267,8 @@ class TestRemote(TestBase):
# put origin to git-url
other_origin = other_repo.remotes.origin
- other_origin.config_writer.set("url", remote_repo_url)
+ with other_origin.config_writer as cw:
+ cw.set("url", remote_repo_url)
# it automatically creates alternates as remote_repo is shared as well.
# It will use the transport though and ignore alternates when fetching
# assert not other_repo.alternates # this would fail
@@ -281,7 +285,7 @@ class TestRemote(TestBase):
# and only provides progress information to ttys
res = fetch_and_test(other_origin)
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):
@@ -328,7 +332,7 @@ class TestRemote(TestBase):
# push new tags
progress = TestRemoteProgress()
to_be_updated = "my_tag.1.0RV"
- new_tag = TagReference.create(rw_repo, to_be_updated)
+ new_tag = TagReference.create(rw_repo, to_be_updated) # @UnusedVariable
other_tag = TagReference.create(rw_repo, "my_obj_tag.2.1aRV", message="my message")
res = remote.push(progress=progress, tags=True)
assert res[-1].flags & PushInfo.NEW_TAG
@@ -403,7 +407,7 @@ class TestRemote(TestBase):
# 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
@@ -413,13 +417,12 @@ class TestRemote(TestBase):
self.failUnlessRaises(IOError, reader.set, opt, "test")
# change value
- writer = remote.config_writer
- new_val = "myval"
- writer.set(opt, new_val)
- assert writer.get(opt) == new_val
- writer.set(opt, val)
- assert writer.get(opt) == val
- del(writer)
+ with remote.config_writer as writer:
+ new_val = "myval"
+ writer.set(opt, new_val)
+ assert writer.get(opt) == new_val
+ writer.set(opt, val)
+ assert writer.get(opt) == val
assert getattr(remote, opt) == val
# END for each default option key
@@ -429,7 +432,7 @@ class TestRemote(TestBase):
assert remote.rename(other_name) == remote
assert prev_name != remote.name
# multiple times
- for time in range(2):
+ for _ in range(2):
assert remote.rename(prev_name).name == prev_name
# END for each rename ( back to prev_name )
diff --git a/git/test/test_repo.py b/git/test/test_repo.py
index e24062c1..1d537e93 100644
--- a/git/test/test_repo.py
+++ b/git/test/test_repo.py
@@ -4,18 +4,15 @@
#
# This module is part of GitPython and is released under
# the BSD License: http://www.opensource.org/licenses/bsd-license.php
+import glob
+from io import BytesIO
+import itertools
+import os
import pickle
+import sys
+import tempfile
+from unittest.case import skipIf
-from git.test.lib import (
- patch,
- TestBase,
- with_rw_repo,
- fixture,
- assert_false,
- assert_equal,
- assert_true,
- raises
-)
from git import (
InvalidGitRepositoryError,
Repo,
@@ -33,24 +30,35 @@ from git import (
BadName,
GitCommandError
)
-from git.repo.fun import touch
-from git.util import join_path_native
+from git.compat import (
+ PY3,
+ is_win,
+ string_types,
+ win_encode,
+)
from git.exc import (
BadObject,
)
+from git.repo.fun import touch
+from git.test.lib import (
+ patch,
+ TestBase,
+ with_rw_repo,
+ fixture,
+ assert_false,
+ assert_equal,
+ assert_true,
+ raises
+)
+from git.test.lib.helper import HIDE_WINDOWS_KNOWN_ERRORS
+from git.test.lib import with_rw_directory
+from git.util import join_path_native, rmtree, rmfile
from gitdb.util import bin_to_hex
-from git.compat import string_types
-from gitdb.test.lib import with_rw_directory
-
-import os
-import sys
-import tempfile
-import shutil
-import itertools
-from io import BytesIO
-
from nose import SkipTest
+import functools as fnt
+import os.path as osp
+
def iter_flatten(lol):
for items in lol:
@@ -62,8 +70,26 @@ def flatten(lol):
return list(iter_flatten(lol))
+_tc_lock_fpaths = osp.join(osp.dirname(__file__), '../../.git/*.lock')
+
+
+def _rm_lock_files():
+ for lfp in glob.glob(_tc_lock_fpaths):
+ rmfile(lfp)
+
+
class TestRepo(TestBase):
+ def setUp(self):
+ _rm_lock_files()
+
+ def tearDown(self):
+ for lfp in glob.glob(_tc_lock_fpaths):
+ if osp.isfile(lfp):
+ raise AssertionError('Previous TC left hanging git-lock file: %s', lfp)
+ import gc
+ gc.collect()
+
@raises(InvalidGitRepositoryError)
def test_new_should_raise_on_invalid_repo_location(self):
Repo(tempfile.gettempdir())
@@ -75,10 +101,10 @@ class TestRepo(TestBase):
@with_rw_repo('0.3.2.1')
def test_repo_creation_from_different_paths(self, rw_repo):
r_from_gitdir = Repo(rw_repo.git_dir)
- assert r_from_gitdir.git_dir == rw_repo.git_dir
+ self.assertEqual(r_from_gitdir.git_dir, rw_repo.git_dir)
assert r_from_gitdir.git_dir.endswith('.git')
assert not rw_repo.git.working_dir.endswith('.git')
- assert r_from_gitdir.git.working_dir == rw_repo.git.working_dir
+ self.assertEqual(r_from_gitdir.git.working_dir, rw_repo.git.working_dir)
def test_description(self):
txt = "Test repository"
@@ -92,32 +118,33 @@ class TestRepo(TestBase):
def test_heads_should_populate_head_data(self):
for head in self.rorepo.heads:
assert head.name
- assert isinstance(head.commit, Commit)
+ self.assertIsInstance(head.commit, Commit)
# END for each head
- assert isinstance(self.rorepo.heads.master, Head)
- assert isinstance(self.rorepo.heads['master'], Head)
+ self.assertIsInstance(self.rorepo.heads.master, Head)
+ self.assertIsInstance(self.rorepo.heads['master'], Head)
def test_tree_from_revision(self):
tree = self.rorepo.tree('0.1.6')
- assert len(tree.hexsha) == 40
- assert tree.type == "tree"
- assert self.rorepo.tree(tree) == tree
+ self.assertEqual(len(tree.hexsha), 40)
+ self.assertEqual(tree.type, "tree")
+ self.assertEqual(self.rorepo.tree(tree), tree)
# 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))
def test_commit_from_revision(self):
commit = self.rorepo.commit('0.1.4')
- assert commit.type == 'commit'
- assert self.rorepo.commit(commit) == commit
+ self.assertEqual(commit.type, 'commit')
+ self.assertEqual(self.rorepo.commit(commit), commit)
+ def test_commits(self):
mc = 10
commits = list(self.rorepo.iter_commits('0.1.6', max_count=mc))
- assert len(commits) == mc
+ self.assertEqual(len(commits), mc)
c = commits[0]
assert_equal('9a4b1d4d11eee3c5362a4152216376e634bd14cf', c.hexsha)
@@ -134,23 +161,23 @@ class TestRepo(TestBase):
assert_equal("Bumped version 0.1.6\n", c.message)
c = commits[1]
- assert isinstance(c.parents, tuple)
+ self.assertIsInstance(c.parents, tuple)
def test_trees(self):
mc = 30
num_trees = 0
for tree in self.rorepo.iter_trees('0.1.5', max_count=mc):
num_trees += 1
- assert isinstance(tree, Tree)
+ self.assertIsInstance(tree, Tree)
# END for each tree
- assert num_trees == mc
+ self.assertEqual(num_trees, mc)
def _assert_empty_repo(self, repo):
# test all kinds of things with an empty, freshly initialized repo.
# It should throw good errors
# entries should be empty
- assert len(repo.index.entries) == 0
+ self.assertEqual(len(repo.index.entries), 0)
# head is accessible
assert repo.head
@@ -182,7 +209,7 @@ class TestRepo(TestBase):
# with specific path
for path in (git_dir_rela, git_dir_abs):
r = Repo.init(path=path, bare=True)
- assert isinstance(r, Repo)
+ self.assertIsInstance(r, Repo)
assert r.bare is True
assert not r.has_separate_working_tree()
assert os.path.isdir(r.git_dir)
@@ -195,7 +222,7 @@ class TestRepo(TestBase):
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
@@ -206,9 +233,9 @@ class TestRepo(TestBase):
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
@@ -226,7 +253,7 @@ class TestRepo(TestBase):
self._assert_empty_repo(r)
finally:
try:
- shutil.rmtree(del_dir_abs)
+ rmtree(del_dir_abs)
except OSError:
pass
os.chdir(prev_cwd)
@@ -238,18 +265,18 @@ class TestRepo(TestBase):
def test_daemon_export(self):
orig_val = self.rorepo.daemon_export
self.rorepo.daemon_export = not orig_val
- assert self.rorepo.daemon_export == (not orig_val)
+ self.assertEqual(self.rorepo.daemon_export, (not orig_val))
self.rorepo.daemon_export = orig_val
- assert self.rorepo.daemon_export == orig_val
+ self.assertEqual(self.rorepo.daemon_export, orig_val)
def test_alternates(self):
cur_alternates = self.rorepo.alternates
# empty alternates
self.rorepo.alternates = []
- assert self.rorepo.alternates == []
+ self.assertEqual(self.rorepo.alternates, [])
alts = ["other/location", "this/location"]
self.rorepo.alternates = alts
- assert alts == self.rorepo.alternates
+ self.assertEqual(alts, self.rorepo.alternates)
self.rorepo.alternates = cur_alternates
def test_repr(self):
@@ -294,25 +321,27 @@ class TestRepo(TestBase):
assert rwrepo.is_dirty(untracked_files=True, path="doc") is True
def test_head(self):
- assert self.rorepo.head.reference.object == self.rorepo.active_branch.object
+ self.assertEqual(self.rorepo.head.reference.object, self.rorepo.active_branch.object)
def test_index(self):
index = self.rorepo.index
- assert isinstance(index, IndexFile)
+ self.assertIsInstance(index, IndexFile)
def test_tag(self):
assert self.rorepo.tag('refs/tags/0.1.5').commit
def test_archive(self):
tmpfile = tempfile.mktemp(suffix='archive-test')
- stream = open(tmpfile, 'wb')
- self.rorepo.archive(stream, '0.1.6', path='doc')
- assert stream.tell()
- stream.close()
+ with open(tmpfile, 'wb') as stream:
+ self.rorepo.archive(stream, '0.1.6', path='doc')
+ assert stream.tell()
os.remove(tmpfile)
@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))
@@ -340,7 +369,7 @@ class TestRepo(TestBase):
# BINARY BLAME
git.return_value = fixture('blame_binary')
blames = self.rorepo.blame('master', 'rps')
- assert len(blames) == 2
+ self.assertEqual(len(blames), 2)
def test_blame_real(self):
c = 0
@@ -360,32 +389,35 @@ class TestRepo(TestBase):
git.return_value = fixture('blame_incremental')
blame_output = self.rorepo.blame_incremental('9debf6b0aafb6f7781ea9d1383c86939a1aacde3', 'AUTHORS')
blame_output = list(blame_output)
- assert len(blame_output) == 5
+ self.assertEqual(len(blame_output), 5)
# Check all outputted line numbers
ranges = flatten([entry.linenos for entry in blame_output])
- assert ranges == flatten([range(2, 3), range(14, 15), range(1, 2), range(3, 14), range(15, 17)]), str(ranges)
+ self.assertEqual(ranges, flatten([range(2, 3), range(14, 15), range(1, 2), range(3, 14), range(15, 17)]))
commits = [entry.commit.hexsha[:7] for entry in blame_output]
- assert commits == ['82b8902', '82b8902', 'c76852d', 'c76852d', 'c76852d'], str(commits)
+ self.assertEqual(commits, ['82b8902', '82b8902', 'c76852d', 'c76852d', 'c76852d'])
# Original filenames
- assert all([entry.orig_path == u'AUTHORS' for entry in blame_output])
+ self.assertSequenceEqual([entry.orig_path for entry in blame_output], [u'AUTHORS'] * len(blame_output))
# Original line numbers
orig_ranges = flatten([entry.orig_linenos for entry in blame_output])
- assert orig_ranges == flatten([range(2, 3), range(14, 15), range(1, 2), range(2, 13), range(13, 15)]), str(orig_ranges) # noqa
+ self.assertEqual(orig_ranges, flatten([range(2, 3), range(14, 15), range(1, 2), range(2, 13), range(13, 15)])) # noqa E501
@patch.object(Git, '_call_process')
def test_blame_complex_revision(self, git):
git.return_value = fixture('blame_complex_revision')
res = self.rorepo.blame("HEAD~10..HEAD", "README.md")
- assert len(res) == 1
- assert len(res[0][1]) == 83, "Unexpected amount of parsed blame lines"
+ self.assertEqual(len(res), 1)
+ self.assertEqual(len(res[0][1]), 83, "Unexpected amount of parsed blame lines")
@with_rw_repo('HEAD', bare=False)
def test_untracked_files(self, rwrepo):
- for (run, repo_add) in enumerate((rwrepo.index.add, rwrepo.git.add)):
+ for run, (repo_add, is_invoking_git) in enumerate((
+ (rwrepo.index.add, False),
+ (rwrepo.git.add, True),
+ )):
base = rwrepo.working_tree_dir
files = (join_path_native(base, u"%i_test _myfile" % run),
join_path_native(base, "%i_test_other_file" % run),
@@ -394,9 +426,8 @@ class TestRepo(TestBase):
num_recently_untracked = 0
for fpath in files:
- fd = open(fpath, "wb")
- fd.close()
- # END for each filename
+ with open(fpath, "wb"):
+ pass
untracked_files = rwrepo.untracked_files
num_recently_untracked = len(untracked_files)
@@ -404,10 +435,15 @@ class TestRepo(TestBase):
num_test_untracked = 0
for utfile in untracked_files:
num_test_untracked += join_path_native(base, utfile) in files
- assert len(files) == num_test_untracked
+ self.assertEqual(len(files), num_test_untracked)
+ if is_win and not PY3 and is_invoking_git:
+ ## On Windows, shell needed when passing unicode cmd-args.
+ #
+ repo_add = fnt.partial(repo_add, shell=True)
+ untracked_files = [win_encode(f) for f in untracked_files]
repo_add(untracked_files)
- assert len(rwrepo.untracked_files) == (num_recently_untracked - len(files))
+ self.assertEqual(len(rwrepo.untracked_files), (num_recently_untracked - len(files)))
# end for each run
def test_config_reader(self):
@@ -419,19 +455,16 @@ class TestRepo(TestBase):
def test_config_writer(self):
for config_level in self.rorepo.config_level:
try:
- writer = self.rorepo.config_writer(config_level)
- assert not writer.read_only
- writer.release()
+ with self.rorepo.config_writer(config_level) as writer:
+ self.assertFalse(writer.read_only)
except IOError:
# its okay not to get a writer for some configuration files if we
# have no permissions
pass
- # END for each config level
def test_config_level_paths(self):
for config_level in self.rorepo.config_level:
assert self.rorepo._get_config_path(config_level)
- # end for each config level
def test_creation_deletion(self):
# just a very quick test to assure it generally works. There are
@@ -441,19 +474,20 @@ class TestRepo(TestBase):
tag = self.rorepo.create_tag("new_tag", "HEAD~2")
self.rorepo.delete_tag(tag)
- writer = self.rorepo.config_writer()
- writer.release()
+ with self.rorepo.config_writer():
+ pass
remote = self.rorepo.create_remote("new_remote", "git@server:repo.git")
self.rorepo.delete_remote(remote)
def test_comparison_and_hash(self):
# this is only a preliminary test, more testing done in test_index
- assert self.rorepo == self.rorepo and not (self.rorepo != self.rorepo)
- assert len(set((self.rorepo, self.rorepo))) == 1
+ self.assertEqual(self.rorepo, self.rorepo)
+ self.assertFalse(self.rorepo != self.rorepo)
+ self.assertEqual(len(set((self.rorepo, self.rorepo))), 1)
@with_rw_directory
def test_tilde_and_env_vars_in_repo_path(self, rw_dir):
- ph = os.environ['HOME']
+ ph = os.environ.get('HOME')
try:
os.environ['HOME'] = rw_dir
Repo.init(os.path.join('~', 'test.git'), bare=True)
@@ -461,8 +495,9 @@ class TestRepo(TestBase):
os.environ['FOO'] = rw_dir
Repo.init(os.path.join('$FOO', 'test.git'), bare=True)
finally:
- os.environ['HOME'] = ph
- del os.environ['FOO']
+ if ph:
+ os.environ['HOME'] = ph
+ del os.environ['FOO']
# end assure HOME gets reset to what it was
def test_git_cmd(self):
@@ -488,57 +523,59 @@ class TestRepo(TestBase):
# readlines no limit
s = mkfull()
lines = s.readlines()
- assert len(lines) == 3 and lines[-1].endswith(b'\n')
- assert s._stream.tell() == len(d) # must have scrubbed to the end
+ self.assertEqual(len(lines), 3)
+ self.assertTrue(lines[-1].endswith(b'\n'), lines[-1])
+ self.assertEqual(s._stream.tell(), len(d)) # must have scrubbed to the end
# realines line limit
s = mkfull()
lines = s.readlines(5)
- assert len(lines) == 1
+ self.assertEqual(len(lines), 1)
# readlines on tiny sections
s = mktiny()
lines = s.readlines()
- assert len(lines) == 1 and lines[0] == l1p
- assert s._stream.tell() == ts + 1
+ self.assertEqual(len(lines), 1)
+ self.assertEqual(lines[0], l1p)
+ self.assertEqual(s._stream.tell(), ts + 1)
# readline no limit
s = mkfull()
- assert s.readline() == l1
- assert s.readline() == l2
- assert s.readline() == l3
- assert s.readline() == b''
- assert s._stream.tell() == len(d)
+ self.assertEqual(s.readline(), l1)
+ self.assertEqual(s.readline(), l2)
+ self.assertEqual(s.readline(), l3)
+ self.assertEqual(s.readline(), b'')
+ self.assertEqual(s._stream.tell(), len(d))
# readline limit
s = mkfull()
- assert s.readline(5) == l1p
- assert s.readline() == l1[5:]
+ self.assertEqual(s.readline(5), l1p)
+ self.assertEqual(s.readline(), l1[5:])
# readline on tiny section
s = mktiny()
- assert s.readline() == l1p
- assert s.readline() == b''
- assert s._stream.tell() == ts + 1
+ self.assertEqual(s.readline(), l1p)
+ self.assertEqual(s.readline(), b'')
+ self.assertEqual(s._stream.tell(), ts + 1)
# read no limit
s = mkfull()
- assert s.read() == d[:-1]
- assert s.read() == b''
- assert s._stream.tell() == len(d)
+ self.assertEqual(s.read(), d[:-1])
+ self.assertEqual(s.read(), b'')
+ self.assertEqual(s._stream.tell(), len(d))
# read limit
s = mkfull()
- assert s.read(5) == l1p
- assert s.read(6) == l1[5:]
- assert s._stream.tell() == 5 + 6 # its not yet done
+ self.assertEqual(s.read(5), l1p)
+ self.assertEqual(s.read(6), l1[5:])
+ self.assertEqual(s._stream.tell(), 5 + 6) # its not yet done
# read tiny
s = mktiny()
- assert s.read(2) == l1[:2]
- assert s._stream.tell() == 2
- assert s.read() == l1[2:ts]
- assert s._stream.tell() == ts + 1
+ self.assertEqual(s.read(2), l1[:2])
+ self.assertEqual(s._stream.tell(), 2)
+ self.assertEqual(s.read(), l1[2:ts])
+ self.assertEqual(s._stream.tell(), ts + 1)
def _assert_rev_parse_types(self, name, rev_obj):
rev_parse = self.rorepo.rev_parse
@@ -548,11 +585,12 @@ class TestRepo(TestBase):
# tree and blob type
obj = rev_parse(name + '^{tree}')
- assert obj == rev_obj.tree
+ self.assertEqual(obj, rev_obj.tree)
obj = rev_parse(name + ':CHANGES')
- assert obj.type == 'blob' and obj.path == 'CHANGES'
- assert rev_obj.tree['CHANGES'] == obj
+ self.assertEqual(obj.type, 'blob')
+ self.assertEqual(obj.path, 'CHANGES')
+ self.assertEqual(rev_obj.tree['CHANGES'], obj)
def _assert_rev_parse(self, name):
"""tries multiple different rev-parse syntaxes with the given name
@@ -568,7 +606,7 @@ class TestRepo(TestBase):
# try history
rev = name + "~"
obj2 = rev_parse(rev)
- assert obj2 == obj.parents[0]
+ self.assertEqual(obj2, obj.parents[0])
self._assert_rev_parse_types(rev, obj2)
# history with number
@@ -581,20 +619,20 @@ class TestRepo(TestBase):
for pn in range(11):
rev = name + "~%i" % (pn + 1)
obj2 = rev_parse(rev)
- assert obj2 == history[pn]
+ self.assertEqual(obj2, history[pn])
self._assert_rev_parse_types(rev, obj2)
# END history check
# parent ( default )
rev = name + "^"
obj2 = rev_parse(rev)
- assert obj2 == obj.parents[0]
+ self.assertEqual(obj2, obj.parents[0])
self._assert_rev_parse_types(rev, obj2)
# parent with number
for pn, parent in enumerate(obj.parents):
rev = name + "^%i" % (pn + 1)
- assert rev_parse(rev) == parent
+ self.assertEqual(rev_parse(rev), parent)
self._assert_rev_parse_types(rev, parent)
# END for each parent
@@ -610,7 +648,7 @@ class TestRepo(TestBase):
rev_parse = self.rorepo.rev_parse
# try special case: This one failed at some point, make sure its fixed
- assert rev_parse("33ebe").hexsha == "33ebe7acec14b25c5f84f35a664803fcab2f7781"
+ self.assertEqual(rev_parse("33ebe").hexsha, "33ebe7acec14b25c5f84f35a664803fcab2f7781")
# start from reference
num_resolved = 0
@@ -621,7 +659,7 @@ class TestRepo(TestBase):
path_section = '/'.join(path_tokens[-(pt + 1):])
try:
obj = self._assert_rev_parse(path_section)
- assert obj.type == ref.object.type
+ self.assertEqual(obj.type, ref.object.type)
num_resolved += 1
except (BadName, BadObject):
print("failed on %s" % path_section)
@@ -636,31 +674,31 @@ class TestRepo(TestBase):
# it works with tags !
tag = self._assert_rev_parse('0.1.4')
- assert tag.type == 'tag'
+ self.assertEqual(tag.type, 'tag')
# try full sha directly ( including type conversion )
- assert tag.object == rev_parse(tag.object.hexsha)
+ self.assertEqual(tag.object, rev_parse(tag.object.hexsha))
self._assert_rev_parse_types(tag.object.hexsha, tag.object)
# multiple tree types result in the same tree: HEAD^{tree}^{tree}:CHANGES
rev = '0.1.4^{tree}^{tree}'
- assert rev_parse(rev) == tag.object.tree
- assert rev_parse(rev + ':CHANGES') == tag.object.tree['CHANGES']
+ self.assertEqual(rev_parse(rev), tag.object.tree)
+ self.assertEqual(rev_parse(rev + ':CHANGES'), tag.object.tree['CHANGES'])
# try to get parents from first revision - it should fail as no such revision
# exists
first_rev = "33ebe7acec14b25c5f84f35a664803fcab2f7781"
commit = rev_parse(first_rev)
- assert len(commit.parents) == 0
- assert commit.hexsha == first_rev
+ self.assertEqual(len(commit.parents), 0)
+ self.assertEqual(commit.hexsha, first_rev)
self.failUnlessRaises(BadName, rev_parse, first_rev + "~")
self.failUnlessRaises(BadName, rev_parse, first_rev + "^")
# short SHA1
commit2 = rev_parse(first_rev[:20])
- assert commit2 == commit
+ self.assertEqual(commit2, commit)
commit2 = rev_parse(first_rev[:5])
- assert commit2 == commit
+ self.assertEqual(commit2, commit)
# todo: dereference tag into a blob 0.1.7^{blob} - quite a special one
# needs a tag which points to a blob
@@ -668,13 +706,13 @@ class TestRepo(TestBase):
# ref^0 returns commit being pointed to, same with ref~0, and ^{}
tag = rev_parse('0.1.4')
for token in (('~0', '^0', '^{}')):
- assert tag.object == rev_parse('0.1.4%s' % token)
+ self.assertEqual(tag.object, rev_parse('0.1.4%s' % token))
# END handle multiple tokens
# try partial parsing
max_items = 40
for i, binsha in enumerate(self.rorepo.odb.sha_iter()):
- assert rev_parse(bin_to_hex(binsha)[:8 - (i % 2)].decode('ascii')).binsha == binsha
+ self.assertEqual(rev_parse(bin_to_hex(binsha)[:8 - (i % 2)].decode('ascii')).binsha, binsha)
if i > max_items:
# this is rather slow currently, as rev_parse returns an object
# which requires accessing packs, it has some additional overhead
@@ -695,13 +733,13 @@ class TestRepo(TestBase):
self.failUnlessRaises(BadObject, rev_parse, "%s@{0}" % head.commit.hexsha)
# uses HEAD.ref by default
- assert rev_parse('@{0}') == head.commit
+ self.assertEqual(rev_parse('@{0}'), head.commit)
if not head.is_detached:
refspec = '%s@{0}' % head.ref.name
- assert rev_parse(refspec) == head.ref.commit
+ self.assertEqual(rev_parse(refspec), head.ref.commit)
# all additional specs work as well
- assert rev_parse(refspec + "^{tree}") == head.commit.tree
- assert rev_parse(refspec + ":CHANGES").type == 'blob'
+ self.assertEqual(rev_parse(refspec + "^{tree}"), head.commit.tree)
+ self.assertEqual(rev_parse(refspec + ":CHANGES").type, 'blob')
# END operate on non-detached head
# position doesn't exist
@@ -717,13 +755,13 @@ class TestRepo(TestBase):
target_type = GitCmdObjectDB
if sys.version_info[:2] < (2, 5):
target_type = GitCmdObjectDB
- assert isinstance(self.rorepo.odb, target_type)
+ self.assertIsInstance(self.rorepo.odb, target_type)
def test_submodules(self):
- assert len(self.rorepo.submodules) == 1 # non-recursive
- assert len(list(self.rorepo.iter_submodules())) >= 2
+ self.assertEqual(len(self.rorepo.submodules), 1) # non-recursive
+ self.assertGreaterEqual(len(list(self.rorepo.iter_submodules())), 2)
- assert isinstance(self.rorepo.submodule("gitdb"), Submodule)
+ self.assertIsInstance(self.rorepo.submodule("gitdb"), Submodule)
self.failUnlessRaises(ValueError, self.rorepo.submodule, "doesn't exist")
@with_rw_repo('HEAD', bare=False)
@@ -736,7 +774,7 @@ class TestRepo(TestBase):
# test create submodule
sm = rwrepo.submodules[0]
sm = rwrepo.create_submodule("my_new_sub", "some_path", join_path_native(self.rorepo.working_tree_dir, sm.path))
- assert isinstance(sm, Submodule)
+ self.assertIsInstance(sm, Submodule)
# note: the rest of this functionality is tested in test_submodule
@@ -746,17 +784,21 @@ class TestRepo(TestBase):
real_path_abs = os.path.abspath(join_path_native(rwrepo.working_tree_dir, '.real'))
os.rename(rwrepo.git_dir, real_path_abs)
git_file_path = join_path_native(rwrepo.working_tree_dir, '.git')
- open(git_file_path, 'wb').write(fixture('git_file'))
+ with open(git_file_path, 'wb') as fp:
+ fp.write(fixture('git_file'))
# Create a repo and make sure it's pointing to the relocated .git directory.
git_file_repo = Repo(rwrepo.working_tree_dir)
- assert os.path.abspath(git_file_repo.git_dir) == real_path_abs
+ self.assertEqual(os.path.abspath(git_file_repo.git_dir), real_path_abs)
# Test using an absolute gitdir path in the .git file.
- open(git_file_path, 'wb').write(('gitdir: %s\n' % real_path_abs).encode('ascii'))
+ with open(git_file_path, 'wb') as fp:
+ fp.write(('gitdir: %s\n' % real_path_abs).encode('ascii'))
git_file_repo = Repo(rwrepo.working_tree_dir)
- assert os.path.abspath(git_file_repo.git_dir) == real_path_abs
+ self.assertEqual(os.path.abspath(git_file_repo.git_dir), real_path_abs)
+ @skipIf(HIDE_WINDOWS_KNOWN_ERRORS and PY3,
+ "FIXME: smmp fails with: TypeError: Can't convert 'bytes' object to str implicitly")
def test_file_handle_leaks(self):
def last_commit(repo, rev, path):
commit = next(repo.iter_commits(rev, path, max_count=1))
@@ -767,7 +809,7 @@ class TestRepo(TestBase):
# And we expect to set max handles to a low value, like 64
# You should set ulimit -n X, see .travis.yml
# The loops below would easily create 500 handles if these would leak (4 pipes + multiple mapped files)
- for i in range(64):
+ for _ in range(64):
for repo_type in (GitCmdObjectDB, GitDB):
repo = Repo(self.rorepo.working_tree_dir, odbt=repo_type)
last_commit(repo, 'master', 'git/test/test_base.py')
@@ -776,7 +818,7 @@ class TestRepo(TestBase):
def test_remote_method(self):
self.failUnlessRaises(ValueError, self.rorepo.remote, 'foo-blue')
- assert isinstance(self.rorepo.remote(name='origin'), Remote)
+ self.assertIsInstance(self.rorepo.remote(name='origin'), Remote)
@with_rw_directory
def test_empty_repo(self, rw_dir):
@@ -784,7 +826,7 @@ class TestRepo(TestBase):
r = Repo.init(rw_dir, mkdir=False)
# It's ok not to be able to iterate a commit, as there is none
self.failUnlessRaises(ValueError, r.iter_commits)
- assert r.active_branch.name == 'master'
+ self.assertEqual(r.active_branch.name, 'master')
assert not r.active_branch.is_valid(), "Branch is yet to be born"
# actually, when trying to create a new branch without a commit, git itself fails
@@ -824,12 +866,15 @@ class TestRepo(TestBase):
# two commit merge-base
res = repo.merge_base(c1, c2)
- assert isinstance(res, list) and len(res) == 1 and isinstance(res[0], Commit)
- assert res[0].hexsha.startswith('3936084')
+ self.assertIsInstance(res, list)
+ self.assertEqual(len(res), 1)
+ self.assertIsInstance(res[0], Commit)
+ self.assertTrue(res[0].hexsha.startswith('3936084'))
for kw in ('a', 'all'):
res = repo.merge_base(c1, c2, c3, **{kw: True})
- assert isinstance(res, list) and len(res) == 1
+ self.assertIsInstance(res, list)
+ self.assertEqual(len(res), 1)
# end for each keyword signalling all merge-bases to be returned
# Test for no merge base - can't do as we have
@@ -852,6 +897,9 @@ class TestRepo(TestBase):
for i, j in itertools.permutations([c1, 'ffffff', ''], r=2):
self.assertRaises(GitCommandError, repo.is_ancestor, i, j)
+ # @skipIf(HIDE_WINDOWS_KNOWN_ERRORS,
+ # "FIXME: helper.wrapper fails with: PermissionError: [WinError 5] Access is denied: "
+ # "'C:\\Users\\appveyor\\AppData\\Local\\Temp\\1\\test_work_tree_unsupportedryfa60di\\master_repo\\.git\\objects\\pack\\pack-bc9e0787aef9f69e1591ef38ea0a6f566ec66fe3.idx") # noqa E501
@with_rw_directory
def test_work_tree_unsupported(self, rw_dir):
git = Git(rw_dir)
diff --git a/git/test/test_submodule.py b/git/test/test_submodule.py
index 17ce605a..46928f51 100644
--- a/git/test/test_submodule.py
+++ b/git/test/test_submodule.py
@@ -1,34 +1,36 @@
# This module is part of GitPython and is released under
# the BSD License: http://www.opensource.org/licenses/bsd-license.php
-import sys
import os
+import sys
+from unittest.case import skipIf
import git
-
-from git.test.lib import (
- TestBase,
- with_rw_repo
-)
-from gitdb.test.lib import with_rw_directory
+from git.compat import string_types, is_win
from git.exc import (
InvalidGitRepositoryError,
RepositoryDirtyError
)
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.repo.fun import (
find_git_dir,
touch
)
+from git.test.lib import (
+ TestBase,
+ with_rw_repo
+)
+from git.test.lib import with_rw_directory
+from git.test.lib.helper import HIDE_WINDOWS_KNOWN_ERRORS
+from git.util import to_native_path_linux, join_path_native
+
# 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
+ import smmap.util # @UnusedImport
smmap.util.MapRegion._test_read_into_memory = True
except ImportError:
sys.stderr.write("The submodule tests will fail as some files cannot be removed due to open file handles.\n")
@@ -49,6 +51,10 @@ prog = TestRootProgress()
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"
@@ -92,28 +98,32 @@ class TestSubmodule(TestBase):
# force it to reread its information
del(smold._url)
- smold.url == sm.url
+ smold.url == sm.url # @NoEffect
# test config_reader/writer methods
sm.config_reader()
new_smclone_path = None # keep custom paths for later
new_csmclone_path = None #
if rwrepo.bare:
- self.failUnlessRaises(InvalidGitRepositoryError, sm.config_writer)
+ with self.assertRaises(InvalidGitRepositoryError):
+ with sm.config_writer() as cw:
+ pass
else:
- writer = sm.config_writer()
- # for faster checkout, set the url to the local path
- new_smclone_path = to_native_path_linux(join_path_native(self.rorepo.working_tree_dir, sm.path))
- writer.set_value('url', new_smclone_path)
- writer.release()
- assert sm.config_reader().get_value('url') == new_smclone_path
- assert sm.url == new_smclone_path
+ with sm.config_writer() as writer:
+ # for faster checkout, set the url to the local path
+ new_smclone_path = to_native_path_linux(join_path_native(self.rorepo.working_tree_dir, sm.path))
+ writer.set_value('url', new_smclone_path)
+ writer.release()
+ assert sm.config_reader().get_value('url') == new_smclone_path
+ assert sm.url == new_smclone_path
# END handle bare repo
smold.config_reader()
# cannot get a writer on historical submodules
if not rwrepo.bare:
- self.failUnlessRaises(ValueError, smold.config_writer)
+ with self.assertRaises(ValueError):
+ with smold.config_writer():
+ pass
# END handle bare repo
# make the old into a new - this doesn't work as the name changed
@@ -204,9 +214,8 @@ class TestSubmodule(TestBase):
# adjust the path of the submodules module to point to the local destination
new_csmclone_path = to_native_path_linux(join_path_native(self.rorepo.working_tree_dir, sm.path, csm.path))
- writer = csm.config_writer()
- writer.set_value('url', new_csmclone_path)
- writer.release()
+ with csm.config_writer() as writer:
+ writer.set_value('url', new_csmclone_path)
assert csm.url == new_csmclone_path
# dry-run does nothing
@@ -219,7 +228,7 @@ class TestSubmodule(TestBase):
assert csm.module_exists()
# tracking branch once again
- csm.module().head.ref.tracking_branch() is not None
+ csm.module().head.ref.tracking_branch() is not None # @NoEffect
# this flushed in a sub-submodule
assert len(list(rwrepo.iter_submodules())) == 2
@@ -268,9 +277,8 @@ class TestSubmodule(TestBase):
# module() is supposed to point to gitdb, which has a child-submodule whose URL is still pointing
# to github. To save time, we will change it to
csm.set_parent_commit(csm.repo.head.commit)
- cw = csm.config_writer()
- cw.set_value('url', self._small_repo_url())
- cw.release()
+ with csm.config_writer() as cw:
+ cw.set_value('url', self._small_repo_url())
csm.repo.index.commit("adjusted URL to point to local source, instead of the internet")
# We have modified the configuration, hence the index is dirty, and the
@@ -278,12 +286,10 @@ class TestSubmodule(TestBase):
# NOTE: As we did a few updates in the meanwhile, the indices were reset
# Hence we create some changes
csm.set_parent_commit(csm.repo.head.commit)
- writer = sm.config_writer()
- writer.set_value("somekey", "somevalue")
- writer.release()
- writer = csm.config_writer()
- writer.set_value("okey", "ovalue")
- writer.release()
+ with sm.config_writer() as writer:
+ writer.set_value("somekey", "somevalue")
+ with csm.config_writer() as writer:
+ writer.set_value("okey", "ovalue")
self.failUnlessRaises(InvalidGitRepositoryError, sm.remove)
# if we remove the dirty index, it would work
sm.module().index.reset()
@@ -305,14 +311,15 @@ class TestSubmodule(TestBase):
# but ... we have untracked files in the child submodule
fn = join_path_native(csm.module().working_tree_dir, "newfile")
- open(fn, 'w').write("hi")
+ with open(fn, 'w') as fd:
+ fd.write("hi")
self.failUnlessRaises(InvalidGitRepositoryError, sm.remove)
# forcibly delete the child repository
prev_count = len(sm.children())
self.failUnlessRaises(ValueError, csm.remove, force=True)
- # We removed sm, which removed all submodules. Howver, the instance we have
- # still points to the commit prior to that, where it still existed
+ # We removed sm, which removed all submodules. However, the instance we
+ # have still points to the commit prior to that, where it still existed
csm.set_parent_commit(csm.repo.commit(), check=False)
assert not csm.exists()
assert not csm.module_exists()
@@ -411,6 +418,10 @@ class TestSubmodule(TestBase):
# Error if there is no submodule file here
self.failUnlessRaises(IOError, Submodule._config_parser, rwrepo, rwrepo.commit(self.k_no_subm_tag), True)
+ # @skipIf(HIDE_WINDOWS_KNOWN_ERRORS,
+ # "FIXME: fails with: PermissionError: [WinError 32] The process cannot access the file because"
+ # "it is being used by another process: "
+ # "'C:\\Users\\ankostis\\AppData\\Local\\Temp\\tmp95c3z83bnon_bare_test_base_rw\\git\\ext\\gitdb\\gitdb\\ext\\smmap'") # noqa E501
@with_rw_repo(k_subm_current)
def test_base_rw(self, rwrepo):
self._do_base_tests(rwrepo)
@@ -419,6 +430,11 @@ class TestSubmodule(TestBase):
def test_base_bare(self, rwrepo):
self._do_base_tests(rwrepo)
+ @skipIf(HIDE_WINDOWS_KNOWN_ERRORS and sys.version_info[:2] == (3, 5), """
+ File "C:\projects\gitpython\git\cmd.py", line 559, in execute
+ raise GitCommandNotFound(command, err)
+ git.exc.GitCommandNotFound: Cmd('git') not found due to: OSError('[WinError 6] The handle is invalid')
+ cmdline: git clone -n --shared -v C:\projects\gitpython\.git Users\appveyor\AppData\Local\Temp\1\tmplyp6kr_rnon_bare_test_root_module""") # noqa E501
@with_rw_repo(k_subm_current, bare=False)
def test_root_module(self, rwrepo):
# Can query everything without problems
@@ -436,8 +452,8 @@ class TestSubmodule(TestBase):
assert len(rm.list_items(rm.module())) == 1
rm.config_reader()
- w = rm.config_writer()
- w.release()
+ with rm.config_writer():
+ pass
# deep traversal gitdb / async
rsmsp = [sm.path for sm in rm.traverse()]
@@ -462,9 +478,8 @@ class TestSubmodule(TestBase):
assert not sm.module_exists() # was never updated after rwrepo's clone
# assure we clone from a local source
- writer = sm.config_writer()
- writer.set_value('url', to_native_path_linux(join_path_native(self.rorepo.working_tree_dir, sm.path)))
- writer.release()
+ with sm.config_writer() as writer:
+ writer.set_value('url', to_native_path_linux(join_path_native(self.rorepo.working_tree_dir, sm.path)))
# dry-run does nothing
sm.update(recursive=False, dry_run=True, progress=prog)
@@ -472,9 +487,8 @@ class TestSubmodule(TestBase):
sm.update(recursive=False)
assert sm.module_exists()
- writer = sm.config_writer()
- writer.set_value('path', fp) # change path to something with prefix AFTER url change
- writer.release()
+ with sm.config_writer() as writer:
+ writer.set_value('path', fp) # change path to something with prefix AFTER url change
# update fails as list_items in such a situations cannot work, as it cannot
# find the entry at the changed path
@@ -561,9 +575,8 @@ class TestSubmodule(TestBase):
# repository at the different url
nsm.set_parent_commit(csmremoved)
nsmurl = to_native_path_linux(join_path_native(self.rorepo.working_tree_dir, rsmsp[0]))
- writer = nsm.config_writer()
- writer.set_value('url', nsmurl)
- writer.release()
+ with nsm.config_writer() as writer:
+ writer.set_value('url', nsmurl)
csmpathchange = rwrepo.index.commit("changed url")
nsm.set_parent_commit(csmpathchange)
@@ -593,9 +606,8 @@ class TestSubmodule(TestBase):
nsmm = nsm.module()
prev_commit = nsmm.head.commit
for branch in ("some_virtual_branch", cur_branch.name):
- writer = nsm.config_writer()
- writer.set_value(Submodule.k_head_option, git.Head.to_full_path(branch))
- writer.release()
+ with nsm.config_writer() as writer:
+ writer.set_value(Submodule.k_head_option, git.Head.to_full_path(branch))
csmbranchchange = rwrepo.index.commit("changed branch to %s" % branch)
nsm.set_parent_commit(csmbranchchange)
# END for each branch to change
@@ -623,9 +635,8 @@ class TestSubmodule(TestBase):
assert nsm.exists() and nsm.module_exists() and len(nsm.children()) >= 1
# assure we pull locally only
nsmc = nsm.children()[0]
- writer = nsmc.config_writer()
- writer.set_value('url', subrepo_url)
- writer.release()
+ with nsmc.config_writer() as writer:
+ writer.set_value('url', subrepo_url)
rm.update(recursive=True, progress=prog, dry_run=True) # just to run the code
rm.update(recursive=True, progress=prog)
@@ -717,6 +728,9 @@ class TestSubmodule(TestBase):
assert commit_sm.binsha == sm_too.binsha
assert sm_too.binsha != sm.binsha
+ # @skipIf(HIDE_WINDOWS_KNOWN_ERRORS,
+ # "FIXME: helper.wrapper fails with: PermissionError: [WinError 5] Access is denied: "
+ # "'C:\\Users\\appveyor\\AppData\\Local\\Temp\\1\\test_work_tree_unsupportedryfa60di\\master_repo\\.git\\objects\\pack\\pack-bc9e0787aef9f69e1591ef38ea0a6f566ec66fe3.idx") # noqa E501
@with_rw_directory
def test_git_submodule_compatibility(self, rwdir):
parent = git.Repo.init(os.path.join(rwdir, 'parent'))
@@ -774,8 +788,8 @@ class TestSubmodule(TestBase):
rsm = parent.submodule_update()
assert_exists(sm)
assert_exists(csm)
- csm_writer = csm.config_writer().set_value('url', 'bar')
- csm_writer.release()
+ with csm.config_writer().set_value('url', 'bar'):
+ pass
csm.repo.index.commit("Have to commit submodule change for algorithm to pick it up")
assert csm.url == 'bar'
@@ -793,6 +807,24 @@ class TestSubmodule(TestBase):
# end for each dry-run mode
@with_rw_directory
+ def test_remove_norefs(self, rwdir):
+ parent = git.Repo.init(os.path.join(rwdir, 'parent'))
+ sm_name = 'mymodules/myname'
+ sm = parent.create_submodule(sm_name, sm_name, url=self._small_repo_url())
+ assert sm.exists()
+
+ parent.index.commit("Added submodule")
+
+ assert sm.repo is parent # yoh was surprised since expected sm repo!!
+ # so created a new instance for submodule
+ smrepo = git.Repo(os.path.join(rwdir, 'parent', sm.path))
+ # Adding a remote without fetching so would have no references
+ smrepo.create_remote('special', 'git@server-shouldnotmatter:repo.git')
+ # And we should be able to remove it just fine
+ sm.remove()
+ assert not sm.exists()
+
+ @with_rw_directory
def test_rename(self, rwdir):
parent = git.Repo.init(os.path.join(rwdir, 'parent'))
sm_name = 'mymodules/myname'
@@ -835,9 +867,8 @@ class TestSubmodule(TestBase):
sm.repo.index.commit("added new file")
# change designated submodule checkout branch to the new upstream feature branch
- smcw = sm.config_writer()
- smcw.set_value('branch', sm_fb.name)
- smcw.release()
+ with sm.config_writer() as smcw:
+ smcw.set_value('branch', sm_fb.name)
assert sm.repo.is_dirty(index=True, working_tree=False)
sm.repo.index.commit("changed submodule branch to '%s'" % sm_fb)
@@ -861,9 +892,8 @@ class TestSubmodule(TestBase):
sm_source_repo.index.commit("new file added, to past of '%r'" % sm_fb)
# Change designated submodule checkout branch to a new commit in its own past
- smcw = sm.config_writer()
- smcw.set_value('branch', sm_pfb.path)
- smcw.release()
+ with sm.config_writer() as smcw:
+ smcw.set_value('branch', sm_pfb.path)
sm.repo.index.commit("changed submodule branch to '%s'" % sm_pfb)
# Test submodule updates - must fail if submodule is dirty
diff --git a/git/test/test_tree.py b/git/test/test_tree.py
index f9282411..bb62d9bf 100644
--- a/git/test/test_tree.py
+++ b/git/test/test_tree.py
@@ -4,18 +4,26 @@
# This module is part of GitPython and is released under
# the BSD License: http://www.opensource.org/licenses/bsd-license.php
+from io import BytesIO
import os
-from git.test.lib import TestBase
+import sys
+from unittest.case import skipIf
+
from git import (
Tree,
Blob
)
-
-from io import BytesIO
+from git.test.lib.helper import HIDE_WINDOWS_KNOWN_ERRORS
+from git.test.lib import TestBase
class TestTree(TestBase):
+ @skipIf(HIDE_WINDOWS_KNOWN_ERRORS and sys.version_info[:2] == (3, 5), """
+ File "C:\projects\gitpython\git\cmd.py", line 559, in execute
+ raise GitCommandNotFound(command, err)
+ git.exc.GitCommandNotFound: Cmd('git') not found due to: OSError('[WinError 6] The handle is invalid')
+ cmdline: git cat-file --batch-check""")
def test_serializable(self):
# tree at the given commit contains a submodule as well
roottree = self.rorepo.tree('6c1faef799095f3990e9970bc2cb10aa0221cf9c')
@@ -44,6 +52,11 @@ class TestTree(TestBase):
testtree._deserialize(stream)
# END for each item in tree
+ @skipIf(HIDE_WINDOWS_KNOWN_ERRORS and sys.version_info[:2] == (3, 5), """
+ File "C:\projects\gitpython\git\cmd.py", line 559, in execute
+ raise GitCommandNotFound(command, err)
+ git.exc.GitCommandNotFound: Cmd('git') not found due to: OSError('[WinError 6] The handle is invalid')
+ cmdline: git cat-file --batch-check""")
def test_traverse(self):
root = self.rorepo.tree('0.1.6')
num_recursive = 0
diff --git a/git/test/test_util.py b/git/test/test_util.py
index 3a67c04b..6ba3d0d4 100644
--- a/git/test/test_util.py
+++ b/git/test/test_util.py
@@ -25,21 +25,25 @@ from git.objects.util import (
parse_date,
)
from git.cmd import dashify
-from git.compat import string_types
+from git.compat import string_types, is_win
import time
+import ddt
class TestIterableMember(object):
"""A member of an iterable list"""
- __slots__ = ("name", "prefix_name")
+ __slots__ = "name"
def __init__(self, name):
self.name = name
- self.prefix_name = name
+ def __repr__(self):
+ return "TestIterableMember(%r)" % self.name
+
+@ddt.ddt
class TestUtils(TestBase):
def setup(self):
@@ -92,23 +96,28 @@ class TestUtils(TestBase):
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
+ extra_time = 0.02
+ if is_win:
+ # for Appveyor
+ extra_time *= 6 # NOTE: Indeterministic failures here...
+ self.assertLess(elapsed, wait_time + extra_time)
def test_user_id(self):
- assert '@' in get_user_id()
+ self.assertIn('@', get_user_id())
def test_parse_date(self):
# test all supported formats
def assert_rval(rval, veri_time, offset=0):
- assert len(rval) == 2
- assert isinstance(rval[0], int) and isinstance(rval[1], int)
- assert rval[0] == veri_time
- assert rval[1] == offset
+ self.assertEqual(len(rval), 2)
+ self.assertIsInstance(rval[0], int)
+ self.assertIsInstance(rval[1], int)
+ self.assertEqual(rval[0], veri_time)
+ self.assertEqual(rval[1], offset)
# now that we are here, test our conversion functions as well
utctz = altz_to_utctz_str(offset)
- assert isinstance(utctz, string_types)
- assert utctz_to_altz(verify_utctz(utctz)) == offset
+ self.assertIsInstance(utctz, string_types)
+ self.assertEqual(utctz_to_altz(verify_utctz(utctz)), offset)
# END assert rval utility
rfc = ("Thu, 07 Apr 2005 22:13:11 +0000", 0)
@@ -129,53 +138,56 @@ class TestUtils(TestBase):
def test_actor(self):
for cr in (None, self.rorepo.config_reader()):
- assert isinstance(Actor.committer(cr), Actor)
- assert isinstance(Actor.author(cr), Actor)
+ self.assertIsInstance(Actor.committer(cr), Actor)
+ self.assertIsInstance(Actor.author(cr), Actor)
# END assure config reader is handled
- def test_iterable_list(self):
- for args in (('name',), ('name', 'prefix_')):
- l = IterableList('name')
-
- m1 = TestIterableMember('one')
- m2 = TestIterableMember('two')
-
- l.extend((m1, m2))
-
- assert len(l) == 2
-
- # contains works with name and identity
- assert m1.name in l
- assert m2.name in l
- assert m2 in l
- assert m2 in l
- assert 'invalid' not in l
-
- # with string index
- assert l[m1.name] is m1
- assert l[m2.name] is m2
-
- # with int index
- assert l[0] is m1
- assert l[1] is m2
-
- # with getattr
- assert l.one is m1
- assert l.two is m2
-
- # test exceptions
- self.failUnlessRaises(AttributeError, getattr, l, 'something')
- self.failUnlessRaises(IndexError, l.__getitem__, 'something')
-
- # delete by name and index
- self.failUnlessRaises(IndexError, l.__delitem__, 'something')
- del(l[m2.name])
- assert len(l) == 1
- assert m2.name not in l and m1.name in l
- del(l[0])
- assert m1.name not in l
- assert len(l) == 0
-
- self.failUnlessRaises(IndexError, l.__delitem__, 0)
- self.failUnlessRaises(IndexError, l.__delitem__, 'something')
- # END for each possible mode
+ @ddt.data(('name', ''), ('name', 'prefix_'))
+ def test_iterable_list(self, case):
+ name, prefix = case
+ l = IterableList(name, prefix)
+
+ name1 = "one"
+ name2 = "two"
+ m1 = TestIterableMember(prefix + name1)
+ m2 = TestIterableMember(prefix + name2)
+
+ l.extend((m1, m2))
+
+ self.assertEqual(len(l), 2)
+
+ # contains works with name and identity
+ self.assertIn(name1, l)
+ self.assertIn(name2, l)
+ self.assertIn(m2, l)
+ self.assertIn(m2, l)
+ self.assertNotIn('invalid', l)
+
+ # with string index
+ self.assertIs(l[name1], m1)
+ self.assertIs(l[name2], m2)
+
+ # with int index
+ self.assertIs(l[0], m1)
+ self.assertIs(l[1], m2)
+
+ # with getattr
+ self.assertIs(l.one, m1)
+ self.assertIs(l.two, m2)
+
+ # test exceptions
+ self.failUnlessRaises(AttributeError, getattr, l, 'something')
+ self.failUnlessRaises(IndexError, l.__getitem__, 'something')
+
+ # delete by name and index
+ self.failUnlessRaises(IndexError, l.__delitem__, 'something')
+ del(l[name2])
+ self.assertEqual(len(l), 1)
+ self.assertNotIn(name2, l)
+ self.assertIn(name1, l)
+ del(l[0])
+ self.assertNotIn(name1, l)
+ self.assertEqual(len(l), 0)
+
+ self.failUnlessRaises(IndexError, l.__delitem__, 0)
+ self.failUnlessRaises(IndexError, l.__delitem__, 'something')
diff --git a/git/util.py b/git/util.py
index 8d97242c..9f8ccea5 100644
--- a/git/util.py
+++ b/git/util.py
@@ -3,44 +3,49 @@
#
# 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
from fcntl import flock, LOCK_UN, LOCK_EX, LOCK_NB
+import getpass
+import logging
import os
+import platform
import re
-import sys
-import time
-import stat
import shutil
-import platform
-import getpass
-import threading
-import logging
+import stat
+import time
-# NOTE: Some of the unused imports might be used/imported by others.
-# Handle once test-cases are back up and running.
-from .exc import InvalidGitRepositoryError
+from functools import wraps
+
+from git.compat import is_win
+from gitdb.util import ( # NOQA
+ make_sha,
+ LockedFD, # @UnusedImport
+ file_contents_ro, # @UnusedImport
+ LazyMixin, # @UnusedImport
+ to_hex_sha, # @UnusedImport
+ to_bin_sha # @UnusedImport
+)
+
+import os.path as osp
from .compat import (
MAXSIZE,
defenc,
PY3
)
+from .exc import InvalidGitRepositoryError
+from unittest.case import SkipTest
+
+# NOTE: Some of the unused imports might be used/imported by others.
+# Handle once test-cases are back up and running.
# 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
- make_sha,
- LockedFD,
- file_contents_ro,
- LazyMixin,
- to_hex_sha,
- to_bin_sha
-)
-
__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
@@ -49,13 +54,13 @@ def unbare_repo(func):
"""Methods with this decorator raise InvalidGitRepositoryError if they
encounter a bare repository"""
+ @wraps(func)
def wrapper(self, *args, **kwargs):
if self.repo.bare:
raise InvalidGitRepositoryError("Method '%s' cannot operate on bare repositories" % func.__name__)
# END bare method
return func(self, *args, **kwargs)
# END wrapper
- wrapper.__name__ = func.__name__
return wrapper
@@ -64,17 +69,31 @@ 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
- # END end onerror
+ # Is the error an access error ?
+ os.chmod(path, stat.S_IWUSR)
+
+ try:
+ func(path) # Will scream if still not possible to delete.
+ except Exception as ex:
+ from git.test.lib.helper import HIDE_WINDOWS_KNOWN_ERRORS
+ if HIDE_WINDOWS_KNOWN_ERRORS:
+ raise SkipTest("FIXME: fails with: PermissionError\n %s", ex)
+ else:
+ raise
+
return shutil.rmtree(path, False, onerror)
+def rmfile(path):
+ """Ensure file deleted also on *Windows* where read-only files need special treatment."""
+ if osp.isfile(path):
+ if is_win:
+ os.chmod(path, 0o777)
+ os.remove(path)
+
+
def stream_copy(source, destination, chunk_size=512 * 1024):
"""Copy all data from the source stream into the destination stream in chunks
of size chunk_size
@@ -108,7 +127,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('/', '\\')
@@ -153,6 +172,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
@@ -232,7 +252,7 @@ class RemoteProgress(object):
# END could not get match
op_code = 0
- remote, op_name, percent, cur_count, max_count, message = match.groups()
+ remote, op_name, percent, cur_count, max_count, message = match.groups() # @UnusedVariable
# get operation id
if op_name == "Counting objects":
@@ -566,7 +586,10 @@ class LockFile(object):
# Create file and lock
try:
- fd = os.open(lock_file, os.O_CREAT, 0)
+ flags = os.O_CREAT
+ if is_win:
+ flags |= os.O_SHORT_LIVED
+ fd = os.open(lock_file, flags, 0)
except OSError as e:
raise IOError(str(e))
@@ -590,8 +613,14 @@ class LockFile(object):
flock(fd, LOCK_UN)
os.close(fd)
- os.remove(lock_file)
+ # if someone removed our file beforhand, lets just flag this issue
+ # instead of failing, to make it more usable.
+ lfp = self._lock_file_path()
+ try:
+ rmfile(lfp)
+ except OSError:
+ pass
self._owns_lock = False
self._file_descriptor = None
@@ -753,35 +782,6 @@ class Iterable(object):
#} 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
diff --git a/requirements.txt b/requirements.txt
index 2316b96e..85d25511 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1 +1,3 @@
gitdb>=0.6.4
+ddt
+mock \ No newline at end of file
diff --git a/setup.py b/setup.py
index 05c12b8f..c7dd25fc 100755
--- a/setup.py
+++ b/setup.py
@@ -9,13 +9,14 @@ except ImportError:
from distutils.command.build_py import build_py as _build_py
from setuptools.command.sdist import sdist as _sdist
+import pkg_resources
+import logging
import os
import sys
from os import path
-v = open(path.join(path.dirname(__file__), 'VERSION'))
-VERSION = v.readline().strip()
-v.close()
+with open(path.join(path.dirname(__file__), 'VERSION')) as v:
+ VERSION = v.readline().strip()
with open('requirements.txt') as reqs_file:
requirements = reqs_file.read().splitlines()
@@ -48,28 +49,42 @@ class sdist(_sdist):
def _stamp_version(filename):
found, out = False, list()
try:
- f = open(filename, 'r')
+ with open(filename, 'r') as f:
+ for line in f:
+ if '__version__ =' in line:
+ line = line.replace("'git'", "'%s'" % VERSION)
+ found = True
+ out.append(line)
except (IOError, OSError):
print("Couldn't find file %s to stamp version" % filename, file=sys.stderr)
- return
- # END handle error, usually happens during binary builds
- for line in f:
- if '__version__ =' in line:
- line = line.replace("'git'", "'%s'" % VERSION)
- found = True
- out.append(line)
- f.close()
if found:
- f = open(filename, 'w')
- f.writelines(out)
- f.close()
+ with open(filename, 'w') as f:
+ f.writelines(out)
else:
print("WARNING: Couldn't find version line in file %s" % filename, file=sys.stderr)
install_requires = ['gitdb >= 0.6.4']
+extras_require = {
+ ':python_version == "2.6"': ['ordereddict'],
+}
+test_requires = ['ddt']
if sys.version_info[:2] < (2, 7):
- install_requires.append('ordereddict')
+ test_requires.append('mock')
+
+try:
+ if 'bdist_wheel' not in sys.argv:
+ for key, value in extras_require.items():
+ if key.startswith(':') and pkg_resources.evaluate_marker(key[1:]):
+ install_requires.extend(value)
+except Exception:
+ logging.getLogger(__name__).exception(
+ 'Something went wrong calculating platform specific dependencies, so '
+ "you're getting them all!"
+ )
+ for key, value in extras_require.items():
+ if key.startswith(':'):
+ install_requires.extend(value)
# end
setup(
@@ -87,10 +102,9 @@ setup(
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""",
+ long_description="""GitPython is a python library used to interact with Git repositories""",
classifiers=[
# Picked from
# http://pypi.python.org/pypi?:action=list_classifiers