diff --git a/NEWS b/NEWS index 187e5fdd..b37f5545 100644 --- a/NEWS +++ b/NEWS @@ -1,1907 +1,1910 @@ 0.19.10 UNRELEASED IMPROVEMENTS * Add `dulwich.porcelain.write_tree`. (Jelmer Vernooij) * Support reading ``MERGE_HEADS`` in ``Repo.do_commit``. (Jelmer Vernooij) * Import from ``collections.abc`` rather than ``collections`` where applicable. Required for 3.8 compatibility. (Jelmer Vernooij) * Support plain strings as refspec arguments to ``dulwich.porcelain.push``. (Jelmer Vernooij) + * Add support for creating signed tags. + (Jelmer Vernooij, #542) + BUG FIXES * Handle invalid ref that pretends to be a sub-folder under a valid ref. (KS Chan) 0.19.9 2018-11-17 BUG FIXES * Avoid fetching ghosts in ``Repo.fetch``. (Jelmer Vernooij) * Preserve port and username in parsed HTTP URLs. (Jelmer Vernooij) * Add basic server side implementation of ``git-upload-archive``. (Jelmer Vernooij) 0.19.8 2018-11-06 * Fix encoding when reading README file in setup.py. (egor , #668) 0.19.7 2018-11-05 CHANGES * Drop support for Python 3 < 3.4. This is because pkg_resources (which get used by setuptools and mock) no longer supports 3.3 and earlier. (Jelmer Vernooij) IMPROVEMENTS * Support ``depth`` argument to ``GitClient.fetch_pack`` and support fetching and updating shallow metadata. (Jelmer Vernooij, #240) BUG FIXES * Don't write to stdout and stderr when they are not available (such as is the case for pythonw). (Sylvia van Os, #652) * Fix compatibility with newer versions of git, which expect CONTENT_LENGTH to be set to 0 for empty body requests. (Jelmer Vernooij, #657) * Raise an exception client-side when a caller tries to request SHAs that are not directly referenced the servers' refs. (Jelmer Vernooij) * Raise more informative errors when unable to connect to repository over SSH or subprocess. (Jelmer Vernooij) * Handle commit identity fields with multiple ">" characters. (Nicolas Dandrimont) IMPROVEMENTS * ``dulwich.porcelain.get_object_by_path`` method for easily accessing a path in another tree. (Jelmer Vernooij) * Support the ``i18n.commitEncoding`` setting in config. (Jelmer Vernooij) 0.19.6 2018-08-11 BUG FIXES * Fix support for custom transport arguments in ``dulwich.porcelain.clone``. (Semyon Slepov) * Fix compatibility with Python 3.8 (Jelmer Vernooij, Daniel M. Capella) * Fix some corner cases in ``path_to_tree_path``. (Romain Keramitas) * Support paths as bytestrings in various places in ``dulwich.index`` (Jelmer Vernooij) * Avoid setup.cfg for now, since it seems to break pypi metadata. (Jelmer Vernooij, #658) 0.19.5 2018-07-08 IMPROVEMENTS * Add ``porcelain.describe``. (Sylvia van Os) BUG FIXES * Fix regression in ``dulwich.porcelain.clone`` that prevented cloning of remote repositories. (Jelmer Vernooij, #639) * Don't leave around empty parent directories for removed refs. (Damien Tournoud, #640) 0.19.4 2018-06-24 IMPROVEMENTS * Add ``porcelain.ls_files``. (Jelmer Vernooij) * Add ``Index.items``. (Jelmer Vernooij) BUG FIXES * Avoid unicode characters (e.g. the digraph ij in my surname) in setup.cfg, since setuptools doesn't deal well with them. See https://github.com/pypa/setuptools/issues/1062. (Jelmer Vernooij, #637) 0.19.3 2018-06-17 IMPROVEMENTS * Add really basic `dulwich.porcelain.fsck` implementation. (Jelmer Vernooij) * When the `DULWICH_PDB` environment variable is set, make SIGQUIT open pdb in the 'dulwich' command. * Add `checkout` argument to `Repo.clone`. (Jelmer Vernooij, #503) * Add `Repo.get_shallow` method. (Jelmer Vernooij) * Add basic `dulwich.stash` module. (Jelmer Vernooij) * Support a `prefix` argument to `dulwich.archive.tar_stream`. (Jelmer Vernooij) BUG FIXES * Fix handling of encoding for tags. (Jelmer Vernooij, #608) * Fix tutorial tests on Python 3. (Jelmer Vernooij, #573) * Fix remote refs created by `porcelain.fetch`. (Daniel Andersson, #623) * More robust pack creation on Windows. (Daniel Andersson) * Fix recursive option for `porcelain.ls_tree`. (Romain Keramitas) TESTS * Some improvements to paramiko tests. (Filipp Frizzy) 0.19.2 2018-04-07 BUG FIXES * Fix deprecated Index.iterblobs method. (Jelmer Vernooij) 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 * Make `dulwich.archive` set the gzip header file modification time so that archives created from the same Git tree are always identical. (#577, Jonas Haag) * Allow comment characters (#, ;) within configuration file strings (Daniel Andersson, #579) * Raise exception when passing in invalid author/committer values to Repo.do_commit(). (Jelmer Vernooij, #602) IMPROVEMENTS * Add a fastimport ``extra``. (Jelmer Vernooij) * Start writing reflog entries. (Jelmer Vernooij) * Add ability to use password and keyfile ssh options with SSHVendor. (Filipp Kucheryavy) * Add ``change_type_same`` flag to ``tree_changes``. (Jelmer Vernooij) API CHANGES * ``GitClient.send_pack`` now accepts a ``generate_pack_data`` rather than a ``generate_pack_contents`` function for performance reasons. (Jelmer Vernooij) * Dulwich now uses urllib3 internally for HTTP requests. The `opener` argument to `dulwich.client.HttpGitClient` that took a `urllib2` opener instance has been replaced by a `pool_manager` argument that takes a `urllib3` pool manager instance. (Daniel Andersson) 0.18.6 2017-11-11 BUG FIXES * Fix handling of empty repositories in ``porcelain.clone``. (#570, Jelmer Vernooij) * Raise an error when attempting to add paths that are not under the repository. (Jelmer Vernooij) * Fix error message for missing trailing ]. (Daniel Andersson) * Raise EmptyFileException when corruption (in the form of an empty file) is detected. (Antoine R. Dumont, #582) IMPROVEMENTS * Enforce date field parsing consistency. This also add checks on those date fields for potential overflow. (Antoine R. Dumont, #567) 0.18.5 2017-10-29 BUG FIXES * Fix cwd for hooks. (Fabian Grünbichler) * Fix setting of origin in config when non-standard origin is passed into ``Repo.clone``. (Kenneth Lareau, #565) * Prevent setting SSH arguments from SSH URLs when using SSH through a subprocess. Note that Dulwich doesn't support cloning submodules. (CVE-2017-16228) (Jelmer Vernooij) IMPROVEMENTS * Silently ignored directories in ``Repo.stage``. (Jelmer Vernooij, #564) API CHANGES * GitFile now raises ``FileLocked`` when encountering a lock rather than OSError(EEXIST). (Jelmer Vernooij) 0.18.4 2017-10-01 BUG FIXES * Make default User-Agent start with "git/" because GitHub won't response to HTTP smart server requests otherwise (and reply with a 404). (Jelmer vernooij, #562) 0.18.3 2017-09-03 BUG FIXES * Read config during porcelain operations that involve remotes. (Jelmer Vernooij, #545) * Fix headers of empty chunks in unified diffs. (Taras Postument, #543) * Properly follow redirects over HTTP. (Jelmer Vernooij, #117) IMPROVEMENTS * Add ``dulwich.porcelain.update_head``. (Jelmer Vernooij, #439) * ``GitClient.fetch_pack`` now returns symrefs. (Jelmer Vernooij, #485) * The server now supports providing symrefs. (Jelmer Vernooij, #485) * Add ``dulwich.object_store.commit_tree_changes`` to incrementally commit changes to a tree structure. (Jelmer Vernooij) * Add basic ``PackBasedObjectStore.repack`` method. (Jelmer Vernooij, Earl Chew, #296, #549, #552) 0.18.2 2017-08-01 TEST FIXES * Use constant timestamp so tests pass in all timezones, not just BST. (Jelmer Vernooij) 0.18.1 2017-07-31 BUG FIXES * Fix syntax error in dulwich.contrib.test_swift_smoke. (Jelmer Vernooij) 0.18.0 2017-07-31 BUG FIXES * Fix remaining tests on Windows. (Jelmer Vernooij, #493) * Fix build of C extensions with Python 3 on Windows. (Jelmer Vernooij) * Pass 'mkdir' argument onto Repo.init_bare in Repo.clone. (Jelmer Vernooij, #504) * In ``dulwich.porcelain.add``, if no files are specified, add from current working directory rather than repository root. (Jelmer Vernooij, #521) * Properly deal with submodules in 'porcelain.status'. (Jelmer Vernooij, #517) * ``dulwich.porcelain.remove`` now actually removes files from disk, not just from the index. (Jelmer Vernooij, #488) * Fix handling of "reset" command with markers and without "from". (Antoine Pietri) * Fix handling of "merge" command with markers. (Antoine Pietri) * Support treeish argument to porcelain.reset(), rather than requiring a ref/commit id. (Jelmer Vernooij) * Handle race condition when mtime doesn't change between writes/reads. (Jelmer Vernooij, #541) * Fix ``dulwich.porcelain.show`` on commits with Python 3. (Jelmer Vernooij, #532) IMPROVEMENTS * Add basic support for reading ignore files in ``dulwich.ignore``. ``dulwich.porcelain.add`` and ``dulwich.porcelain.status`` now honor ignores. (Jelmer Vernooij, Segev Finer, #524, #526) * New ``dulwich.porcelain.check_ignore`` command. (Jelmer Vernooij) * ``dulwich.porcelain.status`` now supports a ``ignored`` argument. (Jelmer Vernooij) DOCUMENTATION * Clarified docstrings for Client.{send_pack,fetch_pack} implementations. (Jelmer Vernooij, #523) 0.17.3 2017-03-20 PLATFORM SUPPORT * List Python 3.3 as supported. (Jelmer Vernooij, #513) BUG FIXES * Fix compatibility with pypy 3. (Jelmer Vernooij) 0.17.2 2017-03-19 BUG FIXES * Add workaround for https://bitbucket.org/pypy/pypy/issues/2499/cpyext-pystring_asstring-doesnt-work, fixing Dulwich when used with C extensions on pypy < 5.6. (Victor Stinner) * Properly quote config values with a '#' character in them. (Jelmer Vernooij, #511) 0.17.1 2017-03-01 IMPROVEMENTS * Add basic 'dulwich pull' command. (Jelmer Vernooij) BUG FIXES * Cope with existing submodules during pull. (Jelmer Vernooij, #505) 0.17.0 2017-03-01 TEST FIXES * Skip test that requires sync to synchronize filesystems if os.sync is not available. (Koen Martens) IMPROVEMENTS * Implement MemoryRepo.{set_description,get_description}. (Jelmer Vernooij) * Raise exception in Repo.stage() when absolute paths are passed in. Allow passing in relative paths to porcelain.add().(Jelmer Vernooij) BUG FIXES * Handle multi-line quoted values in config files. (Jelmer Vernooij, #495) * Allow porcelain.clone of repository without HEAD. (Jelmer Vernooij, #501) * Support passing tag ids to Walker()'s include argument. (Jelmer Vernooij) * Don't strip trailing newlines from extra headers. (Nicolas Dandrimont) * Set bufsize=0 for subprocess interaction with SSH client. Fixes hangs on Python 3. (René Stern, #434) * Don't drop first slash for SSH paths, except for those starting with "~". (Jelmer Vernooij, René Stern, #463) * Properly log off after retrieving just refs. (Jelmer Vernooij) 0.16.3 2016-01-14 TEST FIXES * Remove racy check that relies on clock time changing between writes. (Jelmer Vernooij) IMPROVEMENTS * Add porcelain.remote_add. (Jelmer Vernooij) 0.16.2 2016-01-14 IMPROVEMENTS * Fixed failing test-cases on windows. (Koen Martens) API CHANGES * Repo is now a context manager, so that it can be easily closed using a ``with`` statement. (Søren Løvborg) TEST FIXES * Only run worktree list compat tests against git 2.7.0, when 'git worktree list' was introduced. (Jelmer Vernooij) BUG FIXES * Ignore filemode when building index when core.filemode is false. (Koen Martens) * Initialize core.filemode configuration setting by probing the filesystem for trustable permissions. (Koen Martens) * Fix ``porcelain.reset`` to respect the comittish argument. (Koen Martens) * Fix dulwich.porcelain.ls_remote() on Python 3. (#471, Jelmer Vernooij) * Allow both unicode and byte strings for host paths in dulwich.client. (#435, Jelmer Vernooij) * Add remote from porcelain.clone. (#466, Jelmer Vernooij) * Fix unquoting of credentials before passing to urllib2. (#475, Volodymyr Holovko) * Cope with submodules in `build_index_from_tree`. (#477, Jelmer Vernooij) * Handle deleted files in `get_unstaged_changes`. (#483, Doug Hellmann) * Don't overwrite files when they haven't changed in `build_file_from_blob`. (#479, Benoît HERVIER) * Check for existence of index file before opening pack. Fixes a race when new packs are being added. (#482, wme) 0.16.1 2016-12-25 BUG FIXES * Fix python3 compatibility for dulwich.contrib.release_robot. (Jelmer Vernooij) 0.16.0 2016-12-24 IMPROVEMENTS * Add support for worktrees. See `git-worktree(1)` and `gitrepository-layout(5)`. (Laurent Rineau) * Add support for `commondir` file in Git control directories. (Laurent Rineau) * Add support for passwords in HTTP URLs. (Jon Bain, Mika Mäenpää) * Add `release_robot` script to contrib, allowing easy finding of current version based on Git tags. (Mark Mikofski) * Add ``Blob.splitlines`` method. (Jelmer Vernooij) BUG FIXES * Fix handling of ``Commit.tree`` being set to an actual tree object rather than a tree id. (Jelmer Vernooij) * Return remote refs from LocalGitClient.fetch_pack(), consistent with the documentation for that method. (#461, Jelmer Vernooij) * Fix handling of unknown URL schemes in get_transport_and_path. (#465, Jelmer Vernooij) 0.15.0 2016-10-09 BUG FIXES * Allow missing trailing LF when reading service name from HTTP servers. (Jelmer Vernooij, Andrew Shadura, #442) * Fix dulwich.porcelain.pull() on Python3. (Jelmer Vernooij, #451) * Properly pull in tags during dulwich.porcelain.clone. (Jelmer Vernooij, #408) CHANGES * Changed license from "GNU General Public License, version 2.0 or later" to "Apache License, version 2.0 or later or GNU General Public License, version 2.0 or later". (#153) IMPROVEMENTS * Add ``dulwich.porcelain.ls_tree`` implementation. (Jelmer Vernooij) 0.14.1 2016-07-05 BUG FIXES * Fix regression removing untouched refs when pushing over SSH. (Jelmer Vernooij #441) * Skip Python3 tests for SWIFT contrib module, as it has not yet been ported. 0.14.0 2016-07-03 BUG FIXES * Fix ShaFile.id after modification of a copied ShaFile. (Félix Mattrat, Jelmer Vernooij) * Support removing refs from porcelain.push. (Jelmer Vernooij, #437) * Stop magic protocol ref `capabilities^{}` from leaking out to clients. (Jelmer Vernooij, #254) IMPROVEMENTS * Add `dulwich.config.parse_submodules` function. * Add `RefsContainer.follow` method. (#438) 0.13.0 2016-04-24 IMPROVEMENTS * Support `ssh://` URLs in get_transport_and_path_from_url(). (Jelmer Vernooij, #402) * Support missing empty line after headers in Git commits and tags. (Nicolas Dandrimont, #413) * Fix `dulwich.porcelain.status` when used in empty trees. (Jelmer Vernooij, #415) * Return copies of objects in MemoryObjectStore rather than references, making the behaviour more consistent with that of DiskObjectStore. (Félix Mattrat, Jelmer Vernooij) * Fix ``dulwich.web`` on Python3. (#295, Jonas Haag) CHANGES * Drop support for Python 2.6. * Fix python3 client web support. (Jelmer Vernooij) BUG FIXES * Fix hang on Gzip decompression. (Jonas Haag) * Don't rely on working tell() and seek() methods on wsgi.input. (Jonas Haag) * Support fastexport/fastimport functionality on python3 with newer versions of fastimport (>= 0.9.5). (Jelmer Vernooij, Félix Mattrat) 0.12.0 2015-12-13 IMPROVEMENTS * Add a `dulwich.archive` module that can create tarballs. Based on code from Jonas Haag in klaus. * Add a `dulwich.reflog` module for reading and writing reflogs. (Jelmer Vernooij) * Fix handling of ambiguous refs in `parse_ref` to make it match the behaviour described in https://git-scm.com/docs/gitrevisions. (Chris Bunney) * Support Python3 in C modules. (Lele Gaifax) BUG FIXES * Simplify handling of SSH command invocation. Fixes quoting of paths. Thanks, Thomas Liebetraut. (#384) * Fix inconsistent handling of trailing slashes for DictRefsContainer. (#383) * Add hack to support thin packs duing fetch(), albeit while requiring the entire pack file to be loaded into memory. (jsbain) CHANGES * This will be the last release to support Python 2.6. 0.11.2 2015-09-18 IMPROVEMENTS * Add support for agent= capability. (Jelmer Vernooij, #298) * Add support for quiet capability. (Jelmer Vernooij) CHANGES * The ParamikoSSHVendor class has been moved to * dulwich.contrib.paramiko_vendor, as it's currently untested. (Jelmer Vernooij, #364) 0.11.1 2015-09-13 Fix-up release to exclude broken blame.py file. 0.11.0 2015-09-13 IMPROVEMENTS * Extended Python3 support to most of the codebase. (Gary van der Merwe, Jelmer Vernooij) * The `Repo` object has a new `close` method that can be called to close any open resources. (Gary van der Merwe) * Support 'git.bat' in SubprocessGitClient on Windows. (Stefan Zimmermann) * Advertise 'ofs-delta' capability in receive-pack server side capabilities. (Jelmer Vernooij) * Switched `default_local_git_client_cls` to `LocalGitClient`. (Gary van der Merwe) * Add `porcelain.ls_remote` and `GitClient.get_refs`. (Michael Edgar) * Add `Repo.discover` method. (B. M. Corser) * Add `dulwich.objectspec.parse_refspec`. (Jelmer Vernooij) * Add `porcelain.pack_objects` and `porcelain.repack`. (Jelmer Vernooij) BUG FIXES * Fix handling of 'done' in graph walker and implement the 'no-done' capability. (Tommy Yu, #88) * Avoid recursion limit issues resolving deltas. (William Grant, #81) * Allow arguments in local client binary path overrides. (Jelmer Vernooij) * Fix handling of commands with arguments in paramiko SSH client. (Andreas Klöckner, Jelmer Vernooij, #363) * Fix parsing of quoted strings in configs. (Jelmer Vernooij, #305) 0.10.1 2015-03-25 BUG FIXES * Return `ApplyDeltaError` when encountering delta errors in both C extensions and native delta application code. (Jelmer Vernooij, #259) 0.10.0 2015-03-22 BUG FIXES * In dulwich.index.build_index_from_tree, by default refuse to create entries that start with .git/. * Fix running of testsuite when installed. (Jelmer Vernooij, #223) * Use a block cache in _find_content_rename_candidates(), improving performance. (Mike Williams) * Add support for ``core.protectNTFS`` setting. (Jelmer Vernooij) * Fix TypeError when fetching empty updates. (Hwee Miin Koh) * Resolve delta refs when pulling into a MemoryRepo. (Max Shawabkeh, #256) * Fix handling of tags of non-commits in missing object finder. (Augie Fackler, #211) * Explicitly disable mmap on plan9 where it doesn't work. (Jeff Sickel) IMPROVEMENTS * New public method `Repo.reset_index`. (Jelmer Vernooij) * Prevent duplicate parsing of loose files in objects directory when reading. Thanks to David Keijser for the report. (Jelmer Vernooij, #231) 0.9.9 2015-03-20 SECURITY BUG FIXES * Fix buffer overflow in C implementation of pack apply_delta(). (CVE-2015-0838) Thanks to Ivan Fratric of the Google Security Team for reporting this issue. (Jelmer Vernooij) 0.9.8 2014-11-30 BUG FIXES * Various fixes to improve test suite running on Windows. (Gary van der Merwe) * Limit delta copy length to 64K in v2 pack files. (Robert Brown) * Strip newline from final ACKed SHA while fetching packs. (Michael Edgar) * Remove assignment to PyList_SIZE() that was causing segfaults on pypy. (Jelmer Vernooij, #196) IMPROVEMENTS * Add porcelain 'receive-pack' and 'upload-pack'. (Jelmer Vernooij) * Handle SIGINT signals in bin/dulwich. (Jelmer Vernooij) * Add 'status' support to bin/dulwich. (Jelmer Vernooij) * Add 'branch_create', 'branch_list', 'branch_delete' porcelain. (Jelmer Vernooij) * Add 'fetch' porcelain. (Jelmer Vernooij) * Add 'tag_delete' porcelain. (Jelmer Vernooij) * Add support for serializing/deserializing 'gpgsig' attributes in Commit. (Jelmer Vernooij) CHANGES * dul-web is now available as 'dulwich web-daemon'. (Jelmer Vernooij) * dulwich.porcelain.tag has been renamed to tag_create. dulwich.porcelain.list_tags has been renamed to tag_list. (Jelmer Vernooij) API CHANGES * Restore support for Python 2.6. (Jelmer Vernooij, Gary van der Merwe) 0.9.7 2014-06-08 BUG FIXES * Fix tests dependent on hash ordering. (Michael Edgar) * Support staging symbolic links in Repo.stage. (Robert Brown) * Ensure that all files object are closed when running the test suite. (Gary van der Merwe) * When writing OFS_DELTA pack entries, write correct offset. (Augie Fackler) * Fix handler of larger copy operations in packs. (Augie Fackler) * Various fixes to improve test suite running on Windows. (Gary van der Merwe) * Fix logic for extra adds of identical files in rename detector. (Robert Brown) IMPROVEMENTS * Add porcelain 'status'. (Ryan Faulkner) * Add porcelain 'daemon'. (Jelmer Vernooij) * Add `dulwich.greenthreads` module which provides support for concurrency of some object store operations. (Fabien Boucher) * Various changes to improve compatibility with Python 3. (Gary van der Merwe, Hannu Valtonen, michael-k) * Add OpenStack Swift backed repository implementation in dulwich.contrib. See README.swift for details. (Fabien Boucher) API CHANGES * An optional close function can be passed to the Protocol class. This will be called by its close method. (Gary van der Merwe) * All classes with close methods are now context managers, so that they can be easily closed using a `with` statement. (Gary van der Merwe) * Remove deprecated `num_objects` argument to `write_pack` methods. (Jelmer Vernooij) OTHER CHANGES * The 'dul-daemon' script has been removed. The same functionality is now available as 'dulwich daemon'. (Jelmer Vernooij) 0.9.6 2014-04-23 IMPROVEMENTS * Add support for recursive add in 'git add'. (Ryan Faulkner, Jelmer Vernooij) * Add porcelain 'list_tags'. (Ryan Faulkner) * Add porcelain 'push'. (Ryan Faulkner) * Add porcelain 'pull'. (Ryan Faulkner) * Support 'http.proxy' in HttpGitClient. (Jelmer Vernooij, #1096030) * Support 'http.useragent' in HttpGitClient. (Jelmer Vernooij) * In server, wait for clients to send empty list of wants when talking to empty repository. (Damien Tournoud) * Various changes to improve compatibility with Python 3. (Gary van der Merwe) BUG FIXES * Support unseekable 'wsgi.input' streams. (Jonas Haag) * Raise TypeError when passing unicode() object to Repo.__getitem__. (Jonas Haag) * Fix handling of `reset` command in dulwich.fastexport. (Jelmer Vernooij, #1249029) * In client, don't wait for server to close connection first. Fixes hang when used against GitHub server implementation. (Siddharth Agarwal) * DeltaChainIterator: fix a corner case where an object is inflated as an object already in the repository. (Damien Tournoud, #135) * Stop leaking file handles during pack reload. (Damien Tournoud) * Avoid reopening packs during pack cache reload. (Jelmer Vernooij) API CHANGES * Drop support for Python 2.6. (Jelmer Vernooij) 0.9.5 2014-02-23 IMPROVEMENTS * Add porcelain 'tag'. (Ryan Faulkner) * New module `dulwich.objectspec` for parsing strings referencing objects and commit ranges. (Jelmer Vernooij) * Add shallow branch support. (milki) * Allow passing urllib2 `opener` into HttpGitClient. (Dov Feldstern, #909037) CHANGES * Drop support for Python 2.4 and 2.5. (Jelmer Vernooij) API CHANGES * Remove long deprecated ``Repo.commit``, ``Repo.get_blob``, ``Repo.tree`` and ``Repo.tag``. (Jelmer Vernooij) * Remove long deprecated ``Repo.revision_history`` and ``Repo.ref``. (Jelmer Vernooij) * Remove long deprecated ``Tree.entries``. (Jelmer Vernooij) BUG FIXES * Raise KeyError rather than TypeError when passing in unicode object of length 20 or 40 to Repo.__getitem__. (Jelmer Vernooij) * Use 'rm' rather than 'unlink' in tests, since the latter does not exist on OpenBSD and other platforms. (Dmitrij D. Czarkoff) 0.9.4 2013-11-30 IMPROVEMENTS * Add ssh_kwargs attribute to ParamikoSSHVendor. (milki) * Add Repo.set_description(). (Víðir Valberg Guðmundsson) * Add a basic `dulwich.porcelain` module. (Jelmer Vernooij, Marcin Kuzminski) * Various performance improvements for object access. (Jelmer Vernooij) * New function `get_transport_and_path_from_url`, similar to `get_transport_and_path` but only supports URLs. (Jelmer Vernooij) * Add support for file:// URLs in `get_transport_and_path_from_url`. (Jelmer Vernooij) * Add LocalGitClient implementation. (Jelmer Vernooij) BUG FIXES * Support filesystems with 64bit inode and device numbers. (André Roth) CHANGES * Ref handling has been moved to dulwich.refs. (Jelmer Vernooij) API CHANGES * Remove long deprecated RefsContainer.set_ref(). (Jelmer Vernooij) * Repo.ref() is now deprecated in favour of Repo.refs[]. (Jelmer Vernooij) FEATURES * Add support for graftpoints. (milki) 0.9.3 2013-09-27 BUG FIXES * Fix path for stdint.h in MANIFEST.in. (Jelmer Vernooij) 0.9.2 2013-09-26 BUG FIXES * Include stdint.h in MANIFEST.in (Mark Mikofski) 0.9.1 2013-09-22 BUG FIXES * Support lookups of 40-character refs in BaseRepo.__getitem__. (Chow Loong Jin, Jelmer Vernooij) * Fix fetching packs with side-band-64k capability disabled. (David Keijser, Jelmer Vernooij) * Several fixes in send-pack protocol behaviour - handling of empty pack files and deletes. (milki, #1063087) * Fix capability negotiation when fetching packs over HTTP. (#1072461, William Grant) * Enforce determine_wants returning an empty list rather than None. (Fabien Boucher, Jelmer Vernooij) * In the server, support pushes just removing refs. (Fabien Boucher, Jelmer Vernooij) IMPROVEMENTS * Support passing a single revision to BaseRepo.get_walker() rather than a list of revisions. (Alberto Ruiz) * Add `Repo.get_description` method. (Jelmer Vernooij) * Support thin packs in Pack.iterobjects() and Pack.get_raw(). (William Grant) * Add `MemoryObjectStore.add_pack` and `MemoryObjectStore.add_thin_pack` methods. (David Bennett) * Add paramiko-based SSH vendor. (Aaron O'Mullan) * Support running 'dulwich.server' and 'dulwich.web' using 'python -m'. (Jelmer Vernooij) * Add ObjectStore.close(). (Jelmer Vernooij) * Raise appropriate NotImplementedError when encountering dumb HTTP servers. (Jelmer Vernooij) API CHANGES * SSHVendor.connect_ssh has been renamed to SSHVendor.run_command. (Jelmer Vernooij) * ObjectStore.add_pack() now returns a 3-tuple. The last element will be an abort() method that can be used to cancel the pack operation. (Jelmer Vernooij) 0.9.0 2013-05-31 BUG FIXES * Push efficiency - report missing objects only. (#562676, Artem Tikhomirov) * Use indentation consistent with C Git in config files. (#1031356, Curt Moore, Jelmer Vernooij) * Recognize and skip binary files in diff function. (Takeshi Kanemoto) * Fix handling of relative paths in dulwich.client.get_transport_and_path. (Brian Visel, #1169368) * Preserve ordering of entries in configuration. (Benjamin Pollack) * Support ~ expansion in SSH client paths. (milki, #1083439) * Support relative paths in alternate paths. (milki, Michel Lespinasse, #1175007) * Log all error messages from wsgiref server to the logging module. This makes the test suit quiet again. (Gary van der Merwe) * Support passing None for empty tree in changes_from_tree. (Kevin Watters) * Support fetching empty repository in client. (milki, #1060462) IMPROVEMENTS: * Add optional honor_filemode flag to build_index_from_tree. (Mark Mikofski) * Support core/filemode setting when building trees. (Jelmer Vernooij) * Add chapter on tags in tutorial. (Ryan Faulkner) FEATURES * Add support for mergetags. (milki, #963525) * Add support for posix shell hooks. (milki) 0.8.7 2012-11-27 BUG FIXES * Fix use of alternates in ``DiskObjectStore``.{__contains__,__iter__}. (Dmitriy) * Fix compatibility with Python 2.4. (David Carr) 0.8.6 2012-11-09 API CHANGES * dulwich.__init__ no longer imports client, protocol, repo and server modules. (Jelmer Vernooij) FEATURES * ConfigDict now behaves more like a dictionary. (Adam 'Cezar' Jenkins, issue #58) * HTTPGitApplication now takes an optional `fallback_app` argument. (Jonas Haag, issue #67) * Support for large pack index files. (Jameson Nash) TESTING * Make index entry tests a little bit less strict, to cope with slightly different behaviour on various platforms. (Jelmer Vernooij) * ``setup.py test`` (available when setuptools is installed) now runs all tests, not just the basic unit tests. (Jelmer Vernooij) BUG FIXES * Commit._deserialize now actually deserializes the current state rather than the previous one. (Yifan Zhang, issue #59) * Handle None elements in lists of TreeChange objects. (Alex Holmes) * Support cloning repositories without HEAD set. (D-Key, Jelmer Vernooij, issue #69) * Support ``MemoryRepo.get_config``. (Jelmer Vernooij) * In ``get_transport_and_path``, pass extra keyword arguments on to HttpGitClient. (Jelmer Vernooij) 0.8.5 2012-03-29 BUG FIXES * Avoid use of 'with' in dulwich.index. (Jelmer Vernooij) * Be a little bit strict about OS behaviour in index tests. Should fix the tests on Debian GNU/kFreeBSD. (Jelmer Vernooij) 0.8.4 2012-03-28 BUG FIXES * Options on the same line as sections in config files are now supported. (Jelmer Vernooij, #920553) * Only negotiate capabilities that are also supported by the server. (Rod Cloutier, Risto Kankkunen) * Fix parsing of invalid timezone offsets with two minus signs. (Jason R. Coombs, #697828) * Reset environment variables during tests, to avoid test isolation leaks reading ~/.gitconfig. (Risto Kankkunen) TESTS * $HOME is now explicitly specified for tests that use it to read ``~/.gitconfig``, to prevent test isolation issues. (Jelmer Vernooij, #920330) FEATURES * Additional arguments to get_transport_and_path are now passed on to the constructor of the transport. (Sam Vilain) * The WSGI server now transparently handles when a git client submits data using Content-Encoding: gzip. (David Blewett, Jelmer Vernooij) * Add dulwich.index.build_index_from_tree(). (milki) 0.8.3 2012-01-21 FEATURES * The config parser now supports the git-config file format as described in git-config(1) and can write git config files. (Jelmer Vernooij, #531092, #768687) * ``Repo.do_commit`` will now use the user identity from .git/config or ~/.gitconfig if none was explicitly specified. (Jelmer Vernooij) BUG FIXES * Allow ``determine_wants`` methods to include the zero sha in their return value. (Jelmer Vernooij) 0.8.2 2011-12-18 BUG FIXES * Cope with different zlib buffer sizes in sha1 file parser. (Jelmer Vernooij) * Fix get_transport_and_path for HTTP/HTTPS URLs. (Bruno Renié) * Avoid calling free_objects() on NULL in error cases. (Chris Eberle) * Fix use --bare argument to 'dulwich init'. (Chris Eberle) * Properly abort connections when the determine_wants function raises an exception. (Jelmer Vernooij, #856769) * Tweak xcodebuild hack to deal with more error output. (Jelmer Vernooij, #903840) FEATURES * Add support for retrieving tarballs from remote servers. (Jelmer Vernooij, #379087) * New method ``update_server_info`` which generates data for dumb server access. (Jelmer Vernooij, #731235) 0.8.1 2011-10-31 FEATURES * Repo.do_commit has a new argument 'ref'. * Repo.do_commit has a new argument 'merge_heads'. (Jelmer Vernooij) * New ``Repo.get_walker`` method. (Jelmer Vernooij) * New ``Repo.clone`` method. (Jelmer Vernooij, #725369) * ``GitClient.send_pack`` now supports the 'side-band-64k' capability. (Jelmer Vernooij) * ``HttpGitClient`` which supports the smart server protocol over HTTP. "dumb" access is not yet supported. (Jelmer Vernooij, #373688) * Add basic support for alternates. (Jelmer Vernooij, #810429) CHANGES * unittest2 or python >= 2.7 is now required for the testsuite. testtools is no longer supported. (Jelmer Vernooij, #830713) BUG FIXES * Fix compilation with older versions of MSVC. (Martin gz) * Special case 'refs/stash' as a valid ref. (Jelmer Vernooij, #695577) * Smart protocol clients can now change refs even if they are not uploading new data. (Jelmer Vernooij, #855993) * Don't compile C extensions when running in pypy. (Ronny Pfannschmidt, #881546) * Use different name for strnlen replacement function to avoid clashing with system strnlen. (Jelmer Vernooij, #880362) API CHANGES * ``Repo.revision_history`` is now deprecated in favor of ``Repo.get_walker``. (Jelmer Vernooij) 0.8.0 2011-08-07 FEATURES * New DeltaChainIterator abstract class for quickly iterating all objects in a pack, with implementations for pack indexing and inflation. (Dave Borowitz) * New walk module with a Walker class for customizable commit walking. (Dave Borowitz) * New tree_changes_for_merge function in diff_tree. (Dave Borowitz) * Easy rename detection in RenameDetector even without find_copies_harder. (Dave Borowitz) BUG FIXES * Avoid storing all objects in memory when writing pack. (Jelmer Vernooij, #813268) * Support IPv6 for git:// connections. (Jelmer Vernooij, #801543) * Improve performance of Repo.revision_history(). (Timo Schmid, #535118) * Fix use of SubprocessWrapper on Windows. (Paulo Madeira, #670035) * Fix compilation on newer versions of Mac OS X (Lion and up). (Ryan McKern, #794543) * Prevent raising ValueError for correct refs in RefContainer.__delitem__. * Correctly return a tuple from MemoryObjectStore.get_raw. (Dave Borowitz) * Fix a bug in reading the pack checksum when there are fewer than 20 bytes left in the buffer. (Dave Borowitz) * Support ~ in git:// URL paths. (Jelmer Vernooij, #813555) * Make ShaFile.__eq__ work when other is not a ShaFile. (Dave Borowitz) * ObjectStore.get_graph_walker() now no longer yields the same revision more than once. This has a significant improvement for performance when wide revision graphs are involved. (Jelmer Vernooij, #818168) * Teach ReceivePackHandler how to read empty packs. (Dave Borowitz) * Don't send a pack with duplicates of the same object. (Dave Borowitz) * Teach the server how to serve a clone of an empty repo. (Dave Borowitz) * Correctly advertise capabilities during receive-pack. (Dave Borowitz) * Fix add/add and add/rename conflicts in tree_changes_for_merge. (Dave Borowitz) * Use correct MIME types in web server. (Dave Borowitz) API CHANGES * write_pack no longer takes the num_objects argument and requires an object to be passed in that is iterable (rather than an iterator) and that provides __len__. (Jelmer Vernooij) * write_pack_data has been renamed to write_pack_objects and no longer takes a num_objects argument. (Jelmer Vernooij) * take_msb_bytes, read_zlib_chunks, unpack_objects, and PackStreamReader.read_objects now take an additional argument indicating a crc32 to compute. (Dave Borowitz) * PackObjectIterator was removed; its functionality is still exposed by PackData.iterobjects. (Dave Borowitz) * Add a sha arg to write_pack_object to incrementally compute a SHA. (Dave Borowitz) * Include offset in PackStreamReader results. (Dave Borowitz) * Move PackStreamReader from server to pack. (Dave Borowitz) * Extract a check_length_and_checksum, compute_file_sha, and pack_object_header pack helper functions. (Dave Borowitz) * Extract a compute_file_sha function. (Dave Borowitz) * Remove move_in_thin_pack as a separate method; add_thin_pack now completes the thin pack and moves it in in one step. Remove ThinPackData as well. (Dave Borowitz) * Custom buffer size in read_zlib_chunks. (Dave Borowitz) * New UnpackedObject data class that replaces ad-hoc tuples in the return value of unpack_object and various DeltaChainIterator methods. (Dave Borowitz) * Add a lookup_path convenience method to Tree. (Dave Borowitz) * Optionally create RenameDetectors without passing in tree SHAs. (Dave Borowitz) * Optionally include unchanged entries in RenameDetectors. (Dave Borowitz) * Optionally pass a RenameDetector to tree_changes. (Dave Borowitz) * Optionally pass a request object through to server handlers. (Dave Borowitz) TEST CHANGES * If setuptools is installed, "python setup.py test" will now run the testsuite. (Jelmer Vernooij) * Add a new build_pack test utility for building packs from a simple spec. (Dave Borowitz) * Add a new build_commit_graph test utility for building commits from a simple spec. (Dave Borowitz) 0.7.1 2011-04-12 BUG FIXES * Fix double decref in _diff_tree.c. (Ted Horst, #715528) * Fix the build on Windows. (Pascal Quantin) * Fix get_transport_and_path compatibility with pre-2.6.5 versions of Python. (Max Bowsher, #707438) * BaseObjectStore.determine_wants_all no longer breaks on zero SHAs. (Jelmer Vernooij) * write_tree_diff() now supports submodules. (Jelmer Vernooij) * Fix compilation for XCode 4 and older versions of distutils.sysconfig. (Daniele Sluijters) IMPROVEMENTS * Sphinxified documentation. (Lukasz Balcerzak) * Add Pack.keep.(Marc Brinkmann) API CHANGES * The order of the parameters to Tree.add(name, mode, sha) has changed, and is now consistent with the rest of Dulwich. Existing code will still work but print a DeprecationWarning. (Jelmer Vernooij, #663550) * Tree.entries() is now deprecated in favour of Tree.items() and Tree.iteritems(). (Jelmer Vernooij) 0.7.0 2011-01-21 FEATURES * New `dulwich.diff_tree` module for simple content-based rename detection. (Dave Borowitz) * Add Tree.items(). (Jelmer Vernooij) * Add eof() and unread_pkt_line() methods to Protocol. (Dave Borowitz) * Add write_tree_diff(). (Jelmer Vernooij) * Add `serve_command` function for git server commands as executables. (Jelmer Vernooij) * dulwich.client.get_transport_and_path now supports rsync-style repository URLs. (Dave Borowitz, #568493) BUG FIXES * Correct short-circuiting operation for no-op fetches in the server. (Dave Borowitz) * Support parsing git mbox patches without a version tail, as generated by Mercurial. (Jelmer Vernooij) * Fix dul-receive-pack and dul-upload-pack. (Jelmer Vernooij) * Zero-padded file modes in Tree objects no longer trigger an exception but the check code warns about them. (Augie Fackler, #581064) * Repo.init() now honors the mkdir flag. (#671159) * The ref format is now checked when setting a ref rather than when reading it back. (Dave Borowitz, #653527) * Make sure pack files are closed correctly. (Tay Ray Chuan) DOCUMENTATION * Run the tutorial inside the test suite. (Jelmer Vernooij) * Reorganized and updated the tutorial. (Jelmer Vernooij, Dave Borowitz, #610550, #610540) 0.6.2 2010-10-16 BUG FIXES * HTTP server correctly handles empty CONTENT_LENGTH. (Dave Borowitz) * Don't error when creating GitFiles with the default mode. (Dave Borowitz) * ThinPackData.from_file now works with resolve_ext_ref callback. (Dave Borowitz) * Provide strnlen() on mingw32 which doesn't have it. (Hans Kolek) * Set bare=true in the configuratin for bare repositories. (Dirk Neumann) FEATURES * Use slots for core objects to save up on memory. (Jelmer Vernooij) * Web server supports streaming progress/pack output. (Dave Borowitz) * New public function dulwich.pack.write_pack_header. (Dave Borowitz) * Distinguish between missing files and read errors in HTTP server. (Dave Borowitz) * Initial work on support for fastimport using python-fastimport. (Jelmer Vernooij) * New dulwich.pack.MemoryPackIndex class. (Jelmer Vernooij) * Delegate SHA peeling to the object store. (Dave Borowitz) TESTS * Use GitFile when modifying packed-refs in tests. (Dave Borowitz) * New tests in test_web with better coverage and fewer ad-hoc mocks. (Dave Borowitz) * Standardize quote delimiters in test_protocol. (Dave Borowitz) * Fix use when testtools is installed. (Jelmer Vernooij) * Add trivial test for write_pack_header. (Jelmer Vernooij) * Refactor some of dulwich.tests.compat.server_utils. (Dave Borowitz) * Allow overwriting id property of objects in test utils. (Dave Borowitz) * Use real in-memory objects rather than stubs for server tests. (Dave Borowitz) * Clean up MissingObjectFinder. (Dave Borowitz) API CHANGES * ObjectStore.iter_tree_contents now walks contents in depth-first, sorted order. (Dave Borowitz) * ObjectStore.iter_tree_contents can optionally yield tree objects as well. (Dave Borowitz). * Add side-band-64k support to ReceivePackHandler. (Dave Borowitz) * Change server capabilities methods to classmethods. (Dave Borowitz) * Tweak server handler injection. (Dave Borowitz) * PackIndex1 and PackIndex2 now subclass FilePackIndex, which is itself a subclass of PackIndex. (Jelmer Vernooij) DOCUMENTATION * Add docstrings for various functions in dulwich.objects. (Jelmer Vernooij) * Clean up docstrings in dulwich.protocol. (Dave Borowitz) * Explicitly specify allowed protocol commands to ProtocolGraphWalker.read_proto_line. (Dave Borowitz) * Add utility functions to DictRefsContainer. (Dave Borowitz) 0.6.1 2010-07-22 BUG FIXES * Fix memory leak in C implementation of sorted_tree_items. (Dave Borowitz) * Use correct path separators for named repo files. (Dave Borowitz) * python > 2.7 and testtools-based test runners will now also pick up skipped tests correctly. (Jelmer Vernooij) FEATURES * Move named file initilization to BaseRepo. (Dave Borowitz) * Add logging utilities and git/HTTP server logging. (Dave Borowitz) * The GitClient interface has been cleaned up and instances are now reusable. (Augie Fackler) * Allow overriding paths to executables in GitSSHClient. (Ross Light, Jelmer Vernooij, #585204) * Add PackBasedObjectStore.pack_loose_objects(). (Jelmer Vernooij) TESTS * Add tests for sorted_tree_items and C implementation. (Dave Borowitz) * Add a MemoryRepo that stores everything in memory. (Dave Borowitz) * Quiet logging output from web tests. (Dave Borowitz) * More flexible version checking for compat tests. (Dave Borowitz) * Compat tests for servers with and without side-band-64k. (Dave Borowitz) CLEANUP * Clean up file headers. (Dave Borowitz) TESTS * Use GitFile when modifying packed-refs in tests. (Dave Borowitz) API CHANGES * dulwich.pack.write_pack_index_v{1,2} now take a file-like object rather than a filename. (Jelmer Vernooij) * Make dul-daemon/dul-web trivial wrappers around server functionality. (Dave Borowitz) * Move reference WSGI handler to web.py. (Dave Borowitz) * Factor out _report_status in ReceivePackHandler. (Dave Borowitz) * Factor out a function to convert a line to a pkt-line. (Dave Borowitz) 0.6.0 2010-05-22 note: This list is most likely incomplete for 0.6.0. BUG FIXES * Fix ReceivePackHandler to disallow removing refs without delete-refs. (Dave Borowitz) * Deal with capabilities required by the client, even if they can not be disabled in the server. (Dave Borowitz) * Fix trailing newlines in generated patch files. (Jelmer Vernooij) * Implement RefsContainer.__contains__. (Jelmer Vernooij) * Cope with \r in ref files on Windows. ( http://github.com/jelmer/dulwich/issues/#issue/13, Jelmer Vernooij) * Fix GitFile breakage on Windows. (Anatoly Techtonik, #557585) * Support packed ref deletion with no peeled refs. (Augie Fackler) * Fix send pack when there is nothing to fetch. (Augie Fackler) * Fix fetch if no progress function is specified. (Augie Fackler) * Allow double-staging of files that are deleted in the index. (Dave Borowitz) * Fix RefsContainer.add_if_new to support dangling symrefs. (Dave Borowitz) * Non-existant index files in non-bare repositories are now treated as empty. (Dave Borowitz) * Always update ShaFile.id when the contents of the object get changed. (Jelmer Vernooij) * Various Python2.4-compatibility fixes. (Dave Borowitz) * Fix thin pack handling. (Dave Borowitz) FEATURES * Add include-tag capability to server. (Dave Borowitz) * New dulwich.fastexport module that can generate fastexport streams. (Jelmer Vernooij) * Implemented BaseRepo.__contains__. (Jelmer Vernooij) * Add __setitem__ to DictRefsContainer. (Dave Borowitz) * Overall improvements checking Git objects. (Dave Borowitz) * Packs are now verified while they are received. (Dave Borowitz) TESTS * Add framework for testing compatibility with C Git. (Dave Borowitz) * Add various tests for the use of non-bare repositories. (Dave Borowitz) * Cope with diffstat not being available on all platforms. (Tay Ray Chuan, Jelmer Vernooij) * Add make_object and make_commit convenience functions to test utils. (Dave Borowitz) API BREAKAGES * The 'committer' and 'message' arguments to Repo.do_commit() have been swapped. 'committer' is now optional. (Jelmer Vernooij) * Repo.get_blob, Repo.commit, Repo.tag and Repo.tree are now deprecated. (Jelmer Vernooij) * RefsContainer.set_ref() was renamed to RefsContainer.set_symbolic_ref(), for clarity. (Jelmer Vernooij) API CHANGES * The primary serialization APIs in dulwich.objects now work with chunks of strings rather than with full-text strings. (Jelmer Vernooij) 0.5.02010-03-03 BUG FIXES * Support custom fields in commits (readonly). (Jelmer Vernooij) * Improved ref handling. (Dave Borowitz) * Rework server protocol to be smarter and interoperate with cgit client. (Dave Borowitz) * Add a GitFile class that uses the same locking protocol for writes as cgit. (Dave Borowitz) * Cope with forward slashes correctly in the index on Windows. (Jelmer Vernooij, #526793) FEATURES * --pure option to setup.py to allow building/installing without the C extensions. (Hal Wine, Anatoly Techtonik, Jelmer Vernooij, #434326) * Implement Repo.get_config(). (Jelmer Vernooij, Augie Fackler) * HTTP dumb and smart server. (Dave Borowitz) * Add abstract baseclass for Repo that does not require file system operations. (Dave Borowitz) 0.4.1 2010-01-03 FEATURES * Add ObjectStore.iter_tree_contents(). (Jelmer Vernooij) * Add Index.changes_from_tree(). (Jelmer Vernooij) * Add ObjectStore.tree_changes(). (Jelmer Vernooij) * Add functionality for writing patches in dulwich.patch. (Jelmer Vernooij) 0.4.0 2009-10-07 DOCUMENTATION * Added tutorial. API CHANGES * dulwich.object_store.tree_lookup_path will now return the mode and sha of the object found rather than the object itself. BUG FIXES * Use binascii.hexlify / binascii.unhexlify for better performance. * Cope with extra unknown data in index files by ignoring it (for now). * Add proper error message when server unexpectedly hangs up. (#415843) * Correctly write opcode for equal in create_delta. 0.3.3 2009-07-23 FEATURES * Implement ShaFile.__hash__(). * Implement Tree.__len__() BUG FIXES * Check for 'objects' and 'refs' directories when looking for a Git repository. (#380818) 0.3.2 2009-05-20 BUG FIXES * Support the encoding field in Commits. * Some Windows compatibility fixes. * Fixed several issues in commit support. FEATURES * Basic support for handling submodules. 0.3.1 2009-05-13 FEATURES * Implemented Repo.__getitem__, Repo.__setitem__ and Repo.__delitem__ to access content. API CHANGES * Removed Repo.set_ref, Repo.remove_ref, Repo.tags, Repo.get_refs and Repo.heads in favor of Repo.refs, a dictionary-like object for accessing refs. BUG FIXES * Removed import of 'sha' module in objects.py, which was causing deprecation warnings on Python 2.6. 0.3.0 2009-05-10 FEATURES * A new function 'commit_tree' has been added that can commit a tree based on an index. BUG FIXES * The memory usage when generating indexes has been significantly reduced. * A memory leak in the C implementation of parse_tree has been fixed. * The send-pack smart server command now works. (Thanks Scott Chacon) * The handling of short timestamps (less than 10 digits) has been fixed. * The handling of timezones has been fixed. 0.2.1 2009-04-30 BUG FIXES * Fix compatibility with Python2.4. 0.2.0 2009-04-30 FEATURES * Support for activity reporting in smart protocol client. * Optional C extensions for better performance in a couple of places that are performance-critical. 0.1.1 2009-03-13 BUG FIXES * Fixed regression in Repo.find_missing_objects() * Don't fetch ^{} objects from remote hosts, as requesting them causes a hangup. * Always write pack to disk completely before calculating checksum. FEATURES * Allow disabling thin packs when talking to remote hosts. 0.1.0 2009-01-24 * Initial release. diff --git a/bin/dulwich b/bin/dulwich index 9acba566..eeaee124 100755 --- a/bin/dulwich +++ b/bin/dulwich @@ -1,714 +1,716 @@ #!/usr/bin/python -u # # dulwich - Simple command-line interface to Dulwich # Copyright (C) 2008-2011 Jelmer Vernooij # vim: expandtab # # 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 # for a copy of the GNU General Public License # and for a copy of the Apache # License, Version 2.0. # """Simple command-line interface to Dulwich> This is a very simple command-line wrapper for Dulwich. It is by no means intended to be a full-blown Git command-line interface but just a way to test Dulwich. """ import os import sys from getopt import getopt import optparse import signal def signal_int(signal, frame): sys.exit(1) def signal_quit(signal, frame): import pdb pdb.set_trace() if 'DULWICH_PDB' in os.environ: signal.signal(signal.SIGQUIT, signal_quit) signal.signal(signal.SIGINT, signal_int) from dulwich import porcelain from dulwich.client import get_transport_and_path from dulwich.errors import ApplyDeltaError from dulwich.index import Index from dulwich.pack import Pack, sha_to_hex from dulwich.patch import write_tree_diff from dulwich.repo import Repo class Command(object): """A Dulwich subcommand.""" def run(self, args): """Run the command.""" raise NotImplementedError(self.run) class cmd_archive(Command): def run(self, args): parser = optparse.OptionParser() parser.add_option("--remote", type=str, help="Retrieve archive from specified remote repo") options, args = parser.parse_args(args) committish = args.pop(0) if options.remote: client, path = get_transport_and_path(options.remote) client.archive(path, committish, sys.stdout.write, write_error=sys.stderr.write) else: porcelain.archive('.', committish, outstream=sys.stdout, errstream=sys.stderr) class cmd_add(Command): def run(self, args): opts, args = getopt(args, "", []) porcelain.add(".", paths=args) class cmd_rm(Command): def run(self, args): opts, args = getopt(args, "", []) porcelain.rm(".", paths=args) class cmd_fetch_pack(Command): def run(self, args): opts, args = getopt(args, "", ["all"]) opts = dict(opts) client, path = get_transport_and_path(args.pop(0)) r = Repo(".") if "--all" in opts: determine_wants = r.object_store.determine_wants_all else: determine_wants = lambda x: [y for y in args if not y in r.object_store] client.fetch(path, r, determine_wants) class cmd_fetch(Command): def run(self, args): opts, args = getopt(args, "", []) opts = dict(opts) client, path = get_transport_and_path(args.pop(0)) r = Repo(".") if "--all" in opts: determine_wants = r.object_store.determine_wants_all refs = client.fetch(path, r, progress=sys.stdout.write) print("Remote refs:") for item in refs.items(): print("%s -> %s" % item) class cmd_fsck(Command): def run(self, args): opts, args = getopt(args, "", []) opts = dict(opts) for (obj, msg) in porcelain.fsck('.'): print("%s: %s" % (obj, msg)) class cmd_log(Command): def run(self, args): parser = optparse.OptionParser() parser.add_option("--reverse", dest="reverse", action="store_true", help="Reverse order in which entries are printed") parser.add_option("--name-status", dest="name_status", action="store_true", help="Print name/status for each changed file") options, args = parser.parse_args(args) porcelain.log(".", paths=args, reverse=options.reverse, name_status=options.name_status, outstream=sys.stdout) class cmd_diff(Command): def run(self, args): opts, args = getopt(args, "", []) if args == []: print("Usage: dulwich diff COMMITID") sys.exit(1) r = Repo(".") commit_id = args[0] commit = r[commit_id] parent_commit = r[commit.parents[0]] write_tree_diff(sys.stdout, r.object_store, parent_commit.tree, commit.tree) class cmd_dump_pack(Command): def run(self, args): opts, args = getopt(args, "", []) if args == []: print("Usage: dulwich dump-pack FILENAME") sys.exit(1) basename, _ = os.path.splitext(args[0]) x = Pack(basename) print("Object names checksum: %s" % x.name()) print("Checksum: %s" % sha_to_hex(x.get_stored_checksum())) if not x.check(): print("CHECKSUM DOES NOT MATCH") print("Length: %d" % len(x)) for name in x: try: print("\t%s" % x[name]) except KeyError as k: print("\t%s: Unable to resolve base %s" % (name, k)) except ApplyDeltaError as e: print("\t%s: Unable to apply delta: %r" % (name, e)) class cmd_dump_index(Command): def run(self, args): opts, args = getopt(args, "", []) if args == []: print("Usage: dulwich dump-index FILENAME") sys.exit(1) filename = args[0] idx = Index(filename) for o in idx: print(o, idx[o]) class cmd_init(Command): def run(self, args): opts, args = getopt(args, "", ["bare"]) opts = dict(opts) if args == []: path = os.getcwd() else: path = args[0] porcelain.init(path, bare=("--bare" in opts)) class cmd_clone(Command): def run(self, args): parser = optparse.OptionParser() parser.add_option("--bare", dest="bare", help="Whether to create a bare repository.", action="store_true") parser.add_option("--depth", dest="depth", type=int, help="Depth at which to fetch") options, args = parser.parse_args(args) if args == []: print("usage: dulwich clone host:path [PATH]") sys.exit(1) source = args.pop(0) if len(args) > 0: target = args.pop(0) else: target = None porcelain.clone(source, target, bare=options.bare, depth=options.depth) class cmd_commit(Command): def run(self, args): opts, args = getopt(args, "", ["message"]) opts = dict(opts) porcelain.commit(".", message=opts["--message"]) class cmd_commit_tree(Command): def run(self, args): opts, args = getopt(args, "", ["message"]) if args == []: print("usage: dulwich commit-tree tree") sys.exit(1) opts = dict(opts) porcelain.commit_tree(".", tree=args[0], message=opts["--message"]) class cmd_update_server_info(Command): def run(self, args): porcelain.update_server_info(".") class cmd_symbolic_ref(Command): def run(self, args): opts, args = getopt(args, "", ["ref-name", "force"]) if not args: print("Usage: dulwich symbolic-ref REF_NAME [--force]") sys.exit(1) ref_name = args.pop(0) porcelain.symbolic_ref(".", ref_name=ref_name, force='--force' in args) class cmd_show(Command): def run(self, args): opts, args = getopt(args, "", []) porcelain.show(".", args) class cmd_diff_tree(Command): def run(self, args): opts, args = getopt(args, "", []) if len(args) < 2: print("Usage: dulwich diff-tree OLD-TREE NEW-TREE") sys.exit(1) porcelain.diff_tree(".", args[0], args[1]) class cmd_rev_list(Command): def run(self, args): opts, args = getopt(args, "", []) if len(args) < 1: print('Usage: dulwich rev-list COMMITID...') sys.exit(1) porcelain.rev_list('.', args) class cmd_tag(Command): def run(self, args): - opts, args = getopt(args, '', []) - if len(args) < 2: - print('Usage: dulwich tag NAME') - sys.exit(1) - porcelain.tag('.', args[0]) + parser = optparse.OptionParser() + parser.add_option("-a", "--annotated", help="Create an annotated tag.", action="store_true") + parser.add_option("-s", "--sign", help="Sign the annotated tag.", action="store_true") + options, args = parser.parse_args(args) + porcelain.tag_create( + '.', args[0], annotated=options.annotated, + sign=options.sign) class cmd_repack(Command): def run(self, args): opts, args = getopt(args, "", []) opts = dict(opts) porcelain.repack('.') class cmd_reset(Command): def run(self, args): opts, args = getopt(args, "", ["hard", "soft", "mixed"]) opts = dict(opts) mode = "" if "--hard" in opts: mode = "hard" elif "--soft" in opts: mode = "soft" elif "--mixed" in opts: mode = "mixed" porcelain.reset('.', mode=mode, *args) class cmd_daemon(Command): def run(self, args): from dulwich import log_utils from dulwich.protocol import TCP_GIT_PORT parser = optparse.OptionParser() parser.add_option("-l", "--listen_address", dest="listen_address", default="localhost", help="Binding IP address.") parser.add_option("-p", "--port", dest="port", type=int, default=TCP_GIT_PORT, help="Binding TCP port.") options, args = parser.parse_args(args) log_utils.default_logging_config() if len(args) >= 1: gitdir = args[0] else: gitdir = '.' from dulwich import porcelain porcelain.daemon(gitdir, address=options.listen_address, port=options.port) class cmd_web_daemon(Command): def run(self, args): from dulwich import log_utils parser = optparse.OptionParser() parser.add_option("-l", "--listen_address", dest="listen_address", default="", help="Binding IP address.") parser.add_option("-p", "--port", dest="port", type=int, default=8000, help="Binding TCP port.") options, args = parser.parse_args(args) log_utils.default_logging_config() if len(args) >= 1: gitdir = args[0] else: gitdir = '.' from dulwich import porcelain porcelain.web_daemon(gitdir, address=options.listen_address, port=options.port) class cmd_write_tree(Command): def run(self, args): parser = optparse.OptionParser() options, args = parser.parse_args(args) sys.stdout.write('%s\n' % porcelain.write_tree('.')) class cmd_receive_pack(Command): def run(self, args): parser = optparse.OptionParser() options, args = parser.parse_args(args) if len(args) >= 1: gitdir = args[0] else: gitdir = '.' porcelain.receive_pack(gitdir) class cmd_upload_pack(Command): def run(self, args): parser = optparse.OptionParser() options, args = parser.parse_args(args) if len(args) >= 1: gitdir = args[0] else: gitdir = '.' porcelain.upload_pack(gitdir) class cmd_status(Command): def run(self, args): parser = optparse.OptionParser() options, args = parser.parse_args(args) if len(args) >= 1: gitdir = args[0] else: gitdir = '.' status = porcelain.status(gitdir) if any(names for (kind, names) in status.staged.items()): sys.stdout.write("Changes to be committed:\n\n") for kind, names in status.staged.items(): for name in names: sys.stdout.write("\t%s: %s\n" % ( kind, name.decode(sys.getfilesystemencoding()))) sys.stdout.write("\n") if status.unstaged: sys.stdout.write("Changes not staged for commit:\n\n") for name in status.unstaged: sys.stdout.write("\t%s\n" % name.decode(sys.getfilesystemencoding())) sys.stdout.write("\n") if status.untracked: sys.stdout.write("Untracked files:\n\n") for name in status.untracked: sys.stdout.write("\t%s\n" % name) sys.stdout.write("\n") class cmd_ls_remote(Command): def run(self, args): opts, args = getopt(args, '', []) if len(args) < 1: print('Usage: dulwich ls-remote URL') sys.exit(1) refs = porcelain.ls_remote(args[0]) for ref in sorted(refs): sys.stdout.write("%s\t%s\n" % (ref, refs[ref])) class cmd_ls_tree(Command): def run(self, args): parser = optparse.OptionParser() parser.add_option("-r", "--recursive", action="store_true", help="Recusively list tree contents.") parser.add_option("--name-only", action="store_true", help="Only display name.") options, args = parser.parse_args(args) try: treeish = args.pop(0) except IndexError: treeish = None porcelain.ls_tree( '.', treeish, outstream=sys.stdout, recursive=options.recursive, name_only=options.name_only) class cmd_pack_objects(Command): def run(self, args): opts, args = getopt(args, '', ['stdout']) opts = dict(opts) if len(args) < 1 and not '--stdout' in args: print('Usage: dulwich pack-objects basename') sys.exit(1) object_ids = [l.strip() for l in sys.stdin.readlines()] basename = args[0] if '--stdout' in opts: packf = getattr(sys.stdout, 'buffer', sys.stdout) idxf = None close = [] else: packf = open(basename + '.pack', 'w') idxf = open(basename + '.idx', 'w') close = [packf, idxf] porcelain.pack_objects('.', object_ids, packf, idxf) for f in close: f.close() class cmd_pull(Command): def run(self, args): parser = optparse.OptionParser() options, args = parser.parse_args(args) try: from_location = args[0] except IndexError: from_location = None porcelain.pull('.', from_location) class cmd_push(Command): def run(self, args): parser = optparse.OptionParser() options, args = parser.parse_args(args) if len(args) < 2: print("Usage: dulwich push TO-LOCATION REFSPEC..") sys.exit(1) to_location = args[0] refspecs = args[1:] porcelain.push('.', to_location, refspecs) class cmd_remote_add(Command): def run(self, args): parser = optparse.OptionParser() options, args = parser.parse_args(args) porcelain.remote_add('.', args[0], args[1]) class SuperCommand(Command): subcommands = {} def run(self, args): if not args: print("Supported subcommands: %s" % ', '.join(self.subcommands.keys())) return False cmd = args[0] try: cmd_kls = self.subcommands[cmd] except KeyError: print('No such subcommand: %s' % args[0]) return False return cmd_kls().run(args[1:]) class cmd_remote(SuperCommand): subcommands = { "add": cmd_remote_add, } class cmd_check_ignore(Command): def run(self, args): parser = optparse.OptionParser() options, args = parser.parse_args(args) ret = 1 for path in porcelain.check_ignore('.', args): print(path) ret = 0 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_stash_list(Command): def run(self, args): parser = optparse.OptionParser() options, args = parser.parse_args(args) for i, entry in porcelain.stash_list('.'): print("stash@{%d}: %s" % (i, entry.message.rstrip('\n'))) class cmd_stash_push(Command): def run(self, args): parser = optparse.OptionParser() options, args = parser.parse_args(args) porcelain.stash_push('.') print("Saved working directory and index state") class cmd_stash_pop(Command): def run(self, args): parser = optparse.OptionParser() options, args = parser.parse_args(args) porcelain.stash_pop('.') print("Restrored working directory and index state") class cmd_stash(SuperCommand): subcommands = { "list": cmd_stash_list, "pop": cmd_stash_pop, "push": cmd_stash_push, } class cmd_ls_files(Command): def run(self, args): parser = optparse.OptionParser() options, args = parser.parse_args(args) for name in porcelain.ls_files('.'): print(name) class cmd_describe(Command): def run(self, args): parser = optparse.OptionParser() options, args = parser.parse_args(args) print(porcelain.describe('.')) class cmd_help(Command): def run(self, args): parser = optparse.OptionParser() parser.add_option("-a", "--all", dest="all", action="store_true", help="List all commands.") options, args = parser.parse_args(args) if options.all: print('Available commands:') for cmd in sorted(commands): print(' %s' % cmd) else: print("""\ The dulwich command line tool is currently a very basic frontend for the Dulwich python module. For full functionality, please see the API reference. For a list of supported commands, see 'dulwich help -a'. """) 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, "describe": cmd_describe, "daemon": cmd_daemon, "diff": cmd_diff, "diff-tree": cmd_diff_tree, "dump-pack": cmd_dump_pack, "dump-index": cmd_dump_index, "fetch-pack": cmd_fetch_pack, "fetch": cmd_fetch, "fsck": cmd_fsck, "help": cmd_help, "init": cmd_init, "log": cmd_log, "ls-files": cmd_ls_files, "ls-remote": cmd_ls_remote, "ls-tree": cmd_ls_tree, "pack-objects": cmd_pack_objects, "pull": cmd_pull, "push": cmd_push, "receive-pack": cmd_receive_pack, "remote": cmd_remote, "repack": cmd_repack, "reset": cmd_reset, "rev-list": cmd_rev_list, "rm": cmd_rm, "show": cmd_show, "stash": cmd_stash, "status": cmd_status, "symbolic-ref": cmd_symbolic_ref, "tag": cmd_tag, "update-server-info": cmd_update_server_info, "upload-pack": cmd_upload_pack, "web-daemon": cmd_web_daemon, "write-tree": cmd_write_tree, } if len(sys.argv) < 2: print("Usage: %s <%s> [OPTIONS...]" % (sys.argv[0], "|".join(commands.keys()))) sys.exit(1) cmd = sys.argv[1] try: cmd_kls = commands[cmd] except KeyError: print("No such subcommand: %s" % cmd) sys.exit(1) # TODO(jelmer): Return non-0 on errors cmd_kls().run(sys.argv[2:]) diff --git a/dulwich/objects.py b/dulwich/objects.py index 8e224162..a56fdec9 100644 --- a/dulwich/objects.py +++ b/dulwich/objects.py @@ -1,1381 +1,1400 @@ # objects.py -- Access to base git objects # Copyright (C) 2007 James Westby # Copyright (C) 2008-2013 Jelmer Vernooij # # 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 # for a copy of the GNU General Public License # and for a copy of the Apache # License, Version 2.0. # """Access to base git objects.""" import binascii from io import BytesIO from collections import namedtuple import os import posixpath import stat import sys import warnings import zlib from hashlib import sha1 from dulwich.errors import ( ChecksumMismatch, NotBlobError, NotCommitError, NotTagError, NotTreeError, ObjectFormatException, EmptyFileException, ) from dulwich.file import GitFile ZERO_SHA = b'0' * 40 # Header fields for commits _TREE_HEADER = b'tree' _PARENT_HEADER = b'parent' _AUTHOR_HEADER = b'author' _COMMITTER_HEADER = b'committer' _ENCODING_HEADER = b'encoding' _MERGETAG_HEADER = b'mergetag' _GPGSIG_HEADER = b'gpgsig' # Header fields for objects _OBJECT_HEADER = b'object' _TYPE_HEADER = b'type' _TAG_HEADER = b'tag' _TAGGER_HEADER = b'tagger' S_IFGITLINK = 0o160000 MAX_TIME = 9223372036854775807 # (2**63) - 1 - signed long int max +BEGIN_PGP_SIGNATURE = b"-----BEGIN PGP SIGNATURE-----" + def S_ISGITLINK(m): """Check if a mode indicates a submodule. :param m: Mode to check :return: a ``boolean`` """ return (stat.S_IFMT(m) == S_IFGITLINK) def _decompress(string): dcomp = zlib.decompressobj() dcomped = dcomp.decompress(string) dcomped += dcomp.flush() return dcomped def sha_to_hex(sha): """Takes a string and returns the hex of the sha within""" hexsha = binascii.hexlify(sha) assert len(hexsha) == 40, "Incorrect length of sha1 string: %d" % hexsha return hexsha def hex_to_sha(hex): """Takes a hex sha and returns a binary sha""" assert len(hex) == 40, "Incorrect length of hexsha: %s" % hex try: return binascii.unhexlify(hex) except TypeError as exc: if not isinstance(hex, bytes): raise raise ValueError(exc.args[0]) def valid_hexsha(hex): if len(hex) != 40: return False try: binascii.unhexlify(hex) except (TypeError, binascii.Error): return False else: return True def hex_to_filename(path, hex): """Takes a hex sha and returns its filename relative to the given path.""" # os.path.join accepts bytes or unicode, but all args must be of the same # type. Make sure that hex which is expected to be bytes, is the same type # as path. if getattr(path, 'encode', None) is not None: hex = hex.decode('ascii') dir = hex[:2] file = hex[2:] # Check from object dir return os.path.join(path, dir, file) def filename_to_hex(filename): """Takes an object filename and returns its corresponding hex sha.""" # grab the last (up to) two path components names = filename.rsplit(os.path.sep, 2)[-2:] errmsg = "Invalid object filename: %s" % filename assert len(names) == 2, errmsg base, rest = names assert len(base) == 2 and len(rest) == 38, errmsg hex = (base + rest).encode('ascii') hex_to_sha(hex) return hex def object_header(num_type, length): """Return an object header for the given numeric type and text length.""" return (object_class(num_type).type_name + b' ' + str(length).encode('ascii') + b'\0') def serializable_property(name, docstring=None): """A property that helps tracking whether serialization is necessary. """ def set(obj, value): setattr(obj, "_"+name, value) obj._needs_serialization = True def get(obj): return getattr(obj, "_"+name) return property(get, set, doc=docstring) def object_class(type): """Get the object class corresponding to the given type. :param type: Either a type name string or a numeric type. :return: The ShaFile subclass corresponding to the given type, or None if type is not a valid type name/number. """ return _TYPE_MAP.get(type, None) def check_hexsha(hex, error_msg): """Check if a string is a valid hex sha string. :param hex: Hex string to check :param error_msg: Error message to use in exception :raise ObjectFormatException: Raised when the string is not valid """ if not valid_hexsha(hex): raise ObjectFormatException("%s %s" % (error_msg, hex)) def check_identity(identity, error_msg): """Check if the specified identity is valid. This will raise an exception if the identity is not valid. :param identity: Identity string :param error_msg: Error message to use in exception """ email_start = identity.find(b'<') email_end = identity.find(b'>') if (email_start < 0 or email_end < 0 or email_end <= email_start or identity.find(b'<', email_start + 1) >= 0 or identity.find(b'>', email_end + 1) >= 0 or not identity.endswith(b'>')): raise ObjectFormatException(error_msg) def check_time(time_seconds): """Check if the specified time is not prone to overflow error. This will raise an exception if the time is not valid. :param time_info: author/committer/tagger info """ # Prevent overflow error if time_seconds > MAX_TIME: raise ObjectFormatException( 'Date field should not exceed %s' % MAX_TIME) def git_line(*items): """Formats items into a space separated line.""" return b' '.join(items) + b'\n' class FixedSha(object): """SHA object that behaves like hashlib's but is given a fixed value.""" __slots__ = ('_hexsha', '_sha') def __init__(self, hexsha): if getattr(hexsha, 'encode', None) is not None: hexsha = hexsha.encode('ascii') if not isinstance(hexsha, bytes): raise TypeError('Expected bytes for hexsha, got %r' % hexsha) self._hexsha = hexsha self._sha = hex_to_sha(hexsha) def digest(self): """Return the raw SHA digest.""" return self._sha def hexdigest(self): """Return the hex SHA digest.""" return self._hexsha.decode('ascii') class ShaFile(object): """A git SHA file.""" __slots__ = ('_chunked_text', '_sha', '_needs_serialization') @staticmethod def _parse_legacy_object_header(magic, f): """Parse a legacy object, creating it but not reading the file.""" bufsize = 1024 decomp = zlib.decompressobj() header = decomp.decompress(magic) start = 0 end = -1 while end < 0: extra = f.read(bufsize) header += decomp.decompress(extra) magic += extra end = header.find(b'\0', start) start = len(header) header = header[:end] type_name, size = header.split(b' ', 1) size = int(size) # sanity check obj_class = object_class(type_name) if not obj_class: raise ObjectFormatException("Not a known type: %s" % type_name) return obj_class() def _parse_legacy_object(self, map): """Parse a legacy object, setting the raw string.""" text = _decompress(map) header_end = text.find(b'\0') if header_end < 0: raise ObjectFormatException("Invalid object header, no \\0") self.set_raw_string(text[header_end+1:]) def as_legacy_object_chunks(self): """Return chunks representing the object in the experimental format. :return: List of strings """ compobj = zlib.compressobj() yield compobj.compress(self._header()) for chunk in self.as_raw_chunks(): yield compobj.compress(chunk) yield compobj.flush() def as_legacy_object(self): """Return string representing the object in the experimental format. """ return b''.join(self.as_legacy_object_chunks()) def as_raw_chunks(self): """Return chunks with serialization of the object. :return: List of strings, not necessarily one per line """ if self._needs_serialization: self._sha = None self._chunked_text = self._serialize() self._needs_serialization = False return self._chunked_text def as_raw_string(self): """Return raw string with serialization of the object. :return: String object """ return b''.join(self.as_raw_chunks()) if sys.version_info[0] >= 3: def __bytes__(self): """Return raw string serialization of this object.""" return self.as_raw_string() else: def __str__(self): """Return raw string serialization of this object.""" return self.as_raw_string() def __hash__(self): """Return unique hash for this object.""" return hash(self.id) def as_pretty_string(self): """Return a string representing this object, fit for display.""" return self.as_raw_string() def set_raw_string(self, text, sha=None): """Set the contents of this object from a serialized string.""" if not isinstance(text, bytes): raise TypeError('Expected bytes for text, got %r' % text) self.set_raw_chunks([text], sha) def set_raw_chunks(self, chunks, sha=None): """Set the contents of this object from a list of chunks.""" self._chunked_text = chunks self._deserialize(chunks) if sha is None: self._sha = None else: self._sha = FixedSha(sha) self._needs_serialization = False @staticmethod def _parse_object_header(magic, f): """Parse a new style object, creating it but not reading the file.""" num_type = (ord(magic[0:1]) >> 4) & 7 obj_class = object_class(num_type) if not obj_class: raise ObjectFormatException("Not a known type %d" % num_type) return obj_class() def _parse_object(self, map): """Parse a new style object, setting self._text.""" # skip type and size; type must have already been determined, and # we trust zlib to fail if it's otherwise corrupted byte = ord(map[0:1]) used = 1 while (byte & 0x80) != 0: byte = ord(map[used:used+1]) used += 1 raw = map[used:] self.set_raw_string(_decompress(raw)) @classmethod def _is_legacy_object(cls, magic): b0 = ord(magic[0:1]) b1 = ord(magic[1:2]) word = (b0 << 8) + b1 return (b0 & 0x8F) == 0x08 and (word % 31) == 0 @classmethod def _parse_file(cls, f): map = f.read() if not map: raise EmptyFileException('Corrupted empty file detected') if cls._is_legacy_object(map): obj = cls._parse_legacy_object_header(map, f) obj._parse_legacy_object(map) else: obj = cls._parse_object_header(map, f) obj._parse_object(map) return obj def __init__(self): """Don't call this directly""" self._sha = None self._chunked_text = [] self._needs_serialization = True def _deserialize(self, chunks): raise NotImplementedError(self._deserialize) def _serialize(self): raise NotImplementedError(self._serialize) @classmethod def from_path(cls, path): """Open a SHA file from disk.""" with GitFile(path, 'rb') as f: return cls.from_file(f) @classmethod def from_file(cls, f): """Get the contents of a SHA file on disk.""" try: obj = cls._parse_file(f) obj._sha = None return obj except (IndexError, ValueError): raise ObjectFormatException("invalid object header") @staticmethod def from_raw_string(type_num, string, sha=None): """Creates an object of the indicated type from the raw string given. :param type_num: The numeric type of the object. :param string: The raw uncompressed contents. :param sha: Optional known sha for the object """ obj = object_class(type_num)() obj.set_raw_string(string, sha) return obj @staticmethod def from_raw_chunks(type_num, chunks, sha=None): """Creates an object of the indicated type from the raw chunks given. :param type_num: The numeric type of the object. :param chunks: An iterable of the raw uncompressed contents. :param sha: Optional known sha for the object """ obj = object_class(type_num)() obj.set_raw_chunks(chunks, sha) return obj @classmethod def from_string(cls, string): """Create a ShaFile from a string.""" obj = cls() obj.set_raw_string(string) return obj def _check_has_member(self, member, error_msg): """Check that the object has a given member variable. :param member: the member variable to check for :param error_msg: the message for an error if the member is missing :raise ObjectFormatException: with the given error_msg if member is missing or is None """ if getattr(self, member, None) is None: raise ObjectFormatException(error_msg) def check(self): """Check this object for internal consistency. :raise ObjectFormatException: if the object is malformed in some way :raise ChecksumMismatch: if the object was created with a SHA that does not match its contents """ # TODO: if we find that error-checking during object parsing is a # performance bottleneck, those checks should be moved to the class's # check() method during optimization so we can still check the object # when necessary. old_sha = self.id try: self._deserialize(self.as_raw_chunks()) self._sha = None new_sha = self.id except Exception as e: raise ObjectFormatException(e) if old_sha != new_sha: raise ChecksumMismatch(new_sha, old_sha) def _header(self): return object_header(self.type, self.raw_length()) def raw_length(self): """Returns the length of the raw string of this object.""" ret = 0 for chunk in self.as_raw_chunks(): ret += len(chunk) return ret def sha(self): """The SHA1 object that is the name of this object.""" if self._sha is None or self._needs_serialization: # this is a local because as_raw_chunks() overwrites self._sha new_sha = sha1() new_sha.update(self._header()) for chunk in self.as_raw_chunks(): new_sha.update(chunk) self._sha = new_sha return self._sha def copy(self): """Create a new copy of this SHA1 object from its raw string""" obj_class = object_class(self.get_type()) return obj_class.from_raw_string( self.get_type(), self.as_raw_string(), self.id) @property def id(self): """The hex SHA of this object.""" return self.sha().hexdigest().encode('ascii') def get_type(self): """Return the type number for this object class.""" return self.type_num def set_type(self, type): """Set the type number for this object class.""" self.type_num = type # DEPRECATED: use type_num or type_name as needed. type = property(get_type, set_type) def __repr__(self): return "<%s %s>" % (self.__class__.__name__, self.id) def __ne__(self, other): """Check whether this object does not match the other.""" return not isinstance(other, ShaFile) or self.id != other.id def __eq__(self, other): """Return True if the SHAs of the two objects match. """ return isinstance(other, ShaFile) and self.id == other.id def __lt__(self, other): """Return whether SHA of this object is less than the other. """ if not isinstance(other, ShaFile): raise TypeError return self.id < other.id def __le__(self, other): """Check whether SHA of this object is less than or equal to the other. """ if not isinstance(other, ShaFile): raise TypeError return self.id <= other.id def __cmp__(self, other): """Compare the SHA of this object with that of the other object. """ if not isinstance(other, ShaFile): raise TypeError return cmp(self.id, other.id) # noqa: F821 class Blob(ShaFile): """A Git Blob object.""" __slots__ = () type_name = b'blob' type_num = 3 def __init__(self): super(Blob, self).__init__() self._chunked_text = [] self._needs_serialization = False def _get_data(self): return self.as_raw_string() def _set_data(self, data): self.set_raw_string(data) data = property(_get_data, _set_data, "The text contained within the blob object.") def _get_chunked(self): return self._chunked_text def _set_chunked(self, chunks): self._chunked_text = chunks def _serialize(self): return self._chunked_text def _deserialize(self, chunks): self._chunked_text = chunks chunked = property( _get_chunked, _set_chunked, "The text within the blob object, as chunks (not necessarily lines).") @classmethod def from_path(cls, path): blob = ShaFile.from_path(path) if not isinstance(blob, cls): raise NotBlobError(path) return blob def check(self): """Check this object for internal consistency. :raise ObjectFormatException: if the object is malformed in some way """ super(Blob, self).check() def splitlines(self): """Return list of lines in this blob. This preserves the original line endings. """ chunks = self.chunked if not chunks: return [] if len(chunks) == 1: return chunks[0].splitlines(True) remaining = None ret = [] for chunk in chunks: lines = chunk.splitlines(True) if len(lines) > 1: ret.append((remaining or b"") + lines[0]) ret.extend(lines[1:-1]) remaining = lines[-1] elif len(lines) == 1: if remaining is None: remaining = lines.pop() else: remaining += lines.pop() if remaining is not None: ret.append(remaining) return ret def _parse_message(chunks): """Parse a message with a list of fields and a body. :param chunks: the raw chunks of the tag or commit object. :return: iterator of tuples of (field, value), one per header line, in the order read from the text, possibly including duplicates. Includes a field named None for the freeform tag/commit text. """ f = BytesIO(b''.join(chunks)) k = None v = "" eof = False def _strip_last_newline(value): """Strip the last newline from value""" if value and value.endswith(b'\n'): return value[:-1] return value # Parse the headers # # Headers can contain newlines. The next line is indented with a space. # We store the latest key as 'k', and the accumulated value as 'v'. for line in f: if line.startswith(b' '): # Indented continuation of the previous line v += line[1:] else: if k is not None: # We parsed a new header, return its value yield (k, _strip_last_newline(v)) if line == b'\n': # Empty line indicates end of headers break (k, v) = line.split(b' ', 1) else: # We reached end of file before the headers ended. We still need to # return the previous header, then we need to return a None field for # the text. eof = True if k is not None: yield (k, _strip_last_newline(v)) yield (None, None) if not eof: # We didn't reach the end of file while parsing headers. We can return # the rest of the file as a message. yield (None, f.read()) f.close() class Tag(ShaFile): """A Git Tag object.""" type_name = b'tag' type_num = 4 __slots__ = ('_tag_timezone_neg_utc', '_name', '_object_sha', '_object_class', '_tag_time', '_tag_timezone', - '_tagger', '_message') + '_tagger', '_message', '_signature') def __init__(self): super(Tag, self).__init__() self._tagger = None self._tag_time = None self._tag_timezone = None self._tag_timezone_neg_utc = False + self._signature = None @classmethod def from_path(cls, filename): tag = ShaFile.from_path(filename) if not isinstance(tag, cls): raise NotTagError(filename) return tag def check(self): """Check this object for internal consistency. :raise ObjectFormatException: if the object is malformed in some way """ super(Tag, self).check() self._check_has_member("_object_sha", "missing object sha") self._check_has_member("_object_class", "missing object type") self._check_has_member("_name", "missing tag name") if not self._name: raise ObjectFormatException("empty tag name") check_hexsha(self._object_sha, "invalid object sha") if getattr(self, "_tagger", None): check_identity(self._tagger, "invalid tagger") self._check_has_member("_tag_time", "missing tag time") check_time(self._tag_time) last = None for field, _ in _parse_message(self._chunked_text): if field == _OBJECT_HEADER and last is not None: raise ObjectFormatException("unexpected object") elif field == _TYPE_HEADER and last != _OBJECT_HEADER: raise ObjectFormatException("unexpected type") elif field == _TAG_HEADER and last != _TYPE_HEADER: raise ObjectFormatException("unexpected tag name") elif field == _TAGGER_HEADER and last != _TAG_HEADER: raise ObjectFormatException("unexpected tagger") last = field def _serialize(self): chunks = [] chunks.append(git_line(_OBJECT_HEADER, self._object_sha)) chunks.append(git_line(_TYPE_HEADER, self._object_class.type_name)) chunks.append(git_line(_TAG_HEADER, self._name)) if self._tagger: if self._tag_time is None: chunks.append(git_line(_TAGGER_HEADER, self._tagger)) else: chunks.append(git_line( _TAGGER_HEADER, self._tagger, str(self._tag_time).encode('ascii'), format_timezone( self._tag_timezone, self._tag_timezone_neg_utc))) if self._message is not None: chunks.append(b'\n') # To close headers chunks.append(self._message) + if self._signature is not None: + chunks.append(self._signature) return chunks def _deserialize(self, chunks): """Grab the metadata attached to the tag""" self._tagger = None self._tag_time = None self._tag_timezone = None self._tag_timezone_neg_utc = False for field, value in _parse_message(chunks): if field == _OBJECT_HEADER: self._object_sha = value elif field == _TYPE_HEADER: obj_class = object_class(value) if not obj_class: raise ObjectFormatException("Not a known type: %s" % value) self._object_class = obj_class elif field == _TAG_HEADER: self._name = value elif field == _TAGGER_HEADER: (self._tagger, self._tag_time, (self._tag_timezone, self._tag_timezone_neg_utc)) = parse_time_entry(value) elif field is None: - self._message = value + if value is None: + self._message = None + self._signature = None + else: + try: + sig_idx = value.index(BEGIN_PGP_SIGNATURE) + except ValueError: + self._message = value + self._signature = None + else: + self._message = value[:sig_idx] + self._signature = value[sig_idx:] else: raise ObjectFormatException("Unknown field %s" % field) def _get_object(self): """Get the object pointed to by this tag. :return: tuple of (object class, sha). """ return (self._object_class, self._object_sha) def _set_object(self, value): (self._object_class, self._object_sha) = value self._needs_serialization = True object = property(_get_object, _set_object) name = serializable_property("name", "The name of this tag") tagger = serializable_property( "tagger", "Returns the name of the person who created this tag") tag_time = serializable_property( "tag_time", "The creation timestamp of the tag. As the number of seconds " "since the epoch") tag_timezone = serializable_property( "tag_timezone", "The timezone that tag_time is in.") message = serializable_property( - "message", "The message attached to this tag") + "message", "the message attached to this tag") + + signature = serializable_property( + "signature", "Optional detached GPG signature") class TreeEntry(namedtuple('TreeEntry', ['path', 'mode', 'sha'])): """Named tuple encapsulating a single tree entry.""" def in_path(self, path): """Return a copy of this entry with the given path prepended.""" if not isinstance(self.path, bytes): raise TypeError('Expected bytes for path, got %r' % path) return TreeEntry(posixpath.join(path, self.path), self.mode, self.sha) def parse_tree(text, strict=False): """Parse a tree text. :param text: Serialized text to parse :return: iterator of tuples of (name, mode, sha) :raise ObjectFormatException: if the object was malformed in some way """ count = 0 length = len(text) while count < length: mode_end = text.index(b' ', count) mode_text = text[count:mode_end] if strict and mode_text.startswith(b'0'): raise ObjectFormatException("Invalid mode '%s'" % mode_text) try: mode = int(mode_text, 8) except ValueError: raise ObjectFormatException("Invalid mode '%s'" % mode_text) name_end = text.index(b'\0', mode_end) name = text[mode_end+1:name_end] count = name_end+21 sha = text[name_end+1:count] if len(sha) != 20: raise ObjectFormatException("Sha has invalid length") hexsha = sha_to_hex(sha) yield (name, mode, hexsha) def serialize_tree(items): """Serialize the items in a tree to a text. :param items: Sorted iterable over (name, mode, sha) tuples :return: Serialized tree text as chunks """ for name, mode, hexsha in items: yield (("%04o" % mode).encode('ascii') + b' ' + name + b'\0' + hex_to_sha(hexsha)) def sorted_tree_items(entries, name_order): """Iterate over a tree entries dictionary. :param name_order: If True, iterate entries in order of their name. If False, iterate entries in tree order, that is, treat subtree entries as having '/' appended. :param entries: Dictionary mapping names to (mode, sha) tuples :return: Iterator over (name, mode, hexsha) """ key_func = name_order and key_entry_name_order or key_entry for name, entry in sorted(entries.items(), key=key_func): mode, hexsha = entry # Stricter type checks than normal to mirror checks in the C version. mode = int(mode) if not isinstance(hexsha, bytes): raise TypeError('Expected bytes for SHA, got %r' % hexsha) yield TreeEntry(name, mode, hexsha) def key_entry(entry): """Sort key for tree entry. :param entry: (name, value) tuplee """ (name, value) = entry if stat.S_ISDIR(value[0]): name += b'/' return name def key_entry_name_order(entry): """Sort key for tree entry in name order.""" return entry[0] def pretty_format_tree_entry(name, mode, hexsha, encoding="utf-8"): """Pretty format tree entry. :param name: Name of the directory entry :param mode: Mode of entry :param hexsha: Hexsha of the referenced object :return: string describing the tree entry """ if mode & stat.S_IFDIR: kind = "tree" else: kind = "blob" return "%04o %s %s\t%s\n" % ( mode, kind, hexsha.decode('ascii'), name.decode(encoding, 'replace')) class Tree(ShaFile): """A Git tree object""" type_name = b'tree' type_num = 2 __slots__ = ('_entries') def __init__(self): super(Tree, self).__init__() self._entries = {} @classmethod def from_path(cls, filename): tree = ShaFile.from_path(filename) if not isinstance(tree, cls): raise NotTreeError(filename) return tree def __contains__(self, name): return name in self._entries def __getitem__(self, name): return self._entries[name] def __setitem__(self, name, value): """Set a tree entry by name. :param name: The name of the entry, as a string. :param value: A tuple of (mode, hexsha), where mode is the mode of the entry as an integral type and hexsha is the hex SHA of the entry as a string. """ mode, hexsha = value self._entries[name] = (mode, hexsha) self._needs_serialization = True def __delitem__(self, name): del self._entries[name] self._needs_serialization = True def __len__(self): return len(self._entries) def __iter__(self): return iter(self._entries) def add(self, name, mode, hexsha): """Add an entry to the tree. :param mode: The mode of the entry as an integral type. Not all possible modes are supported by git; see check() for details. :param name: The name of the entry, as a string. :param hexsha: The hex SHA of the entry as a string. """ if isinstance(name, int) and isinstance(mode, bytes): (name, mode) = (mode, name) warnings.warn( "Please use Tree.add(name, mode, hexsha)", category=DeprecationWarning, stacklevel=2) self._entries[name] = mode, hexsha self._needs_serialization = True def iteritems(self, name_order=False): """Iterate over entries. :param name_order: If True, iterate in name order instead of tree order. :return: Iterator over (name, mode, sha) tuples """ return sorted_tree_items(self._entries, name_order) def items(self): """Return the sorted entries in this tree. :return: List with (name, mode, sha) tuples """ return list(self.iteritems()) def _deserialize(self, chunks): """Grab the entries in the tree""" try: parsed_entries = parse_tree(b''.join(chunks)) except ValueError as e: raise ObjectFormatException(e) # TODO: list comprehension is for efficiency in the common (small) # case; if memory efficiency in the large case is a concern, use a # genexp. self._entries = dict([(n, (m, s)) for n, m, s in parsed_entries]) def check(self): """Check this object for internal consistency. :raise ObjectFormatException: if the object is malformed in some way """ super(Tree, self).check() last = None allowed_modes = (stat.S_IFREG | 0o755, stat.S_IFREG | 0o644, stat.S_IFLNK, stat.S_IFDIR, S_IFGITLINK, # TODO: optionally exclude as in git fsck --strict stat.S_IFREG | 0o664) for name, mode, sha in parse_tree(b''.join(self._chunked_text), True): check_hexsha(sha, 'invalid sha %s' % sha) if b'/' in name or name in (b'', b'.', b'..', b'.git'): raise ObjectFormatException( 'invalid name %s' % name.decode('utf-8', 'replace')) if mode not in allowed_modes: raise ObjectFormatException('invalid mode %06o' % mode) entry = (name, (mode, sha)) if last: if key_entry(last) > key_entry(entry): raise ObjectFormatException('entries not sorted') if name == last[0]: raise ObjectFormatException('duplicate entry %s' % name) last = entry def _serialize(self): return list(serialize_tree(self.iteritems())) def as_pretty_string(self): text = [] for name, mode, hexsha in self.iteritems(): text.append(pretty_format_tree_entry(name, mode, hexsha)) return "".join(text) def lookup_path(self, lookup_obj, path): """Look up an object in a Git tree. :param lookup_obj: Callback for retrieving object by SHA1 :param path: Path to lookup :return: A tuple of (mode, SHA) of the resulting path. """ parts = path.split(b'/') sha = self.id mode = None for p in parts: if not p: continue obj = lookup_obj(sha) if not isinstance(obj, Tree): raise NotTreeError(sha) mode, sha = obj[p] return mode, sha def parse_timezone(text): """Parse a timezone text fragment (e.g. '+0100'). :param text: Text to parse. :return: Tuple with timezone as seconds difference to UTC and a boolean indicating whether this was a UTC timezone prefixed with a negative sign (-0000). """ # cgit parses the first character as the sign, and the rest # as an integer (using strtol), which could also be negative. # We do the same for compatibility. See #697828. if not text[0] in b'+-': raise ValueError("Timezone must start with + or - (%(text)s)" % vars()) sign = text[:1] offset = int(text[1:]) if sign == b'-': offset = -offset unnecessary_negative_timezone = (offset >= 0 and sign == b'-') signum = (offset < 0) and -1 or 1 offset = abs(offset) hours = int(offset / 100) minutes = (offset % 100) return (signum * (hours * 3600 + minutes * 60), unnecessary_negative_timezone) def format_timezone(offset, unnecessary_negative_timezone=False): """Format a timezone for Git serialization. :param offset: Timezone offset as seconds difference to UTC :param unnecessary_negative_timezone: Whether to use a minus sign for UTC or positive timezones (-0000 and --700 rather than +0000 / +0700). """ if offset % 60 != 0: raise ValueError("Unable to handle non-minute offset.") if offset < 0 or unnecessary_negative_timezone: sign = '-' offset = -offset else: sign = '+' return ('%c%02d%02d' % (sign, offset / 3600, (offset / 60) % 60)).encode('ascii') def parse_time_entry(value): """Parse time entry behavior :param value: Bytes representing a git commit/tag line :raise: ObjectFormatException in case of parsing error (malformed field date) :return: Tuple of (author, time, (timezone, timezone_neg_utc)) """ try: sep = value.rindex(b'> ') except ValueError: return (value, None, (None, False)) try: person = value[0:sep+1] rest = value[sep+2:] timetext, timezonetext = rest.rsplit(b' ', 1) time = int(timetext) timezone, timezone_neg_utc = parse_timezone(timezonetext) except ValueError as e: raise ObjectFormatException(e) return person, time, (timezone, timezone_neg_utc) def parse_commit(chunks): """Parse a commit object from chunks. :param chunks: Chunks to parse :return: Tuple of (tree, parents, author_info, commit_info, encoding, mergetag, gpgsig, message, extra) """ parents = [] extra = [] tree = None author_info = (None, None, (None, None)) commit_info = (None, None, (None, None)) encoding = None mergetag = [] message = None gpgsig = None for field, value in _parse_message(chunks): # TODO(jelmer): Enforce ordering if field == _TREE_HEADER: tree = value elif field == _PARENT_HEADER: parents.append(value) elif field == _AUTHOR_HEADER: author_info = parse_time_entry(value) elif field == _COMMITTER_HEADER: commit_info = parse_time_entry(value) elif field == _ENCODING_HEADER: encoding = value elif field == _MERGETAG_HEADER: mergetag.append(Tag.from_string(value + b'\n')) elif field == _GPGSIG_HEADER: gpgsig = value elif field is None: message = value else: extra.append((field, value)) return (tree, parents, author_info, commit_info, encoding, mergetag, gpgsig, message, extra) class Commit(ShaFile): """A git commit object""" type_name = b'commit' type_num = 1 __slots__ = ('_parents', '_encoding', '_extra', '_author_timezone_neg_utc', '_commit_timezone_neg_utc', '_commit_time', '_author_time', '_author_timezone', '_commit_timezone', '_author', '_committer', '_tree', '_message', '_mergetag', '_gpgsig') def __init__(self): super(Commit, self).__init__() self._parents = [] self._encoding = None self._mergetag = [] self._gpgsig = None self._extra = [] self._author_timezone_neg_utc = False self._commit_timezone_neg_utc = False @classmethod def from_path(cls, path): commit = ShaFile.from_path(path) if not isinstance(commit, cls): raise NotCommitError(path) return commit def _deserialize(self, chunks): (self._tree, self._parents, author_info, commit_info, self._encoding, self._mergetag, self._gpgsig, self._message, self._extra) = ( parse_commit(chunks)) (self._author, self._author_time, (self._author_timezone, self._author_timezone_neg_utc)) = author_info (self._committer, self._commit_time, (self._commit_timezone, self._commit_timezone_neg_utc)) = commit_info def check(self): """Check this object for internal consistency. :raise ObjectFormatException: if the object is malformed in some way """ super(Commit, self).check() self._check_has_member("_tree", "missing tree") self._check_has_member("_author", "missing author") self._check_has_member("_committer", "missing committer") self._check_has_member("_author_time", "missing author time") self._check_has_member("_commit_time", "missing commit time") for parent in self._parents: check_hexsha(parent, "invalid parent sha") check_hexsha(self._tree, "invalid tree sha") check_identity(self._author, "invalid author") check_identity(self._committer, "invalid committer") check_time(self._author_time) check_time(self._commit_time) last = None for field, _ in _parse_message(self._chunked_text): if field == _TREE_HEADER and last is not None: raise ObjectFormatException("unexpected tree") elif field == _PARENT_HEADER and last not in (_PARENT_HEADER, _TREE_HEADER): raise ObjectFormatException("unexpected parent") elif field == _AUTHOR_HEADER and last not in (_TREE_HEADER, _PARENT_HEADER): raise ObjectFormatException("unexpected author") elif field == _COMMITTER_HEADER and last != _AUTHOR_HEADER: raise ObjectFormatException("unexpected committer") elif field == _ENCODING_HEADER and last != _COMMITTER_HEADER: raise ObjectFormatException("unexpected encoding") last = field # TODO: optionally check for duplicate parents def _serialize(self): chunks = [] tree_bytes = ( self._tree.id if isinstance(self._tree, Tree) else self._tree) chunks.append(git_line(_TREE_HEADER, tree_bytes)) for p in self._parents: chunks.append(git_line(_PARENT_HEADER, p)) chunks.append(git_line( _AUTHOR_HEADER, self._author, str(self._author_time).encode('ascii'), format_timezone( self._author_timezone, self._author_timezone_neg_utc))) chunks.append(git_line( _COMMITTER_HEADER, self._committer, str(self._commit_time).encode('ascii'), format_timezone(self._commit_timezone, self._commit_timezone_neg_utc))) if self.encoding: chunks.append(git_line(_ENCODING_HEADER, self.encoding)) for mergetag in self.mergetag: mergetag_chunks = mergetag.as_raw_string().split(b'\n') chunks.append(git_line(_MERGETAG_HEADER, mergetag_chunks[0])) # Embedded extra header needs leading space for chunk in mergetag_chunks[1:]: chunks.append(b' ' + chunk + b'\n') # No trailing empty line if chunks[-1].endswith(b' \n'): chunks[-1] = chunks[-1][:-2] for k, v in self.extra: if b'\n' in k or b'\n' in v: raise AssertionError( "newline in extra data: %r -> %r" % (k, v)) chunks.append(git_line(k, v)) if self.gpgsig: sig_chunks = self.gpgsig.split(b'\n') chunks.append(git_line(_GPGSIG_HEADER, sig_chunks[0])) for chunk in sig_chunks[1:]: chunks.append(git_line(b'', chunk)) chunks.append(b'\n') # There must be a new line after the headers chunks.append(self._message) return chunks tree = serializable_property( "tree", "Tree that is the state of this commit") def _get_parents(self): """Return a list of parents of this commit.""" return self._parents def _set_parents(self, value): """Set a list of parents of this commit.""" self._needs_serialization = True self._parents = value parents = property(_get_parents, _set_parents, doc="Parents of this commit, by their SHA1.") def _get_extra(self): """Return extra settings of this commit.""" return self._extra extra = property( _get_extra, doc="Extra header fields not understood (presumably added in a " "newer version of git). Kept verbatim so the object can " "be correctly reserialized. For private commit metadata, use " "pseudo-headers in Commit.message, rather than this field.") author = serializable_property( "author", "The name of the author of the commit") committer = serializable_property( "committer", "The name of the committer of the commit") message = serializable_property( "message", "The commit message") commit_time = serializable_property( "commit_time", "The timestamp of the commit. As the number of seconds since the " "epoch.") commit_timezone = serializable_property( "commit_timezone", "The zone the commit time is in") author_time = serializable_property( "author_time", "The timestamp the commit was written. As the number of " "seconds since the epoch.") author_timezone = serializable_property( "author_timezone", "Returns the zone the author time is in.") encoding = serializable_property( "encoding", "Encoding of the commit message.") mergetag = serializable_property( "mergetag", "Associated signed tag.") gpgsig = serializable_property( "gpgsig", "GPG Signature.") OBJECT_CLASSES = ( Commit, Tree, Blob, Tag, ) _TYPE_MAP = {} for cls in OBJECT_CLASSES: _TYPE_MAP[cls.type_name] = cls _TYPE_MAP[cls.type_num] = cls # Hold on to the pure-python implementations for testing _parse_tree_py = parse_tree _sorted_tree_items_py = sorted_tree_items try: # Try to import C versions from dulwich._objects import parse_tree, sorted_tree_items except ImportError: pass diff --git a/dulwich/porcelain.py b/dulwich/porcelain.py index 187b9863..228a664f 100644 --- a/dulwich/porcelain.py +++ b/dulwich/porcelain.py @@ -1,1423 +1,1429 @@ # porcelain.py -- Porcelain-like layer on top of Dulwich # Copyright (C) 2013 Jelmer Vernooij # # 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 # for a copy of the GNU General Public License # and for a copy of the Apache # License, Version 2.0. # """Simple wrapper that provides porcelain-like functions on top of Dulwich. Currently implemented: * archive * add * branch{_create,_delete,_list} * check-ignore * checkout * clone * commit * commit-tree * daemon * describe * diff-tree * fetch * init * ls-files * ls-remote * ls-tree * pull * push * rm * remote{_add} * receive-pack * reset * rev-list * tag{_create,_delete,_list} * upload-pack * update-server-info * status * symbolic-ref These functions are meant to behave similarly to the git subcommands. Differences in behaviour are considered bugs. Functions should generally accept both unicode strings and bytestrings """ from collections import namedtuple from contextlib import ( closing, contextmanager, ) from io import BytesIO, RawIOBase import datetime import os import posixpath import stat import sys import time from dulwich.archive import ( tar_stream, ) from dulwich.client import ( get_transport_and_path, ) from dulwich.config import ( StackedConfig, ) from dulwich.diff_tree import ( CHANGE_ADD, CHANGE_DELETE, CHANGE_MODIFY, CHANGE_RENAME, CHANGE_COPY, RENAME_CHANGE_TYPES, ) from dulwich.errors import ( SendPackError, UpdateRefsError, ) from dulwich.ignore import IgnoreFilterManager from dulwich.index import ( blob_from_path_and_stat, get_unstaged_changes, ) from dulwich.object_store import ( tree_lookup_path, ) from dulwich.objects import ( Commit, Tag, format_timezone, parse_timezone, pretty_format_tree_entry, ) from dulwich.objectspec import ( parse_commit, parse_object, parse_ref, parse_reftuples, parse_tree, ) from dulwich.pack import ( write_pack_index, write_pack_objects, ) from dulwich.patch import write_tree_diff from dulwich.protocol import ( Protocol, ZERO_SHA, ) from dulwich.refs import ( ANNOTATED_TAG_SUFFIX, strip_peeled_refs, ) from dulwich.repo import (BaseRepo, Repo) from dulwich.server import ( FileSystemBackend, TCPGitServer, ReceivePackHandler, UploadPackHandler, update_server_info as server_update_server_info, ) # Module level tuple definition for status output GitStatus = namedtuple('GitStatus', 'staged unstaged untracked') class NoneStream(RawIOBase): """Fallback if stdout or stderr are unavailable, does nothing.""" def read(self, size=-1): return None def readall(self): return None def readinto(self, b): return None def write(self, b): return None default_bytes_out_stream = getattr( sys.stdout, 'buffer', sys.stdout ) or NoneStream() default_bytes_err_stream = getattr( sys.stderr, 'buffer', sys.stderr ) or NoneStream() DEFAULT_ENCODING = 'utf-8' class RemoteExists(Exception): """Raised when the remote already exists.""" def open_repo(path_or_repo): """Open an argument that can be a repository or a path for a repository.""" if isinstance(path_or_repo, BaseRepo): return path_or_repo return Repo(path_or_repo) @contextmanager def _noop_context_manager(obj): """Context manager that has the same api as closing but does nothing.""" yield obj def open_repo_closing(path_or_repo): """Open an argument that can be a repository or a path for a repository. returns a context manager that will close the repo on exit if the argument is a path, else does nothing if the argument is a repo. """ if isinstance(path_or_repo, BaseRepo): return _noop_context_manager(path_or_repo) return closing(Repo(path_or_repo)) def path_to_tree_path(repopath, path): """Convert a path to a path usable in an index, e.g. bytes and relative to the repository root. :param repopath: Repository path, absolute or relative to the cwd :param path: A path, absolute or relative to the cwd :return: A path formatted for use in e.g. an index """ if not isinstance(path, bytes): path = path.encode(sys.getfilesystemencoding()) if not isinstance(repopath, bytes): repopath = repopath.encode(sys.getfilesystemencoding()) treepath = os.path.relpath(path, repopath) if treepath.startswith(b'..'): raise ValueError('Path not in repo') if os.path.sep != '/': treepath = treepath.replace(os.path.sep.encode('ascii'), b'/') return treepath def archive(repo, committish=None, outstream=default_bytes_out_stream, errstream=default_bytes_err_stream): """Create an archive. :param repo: Path of repository for which to generate an archive. :param committish: Commit SHA1 or ref to use :param outstream: Output stream (defaults to stdout) :param errstream: Error stream (defaults to stderr) """ if committish is None: committish = "HEAD" with open_repo_closing(repo) as repo_obj: c = repo_obj[committish] for chunk in tar_stream( repo_obj.object_store, repo_obj.object_store[c.tree], c.commit_time): outstream.write(chunk) def update_server_info(repo="."): """Update server info files for a repository. :param repo: path to the repository """ with open_repo_closing(repo) as r: server_update_server_info(r) def symbolic_ref(repo, ref_name, force=False): """Set git symbolic ref into HEAD. :param repo: path to the repository :param ref_name: short name of the new ref :param force: force settings without checking if it exists in refs/heads """ with open_repo_closing(repo) as repo_obj: ref_path = _make_branch_ref(ref_name) if not force and ref_path not in repo_obj.refs.keys(): raise ValueError('fatal: ref `%s` is not a ref' % ref_name) repo_obj.refs.set_symbolic_ref(b'HEAD', ref_path) def commit(repo=".", message=None, author=None, committer=None, encoding=None): """Create a new commit. :param repo: Path to repository :param message: Optional commit message :param author: Optional author name and email :param committer: Optional committer name and email :return: SHA1 of the new commit """ # FIXME: Support --all argument # FIXME: Support --signoff argument if getattr(message, 'encode', None): message = message.encode(encoding or DEFAULT_ENCODING) if getattr(author, 'encode', None): author = author.encode(encoding or DEFAULT_ENCODING) if getattr(committer, 'encode', None): committer = committer.encode(encoding or DEFAULT_ENCODING) with open_repo_closing(repo) as r: return r.do_commit( message=message, author=author, committer=committer, encoding=encoding) def commit_tree(repo, tree, message=None, author=None, committer=None): """Create a new commit object. :param repo: Path to repository :param tree: An existing tree object :param author: Optional author name and email :param committer: Optional committer name and email """ with open_repo_closing(repo) as r: return r.do_commit( message=message, tree=tree, committer=committer, author=author) def init(path=".", bare=False): """Create a new git repository. :param path: Path to repository. :param bare: Whether to create a bare repository. :return: A Repo instance """ if not os.path.exists(path): os.mkdir(path) if bare: return Repo.init_bare(path) else: return Repo.init(path) def clone(source, target=None, bare=False, checkout=None, errstream=default_bytes_err_stream, outstream=None, origin=b"origin", depth=None, **kwargs): """Clone a local or remote git repository. :param source: Path or URL for source repository :param target: Path to target repository (optional) :param bare: Whether or not to create a bare repository :param checkout: Whether or not to check-out HEAD after cloning :param errstream: Optional stream to write progress to :param outstream: Optional stream to write progress to (deprecated) :param origin: Name of remote from the repository used to clone :param depth: Depth to fetch at :return: The new repository """ # TODO(jelmer): This code overlaps quite a bit with Repo.clone if outstream is not None: import warnings warnings.warn( "outstream= has been deprecated in favour of errstream=.", DeprecationWarning, stacklevel=3) errstream = outstream if checkout is None: checkout = (not bare) if checkout and bare: raise ValueError("checkout and bare are incompatible") if target is None: target = source.split("/")[-1] if not os.path.exists(target): os.mkdir(target) if bare: r = Repo.init_bare(target) else: r = Repo.init(target) reflog_message = b'clone: from ' + source.encode('utf-8') try: fetch_result = fetch( r, source, origin, errstream=errstream, message=reflog_message, depth=depth, **kwargs) target_config = r.get_config() if not isinstance(source, bytes): source = source.encode(DEFAULT_ENCODING) target_config.set((b'remote', origin), b'url', source) target_config.set( (b'remote', origin), b'fetch', b'+refs/heads/*:refs/remotes/' + origin + b'/*') target_config.write_to_path() # TODO(jelmer): Support symref capability, # https://github.com/jelmer/dulwich/issues/485 try: head = r[fetch_result[b'HEAD']] except KeyError: head = None else: r[b'HEAD'] = head.id if checkout and not bare and head is not None: errstream.write(b'Checking out ' + head.id + b'\n') r.reset_index(head.tree) except BaseException: r.close() raise return r def add(repo=".", paths=None): """Add files to the staging area. :param repo: Repository for the files :param paths: Paths to add. No value passed stages all modified files. :return: Tuple with set of added files and ignored files """ ignored = set() with open_repo_closing(repo) as r: ignore_manager = IgnoreFilterManager.from_repo(r) if not paths: paths = list( get_untracked_paths(os.getcwd(), r.path, r.open_index())) relpaths = [] if not isinstance(paths, list): paths = [paths] for p in paths: relpath = os.path.relpath(p, r.path) if relpath.startswith('..' + os.path.sep): raise ValueError('path %r is not in repo' % relpath) # FIXME: Support patterns, directories. if ignore_manager.is_ignored(relpath): ignored.add(relpath) continue relpaths.append(relpath) r.stage(relpaths) return (relpaths, ignored) def remove(repo=".", paths=None, cached=False): """Remove files from the staging area. :param repo: Repository for the files :param paths: Paths to remove """ with open_repo_closing(repo) as r: index = r.open_index() for p in paths: full_path = os.path.abspath(p).encode(sys.getfilesystemencoding()) tree_path = path_to_tree_path(r.path, p) try: index_sha = index[tree_path].sha except KeyError: raise Exception('%s did not match any files' % p) if not cached: try: st = os.lstat(full_path) except OSError: pass else: try: blob = blob_from_path_and_stat(full_path, st) except IOError: pass else: try: committed_sha = tree_lookup_path( r.__getitem__, r[r.head()].tree, tree_path)[1] except KeyError: committed_sha = None if blob.id != index_sha and index_sha != committed_sha: raise Exception( 'file has staged content differing ' 'from both the file and head: %s' % p) if index_sha != committed_sha: raise Exception( 'file has staged changes: %s' % p) os.remove(full_path) del index[tree_path] index.write() rm = remove def commit_decode(commit, contents, default_encoding=DEFAULT_ENCODING): if commit.encoding is not None: return contents.decode(commit.encoding, "replace") return contents.decode(default_encoding, "replace") def print_commit(commit, decode, outstream=sys.stdout): """Write a human-readable commit log entry. :param commit: A `Commit` object :param outstream: A stream file to write to """ outstream.write("-" * 50 + "\n") outstream.write("commit: " + commit.id.decode('ascii') + "\n") if len(commit.parents) > 1: outstream.write( "merge: " + "...".join([c.decode('ascii') for c in commit.parents[1:]]) + "\n") outstream.write("Author: " + decode(commit.author) + "\n") if commit.author != commit.committer: outstream.write("Committer: " + decode(commit.committer) + "\n") time_tuple = time.gmtime(commit.author_time + commit.author_timezone) time_str = time.strftime("%a %b %d %Y %H:%M:%S", time_tuple) timezone_str = format_timezone(commit.author_timezone).decode('ascii') outstream.write("Date: " + time_str + " " + timezone_str + "\n") outstream.write("\n") outstream.write(decode(commit.message) + "\n") outstream.write("\n") def print_tag(tag, decode, outstream=sys.stdout): """Write a human-readable tag. :param tag: A `Tag` object :param decode: Function for decoding bytes to unicode string :param outstream: A stream to write to """ outstream.write("Tagger: " + decode(tag.tagger) + "\n") outstream.write("Date: " + decode(tag.tag_time) + "\n") outstream.write("\n") outstream.write(decode(tag.message) + "\n") outstream.write("\n") def show_blob(repo, blob, decode, outstream=sys.stdout): """Write a blob to a stream. :param repo: A `Repo` object :param blob: A `Blob` object :param decode: Function for decoding bytes to unicode string :param outstream: A stream file to write to """ outstream.write(decode(blob.data)) def show_commit(repo, commit, decode, outstream=sys.stdout): """Show a commit to a stream. :param repo: A `Repo` object :param commit: A `Commit` object :param decode: Function for decoding bytes to unicode string :param outstream: Stream to write to """ print_commit(commit, decode=decode, outstream=outstream) if commit.parents: parent_commit = repo[commit.parents[0]] base_tree = parent_commit.tree else: base_tree = None diffstream = BytesIO() write_tree_diff( diffstream, repo.object_store, base_tree, commit.tree) diffstream.seek(0) outstream.write( diffstream.getvalue().decode( commit.encoding or DEFAULT_ENCODING, 'replace')) def show_tree(repo, tree, decode, outstream=sys.stdout): """Print a tree to a stream. :param repo: A `Repo` object :param tree: A `Tree` object :param decode: Function for decoding bytes to unicode string :param outstream: Stream to write to """ for n in tree: outstream.write(decode(n) + "\n") def show_tag(repo, tag, decode, outstream=sys.stdout): """Print a tag to a stream. :param repo: A `Repo` object :param tag: A `Tag` object :param decode: Function for decoding bytes to unicode string :param outstream: Stream to write to """ print_tag(tag, decode, outstream) show_object(repo, repo[tag.object[1]], outstream) def show_object(repo, obj, decode, outstream): return { b"tree": show_tree, b"blob": show_blob, b"commit": show_commit, b"tag": show_tag, }[obj.type_name](repo, obj, decode, outstream) def print_name_status(changes): """Print a simple status summary, listing changed files. """ for change in changes: if not change: continue if isinstance(change, list): change = change[0] if change.type == CHANGE_ADD: path1 = change.new.path path2 = '' kind = 'A' elif change.type == CHANGE_DELETE: path1 = change.old.path path2 = '' kind = 'D' elif change.type == CHANGE_MODIFY: path1 = change.new.path path2 = '' kind = 'M' elif change.type in RENAME_CHANGE_TYPES: path1 = change.old.path path2 = change.new.path if change.type == CHANGE_RENAME: kind = 'R' elif change.type == CHANGE_COPY: kind = 'C' yield '%-8s%-20s%-20s' % (kind, path1, path2) def log(repo=".", paths=None, outstream=sys.stdout, max_entries=None, reverse=False, name_status=False): """Write commit logs. :param repo: Path to repository :param paths: Optional set of specific paths to print entries for :param outstream: Stream to write log output to :param reverse: Reverse order in which entries are printed :param name_status: Print name status :param max_entries: Optional maximum number of entries to display """ with open_repo_closing(repo) as r: walker = r.get_walker( max_entries=max_entries, paths=paths, reverse=reverse) for entry in walker: def decode(x): return commit_decode(entry.commit, x) print_commit(entry.commit, decode, outstream) if name_status: outstream.writelines( [l+'\n' for l in print_name_status(entry.changes())]) # TODO(jelmer): better default for encoding? def show(repo=".", objects=None, outstream=sys.stdout, default_encoding=DEFAULT_ENCODING): """Print the changes in a commit. :param repo: Path to repository :param objects: Objects to show (defaults to [HEAD]) :param outstream: Stream to write to :param default_encoding: Default encoding to use if none is set in the commit """ if objects is None: objects = ["HEAD"] if not isinstance(objects, list): objects = [objects] with open_repo_closing(repo) as r: for objectish in objects: o = parse_object(r, objectish) if isinstance(o, Commit): def decode(x): return commit_decode(o, x, default_encoding) else: def decode(x): return x.decode(default_encoding) show_object(r, o, decode, outstream) def diff_tree(repo, old_tree, new_tree, outstream=sys.stdout): """Compares the content and mode of blobs found via two tree objects. :param repo: Path to repository :param old_tree: Id of old tree :param new_tree: Id of new tree :param outstream: Stream to write to """ with open_repo_closing(repo) as r: write_tree_diff(outstream, r.object_store, old_tree, new_tree) def rev_list(repo, commits, outstream=sys.stdout): """Lists commit objects in reverse chronological order. :param repo: Path to repository :param commits: Commits over which to iterate :param outstream: Stream to write to """ with open_repo_closing(repo) as r: for entry in r.get_walker(include=[r[c].id for c in commits]): outstream.write(entry.commit.id + b"\n") def tag(*args, **kwargs): import warnings warnings.warn("tag has been deprecated in favour of tag_create.", DeprecationWarning) return tag_create(*args, **kwargs) def tag_create( repo, tag, author=None, message=None, annotated=False, - objectish="HEAD", tag_time=None, tag_timezone=None): + objectish="HEAD", tag_time=None, tag_timezone=None, + sign=False): """Creates a tag in git via dulwich calls: :param repo: Path to repository :param tag: tag string :param author: tag author (optional, if annotated is set) :param message: tag message (optional) :param annotated: whether to create an annotated tag :param objectish: object the tag should point at, defaults to HEAD :param tag_time: Optional time for annotated tag :param tag_timezone: Optional timezone for annotated tag + :param sign: GPG Sign the tag """ with open_repo_closing(repo) as r: object = parse_object(r, objectish) if annotated: # Create the tag object tag_obj = Tag() if author is None: # TODO(jelmer): Don't use repo private method. - author = r._get_user_identity() + author = r._get_user_identity(r.get_config_stack()) tag_obj.tagger = author tag_obj.message = message tag_obj.name = tag tag_obj.object = (type(object), object.id) if tag_time is None: tag_time = int(time.time()) tag_obj.tag_time = tag_time if tag_timezone is None: # TODO(jelmer) Use current user timezone rather than UTC tag_timezone = 0 elif isinstance(tag_timezone, str): tag_timezone = parse_timezone(tag_timezone) tag_obj.tag_timezone = tag_timezone + if sign: + import gpg + with gpg.Context(armor=True) as c: + tag_obj.signature, result = c.sign(tag_obj.as_raw_string()) r.object_store.add_object(tag_obj) tag_id = tag_obj.id else: tag_id = object.id r.refs[_make_tag_ref(tag)] = tag_id def list_tags(*args, **kwargs): import warnings warnings.warn("list_tags has been deprecated in favour of tag_list.", DeprecationWarning) return tag_list(*args, **kwargs) def tag_list(repo, outstream=sys.stdout): """List all tags. :param repo: Path to repository :param outstream: Stream to write tags to """ with open_repo_closing(repo) as r: tags = sorted(r.refs.as_dict(b"refs/tags")) return tags def tag_delete(repo, name): """Remove a tag. :param repo: Path to repository :param name: Name of tag to remove """ with open_repo_closing(repo) as r: if isinstance(name, bytes): names = [name] elif isinstance(name, list): names = name else: raise TypeError("Unexpected tag name type %r" % name) for name in names: del r.refs[_make_tag_ref(name)] def reset(repo, mode, treeish="HEAD"): """Reset current HEAD to the specified state. :param repo: Path to repository :param mode: Mode ("hard", "soft", "mixed") :param treeish: Treeish to reset to """ if mode != "hard": raise ValueError("hard is the only mode currently supported") with open_repo_closing(repo) as r: tree = parse_tree(r, treeish) r.reset_index(tree.id) def push(repo, remote_location, refspecs, outstream=default_bytes_out_stream, errstream=default_bytes_err_stream, **kwargs): """Remote push with dulwich via dulwich.client :param repo: Path to repository :param remote_location: Location of the remote :param refspecs: Refs to push to remote :param outstream: A stream file to write output :param errstream: A stream file to write errors """ # Open the repo with open_repo_closing(repo) as r: # Get the client and path client, path = get_transport_and_path( remote_location, config=r.get_config_stack(), **kwargs) selected_refs = [] def update_refs(refs): selected_refs.extend(parse_reftuples(r.refs, refs, refspecs)) new_refs = {} # TODO: Handle selected_refs == {None: None} for (lh, rh, force) in selected_refs: if lh is None: new_refs[rh] = ZERO_SHA else: new_refs[rh] = r.refs[lh] return new_refs err_encoding = getattr(errstream, 'encoding', None) or DEFAULT_ENCODING remote_location_bytes = client.get_url(path).encode(err_encoding) try: client.send_pack( path, update_refs, generate_pack_data=r.object_store.generate_pack_data, progress=errstream.write) errstream.write( b"Push to " + remote_location_bytes + b" successful.\n") except (UpdateRefsError, SendPackError) as e: errstream.write(b"Push to " + remote_location_bytes + b" failed -> " + e.message.encode(err_encoding) + b"\n") def pull(repo, remote_location=None, refspecs=None, outstream=default_bytes_out_stream, errstream=default_bytes_err_stream, **kwargs): """Pull from remote via dulwich.client :param repo: Path to repository :param remote_location: Location of the remote :param refspec: refspecs to fetch :param outstream: A stream file to write to output :param errstream: A stream file to write to errors """ # Open the repo with open_repo_closing(repo) as r: if remote_location is None: # TODO(jelmer): Lookup 'remote' for current branch in config raise NotImplementedError( "looking up remote from branch config not supported yet") if refspecs is None: refspecs = [b"HEAD"] selected_refs = [] def determine_wants(remote_refs): selected_refs.extend( parse_reftuples(remote_refs, r.refs, refspecs)) return [remote_refs[lh] for (lh, rh, force) in selected_refs] client, path = get_transport_and_path( remote_location, config=r.get_config_stack(), **kwargs) fetch_result = client.fetch( path, r, progress=errstream.write, determine_wants=determine_wants) for (lh, rh, force) in selected_refs: r.refs[rh] = fetch_result.refs[lh] if selected_refs: r[b'HEAD'] = fetch_result.refs[selected_refs[0][1]] # Perform 'git checkout .' - syncs staged changes tree = r[b"HEAD"].tree r.reset_index(tree=tree) def status(repo=".", ignored=False): """Returns staged, unstaged, and untracked changes relative to the HEAD. :param repo: Path to repository or repository object :param ignored: Whether to include ignored files in `untracked` :return: GitStatus tuple, staged - list of staged paths (diff index/HEAD) unstaged - list of unstaged paths (diff index/working-tree) untracked - list of untracked, un-ignored & non-.git paths """ with open_repo_closing(repo) as r: # 1. Get status of staged tracked_changes = get_tree_changes(r) # 2. Get status of unstaged index = r.open_index() unstaged_changes = list(get_unstaged_changes(index, r.path)) ignore_manager = IgnoreFilterManager.from_repo(r) untracked_paths = get_untracked_paths(r.path, r.path, index) if ignored: untracked_changes = list(untracked_paths) else: untracked_changes = [ p for p in untracked_paths if not ignore_manager.is_ignored(p)] return GitStatus(tracked_changes, unstaged_changes, untracked_changes) def get_untracked_paths(frompath, basepath, index): """Get untracked paths. ;param frompath: Path to walk :param basepath: Path to compare to :param index: Index to check against """ # If nothing is specified, add all non-ignored files. for dirpath, dirnames, filenames in os.walk(frompath): # Skip .git and below. if '.git' in dirnames: dirnames.remove('.git') if dirpath != basepath: continue if '.git' in filenames: filenames.remove('.git') if dirpath != basepath: continue for filename in filenames: ap = os.path.join(dirpath, filename) ip = path_to_tree_path(basepath, ap) if ip not in index: yield os.path.relpath(ap, frompath) def get_tree_changes(repo): """Return add/delete/modify changes to tree by comparing index to HEAD. :param repo: repo path or object :return: dict with lists for each type of change """ with open_repo_closing(repo) as r: index = r.open_index() # Compares the Index to the HEAD & determines changes # Iterate through the changes and report add/delete/modify # TODO: call out to dulwich.diff_tree somehow. tracked_changes = { 'add': [], 'delete': [], 'modify': [], } try: tree_id = r[b'HEAD'].tree except KeyError: tree_id = None for change in index.changes_from_tree(r.object_store, tree_id): if not change[0][0]: tracked_changes['add'].append(change[0][1]) elif not change[0][1]: tracked_changes['delete'].append(change[0][0]) elif change[0][0] == change[0][1]: tracked_changes['modify'].append(change[0][0]) else: raise AssertionError('git mv ops not yet supported') return tracked_changes def daemon(path=".", address=None, port=None): """Run a daemon serving Git requests over TCP/IP. :param path: Path to the directory to serve. :param address: Optional address to listen on (defaults to ::) :param port: Optional port to listen on (defaults to TCP_GIT_PORT) """ # TODO(jelmer): Support git-daemon-export-ok and --export-all. backend = FileSystemBackend(path) server = TCPGitServer(backend, address, port) server.serve_forever() def web_daemon(path=".", address=None, port=None): """Run a daemon serving Git requests over HTTP. :param path: Path to the directory to serve :param address: Optional address to listen on (defaults to ::) :param port: Optional port to listen on (defaults to 80) """ from dulwich.web import ( make_wsgi_chain, make_server, WSGIRequestHandlerLogger, WSGIServerLogger) backend = FileSystemBackend(path) app = make_wsgi_chain(backend) server = make_server(address, port, app, handler_class=WSGIRequestHandlerLogger, server_class=WSGIServerLogger) server.serve_forever() def upload_pack(path=".", inf=None, outf=None): """Upload a pack file after negotiating its contents using smart protocol. :param path: Path to the repository :param inf: Input stream to communicate with client :param outf: Output stream to communicate with client """ if outf is None: outf = getattr(sys.stdout, 'buffer', sys.stdout) if inf is None: inf = getattr(sys.stdin, 'buffer', sys.stdin) path = os.path.expanduser(path) backend = FileSystemBackend(path) def send_fn(data): outf.write(data) outf.flush() proto = Protocol(inf.read, send_fn) handler = UploadPackHandler(backend, [path], proto) # FIXME: Catch exceptions and write a single-line summary to outf. handler.handle() return 0 def receive_pack(path=".", inf=None, outf=None): """Receive a pack file after negotiating its contents using smart protocol. :param path: Path to the repository :param inf: Input stream to communicate with client :param outf: Output stream to communicate with client """ if outf is None: outf = getattr(sys.stdout, 'buffer', sys.stdout) if inf is None: inf = getattr(sys.stdin, 'buffer', sys.stdin) path = os.path.expanduser(path) backend = FileSystemBackend(path) def send_fn(data): outf.write(data) outf.flush() proto = Protocol(inf.read, send_fn) handler = ReceivePackHandler(backend, [path], proto) # FIXME: Catch exceptions and write a single-line summary to outf. handler.handle() return 0 def _make_branch_ref(name): if getattr(name, 'encode', None): name = name.encode(DEFAULT_ENCODING) return b"refs/heads/" + name def _make_tag_ref(name): if getattr(name, 'encode', None): name = name.encode(DEFAULT_ENCODING) return b"refs/tags/" + name def branch_delete(repo, name): """Delete a branch. :param repo: Path to the repository :param name: Name of the branch """ with open_repo_closing(repo) as r: if isinstance(name, list): names = name else: names = [name] for name in names: del r.refs[_make_branch_ref(name)] def branch_create(repo, name, objectish=None, force=False): """Create a branch. :param repo: Path to the repository :param name: Name of the new branch :param objectish: Target object to point new branch at (defaults to HEAD) :param force: Force creation of branch, even if it already exists """ with open_repo_closing(repo) as r: if objectish is None: objectish = "HEAD" object = parse_object(r, objectish) refname = _make_branch_ref(name) ref_message = b"branch: Created from " + objectish.encode('utf-8') if force: r.refs.set_if_equals(refname, None, object.id, message=ref_message) else: if not r.refs.add_if_new(refname, object.id, message=ref_message): raise KeyError("Branch with name %s already exists." % name) def branch_list(repo): """List all branches. :param repo: Path to the repository """ with open_repo_closing(repo) as r: return r.refs.keys(base=b"refs/heads/") def fetch(repo, remote_location, remote_name=b'origin', outstream=sys.stdout, errstream=default_bytes_err_stream, message=None, depth=None, **kwargs): """Fetch objects from a remote server. :param repo: Path to the repository :param remote_location: String identifying a remote server :param remote_name: Name for remote server :param outstream: Output stream (defaults to stdout) :param errstream: Error stream (defaults to stderr) :param message: Reflog message (defaults to b"fetch: from ") :param depth: Depth to fetch at :return: Dictionary with refs on the remote """ if message is None: message = b'fetch: from ' + remote_location.encode("utf-8") with open_repo_closing(repo) as r: client, path = get_transport_and_path( remote_location, config=r.get_config_stack(), **kwargs) fetch_result = client.fetch(path, r, progress=errstream.write, depth=depth) stripped_refs = strip_peeled_refs(fetch_result.refs) branches = { n[len(b'refs/heads/'):]: v for (n, v) in stripped_refs.items() if n.startswith(b'refs/heads/')} r.refs.import_refs( b'refs/remotes/' + remote_name, branches, message=message) tags = { n[len(b'refs/tags/'):]: v for (n, v) in stripped_refs.items() if n.startswith(b'refs/tags/') and not n.endswith(ANNOTATED_TAG_SUFFIX)} r.refs.import_refs(b'refs/tags', tags, message=message) return fetch_result.refs def ls_remote(remote, config=None, **kwargs): """List the refs in a remote. :param remote: Remote repository location :param config: Configuration to use :return: Dictionary with remote refs """ if config is None: config = StackedConfig.default() client, host_path = get_transport_and_path(remote, config=config, **kwargs) return client.get_refs(host_path) def repack(repo): """Repack loose files in a repository. Currently this only packs loose objects. :param repo: Path to the repository """ with open_repo_closing(repo) as r: r.object_store.pack_loose_objects() def pack_objects(repo, object_ids, packf, idxf, delta_window_size=None): """Pack objects into a file. :param repo: Path to the repository :param object_ids: List of object ids to write :param packf: File-like object to write to :param idxf: File-like object to write to (can be None) """ with open_repo_closing(repo) as r: entries, data_sum = write_pack_objects( packf, r.object_store.iter_shas((oid, None) for oid in object_ids), delta_window_size=delta_window_size) if idxf is not None: entries = sorted([(k, v[0], v[1]) for (k, v) in entries.items()]) write_pack_index(idxf, entries, data_sum) def ls_tree(repo, treeish=b"HEAD", outstream=sys.stdout, recursive=False, name_only=False): """List contents of a tree. :param repo: Path to the repository :param tree_ish: Tree id to list :param outstream: Output stream (defaults to stdout) :param recursive: Whether to recursively list files :param name_only: Only print item name """ def list_tree(store, treeid, base): for (name, mode, sha) in store[treeid].iteritems(): if base: name = posixpath.join(base, name) if name_only: outstream.write(name + b"\n") else: outstream.write(pretty_format_tree_entry(name, mode, sha)) if stat.S_ISDIR(mode) and recursive: list_tree(store, sha, name) with open_repo_closing(repo) as r: tree = parse_tree(r, treeish) list_tree(r.object_store, tree.id, "") def remote_add(repo, name, url): """Add a remote. :param repo: Path to the repository :param name: Remote name :param url: Remote URL """ if not isinstance(name, bytes): name = name.encode(DEFAULT_ENCODING) if not isinstance(url, bytes): url = url.encode(DEFAULT_ENCODING) with open_repo_closing(repo) as r: c = r.get_config() section = (b'remote', name) if c.has_section(section): raise RemoteExists(section) c.set(section, b"url", url) c.write_to_path() def check_ignore(repo, paths, no_index=False): """Debug gitignore files. :param repo: Path to the repository :param paths: List of paths to check for :param no_index: Don't check index :return: List of ignored files """ with open_repo_closing(repo) as r: index = r.open_index() ignore_manager = IgnoreFilterManager.from_repo(r) for path in paths: if not no_index and path_to_tree_path(r.path, path) in index: continue if os.path.isabs(path): path = os.path.relpath(path, r.path) if ignore_manager.is_ignored(path): yield path def update_head(repo, target, detached=False, new_branch=None): """Update HEAD to point at a new branch/commit. Note that this does not actually update the working tree. :param repo: Path to the repository :param detach: Create a detached head :param target: Branch or committish to switch to :param new_branch: New branch to create """ with open_repo_closing(repo) as r: if new_branch is not None: to_set = _make_branch_ref(new_branch) else: to_set = b"HEAD" if detached: # TODO(jelmer): Provide some way so that the actual ref gets # updated rather than what it points to, so the delete isn't # necessary. del r.refs[to_set] r.refs[to_set] = parse_commit(r, target).id else: 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) def fsck(repo): """Check a repository. :param repo: A path to the repository :return: Iterator over errors/warnings """ with open_repo_closing(repo) as r: # TODO(jelmer): check pack files # TODO(jelmer): check graph # TODO(jelmer): check refs for sha in r.object_store: o = r.object_store[sha] try: o.check() except Exception as e: yield (sha, e) def stash_list(repo): """List all stashes in a repository.""" with open_repo_closing(repo) as r: from dulwich.stash import Stash stash = Stash.from_repo(r) return enumerate(list(stash.stashes())) def stash_push(repo): """Push a new stash onto the stack.""" with open_repo_closing(repo) as r: from dulwich.stash import Stash stash = Stash.from_repo(r) stash.push() def stash_pop(repo): """Pop a new stash from the stack.""" with open_repo_closing(repo) as r: from dulwich.stash import Stash stash = Stash.from_repo(r) stash.pop() def ls_files(repo): """List all files in an index.""" with open_repo_closing(repo) as r: return sorted(r.open_index()) def describe(repo): """Describe the repository version. :param projdir: git repository root :returns: a string description of the current git revision Examples: "gabcdefh", "v0.1" or "v0.1-5-gabcdefh". """ # Get the repository with open_repo_closing(repo) as r: # Get a list of all tags refs = r.get_refs() tags = {} for key, value in refs.items(): key = key.decode() obj = r.get_object(value) if u'tags' not in key: continue _, tag = key.rsplit(u'/', 1) try: commit = obj.object except AttributeError: continue else: commit = r.get_object(commit[1]) tags[tag] = [ datetime.datetime(*time.gmtime(commit.commit_time)[:6]), commit.id.decode('ascii'), ] sorted_tags = sorted(tags.items(), key=lambda tag: tag[1][0], reverse=True) # If there are no tags, return the current commit if len(sorted_tags) == 0: return 'g{}'.format(r[r.head()].id.decode('ascii')[:7]) # We're now 0 commits from the top commit_count = 0 # Get the latest commit latest_commit = r[r.head()] # Walk through all commits walker = r.get_walker() for entry in walker: # Check if tag commit_id = entry.commit.id.decode('ascii') for tag in sorted_tags: tag_name = tag[0] tag_commit = tag[1][1] if commit_id == tag_commit: if commit_count == 0: return tag_name else: return '{}-{}-g{}'.format( tag_name, commit_count, latest_commit.id.decode('ascii')[:7]) commit_count += 1 # Return plain commit if no parent tag can be found return 'g{}'.format(latest_commit.id.decode('ascii')[:7]) def get_object_by_path(repo, path, committish=None): """Get an object by path. :param repo: A path to the repository :param path: Path to look up :param committish: Commit to look up path in :return: A `ShaFile` object """ if committish is None: committish = "HEAD" # Get the repository with open_repo_closing(repo) as r: commit = parse_commit(repo, committish) base_tree = commit.tree if not isinstance(path, bytes): path = path.encode(commit.encoding or DEFAULT_ENCODING) (mode, sha) = tree_lookup_path( r.object_store.__getitem__, base_tree, path) return r[sha] def write_tree(repo): """Write a tree object from the index. :param repo: Repository for which to write tree :return: tree id for the tree that was written """ with open_repo_closing(repo) as r: return r.open_index().commit(r.object_store) diff --git a/dulwich/tests/test_objects.py b/dulwich/tests/test_objects.py index 441f6416..c6556fac 100644 --- a/dulwich/tests/test_objects.py +++ b/dulwich/tests/test_objects.py @@ -1,1310 +1,1313 @@ # test_objects.py -- tests for objects.py # Copyright (C) 2007 James Westby # # 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 # for a copy of the GNU General Public License # and for a copy of the Apache # License, Version 2.0. # """Tests for git base objects.""" # TODO: Round-trip parse-serialize-parse and serialize-parse-serialize tests. from io import BytesIO import datetime from itertools import ( permutations, ) import os import stat import warnings from contextlib import contextmanager from dulwich.errors import ( ObjectFormatException, ) from dulwich.objects import ( Blob, Tree, Commit, ShaFile, Tag, TreeEntry, format_timezone, hex_to_sha, sha_to_hex, hex_to_filename, check_hexsha, check_identity, object_class, parse_timezone, pretty_format_tree_entry, parse_tree, _parse_tree_py, sorted_tree_items, _sorted_tree_items_py, MAX_TIME ) from dulwich.tests import ( TestCase, ) from dulwich.tests.utils import ( make_commit, make_object, functest_builder, ext_functest_builder, ) a_sha = b'6f670c0fb53f9463760b7295fbb814e965fb20c8' b_sha = b'2969be3e8ee1c0222396a5611407e4769f14e54b' c_sha = b'954a536f7819d40e6f637f849ee187dd10066349' tree_sha = b'70c190eb48fa8bbb50ddc692a17b44cb781af7f6' tag_sha = b'71033db03a03c6a36721efcf1968dd8f8e0cf023' class TestHexToSha(TestCase): def test_simple(self): self.assertEqual(b'\xab\xcd' * 10, hex_to_sha(b'abcd' * 10)) def test_reverse(self): self.assertEqual(b'abcd' * 10, sha_to_hex(b'\xab\xcd' * 10)) class BlobReadTests(TestCase): """Test decompression of blobs""" def get_sha_file(self, cls, base, sha): dir = os.path.join(os.path.dirname(__file__), 'data', base) return cls.from_path(hex_to_filename(dir, sha)) def get_blob(self, sha): """Return the blob named sha from the test data dir""" return self.get_sha_file(Blob, 'blobs', sha) def get_tree(self, sha): return self.get_sha_file(Tree, 'trees', sha) def get_tag(self, sha): return self.get_sha_file(Tag, 'tags', sha) def commit(self, sha): return self.get_sha_file(Commit, 'commits', sha) def test_decompress_simple_blob(self): b = self.get_blob(a_sha) self.assertEqual(b.data, b'test 1\n') self.assertEqual(b.sha().hexdigest().encode('ascii'), a_sha) def test_hash(self): b = self.get_blob(a_sha) self.assertEqual(hash(b.id), hash(b)) def test_parse_empty_blob_object(self): sha = b'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391' b = self.get_blob(sha) self.assertEqual(b.data, b'') self.assertEqual(b.id, sha) self.assertEqual(b.sha().hexdigest().encode('ascii'), sha) def test_create_blob_from_string(self): string = b'test 2\n' b = Blob.from_string(string) self.assertEqual(b.data, string) self.assertEqual(b.sha().hexdigest().encode('ascii'), b_sha) def test_legacy_from_file(self): b1 = Blob.from_string(b'foo') b_raw = b1.as_legacy_object() b2 = b1.from_file(BytesIO(b_raw)) self.assertEqual(b1, b2) def test_chunks(self): string = b'test 5\n' b = Blob.from_string(string) self.assertEqual([string], b.chunked) def test_splitlines(self): for case in [ [], [b'foo\nbar\n'], [b'bl\na', b'blie'], [b'bl\na', b'blie', b'bloe\n'], [b'', b'bl\na', b'blie', b'bloe\n'], [b'', b'', b'', b'bla\n'], [b'', b'', b'', b'bla\n', b''], [b'bl', b'', b'a\naaa'], [b'a\naaa', b'a'], ]: b = Blob() b.chunked = case self.assertEqual(b.data.splitlines(True), b.splitlines()) def test_set_chunks(self): b = Blob() b.chunked = [b'te', b'st', b' 5\n'] self.assertEqual(b'test 5\n', b.data) b.chunked = [b'te', b'st', b' 6\n'] self.assertEqual(b'test 6\n', b.as_raw_string()) self.assertEqual(b'test 6\n', bytes(b)) def test_parse_legacy_blob(self): string = b'test 3\n' b = self.get_blob(c_sha) self.assertEqual(b.data, string) self.assertEqual(b.sha().hexdigest().encode('ascii'), c_sha) def test_eq(self): blob1 = self.get_blob(a_sha) blob2 = self.get_blob(a_sha) self.assertEqual(blob1, blob2) def test_read_tree_from_file(self): t = self.get_tree(tree_sha) self.assertEqual(t.items()[0], (b'a', 33188, a_sha)) self.assertEqual(t.items()[1], (b'b', 33188, b_sha)) def test_read_tree_from_file_parse_count(self): old_deserialize = Tree._deserialize def reset_deserialize(): Tree._deserialize = old_deserialize self.addCleanup(reset_deserialize) self.deserialize_count = 0 def counting_deserialize(*args, **kwargs): self.deserialize_count += 1 return old_deserialize(*args, **kwargs) Tree._deserialize = counting_deserialize t = self.get_tree(tree_sha) self.assertEqual(t.items()[0], (b'a', 33188, a_sha)) self.assertEqual(t.items()[1], (b'b', 33188, b_sha)) self.assertEqual(self.deserialize_count, 1) def test_read_tag_from_file(self): t = self.get_tag(tag_sha) self.assertEqual(t.object, (Commit, b'51b668fd5bf7061b7d6fa525f88803e6cfadaa51')) self.assertEqual(t.name, b'signed') self.assertEqual(t.tagger, b'Ali Sabil ') self.assertEqual(t.tag_time, 1231203091) self.assertEqual( t.message, b'This is a signed tag\n' + ) + self.assertEqual( + t.signature, b'-----BEGIN PGP SIGNATURE-----\n' b'Version: GnuPG v1.4.9 (GNU/Linux)\n' b'\n' b'iEYEABECAAYFAkliqx8ACgkQqSMmLy9u/' b'kcx5ACfakZ9NnPl02tOyYP6pkBoEkU1\n' b'5EcAn0UFgokaSvS371Ym/4W9iJj6vh3h\n' b'=ql7y\n' b'-----END PGP SIGNATURE-----\n') def test_read_commit_from_file(self): sha = b'60dacdc733de308bb77bb76ce0fb0f9b44c9769e' c = self.commit(sha) self.assertEqual(c.tree, tree_sha) self.assertEqual(c.parents, [b'0d89f20333fbb1d2f3a94da77f4981373d8f4310']) self.assertEqual(c.author, b'James Westby ') self.assertEqual(c.committer, b'James Westby ') self.assertEqual(c.commit_time, 1174759230) self.assertEqual(c.commit_timezone, 0) self.assertEqual(c.author_timezone, 0) self.assertEqual(c.message, b'Test commit\n') def test_read_commit_no_parents(self): sha = b'0d89f20333fbb1d2f3a94da77f4981373d8f4310' c = self.commit(sha) self.assertEqual(c.tree, b'90182552c4a85a45ec2a835cadc3451bebdfe870') self.assertEqual(c.parents, []) self.assertEqual(c.author, b'James Westby ') self.assertEqual(c.committer, b'James Westby ') self.assertEqual(c.commit_time, 1174758034) self.assertEqual(c.commit_timezone, 0) self.assertEqual(c.author_timezone, 0) self.assertEqual(c.message, b'Test commit\n') def test_read_commit_two_parents(self): sha = b'5dac377bdded4c9aeb8dff595f0faeebcc8498cc' c = self.commit(sha) self.assertEqual(c.tree, b'd80c186a03f423a81b39df39dc87fd269736ca86') self.assertEqual(c.parents, [b'ab64bbdcc51b170d21588e5c5d391ee5c0c96dfd', b'4cffe90e0a41ad3f5190079d7c8f036bde29cbe6']) self.assertEqual(c.author, b'James Westby ') self.assertEqual(c.committer, b'James Westby ') self.assertEqual(c.commit_time, 1174773719) self.assertEqual(c.commit_timezone, 0) self.assertEqual(c.author_timezone, 0) self.assertEqual(c.message, b'Merge ../b\n') def test_stub_sha(self): sha = b'5' * 40 c = make_commit(id=sha, message=b'foo') self.assertTrue(isinstance(c, Commit)) self.assertEqual(sha, c.id) self.assertNotEqual(sha, c.sha()) class ShaFileCheckTests(TestCase): def assertCheckFails(self, cls, data): obj = cls() def do_check(): obj.set_raw_string(data) obj.check() self.assertRaises(ObjectFormatException, do_check) def assertCheckSucceeds(self, cls, data): obj = cls() obj.set_raw_string(data) self.assertEqual(None, obj.check()) small_buffer_zlib_object = ( b'\x48\x89\x15\xcc\x31\x0e\xc2\x30\x0c\x40\x51\xe6' b'\x9c\xc2\x3b\xaa\x64\x37\xc4\xc1\x12\x42\x5c\xc5' b'\x49\xac\x52\xd4\x92\xaa\x78\xe1\xf6\x94\xed\xeb' b'\x0d\xdf\x75\x02\xa2\x7c\xea\xe5\x65\xd5\x81\x8b' b'\x9a\x61\xba\xa0\xa9\x08\x36\xc9\x4c\x1a\xad\x88' b'\x16\xba\x46\xc4\xa8\x99\x6a\x64\xe1\xe0\xdf\xcd' b'\xa0\xf6\x75\x9d\x3d\xf8\xf1\xd0\x77\xdb\xfb\xdc' b'\x86\xa3\x87\xf1\x2f\x93\xed\x00\xb7\xc7\xd2\xab' b'\x2e\xcf\xfe\xf1\x3b\x50\xa4\x91\x53\x12\x24\x38' b'\x23\x21\x86\xf0\x03\x2f\x91\x24\x52' ) class ShaFileTests(TestCase): def test_deflated_smaller_window_buffer(self): # zlib on some systems uses smaller buffers, # resulting in a different header. # See https://github.com/libgit2/libgit2/pull/464 sf = ShaFile.from_file(BytesIO(small_buffer_zlib_object)) self.assertEqual(sf.type_name, b'tag') self.assertEqual(sf.tagger, b' <@localhost>') class CommitSerializationTests(TestCase): def make_commit(self, **kwargs): attrs = {'tree': b'd80c186a03f423a81b39df39dc87fd269736ca86', 'parents': [b'ab64bbdcc51b170d21588e5c5d391ee5c0c96dfd', b'4cffe90e0a41ad3f5190079d7c8f036bde29cbe6'], 'author': b'James Westby ', 'committer': b'James Westby ', 'commit_time': 1174773719, 'author_time': 1174773719, 'commit_timezone': 0, 'author_timezone': 0, 'message': b'Merge ../b\n'} attrs.update(kwargs) return make_commit(**attrs) def test_encoding(self): c = self.make_commit(encoding=b'iso8859-1') self.assertTrue(b'encoding iso8859-1\n' in c.as_raw_string()) def test_short_timestamp(self): c = self.make_commit(commit_time=30) c1 = Commit() c1.set_raw_string(c.as_raw_string()) self.assertEqual(30, c1.commit_time) def test_full_tree(self): c = self.make_commit(commit_time=30) t = Tree() t.add(b'data-x', 0o644, Blob().id) c.tree = t c1 = Commit() c1.set_raw_string(c.as_raw_string()) self.assertEqual(t.id, c1.tree) self.assertEqual(c.as_raw_string(), c1.as_raw_string()) def test_raw_length(self): c = self.make_commit() self.assertEqual(len(c.as_raw_string()), c.raw_length()) def test_simple(self): c = self.make_commit() self.assertEqual(c.id, b'5dac377bdded4c9aeb8dff595f0faeebcc8498cc') self.assertEqual( b'tree d80c186a03f423a81b39df39dc87fd269736ca86\n' b'parent ab64bbdcc51b170d21588e5c5d391ee5c0c96dfd\n' b'parent 4cffe90e0a41ad3f5190079d7c8f036bde29cbe6\n' b'author James Westby ' b'1174773719 +0000\n' b'committer James Westby ' b'1174773719 +0000\n' b'\n' b'Merge ../b\n', c.as_raw_string()) def test_timezone(self): c = self.make_commit(commit_timezone=(5 * 60)) self.assertTrue(b' +0005\n' in c.as_raw_string()) def test_neg_timezone(self): c = self.make_commit(commit_timezone=(-1 * 3600)) self.assertTrue(b' -0100\n' in c.as_raw_string()) def test_deserialize(self): c = self.make_commit() d = Commit() d._deserialize(c.as_raw_chunks()) self.assertEqual(c, d) def test_serialize_gpgsig(self): commit = self.make_commit(gpgsig=b"""-----BEGIN PGP SIGNATURE----- Version: GnuPG v1 iQIcBAABCgAGBQJULCdfAAoJEACAbyvXKaRXuKwP/RyP9PA49uAvu8tQVCC/uBa8 vi975+xvO14R8Pp8k2nps7lSxCdtCd+xVT1VRHs0wNhOZo2YCVoU1HATkPejqSeV NScTHcxnk4/+bxyfk14xvJkNp7FlQ3npmBkA+lbV0Ubr33rvtIE5jiJPyz+SgWAg xdBG2TojV0squj00GoH/euK6aX7GgZtwdtpTv44haCQdSuPGDcI4TORqR6YSqvy3 GPE+3ZqXPFFb+KILtimkxitdwB7CpwmNse2vE3rONSwTvi8nq3ZoQYNY73CQGkUy qoFU0pDtw87U3niFin1ZccDgH0bB6624sLViqrjcbYJeg815Htsu4rmzVaZADEVC XhIO4MThebusdk0AcNGjgpf3HRHk0DPMDDlIjm+Oao0cqovvF6VyYmcb0C+RmhJj dodLXMNmbqErwTk3zEkW0yZvNIYXH7m9SokPCZa4eeIM7be62X6h1mbt0/IU6Th+ v18fS0iTMP/Viug5und+05C/v04kgDo0CPphAbXwWMnkE4B6Tl9sdyUYXtvQsL7x 0+WP1gL27ANqNZiI07Kz/BhbBAQI/+2TFT7oGr0AnFPQ5jHp+3GpUf6OKuT1wT3H ND189UFuRuubxb42vZhpcXRbqJVWnbECTKVUPsGZqat3enQUB63uM4i6/RdONDZA fDeF1m4qYs+cUXKNUZ03 =X6RT -----END PGP SIGNATURE-----""") self.maxDiff = None self.assertEqual(b"""\ tree d80c186a03f423a81b39df39dc87fd269736ca86 parent ab64bbdcc51b170d21588e5c5d391ee5c0c96dfd parent 4cffe90e0a41ad3f5190079d7c8f036bde29cbe6 author James Westby 1174773719 +0000 committer James Westby 1174773719 +0000 gpgsig -----BEGIN PGP SIGNATURE----- Version: GnuPG v1 iQIcBAABCgAGBQJULCdfAAoJEACAbyvXKaRXuKwP/RyP9PA49uAvu8tQVCC/uBa8 vi975+xvO14R8Pp8k2nps7lSxCdtCd+xVT1VRHs0wNhOZo2YCVoU1HATkPejqSeV NScTHcxnk4/+bxyfk14xvJkNp7FlQ3npmBkA+lbV0Ubr33rvtIE5jiJPyz+SgWAg xdBG2TojV0squj00GoH/euK6aX7GgZtwdtpTv44haCQdSuPGDcI4TORqR6YSqvy3 GPE+3ZqXPFFb+KILtimkxitdwB7CpwmNse2vE3rONSwTvi8nq3ZoQYNY73CQGkUy qoFU0pDtw87U3niFin1ZccDgH0bB6624sLViqrjcbYJeg815Htsu4rmzVaZADEVC XhIO4MThebusdk0AcNGjgpf3HRHk0DPMDDlIjm+Oao0cqovvF6VyYmcb0C+RmhJj dodLXMNmbqErwTk3zEkW0yZvNIYXH7m9SokPCZa4eeIM7be62X6h1mbt0/IU6Th+ v18fS0iTMP/Viug5und+05C/v04kgDo0CPphAbXwWMnkE4B6Tl9sdyUYXtvQsL7x 0+WP1gL27ANqNZiI07Kz/BhbBAQI/+2TFT7oGr0AnFPQ5jHp+3GpUf6OKuT1wT3H ND189UFuRuubxb42vZhpcXRbqJVWnbECTKVUPsGZqat3enQUB63uM4i6/RdONDZA fDeF1m4qYs+cUXKNUZ03 =X6RT -----END PGP SIGNATURE----- Merge ../b """, commit.as_raw_string()) # noqa: W291,W293 def test_serialize_mergetag(self): tag = make_object( Tag, object=(Commit, b'a38d6181ff27824c79fc7df825164a212eff6a3f'), object_type_name=b'commit', name=b'v2.6.22-rc7', tag_time=1183319674, tag_timezone=0, tagger=b'Linus Torvalds ', message=default_message) commit = self.make_commit(mergetag=[tag]) self.assertEqual(b"""tree d80c186a03f423a81b39df39dc87fd269736ca86 parent ab64bbdcc51b170d21588e5c5d391ee5c0c96dfd parent 4cffe90e0a41ad3f5190079d7c8f036bde29cbe6 author James Westby 1174773719 +0000 committer James Westby 1174773719 +0000 mergetag object a38d6181ff27824c79fc7df825164a212eff6a3f type commit tag v2.6.22-rc7 tagger Linus Torvalds 1183319674 +0000 Linux 2.6.22-rc7 -----BEGIN PGP SIGNATURE----- Version: GnuPG v1.4.7 (GNU/Linux) iD8DBQBGiAaAF3YsRnbiHLsRAitMAKCiLboJkQECM/jpYsY3WPfvUgLXkACgg3ql OK2XeQOiEeXtT76rV4t2WR4= =ivrA -----END PGP SIGNATURE----- Merge ../b """, commit.as_raw_string()) # noqa: W291,W293 def test_serialize_mergetags(self): tag = make_object( Tag, object=(Commit, b'a38d6181ff27824c79fc7df825164a212eff6a3f'), object_type_name=b'commit', name=b'v2.6.22-rc7', tag_time=1183319674, tag_timezone=0, tagger=b'Linus Torvalds ', message=default_message) commit = self.make_commit(mergetag=[tag, tag]) self.assertEqual(b"""tree d80c186a03f423a81b39df39dc87fd269736ca86 parent ab64bbdcc51b170d21588e5c5d391ee5c0c96dfd parent 4cffe90e0a41ad3f5190079d7c8f036bde29cbe6 author James Westby 1174773719 +0000 committer James Westby 1174773719 +0000 mergetag object a38d6181ff27824c79fc7df825164a212eff6a3f type commit tag v2.6.22-rc7 tagger Linus Torvalds 1183319674 +0000 Linux 2.6.22-rc7 -----BEGIN PGP SIGNATURE----- Version: GnuPG v1.4.7 (GNU/Linux) iD8DBQBGiAaAF3YsRnbiHLsRAitMAKCiLboJkQECM/jpYsY3WPfvUgLXkACgg3ql OK2XeQOiEeXtT76rV4t2WR4= =ivrA -----END PGP SIGNATURE----- mergetag object a38d6181ff27824c79fc7df825164a212eff6a3f type commit tag v2.6.22-rc7 tagger Linus Torvalds 1183319674 +0000 Linux 2.6.22-rc7 -----BEGIN PGP SIGNATURE----- Version: GnuPG v1.4.7 (GNU/Linux) iD8DBQBGiAaAF3YsRnbiHLsRAitMAKCiLboJkQECM/jpYsY3WPfvUgLXkACgg3ql OK2XeQOiEeXtT76rV4t2WR4= =ivrA -----END PGP SIGNATURE----- Merge ../b """, commit.as_raw_string()) # noqa: W291,W293 def test_deserialize_mergetag(self): tag = make_object( Tag, object=(Commit, b'a38d6181ff27824c79fc7df825164a212eff6a3f'), object_type_name=b'commit', name=b'v2.6.22-rc7', tag_time=1183319674, tag_timezone=0, tagger=b'Linus Torvalds ', message=default_message) commit = self.make_commit(mergetag=[tag]) d = Commit() d._deserialize(commit.as_raw_chunks()) self.assertEqual(commit, d) def test_deserialize_mergetags(self): tag = make_object( Tag, object=(Commit, b'a38d6181ff27824c79fc7df825164a212eff6a3f'), object_type_name=b'commit', name=b'v2.6.22-rc7', tag_time=1183319674, tag_timezone=0, tagger=b'Linus Torvalds ', message=default_message) commit = self.make_commit(mergetag=[tag, tag]) d = Commit() d._deserialize(commit.as_raw_chunks()) self.assertEqual(commit, d) default_committer = ( b'James Westby 1174773719 +0000') class CommitParseTests(ShaFileCheckTests): def make_commit_lines(self, tree=b'd80c186a03f423a81b39df39dc87fd269736ca86', parents=[ b'ab64bbdcc51b170d21588e5c5d391ee5c0c96dfd', b'4cffe90e0a41ad3f5190079d7c8f036bde29cbe6'], author=default_committer, committer=default_committer, encoding=None, message=b'Merge ../b\n', extra=None): lines = [] if tree is not None: lines.append(b'tree ' + tree) if parents is not None: lines.extend(b'parent ' + p for p in parents) if author is not None: lines.append(b'author ' + author) if committer is not None: lines.append(b'committer ' + committer) if encoding is not None: lines.append(b'encoding ' + encoding) if extra is not None: for name, value in sorted(extra.items()): lines.append(name + b' ' + value) lines.append(b'') if message is not None: lines.append(message) return lines def make_commit_text(self, **kwargs): return b'\n'.join(self.make_commit_lines(**kwargs)) def test_simple(self): c = Commit.from_string(self.make_commit_text()) self.assertEqual(b'Merge ../b\n', c.message) self.assertEqual(b'James Westby ', c.author) self.assertEqual(b'James Westby ', c.committer) self.assertEqual(b'd80c186a03f423a81b39df39dc87fd269736ca86', c.tree) self.assertEqual([b'ab64bbdcc51b170d21588e5c5d391ee5c0c96dfd', b'4cffe90e0a41ad3f5190079d7c8f036bde29cbe6'], c.parents) expected_time = datetime.datetime(2007, 3, 24, 22, 1, 59) self.assertEqual(expected_time, datetime.datetime.utcfromtimestamp(c.commit_time)) self.assertEqual(0, c.commit_timezone) self.assertEqual(expected_time, datetime.datetime.utcfromtimestamp(c.author_time)) self.assertEqual(0, c.author_timezone) self.assertEqual(None, c.encoding) def test_custom(self): c = Commit.from_string(self.make_commit_text( extra={b'extra-field': b'data'})) self.assertEqual([(b'extra-field', b'data')], c.extra) def test_encoding(self): c = Commit.from_string(self.make_commit_text(encoding=b'UTF-8')) self.assertEqual(b'UTF-8', c.encoding) def test_check(self): self.assertCheckSucceeds(Commit, self.make_commit_text()) self.assertCheckSucceeds(Commit, self.make_commit_text(parents=None)) self.assertCheckSucceeds(Commit, self.make_commit_text(encoding=b'UTF-8')) self.assertCheckFails(Commit, self.make_commit_text(tree=b'xxx')) self.assertCheckFails(Commit, self.make_commit_text( parents=[a_sha, b'xxx'])) bad_committer = b'some guy without an email address 1174773719 +0000' self.assertCheckFails(Commit, self.make_commit_text(committer=bad_committer)) self.assertCheckFails(Commit, self.make_commit_text(author=bad_committer)) self.assertCheckFails(Commit, self.make_commit_text(author=None)) self.assertCheckFails(Commit, self.make_commit_text(committer=None)) self.assertCheckFails(Commit, self.make_commit_text( author=None, committer=None)) def test_check_duplicates(self): # duplicate each of the header fields for i in range(5): lines = self.make_commit_lines(parents=[a_sha], encoding=b'UTF-8') lines.insert(i, lines[i]) text = b'\n'.join(lines) if lines[i].startswith(b'parent'): # duplicate parents are ok for now self.assertCheckSucceeds(Commit, text) else: self.assertCheckFails(Commit, text) def test_check_order(self): lines = self.make_commit_lines(parents=[a_sha], encoding=b'UTF-8') headers = lines[:5] rest = lines[5:] # of all possible permutations, ensure only the original succeeds for perm in permutations(headers): perm = list(perm) text = b'\n'.join(perm + rest) if perm == headers: self.assertCheckSucceeds(Commit, text) else: self.assertCheckFails(Commit, text) def test_check_commit_with_unparseable_time(self): identity_with_wrong_time = ( b'Igor Sysoev 18446743887488505614+42707004') # Those fail at reading time self.assertCheckFails( Commit, self.make_commit_text(author=default_committer, committer=identity_with_wrong_time)) self.assertCheckFails( Commit, self.make_commit_text(author=identity_with_wrong_time, committer=default_committer)) def test_check_commit_with_overflow_date(self): """Date with overflow should raise an ObjectFormatException when checked """ identity_with_wrong_time = ( b'Igor Sysoev 18446743887488505614 +42707004') commit0 = Commit.from_string(self.make_commit_text( author=identity_with_wrong_time, committer=default_committer)) commit1 = Commit.from_string(self.make_commit_text( author=default_committer, committer=identity_with_wrong_time)) # Those fails when triggering the check() method for commit in [commit0, commit1]: with self.assertRaises(ObjectFormatException): commit.check() def test_mangled_author_line(self): """Mangled author line should successfully parse""" author_line = ( b'Karl MacMillan <"Karl MacMillan ' b'"> 1197475547 -0500' ) expected_identity = ( b'Karl MacMillan <"Karl MacMillan ' b'">' ) commit = Commit.from_string( self.make_commit_text(author=author_line) ) # The commit parses properly self.assertEqual(commit.author, expected_identity) # But the check fails because the author identity is bogus with self.assertRaises(ObjectFormatException): commit.check() def test_parse_gpgsig(self): c = Commit.from_string(b"""tree aaff74984cccd156a469afa7d9ab10e4777beb24 author Jelmer Vernooij 1412179807 +0200 committer Jelmer Vernooij 1412179807 +0200 gpgsig -----BEGIN PGP SIGNATURE----- Version: GnuPG v1 iQIcBAABCgAGBQJULCdfAAoJEACAbyvXKaRXuKwP/RyP9PA49uAvu8tQVCC/uBa8 vi975+xvO14R8Pp8k2nps7lSxCdtCd+xVT1VRHs0wNhOZo2YCVoU1HATkPejqSeV NScTHcxnk4/+bxyfk14xvJkNp7FlQ3npmBkA+lbV0Ubr33rvtIE5jiJPyz+SgWAg xdBG2TojV0squj00GoH/euK6aX7GgZtwdtpTv44haCQdSuPGDcI4TORqR6YSqvy3 GPE+3ZqXPFFb+KILtimkxitdwB7CpwmNse2vE3rONSwTvi8nq3ZoQYNY73CQGkUy qoFU0pDtw87U3niFin1ZccDgH0bB6624sLViqrjcbYJeg815Htsu4rmzVaZADEVC XhIO4MThebusdk0AcNGjgpf3HRHk0DPMDDlIjm+Oao0cqovvF6VyYmcb0C+RmhJj dodLXMNmbqErwTk3zEkW0yZvNIYXH7m9SokPCZa4eeIM7be62X6h1mbt0/IU6Th+ v18fS0iTMP/Viug5und+05C/v04kgDo0CPphAbXwWMnkE4B6Tl9sdyUYXtvQsL7x 0+WP1gL27ANqNZiI07Kz/BhbBAQI/+2TFT7oGr0AnFPQ5jHp+3GpUf6OKuT1wT3H ND189UFuRuubxb42vZhpcXRbqJVWnbECTKVUPsGZqat3enQUB63uM4i6/RdONDZA fDeF1m4qYs+cUXKNUZ03 =X6RT -----END PGP SIGNATURE----- foo """) # noqa: W291,W293 self.assertEqual(b'foo\n', c.message) self.assertEqual([], c.extra) self.assertEqual(b"""-----BEGIN PGP SIGNATURE----- Version: GnuPG v1 iQIcBAABCgAGBQJULCdfAAoJEACAbyvXKaRXuKwP/RyP9PA49uAvu8tQVCC/uBa8 vi975+xvO14R8Pp8k2nps7lSxCdtCd+xVT1VRHs0wNhOZo2YCVoU1HATkPejqSeV NScTHcxnk4/+bxyfk14xvJkNp7FlQ3npmBkA+lbV0Ubr33rvtIE5jiJPyz+SgWAg xdBG2TojV0squj00GoH/euK6aX7GgZtwdtpTv44haCQdSuPGDcI4TORqR6YSqvy3 GPE+3ZqXPFFb+KILtimkxitdwB7CpwmNse2vE3rONSwTvi8nq3ZoQYNY73CQGkUy qoFU0pDtw87U3niFin1ZccDgH0bB6624sLViqrjcbYJeg815Htsu4rmzVaZADEVC XhIO4MThebusdk0AcNGjgpf3HRHk0DPMDDlIjm+Oao0cqovvF6VyYmcb0C+RmhJj dodLXMNmbqErwTk3zEkW0yZvNIYXH7m9SokPCZa4eeIM7be62X6h1mbt0/IU6Th+ v18fS0iTMP/Viug5und+05C/v04kgDo0CPphAbXwWMnkE4B6Tl9sdyUYXtvQsL7x 0+WP1gL27ANqNZiI07Kz/BhbBAQI/+2TFT7oGr0AnFPQ5jHp+3GpUf6OKuT1wT3H ND189UFuRuubxb42vZhpcXRbqJVWnbECTKVUPsGZqat3enQUB63uM4i6/RdONDZA fDeF1m4qYs+cUXKNUZ03 =X6RT -----END PGP SIGNATURE-----""", c.gpgsig) def test_parse_header_trailing_newline(self): c = Commit.from_string(b'''\ tree a7d6277f78d3ecd0230a1a5df6db00b1d9c521ac parent c09b6dec7a73760fbdb478383a3c926b18db8bbe author Neil Matatall 1461964057 -1000 committer Neil Matatall 1461964057 -1000 gpgsig -----BEGIN PGP SIGNATURE----- wsBcBAABCAAQBQJXI80ZCRA6pcNDcVZ70gAAarcIABs72xRX3FWeox349nh6ucJK CtwmBTusez2Zwmq895fQEbZK7jpaGO5TRO4OvjFxlRo0E08UFx3pxZHSpj6bsFeL hHsDXnCaotphLkbgKKRdGZo7tDqM84wuEDlh4MwNe7qlFC7bYLDyysc81ZX5lpMm 2MFF1TvjLAzSvkT7H1LPkuR3hSvfCYhikbPOUNnKOo0sYjeJeAJ/JdAVQ4mdJIM0 gl3REp9+A+qBEpNQI7z94Pg5Bc5xenwuDh3SJgHvJV6zBWupWcdB3fAkVd4TPnEZ nHxksHfeNln9RKseIDcy4b2ATjhDNIJZARHNfr6oy4u3XPW4svRqtBsLoMiIeuI= =ms6q -----END PGP SIGNATURE----- 3.3.0 version bump and docs ''') # noqa: W291,W293 self.assertEqual([], c.extra) self.assertEqual(b'''\ -----BEGIN PGP SIGNATURE----- wsBcBAABCAAQBQJXI80ZCRA6pcNDcVZ70gAAarcIABs72xRX3FWeox349nh6ucJK CtwmBTusez2Zwmq895fQEbZK7jpaGO5TRO4OvjFxlRo0E08UFx3pxZHSpj6bsFeL hHsDXnCaotphLkbgKKRdGZo7tDqM84wuEDlh4MwNe7qlFC7bYLDyysc81ZX5lpMm 2MFF1TvjLAzSvkT7H1LPkuR3hSvfCYhikbPOUNnKOo0sYjeJeAJ/JdAVQ4mdJIM0 gl3REp9+A+qBEpNQI7z94Pg5Bc5xenwuDh3SJgHvJV6zBWupWcdB3fAkVd4TPnEZ nHxksHfeNln9RKseIDcy4b2ATjhDNIJZARHNfr6oy4u3XPW4svRqtBsLoMiIeuI= =ms6q -----END PGP SIGNATURE-----\n''', c.gpgsig) _TREE_ITEMS = { b'a.c': (0o100755, b'd80c186a03f423a81b39df39dc87fd269736ca86'), b'a': (stat.S_IFDIR, b'd80c186a03f423a81b39df39dc87fd269736ca86'), b'a/c': (stat.S_IFDIR, b'd80c186a03f423a81b39df39dc87fd269736ca86'), } _SORTED_TREE_ITEMS = [ TreeEntry(b'a.c', 0o100755, b'd80c186a03f423a81b39df39dc87fd269736ca86'), TreeEntry(b'a', stat.S_IFDIR, b'd80c186a03f423a81b39df39dc87fd269736ca86'), TreeEntry(b'a/c', stat.S_IFDIR, b'd80c186a03f423a81b39df39dc87fd269736ca86'), ] class TreeTests(ShaFileCheckTests): def test_add(self): myhexsha = b'd80c186a03f423a81b39df39dc87fd269736ca86' x = Tree() x.add(b'myname', 0o100755, myhexsha) self.assertEqual(x[b'myname'], (0o100755, myhexsha)) self.assertEqual( b'100755 myname\0' + hex_to_sha(myhexsha), x.as_raw_string()) def test_add_old_order(self): myhexsha = b'd80c186a03f423a81b39df39dc87fd269736ca86' x = Tree() warnings.simplefilter("ignore", DeprecationWarning) try: x.add(0o100755, b'myname', myhexsha) finally: warnings.resetwarnings() self.assertEqual(x[b'myname'], (0o100755, myhexsha)) self.assertEqual(b'100755 myname\0' + hex_to_sha(myhexsha), x.as_raw_string()) def test_simple(self): myhexsha = b'd80c186a03f423a81b39df39dc87fd269736ca86' x = Tree() x[b'myname'] = (0o100755, myhexsha) self.assertEqual(b'100755 myname\0' + hex_to_sha(myhexsha), x.as_raw_string()) self.assertEqual(b'100755 myname\0' + hex_to_sha(myhexsha), bytes(x)) def test_tree_update_id(self): x = Tree() x[b'a.c'] = (0o100755, b'd80c186a03f423a81b39df39dc87fd269736ca86') self.assertEqual(b'0c5c6bc2c081accfbc250331b19e43b904ab9cdd', x.id) x[b'a.b'] = (stat.S_IFDIR, b'd80c186a03f423a81b39df39dc87fd269736ca86') self.assertEqual(b'07bfcb5f3ada15bbebdfa3bbb8fd858a363925c8', x.id) def test_tree_iteritems_dir_sort(self): x = Tree() for name, item in _TREE_ITEMS.items(): x[name] = item self.assertEqual(_SORTED_TREE_ITEMS, x.items()) def test_tree_items_dir_sort(self): x = Tree() for name, item in _TREE_ITEMS.items(): x[name] = item self.assertEqual(_SORTED_TREE_ITEMS, x.items()) def _do_test_parse_tree(self, parse_tree): dir = os.path.join(os.path.dirname(__file__), 'data', 'trees') o = Tree.from_path(hex_to_filename(dir, tree_sha)) self.assertEqual([(b'a', 0o100644, a_sha), (b'b', 0o100644, b_sha)], list(parse_tree(o.as_raw_string()))) # test a broken tree that has a leading 0 on the file mode broken_tree = b'0100644 foo\0' + hex_to_sha(a_sha) def eval_parse_tree(*args, **kwargs): return list(parse_tree(*args, **kwargs)) self.assertEqual([(b'foo', 0o100644, a_sha)], eval_parse_tree(broken_tree)) self.assertRaises(ObjectFormatException, eval_parse_tree, broken_tree, strict=True) test_parse_tree = functest_builder(_do_test_parse_tree, _parse_tree_py) test_parse_tree_extension = ext_functest_builder(_do_test_parse_tree, parse_tree) def _do_test_sorted_tree_items(self, sorted_tree_items): def do_sort(entries): return list(sorted_tree_items(entries, False)) actual = do_sort(_TREE_ITEMS) self.assertEqual(_SORTED_TREE_ITEMS, actual) self.assertTrue(isinstance(actual[0], TreeEntry)) # C/Python implementations may differ in specific error types, but # should all error on invalid inputs. # For example, the C implementation has stricter type checks, so may # raise TypeError where the Python implementation raises # AttributeError. errors = (TypeError, ValueError, AttributeError) self.assertRaises(errors, do_sort, b'foo') self.assertRaises(errors, do_sort, {b'foo': (1, 2, 3)}) myhexsha = b'd80c186a03f423a81b39df39dc87fd269736ca86' self.assertRaises(errors, do_sort, {b'foo': (b'xxx', myhexsha)}) self.assertRaises(errors, do_sort, {b'foo': (0o100755, 12345)}) test_sorted_tree_items = functest_builder(_do_test_sorted_tree_items, _sorted_tree_items_py) test_sorted_tree_items_extension = ext_functest_builder( _do_test_sorted_tree_items, sorted_tree_items) def _do_test_sorted_tree_items_name_order(self, sorted_tree_items): self.assertEqual([ TreeEntry(b'a', stat.S_IFDIR, b'd80c186a03f423a81b39df39dc87fd269736ca86'), TreeEntry(b'a.c', 0o100755, b'd80c186a03f423a81b39df39dc87fd269736ca86'), TreeEntry(b'a/c', stat.S_IFDIR, b'd80c186a03f423a81b39df39dc87fd269736ca86'), ], list(sorted_tree_items(_TREE_ITEMS, True))) test_sorted_tree_items_name_order = functest_builder( _do_test_sorted_tree_items_name_order, _sorted_tree_items_py) test_sorted_tree_items_name_order_extension = ext_functest_builder( _do_test_sorted_tree_items_name_order, sorted_tree_items) def test_check(self): t = Tree sha = hex_to_sha(a_sha) # filenames self.assertCheckSucceeds(t, b'100644 .a\0' + sha) self.assertCheckFails(t, b'100644 \0' + sha) self.assertCheckFails(t, b'100644 .\0' + sha) self.assertCheckFails(t, b'100644 a/a\0' + sha) self.assertCheckFails(t, b'100644 ..\0' + sha) self.assertCheckFails(t, b'100644 .git\0' + sha) # modes self.assertCheckSucceeds(t, b'100644 a\0' + sha) self.assertCheckSucceeds(t, b'100755 a\0' + sha) self.assertCheckSucceeds(t, b'160000 a\0' + sha) # TODO more whitelisted modes self.assertCheckFails(t, b'123456 a\0' + sha) self.assertCheckFails(t, b'123abc a\0' + sha) # should fail check, but parses ok self.assertCheckFails(t, b'0100644 foo\0' + sha) # shas self.assertCheckFails(t, b'100644 a\0' + (b'x' * 5)) self.assertCheckFails(t, b'100644 a\0' + (b'x' * 18) + b'\0') self.assertCheckFails( t, b'100644 a\0' + (b'x' * 21) + b'\n100644 b\0' + sha) # ordering sha2 = hex_to_sha(b_sha) self.assertCheckSucceeds( t, b'100644 a\0' + sha + b'\n100644 b\0' + sha) self.assertCheckSucceeds( t, b'100644 a\0' + sha + b'\n100644 b\0' + sha2) self.assertCheckFails(t, b'100644 a\0' + sha + b'\n100755 a\0' + sha2) self.assertCheckFails(t, b'100644 b\0' + sha2 + b'\n100644 a\0' + sha) def test_iter(self): t = Tree() t[b'foo'] = (0o100644, a_sha) self.assertEqual(set([b'foo']), set(t)) class TagSerializeTests(TestCase): def test_serialize_simple(self): x = make_object( Tag, tagger=b'Jelmer Vernooij ', name=b'0.1', message=b'Tag 0.1', object=(Blob, b'd80c186a03f423a81b39df39dc87fd269736ca86'), tag_time=423423423, tag_timezone=0) self.assertEqual((b'object d80c186a03f423a81b39df39dc87fd269736ca86\n' b'type blob\n' b'tag 0.1\n' b'tagger Jelmer Vernooij ' b'423423423 +0000\n' b'\n' b'Tag 0.1'), x.as_raw_string()) def test_serialize_none_message(self): x = make_object( Tag, tagger=b'Jelmer Vernooij ', name=b'0.1', message=None, object=(Blob, b'd80c186a03f423a81b39df39dc87fd269736ca86'), tag_time=423423423, tag_timezone=0) self.assertEqual((b'object d80c186a03f423a81b39df39dc87fd269736ca86\n' b'type blob\n' b'tag 0.1\n' b'tagger Jelmer Vernooij ' b'423423423 +0000\n'), x.as_raw_string()) default_tagger = (b'Linus Torvalds ' b'1183319674 -0700') default_message = b"""Linux 2.6.22-rc7 -----BEGIN PGP SIGNATURE----- Version: GnuPG v1.4.7 (GNU/Linux) iD8DBQBGiAaAF3YsRnbiHLsRAitMAKCiLboJkQECM/jpYsY3WPfvUgLXkACgg3ql OK2XeQOiEeXtT76rV4t2WR4= =ivrA -----END PGP SIGNATURE----- """ class TagParseTests(ShaFileCheckTests): def make_tag_lines(self, object_sha=b'a38d6181ff27824c79fc7df825164a212eff6a3f', object_type_name=b'commit', name=b'v2.6.22-rc7', tagger=default_tagger, message=default_message): lines = [] if object_sha is not None: lines.append(b'object ' + object_sha) if object_type_name is not None: lines.append(b'type ' + object_type_name) if name is not None: lines.append(b'tag ' + name) if tagger is not None: lines.append(b'tagger ' + tagger) if message is not None: lines.append(b'') lines.append(message) return lines def make_tag_text(self, **kwargs): return b'\n'.join(self.make_tag_lines(**kwargs)) def test_parse(self): x = Tag() x.set_raw_string(self.make_tag_text()) self.assertEqual( b'Linus Torvalds ', x.tagger) self.assertEqual(b'v2.6.22-rc7', x.name) object_type, object_sha = x.object self.assertEqual(b'a38d6181ff27824c79fc7df825164a212eff6a3f', object_sha) self.assertEqual(Commit, object_type) self.assertEqual(datetime.datetime.utcfromtimestamp(x.tag_time), datetime.datetime(2007, 7, 1, 19, 54, 34)) self.assertEqual(-25200, x.tag_timezone) def test_parse_no_tagger(self): x = Tag() x.set_raw_string(self.make_tag_text(tagger=None)) self.assertEqual(None, x.tagger) self.assertEqual(b'v2.6.22-rc7', x.name) self.assertEqual(None, x.tag_time) def test_parse_no_message(self): x = Tag() x.set_raw_string(self.make_tag_text(message=None)) self.assertEqual(None, x.message) self.assertEqual( b'Linus Torvalds ', x.tagger) self.assertEqual(datetime.datetime.utcfromtimestamp(x.tag_time), datetime.datetime(2007, 7, 1, 19, 54, 34)) self.assertEqual(-25200, x.tag_timezone) self.assertEqual(b'v2.6.22-rc7', x.name) def test_check(self): self.assertCheckSucceeds(Tag, self.make_tag_text()) self.assertCheckFails(Tag, self.make_tag_text(object_sha=None)) self.assertCheckFails(Tag, self.make_tag_text(object_type_name=None)) self.assertCheckFails(Tag, self.make_tag_text(name=None)) self.assertCheckFails(Tag, self.make_tag_text(name=b'')) self.assertCheckFails(Tag, self.make_tag_text( object_type_name=b'foobar')) self.assertCheckFails(Tag, self.make_tag_text( tagger=b'some guy without an email address 1183319674 -0700')) self.assertCheckFails(Tag, self.make_tag_text( tagger=(b'Linus Torvalds ' b'Sun 7 Jul 2007 12:54:34 +0700'))) self.assertCheckFails(Tag, self.make_tag_text(object_sha=b'xxx')) def test_check_tag_with_unparseable_field(self): self.assertCheckFails(Tag, self.make_tag_text( tagger=(b'Linus Torvalds ' b'423423+0000'))) def test_check_tag_with_overflow_time(self): """Date with overflow should raise an ObjectFormatException when checked """ author = 'Some Dude %s +0000' % (MAX_TIME+1, ) tag = Tag.from_string(self.make_tag_text( tagger=(author.encode()))) with self.assertRaises(ObjectFormatException): tag.check() def test_check_duplicates(self): # duplicate each of the header fields for i in range(4): lines = self.make_tag_lines() lines.insert(i, lines[i]) self.assertCheckFails(Tag, b'\n'.join(lines)) def test_check_order(self): lines = self.make_tag_lines() headers = lines[:4] rest = lines[4:] # of all possible permutations, ensure only the original succeeds for perm in permutations(headers): perm = list(perm) text = b'\n'.join(perm + rest) if perm == headers: self.assertCheckSucceeds(Tag, text) else: self.assertCheckFails(Tag, text) def test_tree_copy_after_update(self): """Check Tree.id is correctly updated when the tree is copied after updated. """ shas = [] tree = Tree() shas.append(tree.id) tree.add(b'data', 0o644, Blob().id) copied = tree.copy() shas.append(tree.id) shas.append(copied.id) self.assertNotIn(shas[0], shas[1:]) self.assertEqual(shas[1], shas[2]) class CheckTests(TestCase): def test_check_hexsha(self): check_hexsha(a_sha, "failed to check good sha") self.assertRaises(ObjectFormatException, check_hexsha, b'1' * 39, 'sha too short') self.assertRaises(ObjectFormatException, check_hexsha, b'1' * 41, 'sha too long') self.assertRaises(ObjectFormatException, check_hexsha, b'x' * 40, 'invalid characters') def test_check_identity(self): check_identity(b'Dave Borowitz ', "failed to check good identity") check_identity(b'', "failed to check good identity") self.assertRaises(ObjectFormatException, check_identity, b'Dave Borowitz', "no email") self.assertRaises(ObjectFormatException, check_identity, b'Dave Borowitz ', "incomplete email") self.assertRaises(ObjectFormatException, check_identity, b'Dave Borowitz <', "typo") self.assertRaises(ObjectFormatException, check_identity, b'Dave Borowitz >', "typo") self.assertRaises(ObjectFormatException, check_identity, b'Dave Borowitz xxx', "trailing characters") class TimezoneTests(TestCase): def test_parse_timezone_utc(self): self.assertEqual((0, False), parse_timezone(b'+0000')) def test_parse_timezone_utc_negative(self): self.assertEqual((0, True), parse_timezone(b'-0000')) def test_generate_timezone_utc(self): self.assertEqual(b'+0000', format_timezone(0)) def test_generate_timezone_utc_negative(self): self.assertEqual(b'-0000', format_timezone(0, True)) def test_parse_timezone_cet(self): self.assertEqual((60 * 60, False), parse_timezone(b'+0100')) def test_format_timezone_cet(self): self.assertEqual(b'+0100', format_timezone(60 * 60)) def test_format_timezone_pdt(self): self.assertEqual(b'-0400', format_timezone(-4 * 60 * 60)) def test_parse_timezone_pdt(self): self.assertEqual((-4 * 60 * 60, False), parse_timezone(b'-0400')) def test_format_timezone_pdt_half(self): self.assertEqual(b'-0440', format_timezone(int(((-4 * 60) - 40) * 60))) def test_format_timezone_double_negative(self): self.assertEqual(b'--700', format_timezone(int(((7 * 60)) * 60), True)) def test_parse_timezone_pdt_half(self): self.assertEqual((((-4 * 60) - 40) * 60, False), parse_timezone(b'-0440')) def test_parse_timezone_double_negative(self): self.assertEqual( (int(((7 * 60)) * 60), False), parse_timezone(b'+700')) self.assertEqual( (int(((7 * 60)) * 60), True), parse_timezone(b'--700')) class ShaFileCopyTests(TestCase): def assert_copy(self, orig): oclass = object_class(orig.type_num) copy = orig.copy() self.assertTrue(isinstance(copy, oclass)) self.assertEqual(copy, orig) self.assertTrue(copy is not orig) def test_commit_copy(self): attrs = {'tree': b'd80c186a03f423a81b39df39dc87fd269736ca86', 'parents': [b'ab64bbdcc51b170d21588e5c5d391ee5c0c96dfd', b'4cffe90e0a41ad3f5190079d7c8f036bde29cbe6'], 'author': b'James Westby ', 'committer': b'James Westby ', 'commit_time': 1174773719, 'author_time': 1174773719, 'commit_timezone': 0, 'author_timezone': 0, 'message': b'Merge ../b\n'} commit = make_commit(**attrs) self.assert_copy(commit) def test_blob_copy(self): blob = make_object(Blob, data=b'i am a blob') self.assert_copy(blob) def test_tree_copy(self): blob = make_object(Blob, data=b'i am a blob') tree = Tree() tree[b'blob'] = (stat.S_IFREG, blob.id) self.assert_copy(tree) def test_tag_copy(self): tag = make_object( Tag, name=b'tag', message=b'', tagger=b'Tagger ', tag_time=12345, tag_timezone=0, object=(Commit, b'0' * 40)) self.assert_copy(tag) class ShaFileSerializeTests(TestCase): """`ShaFile` objects only gets serialized once if they haven't changed. """ @contextmanager def assert_serialization_on_change( self, obj, needs_serialization_after_change=True): old_id = obj.id self.assertFalse(obj._needs_serialization) yield obj if needs_serialization_after_change: self.assertTrue(obj._needs_serialization) else: self.assertFalse(obj._needs_serialization) new_id = obj.id self.assertFalse(obj._needs_serialization) self.assertNotEqual(old_id, new_id) def test_commit_serialize(self): attrs = {'tree': b'd80c186a03f423a81b39df39dc87fd269736ca86', 'parents': [b'ab64bbdcc51b170d21588e5c5d391ee5c0c96dfd', b'4cffe90e0a41ad3f5190079d7c8f036bde29cbe6'], 'author': b'James Westby ', 'committer': b'James Westby ', 'commit_time': 1174773719, 'author_time': 1174773719, 'commit_timezone': 0, 'author_timezone': 0, 'message': b'Merge ../b\n'} commit = make_commit(**attrs) with self.assert_serialization_on_change(commit): commit.parents = [b'ab64bbdcc51b170d21588e5c5d391ee5c0c96dfd'] def test_blob_serialize(self): blob = make_object(Blob, data=b'i am a blob') with self.assert_serialization_on_change( blob, needs_serialization_after_change=False): blob.data = b'i am another blob' def test_tree_serialize(self): blob = make_object(Blob, data=b'i am a blob') tree = Tree() tree[b'blob'] = (stat.S_IFREG, blob.id) with self.assert_serialization_on_change(tree): tree[b'blob2'] = (stat.S_IFREG, blob.id) def test_tag_serialize(self): tag = make_object( Tag, name=b'tag', message=b'', tagger=b'Tagger ', tag_time=12345, tag_timezone=0, object=(Commit, b'0' * 40)) with self.assert_serialization_on_change(tag): tag.message = b'new message' def test_tag_serialize_time_error(self): with self.assertRaises(ObjectFormatException): tag = make_object( Tag, name=b'tag', message=b'some message', tagger=b'Tagger 1174773719+0000', object=(Commit, b'0' * 40)) tag._deserialize(tag._serialize()) class PrettyFormatTreeEntryTests(TestCase): def test_format(self): self.assertEqual( '40000 tree 40820c38cfb182ce6c8b261555410d8382a5918b\tfoo\n', pretty_format_tree_entry( b"foo", 0o40000, b"40820c38cfb182ce6c8b261555410d8382a5918b"))