Turn more bits into properties.
[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 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.
9
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
18 # MA  02110-1301, USA.
19
20 import os
21
22 from commit import Commit
23 from errors import MissingCommitError
24 from objects import (ShaFile,
25                      Commit,
26                      Tree,
27                      Blob,
28                      )
29 from pack import load_packs
30
31 OBJECTDIR = 'objects'
32 PACKDIR = 'pack'
33 SYMREF = 'ref: '
34
35
36 class Tag(object):
37
38     def __init__(self, name, ref):
39         self.name = name
40         self.ref = ref
41
42
43 class Repo(object):
44
45   ref_locs = ['', 'refs', 'refs/tags', 'refs/heads', 'refs/remotes']
46
47   def __init__(self, root):
48     controldir = os.path.join(root, ".git")
49     if os.path.exists(os.path.join(controldir, "objects")):
50       self.bare = False
51       self._basedir = controldir
52     else:
53       self.bare = True
54       self._basedir = root
55     self.path = controldir
56     self.tags = [Tag(name, ref) for name, ref in self.get_tags().items()]
57     self._packs = None
58
59   def basedir(self):
60     return self._basedir
61
62   def object_dir(self):
63     return os.path.join(self.basedir(), OBJECTDIR)
64
65   def pack_dir(self):
66     return os.path.join(self.object_dir(), PACKDIR)
67
68   def _get_packs(self):
69     if self._packs is None:
70         self._packs = list(load_packs(self.pack_dir()))
71     return self._packs
72
73   def _get_ref(self, file):
74     f = open(file, 'rb')
75     try:
76       contents = f.read()
77       if contents.startswith(SYMREF):
78         ref = contents[len(SYMREF):]
79         if ref[-1] == '\n':
80           ref = ref[:-1]
81         return self.ref(ref)
82       assert len(contents) == 41, 'Invalid ref'
83       return contents[:-1]
84     finally:
85       f.close()
86
87   def ref(self, name):
88     for dir in self.ref_locs:
89       file = os.path.join(self.basedir(), dir, name)
90       if os.path.exists(file):
91         return self._get_ref(file)
92
93   def get_tags(self):
94     ret = {}
95     for root, dirs, files in os.walk(os.path.join(self.basedir(), 'refs', 'tags')):
96       for name in files:
97         ret[name] = self._get_ref(os.path.join(root, name))
98     return ret
99
100   def heads(self):
101     ret = {}
102     for root, dirs, files in os.walk(os.path.join(self.basedir(), 'refs', 'heads')):
103       for name in files:
104         ret[name] = self._get_ref(os.path.join(root, name))
105     return ret
106
107   def head(self):
108     return self.ref('HEAD')
109
110   def _get_object(self, sha, cls):
111     assert len(sha) == 40, "Incorrect length sha: %s" % str(sha)
112     dir = sha[:2]
113     file = sha[2:]
114     # Check from object dir
115     path = os.path.join(self.object_dir(), dir, file)
116     if os.path.exists(path):
117       return cls.from_file(path)
118     # Check from packs
119     for pack in self._get_packs():
120         if sha in pack:
121             return pack[sha]
122     # Should this raise instead?
123     return None
124
125   def get_object(self, sha):
126     return self._get_object(sha, ShaFile)
127
128   def commit(self, sha):
129     return self._get_object(sha, Commit)
130
131   def get_tree(self, sha):
132     return self._get_object(sha, Tree)
133
134   def get_blob(self, sha):
135     return self._get_object(sha, Blob)
136
137   def revision_history(self, head):
138     """Returns a list of the commits reachable from head.
139
140     Returns a list of commit objects. the first of which will be the commit
141     of head, then following theat will be the parents.
142
143     Raises NotCommitError if any no commits are referenced, including if the
144     head parameter isn't the sha of a commit.
145
146     XXX: work out how to handle merges.
147     """
148     # We build the list backwards, as parents are more likely to be older
149     # than children
150     pending_commits = [head]
151     history = []
152     while pending_commits != []:
153       head = pending_commits.pop(0)
154       commit = self.commit(head)
155       if commit is None:
156         raise MissingCommitError(head)
157       if commit in history:
158         continue
159       i = 0
160       for known_commit in history:
161         if known_commit.commit_time > commit.commit_time:
162           break
163         i += 1
164       history.insert(i, commit)
165       parents = commit.parents
166       pending_commits += parents
167     history.reverse()
168     return history
169
170   @classmethod
171   def init_bare(cls, path, mkdir=True):
172       for d in [["objects"], 
173                 ["objects", "info"], 
174                 ["objects", "pack"],
175                 ["branches"],
176                 ["refs"],
177                 ["refs", "tags"],
178                 ["refs", "heads"],
179                 ["hooks"],
180                 ["info"]]:
181           os.mkdir(os.path.join(path, *d))
182       open(os.path.join(path, 'HEAD'), 'w').write("ref: refs/heads/master\n")
183       open(os.path.join(path, 'description'), 'w').write("Unnamed repository")
184       open(os.path.join(path, 'info', 'excludes'), 'w').write("")
185
186   create = init_bare
187