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
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.
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,
22
from commit import Commit
51
def __init__(self, name, ref):
58
ref_locs = ['', 'refs', 'refs/tags', 'refs/heads', 'refs/remotes']
60
def __init__(self, root):
61
if os.path.isdir(os.path.join(root, ".git", "objects")):
63
self._controldir = os.path.join(root, ".git")
64
elif os.path.isdir(os.path.join(root, "objects")):
66
self._controldir = root
68
raise NotGitRepository(root)
70
self.tags = [Tag(name, ref) for name, ref in self.get_tags().items()]
71
self._object_store = None
74
return self._controldir
76
def find_missing_objects(self, determine_wants, graph_walker, progress):
77
"""Fetch the missing objects required for a set of revisions.
79
:param determine_wants: Function that takes a dictionary with heads
80
and returns the list of heads to fetch.
81
:param graph_walker: Object that can iterate over the list of revisions
82
to fetch and has an "ack" method that will be called to acknowledge
83
that a revision is present.
84
:param progress: Simple progress function that will be called with
85
updated progress strings.
87
wants = determine_wants(self.heads())
88
commits_to_send = set(wants)
89
ref = graph_walker.next()
91
commits_to_send.add(ref)
92
if ref in self.object_store:
94
ref = graph_walker.next()
96
for sha in commits_to_send:
101
assert isinstance(c, Commit)
104
def parse_tree(tree, sha_done):
105
for mode, name, x in tree.entries():
106
if not x in sha_done:
110
parse_tree(t, sha_done)
115
if treesha not in sha_done:
116
t = self.tree(treesha)
117
sha_done.add(treesha)
118
parse_tree(t, sha_done)
120
progress("counting objects: %d\r" % len(sha_done))
123
def fetch_objects(self, determine_wants, graph_walker, progress):
124
"""Fetch the missing objects required for a set of revisions.
126
:param determine_wants: Function that takes a dictionary with heads
127
and returns the list of heads to fetch.
128
:param graph_walker: Object that can iterate over the list of revisions
129
to fetch and has an "ack" method that will be called to acknowledge
130
that a revision is present.
131
:param progress: Simple progress function that will be called with
132
updated progress strings.
134
shas = self.find_missing_objects(determine_wants, graph_walker, progress)
136
yield self.get_object(sha)
138
def object_dir(self):
139
return os.path.join(self.controldir(), OBJECTDIR)
142
def object_store(self):
143
if self._object_store is None:
144
self._object_store = ObjectStore(self.object_dir())
145
return self._object_store
148
return os.path.join(self.object_dir(), PACKDIR)
150
def _get_ref(self, file):
154
if contents.startswith(SYMREF):
155
ref = contents[len(SYMREF):]
159
assert len(contents) == 41, 'Invalid ref in %s' % file
165
for dir in self.ref_locs:
166
file = os.path.join(self.controldir(), dir, name)
167
if os.path.exists(file):
168
return self._get_ref(file)
171
ret = {"HEAD": self.head()}
172
for dir in ["refs/heads", "refs/tags"]:
173
for name in os.listdir(os.path.join(self.controldir(), dir)):
174
path = os.path.join(self.controldir(), dir, name)
175
if os.path.isfile(path):
176
ret["/".join([dir, name])] = self._get_ref(path)
179
def set_ref(self, name, value):
180
file = os.path.join(self.controldir(), name)
181
open(file, 'w').write(value+"\n")
183
def remove_ref(self, name):
184
file = os.path.join(self.controldir(), name)
185
if os.path.exists(file):
191
for root, dirs, files in os.walk(os.path.join(self.controldir(), 'refs', 'tags')):
193
ret[name] = self._get_ref(os.path.join(root, name))
198
for root, dirs, files in os.walk(os.path.join(self.controldir(), 'refs', 'heads')):
200
ret[name] = self._get_ref(os.path.join(root, name))
204
return self.ref('HEAD')
206
def _get_object(self, sha, cls):
207
assert len(sha) in (20, 40)
208
ret = self.get_object(sha)
209
if ret._type != cls._type:
211
raise NotCommitError(ret)
213
raise NotBlobError(ret)
215
raise NotTreeError(ret)
217
raise Exception("Type invalid: %r != %r" % (ret._type, cls._type))
220
def get_object(self, sha):
221
return self.object_store[sha]
223
def get_parents(self, sha):
224
return self.commit(sha).parents
226
def commit(self, sha):
227
return self._get_object(sha, Commit)
230
return self._get_object(sha, Tree)
232
def get_blob(self, sha):
233
return self._get_object(sha, Blob)
235
def revision_history(self, head):
236
"""Returns a list of the commits reachable from head.
238
Returns a list of commit objects. the first of which will be the commit
239
of head, then following theat will be the parents.
241
Raises NotCommitError if any no commits are referenced, including if the
242
head parameter isn't the sha of a commit.
244
XXX: work out how to handle merges.
246
# We build the list backwards, as parents are more likely to be older
248
pending_commits = [head]
250
while pending_commits != []:
251
head = pending_commits.pop(0)
253
commit = self.commit(head)
255
raise MissingCommitError(head)
256
if commit in history:
259
for known_commit in history:
260
if known_commit.commit_time > commit.commit_time:
263
history.insert(i, commit)
264
parents = commit.parents
265
pending_commits += parents
270
return "<Repo at %r>" % self.path
273
def init_bare(cls, path, mkdir=True):
274
for d in [["objects"],
283
os.mkdir(os.path.join(path, *d))
284
open(os.path.join(path, 'HEAD'), 'w').write("ref: refs/heads/master\n")
285
open(os.path.join(path, 'description'), 'w').write("Unnamed repository")
286
open(os.path.join(path, 'info', 'excludes'), 'w').write("")
291
class ObjectStore(object):
293
def __init__(self, path):
298
return os.path.join(self.path, PACKDIR)
300
def __contains__(self, sha):
301
# TODO: This can be more efficient
310
"""List with pack objects."""
311
if self._packs is None:
312
self._packs = list(load_packs(self.pack_dir()))
315
def _get_shafile(self, sha):
318
# Check from object dir
319
path = os.path.join(self.path, dir, file)
320
if os.path.exists(path):
321
return ShaFile.from_file(path)
324
def get_raw(self, sha):
325
"""Obtain the raw text for an object.
327
:param sha: Sha for the object.
328
:return: tuple with object type and object contents.
330
for pack in self.packs:
332
return pack.get_raw(sha, self.get_raw)
333
# FIXME: Are pack deltas ever against on-disk shafiles ?
334
ret = self._get_shafile(sha)
336
return ret.as_raw_string()
339
def __getitem__(self, sha):
340
assert len(sha) == 40, "Incorrect length sha: %s" % str(sha)
341
ret = self._get_shafile(sha)
345
type, uncomp = self.get_raw(sha)
346
return ShaFile.from_raw_string(type, uncomp)
348
def move_in_pack(self, path):
349
"""Move a specific file containing a pack into the pack directory.
351
:note: The file should be on the same file system as the
354
:param path: Path to the pack file.
357
entries = p.sorted_entries(self.get_raw)
358
basename = os.path.join(self.pack_dir(),
359
"pack-%s" % iter_sha1(entry[0] for entry in entries))
360
write_pack_index_v2(basename+".idx", entries, p.calculate_checksum())
361
os.rename(path, basename + ".pack")
364
"""Add a new pack to this object store.
366
:return: Fileobject to write to and a commit function to
367
call when the pack is finished.
369
fd, path = tempfile.mkstemp(dir=self.pack_dir(), suffix=".pack")
370
f = os.fdopen(fd, 'w')
372
if os.path.getsize(path) > 0:
373
self.move_in_pack(path)