5b7599884429fb85004c31b22809a03ab3f33f38
[jelmer/dulwich-libgit2.git] / dulwich / repo.py
1 # repo.py -- For dealing wih git repositories.
2 # Copyright (C) 2007 James Westby <jw+debian@jameswestby.net>
3 # Copyright (C) 2008-2009 Jelmer Vernooij <jelmer@samba.org>
4
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; version 2
8 # of the License or (at your option) any later version of 
9 # the License.
10
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19 # MA  02110-1301, USA.
20
21
22 """Repository access."""
23
24
25 import errno
26 import os
27
28 from dulwich.errors import (
29     MissingCommitError, 
30     NoIndexPresent,
31     NotBlobError, 
32     NotCommitError, 
33     NotGitRepository,
34     NotTreeError, 
35     NotTagError,
36     PackedRefsException,
37     )
38 from dulwich.file import (
39     ensure_dir_exists,
40     GitFile,
41     )
42 from dulwich.object_store import (
43     DiskObjectStore,
44     )
45 from dulwich.objects import (
46     Blob,
47     Commit,
48     ShaFile,
49     Tag,
50     Tree,
51     hex_to_sha,
52     object_class,
53     )
54 import warnings
55
56
57 OBJECTDIR = 'objects'
58 SYMREF = 'ref: '
59 REFSDIR = 'refs'
60 REFSDIR_TAGS = 'tags'
61 REFSDIR_HEADS = 'heads'
62 INDEX_FILENAME = "index"
63
64 BASE_DIRECTORIES = [
65     ["branches"],
66     [REFSDIR],
67     [REFSDIR, REFSDIR_TAGS],
68     [REFSDIR, REFSDIR_HEADS],
69     ["hooks"],
70     ["info"]
71     ]
72
73
74 def read_info_refs(f):
75     ret = {}
76     for l in f.readlines():
77         (sha, name) = l.rstrip("\r\n").split("\t", 1)
78         ret[name] = sha
79     return ret
80
81
82 def check_ref_format(refname):
83     """Check if a refname is correctly formatted.
84
85     Implements all the same rules as git-check-ref-format[1].
86
87     [1] http://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html
88
89     :param refname: The refname to check
90     :return: True if refname is valid, False otherwise
91     """
92     # These could be combined into one big expression, but are listed separately
93     # to parallel [1].
94     if '/.' in refname or refname.startswith('.'):
95         return False
96     if '/' not in refname:
97         return False
98     if '..' in refname:
99         return False
100     for c in refname:
101         if ord(c) < 040 or c in '\177 ~^:?*[':
102             return False
103     if refname[-1] in '/.':
104         return False
105     if refname.endswith('.lock'):
106         return False
107     if '@{' in refname:
108         return False
109     if '\\' in refname:
110         return False
111     return True
112
113
114 class RefsContainer(object):
115     """A container for refs."""
116
117     def set_ref(self, name, other):
118         warnings.warn("RefsContainer.set_ref() is deprecated."
119             "Use set_symblic_ref instead.",
120             category=DeprecationWarning, stacklevel=2)
121         return self.set_symbolic_ref(name, other)
122
123     def set_symbolic_ref(self, name, other):
124         """Make a ref point at another ref.
125
126         :param name: Name of the ref to set
127         :param other: Name of the ref to point at
128         """
129         self[name] = SYMREF + other + '\n'
130
131     def get_packed_refs(self):
132         """Get contents of the packed-refs file.
133
134         :return: Dictionary mapping ref names to SHA1s
135
136         :note: Will return an empty dictionary when no packed-refs file is
137             present.
138         """
139         raise NotImplementedError(self.get_packed_refs)
140
141     def get_peeled(self, name):
142         """Return the cached peeled value of a ref, if available.
143
144         :param name: Name of the ref to peel
145         :return: The peeled value of the ref. If the ref is known not point to a
146             tag, this will be the SHA the ref refers to. If the ref may point to
147             a tag, but no cached information is available, None is returned.
148         """
149         return None
150
151     def import_refs(self, base, other):
152         for name, value in other.iteritems():
153             self["%s/%s" % (base, name)] = value
154
155     def keys(self, base=None):
156         """Refs present in this container.
157
158         :param base: An optional base to return refs under
159         :return: An unsorted set of valid refs in this container, including
160             packed refs.
161         """
162         if base is not None:
163             return self.subkeys(base)
164         else:
165             return self.allkeys()
166
167     def subkeys(self, base):
168         keys = set()
169         for refname in self.allkeys():
170             if refname.startswith(base):
171                 keys.add(refname)
172         return keys
173
174     def as_dict(self, base=None):
175         """Return the contents of this container as a dictionary.
176
177         """
178         ret = {}
179         keys = self.keys(base)
180         if base is None:
181             base = ""
182         for key in keys:
183             try:
184                 ret[key] = self[("%s/%s" % (base, key)).strip("/")]
185             except KeyError:
186                 continue # Unable to resolve
187
188         return ret
189
190     def _check_refname(self, name):
191         """Ensure a refname is valid and lives in refs or is HEAD.
192
193         HEAD is not a valid refname according to git-check-ref-format, but this
194         class needs to be able to touch HEAD. Also, check_ref_format expects
195         refnames without the leading 'refs/', but this class requires that
196         so it cannot touch anything outside the refs dir (or HEAD).
197
198         :param name: The name of the reference.
199         :raises KeyError: if a refname is not HEAD or is otherwise not valid.
200         """
201         if name == 'HEAD':
202             return
203         if not name.startswith('refs/') or not check_ref_format(name[5:]):
204             raise KeyError(name)
205
206     def read_ref(self, refname):
207         """Read a reference without following any references.
208
209         :param refname: The name of the reference
210         :return: The contents of the ref file, or None if it does 
211             not exist.
212         """
213         contents = self.read_loose_ref(refname)
214         if not contents:
215             contents = self.get_packed_refs().get(refname, None)
216         return contents
217
218     def read_loose_ref(self, name):
219         """Read a loose reference and return its contents.
220
221         :param name: the refname to read
222         :return: The contents of the ref file, or None if it does 
223             not exist.
224         """
225         raise NotImplementedError(self.read_loose_ref)
226
227     def _follow(self, name):
228         """Follow a reference name.
229
230         :return: a tuple of (refname, sha), where refname is the name of the
231             last reference in the symbolic reference chain
232         """
233         self._check_refname(name)
234         contents = SYMREF + name
235         depth = 0
236         while contents.startswith(SYMREF):
237             refname = contents[len(SYMREF):]
238             contents = self.read_ref(refname)
239             if not contents:
240                 break
241             depth += 1
242             if depth > 5:
243                 raise KeyError(name)
244         return refname, contents
245
246     def __contains__(self, refname):
247         if self.read_ref(refname):
248             return True
249         return False
250
251     def __getitem__(self, name):
252         """Get the SHA1 for a reference name.
253
254         This method follows all symbolic references.
255         """
256         _, sha = self._follow(name)
257         if sha is None:
258             raise KeyError(name)
259         return sha
260
261
262 class DictRefsContainer(RefsContainer):
263
264     def __init__(self, refs):
265         self._refs = refs
266
267     def allkeys(self):
268         return self._refs.keys()
269
270     def read_loose_ref(self, name):
271         return self._refs[name]
272
273     def __setitem__(self, name, value):
274         self._refs[name] = value
275
276
277 class DiskRefsContainer(RefsContainer):
278     """Refs container that reads refs from disk."""
279
280     def __init__(self, path):
281         self.path = path
282         self._packed_refs = None
283         self._peeled_refs = None
284
285     def __repr__(self):
286         return "%s(%r)" % (self.__class__.__name__, self.path)
287
288     def subkeys(self, base):
289         keys = set()
290         path = self.refpath(base)
291         for root, dirs, files in os.walk(path):
292             dir = root[len(path):].strip(os.path.sep).replace(os.path.sep, "/")
293             for filename in files:
294                 refname = ("%s/%s" % (dir, filename)).strip("/")
295                 # check_ref_format requires at least one /, so we prepend the
296                 # base before calling it.
297                 if check_ref_format("%s/%s" % (base, refname)):
298                     keys.add(refname)
299         for key in self.get_packed_refs():
300             if key.startswith(base):
301                 keys.add(key[len(base):].strip("/"))
302         return keys
303
304     def allkeys(self):
305         keys = set()
306         if os.path.exists(self.refpath("HEAD")):
307             keys.add("HEAD")
308         path = self.refpath("")
309         for root, dirs, files in os.walk(self.refpath("refs")):
310             dir = root[len(path):].strip(os.path.sep).replace(os.path.sep, "/")
311             for filename in files:
312                 refname = ("%s/%s" % (dir, filename)).strip("/")
313                 if check_ref_format(refname):
314                     keys.add(refname)
315         keys.update(self.get_packed_refs())
316         return keys
317
318     def refpath(self, name):
319         """Return the disk path of a ref.
320
321         """
322         if os.path.sep != "/":
323             name = name.replace("/", os.path.sep)
324         return os.path.join(self.path, name)
325
326     def get_packed_refs(self):
327         """Get contents of the packed-refs file.
328
329         :return: Dictionary mapping ref names to SHA1s
330
331         :note: Will return an empty dictionary when no packed-refs file is
332             present.
333         """
334         # TODO: invalidate the cache on repacking
335         if self._packed_refs is None:
336             self._packed_refs = {}
337             path = os.path.join(self.path, 'packed-refs')
338             try:
339                 f = GitFile(path, 'rb')
340             except IOError, e:
341                 if e.errno == errno.ENOENT:
342                     return {}
343                 raise
344             try:
345                 first_line = iter(f).next().rstrip()
346                 if (first_line.startswith("# pack-refs") and " peeled" in
347                         first_line):
348                     self._peeled_refs = {}
349                     for sha, name, peeled in read_packed_refs_with_peeled(f):
350                         self._packed_refs[name] = sha
351                         if peeled:
352                             self._peeled_refs[name] = peeled
353                 else:
354                     f.seek(0)
355                     for sha, name in read_packed_refs(f):
356                         self._packed_refs[name] = sha
357             finally:
358                 f.close()
359         return self._packed_refs
360
361     def get_peeled(self, name):
362         """Return the cached peeled value of a ref, if available.
363
364         :param name: Name of the ref to peel
365         :return: The peeled value of the ref. If the ref is known not point to a
366             tag, this will be the SHA the ref refers to. If the ref may point to
367             a tag, but no cached information is available, None is returned.
368         """
369         self.get_packed_refs()
370         if self._peeled_refs is None or name not in self._packed_refs:
371             # No cache: no peeled refs were read, or this ref is loose
372             return None
373         if name in self._peeled_refs:
374             return self._peeled_refs[name]
375         else:
376             # Known not peelable
377             return self[name]
378
379     def read_loose_ref(self, name):
380         """Read a reference file and return its contents.
381
382         If the reference file a symbolic reference, only read the first line of
383         the file. Otherwise, only read the first 40 bytes.
384
385         :param name: the refname to read, relative to refpath
386         :return: The contents of the ref file, or None if the file does not
387             exist.
388         :raises IOError: if any other error occurs
389         """
390         filename = self.refpath(name)
391         try:
392             f = GitFile(filename, 'rb')
393             try:
394                 header = f.read(len(SYMREF))
395                 if header == SYMREF:
396                     # Read only the first line
397                     return header + iter(f).next().rstrip("\r\n")
398                 else:
399                     # Read only the first 40 bytes
400                     return header + f.read(40-len(SYMREF))
401             finally:
402                 f.close()
403         except IOError, e:
404             if e.errno == errno.ENOENT:
405                 return None
406             raise
407
408     def _remove_packed_ref(self, name):
409         if self._packed_refs is None:
410             return
411         filename = os.path.join(self.path, 'packed-refs')
412         # reread cached refs from disk, while holding the lock
413         f = GitFile(filename, 'wb')
414         try:
415             self._packed_refs = None
416             self.get_packed_refs()
417
418             if name not in self._packed_refs:
419                 return
420
421             del self._packed_refs[name]
422             if name in self._peeled_refs:
423                 del self._peeled_refs[name]
424             write_packed_refs(f, self._packed_refs, self._peeled_refs)
425             f.close()
426         finally:
427             f.abort()
428
429     def set_if_equals(self, name, old_ref, new_ref):
430         """Set a refname to new_ref only if it currently equals old_ref.
431
432         This method follows all symbolic references, and can be used to perform
433         an atomic compare-and-swap operation.
434
435         :param name: The refname to set.
436         :param old_ref: The old sha the refname must refer to, or None to set
437             unconditionally.
438         :param new_ref: The new sha the refname will refer to.
439         :return: True if the set was successful, False otherwise.
440         """
441         try:
442             realname, _ = self._follow(name)
443         except KeyError:
444             realname = name
445         filename = self.refpath(realname)
446         ensure_dir_exists(os.path.dirname(filename))
447         f = GitFile(filename, 'wb')
448         try:
449             if old_ref is not None:
450                 try:
451                     # read again while holding the lock
452                     orig_ref = self.read_loose_ref(realname)
453                     if orig_ref is None:
454                         orig_ref = self.get_packed_refs().get(realname, None)
455                     if orig_ref != old_ref:
456                         f.abort()
457                         return False
458                 except (OSError, IOError):
459                     f.abort()
460                     raise
461             try:
462                 f.write(new_ref+"\n")
463             except (OSError, IOError):
464                 f.abort()
465                 raise
466         finally:
467             f.close()
468         return True
469
470     def add_if_new(self, name, ref):
471         """Add a new reference only if it does not already exist."""
472         self._check_refname(name)
473         filename = self.refpath(name)
474         ensure_dir_exists(os.path.dirname(filename))
475         f = GitFile(filename, 'wb')
476         try:
477             if os.path.exists(filename) or name in self.get_packed_refs():
478                 f.abort()
479                 return False
480             try:
481                 f.write(ref+"\n")
482             except (OSError, IOError):
483                 f.abort()
484                 raise
485         finally:
486             f.close()
487         return True
488
489     def __setitem__(self, name, ref):
490         """Set a reference name to point to the given SHA1.
491
492         This method follows all symbolic references.
493
494         :note: This method unconditionally overwrites the contents of a reference
495             on disk. To update atomically only if the reference has not changed
496             on disk, use set_if_equals().
497         """
498         self.set_if_equals(name, None, ref)
499
500     def remove_if_equals(self, name, old_ref):
501         """Remove a refname only if it currently equals old_ref.
502
503         This method does not follow symbolic references. It can be used to
504         perform an atomic compare-and-delete operation.
505
506         :param name: The refname to delete.
507         :param old_ref: The old sha the refname must refer to, or None to delete
508             unconditionally.
509         :return: True if the delete was successful, False otherwise.
510         """
511         self._check_refname(name)
512         filename = self.refpath(name)
513         ensure_dir_exists(os.path.dirname(filename))
514         f = GitFile(filename, 'wb')
515         try:
516             if old_ref is not None:
517                 orig_ref = self.read_loose_ref(name)
518                 if orig_ref is None:
519                     orig_ref = self.get_packed_refs().get(name, None)
520                 if orig_ref != old_ref:
521                     return False
522             # may only be packed
523             try:
524                 os.remove(filename)
525             except OSError, e:
526                 if e.errno != errno.ENOENT:
527                     raise
528             self._remove_packed_ref(name)
529         finally:
530             # never write, we just wanted the lock
531             f.abort()
532         return True
533
534     def __delitem__(self, name):
535         """Remove a refname.
536
537         This method does not follow symbolic references.
538         :note: This method unconditionally deletes the contents of a reference
539             on disk. To delete atomically only if the reference has not changed
540             on disk, use set_if_equals().
541         """
542         self.remove_if_equals(name, None)
543
544
545 def _split_ref_line(line):
546     """Split a single ref line into a tuple of SHA1 and name."""
547     fields = line.rstrip("\n").split(" ")
548     if len(fields) != 2:
549         raise PackedRefsException("invalid ref line '%s'" % line)
550     sha, name = fields
551     try:
552         hex_to_sha(sha)
553     except (AssertionError, TypeError), e:
554         raise PackedRefsException(e)
555     if not check_ref_format(name):
556         raise PackedRefsException("invalid ref name '%s'" % name)
557     return (sha, name)
558
559
560 def read_packed_refs(f):
561     """Read a packed refs file.
562
563     Yields tuples with SHA1s and ref names.
564
565     :param f: file-like object to read from
566     """
567     for l in f:
568         if l[0] == "#":
569             # Comment
570             continue
571         if l[0] == "^":
572             raise PackedRefsException(
573                 "found peeled ref in packed-refs without peeled")
574         yield _split_ref_line(l)
575
576
577 def read_packed_refs_with_peeled(f):
578     """Read a packed refs file including peeled refs.
579
580     Assumes the "# pack-refs with: peeled" line was already read. Yields tuples
581     with ref names, SHA1s, and peeled SHA1s (or None).
582
583     :param f: file-like object to read from, seek'ed to the second line
584     """
585     last = None
586     for l in f:
587         if l[0] == "#":
588             continue
589         l = l.rstrip("\r\n")
590         if l[0] == "^":
591             if not last:
592                 raise PackedRefsException("unexpected peeled ref line")
593             try:
594                 hex_to_sha(l[1:])
595             except (AssertionError, TypeError), e:
596                 raise PackedRefsException(e)
597             sha, name = _split_ref_line(last)
598             last = None
599             yield (sha, name, l[1:])
600         else:
601             if last:
602                 sha, name = _split_ref_line(last)
603                 yield (sha, name, None)
604             last = l
605     if last:
606         sha, name = _split_ref_line(last)
607         yield (sha, name, None)
608
609
610 def write_packed_refs(f, packed_refs, peeled_refs=None):
611     """Write a packed refs file.
612
613     :param f: empty file-like object to write to
614     :param packed_refs: dict of refname to sha of packed refs to write
615     :param peeled_refs: dict of refname to peeled value of sha
616     """
617     if peeled_refs is None:
618         peeled_refs = {}
619     else:
620         f.write('# pack-refs with: peeled\n')
621     for refname in sorted(packed_refs.iterkeys()):
622         f.write('%s %s\n' % (packed_refs[refname], refname))
623         if refname in peeled_refs:
624             f.write('^%s\n' % peeled_refs[refname])
625
626
627 class BaseRepo(object):
628     """Base class for a git repository.
629
630     :ivar object_store: Dictionary-like object for accessing
631         the objects
632     :ivar refs: Dictionary-like object with the refs in this repository
633     """
634
635     def __init__(self, object_store, refs):
636         self.object_store = object_store
637         self.refs = refs
638
639     def get_named_file(self, path):
640         """Get a file from the control dir with a specific name.
641
642         Although the filename should be interpreted as a filename relative to
643         the control dir in a disk-baked Repo, the object returned need not be
644         pointing to a file in that location.
645
646         :param path: The path to the file, relative to the control dir.
647         :return: An open file object, or None if the file does not exist.
648         """
649         raise NotImplementedError(self.get_named_file)
650
651     def open_index(self):
652         """Open the index for this repository.
653         
654         :raises NoIndexPresent: If no index is present
655         :return: Index instance
656         """
657         raise NotImplementedError(self.open_index)
658
659     def fetch(self, target, determine_wants=None, progress=None):
660         """Fetch objects into another repository.
661
662         :param target: The target repository
663         :param determine_wants: Optional function to determine what refs to 
664             fetch.
665         :param progress: Optional progress function
666         """
667         if determine_wants is None:
668             determine_wants = lambda heads: heads.values()
669         target.object_store.add_objects(
670             self.fetch_objects(determine_wants, target.get_graph_walker(),
671                 progress))
672         return self.get_refs()
673
674     def fetch_objects(self, determine_wants, graph_walker, progress,
675                       get_tagged=None):
676         """Fetch the missing objects required for a set of revisions.
677
678         :param determine_wants: Function that takes a dictionary with heads 
679             and returns the list of heads to fetch.
680         :param graph_walker: Object that can iterate over the list of revisions 
681             to fetch and has an "ack" method that will be called to acknowledge 
682             that a revision is present.
683         :param progress: Simple progress function that will be called with 
684             updated progress strings.
685         :param get_tagged: Function that returns a dict of pointed-to sha -> tag
686             sha for including tags.
687         :return: iterator over objects, with __len__ implemented
688         """
689         wants = determine_wants(self.get_refs())
690         if not wants:
691             return []
692         haves = self.object_store.find_common_revisions(graph_walker)
693         return self.object_store.iter_shas(
694             self.object_store.find_missing_objects(haves, wants, progress,
695                                                    get_tagged))
696
697     def get_graph_walker(self, heads=None):
698         if heads is None:
699             heads = self.refs.as_dict('refs/heads').values()
700         return self.object_store.get_graph_walker(heads)
701
702     def ref(self, name):
703         """Return the SHA1 a ref is pointing to."""
704         return self.refs[name]
705
706     def get_refs(self):
707         """Get dictionary with all refs."""
708         return self.refs.as_dict()
709
710     def head(self):
711         """Return the SHA1 pointed at by HEAD."""
712         return self.refs['HEAD']
713
714     def _get_object(self, sha, cls):
715         assert len(sha) in (20, 40)
716         ret = self.get_object(sha)
717         if not isinstance(ret, cls):
718             if cls is Commit:
719                 raise NotCommitError(ret)
720             elif cls is Blob:
721                 raise NotBlobError(ret)
722             elif cls is Tree:
723                 raise NotTreeError(ret)
724             elif cls is Tag:
725                 raise NotTagError(ret)
726             else:
727                 raise Exception("Type invalid: %r != %r" % (
728                   ret.type_name, cls.type_name))
729         return ret
730
731     def get_object(self, sha):
732         return self.object_store[sha]
733
734     def get_parents(self, sha):
735         return self.commit(sha).parents
736
737     def get_config(self):
738         import ConfigParser
739         p = ConfigParser.RawConfigParser()
740         p.read(os.path.join(self._controldir, 'config'))
741         return dict((section, dict(p.items(section)))
742                     for section in p.sections())
743
744     def commit(self, sha):
745         """Retrieve the commit with a particular SHA.
746
747         :param sha: SHA of the commit to retrieve
748         :raise NotCommitError: If the SHA provided doesn't point at a Commit
749         :raise KeyError: If the SHA provided didn't exist
750         :return: A `Commit` object
751         """
752         warnings.warn("Repo.commit(sha) is deprecated. Use Repo[sha] instead.",
753             category=DeprecationWarning, stacklevel=2)
754         return self._get_object(sha, Commit)
755
756     def tree(self, sha):
757         """Retrieve the tree with a particular SHA.
758
759         :param sha: SHA of the tree to retrieve
760         :raise NotTreeError: If the SHA provided doesn't point at a Tree
761         :raise KeyError: If the SHA provided didn't exist
762         :return: A `Tree` object
763         """
764         warnings.warn("Repo.tree(sha) is deprecated. Use Repo[sha] instead.",
765             category=DeprecationWarning, stacklevel=2)
766         return self._get_object(sha, Tree)
767
768     def tag(self, sha):
769         """Retrieve the tag with a particular SHA.
770
771         :param sha: SHA of the tag to retrieve
772         :raise NotTagError: If the SHA provided doesn't point at a Tag
773         :raise KeyError: If the SHA provided didn't exist
774         :return: A `Tag` object
775         """
776         warnings.warn("Repo.tag(sha) is deprecated. Use Repo[sha] instead.",
777             category=DeprecationWarning, stacklevel=2)
778         return self._get_object(sha, Tag)
779
780     def get_blob(self, sha):
781         """Retrieve the blob with a particular SHA.
782
783         :param sha: SHA of the blob to retrieve
784         :raise NotBlobError: If the SHA provided doesn't point at a Blob
785         :raise KeyError: If the SHA provided didn't exist
786         :return: A `Blob` object
787         """
788         warnings.warn("Repo.get_blob(sha) is deprecated. Use Repo[sha] "
789             "instead.", category=DeprecationWarning, stacklevel=2)
790         return self._get_object(sha, Blob)
791
792     def get_peeled(self, ref):
793         """Get the peeled value of a ref.
794
795         :param ref: the refname to peel
796         :return: the fully-peeled SHA1 of a tag object, after peeling all
797             intermediate tags; if the original ref does not point to a tag, this
798             will equal the original SHA1.
799         """
800         cached = self.refs.get_peeled(ref)
801         if cached is not None:
802             return cached
803         obj = self[ref]
804         obj_class = object_class(obj.type_name)
805         while obj_class is Tag:
806             obj_class, sha = obj.object
807             obj = self.get_object(sha)
808         return obj.id
809
810     def revision_history(self, head):
811         """Returns a list of the commits reachable from head.
812
813         Returns a list of commit objects. the first of which will be the commit
814         of head, then following theat will be the parents.
815
816         Raises NotCommitError if any no commits are referenced, including if the
817         head parameter isn't the sha of a commit.
818
819         XXX: work out how to handle merges.
820         """
821         # We build the list backwards, as parents are more likely to be older
822         # than children
823         pending_commits = [head]
824         history = []
825         while pending_commits != []:
826             head = pending_commits.pop(0)
827             try:
828                 commit = self[head]
829             except KeyError:
830                 raise MissingCommitError(head)
831             if type(commit) != Commit:
832                 raise NotCommitError(commit)
833             if commit in history:
834                 continue
835             i = 0
836             for known_commit in history:
837                 if known_commit.commit_time > commit.commit_time:
838                     break
839                 i += 1
840             history.insert(i, commit)
841             pending_commits += commit.parents
842         history.reverse()
843         return history
844
845     def __getitem__(self, name):
846         if len(name) in (20, 40):
847             return self.object_store[name]
848         return self.object_store[self.refs[name]]
849
850     def __setitem__(self, name, value):
851         if name.startswith("refs/") or name == "HEAD":
852             if isinstance(value, ShaFile):
853                 self.refs[name] = value.id
854             elif isinstance(value, str):
855                 self.refs[name] = value
856             else:
857                 raise TypeError(value)
858         else:
859             raise ValueError(name)
860
861     def __delitem__(self, name):
862         if name.startswith("refs") or name == "HEAD":
863             del self.refs[name]
864         raise ValueError(name)
865
866     def do_commit(self, message, committer=None, 
867                   author=None, commit_timestamp=None,
868                   commit_timezone=None, author_timestamp=None, 
869                   author_timezone=None, tree=None):
870         """Create a new commit.
871
872         :param message: Commit message
873         :param committer: Committer fullname
874         :param author: Author fullname (defaults to committer)
875         :param commit_timestamp: Commit timestamp (defaults to now)
876         :param commit_timezone: Commit timestamp timezone (defaults to GMT)
877         :param author_timestamp: Author timestamp (defaults to commit timestamp)
878         :param author_timezone: Author timestamp timezone 
879             (defaults to commit timestamp timezone)
880         :param tree: SHA1 of the tree root to use (if not specified the current index will be committed).
881         :return: New commit SHA1
882         """
883         import time
884         index = self.open_index()
885         c = Commit()
886         if tree is None:
887             c.tree = index.commit(self.object_store)
888         else:
889             c.tree = tree
890         # TODO: Allow username to be missing, and get it from .git/config
891         if committer is None:
892             raise ValueError("committer not set")
893         c.committer = committer
894         if commit_timestamp is None:
895             commit_timestamp = time.time()
896         c.commit_time = int(commit_timestamp)
897         if commit_timezone is None:
898             # FIXME: Use current user timezone rather than UTC
899             commit_timezone = 0
900         c.commit_timezone = commit_timezone
901         if author is None:
902             author = committer
903         c.author = author
904         if author_timestamp is None:
905             author_timestamp = commit_timestamp
906         c.author_time = int(author_timestamp)
907         if author_timezone is None:
908             author_timezone = commit_timezone
909         c.author_timezone = author_timezone
910         c.message = message
911         self.object_store.add_object(c)
912         self.refs["HEAD"] = c.id
913         return c.id
914
915
916 class Repo(BaseRepo):
917     """A git repository backed by local disk."""
918
919     def __init__(self, root):
920         if os.path.isdir(os.path.join(root, ".git", OBJECTDIR)):
921             self.bare = False
922             self._controldir = os.path.join(root, ".git")
923         elif (os.path.isdir(os.path.join(root, OBJECTDIR)) and
924               os.path.isdir(os.path.join(root, REFSDIR))):
925             self.bare = True
926             self._controldir = root
927         else:
928             raise NotGitRepository(root)
929         self.path = root
930         object_store = DiskObjectStore(
931             os.path.join(self.controldir(), OBJECTDIR))
932         refs = DiskRefsContainer(self.controldir())
933         BaseRepo.__init__(self, object_store, refs)
934
935     def controldir(self):
936         """Return the path of the control directory."""
937         return self._controldir
938
939     def _put_named_file(self, path, contents):
940         """Write a file from the control dir with a specific name and contents.
941         """
942         f = GitFile(os.path.join(self.controldir(), path), 'wb')
943         try:
944             f.write(contents)
945         finally:
946             f.close()
947
948     def get_named_file(self, path):
949         """Get a file from the control dir with a specific name.
950
951         Although the filename should be interpreted as a filename relative to
952         the control dir in a disk-baked Repo, the object returned need not be
953         pointing to a file in that location.
954
955         :param path: The path to the file, relative to the control dir.
956         :return: An open file object, or None if the file does not exist.
957         """
958         try:
959             return open(os.path.join(self.controldir(), path.lstrip('/')), 'rb')
960         except (IOError, OSError), e:
961             if e.errno == errno.ENOENT:
962                 return None
963             raise
964
965     def index_path(self):
966         """Return path to the index file."""
967         return os.path.join(self.controldir(), INDEX_FILENAME)
968
969     def open_index(self):
970         """Open the index for this repository."""
971         from dulwich.index import Index
972         if not self.has_index():
973             raise NoIndexPresent()
974         return Index(self.index_path())
975
976     def has_index(self):
977         """Check if an index is present."""
978         return os.path.exists(self.index_path())
979
980     def stage(self, paths):
981         """Stage a set of paths.
982
983         :param paths: List of paths, relative to the repository path
984         """
985         from dulwich.index import cleanup_mode
986         index = self.open_index()
987         for path in paths:
988             blob = Blob()
989             try:
990                 st = os.stat(path)
991             except OSError:
992                 # File no longer exists
993                 del index[path]
994             else:
995                 f = open(path, 'rb')
996                 try:
997                     blob.data = f.read()
998                 finally:
999                     f.close()
1000                 self.object_store.add_object(blob)
1001                 # XXX: Cleanup some of the other file properties as well?
1002                 index[path] = (st.st_ctime, st.st_mtime, st.st_dev, st.st_ino,
1003                     cleanup_mode(st.st_mode), st.st_uid, st.st_gid, st.st_size,
1004                     blob.id, 0)
1005         index.write()
1006
1007     def __repr__(self):
1008         return "<Repo at %r>" % self.path
1009
1010     @classmethod
1011     def init(cls, path, mkdir=True):
1012         controldir = os.path.join(path, ".git")
1013         os.mkdir(controldir)
1014         cls.init_bare(controldir)
1015         return cls(path)
1016
1017     @classmethod
1018     def init_bare(cls, path, mkdir=True):
1019         for d in BASE_DIRECTORIES:
1020             os.mkdir(os.path.join(path, *d))
1021         DiskObjectStore.init(os.path.join(path, OBJECTDIR))
1022         ret = cls(path)
1023         ret.refs.set_symbolic_ref("HEAD", "refs/heads/master")
1024         ret._put_named_file('description', "Unnamed repository")
1025         ret._put_named_file('config', """[core]
1026     repositoryformatversion = 0
1027     filemode = true
1028     bare = false
1029     logallrefupdates = true
1030 """)
1031         ret._put_named_file(os.path.join('info', 'exclude'), '')
1032         return ret
1033
1034     create = init_bare