Quellcode durchsuchen

New upstream version 0.19.1

Jelmer Vernooij vor 7 Jahren
Ursprung
Commit
3db91a117b

+ 6 - 0
.coveragerc

@@ -0,0 +1,6 @@
+[run]
+branch = True
+
+[report]
+exclude_lines =
+    raise NotImplementedError

+ 23 - 0
.gitignore

@@ -0,0 +1,23 @@
+_trial_temp
+build
+build-pypy
+MANIFEST
+dist
+apidocs
+*,cover
+.testrepository
+*.pyc
+*.pyd
+*.pyo
+*.so
+*~
+*.swp
+*.swh
+*.swn
+*.swo
+docs/tutorial/index.html
+dulwich.egg-info/
+.tox/
+.idea/
+.coverage
+htmlcov/

+ 23 - 0
.mailmap

@@ -0,0 +1,23 @@
+Jelmer Vernooij <jelmer@jelmer.uk>
+Jelmer Vernooij <jelmer@jelmer.uk> <jelmer@jelmer.uk>
+Jelmer Vernooij <jelmer@jelmer.uk> <jelmer@samba.org>
+Jelmer Vernooij <jelmer@jelmer.uk> <jelmer@debian.org>
+Jelmer Vernooij <jelmer@jelmer.uk> <jelmer@canonical.com>
+Jelmer Vernooij <jelmer@jelmer.uk> <jelmer@google.com>
+Martin <gzlist@googlemail.com> <martin.packman@canonical.com>
+Dave Borowitz <dborowitz@google.com> <ddborowitz@gmail.com>
+Dave Borowitz <dborowitz@google.com> <dborowitz@google.com>
+John Carr <john.carr@unrouted.co.uk>
+Mark Mikofski <bwanamarko@yahoo.com> <mark.mikofski@sunpowercorp.com>
+Mark Mikofski <bwanamarko@yahoo.com> <bwana.marko@yahoo.com>
+David Carr <david@carrclan.us>
+Jon Bain <jsbain@yahoo.com> <jsbain@yahoo.com>
+James Westby <jw+debian@jameswestby.net> <jw+debian@jameswestby.net>
+David Keijser <david.keijser@klarna.com> <keijser@gmail.com>
+Benoît HERVIER <khertan@khertan.net> <khertan@khertan.net>
+Ryan Faulkner <rfaulk@yahoo-inc.com> <rfaulkner@wikimedia.org>
+David Bennett <davbennett@google.com> <david@dbinit.com>
+Risto Kankkunen <risto.kankkunen@iki.fi> <risto.kankkunen@f-secure.com>
+Augie Fackler <durin42@gmail.com> <raf@durin42.com>
+Damien Tournoud <damien@commerceguys.com> <damien@platform.sh>
+Marcin Kuźmiński <marcin@python-blog.com> <marcin@python-works.com>

+ 2 - 0
AUTHORS

@@ -136,5 +136,7 @@ Kenneth Lareau <kenneth.lareau@couchbase.com>
 Antoine R. Dumont (@ardumont) <antoine.romain.dumont@gmail.com>
 Alistair Broomhead <alistair.broomhead@gmail.com>
 Marcel Schnirring <mschnirring@marcel-schnirring.de>
+Adam Bradley <adam_bradley@brown.edu>
+Filipp Frizzy <filipp.s.frizzy@gmail.com>
 
 If you contributed but are missing from this list, please send me an e-mail.

+ 18 - 0
MANIFEST.in

@@ -0,0 +1,18 @@
+include NEWS
+include AUTHORS
+include README.md
+include README.swift.md
+include Makefile
+include COPYING
+include CONTRIBUTING.md
+include TODO
+include setup.cfg
+include dulwich/stdint.h
+recursive-include docs conf.py *.txt Makefile make.bat
+recursive-include examples *.py
+graft dulwich/tests/data
+include tox.ini
+include dulwich.cfg
+include appveyor.yml
+include .testr.conf
+include .travis.yml

+ 33 - 0
NEWS

@@ -1,3 +1,36 @@
+0.19.1	2018-04-05
+
+ IMPROVEMENTS
+
+  * Add 'dulwich.mailmap' file for reading mailmap files.
+    (Jelmer Vernooij)
+
+  * Dulwich no longer depends on urllib3[secure]. Instead,
+    "dulwich[https]" can be used to pull in the necessary
+    dependencies for HTTPS support. (Jelmer Vernooij, #616)
+
+  * Support the `http.sslVerify` and `http.sslCAInfo`
+    configuration options. (Jelmer Vernooij)
+
+  * Factor out `dulwich.client.parse_rsync_url` function.
+    (Jelmer Vernooij)
+
+  * Fix repeat HTTP requests using the same smart HTTP client.
+    (Jelmer Vernooij)
+
+  * New 'client.PLinkSSHVendor' for creating connections using PuTTY's plink.exe.
+    (Adam Bradley, Filipp Frizzy)
+
+  * Only pass in `key_filename` and `password` to SSHVendor
+    implementations if those parameters are set.
+    (This helps with older SSHVendor implementations)
+    (Jelmer Vernooij)
+
+ API CHANGES
+
+  * Index.iterblobs has been renamed to Index.iterobjects.
+   (Jelmer Vernooij)
+
 0.19.0	2018-03-10
 
  BUG FIXES

+ 4 - 3
PKG-INFO

@@ -1,9 +1,8 @@
-Metadata-Version: 1.1
+Metadata-Version: 2.1
 Name: dulwich
-Version: 0.19.0
+Version: 0.19.1
 Summary: Python Git Library
 Home-page: https://www.dulwich.io/
-Author: UNKNOWN
 Author-email: jelmer@jelmer.uk
 License: Apachev2 or later or GPLv2
 Description: 
@@ -30,3 +29,5 @@ Classifier: Programming Language :: Python :: Implementation :: PyPy
 Classifier: Operating System :: POSIX
 Classifier: Operating System :: Microsoft :: Windows
 Classifier: Topic :: Software Development :: Version Control
+Provides-Extra: fastimport
+Provides-Extra: https

+ 21 - 5
bin/dulwich

@@ -501,11 +501,9 @@ class cmd_remote_add(Command):
         porcelain.remote_add('.', args[0], args[1])
 
 
-class cmd_remote(Command):
+class SuperCommand(Command):
 
-    subcommands = {
-        "add": cmd_remote_add,
-    }
+    subcommands = {}
 
     def run(self, args):
         if not args:
@@ -517,7 +515,14 @@ class cmd_remote(Command):
         except KeyError:
             print('No such subcommand: %s' % args[0])
             return False
-        return cmd_kls(args[1:])
+        return cmd_kls().run(args[1:])
+
+
+class cmd_remote(SuperCommand):
+
+    subcommands = {
+        "add": cmd_remote_add,
+    }
 
 
 class cmd_check_ignore(Command):
@@ -532,6 +537,16 @@ class cmd_check_ignore(Command):
         return ret
 
 
+class cmd_check_mailmap(Command):
+
+    def run(self, args):
+        parser = optparse.OptionParser()
+        options, args = parser.parse_args(args)
+        for arg in args:
+            canonical_identity = porcelain.check_mailmap('.', arg)
+            print(canonical_identity)
+
+
 class cmd_help(Command):
 
     def run(self, args):
@@ -558,6 +573,7 @@ commands = {
     "add": cmd_add,
     "archive": cmd_archive,
     "check-ignore": cmd_check_ignore,
+    "check-mailmap": cmd_check_mailmap,
     "clone": cmd_clone,
     "commit": cmd_commit,
     "commit-tree": cmd_commit_tree,

+ 21 - 0
build.cmd

@@ -0,0 +1,21 @@
+@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 %*

+ 15 - 0
devscripts/PREAMBLE.c

@@ -0,0 +1,15 @@
+ * Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
+ * General Public License as public by the Free Software Foundation; version 2.0
+ * or (at your option) any later version. You can redistribute it and/or
+ * modify it under the terms of either of these two licenses.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * You should have received a copy of the licenses; if not, see
+ * <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
+ * and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
+ * License, Version 2.0.

+ 16 - 0
devscripts/PREAMBLE.py

@@ -0,0 +1,16 @@
+# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
+# General Public License as public by the Free Software Foundation; version 2.0
+# or (at your option) any later version. You can redistribute it and/or
+# modify it under the terms of either of these two licenses.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# You should have received a copy of the licenses; if not, see
+# <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
+# and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
+# License, Version 2.0.
+#

+ 3 - 0
devscripts/replace-preamble.sh

@@ -0,0 +1,3 @@
+#!/usr/bin/zsh
+perl -i -p0e "s{\Q$(cat PREAMBLE.py.old)\E}{$(cat devscripts/PREAMBLE.py)}g" dulwich/**/*.py bin/dul*
+perl -i -p0e "s{\Q$(cat PREAMBLE.c.old)\E}{$(cat devscripts/PREAMBLE.c)}g" dulwich/*.c

+ 2 - 0
docs/tutorial/.gitignore

@@ -0,0 +1,2 @@
+*.html
+myrepo

+ 33 - 0
dulwich.egg-info/PKG-INFO

@@ -0,0 +1,33 @@
+Metadata-Version: 2.1
+Name: dulwich
+Version: 0.19.1
+Summary: Python Git Library
+Home-page: https://www.dulwich.io/
+Author-email: jelmer@jelmer.uk
+License: Apachev2 or later or GPLv2
+Description: 
+              Python implementation of the Git file formats and protocols,
+              without the need to have git installed.
+        
+              All functionality is available in pure Python. Optional
+              C extensions can be built for improved performance.
+        
+              The project is named after the part of London that Mr. and Mrs. Git live
+              in in the particular Monty Python sketch.
+              
+Keywords: git
+Platform: UNKNOWN
+Classifier: Development Status :: 4 - Beta
+Classifier: License :: OSI Approved :: Apache Software License
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Operating System :: POSIX
+Classifier: Operating System :: Microsoft :: Windows
+Classifier: Topic :: Software Development :: Version Control
+Provides-Extra: fastimport
+Provides-Extra: https

+ 214 - 0
dulwich.egg-info/SOURCES.txt

@@ -0,0 +1,214 @@
+.coveragerc
+.gitignore
+.mailmap
+.testr.conf
+.travis.yml
+AUTHORS
+CONTRIBUTING.md
+COPYING
+MANIFEST.in
+Makefile
+NEWS
+README.md
+README.swift.md
+TODO
+appveyor.yml
+build.cmd
+dulwich.cfg
+requirements.txt
+setup.cfg
+setup.py
+tox.ini
+bin/dul-receive-pack
+bin/dul-upload-pack
+bin/dulwich
+devscripts/PREAMBLE.c
+devscripts/PREAMBLE.py
+devscripts/replace-preamble.sh
+docs/Makefile
+docs/conf.py
+docs/index.txt
+docs/make.bat
+docs/performance.txt
+docs/protocol.txt
+docs/tutorial/.gitignore
+docs/tutorial/Makefile
+docs/tutorial/conclusion.txt
+docs/tutorial/encoding.txt
+docs/tutorial/file-format.txt
+docs/tutorial/index.txt
+docs/tutorial/introduction.txt
+docs/tutorial/object-store.txt
+docs/tutorial/porcelain.txt
+docs/tutorial/remote.txt
+docs/tutorial/repo.txt
+docs/tutorial/tag.txt
+dulwich/__init__.py
+dulwich/_diff_tree.c
+dulwich/_objects.c
+dulwich/_pack.c
+dulwich/archive.py
+dulwich/client.py
+dulwich/config.py
+dulwich/diff_tree.py
+dulwich/errors.py
+dulwich/fastexport.py
+dulwich/file.py
+dulwich/greenthreads.py
+dulwich/hooks.py
+dulwich/ignore.py
+dulwich/index.py
+dulwich/log_utils.py
+dulwich/lru_cache.py
+dulwich/mailmap.py
+dulwich/object_store.py
+dulwich/objects.py
+dulwich/objectspec.py
+dulwich/pack.py
+dulwich/patch.py
+dulwich/porcelain.py
+dulwich/protocol.py
+dulwich/reflog.py
+dulwich/refs.py
+dulwich/repo.py
+dulwich/server.py
+dulwich/stash.py
+dulwich/stdint.h
+dulwich/walk.py
+dulwich/web.py
+dulwich.egg-info/PKG-INFO
+dulwich.egg-info/SOURCES.txt
+dulwich.egg-info/dependency_links.txt
+dulwich.egg-info/requires.txt
+dulwich.egg-info/top_level.txt
+dulwich/contrib/README.md
+dulwich/contrib/__init__.py
+dulwich/contrib/paramiko_vendor.py
+dulwich/contrib/release_robot.py
+dulwich/contrib/swift.py
+dulwich/contrib/test_release_robot.py
+dulwich/contrib/test_swift.py
+dulwich/contrib/test_swift_smoke.py
+dulwich/tests/__init__.py
+dulwich/tests/test_archive.py
+dulwich/tests/test_blackbox.py
+dulwich/tests/test_client.py
+dulwich/tests/test_config.py
+dulwich/tests/test_diff_tree.py
+dulwich/tests/test_fastexport.py
+dulwich/tests/test_file.py
+dulwich/tests/test_grafts.py
+dulwich/tests/test_greenthreads.py
+dulwich/tests/test_hooks.py
+dulwich/tests/test_ignore.py
+dulwich/tests/test_index.py
+dulwich/tests/test_lru_cache.py
+dulwich/tests/test_mailmap.py
+dulwich/tests/test_missing_obj_finder.py
+dulwich/tests/test_object_store.py
+dulwich/tests/test_objects.py
+dulwich/tests/test_objectspec.py
+dulwich/tests/test_pack.py
+dulwich/tests/test_patch.py
+dulwich/tests/test_porcelain.py
+dulwich/tests/test_protocol.py
+dulwich/tests/test_reflog.py
+dulwich/tests/test_refs.py
+dulwich/tests/test_repository.py
+dulwich/tests/test_server.py
+dulwich/tests/test_utils.py
+dulwich/tests/test_walk.py
+dulwich/tests/test_web.py
+dulwich/tests/utils.py
+dulwich/tests/compat/__init__.py
+dulwich/tests/compat/server_utils.py
+dulwich/tests/compat/test_client.py
+dulwich/tests/compat/test_pack.py
+dulwich/tests/compat/test_repository.py
+dulwich/tests/compat/test_server.py
+dulwich/tests/compat/test_utils.py
+dulwich/tests/compat/test_web.py
+dulwich/tests/compat/utils.py
+dulwich/tests/data/blobs/11/11111111111111111111111111111111111111
+dulwich/tests/data/blobs/6f/670c0fb53f9463760b7295fbb814e965fb20c8
+dulwich/tests/data/blobs/95/4a536f7819d40e6f637f849ee187dd10066349
+dulwich/tests/data/blobs/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391
+dulwich/tests/data/commits/0d/89f20333fbb1d2f3a94da77f4981373d8f4310
+dulwich/tests/data/commits/5d/ac377bdded4c9aeb8dff595f0faeebcc8498cc
+dulwich/tests/data/commits/60/dacdc733de308bb77bb76ce0fb0f9b44c9769e
+dulwich/tests/data/indexes/index
+dulwich/tests/data/packs/pack-bc63ddad95e7321ee734ea11a7a62d314e0d7481.idx
+dulwich/tests/data/packs/pack-bc63ddad95e7321ee734ea11a7a62d314e0d7481.pack
+dulwich/tests/data/repos/.gitattributes
+dulwich/tests/data/repos/issue88_expect_ack_nak_client.export
+dulwich/tests/data/repos/issue88_expect_ack_nak_other.export
+dulwich/tests/data/repos/issue88_expect_ack_nak_server.export
+dulwich/tests/data/repos/server_new.export
+dulwich/tests/data/repos/server_old.export
+dulwich/tests/data/repos/a.git/HEAD
+dulwich/tests/data/repos/a.git/packed-refs
+dulwich/tests/data/repos/a.git/objects/28/237f4dc30d0d462658d6b937b08a0f0b6ef55a
+dulwich/tests/data/repos/a.git/objects/2a/72d929692c41d8554c07f6301757ba18a65d91
+dulwich/tests/data/repos/a.git/objects/4e/f30bbfe26431a69c3820d3a683df54d688f2ec
+dulwich/tests/data/repos/a.git/objects/4f/2e6529203aa6d44b5af6e3292c837ceda003f9
+dulwich/tests/data/repos/a.git/objects/7d/9a07d797595ef11344549b8d08198e48c15364
+dulwich/tests/data/repos/a.git/objects/a2/96d0bb611188cabb256919f36bc30117cca005
+dulwich/tests/data/repos/a.git/objects/a9/0fa2d900a17e99b433217e988c4eb4a2e9a097
+dulwich/tests/data/repos/a.git/objects/b0/931cadc54336e78a1d980420e3268903b57a50
+dulwich/tests/data/repos/a.git/objects/ff/d47d45845a8f6576491e1edb97e3fe6a850e7f
+dulwich/tests/data/repos/a.git/refs/heads/master
+dulwich/tests/data/repos/a.git/refs/tags/mytag
+dulwich/tests/data/repos/empty.git/HEAD
+dulwich/tests/data/repos/empty.git/config
+dulwich/tests/data/repos/empty.git/objects/info/.gitignore
+dulwich/tests/data/repos/empty.git/objects/pack/.gitignore
+dulwich/tests/data/repos/empty.git/refs/heads/.gitignore
+dulwich/tests/data/repos/empty.git/refs/tags/.gitignore
+dulwich/tests/data/repos/ooo_merge.git/HEAD
+dulwich/tests/data/repos/ooo_merge.git/objects/29/69be3e8ee1c0222396a5611407e4769f14e54b
+dulwich/tests/data/repos/ooo_merge.git/objects/38/74e9c60a6d149c44c928140f250d81e6381520
+dulwich/tests/data/repos/ooo_merge.git/objects/6f/670c0fb53f9463760b7295fbb814e965fb20c8
+dulwich/tests/data/repos/ooo_merge.git/objects/70/c190eb48fa8bbb50ddc692a17b44cb781af7f6
+dulwich/tests/data/repos/ooo_merge.git/objects/76/01d7f6231db6a57f7bbb79ee52e4d462fd44d1
+dulwich/tests/data/repos/ooo_merge.git/objects/90/182552c4a85a45ec2a835cadc3451bebdfe870
+dulwich/tests/data/repos/ooo_merge.git/objects/95/4a536f7819d40e6f637f849ee187dd10066349
+dulwich/tests/data/repos/ooo_merge.git/objects/b2/a2766a2879c209ab1176e7e778b81ae422eeaa
+dulwich/tests/data/repos/ooo_merge.git/objects/f5/07291b64138b875c28e03469025b1ea20bc614
+dulwich/tests/data/repos/ooo_merge.git/objects/f9/e39b120c68182a4ba35349f832d0e4e61f485c
+dulwich/tests/data/repos/ooo_merge.git/objects/fb/5b0425c7ce46959bec94d54b9a157645e114f5
+dulwich/tests/data/repos/ooo_merge.git/refs/heads/master
+dulwich/tests/data/repos/refs.git/HEAD
+dulwich/tests/data/repos/refs.git/packed-refs
+dulwich/tests/data/repos/refs.git/objects/3b/9e5457140e738c2dcd39bf6d7acf88379b90d1
+dulwich/tests/data/repos/refs.git/objects/3e/c9c43c84ff242e3ef4a9fc5bc111fd780a76a8
+dulwich/tests/data/repos/refs.git/objects/42/d06bd4b77fed026b154d16493e5deab78f02ec
+dulwich/tests/data/repos/refs.git/objects/a1/8114c31713746a33a2e70d9914d1ef3e781425
+dulwich/tests/data/repos/refs.git/objects/cd/a609072918d7b70057b6bef9f4c2537843fcfe
+dulwich/tests/data/repos/refs.git/objects/df/6800012397fb85c56e7418dd4eb9405dee075c
+dulwich/tests/data/repos/refs.git/refs/heads/40-char-ref-aaaaaaaaaaaaaaaaaa
+dulwich/tests/data/repos/refs.git/refs/heads/loop
+dulwich/tests/data/repos/refs.git/refs/heads/master
+dulwich/tests/data/repos/refs.git/refs/tags/refs-0.2
+dulwich/tests/data/repos/simple_merge.git/HEAD
+dulwich/tests/data/repos/simple_merge.git/objects/0d/89f20333fbb1d2f3a94da77f4981373d8f4310
+dulwich/tests/data/repos/simple_merge.git/objects/1b/6318f651a534b38f9c7aedeebbd56c1e896853
+dulwich/tests/data/repos/simple_merge.git/objects/29/69be3e8ee1c0222396a5611407e4769f14e54b
+dulwich/tests/data/repos/simple_merge.git/objects/4c/ffe90e0a41ad3f5190079d7c8f036bde29cbe6
+dulwich/tests/data/repos/simple_merge.git/objects/5d/ac377bdded4c9aeb8dff595f0faeebcc8498cc
+dulwich/tests/data/repos/simple_merge.git/objects/60/dacdc733de308bb77bb76ce0fb0f9b44c9769e
+dulwich/tests/data/repos/simple_merge.git/objects/6f/670c0fb53f9463760b7295fbb814e965fb20c8
+dulwich/tests/data/repos/simple_merge.git/objects/70/c190eb48fa8bbb50ddc692a17b44cb781af7f6
+dulwich/tests/data/repos/simple_merge.git/objects/90/182552c4a85a45ec2a835cadc3451bebdfe870
+dulwich/tests/data/repos/simple_merge.git/objects/95/4a536f7819d40e6f637f849ee187dd10066349
+dulwich/tests/data/repos/simple_merge.git/objects/ab/64bbdcc51b170d21588e5c5d391ee5c0c96dfd
+dulwich/tests/data/repos/simple_merge.git/objects/d4/bdad6549dfedf25d3b89d21f506aff575b28a7
+dulwich/tests/data/repos/simple_merge.git/objects/d8/0c186a03f423a81b39df39dc87fd269736ca86
+dulwich/tests/data/repos/simple_merge.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391
+dulwich/tests/data/repos/simple_merge.git/refs/heads/master
+dulwich/tests/data/repos/submodule/dotgit
+dulwich/tests/data/tags/71/033db03a03c6a36721efcf1968dd8f8e0cf023
+dulwich/tests/data/trees/70/c190eb48fa8bbb50ddc692a17b44cb781af7f6
+examples/clone.py
+examples/config.py
+examples/diff.py
+examples/latest_change.py

+ 1 - 0
dulwich.egg-info/dependency_links.txt

@@ -0,0 +1 @@
+

+ 8 - 0
dulwich.egg-info/requires.txt

@@ -0,0 +1,8 @@
+certifi
+urllib3>=1.21
+
+[fastimport]
+fastimport
+
+[https]
+urllib3[secure]>=1.21

+ 1 - 0
dulwich.egg-info/top_level.txt

@@ -0,0 +1 @@
+dulwich

+ 1 - 1
dulwich/__init__.py

@@ -22,4 +22,4 @@
 
 """Python implementation of the Git file formats and protocols."""
 
-__version__ = (0, 19, 0)
+__version__ = (0, 19, 1)

+ 85 - 52
dulwich/client.py

@@ -60,10 +60,6 @@ except ImportError:
     import urllib.request as urllib2
     import urllib.parse as urlparse
 
-import certifi
-import urllib3
-import urllib3.util
-
 import dulwich
 from dulwich.errors import (
     GitProtocolError,
@@ -112,13 +108,6 @@ from dulwich.refs import (
     )
 
 
-if sys.version_info < (2, 7, 9):
-    # Before Python 2.7.9 the `ssl` module lacks SNI support and lags behind in
-    # security updates. Use pyOpenSSL instead.
-    import urllib3.contrib.pyopenssl
-    urllib3.contrib.pyopenssl.inject_into_urllib3()
-
-
 def _fileno_can_read(fileno):
     """Check if a file descriptor is readable."""
     return len(select.select([fileno], [], [], 0)[0]) > 0
@@ -276,6 +265,10 @@ class FetchPackResult(object):
             return getattr(self.refs, name)
         return super(FetchPackResult, self).__getattribute__(name)
 
+    def __repr__(self):
+        return "%s(%r, %r, %r)" % (
+                self.__class__.__name__, self.refs, self.symrefs, self.agent)
+
 
 # TODO(durin42): this doesn't correctly degrade if the server doesn't
 # support some capabilities. This should work properly with servers
@@ -1110,11 +1103,9 @@ class SubprocessSSHVendor(SSHVendor):
     def run_command(self, host, command, username=None, port=None,
                     password=None, key_filename=None):
 
-        if password:
+        if password is not None:
             raise NotImplementedError(
-                "You can't set password or passphrase for ssh key "
-                "with SubprocessSSHVendor, use ParamikoSSHVendor instead"
-            )
+                "Setting password not supported by SubprocessSSHVendor.")
 
         args = ['ssh', '-x']
 
@@ -1136,27 +1127,21 @@ class SubprocessSSHVendor(SSHVendor):
         return SubprocessWrapper(proc)
 
 
-class PuttySSHVendor(SSHVendor):
-    """SSH vendor that shells out to the local 'putty' command."""
+class PLinkSSHVendor(SSHVendor):
+    """SSH vendor that shells out to the local 'plink' command."""
 
     def run_command(self, host, command, username=None, port=None,
                     password=None, key_filename=None):
 
-        if password and key_filename:
-            raise NotImplementedError(
-                "You can't set passphrase for ssh key "
-                "with PuttySSHVendor, use ParamikoSSHVendor instead"
-            )
-
         if sys.platform == 'win32':
-            args = ['putty.exe', '-ssh']
+            args = ['plink.exe', '-ssh']
         else:
-            args = ['putty', '-ssh']
+            args = ['plink', '-ssh']
 
-        if password:
+        if password is not None:
             import warnings
             warnings.warn(
-                "Invoking Putty with a password exposes the password in the "
+                "Invoking PLink with a password exposes the password in the "
                 "process list.")
             args.extend(['-pw', str(password)])
 
@@ -1236,9 +1221,14 @@ class SSHGitClient(TraditionalGitClient):
             path = path[1:]
         argv = (self._get_cmd_path(cmd).decode(self._remote_path_encoding) +
                 " '" + path + "'")
+        kwargs = {}
+        if self.password is not None:
+            kwargs['password'] = self.password
+        if self.key_filename is not None:
+            kwargs['key_filename'] = self.key_filename
         con = self.ssh_vendor.run_command(
             self.host, argv, port=self.port, username=self.username,
-            password=self.password, key_filename=self.key_filename)
+            **kwargs)
         return (Protocol(con.read, con.write, con.close,
                          report_activity=self._report_activity),
                 con.can_read)
@@ -1250,17 +1240,18 @@ def default_user_agent_string():
     return "git/dulwich/%s" % ".".join([str(x) for x in dulwich.__version__])
 
 
-def default_urllib3_manager(config, verify_ssl=True):
+def default_urllib3_manager(config, **override_kwargs):
     """Return `urllib3` connection pool manager.
 
     Honour detected proxy configurations.
 
     :param config: `dulwich.config.ConfigDict` instance with Git configuration.
-    :param verify_ssl: Whether SSL verification is enabled.
+    :param kwargs: Additional arguments for urllib3.ProxyManager
     :return: `urllib3.ProxyManager` instance for proxy configurations,
         `urllib3.PoolManager` otherwise.
     """
     proxy_server = user_agent = None
+    ca_certs = ssl_verify = None
 
     if config is not None:
         try:
@@ -1272,24 +1263,55 @@ def default_urllib3_manager(config, verify_ssl=True):
         except KeyError:
             pass
 
-    ssl_kwargs = {}
-    if verify_ssl:
-        ssl_kwargs.update(cert_reqs="CERT_REQUIRED", ca_certs=certifi.where())
+        # TODO(jelmer): Support per-host settings
+        try:
+            ssl_verify = config.get_boolean(b"http", b"sslVerify")
+        except KeyError:
+            ssl_verify = True
+
+        try:
+            ca_certs = config.get_boolean(b"http", b"sslCAInfo")
+        except KeyError:
+            ca_certs = None
 
     if user_agent is None:
         user_agent = default_user_agent_string()
 
     headers = {"User-agent": user_agent}
 
+    kwargs = {}
+    if ssl_verify is True:
+        kwargs['cert_reqs'] = "CERT_REQUIRED"
+    elif ssl_verify is False:
+        kwargs['cert_reqs'] = 'CERT_NONE'
+    else:
+        # Default to SSL verification
+        kwargs['cert_reqs'] = "CERT_REQUIRED"
+
+    if ca_certs is not None:
+        kwargs['ca_certs'] = ca_certs
+    kwargs.update(override_kwargs)
+
+    # Try really hard to find a SSL certificate path
+    if 'ca_certs' not in kwargs and kwargs.get('cert_reqs') != 'CERT_NONE':
+        try:
+            import certifi
+        except ImportError:
+            pass
+        else:
+            kwargs['ca_certs'] = certifi.where()
+
+    import urllib3
+
     if proxy_server is not None:
         # `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 = urllib3.ProxyManager(proxy_server, headers=headers,
-                                       **ssl_kwargs)
+                                       **kwargs)
     else:
-        manager = urllib3.PoolManager(headers=headers, **ssl_kwargs)
+        manager = urllib3.PoolManager(headers=headers, **kwargs)
 
     return manager
 
@@ -1313,6 +1335,7 @@ class HttpGitClient(GitClient):
             # 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)
 
@@ -1400,7 +1423,7 @@ class HttpGitClient(GitClient):
         assert base_url[-1] == "/"
         tail = "info/refs"
         headers = {"Accept": "*/*"}
-        if self.dumb is not False:
+        if self.dumb is not True:
             tail += "?service=%s" % service.decode('ascii')
         url = urlparse.urljoin(base_url, tail)
         resp, read = self._http_request(url, headers, allow_compression=True)
@@ -1575,6 +1598,25 @@ def get_transport_and_path_from_url(url, config=None, **kwargs):
     raise ValueError("unknown scheme '%s'" % parsed.scheme)
 
 
+def parse_rsync_url(location):
+    """Parse a rsync-style URL."""
+    if ':' in location and '@' not in location:
+        # SSH with no user@, zero or one leading slash.
+        (host, path) = location.split(':', 1)
+        user = None
+    elif ':' in location:
+        # SSH with user@host:foo.
+        user_host, path = location.split(':', 1)
+        if '@' in user_host:
+            user, host = user_host.rsplit('@', 1)
+        else:
+            user = None
+            host = user_host
+    else:
+        raise ValueError('not a valid rsync-style URL')
+    return (user, host, path)
+
+
 def get_transport_and_path(location, **kwargs):
     """Obtain a git client from a URL.
 
@@ -1596,19 +1638,10 @@ def get_transport_and_path(location, **kwargs):
         # Windows local path
         return default_local_git_client_cls(**kwargs), location
 
-    if ':' in location and '@' not in location:
-        # SSH with no user@, zero or one leading slash.
-        (hostname, path) = location.split(':', 1)
-        return SSHGitClient(hostname, **kwargs), path
-    elif ':' in location:
-        # SSH with user@host:foo.
-        user_host, path = location.split(':', 1)
-        if '@' in user_host:
-            user, host = user_host.rsplit('@', 1)
-        else:
-            user = None
-            host = user_host
-        return SSHGitClient(host, username=user, **kwargs), path
-
-    # Otherwise, assume it's a local path.
-    return default_local_git_client_cls(**kwargs), location
+    try:
+        (username, hostname, path) = parse_rsync_url(location)
+    except ValueError:
+        # Otherwise, assume it's a local path.
+        return default_local_git_client_cls(**kwargs), location
+    else:
+        return SSHGitClient(hostname, username=username, **kwargs), path

+ 3 - 0
dulwich/contrib/README.md

@@ -0,0 +1,3 @@
+This directory contains code that some may find useful. Code here is not an official
+part of Dulwich, and may no longer work. Unlike the rest of Dulwich, it is not regularly
+tested.

+ 107 - 28
dulwich/index.py

@@ -267,12 +267,17 @@ class Index(object):
         """Return the POSIX file mode for the object at a path."""
         return self[path].mode
 
-    def iterblobs(self):
+    def iterobjects(self):
         """Iterate over path, sha, mode tuples for use with commit_tree."""
         for path in self:
             entry = self[path]
             yield path, entry.sha, cleanup_mode(entry.mode)
 
+    def iterblobs(self):
+        import warnings
+        warnings.warn(PendingDeprecationWarning, 'Use iterobjects() instead.')
+        return self.iterblobs()
+
     def clear(self):
         """Remove all contents from this index."""
         self._byname = {}
@@ -317,7 +322,7 @@ class Index(object):
         :param object_store: Object store to save the tree in
         :return: Root tree SHA
         """
-        return commit_tree(object_store, self.iterblobs())
+        return commit_tree(object_store, self.iterobjects())
 
 
 def commit_tree(object_store, blobs):
@@ -368,7 +373,7 @@ def commit_index(object_store, index):
     :note: This function is deprecated, use index.commit() instead.
     :return: Root tree sha.
     """
-    return commit_tree(object_store, index.iterblobs())
+    return commit_tree(object_store, index.iterobjects())
 
 
 def changes_from_tree(names, lookup_entry, object_store, tree,
@@ -418,7 +423,9 @@ def index_entry_from_stat(stat_val, hex_sha, flags, mode=None):
     """
     if mode is None:
         mode = cleanup_mode(stat_val.st_mode)
-    return (stat_val.st_ctime, stat_val.st_mtime, stat_val.st_dev,
+
+    return IndexEntry(
+            stat_val.st_ctime, stat_val.st_mtime, stat_val.st_dev,
             stat_val.st_ino, mode, stat_val.st_uid,
             stat_val.st_gid, stat_val.st_size, hex_sha, flags)
 
@@ -572,6 +579,24 @@ def blob_from_path_and_stat(fs_path, st):
     return blob
 
 
+def read_submodule_head(path):
+    """Read the head commit of a submodule.
+
+    :param path: path to the submodule
+    :return: HEAD sha, None if not a valid head/repository
+    """
+    from dulwich.errors import NotGitRepository
+    from dulwich.repo import Repo
+    try:
+        repo = Repo(path)
+    except NotGitRepository:
+        return None
+    try:
+        return repo.head()
+    except KeyError:
+        return None
+
+
 def get_unstaged_changes(index, root_path):
     """Walk through an index and check for differences against working tree.
 
@@ -599,12 +624,8 @@ def get_unstaged_changes(index, root_path):
             # This is actually a directory
             if os.path.exists(os.path.join(tree_path, '.git')):
                 # Submodule
-                from dulwich.errors import NotGitRepository
-                from dulwich.repo import Repo
-                try:
-                    if entry.sha != Repo(tree_path).head():
-                        yield tree_path
-                except NotGitRepository:
+                head = read_submodule_head(tree_path)
+                if entry.sha != head:
                     yield tree_path
             else:
                 # The file was changed to a directory, so consider it removed.
@@ -654,42 +675,100 @@ def _fs_to_tree_path(fs_path, fs_encoding=None):
     return tree_path
 
 
-def iter_fresh_entries(index, root_path):
+def index_entry_from_path(path, object_store=None):
+    """Create an index from a filesystem path.
+
+    This returns an index value for files, symlinks
+    and tree references. for directories and
+    non-existant files it returns None
+
+    :param path: Path to create an index entry for
+    :param object_store: Optional object store to
+        save new blobs in
+    :return: An index entry
+    """
+    try:
+        st = os.lstat(path)
+        blob = blob_from_path_and_stat(path, st)
+    except EnvironmentError as e:
+        if e.errno == errno.EISDIR:
+            if os.path.exists(os.path.join(path, '.git')):
+                head = read_submodule_head(path)
+                if head is None:
+                    return None
+                return index_entry_from_stat(
+                    st, head, 0, mode=S_IFGITLINK)
+            else:
+                raise
+        else:
+            raise
+    else:
+        if object_store is not None:
+            object_store.add_object(blob)
+        return index_entry_from_stat(st, blob.id, 0)
+
+
+def iter_fresh_entries(paths, root_path, object_store=None):
     """Iterate over current versions of index entries on disk.
 
-    :param index: Index file
+    :param paths: Paths to iterate over
     :param root_path: Root path to access from
+    :param store: Optional store to save new blobs in
     :return: Iterator over path, index_entry
     """
-    for path in set(index):
+    for path in paths:
         p = _tree_to_fs_path(root_path, path)
         try:
-            st = os.lstat(p)
-            blob = blob_from_path_and_stat(p, st)
-        except OSError as e:
-            if e.errno == errno.ENOENT:
-                del index[path]
-            else:
-                raise
-        except IOError as e:
-            if e.errno == errno.EISDIR:
-                del index[path]
+            entry = index_entry_from_path(p, object_store=object_store)
+        except EnvironmentError as e:
+            if e.errno in (errno.ENOENT, errno.EISDIR):
+                entry = None
             else:
                 raise
-        else:
-            yield path, index_entry_from_stat(st, blob.id, 0)
+        yield path, entry
 
 
 def iter_fresh_blobs(index, root_path):
     """Iterate over versions of blobs on disk referenced by index.
 
+    Don't use this function; it removes missing entries from index.
+
     :param index: Index file
     :param root_path: Root path to access from
+    :param include_deleted: Include deleted entries with sha and
+        mode set to None
     :return: Iterator over path, sha, mode
     """
-    for path, entry in iter_fresh_entries(index, root_path):
-        entry = IndexEntry(*entry)
-        yield path, entry.sha, cleanup_mode(entry.mode)
+    import warnings
+    warnings.warn(PendingDeprecationWarning,
+                  "Use iter_fresh_objects instead.")
+    for entry in iter_fresh_objects(
+            index, root_path, include_deleted=True):
+        if entry[1] is None:
+            del index[entry[0]]
+        else:
+            yield entry
+
+
+def iter_fresh_objects(paths, root_path, include_deleted=False,
+                       object_store=None):
+    """Iterate over versions of objecs on disk referenced by index.
+
+    :param index: Index file
+    :param root_path: Root path to access from
+    :param include_deleted: Include deleted entries with sha and
+        mode set to None
+    :param object_store: Optional object store to report new items to
+    :return: Iterator over path, sha, mode
+    """
+    for path, entry in iter_fresh_entries(paths, root_path,
+                                          object_store=object_store):
+        if entry is None:
+            if include_deleted:
+                yield path, None, None
+        else:
+            entry = IndexEntry(*entry)
+            yield path, entry.sha, cleanup_mode(entry.mode)
 
 
 def refresh_index(index, root_path):

+ 111 - 0
dulwich/mailmap.py

@@ -0,0 +1,111 @@
+# mailmap.py -- Mailmap reader
+# Copyright (C) 2018 Jelmer Vernooij <jelmer@samba.org>
+#
+# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
+# General Public License as public by the Free Software Foundation; version 2.0
+# or (at your option) any later version. You can redistribute it and/or
+# modify it under the terms of either of these two licenses.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# You should have received a copy of the licenses; if not, see
+# <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
+# and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
+# License, Version 2.0.
+#
+
+"""Mailmap file reader."""
+
+
+def parse_identity(text):
+    # TODO(jelmer): Integrate this with dulwich.fastexport.split_email and
+    # dulwich.repo.check_user_identity
+    (name, email) = text.rsplit(b"<", 1)
+    name = name.strip()
+    email = email.rstrip(b">").strip()
+    if not name:
+        name = None
+    if not email:
+        email = None
+    return (name, email)
+
+
+def read_mailmap(f):
+    """Read a mailmap.
+
+    :param f: File-like object to read from
+    :return: Iterator over
+        ((canonical_name, canonical_email), (from_name, from_email)) tuples
+    """
+    for line in f:
+        # Remove comments
+        line = line.split(b'#')[0]
+        line = line.strip()
+        if not line:
+            continue
+        (canonical_identity, from_identity) = line.split(b'>', 1)
+        canonical_identity += b">"
+        if from_identity.strip():
+            parsed_from_identity = parse_identity(from_identity)
+        else:
+            parsed_from_identity = None
+        parsed_canonical_identity = parse_identity(canonical_identity)
+        yield parsed_canonical_identity, parsed_from_identity
+
+
+class Mailmap(object):
+    """Class for accessing a mailmap file."""
+
+    def __init__(self, map=None):
+        self._table = {}
+        if map:
+            for (canonical_identity, from_identity) in map:
+                self.add_entry(canonical_identity, from_identity)
+
+    def add_entry(self, canonical_identity, from_identity=None):
+        """Add an entry to the mail mail.
+
+        Any of the fields can be None, but at least one of them needs to be
+        set.
+
+        :param canonical_identity: The canonical identity (tuple)
+        :param from_identity: The from identity (tuple)
+        """
+        if from_identity is None:
+            from_name, from_email = None, None
+        else:
+            (from_name, from_email) = from_identity
+        (canonical_name, canonical_email) = canonical_identity
+        if from_name is None and from_email is None:
+            self._table[canonical_name, None] = canonical_identity
+            self._table[None, canonical_email] = canonical_identity
+        else:
+            self._table[from_name, from_email] = canonical_identity
+
+    def lookup(self, identity):
+        """Lookup an identity in this mailmail."""
+        if not isinstance(identity, tuple):
+            was_tuple = False
+            identity = parse_identity(identity)
+        else:
+            was_tuple = True
+        for query in [identity, (None, identity[1]), (identity[0], None)]:
+            canonical_identity = self._table.get(query)
+            if canonical_identity is not None:
+                identity = (
+                        canonical_identity[0] or identity[0],
+                        canonical_identity[1] or identity[1])
+                break
+        if was_tuple:
+            return identity
+        else:
+            return identity[0] + b" <" + identity[1] + b">"
+
+    @classmethod
+    def from_path(cls, path):
+        with open(path, 'rb') as f:
+            return cls(read_mailmap(f))

+ 19 - 0
dulwich/porcelain.py

@@ -1220,3 +1220,22 @@ def update_head(repo, target, detached=False, new_branch=None):
             r.refs.set_symbolic_ref(to_set, parse_ref(r, target))
         if new_branch is not None:
             r.refs.set_symbolic_ref(b"HEAD", to_set)
+
+
+def check_mailmap(repo, contact):
+    """Check canonical name and email of contact.
+
+    :param repo: Path to the repository
+    :param contact: Contact name and/or email
+    :return: Canonical contact data
+    """
+    with open_repo_closing(repo) as r:
+        from dulwich.mailmap import Mailmap
+        import errno
+        try:
+            mailmap = Mailmap.from_path(os.path.join(r.path, '.mailmap'))
+        except IOError as e:
+            if e.errno != errno.ENOENT:
+                raise
+            mailmap = Mailmap()
+        return mailmap.lookup(contact)

+ 4 - 3
dulwich/protocol.py

@@ -343,9 +343,10 @@ class ReceivableProtocol(Protocol):
     will still block until at least one byte is read.
     """
 
-    def __init__(self, recv, write, report_activity=None, rbufsize=_RBUFSIZE):
-        super(ReceivableProtocol, self).__init__(self.read, write,
-                                                 report_activity)
+    def __init__(self, recv, write, close=None, report_activity=None,
+                 rbufsize=_RBUFSIZE):
+        super(ReceivableProtocol, self).__init__(
+                self.read, write, close=close, report_activity=report_activity)
         self._recv = recv
         self._rbuf = BytesIO()
         self._rbufsize = rbufsize

+ 1 - 0
dulwich/refs.py

@@ -43,6 +43,7 @@ from dulwich.file import (
 
 SYMREF = b'ref: '
 LOCAL_BRANCH_PREFIX = b'refs/heads/'
+LOCAL_TAG_PREFIX = b'refs/tags/'
 BAD_REF_CHARS = set(b'\177 ~^:?*[')
 ANNOTATED_TAG_SUFFIX = b'^{}'
 

+ 119 - 0
dulwich/stash.py

@@ -0,0 +1,119 @@
+# stash.py
+# Copyright (C) 2018 Jelmer Vernooij <jelmer@samba.org>
+#
+# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
+# General Public License as public by the Free Software Foundation; version 2.0
+# or (at your option) any later version. You can redistribute it and/or
+# modify it under the terms of either of these two licenses.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# You should have received a copy of the licenses; if not, see
+# <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
+# and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
+# License, Version 2.0.
+#
+
+"""Stash handling."""
+
+from __future__ import absolute_import
+
+import errno
+import os
+
+from dulwich.file import GitFile
+from dulwich.index import (
+    commit_tree,
+    iter_fresh_objects,
+    )
+from dulwich.reflog import read_reflog
+
+
+DEFAULT_STASH_REF = b"refs/stash"
+
+
+class Stash(object):
+    """A Git stash.
+
+    Note that this doesn't currently update the working tree.
+    """
+
+    def __init__(self, repo, ref=DEFAULT_STASH_REF):
+        self._ref = ref
+        self._repo = repo
+
+    def stashes(self):
+        reflog_path = os.path.join(
+            self._repo.commondir(), 'logs', self._ref)
+        try:
+            with GitFile(reflog_path, 'rb') as f:
+                return reversed(list(read_reflog(f)))
+        except EnvironmentError as e:
+            if e.errno == errno.ENOENT:
+                return []
+            raise
+
+    @classmethod
+    def from_repo(cls, repo):
+        """Create a new stash from a Repo object."""
+        return cls(repo)
+
+    def drop(self, index):
+        """Drop entry with specified index."""
+        raise NotImplementedError(self.drop)
+
+    def pop(self, index):
+        raise NotImplementedError(self.drop)
+
+    def create(self, committer=None, author=None, message=None):
+        """Create a new stash.
+
+        :param committer: Optional committer name to use
+        :param author: Optional author name to use
+        :param message: Optional commit message
+        """
+        # First, create the index commit.
+        commit_kwargs = {}
+        if committer is not None:
+            commit_kwargs['committer'] = committer
+        if author is not None:
+            commit_kwargs['author'] = author
+
+        index = self._repo.open_index()
+        index_tree_id = index.commit(self._repo.object_store)
+        index_commit_id = self._repo.do_commit(
+            ref=None, tree=index_tree_id,
+            message=b"Index stash",
+            merge_heads=[self._repo.head()],
+            **commit_kwargs)
+
+        # Then, the working tree one.
+        stash_tree_id = commit_tree(
+                self._repo.object_store,
+                iter_fresh_objects(
+                    index, self._repo.path,
+                    object_store=self._repo.object_store))
+
+        if message is None:
+            message = b"A stash on " + self._repo.head()
+
+        # TODO(jelmer): Just pass parents into do_commit()?
+        self._repo.refs[self._ref] = self._repo.head()
+
+        cid = self._repo.do_commit(
+            ref=self._ref, tree=stash_tree_id,
+            message=message,
+            merge_heads=[index_commit_id],
+            **commit_kwargs)
+
+        return cid
+
+    def __getitem__(self, index):
+        return self._stashes()[index]
+
+    def __len__(self):
+        return len(self._stashes())

+ 1 - 0
dulwich/tests/__init__.py

@@ -111,6 +111,7 @@ def self_test_suite():
         'ignore',
         'index',
         'lru_cache',
+        'mailmap',
         'objects',
         'objectspec',
         'object_store',

+ 12 - 0
dulwich/tests/compat/test_client.py

@@ -210,6 +210,18 @@ class DulwichClientTestBase(object):
                 dest.refs.set_if_equals(r[0], None, r[1])
             self.assertDestEqualsSrc()
 
+    def test_repeat(self):
+        c = self._client()
+        with repo.Repo(os.path.join(self.gitroot, 'dest')) as dest:
+            result = c.fetch(self._build_path('/server_new.export'), dest)
+            for r in result.refs.items():
+                dest.refs.set_if_equals(r[0], None, r[1])
+            self.assertDestEqualsSrc()
+            result = c.fetch(self._build_path('/server_new.export'), dest)
+            for r in result.refs.items():
+                dest.refs.set_if_equals(r[0], None, r[1])
+            self.assertDestEqualsSrc()
+
     def test_incremental_fetch_pack(self):
         self.test_fetch_pack()
         dest, dummy = self.disable_ff_and_make_dummy_commit()

+ 77 - 26
dulwich/tests/test_client.py

@@ -35,7 +35,6 @@ try:
 except ImportError:
     import urllib.parse as urlparse
 
-import certifi
 import urllib3
 
 import dulwich
@@ -52,11 +51,12 @@ from dulwich.client import (
     SendPackError,
     StrangeHostname,
     SubprocessSSHVendor,
-    PuttySSHVendor,
+    PLinkSSHVendor,
     UpdateRefsError,
     default_urllib3_manager,
     get_transport_and_path,
     get_transport_and_path_from_url,
+    parse_rsync_url,
     )
 from dulwich.config import (
     ConfigDict,
@@ -961,19 +961,28 @@ class TCPGitClientTests(TestCase):
 
 class DefaultUrllib3ManagerTest(TestCase):
 
-    def assert_verify_ssl(self, manager, assertion=True):
-        pool_keywords = tuple(manager.connection_pool_kw.items())
-        assert_method = self.assertIn if assertion else self.assertNotIn
-        assert_method(('cert_reqs', 'CERT_REQUIRED'), pool_keywords)
-        assert_method(('ca_certs', certifi.where()), pool_keywords)
-
     def test_no_config(self):
         manager = default_urllib3_manager(config=None)
-        self.assert_verify_ssl(manager)
+        self.assertEqual(manager.connection_pool_kw['cert_reqs'],
+                         'CERT_REQUIRED')
 
     def test_config_no_proxy(self):
         manager = default_urllib3_manager(config=ConfigDict())
-        self.assert_verify_ssl(manager)
+        self.assertNotIsInstance(manager, urllib3.ProxyManager)
+
+    def test_config_ssl(self):
+        config = ConfigDict()
+        config.set(b'http', b'sslVerify', b'true')
+        manager = default_urllib3_manager(config=config)
+        self.assertEqual(manager.connection_pool_kw['cert_reqs'],
+                         'CERT_REQUIRED')
+
+    def test_config_no_ssl(self):
+        config = ConfigDict()
+        config.set(b'http', b'sslVerify', b'false')
+        manager = default_urllib3_manager(config=config)
+        self.assertEqual(manager.connection_pool_kw['cert_reqs'],
+                         'CERT_NONE')
 
     def test_config_proxy(self):
         config = ConfigDict()
@@ -985,11 +994,10 @@ class DefaultUrllib3ManagerTest(TestCase):
         self.assertEqual(manager.proxy.scheme, 'http')
         self.assertEqual(manager.proxy.host, 'localhost')
         self.assertEqual(manager.proxy.port, 3128)
-        self.assert_verify_ssl(manager)
 
     def test_config_no_verify_ssl(self):
-        manager = default_urllib3_manager(config=None, verify_ssl=False)
-        self.assert_verify_ssl(manager, assertion=False)
+        manager = default_urllib3_manager(config=None, cert_reqs="CERT_NONE")
+        self.assertEqual(manager.connection_pool_kw['cert_reqs'], 'CERT_NONE')
 
 
 class SubprocessSSHVendorTests(TestCase):
@@ -1033,7 +1041,7 @@ class SubprocessSSHVendorTests(TestCase):
         self.assertListEqual(expected, args[0])
 
 
-class PuttySSHVendorTests(TestCase):
+class PLinkSSHVendorTests(TestCase):
 
     def setUp(self):
         # Monkey Patch client subprocess popen
@@ -1044,24 +1052,53 @@ class PuttySSHVendorTests(TestCase):
         dulwich.client.subprocess.Popen = self._orig_popen
 
     def test_run_command_dashes(self):
-        vendor = PuttySSHVendor()
+        vendor = PLinkSSHVendor()
         self.assertRaises(StrangeHostname, vendor.run_command, '--weird-host',
                           'git-clone-url')
 
     def test_run_command_password_and_privkey(self):
-        vendor = PuttySSHVendor()
-        self.assertRaises(NotImplementedError, vendor.run_command,
-                          'host', 'git-clone-url',
-                          password='12345', key_filename='/tmp/id_rsa')
+        vendor = PLinkSSHVendor()
+
+        warnings.simplefilter("always", UserWarning)
+        self.addCleanup(warnings.resetwarnings)
+        warnings_list, restore_warnings = setup_warning_catcher()
+        self.addCleanup(restore_warnings)
+
+        command = vendor.run_command(
+                'host', 'git-clone-url', password='12345',
+                key_filename='/tmp/id_rsa')
+
+        expected_warning = UserWarning(
+            'Invoking PLink with a password exposes the password in the '
+            'process list.')
+
+        for w in warnings_list:
+            if (type(w) == type(expected_warning) and
+                    w.args == expected_warning.args):
+                break
+        else:
+            raise AssertionError(
+                'Expected warning %r not in %r' %
+                (expected_warning, warnings_list))
+
+        args = command.proc.args
+
+        if sys.platform == 'win32':
+            binary = ['plink.exe', '-ssh']
+        else:
+            binary = ['plink', '-ssh']
+        expected = binary + [
+                '-pw', '12345', '-i', '/tmp/id_rsa', 'host', 'git-clone-url']
+        self.assertListEqual(expected, args[0])
 
     def test_run_command_password(self):
         if sys.platform == 'win32':
-            binary = ['putty.exe', '-ssh']
+            binary = ['plink.exe', '-ssh']
         else:
-            binary = ['putty', '-ssh']
+            binary = ['plink', '-ssh']
         expected = binary + ['-pw', '12345', 'host', 'git-clone-url']
 
-        vendor = PuttySSHVendor()
+        vendor = PLinkSSHVendor()
 
         warnings.simplefilter("always", UserWarning)
         self.addCleanup(warnings.resetwarnings)
@@ -1071,7 +1108,7 @@ class PuttySSHVendorTests(TestCase):
         command = vendor.run_command('host', 'git-clone-url', password='12345')
 
         expected_warning = UserWarning(
-            'Invoking Putty with a password exposes the password in the '
+            'Invoking PLink with a password exposes the password in the '
             'process list.')
 
         for w in warnings_list:
@@ -1089,14 +1126,14 @@ class PuttySSHVendorTests(TestCase):
 
     def test_run_command_with_port_username_and_privkey(self):
         if sys.platform == 'win32':
-            binary = ['putty.exe', '-ssh']
+            binary = ['plink.exe', '-ssh']
         else:
-            binary = ['putty', '-ssh']
+            binary = ['plink', '-ssh']
         expected = binary + [
             '-P', '2200', '-i', '/tmp/id_rsa',
             'user@host', 'git-clone-url']
 
-        vendor = PuttySSHVendor()
+        vendor = PLinkSSHVendor()
         command = vendor.run_command(
             'host', 'git-clone-url',
             username='user', port='2200',
@@ -1105,3 +1142,17 @@ class PuttySSHVendorTests(TestCase):
         args = command.proc.args
 
         self.assertListEqual(expected, args[0])
+
+
+class RsyncUrlTests(TestCase):
+
+    def test_simple(self):
+        self.assertEqual(
+                parse_rsync_url('foo:bar/path'),
+                (None, 'foo', 'bar/path'))
+        self.assertEqual(
+                parse_rsync_url('user@foo:bar/path'),
+                ('user', 'foo', 'bar/path'))
+
+    def test_path(self):
+        self.assertRaises(ValueError, parse_rsync_url, '/path')

+ 90 - 0
dulwich/tests/test_mailmap.py

@@ -0,0 +1,90 @@
+# test_mailmap.py -- Tests for dulwich.mailmap
+# Copyright (C) 2018 Jelmer Vernooij <jelmer@jelmer.uk>
+#
+# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
+# General Public License as public by the Free Software Foundation; version 2.0
+# or (at your option) any later version. You can redistribute it and/or
+# modify it under the terms of either of these two licenses.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# You should have received a copy of the licenses; if not, see
+# <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
+# and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
+# License, Version 2.0.
+#
+
+"""Tests for dulwich.mailmap."""
+
+from io import BytesIO
+
+from unittest import TestCase
+
+from dulwich.mailmap import Mailmap, read_mailmap
+
+
+class ReadMailmapTests(TestCase):
+
+    def test_read(self):
+        b = BytesIO(b"""\
+Jane Doe         <jane@desktop.(none)>
+Joe R. Developer <joe@example.com>
+# A comment
+<cto@company.xx>                       <cto@coompany.xx> # Comment
+Some Dude <some@dude.xx>         nick1 <bugs@company.xx>
+Other Author <other@author.xx>   nick2 <bugs@company.xx>
+Other Author <other@author.xx>         <nick2@company.xx>
+Santa Claus <santa.claus@northpole.xx> <me@company.xx>
+""")
+        self.assertEqual([
+            ((b'Jane Doe', b'jane@desktop.(none)'), None),
+            ((b'Joe R. Developer', b'joe@example.com'), None),
+            ((None, b'cto@company.xx'), (None, b'cto@coompany.xx')),
+            ((b'Some Dude', b'some@dude.xx'), (b'nick1', b'bugs@company.xx')),
+            ((b'Other Author', b'other@author.xx'),
+                (b'nick2', b'bugs@company.xx')),
+            ((b'Other Author', b'other@author.xx'),
+                (None, b'nick2@company.xx')),
+            ((b'Santa Claus', b'santa.claus@northpole.xx'),
+                (None, b'me@company.xx'))],
+            list(read_mailmap(b)))
+
+
+class MailmapTests(TestCase):
+
+    def test_lookup(self):
+        m = Mailmap()
+        m.add_entry((b'Jane Doe', b'jane@desktop.(none)'), (None, None))
+        m.add_entry((b'Joe R. Developer', b'joe@example.com'), None)
+        m.add_entry((None, b'cto@company.xx'), (None, b'cto@coompany.xx'))
+        m.add_entry(
+                (b'Some Dude', b'some@dude.xx'),
+                (b'nick1', b'bugs@company.xx'))
+        m.add_entry(
+                (b'Other Author', b'other@author.xx'),
+                (b'nick2', b'bugs@company.xx'))
+        m.add_entry(
+                (b'Other Author', b'other@author.xx'),
+                (None, b'nick2@company.xx'))
+        m.add_entry(
+                (b'Santa Claus', b'santa.claus@northpole.xx'),
+                (None, b'me@company.xx'))
+        self.assertEqual(
+            b'Jane Doe <jane@desktop.(none)>',
+            m.lookup(b'Jane Doe <jane@desktop.(none)>'))
+        self.assertEqual(
+            b'Jane Doe <jane@desktop.(none)>',
+            m.lookup(b'Jane Doe <jane@example.com>'))
+        self.assertEqual(
+            b'Jane Doe <jane@desktop.(none)>',
+            m.lookup(b'Jane D. <jane@desktop.(none)>'))
+        self.assertEqual(
+            b'Some Dude <some@dude.xx>',
+            m.lookup(b'nick1 <bugs@company.xx>'))
+        self.assertEqual(
+            b'CTO <cto@company.xx>',
+            m.lookup(b'CTO <cto@coompany.xx>'))

+ 19 - 0
dulwich/tests/test_porcelain.py

@@ -1273,3 +1273,22 @@ class UpdateHeadTests(PorcelainTestCase):
         self.assertEqual(c1.id, self.repo.head())
         self.assertEqual(b'ref: refs/heads/bar',
                          self.repo.refs.read_ref(b'HEAD'))
+
+
+class MailmapTests(PorcelainTestCase):
+
+    def test_no_mailmap(self):
+        self.assertEqual(
+            b'Jelmer Vernooij <jelmer@samba.org>',
+            porcelain.check_mailmap(
+                self.repo, b'Jelmer Vernooij <jelmer@samba.org>'))
+
+    def test_mailmap_lookup(self):
+        with open(os.path.join(self.repo.path, '.mailmap'), 'wb') as f:
+            f.write(b"""\
+Jelmer Vernooij <jelmer@debian.org>
+""")
+        self.assertEqual(
+            b'Jelmer Vernooij <jelmer@debian.org>',
+            porcelain.check_mailmap(
+                self.repo, b'Jelmer Vernooij <jelmer@samba.org>'))

+ 1 - 0
requirements.txt

@@ -0,0 +1 @@
+urllib3[secure]==1.22

+ 5 - 0
setup.cfg

@@ -1 +1,6 @@
 [build_ext]
+
+[egg_info]
+tag_build = 
+tag_date = 0
+

+ 6 - 3
setup.py

@@ -14,7 +14,7 @@ from distutils.core import Distribution
 import os
 import sys
 
-dulwich_version_string = '0.19.0'
+dulwich_version_string = '0.19.1'
 
 include_dirs = []
 # Windows MSVC support
@@ -74,8 +74,11 @@ ext_modules = [
 setup_kwargs = {}
 
 if has_setuptools:
-    setup_kwargs['extras_require'] = {'fastimport': ['fastimport']}
-    setup_kwargs['install_requires'] = ['urllib3[secure]>=1.21']
+    setup_kwargs['extras_require'] = {
+        'fastimport': ['fastimport'],
+        'https': ['urllib3[secure]>=1.21'],
+        }
+    setup_kwargs['install_requires'] = ['urllib3>=1.21', 'certifi']
     setup_kwargs['include_package_data'] = True
     setup_kwargs['test_suite'] = 'dulwich.tests.test_suite'
     setup_kwargs['tests_require'] = tests_require