commit - 0c434308d1ee1b1b9abd345f147baa4ddb578585
commit + e13cc98a4f8ca8afc857730719164e4608c28d3f
blob - 2762e93d176e655d090d0063bd9b2fa9c837f649
blob + c68de5a472fda8cf8f731d472dae6ec625d41c94
--- .github/workflows/pythonpackage.yml
+++ .github/workflows/pythonpackage.yml
name: Python package
-on: [push, pull_request]
+on:
+ push:
+ pull_request:
+ schedule:
+ - cron: '0 6 * * *' # Daily 6AM UTC build
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
- python-version: [3.5, 3.6, 3.7, 3.8, 3.9, pypy3]
+ python-version: [3.5, 3.6, 3.7, 3.8, 3.9, 3.10-dev, pypy3]
exclude:
# sqlite3 exit handling seems to get in the way
- os: macos-latest
if: "matrix.os != 'windows-latest' && matrix.python-version != 'pypy3'"
- name: Install mypy
run: |
- pip install -U mypy
+ pip install -U mypy types-paramiko types-certifi
if: "matrix.python-version != 'pypy3'"
- name: Style checks
run: |
blob - 7eaeae907ad187a5b602325d2be7510490c74357
blob + 953bbaf1c3d45903f5987567909163d9d9385983
--- NEWS
+++ NEWS
+0.20.25 2021-08-23
+
+ * Fix ``dulwich`` script when installed via setup.py.
+ (Dan Villiom Podlaski Christiansen)
+
+ * Make default file mask consistent
+ with Git. (Dan Villiom Podlaski Christiansen, #884)
+
+0.20.24 2021-07-18
+
+ * config: disregard UTF-8 BOM when reading file.
+ (Dan Villiom Podlaski Christiansen)
+
+ * Skip lines with spaces only in .gitignore. (Andrey Torsunov, #878)
+
+ * Add a separate HTTPProxyUnauthorized exception for 407 errors.
+ (Jelmer Vernooij, #822)
+
+ * Split out a AbstractHTTPGitClient class.
+ (Jelmer Vernooij)
+
0.20.23 2021-05-24
* Fix installation of GPG during package publishing.
BUG FIXES
* Avoid ``PermissionError``, since it is Python3-specific.
- (Jelmer Vernooij)
+ (Jelmer Vernooij)
* Fix regression that added a dependency on C git for the
test suite. (Jelmer Vernooij, #720)
blob - e54aeeed9312dfef00c8d803064cdd5db15255df
blob + f42454ceef107f4fb2a83f9dfedde4aad6197efb
--- PKG-INFO
+++ PKG-INFO
Metadata-Version: 2.1
Name: dulwich
-Version: 0.20.23
+Version: 0.20.25
Summary: Python Git Library
Home-page: https://www.dulwich.io/
Author: Jelmer Vernooij
Project-URL: Bug Tracker, https://github.com/dulwich/dulwich/issues
Project-URL: Repository, https://www.dulwich.io/code/
Project-URL: GitHub, https://github.com/dulwich/dulwich
-Description: Dulwich
- =======
-
- This is the Dulwich project.
-
- It aims to provide an interface to git repos (both local and remote) that
- doesn't call out to git directly but instead uses pure Python.
-
- **Main website**: <https://www.dulwich.io/>
-
- **License**: Apache License, version 2 or GNU General Public License, version 2 or later.
-
- The project is named after the part of London that Mr. and Mrs. Git live in
- in the particular Monty Python sketch.
-
- Installation
- ------------
-
- By default, Dulwich' setup.py will attempt to build and install the optional C
- extensions. The reason for this is that they significantly improve the performance
- since some low-level operations that are executed often are much slower in CPython.
-
- If you don't want to install the C bindings, specify the --pure argument to setup.py::
-
- $ python setup.py --pure install
-
- or if you are installing from pip::
-
- $ pip install dulwich --global-option="--pure"
-
- Note that you can also specify --global-option in a
- `requirements.txt <https://pip.pypa.io/en/stable/reference/pip_install/#requirement-specifiers>`_
- file, e.g. like this::
-
- dulwich --global-option=--pure
-
- Getting started
- ---------------
-
- Dulwich comes with both a lower-level API and higher-level plumbing ("porcelain").
-
- For example, to use the lower level API to access the commit message of the
- last commit::
-
- >>> from dulwich.repo import Repo
- >>> r = Repo('.')
- >>> r.head()
- '57fbe010446356833a6ad1600059d80b1e731e15'
- >>> c = r[r.head()]
- >>> c
- <Commit 015fc1267258458901a94d228e39f0a378370466>
- >>> c.message
- 'Add note about encoding.\n'
-
- And to print it using porcelain::
-
- >>> from dulwich import porcelain
- >>> porcelain.log('.', max_entries=1)
- --------------------------------------------------
- commit: 57fbe010446356833a6ad1600059d80b1e731e15
- Author: Jelmer Vernooij <jelmer@jelmer.uk>
- Date: Sat Apr 29 2017 23:57:34 +0000
-
- Add note about encoding.
-
- Further documentation
- ---------------------
-
- The dulwich documentation can be found in docs/ and built by running ``make
- doc``. It can also be found `on the web <https://www.dulwich.io/docs/>`_.
-
- Help
- ----
-
- There is a *#dulwich* IRC channel on the `OFTC <https://www.oftc.net/>`_, and
- `dulwich-announce <https://groups.google.com/forum/#!forum/dulwich-announce>`_
- and `dulwich-discuss <https://groups.google.com/forum/#!forum/dulwich-discuss>`_
- mailing lists.
-
- Contributing
- ------------
-
- For a full list of contributors, see the git logs or `AUTHORS <AUTHORS>`_.
-
- If you'd like to contribute to Dulwich, see the `CONTRIBUTING <CONTRIBUTING.rst>`_
- file and `list of open issues <https://github.com/dulwich/dulwich/issues>`_.
-
- Supported versions of Python
- ----------------------------
-
- At the moment, Dulwich supports (and is tested on) CPython 3.5 and later and
- Pypy.
-
- The latest release series to support Python 2.x was the 0.19 series. See
- the 0.19 branch in the Dulwich git repository.
-
Keywords: git vcs
Platform: UNKNOWN
Classifier: Development Status :: 4 - Beta
Provides-Extra: https
Provides-Extra: pgp
Provides-Extra: watch
+License-File: COPYING
+License-File: AUTHORS
+
+Dulwich
+=======
+
+This is the Dulwich project.
+
+It aims to provide an interface to git repos (both local and remote) that
+doesn't call out to git directly but instead uses pure Python.
+
+**Main website**: <https://www.dulwich.io/>
+
+**License**: Apache License, version 2 or GNU General Public License, version 2 or later.
+
+The project is named after the part of London that Mr. and Mrs. Git live in
+in the particular Monty Python sketch.
+
+Installation
+------------
+
+By default, Dulwich' setup.py will attempt to build and install the optional C
+extensions. The reason for this is that they significantly improve the performance
+since some low-level operations that are executed often are much slower in CPython.
+
+If you don't want to install the C bindings, specify the --pure argument to setup.py::
+
+ $ python setup.py --pure install
+
+or if you are installing from pip::
+
+ $ pip install dulwich --global-option="--pure"
+
+Note that you can also specify --global-option in a
+`requirements.txt <https://pip.pypa.io/en/stable/reference/pip_install/#requirement-specifiers>`_
+file, e.g. like this::
+
+ dulwich --global-option=--pure
+
+Getting started
+---------------
+
+Dulwich comes with both a lower-level API and higher-level plumbing ("porcelain").
+
+For example, to use the lower level API to access the commit message of the
+last commit::
+
+ >>> from dulwich.repo import Repo
+ >>> r = Repo('.')
+ >>> r.head()
+ '57fbe010446356833a6ad1600059d80b1e731e15'
+ >>> c = r[r.head()]
+ >>> c
+ <Commit 015fc1267258458901a94d228e39f0a378370466>
+ >>> c.message
+ 'Add note about encoding.\n'
+
+And to print it using porcelain::
+
+ >>> from dulwich import porcelain
+ >>> porcelain.log('.', max_entries=1)
+ --------------------------------------------------
+ commit: 57fbe010446356833a6ad1600059d80b1e731e15
+ Author: Jelmer Vernooij <jelmer@jelmer.uk>
+ Date: Sat Apr 29 2017 23:57:34 +0000
+
+ Add note about encoding.
+
+Further documentation
+---------------------
+
+The dulwich documentation can be found in docs/ and built by running ``make
+doc``. It can also be found `on the web <https://www.dulwich.io/docs/>`_.
+
+Help
+----
+
+There is a *#dulwich* IRC channel on the `OFTC <https://www.oftc.net/>`_, and
+`dulwich-announce <https://groups.google.com/forum/#!forum/dulwich-announce>`_
+and `dulwich-discuss <https://groups.google.com/forum/#!forum/dulwich-discuss>`_
+mailing lists.
+
+Contributing
+------------
+
+For a full list of contributors, see the git logs or `AUTHORS <AUTHORS>`_.
+
+If you'd like to contribute to Dulwich, see the `CONTRIBUTING <CONTRIBUTING.rst>`_
+file and `list of open issues <https://github.com/dulwich/dulwich/issues>`_.
+
+Supported versions of Python
+----------------------------
+
+At the moment, Dulwich supports (and is tested on) CPython 3.5 and later and
+Pypy.
+
+The latest release series to support Python 2.x was the 0.19 series. See
+the 0.19 branch in the Dulwich git repository.
+
+
blob - 23df2b69b8598fb4a22f6c2fa5a9ca1d699e0bf6 (mode 644)
blob + /dev/null
--- build.cmd
+++ /dev/null
-@echo off
-:: To build extensions for 64 bit Python 3, we need to configure environment
-:: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of:
-:: MS Windows SDK for Windows 7 and .NET Framework 4
-::
-:: More details at:
-:: https://github.com/cython/cython/wiki/CythonExtensionsOnWindows
-
-IF "%DISTUTILS_USE_SDK%"=="1" (
- ECHO Configuring environment to build with MSVC on a 64bit architecture
- ECHO Using Windows SDK 7.1
- "C:\Program Files\Microsoft SDKs\Windows\v7.1\Setup\WindowsSdkVer.exe" -q -version:v7.1
- CALL "C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\SetEnv.cmd" /x64 /release
- SET MSSdk=1
- REM Need the following to allow tox to see the SDK compiler
- SET TOX_TESTENV_PASSENV=DISTUTILS_USE_SDK MSSdk INCLUDE LIB
-) ELSE (
- ECHO Using default MSVC build environment
-)
-
-CALL %*
blob - 46661a032954a650806d5726c2539e2e0ee12cba
blob + 17705982feb817acf7ec992a221782feac8d18e6
--- dulwich/__init__.py
+++ dulwich/__init__.py
"""Python implementation of the Git file formats and protocols."""
-__version__ = (0, 20, 23)
+__version__ = (0, 20, 25)
blob - /dev/null
blob + 3453a7e6d4cae4b300f04411042416a7eb7f3963 (mode 644)
--- /dev/null
+++ dulwich/__main__.py
+from . import cli
+
+if __name__ == "__main__":
+ cli._main()
blob - bcdd094b0d0e89eb1bfa26849b831f4fa64a8d1f
blob + d0cedf7479da398329c5c53b34651510a932c13e
--- dulwich/cli.py
+++ dulwich/cli.py
def main(argv=None):
if argv is None:
- argv = sys.argv
+ argv = sys.argv[1:]
if len(argv) < 1:
print("Usage: dulwich <%s> [OPTIONS...]" % ("|".join(commands.keys())))
return cmd_kls().run(argv[1:])
-if __name__ == "__main__":
+def _main():
if "DULWICH_PDB" in os.environ and getattr(signal, "SIGQUIT", None):
signal.signal(signal.SIGQUIT, signal_quit) # type: ignore
signal.signal(signal.SIGINT, signal_int)
- sys.exit(main(sys.argv[1:]))
+ sys.exit(main())
+
+
+if __name__ == "__main__":
+ _main()
blob - 8cc1f40c9d55657633f6e2f98cd540f28b8ce803
blob + 2527853c78580ce737d656858002f15513a3bc57
--- dulwich/client.py
+++ dulwich/client.py
def __init__(self, www_authenticate, url):
Exception.__init__(self, "No valid credentials provided")
self.www_authenticate = www_authenticate
+ self.url = url
+
+
+class HTTPProxyUnauthorized(Exception):
+ """Raised when proxy authentication fails."""
+
+ def __init__(self, proxy_authenticate, url):
+ Exception.__init__(self, "No valid proxy credentials provided")
+ self.proxy_authenticate = proxy_authenticate
self.url = url
Args:
path: Remote path to fetch from
determine_wants: Function determine what refs
- to fetch. Receives dictionary of name->sha, should return
- list of shas to fetch.
+ to fetch. Receives dictionary of name->sha, should return
+ list of shas to fetch.
graph_walker: Object with next() and ack().
pack_data: Callback called for each bit of data in the pack
progress: Callback for progress reports (strings)
Args:
path: Repository path (as bytestring)
update_refs: Function to determine changes to remote refs.
- Receive dict with existing remote refs, returns dict with
- changed refs (name -> sha, where sha=ZERO_SHA for deletions)
+ Receive dict with existing remote refs, returns dict with
+ changed refs (name -> sha, where sha=ZERO_SHA for deletions)
generate_pack_data: Function that can return a tuple with
- number of objects and pack data to upload.
+ number of objects and pack data to upload.
progress: Optional callback called with progress updates
Returns:
Args:
path: Remote path to fetch from
determine_wants: Function determine what refs
- to fetch. Receives dictionary of name->sha, should return
- list of shas to fetch.
+ to fetch. Receives dictionary of name->sha, should return
+ list of shas to fetch.
graph_walker: Object with next() and ack().
pack_data: Callback called for each bit of data in the pack
progress: Callback for progress reports (strings)
Args:
path: Repository path (as bytestring)
update_refs: Function to determine changes to remote refs.
- Receive dict with existing remote refs, returns dict with
- changed refs (name -> sha, where sha=ZERO_SHA for deletions)
- with number of items and pack data to upload.
+ Receive dict with existing remote refs, returns dict with
+ changed refs (name -> sha, where sha=ZERO_SHA for deletions)
+ with number of items and pack data to upload.
progress: Optional progress function
Returns:
path: Path to fetch from (as bytestring)
target: Target repository to fetch into
determine_wants: Optional function determine what refs
- to fetch. Receives dictionary of name->sha, should return
- list of shas to fetch. Defaults to all shas.
+ to fetch. Receives dictionary of name->sha, should return
+ list of shas to fetch. Defaults to all shas.
progress: Optional progress function
depth: Shallow fetch depth
Args:
path: Remote path to fetch from
determine_wants: Function determine what refs
- to fetch. Receives dictionary of name->sha, should return
- list of shas to fetch.
+ to fetch. Receives dictionary of name->sha, should return
+ list of shas to fetch.
graph_walker: Object with next() and ack().
pack_data: Callback called for each bit of data in the pack
progress: Callback for progress reports (strings)
if proxy_server is not None:
if proxy_manager_cls is None:
proxy_manager_cls = urllib3.ProxyManager
- # `urllib3` requires a `str` object in both Python 2 and 3, while
- # `ConfigDict` coerces entries to `bytes` on Python 3. Compensate.
if not isinstance(proxy_server, str):
proxy_server = proxy_server.decode()
manager = proxy_manager_cls(proxy_server, headers=headers, **kwargs)
return manager
-class HttpGitClient(GitClient):
- def __init__(
- self,
- base_url,
- dumb=None,
- pool_manager=None,
- config=None,
- username=None,
- password=None,
- **kwargs
- ):
- self._base_url = base_url.rstrip("/") + "/"
- self._username = username
- self._password = password
- self.dumb = dumb
+class AbstractHttpGitClient(GitClient):
+ """Abstract base class for HTTP Git Clients.
- if pool_manager is None:
- self.pool_manager = default_urllib3_manager(config)
- else:
- self.pool_manager = pool_manager
+ This is agonistic of the actual HTTP implementation.
- if username is not None:
- # No escaping needed: ":" is not allowed in username:
- # https://tools.ietf.org/html/rfc2617#section-2
- credentials = "%s:%s" % (username, password)
- import urllib3.util
-
- basic_auth = urllib3.util.make_headers(basic_auth=credentials)
- self.pool_manager.headers.update(basic_auth)
+ Subclasses should provide an implementation of the
+ _http_request method.
+ """
+ def __init__(self, base_url, dumb=False, **kwargs):
+ self._base_url = base_url.rstrip("/") + "/"
+ self.dumb = dumb
GitClient.__init__(self, **kwargs)
- def get_url(self, path):
- return self._get_url(path).rstrip("/")
-
- @classmethod
- def from_parsedurl(cls, parsedurl, **kwargs):
- password = parsedurl.password
- if password is not None:
- kwargs["password"] = urlunquote(password)
- username = parsedurl.username
- if username is not None:
- kwargs["username"] = urlunquote(username)
- netloc = parsedurl.hostname
- if parsedurl.port:
- netloc = "%s:%s" % (netloc, parsedurl.port)
- if parsedurl.username:
- netloc = "%s@%s" % (parsedurl.username, netloc)
- parsedurl = parsedurl._replace(netloc=netloc)
- return cls(urlunparse(parsedurl), **kwargs)
-
- def __repr__(self):
- return "%s(%r, dumb=%r)" % (
- type(self).__name__,
- self._base_url,
- self.dumb,
- )
-
- def _get_url(self, path):
- if not isinstance(path, str):
- # urllib3.util.url._encode_invalid_chars() converts the path back
- # to bytes using the utf-8 codec.
- path = path.decode("utf-8")
- return urljoin(self._base_url, path).rstrip("/") + "/"
-
def _http_request(self, url, headers=None, data=None, allow_compression=False):
"""Perform HTTP request.
method for the response data.
"""
- req_headers = self.pool_manager.headers.copy()
- if headers is not None:
- req_headers.update(headers)
- req_headers["Pragma"] = "no-cache"
- if allow_compression:
- req_headers["Accept-Encoding"] = "gzip"
- else:
- req_headers["Accept-Encoding"] = "identity"
- if data is None:
- resp = self.pool_manager.request("GET", url, headers=req_headers)
- else:
- resp = self.pool_manager.request(
- "POST", url, headers=req_headers, body=data
- )
-
- if resp.status == 404:
- raise NotGitRepository()
- if resp.status == 401:
- raise HTTPUnauthorized(resp.getheader("WWW-Authenticate"), url)
- if resp.status != 200:
- raise GitProtocolError(
- "unexpected http resp %d for %s" % (resp.status, url)
- )
-
- # TODO: Optimization available by adding `preload_content=False` to the
- # request and just passing the `read` method on instead of going via
- # `BytesIO`, if we can guarantee that the entire response is consumed
- # before issuing the next to still allow for connection reuse from the
- # pool.
- read = BytesIO(resp.data).read
-
- resp.content_type = resp.getheader("Content-Type")
- # Check if geturl() is available (urllib3 version >= 1.23)
- try:
- resp_url = resp.geturl()
- except AttributeError:
- # get_redirect_location() is available for urllib3 >= 1.1
- resp.redirect_location = resp.get_redirect_location()
- else:
- resp.redirect_location = resp_url if resp_url != url else ""
- return resp, read
+ raise NotImplementedError(self._http_request)
def _discover_references(self, service, base_url):
assert base_url[-1] == "/"
resp.close()
def _smart_request(self, service, url, data):
+ """Send a 'smart' HTTP request.
+
+ This is a simple wrapper around _http_request that sets
+ a couple of extra headers.
+ """
assert url[-1] == "/"
url = urljoin(url, service)
result_content_type = "application/x-%s-result" % service
Args:
path: Repository path (as bytestring)
update_refs: Function to determine changes to remote refs.
- Receives dict with existing remote refs, returns dict with
- changed refs (name -> sha, where sha=ZERO_SHA for deletions)
+ Receives dict with existing remote refs, returns dict with
+ changed refs (name -> sha, where sha=ZERO_SHA for deletions)
generate_pack_data: Function that can return a tuple
- with number of elements and pack data to upload.
+ with number of elements and pack data to upload.
progress: Optional progress function
Returns:
refs, _, _ = self._discover_references(b"git-upload-pack", url)
return refs
+ def get_url(self, path):
+ return self._get_url(path).rstrip("/")
+ def _get_url(self, path):
+ return urljoin(self._base_url, path).rstrip("/") + "/"
+
+ @classmethod
+ def from_parsedurl(cls, parsedurl, **kwargs):
+ password = parsedurl.password
+ if password is not None:
+ kwargs["password"] = urlunquote(password)
+ username = parsedurl.username
+ if username is not None:
+ kwargs["username"] = urlunquote(username)
+ netloc = parsedurl.hostname
+ if parsedurl.port:
+ netloc = "%s:%s" % (netloc, parsedurl.port)
+ if parsedurl.username:
+ netloc = "%s@%s" % (parsedurl.username, netloc)
+ parsedurl = parsedurl._replace(netloc=netloc)
+ return cls(urlunparse(parsedurl), **kwargs)
+
+ def __repr__(self):
+ return "%s(%r, dumb=%r)" % (
+ type(self).__name__,
+ self._base_url,
+ self.dumb,
+ )
+
+
+class Urllib3HttpGitClient(AbstractHttpGitClient):
+ def __init__(
+ self,
+ base_url,
+ dumb=None,
+ pool_manager=None,
+ config=None,
+ username=None,
+ password=None,
+ **kwargs
+ ):
+ self._username = username
+ self._password = password
+
+ if pool_manager is None:
+ self.pool_manager = default_urllib3_manager(config)
+ else:
+ self.pool_manager = pool_manager
+
+ if username is not None:
+ # No escaping needed: ":" is not allowed in username:
+ # https://tools.ietf.org/html/rfc2617#section-2
+ credentials = "%s:%s" % (username, password)
+ import urllib3.util
+
+ basic_auth = urllib3.util.make_headers(basic_auth=credentials)
+ self.pool_manager.headers.update(basic_auth)
+
+ super(Urllib3HttpGitClient, self).__init__(
+ base_url=base_url, dumb=dumb, **kwargs)
+
+ def _get_url(self, path):
+ if not isinstance(path, str):
+ # urllib3.util.url._encode_invalid_chars() converts the path back
+ # to bytes using the utf-8 codec.
+ path = path.decode("utf-8")
+ return urljoin(self._base_url, path).rstrip("/") + "/"
+
+ def _http_request(self, url, headers=None, data=None, allow_compression=False):
+ req_headers = self.pool_manager.headers.copy()
+ if headers is not None:
+ req_headers.update(headers)
+ req_headers["Pragma"] = "no-cache"
+ if allow_compression:
+ req_headers["Accept-Encoding"] = "gzip"
+ else:
+ req_headers["Accept-Encoding"] = "identity"
+
+ if data is None:
+ resp = self.pool_manager.request("GET", url, headers=req_headers)
+ else:
+ resp = self.pool_manager.request(
+ "POST", url, headers=req_headers, body=data
+ )
+
+ if resp.status == 404:
+ raise NotGitRepository()
+ if resp.status == 401:
+ raise HTTPUnauthorized(resp.getheader("WWW-Authenticate"), url)
+ if resp.status == 407:
+ raise HTTPProxyUnauthorized(resp.getheader("Proxy-Authenticate"), url)
+ if resp.status != 200:
+ raise GitProtocolError(
+ "unexpected http resp %d for %s" % (resp.status, url)
+ )
+
+ # TODO: Optimization available by adding `preload_content=False` to the
+ # request and just passing the `read` method on instead of going via
+ # `BytesIO`, if we can guarantee that the entire response is consumed
+ # before issuing the next to still allow for connection reuse from the
+ # pool.
+ read = BytesIO(resp.data).read
+
+ resp.content_type = resp.getheader("Content-Type")
+ # Check if geturl() is available (urllib3 version >= 1.23)
+ try:
+ resp_url = resp.geturl()
+ except AttributeError:
+ # get_redirect_location() is available for urllib3 >= 1.1
+ resp.redirect_location = resp.get_redirect_location()
+ else:
+ resp.redirect_location = resp_url if resp_url != url else ""
+ return resp, read
+
+
+HttpGitClient = Urllib3HttpGitClient
+
+
def get_transport_and_path_from_url(url, config=None, **kwargs):
"""Obtain a git client from a URL.
blob - 7868faece3ef5c7566164681f7f2cfa2d7818291
blob + 65f7de9e6ed5c8aacbec0b0c84dff92f24c76224
--- dulwich/config.py
+++ dulwich/config.py
MutableMapping,
)
except ImportError: # python < 3.7
- from collections import (
+ from collections import ( # type: ignore
Iterable,
MutableMapping,
)
super(ConfigFile, self).__init__(values=values, encoding=encoding)
self.path = None
- @classmethod
- def from_file(cls, f: BinaryIO) -> "ConfigFile":
+ @classmethod # noqa: C901
+ def from_file(cls, f: BinaryIO) -> "ConfigFile": # noqa: C901
"""Read configuration from a file-like object."""
ret = cls()
section = None # type: Optional[Tuple[bytes, ...]]
setting = None
continuation = None
for lineno, line in enumerate(f.readlines()):
+ if lineno == 0 and line.startswith(b'\xef\xbb\xbf'):
+ line = line[3:]
line = line.lstrip()
if setting is None:
# Parse section header ("[bla]")
blob - 6abdc27dce50a22d372feeb383b89b909485cc7a
blob + acdec8c74d0e42c3480a4ad10ba5bf517107beca
--- dulwich/file.py
+++ dulwich/file.py
os.remove(tmpfile)
-def GitFile(filename, mode="rb", bufsize=-1):
+def GitFile(filename, mode="rb", bufsize=-1, mask=0o644):
"""Create a file object that obeys the git file locking protocol.
Returns: a builtin file object or a _GitFile object
are not. To read and write from the same file, you can take advantage of
the fact that opening a file for write does not actually open the file you
request.
+
+ The default file mask makes any created files user-writable and
+ world-readable.
+
"""
if "a" in mode:
raise IOError("append mode not supported for Git files")
if "b" not in mode:
raise IOError("text mode not supported for Git files")
if "w" in mode:
- return _GitFile(filename, mode, bufsize)
+ return _GitFile(filename, mode, bufsize, mask)
else:
return io.open(filename, mode, bufsize)
"writelines",
)
- def __init__(self, filename, mode, bufsize):
+ def __init__(self, filename, mode, bufsize, mask):
self._filename = filename
if isinstance(self._filename, bytes):
self._lockfilename = self._filename + b".lock"
fd = os.open(
self._lockfilename,
os.O_RDWR | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0),
+ mask,
)
except FileExistsError:
raise FileLocked(filename, self._lockfilename)
blob - cc457a22c467b77297ccf68cd3a37a57fc94e2c2
blob + b75560f35c84987f5fe9a4ab835faf773f428756
--- dulwich/ignore.py
+++ dulwich/ignore.py
line = line.rstrip(b"\r\n")
# Ignore blank lines, they're used for readability.
- if not line:
+ if not line.strip():
continue
if line.startswith(b"#"):
blob - 99c816388356f2a3d2bc51a88eac1178be84de1e
blob + 551f9c1f100fe4abcfe4907bf58ca38e468bce2d
--- dulwich/object_store.py
+++ dulwich/object_store.py
INFODIR = "info"
PACKDIR = "pack"
+# use permissions consistent with Git; just readable by everyone
+# TODO: should packs also be non-writable on Windows? if so, that
+# would requite some rather significant adjustments to the test suite
+PACK_MODE = 0o444 if sys.platform != "win32" else 0o644
+
class BaseObjectStore(object):
"""Object store interface."""
os.rename(path, target_pack)
# Write the index.
- index_file = GitFile(pack_base_name + ".idx", "wb")
+ index_file = GitFile(pack_base_name + ".idx", "wb", mask=PACK_MODE)
try:
write_pack_index_v2(index_file, entries, pack_sha)
index_file.close()
fd, path = tempfile.mkstemp(dir=self.path, prefix="tmp_pack_")
with os.fdopen(fd, "w+b") as f:
+ os.chmod(path, PACK_MODE)
indexer = PackIndexer(f, resolve_ext_ref=self.get_raw)
copier = PackStreamCopier(read_all, read_some, f, delta_iter=indexer)
copier.verify()
basename = self._get_pack_basepath(entries)
index_name = basename + ".idx"
if not os.path.exists(index_name):
- with GitFile(index_name, "wb") as f:
+ with GitFile(index_name, "wb", mask=PACK_MODE) as f:
write_pack_index_v2(f, entries, p.get_stored_checksum())
for pack in self.packs:
if pack._basename == basename:
fd, path = tempfile.mkstemp(dir=self.pack_dir, suffix=".pack")
f = os.fdopen(fd, "wb")
+ os.chmod(path, PACK_MODE)
def commit():
f.flush()
pass
if os.path.exists(path):
return # Already there, no need to write again
- with GitFile(path, "wb") as f:
+ with GitFile(path, "wb", mask=PACK_MODE) as f:
f.write(
obj.as_legacy_object(compression_level=self.loose_compression_level)
)
blob - ea1722ed440b2f655df90a7287cbdfa073d34cbb
blob + e3cf42621f37b8972edb77a7ab332d747c21ae62
--- dulwich/porcelain.py
+++ dulwich/porcelain.py
stash.push()
-def stash_pop(repo):
- """Pop a new stash from the stack."""
+def stash_pop(repo, index):
+ """Pop a stash from the stack."""
with open_repo_closing(repo) as r:
from dulwich.stash import Stash
stash = Stash.from_repo(r)
- stash.pop()
+ stash.pop(index)
+def stash_drop(repo, index):
+ """Drop a stash from the stack."""
+ with open_repo_closing(repo) as r:
+ from dulwich.stash import Stash
+
+ stash = Stash.from_repo(r)
+ stash.drop(index)
+
+
def ls_files(repo):
"""List all files in an index."""
with open_repo_closing(repo) as r:
blob - 6cbdbf8f0833d8b11ca159fa3460e298f4cb27c3
blob + 0d34658f1ee8cf20e73ecdb9bd36bad5a269f8ba
--- dulwich/repo.py
+++ dulwich/repo.py
root_path_bytes = os.fsencode(self.path)
- if not isinstance(fs_paths, list):
+ if isinstance(fs_paths, str):
fs_paths = [fs_paths]
+ fs_paths = list(fs_paths)
+
from dulwich.index import (
blob_from_path_and_stat,
index_entry_from_stat,
blob - 72f9e5c2d9aabb93cd8065664e910b485e9ccea2
blob + 487c2aa87a71268507d4f499917e408a8868ef4e
--- dulwich/tests/test_config.py
+++ dulwich/tests/test_config.py
self.assertEqual(b"bar", cf.get((b"core",), b"foo"))
self.assertEqual(b"bar", cf.get((b"core", b"foo"), b"foo"))
+ def test_from_file_utf8_bom(self):
+ text = "[core]\nfoo = b\u00e4r\n".encode("utf-8-sig")
+ cf = self.from_file(text)
+ self.assertEqual(b"b\xc3\xa4r", cf.get((b"core",), b"foo"))
+
def test_from_file_section_case_insensitive_lower(self):
cf = self.from_file(b"[cOre]\nfOo = bar\n")
self.assertEqual(b"bar", cf.get((b"core",), b"foo"))
blob - 1e947beba3a3f8a5a19c8f9aee9538832dbe232c
blob + 6cb5c6611243da3c836351588f13e1cec027657a
--- dulwich/tests/test_ignore.py
+++ dulwich/tests/test_ignore.py
f = BytesIO(
b"""
# a comment
-
+\x20\x20
# and an empty line:
\\#not a comment
blob - 2e416cff8dfc51bbc9809e0e69387b8508eb2a15
blob + 68789aacb3abed59695a69a647b10b55b13e909d
--- dulwich/tests/test_object_store.py
+++ dulwich/tests/test_object_store.py
import os
import shutil
import stat
+import sys
import tempfile
from dulwich.index import (
store.add_alternate_path("# comment")
for alt_path in store._read_alternate_paths():
self.assertNotIn("#", alt_path)
+
+ def test_file_modes(self):
+ self.store.add_object(testobject)
+ path = self.store._get_shafile_path(testobject.id)
+ mode = os.stat(path).st_mode
+
+ packmode = "0o100444" if sys.platform != "win32" else "0o100666"
+ self.assertEqual(oct(mode), packmode)
def test_corrupted_object_raise_exception(self):
"""Corrupted sha1 disk file should raise specific exception"""
self.assertIsNotNone(self.store._get_loose_object(testobject.id))
path = self.store._get_shafile_path(testobject.id)
+ old_mode = os.stat(path).st_mode
+ os.chmod(path, 0o600)
with open(path, "wb") as f: # corrupt the file
f.write(b"")
+ os.chmod(path, old_mode)
expected_error_msg = "Corrupted empty file detected"
try:
blob - ded79b24a6dc65b53153a374c50447a9fd2dc17e
blob + 278259c3b4eb3afb8a8d9170cdf94b9786105bff
--- dulwich/tests/test_pack.py
+++ dulwich/tests/test_pack.py
from hashlib import sha1
import os
import shutil
+import sys
import tempfile
import zlib
a_sha = b"6f670c0fb53f9463760b7295fbb814e965fb20c8"
tree_sha = b"b2a2766a2879c209ab1176e7e778b81ae422eeaa"
commit_sha = b"f18faa16531ac570a3fdc8c7ca16682548dafd12"
+indexmode = "0o100644" if sys.platform != "win32" else "0o100666"
class PackTests(TestCase):
p.create_index_v1(filename)
idx1 = load_pack_index(filename)
idx2 = self.get_pack_index(pack1_sha)
+ self.assertEqual(oct(os.stat(filename).st_mode), indexmode)
self.assertEqual(idx1, idx2)
def test_create_index_v2(self):
p.create_index_v2(filename)
idx1 = load_pack_index(filename)
idx2 = self.get_pack_index(pack1_sha)
+ self.assertEqual(oct(os.stat(filename).st_mode), indexmode)
self.assertEqual(idx1, idx2)
def test_compute_file_sha(self):
blob - 0a0e1882e5bf32cd0f4640be692a5c03e272a5c8
blob + 7717701f4009b24b8b2fe7246c08490fa59c9b2c
--- dulwich/tests/test_porcelain.py
+++ dulwich/tests/test_porcelain.py
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
-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
-=YPjc
+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
+brMb2upTHa+kjktjdO96/BgSnKYqmNmPB/qB
+=ivA/
-----END PGP PRIVATE KEY BLOCK-----
"""
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
-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
-=kOKh
+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
+=9zU5
-----END PGP PRIVATE KEY BLOCK-----
"""
blob - 9495d2f111061be0689b18bbb8012ac26c3e6bbf
blob + 4b75e0431b816f9e47d5ac5f0fb206354d533542
--- dulwich/tests/test_repository.py
+++ dulwich/tests/test_repository.py
"""Tests for the repository."""
+import glob
import locale
import os
-import stat
import shutil
+import stat
import sys
import tempfile
import warnings
with repo.get_named_file("config") as f:
config_text = f.read()
self.assertTrue(barestr in config_text, "%r" % config_text)
+
+ if isinstance(repo, Repo):
+ expected_mode = '0o100644' if expect_filemode else '0o100666'
+ expected = {
+ 'HEAD': expected_mode,
+ 'config': expected_mode,
+ 'description': expected_mode,
+ }
+ actual = {
+ f[len(repo._controldir) + 1:]: oct(os.stat(f).st_mode)
+ for f in glob.glob(os.path.join(repo._controldir, '*'))
+ if os.path.isfile(f)
+ }
+ self.assertEqual(expected, actual)
+
def test_create_memory(self):
repo = MemoryRepo.init_bare([], {})
self._check_repo_contents(repo, True)
blob - e54aeeed9312dfef00c8d803064cdd5db15255df
blob + f42454ceef107f4fb2a83f9dfedde4aad6197efb
--- dulwich.egg-info/PKG-INFO
+++ dulwich.egg-info/PKG-INFO
Metadata-Version: 2.1
Name: dulwich
-Version: 0.20.23
+Version: 0.20.25
Summary: Python Git Library
Home-page: https://www.dulwich.io/
Author: Jelmer Vernooij
Project-URL: Bug Tracker, https://github.com/dulwich/dulwich/issues
Project-URL: Repository, https://www.dulwich.io/code/
Project-URL: GitHub, https://github.com/dulwich/dulwich
-Description: Dulwich
- =======
-
- This is the Dulwich project.
-
- It aims to provide an interface to git repos (both local and remote) that
- doesn't call out to git directly but instead uses pure Python.
-
- **Main website**: <https://www.dulwich.io/>
-
- **License**: Apache License, version 2 or GNU General Public License, version 2 or later.
-
- The project is named after the part of London that Mr. and Mrs. Git live in
- in the particular Monty Python sketch.
-
- Installation
- ------------
-
- By default, Dulwich' setup.py will attempt to build and install the optional C
- extensions. The reason for this is that they significantly improve the performance
- since some low-level operations that are executed often are much slower in CPython.
-
- If you don't want to install the C bindings, specify the --pure argument to setup.py::
-
- $ python setup.py --pure install
-
- or if you are installing from pip::
-
- $ pip install dulwich --global-option="--pure"
-
- Note that you can also specify --global-option in a
- `requirements.txt <https://pip.pypa.io/en/stable/reference/pip_install/#requirement-specifiers>`_
- file, e.g. like this::
-
- dulwich --global-option=--pure
-
- Getting started
- ---------------
-
- Dulwich comes with both a lower-level API and higher-level plumbing ("porcelain").
-
- For example, to use the lower level API to access the commit message of the
- last commit::
-
- >>> from dulwich.repo import Repo
- >>> r = Repo('.')
- >>> r.head()
- '57fbe010446356833a6ad1600059d80b1e731e15'
- >>> c = r[r.head()]
- >>> c
- <Commit 015fc1267258458901a94d228e39f0a378370466>
- >>> c.message
- 'Add note about encoding.\n'
-
- And to print it using porcelain::
-
- >>> from dulwich import porcelain
- >>> porcelain.log('.', max_entries=1)
- --------------------------------------------------
- commit: 57fbe010446356833a6ad1600059d80b1e731e15
- Author: Jelmer Vernooij <jelmer@jelmer.uk>
- Date: Sat Apr 29 2017 23:57:34 +0000
-
- Add note about encoding.
-
- Further documentation
- ---------------------
-
- The dulwich documentation can be found in docs/ and built by running ``make
- doc``. It can also be found `on the web <https://www.dulwich.io/docs/>`_.
-
- Help
- ----
-
- There is a *#dulwich* IRC channel on the `OFTC <https://www.oftc.net/>`_, and
- `dulwich-announce <https://groups.google.com/forum/#!forum/dulwich-announce>`_
- and `dulwich-discuss <https://groups.google.com/forum/#!forum/dulwich-discuss>`_
- mailing lists.
-
- Contributing
- ------------
-
- For a full list of contributors, see the git logs or `AUTHORS <AUTHORS>`_.
-
- If you'd like to contribute to Dulwich, see the `CONTRIBUTING <CONTRIBUTING.rst>`_
- file and `list of open issues <https://github.com/dulwich/dulwich/issues>`_.
-
- Supported versions of Python
- ----------------------------
-
- At the moment, Dulwich supports (and is tested on) CPython 3.5 and later and
- Pypy.
-
- The latest release series to support Python 2.x was the 0.19 series. See
- the 0.19 branch in the Dulwich git repository.
-
Keywords: git vcs
Platform: UNKNOWN
Classifier: Development Status :: 4 - Beta
Provides-Extra: https
Provides-Extra: pgp
Provides-Extra: watch
+License-File: COPYING
+License-File: AUTHORS
+
+Dulwich
+=======
+
+This is the Dulwich project.
+
+It aims to provide an interface to git repos (both local and remote) that
+doesn't call out to git directly but instead uses pure Python.
+
+**Main website**: <https://www.dulwich.io/>
+
+**License**: Apache License, version 2 or GNU General Public License, version 2 or later.
+
+The project is named after the part of London that Mr. and Mrs. Git live in
+in the particular Monty Python sketch.
+
+Installation
+------------
+
+By default, Dulwich' setup.py will attempt to build and install the optional C
+extensions. The reason for this is that they significantly improve the performance
+since some low-level operations that are executed often are much slower in CPython.
+
+If you don't want to install the C bindings, specify the --pure argument to setup.py::
+
+ $ python setup.py --pure install
+
+or if you are installing from pip::
+
+ $ pip install dulwich --global-option="--pure"
+
+Note that you can also specify --global-option in a
+`requirements.txt <https://pip.pypa.io/en/stable/reference/pip_install/#requirement-specifiers>`_
+file, e.g. like this::
+
+ dulwich --global-option=--pure
+
+Getting started
+---------------
+
+Dulwich comes with both a lower-level API and higher-level plumbing ("porcelain").
+
+For example, to use the lower level API to access the commit message of the
+last commit::
+
+ >>> from dulwich.repo import Repo
+ >>> r = Repo('.')
+ >>> r.head()
+ '57fbe010446356833a6ad1600059d80b1e731e15'
+ >>> c = r[r.head()]
+ >>> c
+ <Commit 015fc1267258458901a94d228e39f0a378370466>
+ >>> c.message
+ 'Add note about encoding.\n'
+
+And to print it using porcelain::
+
+ >>> from dulwich import porcelain
+ >>> porcelain.log('.', max_entries=1)
+ --------------------------------------------------
+ commit: 57fbe010446356833a6ad1600059d80b1e731e15
+ Author: Jelmer Vernooij <jelmer@jelmer.uk>
+ Date: Sat Apr 29 2017 23:57:34 +0000
+
+ Add note about encoding.
+
+Further documentation
+---------------------
+
+The dulwich documentation can be found in docs/ and built by running ``make
+doc``. It can also be found `on the web <https://www.dulwich.io/docs/>`_.
+
+Help
+----
+
+There is a *#dulwich* IRC channel on the `OFTC <https://www.oftc.net/>`_, and
+`dulwich-announce <https://groups.google.com/forum/#!forum/dulwich-announce>`_
+and `dulwich-discuss <https://groups.google.com/forum/#!forum/dulwich-discuss>`_
+mailing lists.
+
+Contributing
+------------
+
+For a full list of contributors, see the git logs or `AUTHORS <AUTHORS>`_.
+
+If you'd like to contribute to Dulwich, see the `CONTRIBUTING <CONTRIBUTING.rst>`_
+file and `list of open issues <https://github.com/dulwich/dulwich/issues>`_.
+
+Supported versions of Python
+----------------------------
+
+At the moment, Dulwich supports (and is tested on) CPython 3.5 and later and
+Pypy.
+
+The latest release series to support Python 2.x was the 0.19 series. See
+the 0.19 branch in the Dulwich git repository.
+
+
blob - 7e9c7a83aa250024ec3157354277444b57b1af47
blob + 75bc62eb2b568f43d43518ae5b2c38c61eaa47be
--- dulwich.egg-info/SOURCES.txt
+++ dulwich.egg-info/SOURCES.txt
README.swift.rst
SECURITY.md
TODO
-build.cmd
dulwich.cfg
releaser.conf
requirements.txt
docs/tutorial/repo.txt
docs/tutorial/tag.txt
dulwich/__init__.py
+dulwich/__main__.py
dulwich/_diff_tree.c
dulwich/_objects.c
dulwich/_pack.c
dulwich.egg-info/SOURCES.txt
dulwich.egg-info/dependency_links.txt
dulwich.egg-info/entry_points.txt
+dulwich.egg-info/not-zip-safe
dulwich.egg-info/requires.txt
dulwich.egg-info/top_level.txt
dulwich/cloud/__init__.py
blob - /dev/null
blob + 8b137891791fe96927ad78e64b0aad7bded08bdc (mode 644)
--- /dev/null
+++ dulwich.egg-info/not-zip-safe
+
blob - cf6b7a3527d1b3a5537644f176fbbab6df669daf
blob + bcad72df255a46d39eb9f694dbc8467fb8880de4
--- releaser.conf
+++ releaser.conf
+# See https://github.com/jelmer/releaser
news_file: "NEWS"
timeout_days: 5
tag_name: "dulwich-$VERSION"
blob - c9da33d268834db81a4b27b70faab81bce4e93ef
blob + 8168ef69730e5fec94de224eeac1c970f189fe58
--- setup.py
+++ setup.py
'For 2.7 support, please install a version prior to 0.20')
-dulwich_version_string = '0.20.23'
+dulwich_version_string = '0.20.25'
class DulwichDistribution(Distribution):
package_data={'': ['../docs/tutorial/*.txt', 'py.typed']},
scripts=scripts,
ext_modules=ext_modules,
+ zip_safe=False,
distclass=DulwichDistribution,
classifiers=[
'Development Status :: 4 - Beta',