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>
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
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.
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,
23
from commit import Commit
31
from object_store import ObjectStore
45
def __init__(self, tagdir, tags):
49
def __getitem__(self, name):
50
return self.tags[name]
52
def __setitem__(self, name, ref):
54
f = open(os.path.join(self.tagdir, name), 'wb')
70
ref_locs = ['', 'refs', 'refs/tags', 'refs/heads', 'refs/remotes']
72
def __init__(self, root):
73
if os.path.isdir(os.path.join(root, ".git", "objects")):
75
self._controldir = os.path.join(root, ".git")
76
elif os.path.isdir(os.path.join(root, "objects")):
78
self._controldir = root
80
raise NotGitRepository(root)
82
self.tags = Tags(self.tagdir(), self.get_tags())
83
self._object_store = None
86
return self._controldir
88
def find_missing_objects(self, determine_wants, graph_walker, progress):
89
"""Fetch the missing objects required for a set of revisions.
91
:param determine_wants: Function that takes a dictionary with heads
92
and returns the list of heads to fetch.
93
:param graph_walker: Object that can iterate over the list of revisions
94
to fetch and has an "ack" method that will be called to acknowledge
95
that a revision is present.
96
:param progress: Simple progress function that will be called with
97
updated progress strings.
99
wants = determine_wants(self.get_refs())
100
commits_to_send = set(wants)
102
ref = graph_walker.next()
104
if ref in self.object_store:
105
graph_walker.ack(ref)
106
ref = graph_walker.next()
107
while commits_to_send:
108
sha = (commits_to_send.pop(), None)
113
assert isinstance(c, Commit)
114
sha_done.add((sha, None))
116
commits_to_send.update([p for p in c.parents if not p in sha_done])
118
def parse_tree(tree, sha_done):
119
for mode, name, sha in tree.entries():
120
if (sha, name) in sha_done:
122
if mode & stat.S_IFDIR:
123
parse_tree(self.tree(sha), sha_done)
124
sha_done.add((sha, name))
127
if c.tree not in sha_done:
128
parse_tree(self.tree(c.tree), sha_done)
129
sha_done.add((c.tree, None))
131
progress("counting objects: %d\r" % len(sha_done))
134
def fetch_objects(self, determine_wants, graph_walker, progress):
135
"""Fetch the missing objects required for a set of revisions.
137
:param determine_wants: Function that takes a dictionary with heads
138
and returns the list of heads to fetch.
139
:param graph_walker: Object that can iterate over the list of revisions
140
to fetch and has an "ack" method that will be called to acknowledge
141
that a revision is present.
142
:param progress: Simple progress function that will be called with
143
updated progress strings.
144
:return: tuple with number of objects, iterator over objects
146
shas = self.find_missing_objects(determine_wants, graph_walker, progress)
147
return (len(shas), ((self.get_object(sha), path) for sha, path in shas))
149
def object_dir(self):
150
return os.path.join(self.controldir(), OBJECTDIR)
153
def object_store(self):
154
if self._object_store is None:
155
self._object_store = ObjectStore(self.object_dir())
156
return self._object_store
159
return os.path.join(self.object_dir(), PACKDIR)
161
def _get_ref(self, file):
165
if contents.startswith(SYMREF):
166
ref = contents[len(SYMREF):]
170
assert len(contents) == 41, 'Invalid ref in %s' % file
176
for dir in self.ref_locs:
177
file = os.path.join(self.controldir(), dir, name)
178
if os.path.exists(file):
179
return self._get_ref(file)
184
ret['HEAD'] = self.head()
185
for dir in ["refs/heads", "refs/tags"]:
186
for name in os.listdir(os.path.join(self.controldir(), dir)):
187
path = os.path.join(self.controldir(), dir, name)
188
if os.path.isfile(path):
189
ret["/".join([dir, name])] = self._get_ref(path)
192
def set_ref(self, name, value):
193
file = os.path.join(self.controldir(), name)
194
open(file, 'w').write(value+"\n")
196
def remove_ref(self, name):
197
file = os.path.join(self.controldir(), name)
198
if os.path.exists(file):
203
return os.path.join(self.controldir(), 'refs', 'tags')
207
for root, dirs, files in os.walk(self.tagdir()):
209
ret[name] = self._get_ref(os.path.join(root, name))
214
for root, dirs, files in os.walk(os.path.join(self.controldir(), 'refs', 'heads')):
216
ret[name] = self._get_ref(os.path.join(root, name))
220
return self.ref('HEAD')
222
def _get_object(self, sha, cls):
223
assert len(sha) in (20, 40)
224
ret = self.get_object(sha)
225
if ret._type != cls._type:
227
raise NotCommitError(ret)
229
raise NotBlobError(ret)
231
raise NotTreeError(ret)
233
raise Exception("Type invalid: %r != %r" % (ret._type, cls._type))
236
def get_object(self, sha):
237
return self.object_store[sha]
239
def get_parents(self, sha):
240
return self.commit(sha).parents
242
def commit(self, sha):
243
return self._get_object(sha, Commit)
246
return self._get_object(sha, Tree)
248
def get_blob(self, sha):
249
return self._get_object(sha, Blob)
251
def revision_history(self, head):
252
"""Returns a list of the commits reachable from head.
254
Returns a list of commit objects. the first of which will be the commit
255
of head, then following theat will be the parents.
257
Raises NotCommitError if any no commits are referenced, including if the
258
head parameter isn't the sha of a commit.
260
XXX: work out how to handle merges.
262
# We build the list backwards, as parents are more likely to be older
264
pending_commits = [head]
266
while pending_commits != []:
267
head = pending_commits.pop(0)
269
commit = self.commit(head)
271
raise MissingCommitError(head)
272
if commit in history:
275
for known_commit in history:
276
if known_commit.commit_time > commit.commit_time:
279
history.insert(i, commit)
280
parents = commit.parents
281
pending_commits += parents
286
return "<Repo at %r>" % self.path
289
def init(cls, path, mkdir=True):
290
controldir = os.path.join(path, ".git")
292
cls.init_bare(controldir)
295
def init_bare(cls, path, mkdir=True):
296
for d in [["objects"],
305
os.mkdir(os.path.join(path, *d))
306
open(os.path.join(path, 'HEAD'), 'w').write("ref: refs/heads/master\n")
307
open(os.path.join(path, 'description'), 'w').write("Unnamed repository")
308
open(os.path.join(path, 'info', 'excludes'), 'w').write("")