diff -Nru dulwich-0.8.2/bin/dulwich dulwich-0.8.3/bin/dulwich --- dulwich-0.8.2/bin/dulwich 2011-12-18 20:30:33.000000000 +0000 +++ dulwich-0.8.3/bin/dulwich 2012-01-21 21:20:17.000000000 +0000 @@ -54,8 +54,17 @@ 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] - graphwalker = r.get_graph_walker() - client.fetch(path, r.object_store, determine_wants) + client.fetch(path, r, determine_wants) + + +def cmd_fetch(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) def cmd_log(args): @@ -183,6 +192,7 @@ commands = { "commit": cmd_commit, "fetch-pack": cmd_fetch_pack, + "fetch": cmd_fetch, "dump-pack": cmd_dump_pack, "dump-index": cmd_dump_index, "init": cmd_init, diff -Nru dulwich-0.8.2/debian/changelog dulwich-0.8.3/debian/changelog --- dulwich-0.8.2/debian/changelog 2011-12-18 20:35:43.000000000 +0000 +++ dulwich-0.8.3/debian/changelog 2012-01-25 01:45:33.000000000 +0000 @@ -1,3 +1,9 @@ +dulwich (0.8.3-1) unstable; urgency=low + + * New upstream release. + + -- Jelmer Vernooij Sat, 21 Jan 2012 22:24:12 +0100 + dulwich (0.8.2-1) unstable; urgency=low * Fix Vcs URL. diff -Nru dulwich-0.8.2/debian/copyright dulwich-0.8.3/debian/copyright --- dulwich-0.8.2/debian/copyright 2010-12-28 00:49:51.000000000 +0000 +++ dulwich-0.8.3/debian/copyright 2012-01-25 01:42:52.000000000 +0000 @@ -1,21 +1,22 @@ -Format-Specification: http://wiki.debian.org/Proposals/CopyrightFormat?action=recall&rev=143 +Format: http://dep.debian.net/deps/dep5 Upstream-Name: dulwich +Upstream-Contact: Jelmer Vernooij +Source: http://samba.org/~jelmer/dulwich Debianized-By: Jelmer Vernooij Debianized-Date: Tue, 13 Jan 2009 16:56:47 +0100 -It was downloaded from http://launchpad.net/dulwich. - Files: * -Copyright 2005 Linus Torvalds -Copyright 2007 James Westby -Copyright 2007-2009 Jelmer Vernooij -Copyright 2008 John Carr -License: GPL-2 +Copyright: 2005 Linus Torvalds +Copyright: 2007 James Westby +Copyright: 2007-2012 Jelmer Vernooij +Copyright: 2008 John Carr +Copyright: 2010 Google, Inc. +License: GPL-2+ On Debian systems the full text of the GNU General Public License version 2 can be found in the `/usr/share/common-licenses/GPL-2' file. Files: debian/* -Copyright 2009 Jelmer Vernooij +Copyright: 2009-2012 Jelmer Vernooij License: GPL-2+ On Debian systems the full text of the GNU General Public License version 2 can be found in the `/usr/share/common-licenses/GPL-2' file. diff -Nru dulwich-0.8.2/docs/tutorial/index.txt dulwich-0.8.3/docs/tutorial/index.txt --- dulwich-0.8.2/docs/tutorial/index.txt 2011-12-18 20:30:33.000000000 +0000 +++ dulwich-0.8.3/docs/tutorial/index.txt 2012-01-21 21:20:17.000000000 +0000 @@ -10,5 +10,6 @@ introduction repo object-store + remote conclusion diff -Nru dulwich-0.8.2/docs/tutorial/remote.txt dulwich-0.8.3/docs/tutorial/remote.txt --- dulwich-0.8.2/docs/tutorial/remote.txt 1970-01-01 00:00:00.000000000 +0000 +++ dulwich-0.8.3/docs/tutorial/remote.txt 2012-01-21 21:20:17.000000000 +0000 @@ -0,0 +1,83 @@ +.. _tutorial-remote: + +Most of the tests in this file require a Dulwich server, so let's start one: + + >>> from dulwich.repo import Repo + >>> from dulwich.server import DictBackend, TCPGitServer + >>> import threading + >>> repo = Repo.init("remote", mkdir=True) + >>> cid = repo.do_commit("message", committer="Jelmer ") + >>> backend = DictBackend({'/': repo}) + >>> dul_server = TCPGitServer(backend, 'localhost', 0) + >>> threading.Thread(target=dul_server.serve).start() + >>> server_address, server_port = dul_server.socket.getsockname() + +Remote repositories +=================== + +The interface for remote Git repositories is different from that +for local repositories. + +The Git smart server protocol provides three basic operations: + + * upload-pack - provides a pack with objects requested by the client + * receive-pack - imports a pack with objects provided by the client + * upload-archive - provides a tarball with the contents of a specific revision + +The smart server protocol can be accessed over either plain TCP (git://), +SSH (git+ssh://) or tunneled over HTTP (http://). + +Dulwich provides support for accessing remote repositories in +``dulwich.client``. To create a new client, you can either construct +one manually:: + + >>> from dulwich.client import TCPGitClient + >>> client = TCPGitClient(server_address, server_port) + +Retrieving raw pack files +------------------------- + +The client object can then be used to retrieve a pack. The ``fetch_pack`` +method takes a ``determine_wants`` callback argument, which allows the +client to determine which objects it wants to end up with:: + + >>> def determine_wants(refs): + ... # retrieve all objects + ... return refs.values() + +Another required object is a "graph walker", which is used to determine +which objects that the client already has should not be sent again +by the server. Here in the tutorial we'll just use a dummy graph walker +which claims that the client doesn't have any objects:: + + >>> class DummyGraphWalker(object): + ... def ack(self, sha): pass + ... def next(self): pass + +With the determine_wants function in place, we can now fetch a pack, +which we will write to a ``StringIO`` object:: + + >>> from cStringIO import StringIO + >>> f = StringIO() + >>> remote_refs = client.fetch_pack("/", determine_wants, + ... DummyGraphWalker(), pack_data=f.write) + +``f`` will now contain a full pack file:: + + >>> f.getvalue()[:4] + 'PACK' + +Fetching objects into a local repository +---------------------------------------- + +It also possible to fetch from a remote repository into a local repository, +in which case dulwich takes care of providing the right graph walker, and +importing the received pack file into the local repository:: + + >>> from dulwich.repo import Repo + >>> local = Repo.init("local", mkdir=True) + >>> remote_refs = client.fetch("/", local) + +Let's show down the server now that all tests have been run:: + + >>> dul_server.shutdown() diff -Nru dulwich-0.8.2/docs/tutorial/repo.txt dulwich-0.8.3/docs/tutorial/repo.txt --- dulwich-0.8.2/docs/tutorial/repo.txt 2011-12-18 20:30:33.000000000 +0000 +++ dulwich-0.8.3/docs/tutorial/repo.txt 2012-01-21 21:20:17.000000000 +0000 @@ -1,6 +1,6 @@ .. _tutorial-repo: -The Repository +The repository ============== After this introduction, let's start directly with code:: @@ -18,6 +18,9 @@ contains itself the "branches", "hooks"... folders. These are used for published repositories (mirrors). They do not have a working tree. +Creating a repository +--------------------- + Let's create a folder and turn it into a repository, like ``git init`` would:: >>> from os import mkdir @@ -28,3 +31,70 @@ You can already look a the structure of the "myrepo/.git" folder, though it is mostly empty for now. + +Opening an existing repository +------------------------------ + +To reopen an existing repository, simply pass its path to the constructor +of ``Repo``:: + + >>> repo = Repo("myrepo") + >>> repo + + +Opening the index +----------------- + +The index is used as a staging area. Once you do a commit, +the files tracked in the index will be recorded as the contents of the new +commit. As mentioned earlier, only non-bare repositories have a working tree, +so only non-bare repositories will have an index, too. To open the index, simply +call:: + + >>> index = repo.open_index() + >>> repr(index).replace('\\\\', '/') + "Index('myrepo/.git/index')" + +Since the repository was just created, the index will be empty:: + + >>> list(index) + [] + +Staging new files +----------------- + +The repository allows "staging" files. Only files can be staged - directories +aren't tracked explicitly by git. Let's create a simple text file and stage it:: + + >>> f = open('myrepo/foo', 'w') + >>> f.write("monty") + >>> f.close() + + >>> repo.stage(["foo"]) + +It will now show up in the index:: + + >>> list(repo.open_index()) + ['foo'] + + +Creating new commits +-------------------- + +Now that we have staged a change, we can commit it. The easiest way to +do this is by using ``Repo.do_commit``. It is also possible to manipulate +the lower-level objects involved in this, but we'll leave that for a +separate chapter of the tutorial. + +To create a simple commit on the current branch, it is only necessary +to specify the message. The committer and author will be retrieved from the +repository configuration or global configuration if they are not specified:: + + >>> commit_id = repo.do_commit( + ... "The first commit", committer="Jelmer Vernooij ") + +``do_commit`` returns the SHA1 of the commit. Since the commit was to the +default branch, the repository's head will now be set to that commit:: + + >>> repo.head() == commit_id + True diff -Nru dulwich-0.8.2/dulwich/client.py dulwich-0.8.3/dulwich/client.py --- dulwich-0.8.2/dulwich/client.py 2011-12-18 20:30:33.000000000 +0000 +++ dulwich-0.8.3/dulwich/client.py 2012-01-21 21:20:17.000000000 +0000 @@ -17,7 +17,24 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. -"""Client side support for the Git protocol.""" +"""Client side support for the Git protocol. + +The Dulwich client supports the following capabilities: + + * thin-pack + * multi_ack_detailed + * multi_ack + * side-band-64k + * ofs-delta + * report-status + * delete-refs + +Known capabilities that are not supported: + + * shallow + * no-progress + * include-tag +""" __docformat__ = 'restructuredText' @@ -177,7 +194,7 @@ :param determine_wants: Optional function to determine what refs to fetch :param progress: Optional progress function - :return: remote refs + :return: remote refs as dictionary """ if determine_wants is None: determine_wants = target.object_store.determine_wants_all @@ -189,7 +206,7 @@ commit() def fetch_pack(self, path, determine_wants, graph_walker, pack_data, - progress): + progress=None): """Retrieve a pack from a git smart server. :param determine_wants: Callback that returns list of commits to fetch @@ -286,7 +303,7 @@ proto.write_pkt_line(None) return (have, want) - def _handle_receive_pack_tail(self, proto, capabilities, progress): + def _handle_receive_pack_tail(self, proto, capabilities, progress=None): """Handle the tail of a 'git-receive-pack' request. :param proto: Protocol object to read from @@ -298,6 +315,8 @@ else: report_status_parser = None if "side-band-64k" in capabilities: + if progress is None: + progress = lambda x: None channel_callbacks = { 2: progress } if 'report-status' in capabilities: channel_callbacks[1] = PktLineParser( @@ -351,7 +370,7 @@ proto.write_pkt_line('done\n') def _handle_upload_pack_tail(self, proto, capabilities, graph_walker, - pack_data, progress, rbufsize=_RBUFSIZE): + pack_data, progress=None, rbufsize=_RBUFSIZE): """Handle the tail of a 'git-upload-pack' request. :param proto: Protocol object to read from @@ -371,6 +390,9 @@ break pkt = proto.read_pkt_line() if "side-band-64k" in capabilities: + if progress is None: + # Just ignore progress data + progress = lambda x: None self._read_side_band64k_data(proto, {1: pack_data, 2: progress}) # wait for EOF before returning data = proto.read() @@ -455,6 +477,8 @@ except: proto.write_pkt_line(None) raise + if wants is not None: + wants = [cid for cid in wants if cid != ZERO_SHA] if not wants: proto.write_pkt_line(None) return refs @@ -707,19 +731,22 @@ return new_refs def fetch_pack(self, path, determine_wants, graph_walker, pack_data, - progress): + progress=None): """Retrieve a pack from a git smart server. :param determine_wants: Callback that returns list of commits to fetch :param graph_walker: Object with next() and ack(). :param pack_data: Callback called for each bit of data in the pack :param progress: Callback for progress reports (strings) + :return: Dictionary with the refs of the remote repository """ url = self._get_url(path) refs, server_capabilities = self._discover_references( "git-upload-pack", url) negotiated_capabilities = list(server_capabilities) wants = determine_wants(refs) + if wants is not None: + wants = [cid for cid in wants if cid != ZERO_SHA] if not wants: return refs if self.dumb: diff -Nru dulwich-0.8.2/dulwich/config.py dulwich-0.8.3/dulwich/config.py --- dulwich-0.8.2/dulwich/config.py 1970-01-01 00:00:00.000000000 +0000 +++ dulwich-0.8.3/dulwich/config.py 2012-01-21 21:20:17.000000000 +0000 @@ -0,0 +1,343 @@ +# config.py - Reading and writing Git config files +# Copyright (C) 2011 Jelmer Vernooij +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; version 2 +# of the License or (at your option) a later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. + +"""Reading and writing Git configuration files. + +TODO: + * preserve formatting when updating configuration files + * treat subsection names as case-insensitive for [branch.foo] style + subsections +""" + +import errno +import os +import re + +from dulwich.file import GitFile + + +class Config(object): + """A Git configuration.""" + + def get(self, section, name): + """Retrieve the contents of a configuration setting. + + :param section: Tuple with section name and optional subsection namee + :param subsection: Subsection name + :return: Contents of the setting + :raise KeyError: if the value is not set + """ + raise NotImplementedError(self.get) + + def get_boolean(self, section, name, default=None): + """Retrieve a configuration setting as boolean. + + :param section: Tuple with section name and optional subsection namee + :param name: Name of the setting, including section and possible + subsection. + :return: Contents of the setting + :raise KeyError: if the value is not set + """ + try: + value = self.get(section, name) + except KeyError: + return default + if value.lower() == "true": + return True + elif value.lower() == "false": + return False + raise ValueError("not a valid boolean string: %r" % value) + + def set(self, section, name, value): + """Set a configuration value. + + :param name: Name of the configuration value, including section + and optional subsection + :param: Value of the setting + """ + raise NotImplementedError(self.set) + + +class ConfigDict(Config): + """Git configuration stored in a dictionary.""" + + def __init__(self, values=None): + """Create a new ConfigDict.""" + if values is None: + values = {} + self._values = values + + def __repr__(self): + return "%s(%r)" % (self.__class__.__name__, self._values) + + def __eq__(self, other): + return ( + isinstance(other, self.__class__) and + other._values == self._values) + + @classmethod + def _parse_setting(cls, name): + parts = name.split(".") + if len(parts) == 3: + return (parts[0], parts[1], parts[2]) + else: + return (parts[0], None, parts[1]) + + def get(self, section, name): + if isinstance(section, basestring): + section = (section, ) + if len(section) > 1: + try: + return self._values[section][name] + except KeyError: + pass + return self._values[(section[0],)][name] + + def set(self, section, name, value): + if isinstance(section, basestring): + section = (section, ) + self._values.setdefault(section, {})[name] = value + + +def _format_string(value): + if (value.startswith(" ") or + value.startswith("\t") or + value.endswith(" ") or + value.endswith("\t")): + return '"%s"' % _escape_value(value) + return _escape_value(value) + + +def _parse_string(value): + value = value.strip() + ret = [] + block = [] + in_quotes = False + for c in value: + if c == "\"": + in_quotes = (not in_quotes) + ret.append(_unescape_value("".join(block))) + block = [] + elif c in ("#", ";") and not in_quotes: + # the rest of the line is a comment + break + else: + block.append(c) + + if in_quotes: + raise ValueError("value starts with quote but lacks end quote") + + ret.append(_unescape_value("".join(block)).rstrip()) + + return "".join(ret) + + +def _unescape_value(value): + """Unescape a value.""" + def unescape(c): + return { + "\\\\": "\\", + "\\\"": "\"", + "\\n": "\n", + "\\t": "\t", + "\\b": "\b", + }[c.group(0)] + return re.sub(r"(\\.)", unescape, value) + + +def _escape_value(value): + """Escape a value.""" + return value.replace("\\", "\\\\").replace("\n", "\\n").replace("\t", "\\t").replace("\"", "\\\"") + + +def _check_variable_name(name): + for c in name: + if not c.isalnum() and c != '-': + return False + return True + + +def _check_section_name(name): + for c in name: + if not c.isalnum() and c not in ('-', '.'): + return False + return True + + +def _strip_comments(line): + line = line.split("#")[0] + line = line.split(";")[0] + return line + + +class ConfigFile(ConfigDict): + """A Git configuration file, like .git/config or ~/.gitconfig. + """ + + @classmethod + def from_file(cls, f): + """Read configuration from a file-like object.""" + ret = cls() + section = None + setting = None + for lineno, line in enumerate(f.readlines()): + line = line.lstrip() + if setting is None: + if _strip_comments(line).strip() == "": + continue + if line[0] == "[": + line = _strip_comments(line).rstrip() + if line[-1] != "]": + raise ValueError("expected trailing ]") + key = line.strip() + pts = key[1:-1].split(" ", 1) + pts[0] = pts[0].lower() + if len(pts) == 2: + if pts[1][0] != "\"" or pts[1][-1] != "\"": + raise ValueError( + "Invalid subsection " + pts[1]) + else: + pts[1] = pts[1][1:-1] + if not _check_section_name(pts[0]): + raise ValueError("invalid section name %s" % + pts[0]) + section = (pts[0], pts[1]) + else: + if not _check_section_name(pts[0]): + raise ValueError("invalid section name %s" % + pts[0]) + pts = pts[0].split(".", 1) + if len(pts) == 2: + section = (pts[0], pts[1]) + else: + section = (pts[0], ) + ret._values[section] = {} + else: + if section is None: + raise ValueError("setting %r without section" % line) + try: + setting, value = line.split("=", 1) + except ValueError: + setting = line + value = "true" + setting = setting.strip().lower() + if not _check_variable_name(setting): + raise ValueError("invalid variable name %s" % setting) + if value.endswith("\\\n"): + value = value[:-2] + continuation = True + else: + continuation = False + value = _parse_string(value) + ret._values[section][setting] = value + if not continuation: + setting = None + else: # continuation line + if line.endswith("\\\n"): + line = line[:-2] + continuation = True + else: + continuation = False + value = _parse_string(line) + ret._values[section][setting] += value + if not continuation: + setting = None + return ret + + @classmethod + def from_path(cls, path): + """Read configuration from a file on disk.""" + f = GitFile(path, 'rb') + try: + ret = cls.from_file(f) + ret.path = path + return ret + finally: + f.close() + + def write_to_path(self, path=None): + """Write configuration to a file on disk.""" + if path is None: + path = self.path + f = GitFile(path, 'wb') + try: + self.write_to_file(f) + finally: + f.close() + + def write_to_file(self, f): + """Write configuration to a file-like object.""" + for section, values in self._values.iteritems(): + try: + section_name, subsection_name = section + except ValueError: + (section_name, ) = section + subsection_name = None + if subsection_name is None: + f.write("[%s]\n" % section_name) + else: + f.write("[%s \"%s\"]\n" % (section_name, subsection_name)) + for key, value in values.iteritems(): + f.write("%s = %s\n" % (key, _escape_value(value))) + + +class StackedConfig(Config): + """Configuration which reads from multiple config files..""" + + def __init__(self, backends, writable=None): + self.backends = backends + self.writable = writable + + def __repr__(self): + return "<%s for %r>" % (self.__class__.__name__, self.backends) + + @classmethod + def default_backends(cls): + """Retrieve the default configuration. + + This will look in the repository configuration (if for_path is + specified), the users' home directory and the system + configuration. + """ + paths = [] + paths.append(os.path.expanduser("~/.gitconfig")) + paths.append("/etc/gitconfig") + backends = [] + for path in paths: + try: + cf = ConfigFile.from_path(path) + except (IOError, OSError), e: + if e.errno != errno.ENOENT: + raise + else: + continue + backends.append(cf) + return backends + + def get(self, section, name): + for backend in self.backends: + try: + return backend.get(section, name) + except KeyError: + pass + raise KeyError(name) + + def set(self, section, name, value): + if self.writable is None: + raise NotImplementedError(self.set) + return self.writable.set(section, name, value) diff -Nru dulwich-0.8.2/dulwich/index.py dulwich-0.8.3/dulwich/index.py --- dulwich-0.8.2/dulwich/index.py 2011-12-18 20:30:33.000000000 +0000 +++ dulwich-0.8.3/dulwich/index.py 2012-01-21 21:20:17.000000000 +0000 @@ -193,6 +193,9 @@ self.clear() self.read() + def __repr__(self): + return "%s(%r)" % (self.__class__.__name__, self._filename) + def write(self): """Write current contents of index to disk.""" f = GitFile(self._filename, 'wb') @@ -372,13 +375,15 @@ yield ((None, name), (None, other_mode), (None, other_sha)) -def index_entry_from_stat(stat_val, hex_sha, flags): +def index_entry_from_stat(stat_val, hex_sha, flags, mode=None): """Create a new index entry from a stat value. :param stat_val: POSIX stat_result instance :param hex_sha: Hex sha of the object :param flags: Index flags """ + if mode is None: + mode = cleanup_mode(stat_val.st_mode) return (stat_val.st_ctime, stat_val.st_mtime, stat_val.st_dev, - stat_val.st_ino, stat_val.st_mode, stat_val.st_uid, + stat_val.st_ino, mode, stat_val.st_uid, stat_val.st_gid, stat_val.st_size, hex_sha, flags) diff -Nru dulwich-0.8.2/dulwich/__init__.py dulwich-0.8.3/dulwich/__init__.py --- dulwich-0.8.2/dulwich/__init__.py 2011-12-18 20:30:33.000000000 +0000 +++ dulwich-0.8.3/dulwich/__init__.py 2012-01-21 21:20:17.000000000 +0000 @@ -23,4 +23,4 @@ from dulwich import (client, protocol, repo, server) -__version__ = (0, 8, 2) +__version__ = (0, 8, 3) diff -Nru dulwich-0.8.2/dulwich/objects.py dulwich-0.8.3/dulwich/objects.py --- dulwich-0.8.2/dulwich/objects.py 2011-12-18 20:30:33.000000000 +0000 +++ dulwich-0.8.3/dulwich/objects.py 2012-01-21 21:20:17.000000000 +0000 @@ -19,7 +19,6 @@ """Access to base git objects.""" - import binascii from cStringIO import ( StringIO, @@ -64,6 +63,11 @@ S_IFGITLINK = 0160000 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) @@ -114,6 +118,8 @@ def serializable_property(name, docstring=None): + """A property that helps tracking whether serialization is necessary. + """ def set(obj, value): obj._ensure_parsed() setattr(obj, "_"+name, value) @@ -135,6 +141,12 @@ 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 + """ try: hex_to_sha(hex) except (TypeError, AssertionError): @@ -168,9 +180,11 @@ 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 @@ -213,6 +227,10 @@ 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(): @@ -220,9 +238,15 @@ yield compobj.flush() def as_legacy_object(self): + """Return string representing the object in the experimental format. + """ return "".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_parsing: self._ensure_parsed() elif self._needs_serialization: @@ -230,15 +254,22 @@ return self._chunked_text def as_raw_string(self): + """Return raw string with serialization of the object. + + :return: String object + """ return "".join(self.as_raw_chunks()) 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 _ensure_parsed(self): @@ -256,11 +287,13 @@ self._needs_parsing = False def set_raw_string(self, text): + """Set the contents of this object from a serialized string.""" if type(text) != str: raise TypeError(text) self.set_raw_chunks([text]) def set_raw_chunks(self, chunks): + """Set the contents of this object from a list of chunks.""" self._chunked_text = chunks self._deserialize(chunks) self._sha = None @@ -339,6 +372,7 @@ @classmethod def from_path(cls, path): + """Open a SHA file from disk.""" f = GitFile(path, 'rb') try: obj = cls.from_file(f) @@ -454,12 +488,15 @@ @property def id(self): + """The hex SHA of this object.""" return self.sha().hexdigest() 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. @@ -557,6 +594,7 @@ def parse_tag(text): + """Parse a tag object.""" return _parse_tag_or_commit(text) diff -Nru dulwich-0.8.2/dulwich/object_store.py dulwich-0.8.3/dulwich/object_store.py --- dulwich-0.8.2/dulwich/object_store.py 2011-12-18 20:30:33.000000000 +0000 +++ dulwich-0.8.3/dulwich/object_store.py 2012-01-21 21:20:17.000000000 +0000 @@ -132,8 +132,8 @@ def tree_changes(self, source, target, want_unchanged=False): """Find the differences between the contents of two trees - :param object_store: Object store to use for retrieving tree contents - :param tree: SHA1 of the root tree + :param source: SHA1 of the source tree + :param target: SHA1 of the target tree :param want_unchanged: Whether unchanged files should be reported :return: Iterator over tuples with (oldpath, newpath), (oldmode, newmode), (oldsha, newsha) @@ -471,9 +471,15 @@ f.seek(0) write_pack_header(f, len(entries) + len(indexer.ext_refs())) + # Must flush before reading (http://bugs.python.org/issue3207) + f.flush() + # Rescan the rest of the pack, computing the SHA with the new header. new_sha = compute_file_sha(f, end_ofs=-20) + # Must reposition before writing (http://bugs.python.org/issue3207) + f.seek(0, os.SEEK_CUR) + # Complete the pack. for ext_sha in indexer.ext_refs(): assert len(ext_sha) == 20 diff -Nru dulwich-0.8.2/dulwich/repo.py dulwich-0.8.3/dulwich/repo.py --- dulwich-0.8.2/dulwich/repo.py 2011-12-18 20:30:33.000000000 +0000 +++ dulwich-0.8.3/dulwich/repo.py 2012-01-21 21:20:17.000000000 +0000 @@ -19,7 +19,13 @@ # MA 02110-1301, USA. -"""Repository access.""" +"""Repository access. + +This module contains the base class for git repositories +(BaseRepo) and an implementation which uses a repository on +local disk (Repo). + +""" from cStringIO import StringIO import errno @@ -791,21 +797,34 @@ :ivar object_store: Dictionary-like object for accessing the objects - :ivar refs: Dictionary-like object with the refs in this repository + :ivar refs: Dictionary-like object with the refs in this + repository """ def __init__(self, object_store, refs): + """Open a repository. + + This shouldn't be called directly, but rather through one of the + base classes, such as MemoryRepo or Repo. + + :param object_store: Object store to use + :param refs: Refs container to use + """ self.object_store = object_store self.refs = refs def _init_files(self, bare): """Initialize a default set of named files.""" + from dulwich.config import ConfigFile self._put_named_file('description', "Unnamed repository") - self._put_named_file('config', ('[core]\n' - 'repositoryformatversion = 0\n' - 'filemode = true\n' - 'bare = ' + str(bare).lower() + '\n' - 'logallrefupdates = true\n')) + f = StringIO() + cf = ConfigFile() + cf.set("core", "repositoryformatversion", "0") + cf.set("core", "filemode", "true") + cf.set("core", "bare", str(bare).lower()) + cf.set("core", "logallrefupdates", "true") + cf.write_to_file(f) + self._put_named_file('config', f.getvalue()) self._put_named_file(os.path.join('info', 'exclude'), '') def get_named_file(self, path): @@ -877,6 +896,14 @@ get_tagged)) def get_graph_walker(self, heads=None): + """Retrieve a graph walker. + + A graph walker is used by a remote repository (or proxy) + to find out which objects are present in this repository. + + :param heads: Repository heads to use (optional) + :return: A graph walker object + """ if heads is None: heads = self.refs.as_dict('refs/heads').values() return self.object_store.get_graph_walker(heads) @@ -911,17 +938,50 @@ return ret def get_object(self, sha): + """Retrieve the object with the specified SHA. + + :param sha: SHA to retrieve + :return: A ShaFile object + :raise KeyError: when the object can not be found + """ return self.object_store[sha] def get_parents(self, sha): + """Retrieve the parents of a specific commit. + + :param sha: SHA of the commit for which to retrieve the parents + :return: List of parents + """ return self.commit(sha).parents def get_config(self): - import ConfigParser - p = ConfigParser.RawConfigParser() - p.read(os.path.join(self._controldir, 'config')) - return dict((section, dict(p.items(section))) - for section in p.sections()) + """Retrieve the config object. + + :return: `ConfigFile` object for the ``.git/config`` file. + """ + from dulwich.config import ConfigFile + path = os.path.join(self._controldir, 'config') + try: + return ConfigFile.from_path(path) + except (IOError, OSError), e: + if e.errno != errno.ENOENT: + raise + ret = ConfigFile() + ret.path = path + return ret + + def get_config_stack(self): + """Return a config stack for this repository. + + This stack accesses the configuration for both this repository + itself (.git/config) and the global configuration, which usually + lives in ~/.gitconfig. + + :return: `Config` instance for this repository + """ + from dulwich.config import StackedConfig + backends = [self.get_config()] + StackedConfig.default_backends() + return StackedConfig(backends, writable=backends[0]) def commit(self, sha): """Retrieve the commit with a particular SHA. @@ -1028,6 +1088,12 @@ return [e.commit for e in self.get_walker(include=[head])] def __getitem__(self, name): + """Retrieve a Git object by SHA1 or ref. + + :param name: A Git object SHA1 or a ref name + :return: A `ShaFile` object, such as a Commit or Blob + :raise KeyError: when the specified ref or object does not exist + """ if len(name) in (20, 40): try: return self.object_store[name] @@ -1038,16 +1104,22 @@ except RefFormatError: raise KeyError(name) - def __iter__(self): - raise NotImplementedError(self.__iter__) - def __contains__(self, name): + """Check if a specific Git object or ref is present. + + :param name: Git object SHA1 or ref name + """ if len(name) in (20, 40): return name in self.object_store or name in self.refs else: return name in self.refs def __setitem__(self, name, value): + """Set a ref. + + :param name: ref name + :param value: Ref value - either a ShaFile object, or a hex sha + """ if name.startswith("refs/") or name == "HEAD": if isinstance(value, ShaFile): self.refs[name] = value.id @@ -1059,11 +1131,21 @@ raise ValueError(name) def __delitem__(self, name): - if name.startswith("refs") or name == "HEAD": + """Remove a ref. + + :param name: Name of the ref to remove + """ + if name.startswith("refs/") or name == "HEAD": del self.refs[name] else: raise ValueError(name) + def _get_user_identity(self): + config = self.get_config_stack() + return "%s <%s>" % ( + config.get(("user", ), "name"), + config.get(("user", ), "email")) + def do_commit(self, message=None, committer=None, author=None, commit_timestamp=None, commit_timezone=None, author_timestamp=None, @@ -1098,9 +1180,8 @@ if merge_heads is None: # FIXME: Read merge heads from .git/MERGE_HEADS merge_heads = [] - # TODO: Allow username to be missing, and get it from .git/config if committer is None: - raise ValueError("committer not set") + committer = self._get_user_identity() c.committer = committer if commit_timestamp is None: commit_timestamp = time.time() @@ -1142,7 +1223,13 @@ class Repo(BaseRepo): - """A git repository backed by local disk.""" + """A git repository backed by local disk. + + To open an existing repository, call the contructor with + the path of the repository. + + To create a new repository, use the Repo.init class method. + """ def __init__(self, root): if os.path.isdir(os.path.join(root, ".git", OBJECTDIR)): @@ -1219,11 +1306,12 @@ :param paths: List of paths, relative to the repository path """ - from dulwich.index import cleanup_mode + if isinstance(paths, basestring): + paths = [paths] + from dulwich.index import index_entry_from_stat index = self.open_index() for path in paths: full_path = os.path.join(self.path, path) - blob = Blob() try: st = os.stat(full_path) except OSError: @@ -1231,21 +1319,20 @@ try: del index[path] except KeyError: - pass # Doesn't exist in the index either + pass # already removed else: + blob = Blob() f = open(full_path, 'rb') try: blob.data = f.read() finally: f.close() self.object_store.add_object(blob) - # XXX: Cleanup some of the other file properties as well? - index[path] = (st.st_ctime, st.st_mtime, st.st_dev, st.st_ino, - cleanup_mode(st.st_mode), st.st_uid, st.st_gid, st.st_size, - blob.id, 0) + index[path] = index_entry_from_stat(st, blob.id, 0) index.write() - def clone(self, target_path, mkdir=True, bare=False, origin="origin"): + def clone(self, target_path, mkdir=True, bare=False, + origin="origin"): """Clone this repository. :param target_path: Target path @@ -1285,6 +1372,12 @@ @classmethod def init(cls, path, mkdir=False): + """Create a new repository. + + :param path: Path in which to create the repository + :param mkdir: Whether to create the directory + :return: `Repo` instance + """ if mkdir: os.mkdir(path) controldir = os.path.join(path, ".git") @@ -1294,6 +1387,13 @@ @classmethod def init_bare(cls, path): + """Create a new bare repository. + + ``path`` should already exist and be an emty directory. + + :param path: Path to create bare repository in + :return: a `Repo` instance + """ return cls._init_maybe_bare(path, True) create = init_bare @@ -1340,6 +1440,13 @@ @classmethod def init_bare(cls, objects, refs): + """Create a new bare repository in memory. + + :param objects: Objects for the new repository, + as iterable + :param refs: Refs as dictionary, mapping names + to object SHA1s + """ ret = cls() for obj in objects: ret.object_store.add_object(obj) diff -Nru dulwich-0.8.2/dulwich/server.py dulwich-0.8.3/dulwich/server.py --- dulwich-0.8.2/dulwich/server.py 2011-12-18 20:30:33.000000000 +0000 +++ dulwich-0.8.3/dulwich/server.py 2012-01-21 21:20:17.000000000 +0000 @@ -23,8 +23,22 @@ * Documentation/technical/protocol-capabilities.txt * Documentation/technical/pack-protocol.txt -""" +Currently supported capabilities: + + * include-tag + * thin-pack + * multi_ack_detailed + * multi_ack + * side-band-64k + * ofs-delta + * no-progress + * report-status + * delete-refs + +Known capabilities that are not supported: + * shallow (http://pad.lv/909524) +""" import collections import os diff -Nru dulwich-0.8.2/dulwich/tests/compat/test_client.py dulwich-0.8.3/dulwich/tests/compat/test_client.py --- dulwich-0.8.2/dulwich/tests/compat/test_client.py 2011-12-18 20:30:33.000000000 +0000 +++ dulwich-0.8.3/dulwich/tests/compat/test_client.py 2012-01-21 21:20:17.000000000 +0000 @@ -191,6 +191,15 @@ map(lambda r: dest.refs.set_if_equals(r[0], None, r[1]), refs.items()) self.assertDestEqualsSrc() + def test_fetch_pack_zero_sha(self): + # zero sha1s are already present on the client, and should + # be ignored + c = self._client() + dest = repo.Repo(os.path.join(self.gitroot, 'dest')) + refs = c.fetch(self._build_path('/server_new.export'), dest, + lambda refs: [protocol.ZERO_SHA]) + map(lambda r: dest.refs.set_if_equals(r[0], None, r[1]), refs.items()) + def test_send_remove_branch(self): dest = repo.Repo(os.path.join(self.gitroot, 'dest')) dummy_commit = self.make_dummy_commit(dest) diff -Nru dulwich-0.8.2/dulwich/tests/__init__.py dulwich-0.8.3/dulwich/tests/__init__.py --- dulwich-0.8.2/dulwich/tests/__init__.py 2011-12-18 20:30:33.000000000 +0000 +++ dulwich-0.8.3/dulwich/tests/__init__.py 2012-01-21 21:20:17.000000000 +0000 @@ -68,6 +68,7 @@ return subprocess.Popen(argv, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE, + universal_newlines=True, env=env) @@ -75,6 +76,7 @@ names = [ 'blackbox', 'client', + 'config', 'diff_tree', 'fastexport', 'file', @@ -100,6 +102,7 @@ 'introduction', 'repo', 'object-store', + 'remote', 'conclusion', ] tutorial_files = ["../../docs/tutorial/%s.txt" % name for name in tutorial] @@ -108,8 +111,8 @@ test.__dulwich_tempdir = tempfile.mkdtemp() os.chdir(test.__dulwich_tempdir) def teardown(test): - shutil.rmtree(test.__dulwich_tempdir) os.chdir(test.__old_cwd) + shutil.rmtree(test.__dulwich_tempdir) return doctest.DocFileSuite(setUp=setup, tearDown=teardown, *tutorial_files) diff -Nru dulwich-0.8.2/dulwich/tests/test_config.py dulwich-0.8.3/dulwich/tests/test_config.py --- dulwich-0.8.2/dulwich/tests/test_config.py 1970-01-01 00:00:00.000000000 +0000 +++ dulwich-0.8.3/dulwich/tests/test_config.py 2012-01-21 21:20:17.000000000 +0000 @@ -0,0 +1,248 @@ +# test_config.py -- Tests for reading and writing configuration files +# Copyright (C) 2011 Jelmer Vernooij +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# or (at your option) a later version of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. + +"""Tests for reading and writing configuraiton files.""" + +from cStringIO import StringIO +from dulwich.config import ( + ConfigDict, + ConfigFile, + StackedConfig, + _check_section_name, + _check_variable_name, + _format_string, + _escape_value, + _parse_string, + _unescape_value, + ) +from dulwich.tests import TestCase + + +class ConfigFileTests(TestCase): + + def from_file(self, text): + return ConfigFile.from_file(StringIO(text)) + + def test_empty(self): + ConfigFile() + + def test_eq(self): + self.assertEquals(ConfigFile(), ConfigFile()) + + def test_default_config(self): + cf = self.from_file("""[core] + repositoryformatversion = 0 + filemode = true + bare = false + logallrefupdates = true +""") + self.assertEquals(ConfigFile({("core", ): { + "repositoryformatversion": "0", + "filemode": "true", + "bare": "false", + "logallrefupdates": "true"}}), cf) + + def test_from_file_empty(self): + cf = self.from_file("") + self.assertEquals(ConfigFile(), cf) + + def test_empty_line_before_section(self): + cf = self.from_file("\n[section]\n") + self.assertEquals(ConfigFile({("section", ): {}}), cf) + + def test_comment_before_section(self): + cf = self.from_file("# foo\n[section]\n") + self.assertEquals(ConfigFile({("section", ): {}}), cf) + + def test_comment_after_section(self): + cf = self.from_file("[section] # foo\n") + self.assertEquals(ConfigFile({("section", ): {}}), cf) + + def test_comment_after_variable(self): + cf = self.from_file("[section]\nbar= foo # a comment\n") + self.assertEquals(ConfigFile({("section", ): {"bar": "foo"}}), cf) + + def test_from_file_section(self): + cf = self.from_file("[core]\nfoo = bar\n") + self.assertEquals("bar", cf.get(("core", ), "foo")) + self.assertEquals("bar", cf.get(("core", "foo"), "foo")) + + def test_from_file_section_case_insensitive(self): + cf = self.from_file("[cOre]\nfOo = bar\n") + self.assertEquals("bar", cf.get(("core", ), "foo")) + self.assertEquals("bar", cf.get(("core", "foo"), "foo")) + + def test_from_file_with_mixed_quoted(self): + cf = self.from_file("[core]\nfoo = \"bar\"la\n") + self.assertEquals("barla", cf.get(("core", ), "foo")) + + def test_from_file_with_open_quoted(self): + self.assertRaises(ValueError, + self.from_file, "[core]\nfoo = \"bar\n") + + def test_from_file_with_quotes(self): + cf = self.from_file( + "[core]\n" + 'foo = " bar"\n') + self.assertEquals(" bar", cf.get(("core", ), "foo")) + + def test_from_file_with_interrupted_line(self): + cf = self.from_file( + "[core]\n" + 'foo = bar\\\n' + ' la\n') + self.assertEquals("barla", cf.get(("core", ), "foo")) + + def test_from_file_with_boolean_setting(self): + cf = self.from_file( + "[core]\n" + 'foo\n') + self.assertEquals("true", cf.get(("core", ), "foo")) + + def test_from_file_subsection(self): + cf = self.from_file("[branch \"foo\"]\nfoo = bar\n") + self.assertEquals("bar", cf.get(("branch", "foo"), "foo")) + + def test_from_file_subsection_invalid(self): + self.assertRaises(ValueError, + self.from_file, "[branch \"foo]\nfoo = bar\n") + + def test_from_file_subsection_not_quoted(self): + cf = self.from_file("[branch.foo]\nfoo = bar\n") + self.assertEquals("bar", cf.get(("branch", "foo"), "foo")) + + def test_write_to_file_empty(self): + c = ConfigFile() + f = StringIO() + c.write_to_file(f) + self.assertEquals("", f.getvalue()) + + def test_write_to_file_section(self): + c = ConfigFile() + c.set(("core", ), "foo", "bar") + f = StringIO() + c.write_to_file(f) + self.assertEquals("[core]\nfoo = bar\n", f.getvalue()) + + def test_write_to_file_subsection(self): + c = ConfigFile() + c.set(("branch", "blie"), "foo", "bar") + f = StringIO() + c.write_to_file(f) + self.assertEquals("[branch \"blie\"]\nfoo = bar\n", f.getvalue()) + + +class ConfigDictTests(TestCase): + + def test_get_set(self): + cd = ConfigDict() + self.assertRaises(KeyError, cd.get, "foo", "core") + cd.set(("core", ), "foo", "bla") + self.assertEquals("bla", cd.get(("core", ), "foo")) + cd.set(("core", ), "foo", "bloe") + self.assertEquals("bloe", cd.get(("core", ), "foo")) + + def test_get_boolean(self): + cd = ConfigDict() + cd.set(("core", ), "foo", "true") + self.assertTrue(cd.get_boolean(("core", ), "foo")) + cd.set(("core", ), "foo", "false") + self.assertFalse(cd.get_boolean(("core", ), "foo")) + cd.set(("core", ), "foo", "invalid") + self.assertRaises(ValueError, cd.get_boolean, ("core", ), "foo") + + +class StackedConfigTests(TestCase): + + def test_default_backends(self): + StackedConfig.default_backends() + + +class UnescapeTests(TestCase): + + def test_nothing(self): + self.assertEquals("", _unescape_value("")) + + def test_tab(self): + self.assertEquals("\tbar\t", _unescape_value("\\tbar\\t")) + + def test_newline(self): + self.assertEquals("\nbar\t", _unescape_value("\\nbar\\t")) + + def test_quote(self): + self.assertEquals("\"foo\"", _unescape_value("\\\"foo\\\"")) + + +class EscapeValueTests(TestCase): + + def test_nothing(self): + self.assertEquals("foo", _escape_value("foo")) + + def test_backslash(self): + self.assertEquals("foo\\\\", _escape_value("foo\\")) + + def test_newline(self): + self.assertEquals("foo\\n", _escape_value("foo\n")) + + +class FormatStringTests(TestCase): + + def test_quoted(self): + self.assertEquals('" foo"', _format_string(" foo")) + self.assertEquals('"\\tfoo"', _format_string("\tfoo")) + + def test_not_quoted(self): + self.assertEquals('foo', _format_string("foo")) + self.assertEquals('foo bar', _format_string("foo bar")) + + +class ParseStringTests(TestCase): + + def test_quoted(self): + self.assertEquals(' foo', _parse_string('" foo"')) + self.assertEquals('\tfoo', _parse_string('"\\tfoo"')) + + def test_not_quoted(self): + self.assertEquals('foo', _parse_string("foo")) + self.assertEquals('foo bar', _parse_string("foo bar")) + + +class CheckVariableNameTests(TestCase): + + def test_invalid(self): + self.assertFalse(_check_variable_name("foo ")) + self.assertFalse(_check_variable_name("bar,bar")) + self.assertFalse(_check_variable_name("bar.bar")) + + def test_valid(self): + self.assertTrue(_check_variable_name("FOO")) + self.assertTrue(_check_variable_name("foo")) + self.assertTrue(_check_variable_name("foo-bar")) + + +class CheckSectionNameTests(TestCase): + + def test_invalid(self): + self.assertFalse(_check_section_name("foo ")) + self.assertFalse(_check_section_name("bar,bar")) + + def test_valid(self): + self.assertTrue(_check_section_name("FOO")) + self.assertTrue(_check_section_name("foo")) + self.assertTrue(_check_section_name("foo-bar")) + self.assertTrue(_check_section_name("bar.bar")) diff -Nru dulwich-0.8.2/dulwich/tests/test_index.py dulwich-0.8.3/dulwich/tests/test_index.py --- dulwich-0.8.2/dulwich/tests/test_index.py 2011-12-18 20:30:33.000000000 +0000 +++ dulwich-0.8.3/dulwich/tests/test_index.py 2012-01-21 21:20:17.000000000 +0000 @@ -23,7 +23,6 @@ StringIO, ) import os -import posix import shutil import stat import struct @@ -176,7 +175,7 @@ class IndexEntryFromStatTests(TestCase): def test_simple(self): - st = posix.stat_result((16877, 131078, 64769L, + st = os.stat_result((16877, 131078, 64769L, 154, 1000, 1000, 12288, 1323629595, 1324180496, 1324180496)) entry = index_entry_from_stat(st, "22" * 20, 0) @@ -185,7 +184,25 @@ 1324180496, 64769L, 131078, - 16877, + 16384, + 1000, + 1000, + 12288, + '2222222222222222222222222222222222222222', + 0)) + + def test_override_mode(self): + st = os.stat_result((stat.S_IFREG + 0644, 131078, 64769L, + 154, 1000, 1000, 12288, + 1323629595, 1324180496, 1324180496)) + entry = index_entry_from_stat(st, "22" * 20, 0, + mode=stat.S_IFREG + 0755) + self.assertEquals(entry, ( + 1324180496, + 1324180496, + 64769L, + 131078, + 33261, 1000, 1000, 12288, diff -Nru dulwich-0.8.2/dulwich/tests/test_object_store.py dulwich-0.8.3/dulwich/tests/test_object_store.py --- dulwich-0.8.2/dulwich/tests/test_object_store.py 2011-12-18 20:30:33.000000000 +0000 +++ dulwich-0.8.3/dulwich/tests/test_object_store.py 2012-01-21 21:20:17.000000000 +0000 @@ -270,15 +270,20 @@ (REF_DELTA, (blob.id, 'more yummy data')), ], store=o) pack = o.add_thin_pack(f.read, None) + try: + packed_blob_sha = sha_to_hex(entries[0][3]) + pack.check_length_and_checksum() + self.assertEqual(sorted([blob.id, packed_blob_sha]), list(pack)) + self.assertTrue(o.contains_packed(packed_blob_sha)) + self.assertTrue(o.contains_packed(blob.id)) + self.assertEqual((Blob.type_num, 'more yummy data'), + o.get_raw(packed_blob_sha)) + finally: + # FIXME: DiskObjectStore should have close() which do the following: + for p in o._pack_cache or []: + p.close() - packed_blob_sha = sha_to_hex(entries[0][3]) - pack.check_length_and_checksum() - self.assertEqual(sorted([blob.id, packed_blob_sha]), list(pack)) - self.assertTrue(o.contains_packed(packed_blob_sha)) - self.assertTrue(o.contains_packed(blob.id)) - self.assertEqual((Blob.type_num, 'more yummy data'), - o.get_raw(packed_blob_sha)) - + pack.close() class TreeLookupPathTests(TestCase): diff -Nru dulwich-0.8.2/dulwich/tests/test_repository.py dulwich-0.8.3/dulwich/tests/test_repository.py --- dulwich-0.8.2/dulwich/tests/test_repository.py 2011-12-18 20:30:33.000000000 +0000 +++ dulwich-0.8.3/dulwich/tests/test_repository.py 2012-01-21 21:20:17.000000000 +0000 @@ -33,6 +33,7 @@ tree_lookup_path, ) from dulwich import objects +from dulwich.config import Config from dulwich.repo import ( check_ref_format, DictRefsContainer, @@ -73,7 +74,8 @@ self.assertFileContentsEqual('', repo, os.path.join('info', 'exclude')) self.assertFileContentsEqual(None, repo, 'nonexistent file') barestr = 'bare = %s' % str(expect_bare).lower() - self.assertTrue(barestr in repo.get_named_file('config').read()) + config_text = repo.get_named_file('config').read() + self.assertTrue(barestr in config_text, "%r" % config_text) def test_create_disk_bare(self): tmp_dir = tempfile.mkdtemp() @@ -114,10 +116,6 @@ self.assertEqual(r.ref('refs/heads/master'), 'a90fa2d900a17e99b433217e988c4eb4a2e9a097') - def test_iter(self): - r = self._repo = open_repo('a.git') - self.assertRaises(NotImplementedError, r.__iter__) - def test_setitem(self): r = self._repo = open_repo('a.git') r["refs/tags/foo"] = 'a90fa2d900a17e99b433217e988c4eb4a2e9a097' @@ -319,7 +317,11 @@ def test_get_config(self): r = self._repo = open_repo('ooo_merge.git') - self.assertEquals({}, r.get_config()) + self.assertIsInstance(r.get_config(), Config) + + def test_get_config_stack(self): + r = self._repo = open_repo('ooo_merge.git') + self.assertIsInstance(r.get_config_stack(), Config) def test_common_revisions(self): """ @@ -455,6 +457,21 @@ encoding="iso8859-1") self.assertEquals("iso8859-1", r[commit_sha].encoding) + def test_commit_config_identity(self): + # commit falls back to the users' identity if it wasn't specified + r = self._repo + c = r.get_config() + c.set(("user", ), "name", "Jelmer") + c.set(("user", ), "email", "jelmer@apache.org") + c.write_to_path() + commit_sha = r.do_commit('message') + self.assertEquals( + "Jelmer ", + r[commit_sha].author) + self.assertEquals( + "Jelmer ", + r[commit_sha].committer) + def test_commit_fail_ref(self): r = self._repo diff -Nru dulwich-0.8.2/dulwich/tests/test_web.py dulwich-0.8.3/dulwich/tests/test_web.py --- dulwich-0.8.2/dulwich/tests/test_web.py 2011-12-18 20:30:33.000000000 +0000 +++ dulwich-0.8.3/dulwich/tests/test_web.py 2012-01-21 21:20:17.000000000 +0000 @@ -20,6 +20,7 @@ from cStringIO import StringIO import re +import os from dulwich.object_store import ( MemoryObjectStore, @@ -198,7 +199,7 @@ self.assertEquals(HTTP_ERROR, self._status) def test_get_pack_file(self): - pack_name = 'objects/pack/pack-%s.pack' % ('1' * 40) + pack_name = os.path.join('objects', 'pack', 'pack-%s.pack' % ('1' * 40)) backend = _test_backend([], named_files={pack_name: 'pack contents'}) mat = re.search('.*', pack_name) output = ''.join(get_pack_file(self._req, backend, mat)) @@ -208,7 +209,7 @@ self.assertTrue(self._req.cached) def test_get_idx_file(self): - idx_name = 'objects/pack/pack-%s.idx' % ('1' * 40) + idx_name = os.path.join('objects', 'pack', 'pack-%s.idx' % ('1' * 40)) backend = _test_backend([], named_files={idx_name: 'idx contents'}) mat = re.search('.*', idx_name) output = ''.join(get_idx_file(self._req, backend, mat)) diff -Nru dulwich-0.8.2/Makefile dulwich-0.8.3/Makefile --- dulwich-0.8.2/Makefile 2011-12-18 20:30:33.000000000 +0000 +++ dulwich-0.8.3/Makefile 2012-01-21 21:20:17.000000000 +0000 @@ -25,6 +25,9 @@ check:: build $(RUNTEST) dulwich.tests.test_suite +check-tutorial:: build + $(RUNTEST) dulwich.tests.tutorial_test_suite + check-nocompat:: build $(RUNTEST) dulwich.tests.nocompat_test_suite diff -Nru dulwich-0.8.2/NEWS dulwich-0.8.3/NEWS --- dulwich-0.8.2/NEWS 2011-12-18 20:30:33.000000000 +0000 +++ dulwich-0.8.3/NEWS 2012-01-21 21:20:17.000000000 +0000 @@ -1,3 +1,20 @@ +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 diff -Nru dulwich-0.8.2/setup.py dulwich-0.8.3/setup.py --- dulwich-0.8.2/setup.py 2011-12-18 20:30:33.000000000 +0000 +++ dulwich-0.8.3/setup.py 2012-01-21 21:20:17.000000000 +0000 @@ -10,7 +10,7 @@ has_setuptools = False from distutils.core import Distribution -dulwich_version_string = '0.8.2' +dulwich_version_string = '0.8.3' include_dirs = [] # Windows MSVC support