Allow opening pack objects from memory.
[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     PackedRefsException,
36     )
37 from dulwich.file import (
38     ensure_dir_exists,
39     GitFile,
40     )
41 from dulwich.object_store import (
42     DiskObjectStore,
43     )
44 from dulwich.objects import (
45     Blob,
46     Commit,
47     ShaFile,
48     Tag,
49     Tree,
50     hex_to_sha,
51     )
52
53 OBJECTDIR = 'objects'
54 SYMREF = 'ref: '
55 REFSDIR = 'refs'
56 REFSDIR_TAGS = 'tags'
57 REFSDIR_HEADS = 'heads'
58 INDEX_FILENAME = "index"
59
60 BASE_DIRECTORIES = [
61     [OBJECTDIR], 
62     [OBJECTDIR, "info"], 
63     [OBJECTDIR, "pack"],
64     ["branches"],
65     [REFSDIR],
66     [REFSDIR, REFSDIR_TAGS],
67     [REFSDIR, REFSDIR_HEADS],
68     ["hooks"],
69     ["info"]
70     ]
71
72
73 def check_ref_format(refname):
74     """Check if a refname is correctly formatted.
75
76     Implements all the same rules as git-check-ref-format[1].
77
78     [1] http://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html
79
80     :param refname: The refname to check
81     :return: True if refname is valid, False otherwise
82     """
83     # These could be combined into one big expression, but are listed separately
84     # to parallel [1].
85     if '/.' in refname or refname.startswith('.'):
86         return False
87     if '/' not in refname:
88         return False
89     if '..' in refname:
90         return False
91     for c in refname:
92         if ord(c) < 040 or c in '\177 ~^:?*[':
93             return False
94     if refname[-1] in '/.':
95         return False
96     if refname.endswith('.lock'):
97         return False
98     if '@{' in refname:
99         return False
100     if '\\' in refname:
101         return False
102     return True
103
104
105 class RefsContainer(object):
106     """A container for refs."""
107
108     def set_ref(self, name, other):
109         """Make a ref point at another ref.
110
111         :param name: Name of the ref to set
112         :param other: Name of the ref to point at
113         """
114         self[name] = SYMREF + other + '\n'
115
116     def get_packed_refs(self):
117         """Get contents of the packed-refs file.
118
119         :return: Dictionary mapping ref names to SHA1s
120
121         :note: Will return an empty dictionary when no packed-refs file is
122             present.
123         """
124         raise NotImplementedError(self.get_packed_refs)
125
126     def import_refs(self, base, other):
127         for name, value in other.iteritems():
128             self["%s/%s" % (base, name)] = value
129
130     def keys(self, base=None):
131         """Refs present in this container.
132
133         :param base: An optional base to return refs under
134         :return: An unsorted set of valid refs in this container, including
135             packed refs.
136         """
137         if base is not None:
138             return self.subkeys(base)
139         else:
140             return self.allkeys()
141
142     def as_dict(self, base=None):
143         """Return the contents of this container as a dictionary.
144
145         """
146         ret = {}
147         keys = self.keys(base)
148         if base is None:
149             base = ""
150         for key in keys:
151             try:
152                 ret[key] = self[("%s/%s" % (base, key)).strip("/")]
153             except KeyError:
154                 continue # Unable to resolve
155
156         return ret
157
158     def _check_refname(self, name):
159         """Ensure a refname is valid and lives in refs or is HEAD.
160
161         HEAD is not a valid refname according to git-check-ref-format, but this
162         class needs to be able to touch HEAD. Also, check_ref_format expects
163         refnames without the leading 'refs/', but this class requires that
164         so it cannot touch anything outside the refs dir (or HEAD).
165
166         :param name: The name of the reference.
167         :raises KeyError: if a refname is not HEAD or is otherwise not valid.
168         """
169         if name == 'HEAD':
170             return
171         if not name.startswith('refs/') or not check_ref_format(name[5:]):
172             raise KeyError(name)
173
174     def read_loose_ref(self, name):
175         """Read a loose reference and return its contents.
176
177         :param name: the refname to read
178         :return: The contents of the ref file, or None if it does 
179             not exist.
180         """
181         raise NotImplementedError(self.read_loose_ref)
182
183     def _follow(self, name):
184         """Follow a reference name.
185
186         :return: a tuple of (refname, sha), where refname is the name of the
187             last reference in the symbolic reference chain
188         """
189         self._check_refname(name)
190         contents = SYMREF + name
191         depth = 0
192         while contents.startswith(SYMREF):
193             refname = contents[len(SYMREF):]
194             contents = self.read_loose_ref(refname)
195             if not contents:
196                 contents = self.get_packed_refs().get(refname, None)
197                 if not contents:
198                     break
199             depth += 1
200             if depth > 5:
201                 raise KeyError(name)
202         return refname, contents
203
204     def __getitem__(self, name):
205         """Get the SHA1 for a reference name.
206
207         This method follows all symbolic references.
208         """
209         _, sha = self._follow(name)
210         if sha is None:
211             raise KeyError(name)
212         return sha
213
214
215 class DiskRefsContainer(RefsContainer):
216     """Refs container that reads refs from disk."""
217
218     def __init__(self, path):
219         self.path = path
220         self._packed_refs = None
221         self._peeled_refs = {}
222
223     def __repr__(self):
224         return "%s(%r)" % (self.__class__.__name__, self.path)
225
226     def subkeys(self, base):
227         keys = set()
228         path = self.refpath(base)
229         for root, dirs, files in os.walk(path):
230             dir = root[len(path):].strip(os.path.sep).replace(os.path.sep, "/")
231             for filename in files:
232                 refname = ("%s/%s" % (dir, filename)).strip("/")
233                 # check_ref_format requires at least one /, so we prepend the
234                 # base before calling it.
235                 if check_ref_format("%s/%s" % (base, refname)):
236                     keys.add(refname)
237         for key in self.get_packed_refs():
238             if key.startswith(base):
239                 keys.add(key[len(base):].strip("/"))
240         return keys
241
242     def allkeys(self):
243         keys = set()
244         if os.path.exists(self.refpath("HEAD")):
245             keys.add("HEAD")
246         path = self.refpath("")
247         for root, dirs, files in os.walk(self.refpath("refs")):
248             dir = root[len(path):].strip(os.path.sep).replace(os.path.sep, "/")
249             for filename in files:
250                 refname = ("%s/%s" % (dir, filename)).strip("/")
251                 if check_ref_format(refname):
252                     keys.add(refname)
253         keys.update(self.get_packed_refs())
254         return keys
255
256     def refpath(self, name):
257         """Return the disk path of a ref.
258
259         """
260         if os.path.sep != "/":
261             name = name.replace("/", os.path.sep)
262         return os.path.join(self.path, name)
263
264     def get_packed_refs(self):
265         """Get contents of the packed-refs file.
266
267         :return: Dictionary mapping ref names to SHA1s
268
269         :note: Will return an empty dictionary when no packed-refs file is
270             present.
271         """
272         # TODO: invalidate the cache on repacking
273         if self._packed_refs is None:
274             self._packed_refs = {}
275             path = os.path.join(self.path, 'packed-refs')
276             try:
277                 f = GitFile(path, 'rb')
278             except IOError, e:
279                 if e.errno == errno.ENOENT:
280                     return {}
281                 raise
282             try:
283                 first_line = iter(f).next().rstrip()
284                 if (first_line.startswith("# pack-refs") and " peeled" in
285                         first_line):
286                     for sha, name, peeled in read_packed_refs_with_peeled(f):
287                         self._packed_refs[name] = sha
288                         if peeled:
289                             self._peeled_refs[name] = peeled
290                 else:
291                     f.seek(0)
292                     for sha, name in read_packed_refs(f):
293                         self._packed_refs[name] = sha
294             finally:
295                 f.close()
296         return self._packed_refs
297
298     def read_loose_ref(self, name):
299         """Read a reference file and return its contents.
300
301         If the reference file a symbolic reference, only read the first line of
302         the file. Otherwise, only read the first 40 bytes.
303
304         :param name: the refname to read, relative to refpath
305         :return: The contents of the ref file, or None if the file does not
306             exist.
307         :raises IOError: if any other error occurs
308         """
309         filename = self.refpath(name)
310         try:
311             f = GitFile(filename, 'rb')
312             try:
313                 header = f.read(len(SYMREF))
314                 if header == SYMREF:
315                     # Read only the first line
316                     return header + iter(f).next().rstrip("\n")
317                 else:
318                     # Read only the first 40 bytes
319                     return header + f.read(40-len(SYMREF))
320             finally:
321                 f.close()
322         except IOError, e:
323             if e.errno == errno.ENOENT:
324                 return None
325             raise
326
327     def _remove_packed_ref(self, name):
328         if self._packed_refs is None:
329             return
330         filename = os.path.join(self.path, 'packed-refs')
331         # reread cached refs from disk, while holding the lock
332         f = GitFile(filename, 'wb')
333         try:
334             self._packed_refs = None
335             self.get_packed_refs()
336
337             if name not in self._packed_refs:
338                 return
339
340             del self._packed_refs[name]
341             if name in self._peeled_refs:
342                 del self._peeled_refs[name]
343             write_packed_refs(f, self._packed_refs, self._peeled_refs)
344             f.close()
345         finally:
346             f.abort()
347
348     def set_if_equals(self, name, old_ref, new_ref):
349         """Set a refname to new_ref only if it currently equals old_ref.
350
351         This method follows all symbolic references, and can be used to perform
352         an atomic compare-and-swap operation.
353
354         :param name: The refname to set.
355         :param old_ref: The old sha the refname must refer to, or None to set
356             unconditionally.
357         :param new_ref: The new sha the refname will refer to.
358         :return: True if the set was successful, False otherwise.
359         """
360         try:
361             realname, _ = self._follow(name)
362         except KeyError:
363             realname = name
364         filename = self.refpath(realname)
365         ensure_dir_exists(os.path.dirname(filename))
366         f = GitFile(filename, 'wb')
367         try:
368             if old_ref is not None:
369                 try:
370                     # read again while holding the lock
371                     orig_ref = self.read_loose_ref(realname)
372                     if orig_ref is None:
373                         orig_ref = self.get_packed_refs().get(realname, None)
374                     if orig_ref != old_ref:
375                         f.abort()
376                         return False
377                 except (OSError, IOError):
378                     f.abort()
379                     raise
380             try:
381                 f.write(new_ref+"\n")
382             except (OSError, IOError):
383                 f.abort()
384                 raise
385         finally:
386             f.close()
387         return True
388
389     def add_if_new(self, name, ref):
390         """Add a new reference only if it does not already exist."""
391         self._check_refname(name)
392         filename = self.refpath(name)
393         ensure_dir_exists(os.path.dirname(filename))
394         f = GitFile(filename, 'wb')
395         try:
396             if os.path.exists(filename) or name in self.get_packed_refs():
397                 f.abort()
398                 return False
399             try:
400                 f.write(ref+"\n")
401             except (OSError, IOError):
402                 f.abort()
403                 raise
404         finally:
405             f.close()
406         return True
407
408     def __setitem__(self, name, ref):
409         """Set a reference name to point to the given SHA1.
410
411         This method follows all symbolic references.
412
413         :note: This method unconditionally overwrites the contents of a reference
414             on disk. To update atomically only if the reference has not changed
415             on disk, use set_if_equals().
416         """
417         self.set_if_equals(name, None, ref)
418
419     def remove_if_equals(self, name, old_ref):
420         """Remove a refname only if it currently equals old_ref.
421
422         This method does not follow symbolic references. It can be used to
423         perform an atomic compare-and-delete operation.
424
425         :param name: The refname to delete.
426         :param old_ref: The old sha the refname must refer to, or None to delete
427             unconditionally.
428         :return: True if the delete was successful, False otherwise.
429         """
430         self._check_refname(name)
431         filename = self.refpath(name)
432         ensure_dir_exists(os.path.dirname(filename))
433         f = GitFile(filename, 'wb')
434         try:
435             if old_ref is not None:
436                 orig_ref = self.read_loose_ref(name)
437                 if orig_ref is None:
438                     orig_ref = self.get_packed_refs().get(name, None)
439                 if orig_ref != old_ref:
440                     return False
441             # may only be packed
442             try:
443                 os.remove(filename)
444             except OSError, e:
445                 if e.errno != errno.ENOENT:
446                     raise
447             self._remove_packed_ref(name)
448         finally:
449             # never write, we just wanted the lock
450             f.abort()
451         return True
452
453     def __delitem__(self, name):
454         """Remove a refname.
455
456         This method does not follow symbolic references.
457         :note: This method unconditionally deletes the contents of a reference
458             on disk. To delete atomically only if the reference has not changed
459             on disk, use set_if_equals().
460         """
461         self.remove_if_equals(name, None)
462
463
464 def _split_ref_line(line):
465     """Split a single ref line into a tuple of SHA1 and name."""
466     fields = line.rstrip("\n").split(" ")
467     if len(fields) != 2:
468         raise PackedRefsException("invalid ref line '%s'" % line)
469     sha, name = fields
470     try:
471         hex_to_sha(sha)
472     except (AssertionError, TypeError), e:
473         raise PackedRefsException(e)
474     if not check_ref_format(name):
475         raise PackedRefsException("invalid ref name '%s'" % name)
476     return (sha, name)
477
478
479 def read_packed_refs(f):
480     """Read a packed refs file.
481
482     Yields tuples with SHA1s and ref names.
483
484     :param f: file-like object to read from
485     """
486     for l in f:
487         if l[0] == "#":
488             # Comment
489             continue
490         if l[0] == "^":
491             raise PackedRefsException(
492                 "found peeled ref in packed-refs without peeled")
493         yield _split_ref_line(l)
494
495
496 def read_packed_refs_with_peeled(f):
497     """Read a packed refs file including peeled refs.
498
499     Assumes the "# pack-refs with: peeled" line was already read. Yields tuples
500     with ref names, SHA1s, and peeled SHA1s (or None).
501
502     :param f: file-like object to read from, seek'ed to the second line
503     """
504     last = None
505     for l in f:
506         if l[0] == "#":
507             continue
508         l = l.rstrip("\n")
509         if l[0] == "^":
510             if not last:
511                 raise PackedRefsException("unexpected peeled ref line")
512             try:
513                 hex_to_sha(l[1:])
514             except (AssertionError, TypeError), e:
515                 raise PackedRefsException(e)
516             sha, name = _split_ref_line(last)
517             last = None
518             yield (sha, name, l[1:])
519         else:
520             if last:
521                 sha, name = _split_ref_line(last)
522                 yield (sha, name, None)
523             last = l
524     if last:
525         sha, name = _split_ref_line(last)
526         yield (sha, name, None)
527
528
529 def write_packed_refs(f, packed_refs, peeled_refs=None):
530     """Write a packed refs file.
531
532     :param f: empty file-like object to write to
533     :param packed_refs: dict of refname to sha of packed refs to write
534     """
535     if peeled_refs is None:
536         peeled_refs = {}
537     else:
538         f.write('# pack-refs with: peeled\n')
539     for refname in sorted(packed_refs.iterkeys()):
540         f.write('%s %s\n' % (packed_refs[refname], refname))
541         if refname in peeled_refs:
542             f.write('^%s\n' % peeled_refs[refname])
543
544
545 class BaseRepo(object):
546     """Base class for a git repository.
547
548     :ivar object_store: Dictionary-like object for accessing
549         the objects
550     :ivar refs: Dictionary-like object with the refs in this repository
551     """
552
553     def __init__(self, object_store, refs):
554         self.object_store = object_store
555         self.refs = refs
556
557     def get_named_file(self, path):
558         """Get a file from the control dir with a specific name.
559
560         Although the filename should be interpreted as a filename relative to
561         the control dir in a disk-baked Repo, the object returned need not be
562         pointing to a file in that location.
563
564         :param path: The path to the file, relative to the control dir.
565         :return: An open file object, or None if the file does not exist.
566         """
567         raise NotImplementedError(self.get_named_file)
568
569     def put_named_file(self, relpath, contents):
570         """Write a file in the control directory with specified name and 
571         contents.
572
573         Although the filename should be interpreted as a filename relative to
574         the control dir in a disk-baked Repo, the object returned need not be
575         pointing to a file in that location.
576
577         :param path: The path to the file, relative to the control dir.
578         :param contents: Contents of the new file
579         """
580         raise NotImplementedError(self.put_named_file)
581
582     def open_index(self):
583         """Open the index for this repository.
584         
585         :raises NoIndexPresent: If no index is present
586         :return: Index instance
587         """
588         raise NotImplementedError(self.open_index)
589
590     def fetch(self, target, determine_wants=None, progress=None):
591         """Fetch objects into another repository.
592
593         :param target: The target repository
594         :param determine_wants: Optional function to determine what refs to 
595             fetch.
596         :param progress: Optional progress function
597         """
598         if determine_wants is None:
599             determine_wants = lambda heads: heads.values()
600         target.object_store.add_objects(
601             self.fetch_objects(determine_wants, target.get_graph_walker(),
602                 progress))
603         return self.get_refs()
604
605     def fetch_objects(self, determine_wants, graph_walker, progress):
606         """Fetch the missing objects required for a set of revisions.
607
608         :param determine_wants: Function that takes a dictionary with heads 
609             and returns the list of heads to fetch.
610         :param graph_walker: Object that can iterate over the list of revisions 
611             to fetch and has an "ack" method that will be called to acknowledge 
612             that a revision is present.
613         :param progress: Simple progress function that will be called with 
614             updated progress strings.
615         :return: iterator over objects, with __len__ implemented
616         """
617         wants = determine_wants(self.get_refs())
618         haves = self.object_store.find_common_revisions(graph_walker)
619         return self.object_store.iter_shas(
620             self.object_store.find_missing_objects(haves, wants, progress))
621
622     def get_graph_walker(self, heads=None):
623         if heads is None:
624             heads = self.refs.as_dict('refs/heads').values()
625         return self.object_store.get_graph_walker(heads)
626
627     def ref(self, name):
628         """Return the SHA1 a ref is pointing to."""
629         return self.refs[name]
630
631     def get_refs(self):
632         """Get dictionary with all refs."""
633         return self.refs.as_dict()
634
635     def head(self):
636         """Return the SHA1 pointed at by HEAD."""
637         return self.refs['HEAD']
638
639     def _get_object(self, sha, cls):
640         assert len(sha) in (20, 40)
641         ret = self.get_object(sha)
642         if ret._type != cls._type:
643             if cls is Commit:
644                 raise NotCommitError(ret)
645             elif cls is Blob:
646                 raise NotBlobError(ret)
647             elif cls is Tree:
648                 raise NotTreeError(ret)
649             else:
650                 raise Exception("Type invalid: %r != %r" % (ret._type, cls._type))
651         return ret
652
653     def get_object(self, sha):
654         return self.object_store[sha]
655
656     def get_parents(self, sha):
657         return self.commit(sha).parents
658
659     def get_config(self):
660         from configobj import ConfigObj
661         return ConfigObj(os.path.join(self._controldir, 'config'))
662
663     def commit(self, sha):
664         return self._get_object(sha, Commit)
665
666     def tree(self, sha):
667         return self._get_object(sha, Tree)
668
669     def tag(self, sha):
670         return self._get_object(sha, Tag)
671
672     def get_blob(self, sha):
673         return self._get_object(sha, Blob)
674
675     def revision_history(self, head):
676         """Returns a list of the commits reachable from head.
677
678         Returns a list of commit objects. the first of which will be the commit
679         of head, then following theat will be the parents.
680
681         Raises NotCommitError if any no commits are referenced, including if the
682         head parameter isn't the sha of a commit.
683
684         XXX: work out how to handle merges.
685         """
686         # We build the list backwards, as parents are more likely to be older
687         # than children
688         pending_commits = [head]
689         history = []
690         while pending_commits != []:
691             head = pending_commits.pop(0)
692             try:
693                 commit = self.commit(head)
694             except KeyError:
695                 raise MissingCommitError(head)
696             if commit in history:
697                 continue
698             i = 0
699             for known_commit in history:
700                 if known_commit.commit_time > commit.commit_time:
701                     break
702                 i += 1
703             history.insert(i, commit)
704             parents = commit.parents
705             pending_commits += parents
706         history.reverse()
707         return history
708
709     def __getitem__(self, name):
710         if len(name) in (20, 40):
711             return self.object_store[name]
712         return self.object_store[self.refs[name]]
713
714     def __setitem__(self, name, value):
715         if name.startswith("refs/") or name == "HEAD":
716             if isinstance(value, ShaFile):
717                 self.refs[name] = value.id
718             elif isinstance(value, str):
719                 self.refs[name] = value
720             else:
721                 raise TypeError(value)
722         raise ValueError(name)
723
724     def __delitem__(self, name):
725         if name.startswith("refs") or name == "HEAD":
726             del self.refs[name]
727         raise ValueError(name)
728
729     def do_commit(self, committer, message,
730                   author=None, commit_timestamp=None,
731                   commit_timezone=None, author_timestamp=None, 
732                   author_timezone=None, tree=None):
733         """Create a new commit.
734
735         :param committer: Committer fullname
736         :param message: Commit message
737         :param author: Author fullname (defaults to committer)
738         :param commit_timestamp: Commit timestamp (defaults to now)
739         :param commit_timezone: Commit timestamp timezone (defaults to GMT)
740         :param author_timestamp: Author timestamp (defaults to commit timestamp)
741         :param author_timezone: Author timestamp timezone 
742             (defaults to commit timestamp timezone)
743         :param tree: SHA1 of the tree root to use (if not specified the current index will be committed).
744         :return: New commit SHA1
745         """
746         from dulwich.index import commit_index
747         import time
748         index = self.open_index()
749         c = Commit()
750         if tree is None:
751             c.tree = commit_index(self.object_store, index)
752         else:
753             c.tree = tree
754         c.committer = committer
755         if commit_timestamp is None:
756             commit_timestamp = time.time()
757         c.commit_time = int(commit_timestamp)
758         if commit_timezone is None:
759             commit_timezone = 0
760         c.commit_timezone = commit_timezone
761         if author is None:
762             author = committer
763         c.author = author
764         if author_timestamp is None:
765             author_timestamp = commit_timestamp
766         c.author_time = int(author_timestamp)
767         if author_timezone is None:
768             author_timezone = commit_timezone
769         c.author_timezone = author_timezone
770         c.message = message
771         self.object_store.add_object(c)
772         self.refs["HEAD"] = c.id
773         return c.id
774
775
776 class Repo(BaseRepo):
777     """A git repository backed by local disk."""
778
779     def __init__(self, root):
780         if os.path.isdir(os.path.join(root, ".git", OBJECTDIR)):
781             self.bare = False
782             self._controldir = os.path.join(root, ".git")
783         elif (os.path.isdir(os.path.join(root, OBJECTDIR)) and
784               os.path.isdir(os.path.join(root, REFSDIR))):
785             self.bare = True
786             self._controldir = root
787         else:
788             raise NotGitRepository(root)
789         self.path = root
790         object_store = DiskObjectStore(
791             os.path.join(self.controldir(), OBJECTDIR))
792         refs = DiskRefsContainer(self.controldir())
793         BaseRepo.__init__(self, object_store, refs)
794
795     def controldir(self):
796         """Return the path of the control directory."""
797         return self._controldir
798
799     def put_named_file(self, path, contents):
800         """Write a file from the control dir with a specific name and contents.
801         """
802         f = GitFile(os.path.join(self.controldir(), path), 'wb')
803         try:
804             f.write(contents)
805         finally:
806             f.close()
807
808     def get_named_file(self, path):
809         """Get a file from the control dir with a specific name.
810
811         Although the filename should be interpreted as a filename relative to
812         the control dir in a disk-baked Repo, the object returned need not be
813         pointing to a file in that location.
814
815         :param path: The path to the file, relative to the control dir.
816         :return: An open file object, or None if the file does not exist.
817         """
818         try:
819             return open(os.path.join(self.controldir(), path.lstrip('/')), 'rb')
820         except (IOError, OSError), e:
821             if e.errno == errno.ENOENT:
822                 return None
823             raise
824
825     def index_path(self):
826         """Return path to the index file."""
827         return os.path.join(self.controldir(), INDEX_FILENAME)
828
829     def open_index(self):
830         """Open the index for this repository."""
831         from dulwich.index import Index
832         if not self.has_index():
833             raise NoIndexPresent()
834         return Index(self.index_path())
835
836     def has_index(self):
837         """Check if an index is present."""
838         return os.path.exists(self.index_path())
839
840     def __repr__(self):
841         return "<Repo at %r>" % self.path
842
843     @classmethod
844     def init(cls, path, mkdir=True):
845         controldir = os.path.join(path, ".git")
846         os.mkdir(controldir)
847         cls.init_bare(controldir)
848         return cls(path)
849
850     @classmethod
851     def init_bare(cls, path, mkdir=True):
852         for d in BASE_DIRECTORIES:
853             os.mkdir(os.path.join(path, *d))
854         ret = cls(path)
855         ret.refs.set_ref("HEAD", "refs/heads/master")
856         ret.put_named_file('description', "Unnamed repository")
857         ret.put_named_file('config', """[core]
858     repositoryformatversion = 0
859     filemode = true
860     bare = false
861     logallrefupdates = true
862 """)
863         ret.put_named_file(os.path.join('info', 'excludes'), '')
864         return ret
865
866     create = init_bare