summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.appveyor.yml20
-rw-r--r--.dockerignore2
-rw-r--r--.travis.yml6
-rw-r--r--AUTHORS2
-rw-r--r--Dockerfile80
-rw-r--r--MANIFEST.in4
-rw-r--r--Makefile18
-rw-r--r--README.md2
-rw-r--r--doc/source/changes.rst11
-rwxr-xr-xdockernose.sh10
-rw-r--r--git/cmd.py28
-rw-r--r--git/config.py137
m---------git/ext/gitdb0
-rw-r--r--git/objects/tag.py5
-rw-r--r--git/remote.py2
-rw-r--r--git/repo/base.py24
-rw-r--r--git/test/fixtures/git_config_multiple7
-rw-r--r--git/test/test_config.py109
-rw-r--r--git/test/test_remote.py2
-rw-r--r--git/test/test_repo.py16
-rw-r--r--git/test/test_util.py6
-rw-r--r--git/util.py4
-rw-r--r--requirements.txt3
-rwxr-xr-xsetup.py14
-rw-r--r--test-requirements.txt6
25 files changed, 455 insertions, 63 deletions
diff --git a/.appveyor.yml b/.appveyor.yml
index 8eeca501..017cf120 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -5,8 +5,6 @@ environment:
CYGWIN64_GIT_PATH: "C:\\cygwin64\\bin;%GIT_DAEMON_PATH%"
matrix:
- ## MINGW
- #
- PYTHON: "C:\\Python27"
PYTHON_VERSION: "2.7"
GIT_PATH: "%GIT_DAEMON_PATH%"
@@ -16,24 +14,33 @@ environment:
- PYTHON: "C:\\Python35-x64"
PYTHON_VERSION: "3.5"
GIT_PATH: "%GIT_DAEMON_PATH%"
+ - PYTHON: "C:\\Python36-x64"
+ PYTHON_VERSION: "3.6"
+ GIT_PATH: "%GIT_DAEMON_PATH%"
+ - PYTHON: "C:\\Python37-x64"
+ PYTHON_VERSION: "3.7"
+ GIT_PATH: "%GIT_DAEMON_PATH%"
- PYTHON: "C:\\Miniconda35-x64"
PYTHON_VERSION: "3.5"
IS_CONDA: "yes"
+ MAYFAIL: "yes"
GIT_PATH: "%GIT_DAEMON_PATH%"
-
## Cygwin
- #
- PYTHON: "C:\\Miniconda-x64"
PYTHON_VERSION: "2.7"
IS_CONDA: "yes"
IS_CYGWIN: "yes"
+ MAYFAIL: "yes"
GIT_PATH: "%CYGWIN_GIT_PATH%"
- PYTHON: "C:\\Python35-x64"
PYTHON_VERSION: "3.5"
- GIT_PATH: "%CYGWIN64_GIT_PATH%"
IS_CYGWIN: "yes"
+ MAYFAIL: "yes"
+ GIT_PATH: "%CYGWIN64_GIT_PATH%"
-
+matrix:
+ allow_failures:
+ - MAYFAIL: "yes"
install:
- set PATH=%PYTHON%;%PYTHON%\Scripts;%GIT_PATH%;%PATH%
@@ -51,6 +58,7 @@ install:
conda info -a &
conda install --yes --quiet pip
)
+ - pip install -r requirements.txt
- pip install -r test-requirements.txt
- pip install codecov
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 00000000..b59962d2
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,2 @@
+.git/
+.tox/
diff --git a/.travis.yml b/.travis.yml
index adb693dd..aed714af 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -9,12 +9,11 @@ matrix:
include:
- python: 3.7
dist: xenial
- sudo: required
- python: "nightly"
dist: xenial
- sudo: required
allow_failures:
- python: "nightly"
+ dist: xenial
git:
# a higher depth is needed for most of the tests - must be high enough to not actually be shallow
# as we clone our own repository in the process
@@ -40,8 +39,7 @@ script:
- ulimit -n 128
- ulimit -n
- nosetests -v --with-coverage
- - if [ "$TRAVIS_PYTHON_VERSION" == '3.4' ]; then flake8 --ignore=W293,E265,E266,W503,W504,E731; fi
- if [ "$TRAVIS_PYTHON_VERSION" == '3.5' ]; then cd doc && make html; fi
- -
+ - if [ "$TRAVIS_PYTHON_VERSION" == '3.6' ]; then flake8 --ignore=W293,E265,E266,W503,W504,E731; fi
after_success:
- codecov
diff --git a/AUTHORS b/AUTHORS
index 24cf239b..5f42f856 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -29,6 +29,8 @@ Contributors are:
-Tim Swast <swast _at_ google.com>
-William Luc Ritchie
-David Host <hostdm _at_ outlook.com>
+-A. Jesse Jiryu Davis <jesse _at_ emptysquare.net>
+-Steven Whitman <ninloot _at_ gmail.com>
-Stefan Stancu <stefan.stancu _at_ gmail.com>
Portions derived from other open source works and are clearly marked.
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000..fc42f18f
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,80 @@
+#
+# Contributed by: James E. King III (@jeking3) <jking@apache.org>
+#
+# This Dockerfile creates an Ubuntu Xenial build environment
+# that can run the same test suite as Travis CI.
+#
+
+FROM ubuntu:xenial
+MAINTAINER James E. King III <jking@apache.org>
+ENV CONTAINER_USER=user
+ENV DEBIAN_FRONTEND noninteractive
+
+RUN apt-get update && \
+ apt-get install -y --no-install-recommends \
+ add-apt-key \
+ apt \
+ apt-transport-https \
+ apt-utils \
+ ca-certificates \
+ curl \
+ git \
+ net-tools \
+ openssh-client \
+ sudo \
+ vim \
+ wget
+
+RUN add-apt-key -v 6A755776 -k keyserver.ubuntu.com && \
+ add-apt-key -v E1DF1F24 -k keyserver.ubuntu.com && \
+ echo "deb http://ppa.launchpad.net/git-core/ppa/ubuntu xenial main" >> /etc/apt/sources.list && \
+ echo "deb http://ppa.launchpad.net/deadsnakes/ppa/ubuntu xenial main" >> /etc/apt/sources.list && \
+ apt-get update && \
+ apt-get install -y --install-recommends git python2.7 python3.4 python3.5 python3.6 python3.7 && \
+ update-alternatives --install /usr/bin/python3 python3 /usr/bin/python2.7 27 && \
+ update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.4 34 && \
+ update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.5 35 && \
+ update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.6 36 && \
+ update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.7 37
+
+RUN curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && \
+ python3 get-pip.py && \
+ pip3 install tox
+
+# Clean up
+RUN rm -rf /var/cache/apt/* && \
+ rm -rf /var/lib/apt/lists/* && \
+ rm -rf /tmp/* && \
+ rm -rf /var/tmp/*
+
+#################################################################
+# Build as a regular user
+# Credit: https://github.com/delcypher/docker-ubuntu-cxx-dev/blob/master/Dockerfile
+# License: None specified at time of import
+# Add non-root user for container but give it sudo access.
+# Password is the same as the username
+RUN useradd -m ${CONTAINER_USER} && \
+ echo ${CONTAINER_USER}:${CONTAINER_USER} | chpasswd && \
+ echo "${CONTAINER_USER} ALL=(root) ALL" >> /etc/sudoers
+RUN chsh --shell /bin/bash ${CONTAINER_USER}
+USER ${CONTAINER_USER}
+#################################################################
+
+# The test suite will not tolerate running against a branch that isn't "master", so
+# check out the project to a well-known location that can be used by the test suite.
+# This has the added benefit of protecting the local repo fed into the container
+# as a volume from getting destroyed by a bug exposed by the test suite. :)
+ENV TRAVIS=ON
+RUN git clone --recursive https://github.com/gitpython-developers/GitPython.git /home/${CONTAINER_USER}/testrepo && \
+ cd /home/${CONTAINER_USER}/testrepo && \
+ ./init-tests-after-clone.sh
+ENV GIT_PYTHON_TEST_GIT_REPO_BASE=/home/${CONTAINER_USER}/testrepo
+ENV TRAVIS=
+
+# Ensure any local pip installations get on the path
+ENV PATH=/home/${CONTAINER_USER}/.local/bin:${PATH}
+
+# Set the global default git user to be someone non-descript
+RUN git config --global user.email ci@gitpython.org && \
+ git config --global user.name "GitPython CI User"
+
diff --git a/MANIFEST.in b/MANIFEST.in
index 15ac959e..e6bf5249 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -2,8 +2,10 @@ include VERSION
include LICENSE
include CHANGES
include AUTHORS
-include README
+include CONTRIBUTING.md
+include README.md
include requirements.txt
+include test-requirements.txt
recursive-include doc *
diff --git a/Makefile b/Makefile
index 648b8595..ae74a0d8 100644
--- a/Makefile
+++ b/Makefile
@@ -1,8 +1,10 @@
+.PHONY: all clean release force_release docker-build test nose-pdb
+
all:
@grep -Ee '^[a-z].*:' Makefile | cut -d: -f1 | grep -vF all
clean:
- rm -rf build/ dist/
+ rm -rf build/ dist/ .eggs/ .tox/
release: clean
# Check if latest tag is the current head we're releasing
@@ -16,3 +18,17 @@ force_release: clean
git push --tags origin master
python3 setup.py sdist bdist_wheel
twine upload -s -i byronimo@gmail.com dist/*
+
+docker-build:
+ docker build --quiet -t gitpython:xenial -f Dockerfile .
+
+test: docker-build
+ # NOTE!!!
+ # NOTE!!! If you are not running from master or have local changes then tests will fail
+ # NOTE!!!
+ docker run --rm -v ${CURDIR}:/src -w /src -t gitpython:xenial tox
+
+nose-pdb: docker-build
+ # run tests under nose and break on error or failure into python debugger
+ # HINT: set PYVER to "pyXX" to change from the default of py37 to pyXX for nose tests
+ docker run --rm --env PYVER=${PYVER} -v ${CURDIR}:/src -w /src -it gitpython:xenial /bin/bash dockernose.sh
diff --git a/README.md b/README.md
index f5735bc2..e252c34c 100644
--- a/README.md
+++ b/README.md
@@ -192,8 +192,10 @@ New BSD License. See the LICENSE file.
[![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)
+[![Packaging status](https://repology.org/badge/tiny-repos/python:gitpython.svg)](https://repology.org/metapackage/python:gitpython/versions)
[![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
diff --git a/doc/source/changes.rst b/doc/source/changes.rst
index 32e58c7f..92c28b69 100644
--- a/doc/source/changes.rst
+++ b/doc/source/changes.rst
@@ -2,6 +2,17 @@
Changelog
=========
+2.1.12 - Bugfixes and Features
+==============================
+
+* Multi-value support and interface improvements for Git configuration. Thanks to A. Jesse Jiryu Davis.
+
+see the following for (most) details:
+https://github.com/gitpython-developers/gitpython/milestone/27?closed=1
+
+or run have a look at the difference between tags v2.1.11 and v2.1.12:
+https://github.com/gitpython-developers/GitPython/compare/2.1.11...2.1.12
+
2.1.11 - Bugfixes
=================
diff --git a/dockernose.sh b/dockernose.sh
new file mode 100755
index 00000000..c9227118
--- /dev/null
+++ b/dockernose.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+set -ex
+if [ -z "${PYVER}" ]; then
+ PYVER=py37
+fi
+
+# remember to use "-s" if you inject pdb.set_trace() as this disables nosetests capture of streams
+
+tox -e ${PYVER} --notest
+PYTHONPATH=/src/.tox/${PYVER}/lib/python*/site-packages /src/.tox/${PYVER}/bin/nosetests --pdb $*
diff --git a/git/cmd.py b/git/cmd.py
index a4faefe2..64c3d480 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -43,6 +43,10 @@ from .util import (
stream_copy,
)
+try:
+ PermissionError
+except NameError: # Python < 3.3
+ PermissionError = OSError
execute_kwargs = {'istream', 'with_extended_output',
'with_exceptions', 'as_process', 'stdout_as_string',
@@ -211,22 +215,15 @@ class Git(LazyMixin):
# test if the new git executable path is valid
- if sys.version_info < (3,):
- # - a GitCommandNotFound error is spawned by ourselves
- # - a OSError is spawned if the git executable provided
- # cannot be executed for whatever reason
- exceptions = (GitCommandNotFound, OSError)
- else:
- # - a GitCommandNotFound error is spawned by ourselves
- # - a PermissionError is spawned if the git executable provided
- # cannot be executed for whatever reason
- exceptions = (GitCommandNotFound, PermissionError)
-
+ # - a GitCommandNotFound error is spawned by ourselves
+ # - a PermissionError is spawned if the git executable provided
+ # cannot be executed for whatever reason
+
has_git = False
try:
cls().version()
has_git = True
- except exceptions:
+ except (GitCommandNotFound, PermissionError):
pass
# warn or raise exception if test failed
@@ -718,8 +715,11 @@ class Git(LazyMixin):
stdout_sink = (PIPE
if with_stdout
else getattr(subprocess, 'DEVNULL', None) or open(os.devnull, 'wb'))
- log.debug("Popen(%s, cwd=%s, universal_newlines=%s, shell=%s)",
- command, cwd, universal_newlines, shell)
+ istream_ok = "None"
+ if istream:
+ istream_ok = "<valid stream>"
+ log.debug("Popen(%s, cwd=%s, universal_newlines=%s, shell=%s, istream=%s)",
+ command, cwd, universal_newlines, shell, istream_ok)
try:
proc = Popen(command,
env=env,
diff --git a/git/config.py b/git/config.py
index 68d65ae9..b03d9d42 100644
--- a/git/config.py
+++ b/git/config.py
@@ -146,6 +146,51 @@ class SectionConstraint(object):
self._config.__exit__(exception_type, exception_value, traceback)
+class _OMD(OrderedDict):
+ """Ordered multi-dict."""
+
+ def __setitem__(self, key, value):
+ super(_OMD, self).__setitem__(key, [value])
+
+ def add(self, key, value):
+ if key not in self:
+ super(_OMD, self).__setitem__(key, [value])
+ return
+
+ super(_OMD, self).__getitem__(key).append(value)
+
+ def setall(self, key, values):
+ super(_OMD, self).__setitem__(key, values)
+
+ def __getitem__(self, key):
+ return super(_OMD, self).__getitem__(key)[-1]
+
+ def getlast(self, key):
+ return super(_OMD, self).__getitem__(key)[-1]
+
+ def setlast(self, key, value):
+ if key not in self:
+ super(_OMD, self).__setitem__(key, [value])
+ return
+
+ prior = super(_OMD, self).__getitem__(key)
+ prior[-1] = value
+
+ def get(self, key, default=None):
+ return super(_OMD, self).get(key, [default])[-1]
+
+ def getall(self, key):
+ return super(_OMD, self).__getitem__(key)
+
+ def items(self):
+ """List of (key, last value for key)."""
+ return [(k, self[k]) for k in self]
+
+ def items_all(self):
+ """List of (key, list of values for key)."""
+ return [(k, self.getall(k)) for k in self]
+
+
class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, object)):
"""Implements specifics required to read git style configuration files.
@@ -200,7 +245,7 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
contents into ours. This makes it impossible to write back an individual configuration file.
Thus, if you want to modify a single configuration file, turn this off to leave the original
dataset unaltered when reading it."""
- cp.RawConfigParser.__init__(self, dict_type=OrderedDict)
+ cp.RawConfigParser.__init__(self, dict_type=_OMD)
# Used in python 3, needs to stay in sync with sections for underlying implementation to work
if not hasattr(self, '_proxies'):
@@ -348,7 +393,8 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
is_multi_line = True
optval = string_decode(optval[1:])
# end handle multi-line
- cursect[optname] = optval
+ # preserves multiple values for duplicate optnames
+ cursect.add(optname, optval)
else:
# check if it's an option with no value - it's just ignored by git
if not self.OPTVALUEONLY.match(line):
@@ -362,7 +408,8 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
is_multi_line = False
line = line[:-1]
# end handle quotations
- cursect[optname] += string_decode(line)
+ optval = cursect.getlast(optname)
+ cursect.setlast(optname, optval + string_decode(line))
# END parse section or option
# END while reading
@@ -442,9 +489,12 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
git compatible format"""
def write_section(name, section_dict):
fp.write(("[%s]\n" % name).encode(defenc))
- for (key, value) in section_dict.items():
- if key != "__name__":
- fp.write(("\t%s = %s\n" % (key, self._value_to_string(value).replace('\n', '\n\t'))).encode(defenc))
+ for (key, values) in section_dict.items_all():
+ if key == "__name__":
+ continue
+
+ for v in values:
+ fp.write(("\t%s = %s\n" % (key, self._value_to_string(v).replace('\n', '\n\t'))).encode(defenc))
# END if key is not __name__
# END section writing
@@ -457,6 +507,22 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
""":return: list((option, value), ...) pairs of all items in the given section"""
return [(k, v) for k, v in super(GitConfigParser, self).items(section_name) if k != '__name__']
+ def items_all(self, section_name):
+ """:return: list((option, [values...]), ...) pairs of all items in the given section"""
+ rv = _OMD(self._defaults)
+
+ for k, vs in self._sections[section_name].items_all():
+ if k == '__name__':
+ continue
+
+ if k in rv and rv.getall(k) == vs:
+ continue
+
+ for v in vs:
+ rv.add(k, v)
+
+ return rv.items_all()
+
@needs_values
def write(self):
"""Write changes to our file, if there are changes at all
@@ -508,7 +574,11 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
return self._read_only
def get_value(self, section, option, default=None):
- """
+ """Get an option's value.
+
+ If multiple values are specified for this option in the section, the
+ last one specified is returned.
+
:param default:
If not None, the given default value will be returned in case
the option did not exist
@@ -523,6 +593,31 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
return default
raise
+ return self._string_to_value(valuestr)
+
+ def get_values(self, section, option, default=None):
+ """Get an option's values.
+
+ If multiple values are specified for this option in the section, all are
+ returned.
+
+ :param default:
+ If not None, a list containing the given default value will be
+ returned in case the option did not exist
+ :return: a list of properly typed values, either int, float or string
+
+ :raise TypeError: in case the value could not be understood
+ Otherwise the exceptions known to the ConfigParser will be raised."""
+ try:
+ lst = self._sections[section].getall(option)
+ except Exception:
+ if default is not None:
+ return [default]
+ raise
+
+ return [self._string_to_value(valuestr) for valuestr in lst]
+
+ def _string_to_value(self, valuestr):
types = (int, float)
for numtype in types:
try:
@@ -545,7 +640,9 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
return True
if not isinstance(valuestr, string_types):
- raise TypeError("Invalid value type: only int, long, float and str are allowed", valuestr)
+ raise TypeError(
+ "Invalid value type: only int, long, float and str are allowed",
+ valuestr)
return valuestr
@@ -572,6 +669,25 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
self.set(section, option, self._value_to_string(value))
return self
+ @needs_values
+ @set_dirty_and_flush_changes
+ def add_value(self, section, option, value):
+ """Adds a value for the given option in section.
+ It will create the section if required, and will not throw as opposed to the default
+ ConfigParser 'set' method. The value becomes the new value of the option as returned
+ by 'get_value', and appends to the list of values returned by 'get_values`'.
+
+ :param section: Name of the section in which the option resides or should reside
+ :param option: Name of the option
+
+ :param value: Value to add to option. It must be a string or convertible
+ to a string
+ :return: this instance"""
+ if not self.has_section(section):
+ self.add_section(section)
+ self._sections[section].add(option, self._value_to_string(value))
+ return self
+
def rename_section(self, section, new_name):
"""rename the given section to new_name
:raise ValueError: if section doesn't exit
@@ -584,8 +700,9 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
raise ValueError("Destination section '%s' already exists" % new_name)
super(GitConfigParser, self).add_section(new_name)
- for k, v in self.items(section):
- self.set(new_name, k, self._value_to_string(v))
+ new_section = self._sections[new_name]
+ for k, vs in self.items_all(section):
+ new_section.setall(k, vs)
# end for each value to copy
# This call writes back the changes, which is why we don't have the respective decorator
diff --git a/git/ext/gitdb b/git/ext/gitdb
-Subproject 3e71833044718f60be36ac47494668469a2a235
+Subproject c0fd43b5ff8c356fcf9cdebbbbd1803a502b465
diff --git a/git/objects/tag.py b/git/objects/tag.py
index 19cb04bf..4295a03a 100644
--- a/git/objects/tag.py
+++ b/git/objects/tag.py
@@ -59,8 +59,9 @@ class TagObject(base.Object):
self.tag = lines[2][4:] # tag <tag name>
- tagger_info = lines[3] # tagger <actor> <date>
- self.tagger, self.tagged_date, self.tagger_tz_offset = parse_actor_and_date(tagger_info)
+ if len(lines) > 3:
+ tagger_info = lines[3] # tagger <actor> <date>
+ self.tagger, self.tagged_date, self.tagger_tz_offset = parse_actor_and_date(tagger_info)
# line 4 empty - it could mark the beginning of the next header
# in case there really is no message, it would not exist. Otherwise
diff --git a/git/remote.py b/git/remote.py
index 8c28e636..4f32540f 100644
--- a/git/remote.py
+++ b/git/remote.py
@@ -694,6 +694,8 @@ class Remote(LazyMixin, Iterable):
msg += "Will ignore extra progress lines or fetch head lines."
msg %= (l_fil, l_fhi)
log.debug(msg)
+ log.debug("info lines: " + str(fetch_info_lines))
+ log.debug("head info : " + str(fetch_head_info))
if l_fil < l_fhi:
fetch_head_info = fetch_head_info[:l_fil]
else:
diff --git a/git/repo/base.py b/git/repo/base.py
index 58f11e51..f3587080 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -559,11 +559,11 @@ class Repo(object):
return res
def is_ancestor(self, ancestor_rev, rev):
- """Check if a commit is an ancestor of another
+ """Check if a commit is an ancestor of another
:param ancestor_rev: Rev which should be an ancestor
:param rev: Rev to test against ancestor_rev
- :return: ``True``, ancestor_rev is an accestor to rev.
+ :return: ``True``, ancestor_rev is an ancestor to rev.
"""
try:
self.git.merge_base(ancestor_rev, rev, is_ancestor=True)
@@ -931,7 +931,7 @@ class Repo(object):
return cls(path, odbt=odbt)
@classmethod
- def _clone(cls, git, url, path, odb_default_type, progress, **kwargs):
+ def _clone(cls, git, url, path, odb_default_type, progress, multi_options=None, **kwargs):
if progress is not None:
progress = to_progress_instance(progress)
@@ -953,7 +953,10 @@ class Repo(object):
sep_dir = kwargs.get('separate_git_dir')
if sep_dir:
kwargs['separate_git_dir'] = Git.polish_url(sep_dir)
- proc = git.clone(Git.polish_url(url), clone_path, with_extended_output=True, as_process=True,
+ multi = None
+ if multi_options:
+ multi = ' '.join(multi_options).split(' ')
+ proc = git.clone(multi, Git.polish_url(url), clone_path, with_extended_output=True, as_process=True,
v=True, universal_newlines=True, **add_progress(kwargs, git, progress))
if progress:
handle_process_output(proc, None, progress.new_message_handler(), finalize_process, decode_streams=False)
@@ -983,33 +986,38 @@ class Repo(object):
# END handle remote repo
return repo
- def clone(self, path, progress=None, **kwargs):
+ def clone(self, path, progress=None, multi_options=None, **kwargs):
"""Create a clone from this repository.
:param path: is the full path of the new repo (traditionally ends with ./<name>.git).
:param progress: See 'git.remote.Remote.push'.
+ :param multi_options: A list of Clone options that can be provided multiple times. One
+ option per list item which is passed exactly as specified to clone.
+ For example ['--config core.filemode=false', '--config core.ignorecase',
+ '--recurse-submodule=repo1_path', '--recurse-submodule=repo2_path']
:param kwargs:
* odbt = ObjectDatabase Type, allowing to determine the object database
implementation used by the returned Repo instance
* All remaining keyword arguments are given to the git-clone command
:return: ``git.Repo`` (the newly cloned repo)"""
- return self._clone(self.git, self.common_dir, path, type(self.odb), progress, **kwargs)
+ return self._clone(self.git, self.common_dir, path, type(self.odb), progress, multi_options, **kwargs)
@classmethod
- def clone_from(cls, url, to_path, progress=None, env=None, **kwargs):
+ def clone_from(cls, url, to_path, progress=None, env=None, multi_options=None, **kwargs):
"""Create a clone from the given URL
:param url: valid git url, see http://www.kernel.org/pub/software/scm/git/docs/git-clone.html#URLS
:param to_path: Path to which the repository should be cloned to
:param progress: See 'git.remote.Remote.push'.
:param env: Optional dictionary containing the desired environment variables.
+ :param mutli_options: See ``clone`` method
:param kwargs: see the ``clone`` method
:return: Repo instance pointing to the cloned directory"""
git = Git(os.getcwd())
if env is not None:
git.update_environment(**env)
- return cls._clone(git, url, to_path, GitCmdObjectDB, progress, **kwargs)
+ return cls._clone(git, url, to_path, GitCmdObjectDB, progress, multi_options, **kwargs)
def archive(self, ostream, treeish=None, prefix=None, **kwargs):
"""Archive the tree at the given revision.
diff --git a/git/test/fixtures/git_config_multiple b/git/test/fixtures/git_config_multiple
new file mode 100644
index 00000000..03a97568
--- /dev/null
+++ b/git/test/fixtures/git_config_multiple
@@ -0,0 +1,7 @@
+[section0]
+ option0 = value0
+
+[section1]
+ option1 = value1a
+ option1 = value1b
+ other_option1 = other_value1
diff --git a/git/test/test_config.py b/git/test/test_config.py
index 4d6c8236..93f94748 100644
--- a/git/test/test_config.py
+++ b/git/test/test_config.py
@@ -11,7 +11,7 @@ from git import (
GitConfigParser
)
from git.compat import string_types
-from git.config import cp
+from git.config import _OMD, cp
from git.test.lib import (
TestCase,
fixture_path,
@@ -265,3 +265,110 @@ class TestBase(TestCase):
with self.assertRaises(cp.NoOptionError):
cr.get_value('color', 'ui')
+
+ def test_multiple_values(self):
+ file_obj = self._to_memcache(fixture_path('git_config_multiple'))
+ with GitConfigParser(file_obj, read_only=False) as cw:
+ self.assertEqual(cw.get('section0', 'option0'), 'value0')
+ self.assertEqual(cw.get_values('section0', 'option0'), ['value0'])
+ self.assertEqual(cw.items('section0'), [('option0', 'value0')])
+
+ # Where there are multiple values, "get" returns the last.
+ self.assertEqual(cw.get('section1', 'option1'), 'value1b')
+ self.assertEqual(cw.get_values('section1', 'option1'),
+ ['value1a', 'value1b'])
+ self.assertEqual(cw.items('section1'),
+ [('option1', 'value1b'),
+ ('other_option1', 'other_value1')])
+ self.assertEqual(cw.items_all('section1'),
+ [('option1', ['value1a', 'value1b']),
+ ('other_option1', ['other_value1'])])
+ with self.assertRaises(KeyError):
+ cw.get_values('section1', 'missing')
+
+ self.assertEqual(cw.get_values('section1', 'missing', 1), [1])
+ self.assertEqual(cw.get_values('section1', 'missing', 's'), ['s'])
+
+ def test_multiple_values_rename(self):
+ file_obj = self._to_memcache(fixture_path('git_config_multiple'))
+ with GitConfigParser(file_obj, read_only=False) as cw:
+ cw.rename_section('section1', 'section2')
+ cw.write()
+ file_obj.seek(0)
+ cr = GitConfigParser(file_obj, read_only=True)
+ self.assertEqual(cr.get_value('section2', 'option1'), 'value1b')
+ self.assertEqual(cr.get_values('section2', 'option1'),
+ ['value1a', 'value1b'])
+ self.assertEqual(cr.items('section2'),
+ [('option1', 'value1b'),
+ ('other_option1', 'other_value1')])
+ self.assertEqual(cr.items_all('section2'),
+ [('option1', ['value1a', 'value1b']),
+ ('other_option1', ['other_value1'])])
+
+ def test_multiple_to_single(self):
+ file_obj = self._to_memcache(fixture_path('git_config_multiple'))
+ with GitConfigParser(file_obj, read_only=False) as cw:
+ cw.set_value('section1', 'option1', 'value1c')
+
+ cw.write()
+ file_obj.seek(0)
+ cr = GitConfigParser(file_obj, read_only=True)
+ self.assertEqual(cr.get_value('section1', 'option1'), 'value1c')
+ self.assertEqual(cr.get_values('section1', 'option1'), ['value1c'])
+ self.assertEqual(cr.items('section1'),
+ [('option1', 'value1c'),
+ ('other_option1', 'other_value1')])
+ self.assertEqual(cr.items_all('section1'),
+ [('option1', ['value1c']),
+ ('other_option1', ['other_value1'])])
+
+ def test_single_to_multiple(self):
+ file_obj = self._to_memcache(fixture_path('git_config_multiple'))
+ with GitConfigParser(file_obj, read_only=False) as cw:
+ cw.add_value('section1', 'other_option1', 'other_value1a')
+
+ cw.write()
+ file_obj.seek(0)
+ cr = GitConfigParser(file_obj, read_only=True)
+ self.assertEqual(cr.get_value('section1', 'option1'), 'value1b')
+ self.assertEqual(cr.get_values('section1', 'option1'),
+ ['value1a', 'value1b'])
+ self.assertEqual(cr.get_value('section1', 'other_option1'),
+ 'other_value1a')
+ self.assertEqual(cr.get_values('section1', 'other_option1'),
+ ['other_value1', 'other_value1a'])
+ self.assertEqual(cr.items('section1'),
+ [('option1', 'value1b'),
+ ('other_option1', 'other_value1a')])
+ self.assertEqual(
+ cr.items_all('section1'),
+ [('option1', ['value1a', 'value1b']),
+ ('other_option1', ['other_value1', 'other_value1a'])])
+
+ def test_add_to_multiple(self):
+ file_obj = self._to_memcache(fixture_path('git_config_multiple'))
+ with GitConfigParser(file_obj, read_only=False) as cw:
+ cw.add_value('section1', 'option1', 'value1c')
+ cw.write()
+ file_obj.seek(0)
+ cr = GitConfigParser(file_obj, read_only=True)
+ self.assertEqual(cr.get_value('section1', 'option1'), 'value1c')
+ self.assertEqual(cr.get_values('section1', 'option1'),
+ ['value1a', 'value1b', 'value1c'])
+ self.assertEqual(cr.items('section1'),
+ [('option1', 'value1c'),
+ ('other_option1', 'other_value1')])
+ self.assertEqual(cr.items_all('section1'),
+ [('option1', ['value1a', 'value1b', 'value1c']),
+ ('other_option1', ['other_value1'])])
+
+ def test_setlast(self):
+ # Test directly, not covered by higher-level tests.
+ omd = _OMD()
+ omd.setlast('key', 'value1')
+ self.assertEqual(omd['key'], 'value1')
+ self.assertEqual(omd.getall('key'), ['value1'])
+ omd.setlast('key', 'value2')
+ self.assertEqual(omd['key'], 'value2')
+ self.assertEqual(omd.getall('key'), ['value2'])
diff --git a/git/test/test_remote.py b/git/test/test_remote.py
index f3a214cb..99949b9e 100644
--- a/git/test/test_remote.py
+++ b/git/test/test_remote.py
@@ -638,7 +638,7 @@ class TestRemote(TestBase):
def test_fetch_error(self):
rem = self.rorepo.remote('origin')
- with self.assertRaisesRegex(GitCommandError, "Couldn't find remote ref __BAD_REF__"):
+ with self.assertRaisesRegex(GitCommandError, "[Cc]ouldn't find remote ref __BAD_REF__"):
rem.fetch('__BAD_REF__')
@with_rw_repo('0.1.6', bare=False)
diff --git a/git/test/test_repo.py b/git/test/test_repo.py
index 7fc49f3b..0577bd58 100644
--- a/git/test/test_repo.py
+++ b/git/test/test_repo.py
@@ -229,6 +229,22 @@ class TestRepo(TestBase):
Repo.clone_from(original_repo.git_dir, pathlib.Path(rw_dir) / "clone_pathlib")
+ @with_rw_directory
+ def test_clone_from_pathlib_withConfig(self, rw_dir):
+ if pathlib is None: # pythons bellow 3.4 don't have pathlib
+ raise SkipTest("pathlib was introduced in 3.4")
+
+ original_repo = Repo.init(osp.join(rw_dir, "repo"))
+
+ cloned = Repo.clone_from(original_repo.git_dir, pathlib.Path(rw_dir) / "clone_pathlib_withConfig",
+ multi_options=["--recurse-submodules=repo",
+ "--config core.filemode=false",
+ "--config submodule.repo.update=checkout"])
+
+ assert_equal(cloned.config_reader().get_value('submodule', 'active'), 'repo')
+ assert_equal(cloned.config_reader().get_value('core', 'filemode'), False)
+ assert_equal(cloned.config_reader().get_value('submodule "repo"', 'update'), 'checkout')
+
@with_rw_repo('HEAD')
def test_max_chunk_size(self, repo):
class TestOutputStream(object):
diff --git a/git/test/test_util.py b/git/test/test_util.py
index 9c993205..b5f9d222 100644
--- a/git/test/test_util.py
+++ b/git/test/test_util.py
@@ -212,6 +212,12 @@ class TestUtils(TestBase):
self.assertIsInstance(Actor.author(cr), Actor)
# END assure config reader is handled
+ def test_actor_from_string(self):
+ self.assertEqual(Actor._from_string("name"), Actor("name", None))
+ self.assertEqual(Actor._from_string("name <>"), Actor("name", ""))
+ self.assertEqual(Actor._from_string("name last another <some-very-long-email@example.com>"),
+ Actor("name last another", "some-very-long-email@example.com"))
+
@ddt.data(('name', ''), ('name', 'prefix_'))
def test_iterable_list(self, case):
name, prefix = case
diff --git a/git/util.py b/git/util.py
index a3b1fbfb..3ba58857 100644
--- a/git/util.py
+++ b/git/util.py
@@ -534,8 +534,8 @@ class Actor(object):
can be committers and authors or anything with a name and an email as
mentioned in the git log entries."""
# PRECOMPILED REGEX
- name_only_regex = re.compile(r'<(.+)>')
- name_email_regex = re.compile(r'(.*) <(.+?)>')
+ name_only_regex = re.compile(r'<(.*)>')
+ name_email_regex = re.compile(r'(.*) <(.*?)>')
# ENVIRONMENT VARIABLES
# read when creating new commits
diff --git a/requirements.txt b/requirements.txt
index 39644606..63d5ddfe 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1 @@
-gitdb>=0.6.4
-ddt>=1.1.1
+gitdb2 (>=2.0.0)
diff --git a/setup.py b/setup.py
index cb0300f7..49288f69 100755
--- a/setup.py
+++ b/setup.py
@@ -19,6 +19,9 @@ with open(path.join(path.dirname(__file__), 'VERSION')) as v:
with open('requirements.txt') as reqs_file:
requirements = reqs_file.read().splitlines()
+with open('test-requirements.txt') as reqs_file:
+ test_requirements = reqs_file.read().splitlines()
+
class build_py(_build_py):
@@ -63,10 +66,6 @@ def _stamp_version(filename):
print("WARNING: Couldn't find version line in file %s" % filename, file=sys.stderr)
-install_requires = ['gitdb2 >= 2.0.0']
-test_requires = ['ddt>=1.1.1']
-# end
-
setup(
name="GitPython",
cmdclass={'build_py': build_py, 'sdist': sdist},
@@ -81,9 +80,8 @@ setup(
package_dir={'git': 'git'},
license="BSD License",
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
- requires=['gitdb2 (>=2.0.0)'],
- install_requires=install_requires,
- test_requirements=test_requires + install_requires,
+ install_requires=requirements,
+ tests_require=requirements + test_requirements,
zip_safe=False,
long_description="""GitPython is a python library used to interact with Git repositories""",
classifiers=[
@@ -110,6 +108,6 @@ setup(
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
- "Programming Language :: Python :: 3.7",
+ "Programming Language :: Python :: 3.7"
]
)
diff --git a/test-requirements.txt b/test-requirements.txt
index 1cea3aa2..ec0e4c56 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1,6 +1,6 @@
--r requirements.txt
-
+ddt>=1.1.1
coverage
flake8
nose
-mock; python_version=='2.7' \ No newline at end of file
+tox
+mock; python_version=='2.7'