Codebase list dulwich / 8efe706
New upstream release. Jelmer Vernooij 2 years ago
26 changed file(s) with 591 addition(s) and 461 deletion(s). Raw diff Collapse all Expand all
00 name: Python package
11
2 on: [push, pull_request]
2 on:
3 push:
4 pull_request:
5 schedule:
6 - cron: '0 6 * * *' # Daily 6AM UTC build
37
48 jobs:
59 build:
812 strategy:
913 matrix:
1014 os: [ubuntu-latest, macos-latest, windows-latest]
11 python-version: [3.5, 3.6, 3.7, 3.8, 3.9, pypy3]
15 python-version: [3.5, 3.6, 3.7, 3.8, 3.9, 3.10-dev, pypy3]
1216 exclude:
1317 # sqlite3 exit handling seems to get in the way
1418 - os: macos-latest
4549 if: "matrix.os != 'windows-latest' && matrix.python-version != 'pypy3'"
4650 - name: Install mypy
4751 run: |
48 pip install -U mypy
52 pip install -U mypy types-paramiko types-certifi
4953 if: "matrix.python-version != 'pypy3'"
5054 - name: Style checks
5155 run: |
0 0.20.25 2021-08-23
1
2 * Fix ``dulwich`` script when installed via setup.py.
3 (Dan Villiom Podlaski Christiansen)
4
5 * Make default file mask consistent
6 with Git. (Dan Villiom Podlaski Christiansen, #884)
7
8 0.20.24 2021-07-18
9
10 * config: disregard UTF-8 BOM when reading file.
11 (Dan Villiom Podlaski Christiansen)
12
13 * Skip lines with spaces only in .gitignore. (Andrey Torsunov, #878)
14
15 * Add a separate HTTPProxyUnauthorized exception for 407 errors.
16 (Jelmer Vernooij, #822)
17
18 * Split out a AbstractHTTPGitClient class.
19 (Jelmer Vernooij)
20
021 0.20.23 2021-05-24
122
223 * Fix installation of GPG during package publishing.
281302 BUG FIXES
282303
283304 * Avoid ``PermissionError``, since it is Python3-specific.
284 (Jelmer Vernooij)
305 (Jelmer Vernooij)
285306
286307 * Fix regression that added a dependency on C git for the
287308 test suite. (Jelmer Vernooij, #720)
00 Metadata-Version: 2.1
11 Name: dulwich
2 Version: 0.20.23
2 Version: 0.20.25
33 Summary: Python Git Library
44 Home-page: https://www.dulwich.io/
55 Author: Jelmer Vernooij
88 Project-URL: Bug Tracker, https://github.com/dulwich/dulwich/issues
99 Project-URL: Repository, https://www.dulwich.io/code/
1010 Project-URL: GitHub, https://github.com/dulwich/dulwich
11 Description: Dulwich
12 =======
13
14 This is the Dulwich project.
15
16 It aims to provide an interface to git repos (both local and remote) that
17 doesn't call out to git directly but instead uses pure Python.
18
19 **Main website**: <https://www.dulwich.io/>
20
21 **License**: Apache License, version 2 or GNU General Public License, version 2 or later.
22
23 The project is named after the part of London that Mr. and Mrs. Git live in
24 in the particular Monty Python sketch.
25
26 Installation
27 ------------
28
29 By default, Dulwich' setup.py will attempt to build and install the optional C
30 extensions. The reason for this is that they significantly improve the performance
31 since some low-level operations that are executed often are much slower in CPython.
32
33 If you don't want to install the C bindings, specify the --pure argument to setup.py::
34
35 $ python setup.py --pure install
36
37 or if you are installing from pip::
38
39 $ pip install dulwich --global-option="--pure"
40
41 Note that you can also specify --global-option in a
42 `requirements.txt <https://pip.pypa.io/en/stable/reference/pip_install/#requirement-specifiers>`_
43 file, e.g. like this::
44
45 dulwich --global-option=--pure
46
47 Getting started
48 ---------------
49
50 Dulwich comes with both a lower-level API and higher-level plumbing ("porcelain").
51
52 For example, to use the lower level API to access the commit message of the
53 last commit::
54
55 >>> from dulwich.repo import Repo
56 >>> r = Repo('.')
57 >>> r.head()
58 '57fbe010446356833a6ad1600059d80b1e731e15'
59 >>> c = r[r.head()]
60 >>> c
61 <Commit 015fc1267258458901a94d228e39f0a378370466>
62 >>> c.message
63 'Add note about encoding.\n'
64
65 And to print it using porcelain::
66
67 >>> from dulwich import porcelain
68 >>> porcelain.log('.', max_entries=1)
69 --------------------------------------------------
70 commit: 57fbe010446356833a6ad1600059d80b1e731e15
71 Author: Jelmer Vernooij <jelmer@jelmer.uk>
72 Date: Sat Apr 29 2017 23:57:34 +0000
73
74 Add note about encoding.
75
76 Further documentation
77 ---------------------
78
79 The dulwich documentation can be found in docs/ and built by running ``make
80 doc``. It can also be found `on the web <https://www.dulwich.io/docs/>`_.
81
82 Help
83 ----
84
85 There is a *#dulwich* IRC channel on the `OFTC <https://www.oftc.net/>`_, and
86 `dulwich-announce <https://groups.google.com/forum/#!forum/dulwich-announce>`_
87 and `dulwich-discuss <https://groups.google.com/forum/#!forum/dulwich-discuss>`_
88 mailing lists.
89
90 Contributing
91 ------------
92
93 For a full list of contributors, see the git logs or `AUTHORS <AUTHORS>`_.
94
95 If you'd like to contribute to Dulwich, see the `CONTRIBUTING <CONTRIBUTING.rst>`_
96 file and `list of open issues <https://github.com/dulwich/dulwich/issues>`_.
97
98 Supported versions of Python
99 ----------------------------
100
101 At the moment, Dulwich supports (and is tested on) CPython 3.5 and later and
102 Pypy.
103
104 The latest release series to support Python 2.x was the 0.19 series. See
105 the 0.19 branch in the Dulwich git repository.
106
10711 Keywords: git vcs
10812 Platform: UNKNOWN
10913 Classifier: Development Status :: 4 - Beta
12327 Provides-Extra: https
12428 Provides-Extra: pgp
12529 Provides-Extra: watch
30 License-File: COPYING
31 License-File: AUTHORS
32
33 Dulwich
34 =======
35
36 This is the Dulwich project.
37
38 It aims to provide an interface to git repos (both local and remote) that
39 doesn't call out to git directly but instead uses pure Python.
40
41 **Main website**: <https://www.dulwich.io/>
42
43 **License**: Apache License, version 2 or GNU General Public License, version 2 or later.
44
45 The project is named after the part of London that Mr. and Mrs. Git live in
46 in the particular Monty Python sketch.
47
48 Installation
49 ------------
50
51 By default, Dulwich' setup.py will attempt to build and install the optional C
52 extensions. The reason for this is that they significantly improve the performance
53 since some low-level operations that are executed often are much slower in CPython.
54
55 If you don't want to install the C bindings, specify the --pure argument to setup.py::
56
57 $ python setup.py --pure install
58
59 or if you are installing from pip::
60
61 $ pip install dulwich --global-option="--pure"
62
63 Note that you can also specify --global-option in a
64 `requirements.txt <https://pip.pypa.io/en/stable/reference/pip_install/#requirement-specifiers>`_
65 file, e.g. like this::
66
67 dulwich --global-option=--pure
68
69 Getting started
70 ---------------
71
72 Dulwich comes with both a lower-level API and higher-level plumbing ("porcelain").
73
74 For example, to use the lower level API to access the commit message of the
75 last commit::
76
77 >>> from dulwich.repo import Repo
78 >>> r = Repo('.')
79 >>> r.head()
80 '57fbe010446356833a6ad1600059d80b1e731e15'
81 >>> c = r[r.head()]
82 >>> c
83 <Commit 015fc1267258458901a94d228e39f0a378370466>
84 >>> c.message
85 'Add note about encoding.\n'
86
87 And to print it using porcelain::
88
89 >>> from dulwich import porcelain
90 >>> porcelain.log('.', max_entries=1)
91 --------------------------------------------------
92 commit: 57fbe010446356833a6ad1600059d80b1e731e15
93 Author: Jelmer Vernooij <jelmer@jelmer.uk>
94 Date: Sat Apr 29 2017 23:57:34 +0000
95
96 Add note about encoding.
97
98 Further documentation
99 ---------------------
100
101 The dulwich documentation can be found in docs/ and built by running ``make
102 doc``. It can also be found `on the web <https://www.dulwich.io/docs/>`_.
103
104 Help
105 ----
106
107 There is a *#dulwich* IRC channel on the `OFTC <https://www.oftc.net/>`_, and
108 `dulwich-announce <https://groups.google.com/forum/#!forum/dulwich-announce>`_
109 and `dulwich-discuss <https://groups.google.com/forum/#!forum/dulwich-discuss>`_
110 mailing lists.
111
112 Contributing
113 ------------
114
115 For a full list of contributors, see the git logs or `AUTHORS <AUTHORS>`_.
116
117 If you'd like to contribute to Dulwich, see the `CONTRIBUTING <CONTRIBUTING.rst>`_
118 file and `list of open issues <https://github.com/dulwich/dulwich/issues>`_.
119
120 Supported versions of Python
121 ----------------------------
122
123 At the moment, Dulwich supports (and is tested on) CPython 3.5 and later and
124 Pypy.
125
126 The latest release series to support Python 2.x was the 0.19 series. See
127 the 0.19 branch in the Dulwich git repository.
128
129
+0
-21
build.cmd less more
0 @echo off
1 :: To build extensions for 64 bit Python 3, we need to configure environment
2 :: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of:
3 :: MS Windows SDK for Windows 7 and .NET Framework 4
4 ::
5 :: More details at:
6 :: https://github.com/cython/cython/wiki/CythonExtensionsOnWindows
7
8 IF "%DISTUTILS_USE_SDK%"=="1" (
9 ECHO Configuring environment to build with MSVC on a 64bit architecture
10 ECHO Using Windows SDK 7.1
11 "C:\Program Files\Microsoft SDKs\Windows\v7.1\Setup\WindowsSdkVer.exe" -q -version:v7.1
12 CALL "C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\SetEnv.cmd" /x64 /release
13 SET MSSdk=1
14 REM Need the following to allow tox to see the SDK compiler
15 SET TOX_TESTENV_PASSENV=DISTUTILS_USE_SDK MSSdk INCLUDE LIB
16 ) ELSE (
17 ECHO Using default MSVC build environment
18 )
19
20 CALL %*
0 dulwich (0.20.25-1) UNRELEASED; urgency=low
1
2 * New upstream release.
3
4 -- Jelmer Vernooij <jelmer@debian.org> Wed, 25 Aug 2021 17:47:30 -0000
5
06 dulwich (0.20.23-1) unstable; urgency=medium
17
28 * Update watch file format version to 4.
2121
2222 """Python implementation of the Git file formats and protocols."""
2323
24 __version__ = (0, 20, 23)
24 __version__ = (0, 20, 25)
0 from . import cli
1
2 if __name__ == "__main__":
3 cli._main()
730730
731731 def main(argv=None):
732732 if argv is None:
733 argv = sys.argv
733 argv = sys.argv[1:]
734734
735735 if len(argv) < 1:
736736 print("Usage: dulwich <%s> [OPTIONS...]" % ("|".join(commands.keys())))
746746 return cmd_kls().run(argv[1:])
747747
748748
749 if __name__ == "__main__":
749 def _main():
750750 if "DULWICH_PDB" in os.environ and getattr(signal, "SIGQUIT", None):
751751 signal.signal(signal.SIGQUIT, signal_quit) # type: ignore
752752 signal.signal(signal.SIGINT, signal_int)
753753
754 sys.exit(main(sys.argv[1:]))
754 sys.exit(main())
755
756
757 if __name__ == "__main__":
758 _main()
132132 self.url = url
133133
134134
135 class HTTPProxyUnauthorized(Exception):
136 """Raised when proxy authentication fails."""
137
138 def __init__(self, proxy_authenticate, url):
139 Exception.__init__(self, "No valid proxy credentials provided")
140 self.proxy_authenticate = proxy_authenticate
141 self.url = url
142
143
135144 def _fileno_can_read(fileno):
136145 """Check if a file descriptor is readable."""
137146 return len(select.select([fileno], [], [], 0)[0]) > 0
548557 Args:
549558 path: Remote path to fetch from
550559 determine_wants: Function determine what refs
551 to fetch. Receives dictionary of name->sha, should return
552 list of shas to fetch.
560 to fetch. Receives dictionary of name->sha, should return
561 list of shas to fetch.
553562 graph_walker: Object with next() and ack().
554563 pack_data: Callback called for each bit of data in the pack
555564 progress: Callback for progress reports (strings)
900909 Args:
901910 path: Repository path (as bytestring)
902911 update_refs: Function to determine changes to remote refs.
903 Receive dict with existing remote refs, returns dict with
904 changed refs (name -> sha, where sha=ZERO_SHA for deletions)
912 Receive dict with existing remote refs, returns dict with
913 changed refs (name -> sha, where sha=ZERO_SHA for deletions)
905914 generate_pack_data: Function that can return a tuple with
906 number of objects and pack data to upload.
915 number of objects and pack data to upload.
907916 progress: Optional callback called with progress updates
908917
909918 Returns:
9941003 Args:
9951004 path: Remote path to fetch from
9961005 determine_wants: Function determine what refs
997 to fetch. Receives dictionary of name->sha, should return
998 list of shas to fetch.
1006 to fetch. Receives dictionary of name->sha, should return
1007 list of shas to fetch.
9991008 graph_walker: Object with next() and ack().
10001009 pack_data: Callback called for each bit of data in the pack
10011010 progress: Callback for progress reports (strings)
12911300 Args:
12921301 path: Repository path (as bytestring)
12931302 update_refs: Function to determine changes to remote refs.
1294 Receive dict with existing remote refs, returns dict with
1295 changed refs (name -> sha, where sha=ZERO_SHA for deletions)
1296 with number of items and pack data to upload.
1303 Receive dict with existing remote refs, returns dict with
1304 changed refs (name -> sha, where sha=ZERO_SHA for deletions)
1305 with number of items and pack data to upload.
12971306 progress: Optional progress function
12981307
12991308 Returns:
13521361 path: Path to fetch from (as bytestring)
13531362 target: Target repository to fetch into
13541363 determine_wants: Optional function determine what refs
1355 to fetch. Receives dictionary of name->sha, should return
1356 list of shas to fetch. Defaults to all shas.
1364 to fetch. Receives dictionary of name->sha, should return
1365 list of shas to fetch. Defaults to all shas.
13571366 progress: Optional progress function
13581367 depth: Shallow fetch depth
13591368
13841393 Args:
13851394 path: Remote path to fetch from
13861395 determine_wants: Function determine what refs
1387 to fetch. Receives dictionary of name->sha, should return
1388 list of shas to fetch.
1396 to fetch. Receives dictionary of name->sha, should return
1397 list of shas to fetch.
13891398 graph_walker: Object with next() and ack().
13901399 pack_data: Callback called for each bit of data in the pack
13911400 progress: Callback for progress reports (strings)
17581767 if proxy_server is not None:
17591768 if proxy_manager_cls is None:
17601769 proxy_manager_cls = urllib3.ProxyManager
1761 # `urllib3` requires a `str` object in both Python 2 and 3, while
1762 # `ConfigDict` coerces entries to `bytes` on Python 3. Compensate.
17631770 if not isinstance(proxy_server, str):
17641771 proxy_server = proxy_server.decode()
17651772 manager = proxy_manager_cls(proxy_server, headers=headers, **kwargs)
17711778 return manager
17721779
17731780
1774 class HttpGitClient(GitClient):
1775 def __init__(
1776 self,
1777 base_url,
1778 dumb=None,
1779 pool_manager=None,
1780 config=None,
1781 username=None,
1782 password=None,
1783 **kwargs
1784 ):
1781 class AbstractHttpGitClient(GitClient):
1782 """Abstract base class for HTTP Git Clients.
1783
1784 This is agonistic of the actual HTTP implementation.
1785
1786 Subclasses should provide an implementation of the
1787 _http_request method.
1788 """
1789
1790 def __init__(self, base_url, dumb=False, **kwargs):
17851791 self._base_url = base_url.rstrip("/") + "/"
1786 self._username = username
1787 self._password = password
17881792 self.dumb = dumb
1789
1790 if pool_manager is None:
1791 self.pool_manager = default_urllib3_manager(config)
1792 else:
1793 self.pool_manager = pool_manager
1794
1795 if username is not None:
1796 # No escaping needed: ":" is not allowed in username:
1797 # https://tools.ietf.org/html/rfc2617#section-2
1798 credentials = "%s:%s" % (username, password)
1799 import urllib3.util
1800
1801 basic_auth = urllib3.util.make_headers(basic_auth=credentials)
1802 self.pool_manager.headers.update(basic_auth)
1803
18041793 GitClient.__init__(self, **kwargs)
1805
1806 def get_url(self, path):
1807 return self._get_url(path).rstrip("/")
1808
1809 @classmethod
1810 def from_parsedurl(cls, parsedurl, **kwargs):
1811 password = parsedurl.password
1812 if password is not None:
1813 kwargs["password"] = urlunquote(password)
1814 username = parsedurl.username
1815 if username is not None:
1816 kwargs["username"] = urlunquote(username)
1817 netloc = parsedurl.hostname
1818 if parsedurl.port:
1819 netloc = "%s:%s" % (netloc, parsedurl.port)
1820 if parsedurl.username:
1821 netloc = "%s@%s" % (parsedurl.username, netloc)
1822 parsedurl = parsedurl._replace(netloc=netloc)
1823 return cls(urlunparse(parsedurl), **kwargs)
1824
1825 def __repr__(self):
1826 return "%s(%r, dumb=%r)" % (
1827 type(self).__name__,
1828 self._base_url,
1829 self.dumb,
1830 )
1831
1832 def _get_url(self, path):
1833 if not isinstance(path, str):
1834 # urllib3.util.url._encode_invalid_chars() converts the path back
1835 # to bytes using the utf-8 codec.
1836 path = path.decode("utf-8")
1837 return urljoin(self._base_url, path).rstrip("/") + "/"
18381794
18391795 def _http_request(self, url, headers=None, data=None, allow_compression=False):
18401796 """Perform HTTP request.
18521808 method for the response data.
18531809
18541810 """
1855 req_headers = self.pool_manager.headers.copy()
1856 if headers is not None:
1857 req_headers.update(headers)
1858 req_headers["Pragma"] = "no-cache"
1859 if allow_compression:
1860 req_headers["Accept-Encoding"] = "gzip"
1861 else:
1862 req_headers["Accept-Encoding"] = "identity"
1863
1864 if data is None:
1865 resp = self.pool_manager.request("GET", url, headers=req_headers)
1866 else:
1867 resp = self.pool_manager.request(
1868 "POST", url, headers=req_headers, body=data
1869 )
1870
1871 if resp.status == 404:
1872 raise NotGitRepository()
1873 if resp.status == 401:
1874 raise HTTPUnauthorized(resp.getheader("WWW-Authenticate"), url)
1875 if resp.status != 200:
1876 raise GitProtocolError(
1877 "unexpected http resp %d for %s" % (resp.status, url)
1878 )
1879
1880 # TODO: Optimization available by adding `preload_content=False` to the
1881 # request and just passing the `read` method on instead of going via
1882 # `BytesIO`, if we can guarantee that the entire response is consumed
1883 # before issuing the next to still allow for connection reuse from the
1884 # pool.
1885 read = BytesIO(resp.data).read
1886
1887 resp.content_type = resp.getheader("Content-Type")
1888 # Check if geturl() is available (urllib3 version >= 1.23)
1889 try:
1890 resp_url = resp.geturl()
1891 except AttributeError:
1892 # get_redirect_location() is available for urllib3 >= 1.1
1893 resp.redirect_location = resp.get_redirect_location()
1894 else:
1895 resp.redirect_location = resp_url if resp_url != url else ""
1896 return resp, read
1811
1812 raise NotImplementedError(self._http_request)
18971813
18981814 def _discover_references(self, service, base_url):
18991815 assert base_url[-1] == "/"
19331849 resp.close()
19341850
19351851 def _smart_request(self, service, url, data):
1852 """Send a 'smart' HTTP request.
1853
1854 This is a simple wrapper around _http_request that sets
1855 a couple of extra headers.
1856 """
19361857 assert url[-1] == "/"
19371858 url = urljoin(url, service)
19381859 result_content_type = "application/x-%s-result" % service
19541875 Args:
19551876 path: Repository path (as bytestring)
19561877 update_refs: Function to determine changes to remote refs.
1957 Receives dict with existing remote refs, returns dict with
1958 changed refs (name -> sha, where sha=ZERO_SHA for deletions)
1878 Receives dict with existing remote refs, returns dict with
1879 changed refs (name -> sha, where sha=ZERO_SHA for deletions)
19591880 generate_pack_data: Function that can return a tuple
1960 with number of elements and pack data to upload.
1881 with number of elements and pack data to upload.
19611882 progress: Optional progress function
19621883
19631884 Returns:
20882009 refs, _, _ = self._discover_references(b"git-upload-pack", url)
20892010 return refs
20902011
2012 def get_url(self, path):
2013 return self._get_url(path).rstrip("/")
2014
2015 def _get_url(self, path):
2016 return urljoin(self._base_url, path).rstrip("/") + "/"
2017
2018 @classmethod
2019 def from_parsedurl(cls, parsedurl, **kwargs):
2020 password = parsedurl.password
2021 if password is not None:
2022 kwargs["password"] = urlunquote(password)
2023 username = parsedurl.username
2024 if username is not None:
2025 kwargs["username"] = urlunquote(username)
2026 netloc = parsedurl.hostname
2027 if parsedurl.port:
2028 netloc = "%s:%s" % (netloc, parsedurl.port)
2029 if parsedurl.username:
2030 netloc = "%s@%s" % (parsedurl.username, netloc)
2031 parsedurl = parsedurl._replace(netloc=netloc)
2032 return cls(urlunparse(parsedurl), **kwargs)
2033
2034 def __repr__(self):
2035 return "%s(%r, dumb=%r)" % (
2036 type(self).__name__,
2037 self._base_url,
2038 self.dumb,
2039 )
2040
2041
2042 class Urllib3HttpGitClient(AbstractHttpGitClient):
2043 def __init__(
2044 self,
2045 base_url,
2046 dumb=None,
2047 pool_manager=None,
2048 config=None,
2049 username=None,
2050 password=None,
2051 **kwargs
2052 ):
2053 self._username = username
2054 self._password = password
2055
2056 if pool_manager is None:
2057 self.pool_manager = default_urllib3_manager(config)
2058 else:
2059 self.pool_manager = pool_manager
2060
2061 if username is not None:
2062 # No escaping needed: ":" is not allowed in username:
2063 # https://tools.ietf.org/html/rfc2617#section-2
2064 credentials = "%s:%s" % (username, password)
2065 import urllib3.util
2066
2067 basic_auth = urllib3.util.make_headers(basic_auth=credentials)
2068 self.pool_manager.headers.update(basic_auth)
2069
2070 super(Urllib3HttpGitClient, self).__init__(
2071 base_url=base_url, dumb=dumb, **kwargs)
2072
2073 def _get_url(self, path):
2074 if not isinstance(path, str):
2075 # urllib3.util.url._encode_invalid_chars() converts the path back
2076 # to bytes using the utf-8 codec.
2077 path = path.decode("utf-8")
2078 return urljoin(self._base_url, path).rstrip("/") + "/"
2079
2080 def _http_request(self, url, headers=None, data=None, allow_compression=False):
2081 req_headers = self.pool_manager.headers.copy()
2082 if headers is not None:
2083 req_headers.update(headers)
2084 req_headers["Pragma"] = "no-cache"
2085 if allow_compression:
2086 req_headers["Accept-Encoding"] = "gzip"
2087 else:
2088 req_headers["Accept-Encoding"] = "identity"
2089
2090 if data is None:
2091 resp = self.pool_manager.request("GET", url, headers=req_headers)
2092 else:
2093 resp = self.pool_manager.request(
2094 "POST", url, headers=req_headers, body=data
2095 )
2096
2097 if resp.status == 404:
2098 raise NotGitRepository()
2099 if resp.status == 401:
2100 raise HTTPUnauthorized(resp.getheader("WWW-Authenticate"), url)
2101 if resp.status == 407:
2102 raise HTTPProxyUnauthorized(resp.getheader("Proxy-Authenticate"), url)
2103 if resp.status != 200:
2104 raise GitProtocolError(
2105 "unexpected http resp %d for %s" % (resp.status, url)
2106 )
2107
2108 # TODO: Optimization available by adding `preload_content=False` to the
2109 # request and just passing the `read` method on instead of going via
2110 # `BytesIO`, if we can guarantee that the entire response is consumed
2111 # before issuing the next to still allow for connection reuse from the
2112 # pool.
2113 read = BytesIO(resp.data).read
2114
2115 resp.content_type = resp.getheader("Content-Type")
2116 # Check if geturl() is available (urllib3 version >= 1.23)
2117 try:
2118 resp_url = resp.geturl()
2119 except AttributeError:
2120 # get_redirect_location() is available for urllib3 >= 1.1
2121 resp.redirect_location = resp.get_redirect_location()
2122 else:
2123 resp.redirect_location = resp_url if resp_url != url else ""
2124 return resp, read
2125
2126
2127 HttpGitClient = Urllib3HttpGitClient
2128
20912129
20922130 def get_transport_and_path_from_url(url, config=None, **kwargs):
20932131 """Obtain a git client from a URL.
4040 MutableMapping,
4141 )
4242 except ImportError: # python < 3.7
43 from collections import (
43 from collections import ( # type: ignore
4444 Iterable,
4545 MutableMapping,
4646 )
386386 super(ConfigFile, self).__init__(values=values, encoding=encoding)
387387 self.path = None
388388
389 @classmethod
390 def from_file(cls, f: BinaryIO) -> "ConfigFile":
389 @classmethod # noqa: C901
390 def from_file(cls, f: BinaryIO) -> "ConfigFile": # noqa: C901
391391 """Read configuration from a file-like object."""
392392 ret = cls()
393393 section = None # type: Optional[Tuple[bytes, ...]]
394394 setting = None
395395 continuation = None
396396 for lineno, line in enumerate(f.readlines()):
397 if lineno == 0 and line.startswith(b'\xef\xbb\xbf'):
398 line = line[3:]
397399 line = line.lstrip()
398400 if setting is None:
399401 # Parse section header ("[bla]")
6565 os.remove(tmpfile)
6666
6767
68 def GitFile(filename, mode="rb", bufsize=-1):
68 def GitFile(filename, mode="rb", bufsize=-1, mask=0o644):
6969 """Create a file object that obeys the git file locking protocol.
7070
7171 Returns: a builtin file object or a _GitFile object
7676 are not. To read and write from the same file, you can take advantage of
7777 the fact that opening a file for write does not actually open the file you
7878 request.
79
80 The default file mask makes any created files user-writable and
81 world-readable.
82
7983 """
8084 if "a" in mode:
8185 raise IOError("append mode not supported for Git files")
8488 if "b" not in mode:
8589 raise IOError("text mode not supported for Git files")
8690 if "w" in mode:
87 return _GitFile(filename, mode, bufsize)
91 return _GitFile(filename, mode, bufsize, mask)
8892 else:
8993 return io.open(filename, mode, bufsize)
9094
135139 "writelines",
136140 )
137141
138 def __init__(self, filename, mode, bufsize):
142 def __init__(self, filename, mode, bufsize, mask):
139143 self._filename = filename
140144 if isinstance(self._filename, bytes):
141145 self._lockfilename = self._filename + b".lock"
145149 fd = os.open(
146150 self._lockfilename,
147151 os.O_RDWR | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0),
152 mask,
148153 )
149154 except FileExistsError:
150155 raise FileLocked(filename, self._lockfilename)
122122 line = line.rstrip(b"\r\n")
123123
124124 # Ignore blank lines, they're used for readability.
125 if not line:
125 if not line.strip():
126126 continue
127127
128128 if line.startswith(b"#"):
6969 INFODIR = "info"
7070 PACKDIR = "pack"
7171
72 # use permissions consistent with Git; just readable by everyone
73 # TODO: should packs also be non-writable on Windows? if so, that
74 # would requite some rather significant adjustments to the test suite
75 PACK_MODE = 0o444 if sys.platform != "win32" else 0o644
76
7277
7378 class BaseObjectStore(object):
7479 """Object store interface."""
804809 os.rename(path, target_pack)
805810
806811 # Write the index.
807 index_file = GitFile(pack_base_name + ".idx", "wb")
812 index_file = GitFile(pack_base_name + ".idx", "wb", mask=PACK_MODE)
808813 try:
809814 write_pack_index_v2(index_file, entries, pack_sha)
810815 index_file.close()
836841
837842 fd, path = tempfile.mkstemp(dir=self.path, prefix="tmp_pack_")
838843 with os.fdopen(fd, "w+b") as f:
844 os.chmod(path, PACK_MODE)
839845 indexer = PackIndexer(f, resolve_ext_ref=self.get_raw)
840846 copier = PackStreamCopier(read_all, read_some, f, delta_iter=indexer)
841847 copier.verify()
855861 basename = self._get_pack_basepath(entries)
856862 index_name = basename + ".idx"
857863 if not os.path.exists(index_name):
858 with GitFile(index_name, "wb") as f:
864 with GitFile(index_name, "wb", mask=PACK_MODE) as f:
859865 write_pack_index_v2(f, entries, p.get_stored_checksum())
860866 for pack in self.packs:
861867 if pack._basename == basename:
884890
885891 fd, path = tempfile.mkstemp(dir=self.pack_dir, suffix=".pack")
886892 f = os.fdopen(fd, "wb")
893 os.chmod(path, PACK_MODE)
887894
888895 def commit():
889896 f.flush()
915922 pass
916923 if os.path.exists(path):
917924 return # Already there, no need to write again
918 with GitFile(path, "wb") as f:
925 with GitFile(path, "wb", mask=PACK_MODE) as f:
919926 f.write(
920927 obj.as_legacy_object(compression_level=self.loose_compression_level)
921928 )
18071807 stash.push()
18081808
18091809
1810 def stash_pop(repo):
1811 """Pop a new stash from the stack."""
1810 def stash_pop(repo, index):
1811 """Pop a stash from the stack."""
18121812 with open_repo_closing(repo) as r:
18131813 from dulwich.stash import Stash
18141814
18151815 stash = Stash.from_repo(r)
1816 stash.pop()
1816 stash.pop(index)
1817
1818
1819 def stash_drop(repo, index):
1820 """Drop a stash from the stack."""
1821 with open_repo_closing(repo) as r:
1822 from dulwich.stash import Stash
1823
1824 stash = Stash.from_repo(r)
1825 stash.drop(index)
18171826
18181827
18191828 def ls_files(repo):
12611261
12621262 root_path_bytes = os.fsencode(self.path)
12631263
1264 if not isinstance(fs_paths, list):
1264 if isinstance(fs_paths, str):
12651265 fs_paths = [fs_paths]
1266 fs_paths = list(fs_paths)
1267
12661268 from dulwich.index import (
12671269 blob_from_path_and_stat,
12681270 index_entry_from_stat,
107107 self.assertEqual(b"bar", cf.get((b"core",), b"foo"))
108108 self.assertEqual(b"bar", cf.get((b"core", b"foo"), b"foo"))
109109
110 def test_from_file_utf8_bom(self):
111 text = "[core]\nfoo = b\u00e4r\n".encode("utf-8-sig")
112 cf = self.from_file(text)
113 self.assertEqual(b"b\xc3\xa4r", cf.get((b"core",), b"foo"))
114
110115 def test_from_file_section_case_insensitive_lower(self):
111116 cf = self.from_file(b"[cOre]\nfOo = bar\n")
112117 self.assertEqual(b"bar", cf.get((b"core",), b"foo"))
104104 f = BytesIO(
105105 b"""
106106 # a comment
107
107 \x20\x20
108108 # and an empty line:
109109
110110 \\#not a comment
2626 import os
2727 import shutil
2828 import stat
29 import sys
2930 import tempfile
3031
3132 from dulwich.index import (
437438 for alt_path in store._read_alternate_paths():
438439 self.assertNotIn("#", alt_path)
439440
441 def test_file_modes(self):
442 self.store.add_object(testobject)
443 path = self.store._get_shafile_path(testobject.id)
444 mode = os.stat(path).st_mode
445
446 packmode = "0o100444" if sys.platform != "win32" else "0o100666"
447 self.assertEqual(oct(mode), packmode)
448
440449 def test_corrupted_object_raise_exception(self):
441450 """Corrupted sha1 disk file should raise specific exception"""
442451 self.store.add_object(testobject)
447456 self.assertIsNotNone(self.store._get_loose_object(testobject.id))
448457
449458 path = self.store._get_shafile_path(testobject.id)
459 old_mode = os.stat(path).st_mode
460 os.chmod(path, 0o600)
450461 with open(path, "wb") as f: # corrupt the file
451462 f.write(b"")
463 os.chmod(path, old_mode)
452464
453465 expected_error_msg = "Corrupted empty file detected"
454466 try:
2525 from hashlib import sha1
2626 import os
2727 import shutil
28 import sys
2829 import tempfile
2930 import zlib
3031
8283 a_sha = b"6f670c0fb53f9463760b7295fbb814e965fb20c8"
8384 tree_sha = b"b2a2766a2879c209ab1176e7e778b81ae422eeaa"
8485 commit_sha = b"f18faa16531ac570a3fdc8c7ca16682548dafd12"
86 indexmode = "0o100644" if sys.platform != "win32" else "0o100666"
8587
8688
8789 class PackTests(TestCase):
337339 p.create_index_v1(filename)
338340 idx1 = load_pack_index(filename)
339341 idx2 = self.get_pack_index(pack1_sha)
342 self.assertEqual(oct(os.stat(filename).st_mode), indexmode)
340343 self.assertEqual(idx1, idx2)
341344
342345 def test_create_index_v2(self):
345348 p.create_index_v2(filename)
346349 idx1 = load_pack_index(filename)
347350 idx2 = self.get_pack_index(pack1_sha)
351 self.assertEqual(oct(os.stat(filename).st_mode), indexmode)
348352 self.assertEqual(idx1, idx2)
349353
350354 def test_compute_file_sha(self):
110110 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
161 =YPjc
113 dC5jb20+iQHOBBMBCAA4AhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAFiEEjrR8
114 MQ4fJK44PYMvfN2AClLmXiYFAmDcEZEACgkQfN2AClLmXibZzgv/ZfeTpTuqQE1W
115 C1jT5KpQExnt0BizTX0U7BvSn8Fr6VXTyol6kYc3u71GLUuJyawCLtIzOXqOXJvz
116 bjcZqymcMADuftKcfMy513FhbF6MhdVd6QoeBP6+7/xXOFJCi+QVYF7SQ2h7K1Qm
117 +yXOiAMgSxhCZQGPBNJLlDUOd47nSIMANvlumFtmLY/1FD7RpG7WQWjeX1mnxNTw
118 hUU+Yv7GuFc/JprXCIYqHbhWfvXyVtae2ZK4xuVi5eqwA2RfggOVM7drb+CgPhG0
119 +9aEDDLOZqVi65wK7J73Puo3rFTbPQMljxw5s27rWqF+vB6hhVdJOPNomWy3naPi
120 k5MW0mhsacASz1WYndpZz+XaQTq/wJF5HUyyeUWJ0vlOEdwx021PHcqSTyfNnkjD
121 KncrE21t2sxWRsgGDETxIwkd2b2HNGAvveUD0ffFK/oJHGSXjAERFGc3wuiDj3mQ
122 BvKm4wt4QF9ZMrCdhMAA6ax5kfEUqQR4ntmrJk/khp/mV7TILaI4nQVYBGBjIyIB
123 DADghIo9wXnRxzfdDTvwnP8dHpLAIaPokgdpyLswqUCixJWiW2xcV6weUjEWwH6n
124 eN/t1uZYVehbrotxVPla+MPvzhxp6/cmG+2lhzEBOp6zRwnL1wIB6HoKJfpREhyM
125 c8rLR0zMso1L1bJTyydvnu07a7BWo3VWKjilb0rEZZUSD/2hidx5HxMOJSoidLWe
126 d/PPuv6yht3NtA4UThlcfldm9G6PbqCdm1kMEKAkq0wVJvhPJ6gEFRNJimgygfUw
127 MDFXEIhQtxjgdV5Uoz3O5452VLoRsDlgpi3E0WDGj7WXDaO5uSU0T5aJgVgHCP/f
128 xZhHuQFk2YYIl5nCBpOZyWWI0IKmscTuEwzpkhICQDQFvcMZ5ibsl7wA2P7YTrQf
129 FDMjjzuaK80GYPfxDFlyKUyLqFt8w/QzsZLDLX7+jxIEpbRAaMw/JsWqm5BMxxbS
130 3CIQiS5S3oSKDsNINelqWFfwvLhvlQra8gIxyNTlek25OdgG66BiiX+seH8A/ql+
131 F+MAEQEAAQAL/1jrNSLjMt9pwo6qFKClVQZP2vf7+sH7v7LeHIDXr3EnYUnVYnOq
132 B1FU5PspTp/+J9W25DB9CZLx7Gj8qeslFdiuLSOoIBB4RCToB3kAoeTH0DHqW/Gs
133 hFTrmJkuDp9zpo/ek6SIXJx5rHAyR9KVw0fizQprH2f6PcgLbTWeM61dJuqowmg3
134 7eCOyIKv7VQvFqEhYokLD+JNmrvg+Htg0DXGvdjRjAwPf/NezEXpj67a6cHTp1/C
135 hwp7pevG+3fTxaCJFesl5/TxxtnaBLE8m2uo/S6Hxgn9l0edonroe1QlTjEqGLy2
136 7qi2z5Rem+v6GWNDRgvAWur13v8FNdyduHlioG/NgRsU9mE2MYeFsfi3cfNpJQp/
137 wC9PSCIXrb/45mkS8KyjZpCrIPB9RV/m0MREq01TPom7rstZc4A1pD0Ot7AtUYS3
138 e95zLyEmeLziPJ9fV4fgPmEudDr1uItnmV0LOskKlpg5sc0hhdrwYoobfkKt2dx6
139 DqfMlcM1ZkUbLQYA4jwfpFJG4HmYvjL2xCJxM0ycjvMbqFN+4UjgYWVlRfOrm1V4
140 Op86FjbRbV6OOCNhznotAg7mul4xtzrrTkK8o3YLBeJseDgl4AWuzXtNa9hE0XpK
141 9gJoEHUuBOOsamVh2HpXESFyE5CclOV7JSh541TlZKfnqfZYCg4JSbp0UijkawCL
142 5bJJUiGGMD9rZUxIAKQO1DvUEzptS7Jl6S3y5sbIIhilp4KfYWbSk3PPu9CnZD5b
143 LhEQp0elxnb/IL8PBgD+DpTeC8unkGKXUpbe9x0ISI6V1D6FmJq/FxNg7fMa3QCh
144 fGiAyoTm80ZETynj+blRaDO3gY4lTLa3Opubof1EqK2QmwXmpyvXEZNYcQfQ2CCS
145 GOWUCK8jEQamUPf1PWndZXJUmROI1WukhlL71V/ir6zQeVCv1wcwPwclJPnAe87u
146 pEklnCYpvsEldwHUX9u0BWzoULIEsi+ddtHmT0KTeF/DHRy0W15jIHbjFqhqckj1
147 /6fmr7l7kIi/kN4vWe0F/0Q8IXX+cVMgbl3aIuaGcvENLGcoAsAtPGx88SfRgmfu
148 HK64Y7hx1m+Bo215rxJzZRjqHTBPp0BmCi+JKkaavIBrYRbsx20gveI4dzhLcUhB
149 kiT4Q7oz0/VbGHS1CEf9KFeS/YOGj57s4yHauSVI0XdP9kBRTWmXvBkzsooB2cKH
150 hwhUN7iiT1k717CiTNUT6Q/pcPFCyNuMoBBGQTU206JEgIjQvI3f8xMUMGmGVVQz
151 9/k716ycnhb2JZ/Q/AyQIeHJiQG2BBgBCAAgAhsMFiEEjrR8MQ4fJK44PYMvfN2A
152 ClLmXiYFAmDcEa4ACgkQfN2AClLmXiZxxQv/XaMN0hPCygtrQMbCsTNb34JbvJzh
153 hngPuUAfTbRHrR3YeATyQofNbL0DD3fvfzeFF8qESqvzCSZxS6dYsXPd4MCJTzlp
154 zYBZ2X0sOrgDqZvqCZKN72RKgdk0KvthdzAxsIm2dfcQOxxowXMxhJEXZmsFpusx
155 jKJxOcrfVRjXJnh9isY0NpCoqMQ+3k3wDJ3VGEHV7G+A+vFkWfbLJF5huQ96uaH9
156 Uc+jUsREUH9G82ZBqpoioEN8Ith4VXpYnKdTMonK/+ZcyeraJZhXrvbjnEomKdzU
157 0pu4bt1HlLR3dcnpjN7b009MBf2xLgEfQk2nPZ4zzY+tDkxygtPllaB4dldFjBpT
158 j7Q+t49sWMjmlJUbLlHfuJ7nUUK5+cGjBsWVObAEcyfemHWCTVFnEa2BJslGC08X
159 rFcjRRcMEr9ct4551QFBHsv3O/Wp3/wqczYgE9itSnGT05w+4vLt4smG+dnEHjRJ
160 brMb2upTHa+kjktjdO96/BgSnKYqmNmPB/qB
161 =ivA/
162162 -----END PGP PRIVATE KEY BLOCK-----
163163 """
164164
196196 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
247 =kOKh
199 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
247 =9zU5
248248 -----END PGP PRIVATE KEY BLOCK-----
249249 """
250250
2020
2121 """Tests for the repository."""
2222
23 import glob
2324 import locale
2425 import os
26 import shutil
2527 import stat
26 import shutil
2728 import sys
2829 import tempfile
2930 import warnings
7879 with repo.get_named_file("config") as f:
7980 config_text = f.read()
8081 self.assertTrue(barestr in config_text, "%r" % config_text)
82
83 if isinstance(repo, Repo):
84 expected_mode = '0o100644' if expect_filemode else '0o100666'
85 expected = {
86 'HEAD': expected_mode,
87 'config': expected_mode,
88 'description': expected_mode,
89 }
90 actual = {
91 f[len(repo._controldir) + 1:]: oct(os.stat(f).st_mode)
92 for f in glob.glob(os.path.join(repo._controldir, '*'))
93 if os.path.isfile(f)
94 }
95
96 self.assertEqual(expected, actual)
8197
8298 def test_create_memory(self):
8399 repo = MemoryRepo.init_bare([], {})
00 Metadata-Version: 2.1
11 Name: dulwich
2 Version: 0.20.23
2 Version: 0.20.25
33 Summary: Python Git Library
44 Home-page: https://www.dulwich.io/
55 Author: Jelmer Vernooij
88 Project-URL: Bug Tracker, https://github.com/dulwich/dulwich/issues
99 Project-URL: Repository, https://www.dulwich.io/code/
1010 Project-URL: GitHub, https://github.com/dulwich/dulwich
11 Description: Dulwich
12 =======
13
14 This is the Dulwich project.
15
16 It aims to provide an interface to git repos (both local and remote) that
17 doesn't call out to git directly but instead uses pure Python.
18
19 **Main website**: <https://www.dulwich.io/>
20
21 **License**: Apache License, version 2 or GNU General Public License, version 2 or later.
22
23 The project is named after the part of London that Mr. and Mrs. Git live in
24 in the particular Monty Python sketch.
25
26 Installation
27 ------------
28
29 By default, Dulwich' setup.py will attempt to build and install the optional C
30 extensions. The reason for this is that they significantly improve the performance
31 since some low-level operations that are executed often are much slower in CPython.
32
33 If you don't want to install the C bindings, specify the --pure argument to setup.py::
34
35 $ python setup.py --pure install
36
37 or if you are installing from pip::
38
39 $ pip install dulwich --global-option="--pure"
40
41 Note that you can also specify --global-option in a
42 `requirements.txt <https://pip.pypa.io/en/stable/reference/pip_install/#requirement-specifiers>`_
43 file, e.g. like this::
44
45 dulwich --global-option=--pure
46
47 Getting started
48 ---------------
49
50 Dulwich comes with both a lower-level API and higher-level plumbing ("porcelain").
51
52 For example, to use the lower level API to access the commit message of the
53 last commit::
54
55 >>> from dulwich.repo import Repo
56 >>> r = Repo('.')
57 >>> r.head()
58 '57fbe010446356833a6ad1600059d80b1e731e15'
59 >>> c = r[r.head()]
60 >>> c
61 <Commit 015fc1267258458901a94d228e39f0a378370466>
62 >>> c.message
63 'Add note about encoding.\n'
64
65 And to print it using porcelain::
66
67 >>> from dulwich import porcelain
68 >>> porcelain.log('.', max_entries=1)
69 --------------------------------------------------
70 commit: 57fbe010446356833a6ad1600059d80b1e731e15
71 Author: Jelmer Vernooij <jelmer@jelmer.uk>
72 Date: Sat Apr 29 2017 23:57:34 +0000
73
74 Add note about encoding.
75
76 Further documentation
77 ---------------------
78
79 The dulwich documentation can be found in docs/ and built by running ``make
80 doc``. It can also be found `on the web <https://www.dulwich.io/docs/>`_.
81
82 Help
83 ----
84
85 There is a *#dulwich* IRC channel on the `OFTC <https://www.oftc.net/>`_, and
86 `dulwich-announce <https://groups.google.com/forum/#!forum/dulwich-announce>`_
87 and `dulwich-discuss <https://groups.google.com/forum/#!forum/dulwich-discuss>`_
88 mailing lists.
89
90 Contributing
91 ------------
92
93 For a full list of contributors, see the git logs or `AUTHORS <AUTHORS>`_.
94
95 If you'd like to contribute to Dulwich, see the `CONTRIBUTING <CONTRIBUTING.rst>`_
96 file and `list of open issues <https://github.com/dulwich/dulwich/issues>`_.
97
98 Supported versions of Python
99 ----------------------------
100
101 At the moment, Dulwich supports (and is tested on) CPython 3.5 and later and
102 Pypy.
103
104 The latest release series to support Python 2.x was the 0.19 series. See
105 the 0.19 branch in the Dulwich git repository.
106
10711 Keywords: git vcs
10812 Platform: UNKNOWN
10913 Classifier: Development Status :: 4 - Beta
12327 Provides-Extra: https
12428 Provides-Extra: pgp
12529 Provides-Extra: watch
30 License-File: COPYING
31 License-File: AUTHORS
32
33 Dulwich
34 =======
35
36 This is the Dulwich project.
37
38 It aims to provide an interface to git repos (both local and remote) that
39 doesn't call out to git directly but instead uses pure Python.
40
41 **Main website**: <https://www.dulwich.io/>
42
43 **License**: Apache License, version 2 or GNU General Public License, version 2 or later.
44
45 The project is named after the part of London that Mr. and Mrs. Git live in
46 in the particular Monty Python sketch.
47
48 Installation
49 ------------
50
51 By default, Dulwich' setup.py will attempt to build and install the optional C
52 extensions. The reason for this is that they significantly improve the performance
53 since some low-level operations that are executed often are much slower in CPython.
54
55 If you don't want to install the C bindings, specify the --pure argument to setup.py::
56
57 $ python setup.py --pure install
58
59 or if you are installing from pip::
60
61 $ pip install dulwich --global-option="--pure"
62
63 Note that you can also specify --global-option in a
64 `requirements.txt <https://pip.pypa.io/en/stable/reference/pip_install/#requirement-specifiers>`_
65 file, e.g. like this::
66
67 dulwich --global-option=--pure
68
69 Getting started
70 ---------------
71
72 Dulwich comes with both a lower-level API and higher-level plumbing ("porcelain").
73
74 For example, to use the lower level API to access the commit message of the
75 last commit::
76
77 >>> from dulwich.repo import Repo
78 >>> r = Repo('.')
79 >>> r.head()
80 '57fbe010446356833a6ad1600059d80b1e731e15'
81 >>> c = r[r.head()]
82 >>> c
83 <Commit 015fc1267258458901a94d228e39f0a378370466>
84 >>> c.message
85 'Add note about encoding.\n'
86
87 And to print it using porcelain::
88
89 >>> from dulwich import porcelain
90 >>> porcelain.log('.', max_entries=1)
91 --------------------------------------------------
92 commit: 57fbe010446356833a6ad1600059d80b1e731e15
93 Author: Jelmer Vernooij <jelmer@jelmer.uk>
94 Date: Sat Apr 29 2017 23:57:34 +0000
95
96 Add note about encoding.
97
98 Further documentation
99 ---------------------
100
101 The dulwich documentation can be found in docs/ and built by running ``make
102 doc``. It can also be found `on the web <https://www.dulwich.io/docs/>`_.
103
104 Help
105 ----
106
107 There is a *#dulwich* IRC channel on the `OFTC <https://www.oftc.net/>`_, and
108 `dulwich-announce <https://groups.google.com/forum/#!forum/dulwich-announce>`_
109 and `dulwich-discuss <https://groups.google.com/forum/#!forum/dulwich-discuss>`_
110 mailing lists.
111
112 Contributing
113 ------------
114
115 For a full list of contributors, see the git logs or `AUTHORS <AUTHORS>`_.
116
117 If you'd like to contribute to Dulwich, see the `CONTRIBUTING <CONTRIBUTING.rst>`_
118 file and `list of open issues <https://github.com/dulwich/dulwich/issues>`_.
119
120 Supported versions of Python
121 ----------------------------
122
123 At the moment, Dulwich supports (and is tested on) CPython 3.5 and later and
124 Pypy.
125
126 The latest release series to support Python 2.x was the 0.19 series. See
127 the 0.19 branch in the Dulwich git repository.
128
129
1414 README.swift.rst
1515 SECURITY.md
1616 TODO
17 build.cmd
1817 dulwich.cfg
1918 releaser.conf
2019 requirements.txt
5049 docs/tutorial/repo.txt
5150 docs/tutorial/tag.txt
5251 dulwich/__init__.py
52 dulwich/__main__.py
5353 dulwich/_diff_tree.c
5454 dulwich/_objects.c
5555 dulwich/_pack.c
9292 dulwich.egg-info/SOURCES.txt
9393 dulwich.egg-info/dependency_links.txt
9494 dulwich.egg-info/entry_points.txt
95 dulwich.egg-info/not-zip-safe
9596 dulwich.egg-info/requires.txt
9697 dulwich.egg-info/top_level.txt
9798 dulwich/cloud/__init__.py
0 # See https://github.com/jelmer/releaser
01 news_file: "NEWS"
12 timeout_days: 5
23 tag_name: "dulwich-$VERSION"
2222 'For 2.7 support, please install a version prior to 0.20')
2323
2424
25 dulwich_version_string = '0.20.23'
25 dulwich_version_string = '0.20.25'
2626
2727
2828 class DulwichDistribution(Distribution):
115115 package_data={'': ['../docs/tutorial/*.txt', 'py.typed']},
116116 scripts=scripts,
117117 ext_modules=ext_modules,
118 zip_safe=False,
118119 distclass=DulwichDistribution,
119120 classifiers=[
120121 'Development Status :: 4 - Beta',