1 # object_store.py -- Object store for git objects
2 # Copyright (C) 2008-2013 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; either version 2
8 # or (at your option) a later version of the License.
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,
21 """Git object store interfaces and implementation."""
24 from cStringIO import StringIO
31 from dulwich.diff_tree import (
35 from dulwich.errors import (
38 from dulwich.file import GitFile
39 from dulwich.objects import (
51 from dulwich.pack import (
69 class BaseObjectStore(object):
70 """Object store interface."""
72 def determine_wants_all(self, refs):
73 return [sha for (ref, sha) in refs.iteritems()
74 if not sha in self and not ref.endswith("^{}") and
77 def iter_shas(self, shas):
78 """Iterate over the objects for the specified shas.
80 :param shas: Iterable object with SHAs
81 :return: Object iterator
83 return ObjectStoreIterator(self, shas)
85 def contains_loose(self, sha):
86 """Check if a particular object is present by SHA1 and is loose."""
87 raise NotImplementedError(self.contains_loose)
89 def contains_packed(self, sha):
90 """Check if a particular object is present by SHA1 and is packed."""
91 raise NotImplementedError(self.contains_packed)
93 def __contains__(self, sha):
94 """Check if a particular object is present by SHA1.
96 This method makes no distinction between loose and packed objects.
98 return self.contains_packed(sha) or self.contains_loose(sha)
102 """Iterable of pack objects."""
103 raise NotImplementedError
105 def get_raw(self, name):
106 """Obtain the raw text for an object.
108 :param name: sha for the object.
109 :return: tuple with numeric type and object contents.
111 raise NotImplementedError(self.get_raw)
113 def __getitem__(self, sha):
114 """Obtain an object by SHA1."""
115 type_num, uncomp = self.get_raw(sha)
116 return ShaFile.from_raw_string(type_num, uncomp)
119 """Iterate over the SHAs that are present in this store."""
120 raise NotImplementedError(self.__iter__)
122 def add_object(self, obj):
123 """Add a single object to this object store.
126 raise NotImplementedError(self.add_object)
128 def add_objects(self, objects):
129 """Add a set of objects to this object store.
131 :param objects: Iterable over a list of objects.
133 raise NotImplementedError(self.add_objects)
135 def tree_changes(self, source, target, want_unchanged=False):
136 """Find the differences between the contents of two trees
138 :param source: SHA1 of the source tree
139 :param target: SHA1 of the target tree
140 :param want_unchanged: Whether unchanged files should be reported
141 :return: Iterator over tuples with
142 (oldpath, newpath), (oldmode, newmode), (oldsha, newsha)
144 for change in tree_changes(self, source, target,
145 want_unchanged=want_unchanged):
146 yield ((change.old.path, change.new.path),
147 (change.old.mode, change.new.mode),
148 (change.old.sha, change.new.sha))
150 def iter_tree_contents(self, tree_id, include_trees=False):
151 """Iterate the contents of a tree and all subtrees.
153 Iteration is depth-first pre-order, as in e.g. os.walk.
155 :param tree_id: SHA1 of the tree.
156 :param include_trees: If True, include tree objects in the iteration.
157 :return: Iterator over TreeEntry namedtuples for all the objects in a
160 for entry, _ in walk_trees(self, tree_id, None):
161 if not stat.S_ISDIR(entry.mode) or include_trees:
164 def find_missing_objects(self, haves, wants, progress=None,
166 """Find the missing objects required for a set of revisions.
168 :param haves: Iterable over SHAs already in common.
169 :param wants: Iterable over SHAs of objects to fetch.
170 :param progress: Simple progress function that will be called with
171 updated progress strings.
172 :param get_tagged: Function that returns a dict of pointed-to sha -> tag
173 sha for including tags.
174 :return: Iterator over (sha, path) pairs.
176 finder = MissingObjectFinder(self, haves, wants, progress, get_tagged)
177 return iter(finder.next, None)
179 def find_common_revisions(self, graphwalker):
180 """Find which revisions this store has in common using graphwalker.
182 :param graphwalker: A graphwalker object.
183 :return: List of SHAs that are in common
186 sha = graphwalker.next()
191 sha = graphwalker.next()
194 def get_graph_walker(self, heads):
195 """Obtain a graph walker for this object store.
197 :param heads: Local heads to start search with
198 :return: GraphWalker object
200 return ObjectStoreGraphWalker(heads, lambda sha: self[sha].parents)
202 def generate_pack_contents(self, have, want, progress=None):
203 """Iterate over the contents of a pack file.
205 :param have: List of SHA1s of objects that should not be sent
206 :param want: List of SHA1s of objects that should be sent
207 :param progress: Optional progress reporting method
209 return self.iter_shas(self.find_missing_objects(have, want, progress))
211 def peel_sha(self, sha):
212 """Peel all tags from a SHA.
214 :param sha: The object SHA to peel.
215 :return: The fully-peeled SHA1 of a tag object, after peeling all
216 intermediate tags; if the original ref does not point to a tag, this
217 will equal the original SHA1.
220 obj_class = object_class(obj.type_name)
221 while obj_class is Tag:
222 obj_class, sha = obj.object
226 def _collect_ancestors(self, heads, common=set()):
227 """Collect all ancestors of heads up to (excluding) those in common.
229 :param heads: commits to start from
230 :param common: commits to end at, or empty set to walk repository
232 :return: a tuple (A, B) where A - all commits reachable
233 from heads but not present in common, B - common (shared) elements
234 that are directly reachable from heads
244 elif e not in commits:
247 queue.extend(cmt.parents)
248 return (commits, bases)
251 """Close any files opened by this object store."""
252 # Default implementation is a NO-OP
255 class PackBasedObjectStore(BaseObjectStore):
258 self._pack_cache = None
261 def alternates(self):
264 def contains_packed(self, sha):
265 """Check if a particular object is present by SHA1 and is packed.
267 This does not check alternates.
269 for pack in self.packs:
274 def __contains__(self, sha):
275 """Check if a particular object is present by SHA1.
277 This method makes no distinction between loose and packed objects.
279 if self.contains_packed(sha) or self.contains_loose(sha):
281 for alternate in self.alternates:
286 def _load_packs(self):
287 raise NotImplementedError(self._load_packs)
289 def _pack_cache_stale(self):
290 """Check whether the pack cache is stale."""
291 raise NotImplementedError(self._pack_cache_stale)
293 def _add_known_pack(self, pack):
294 """Add a newly appeared pack to the cache by path.
297 if self._pack_cache is not None:
298 self._pack_cache.append(pack)
301 pack_cache = self._pack_cache
302 self._pack_cache = None
304 pack = pack_cache.pop()
309 """List with pack objects."""
310 if self._pack_cache is None or self._pack_cache_stale():
311 self._pack_cache = self._load_packs()
312 return self._pack_cache
314 def _iter_alternate_objects(self):
315 """Iterate over the SHAs of all the objects in alternate stores."""
316 for alternate in self.alternates:
317 for alternate_object in alternate:
318 yield alternate_object
320 def _iter_loose_objects(self):
321 """Iterate over the SHAs of all loose objects."""
322 raise NotImplementedError(self._iter_loose_objects)
324 def _get_loose_object(self, sha):
325 raise NotImplementedError(self._get_loose_object)
327 def _remove_loose_object(self, sha):
328 raise NotImplementedError(self._remove_loose_object)
330 def pack_loose_objects(self):
331 """Pack loose objects.
333 :return: Number of objects packed
336 for sha in self._iter_loose_objects():
337 objects.add((self._get_loose_object(sha), None))
338 self.add_objects(list(objects))
339 for obj, path in objects:
340 self._remove_loose_object(obj.id)
344 """Iterate over the SHAs that are present in this store."""
345 iterables = self.packs + [self._iter_loose_objects()] + [self._iter_alternate_objects()]
346 return itertools.chain(*iterables)
348 def contains_loose(self, sha):
349 """Check if a particular object is present by SHA1 and is loose.
351 This does not check alternates.
353 return self._get_loose_object(sha) is not None
355 def get_raw(self, name):
356 """Obtain the raw text for an object.
358 :param name: sha for the object.
359 :return: tuple with numeric type and object contents.
362 sha = hex_to_sha(name)
364 elif len(name) == 20:
368 raise AssertionError("Invalid object name %r" % name)
369 for pack in self.packs:
371 return pack.get_raw(sha)
375 hexsha = sha_to_hex(name)
376 ret = self._get_loose_object(hexsha)
378 return ret.type_num, ret.as_raw_string()
379 for alternate in self.alternates:
381 return alternate.get_raw(hexsha)
384 raise KeyError(hexsha)
386 def add_objects(self, objects):
387 """Add a set of objects to this object store.
389 :param objects: Iterable over objects, should support __len__.
390 :return: Pack object of the objects written.
392 if len(objects) == 0:
393 # Don't bother writing an empty pack file
395 f, commit, abort = self.add_pack()
397 write_pack_objects(f, objects)
405 class DiskObjectStore(PackBasedObjectStore):
406 """Git-style object store that exists on disk."""
408 def __init__(self, path):
409 """Open an object store.
411 :param path: Path of the object store.
413 super(DiskObjectStore, self).__init__()
415 self.pack_dir = os.path.join(self.path, PACKDIR)
416 self._pack_cache_time = 0
417 self._alternates = None
420 def alternates(self):
421 if self._alternates is not None:
422 return self._alternates
423 self._alternates = []
424 for path in self._read_alternate_paths():
425 self._alternates.append(DiskObjectStore(path))
426 return self._alternates
428 def _read_alternate_paths(self):
430 f = GitFile(os.path.join(self.path, "info", "alternates"),
432 except (OSError, IOError), e:
433 if e.errno == errno.ENOENT:
438 for l in f.readlines():
445 ret.append(os.path.join(self.path, l))
450 def add_alternate_path(self, path):
451 """Add an alternate path to this object store.
454 os.mkdir(os.path.join(self.path, "info"))
456 if e.errno != errno.EEXIST:
458 alternates_path = os.path.join(self.path, "info/alternates")
459 f = GitFile(alternates_path, 'wb')
462 orig_f = open(alternates_path, 'rb')
463 except (OSError, IOError), e:
464 if e.errno != errno.ENOENT:
468 f.write(orig_f.read())
471 f.write("%s\n" % path)
475 if not os.path.isabs(path):
476 path = os.path.join(self.path, path)
477 self.alternates.append(DiskObjectStore(path))
479 def _load_packs(self):
482 self._pack_cache_time = os.stat(self.pack_dir).st_mtime
483 pack_dir_contents = os.listdir(self.pack_dir)
484 for name in pack_dir_contents:
485 # TODO: verify that idx exists first
486 if name.startswith("pack-") and name.endswith(".pack"):
487 filename = os.path.join(self.pack_dir, name)
488 pack_files.append((os.stat(filename).st_mtime, filename))
490 if e.errno == errno.ENOENT:
493 pack_files.sort(reverse=True)
494 suffix_len = len(".pack")
497 for _, f in pack_files:
498 result.append(Pack(f[:-suffix_len]))
505 def _pack_cache_stale(self):
507 return os.stat(self.pack_dir).st_mtime > self._pack_cache_time
509 if e.errno == errno.ENOENT:
513 def _get_shafile_path(self, sha):
514 # Check from object dir
515 return hex_to_filename(self.path, sha)
517 def _iter_loose_objects(self):
518 for base in os.listdir(self.path):
521 for rest in os.listdir(os.path.join(self.path, base)):
524 def _get_loose_object(self, sha):
525 path = self._get_shafile_path(sha)
527 return ShaFile.from_path(path)
528 except (OSError, IOError), e:
529 if e.errno == errno.ENOENT:
533 def _remove_loose_object(self, sha):
534 os.remove(self._get_shafile_path(sha))
536 def _complete_thin_pack(self, f, path, copier, indexer):
537 """Move a specific file containing a pack into the pack directory.
539 :note: The file should be on the same file system as the
542 :param f: Open file object for the pack.
543 :param path: Path to the pack file.
544 :param copier: A PackStreamCopier to use for writing pack data.
545 :param indexer: A PackIndexer for indexing the pack.
547 entries = list(indexer)
549 # Update the header with the new number of objects.
551 write_pack_header(f, len(entries) + len(indexer.ext_refs()))
553 # Must flush before reading (http://bugs.python.org/issue3207)
556 # Rescan the rest of the pack, computing the SHA with the new header.
557 new_sha = compute_file_sha(f, end_ofs=-20)
559 # Must reposition before writing (http://bugs.python.org/issue3207)
560 f.seek(0, os.SEEK_CUR)
563 for ext_sha in indexer.ext_refs():
564 assert len(ext_sha) == 20
565 type_num, data = self.get_raw(ext_sha)
567 crc32 = write_pack_object(f, type_num, data, sha=new_sha)
568 entries.append((ext_sha, offset, crc32))
569 pack_sha = new_sha.digest()
575 pack_base_name = os.path.join(
576 self.pack_dir, 'pack-' + iter_sha1(e[0] for e in entries))
577 os.rename(path, pack_base_name + '.pack')
580 index_file = GitFile(pack_base_name + '.idx', 'wb')
582 write_pack_index_v2(index_file, entries, pack_sha)
587 # Add the pack to the store and return it.
588 final_pack = Pack(pack_base_name)
589 final_pack.check_length_and_checksum()
590 self._add_known_pack(final_pack)
593 def add_thin_pack(self, read_all, read_some):
594 """Add a new thin pack to this object store.
596 Thin packs are packs that contain deltas with parents that exist outside
597 the pack. They should never be placed in the object store directly, and
598 always indexed and completed as they are copied.
600 :param read_all: Read function that blocks until the number of requested
602 :param read_some: Read function that returns at least one byte, but may
603 not return the number of bytes requested.
604 :return: A Pack object pointing at the now-completed thin pack in the
605 objects/pack directory.
607 fd, path = tempfile.mkstemp(dir=self.path, prefix='tmp_pack_')
608 f = os.fdopen(fd, 'w+b')
611 indexer = PackIndexer(f, resolve_ext_ref=self.get_raw)
612 copier = PackStreamCopier(read_all, read_some, f,
615 return self._complete_thin_pack(f, path, copier, indexer)
619 def move_in_pack(self, path):
620 """Move a specific file containing a pack into the pack directory.
622 :note: The file should be on the same file system as the
625 :param path: Path to the pack file.
629 entries = p.sorted_entries()
630 basename = os.path.join(self.pack_dir,
631 "pack-%s" % iter_sha1(entry[0] for entry in entries))
632 f = GitFile(basename+".idx", "wb")
634 write_pack_index_v2(f, entries, p.get_stored_checksum())
639 os.rename(path, basename + ".pack")
640 final_pack = Pack(basename)
641 self._add_known_pack(final_pack)
645 """Add a new pack to this object store.
647 :return: Fileobject to write to, a commit function to
648 call when the pack is finished and an abort
651 fd, path = tempfile.mkstemp(dir=self.pack_dir, suffix=".pack")
652 f = os.fdopen(fd, 'wb')
656 if os.path.getsize(path) > 0:
657 return self.move_in_pack(path)
664 return f, commit, abort
666 def add_object(self, obj):
667 """Add a single object to this object store.
669 :param obj: Object to add
671 dir = os.path.join(self.path, obj.id[:2])
675 if e.errno != errno.EEXIST:
677 path = os.path.join(dir, obj.id[2:])
678 if os.path.exists(path):
679 return # Already there, no need to write again
680 f = GitFile(path, 'wb')
682 f.write(obj.as_legacy_object())
691 if e.errno != errno.EEXIST:
693 os.mkdir(os.path.join(path, "info"))
694 os.mkdir(os.path.join(path, PACKDIR))
698 class MemoryObjectStore(BaseObjectStore):
699 """Object store that keeps all objects in memory."""
702 super(MemoryObjectStore, self).__init__()
705 def _to_hexsha(self, sha):
709 return sha_to_hex(sha)
711 raise ValueError("Invalid sha %r" % (sha,))
713 def contains_loose(self, sha):
714 """Check if a particular object is present by SHA1 and is loose."""
715 return self._to_hexsha(sha) in self._data
717 def contains_packed(self, sha):
718 """Check if a particular object is present by SHA1 and is packed."""
722 """Iterate over the SHAs that are present in this store."""
723 return self._data.iterkeys()
727 """List with pack objects."""
730 def get_raw(self, name):
731 """Obtain the raw text for an object.
733 :param name: sha for the object.
734 :return: tuple with numeric type and object contents.
736 obj = self[self._to_hexsha(name)]
737 return obj.type_num, obj.as_raw_string()
739 def __getitem__(self, name):
740 return self._data[self._to_hexsha(name)]
742 def __delitem__(self, name):
743 """Delete an object from this store, for testing only."""
744 del self._data[self._to_hexsha(name)]
746 def add_object(self, obj):
747 """Add a single object to this object store.
750 self._data[obj.id] = obj
752 def add_objects(self, objects):
753 """Add a set of objects to this object store.
755 :param objects: Iterable over a list of objects.
757 for obj, path in objects:
758 self._data[obj.id] = obj
761 """Add a new pack to this object store.
763 Because this object store doesn't support packs, we extract and add the
766 :return: Fileobject to write to and a commit function to
767 call when the pack is finished.
771 p = PackData.from_file(StringIO(f.getvalue()), f.tell())
773 for obj in PackInflater.for_pack_data(p):
774 self._data[obj.id] = obj
777 return f, commit, abort
779 def _complete_thin_pack(self, f, indexer):
780 """Complete a thin pack by adding external references.
782 :param f: Open file object for the pack.
783 :param indexer: A PackIndexer for indexing the pack.
785 entries = list(indexer)
787 # Update the header with the new number of objects.
789 write_pack_header(f, len(entries) + len(indexer.ext_refs()))
791 # Rescan the rest of the pack, computing the SHA with the new header.
792 new_sha = compute_file_sha(f, end_ofs=-20)
795 for ext_sha in indexer.ext_refs():
796 assert len(ext_sha) == 20
797 type_num, data = self.get_raw(ext_sha)
798 write_pack_object(f, type_num, data, sha=new_sha)
799 pack_sha = new_sha.digest()
802 def add_thin_pack(self, read_all, read_some):
803 """Add a new thin pack to this object store.
805 Thin packs are packs that contain deltas with parents that exist outside
806 the pack. Because this object store doesn't support packs, we extract
807 and add the individual objects.
809 :param read_all: Read function that blocks until the number of requested
811 :param read_some: Read function that returns at least one byte, but may
812 not return the number of bytes requested.
814 f, commit, abort = self.add_pack()
816 indexer = PackIndexer(f, resolve_ext_ref=self.get_raw)
817 copier = PackStreamCopier(read_all, read_some, f, delta_iter=indexer)
819 self._complete_thin_pack(f, indexer)
827 class ObjectImporter(object):
828 """Interface for importing objects."""
830 def __init__(self, count):
831 """Create a new ObjectImporter.
833 :param count: Number of objects that's going to be imported.
837 def add_object(self, object):
839 raise NotImplementedError(self.add_object)
841 def finish(self, object):
842 """Finish the import and write objects to disk."""
843 raise NotImplementedError(self.finish)
846 class ObjectIterator(object):
847 """Interface for iterating over objects."""
849 def iterobjects(self):
850 raise NotImplementedError(self.iterobjects)
853 class ObjectStoreIterator(ObjectIterator):
854 """ObjectIterator that works on top of an ObjectStore."""
856 def __init__(self, store, sha_iter):
857 """Create a new ObjectIterator.
859 :param store: Object store to retrieve from
860 :param sha_iter: Iterator over (sha, path) tuples
863 self.sha_iter = sha_iter
867 """Yield tuple with next object and path."""
868 for sha, path in self.itershas():
869 yield self.store[sha], path
871 def iterobjects(self):
872 """Iterate over just the objects."""
877 """Iterate over the SHAs."""
878 for sha in self._shas:
880 for sha in self.sha_iter:
881 self._shas.append(sha)
884 def __contains__(self, needle):
885 """Check if an object is present.
887 :note: This checks if the object is present in
888 the underlying object store, not if it would
889 be yielded by the iterator.
891 :param needle: SHA1 of the object to check for
893 return needle in self.store
895 def __getitem__(self, key):
896 """Find an object by SHA1.
898 :note: This retrieves the object from the underlying
899 object store. It will also succeed if the object would
900 not be returned by the iterator.
902 return self.store[key]
905 """Return the number of objects."""
906 return len(list(self.itershas()))
909 def tree_lookup_path(lookup_obj, root_sha, path):
910 """Look up an object in a Git tree.
912 :param lookup_obj: Callback for retrieving object by SHA1
913 :param root_sha: SHA1 of the root tree
914 :param path: Path to lookup
915 :return: A tuple of (mode, SHA) of the resulting path.
917 tree = lookup_obj(root_sha)
918 if not isinstance(tree, Tree):
919 raise NotTreeError(root_sha)
920 return tree.lookup_path(lookup_obj, path)
923 def _collect_filetree_revs(obj_store, tree_sha, kset):
924 """Collect SHA1s of files and directories for specified tree.
926 :param obj_store: Object store to get objects by SHA from
927 :param tree_sha: tree reference to walk
928 :param kset: set to fill with references to files and directories
930 filetree = obj_store[tree_sha]
931 for name, mode, sha in filetree.iteritems():
932 if not S_ISGITLINK(mode) and sha not in kset:
934 if stat.S_ISDIR(mode):
935 _collect_filetree_revs(obj_store, sha, kset)
938 def _split_commits_and_tags(obj_store, lst, ignore_unknown=False):
939 """Split object id list into two list with commit SHA1s and tag SHA1s.
941 Commits referenced by tags are included into commits
942 list as well. Only SHA1s known in this repository will get
943 through, and unless ignore_unknown argument is True, KeyError
944 is thrown for SHA1 missing in the repository
946 :param obj_store: Object store to get objects by SHA1 from
947 :param lst: Collection of commit and tag SHAs
948 :param ignore_unknown: True to skip SHA1 missing in the repository
950 :return: A tuple of (commits, tags) SHA1s
958 if not ignore_unknown:
961 if isinstance(o, Commit):
963 elif isinstance(o, Tag):
965 commits.add(o.object[1])
967 raise KeyError('Not a commit or a tag: %s' % e)
968 return (commits, tags)
971 class MissingObjectFinder(object):
972 """Find the objects missing from another object store.
974 :param object_store: Object store containing at least all objects to be
976 :param haves: SHA1s of commits not to send (already present in target)
977 :param wants: SHA1s of commits to send
978 :param progress: Optional function to report progress to.
979 :param get_tagged: Function that returns a dict of pointed-to sha -> tag
980 sha for including tags.
981 :param tagged: dict of pointed-to sha -> tag sha for including tags
984 def __init__(self, object_store, haves, wants, progress=None,
986 self.object_store = object_store
987 # process Commits and Tags differently
988 # Note, while haves may list commits/tags not available locally,
989 # and such SHAs would get filtered out by _split_commits_and_tags,
990 # wants shall list only known SHAs, and otherwise
991 # _split_commits_and_tags fails with KeyError
992 have_commits, have_tags = \
993 _split_commits_and_tags(object_store, haves, True)
994 want_commits, want_tags = \
995 _split_commits_and_tags(object_store, wants, False)
996 # all_ancestors is a set of commits that shall not be sent
997 # (complete repository up to 'haves')
998 all_ancestors = object_store._collect_ancestors(have_commits)[0]
999 # all_missing - complete set of commits between haves and wants
1000 # common - commits from all_ancestors we hit into while
1001 # traversing parent hierarchy of wants
1002 missing_commits, common_commits = \
1003 object_store._collect_ancestors(want_commits, all_ancestors)
1004 self.sha_done = set()
1005 # Now, fill sha_done with commits and revisions of
1006 # files and directories known to be both locally
1007 # and on target. Thus these commits and files
1008 # won't get selected for fetch
1009 for h in common_commits:
1010 self.sha_done.add(h)
1011 cmt = object_store[h]
1012 _collect_filetree_revs(object_store, cmt.tree, self.sha_done)
1013 # record tags we have as visited, too
1015 self.sha_done.add(t)
1017 missing_tags = want_tags.difference(have_tags)
1018 # in fact, what we 'want' is commits and tags
1019 # we've found missing
1020 wants = missing_commits.union(missing_tags)
1022 self.objects_to_send = set([(w, None, False) for w in wants])
1024 if progress is None:
1025 self.progress = lambda x: None
1027 self.progress = progress
1028 self._tagged = get_tagged and get_tagged() or {}
1030 def add_todo(self, entries):
1031 self.objects_to_send.update([e for e in entries
1032 if not e[0] in self.sha_done])
1036 if not self.objects_to_send:
1038 (sha, name, leaf) = self.objects_to_send.pop()
1039 if sha not in self.sha_done:
1042 o = self.object_store[sha]
1043 if isinstance(o, Commit):
1044 self.add_todo([(o.tree, "", False)])
1045 elif isinstance(o, Tree):
1046 self.add_todo([(s, n, not stat.S_ISDIR(m))
1047 for n, m, s in o.iteritems()
1048 if not S_ISGITLINK(m)])
1049 elif isinstance(o, Tag):
1050 self.add_todo([(o.object[1], None, False)])
1051 if sha in self._tagged:
1052 self.add_todo([(self._tagged[sha], None, True)])
1053 self.sha_done.add(sha)
1054 self.progress("counting objects: %d\r" % len(self.sha_done))
1058 class ObjectStoreGraphWalker(object):
1059 """Graph walker that finds what commits are missing from an object store.
1061 :ivar heads: Revisions without descendants in the local repo
1062 :ivar get_parents: Function to retrieve parents in the local repo
1065 def __init__(self, local_heads, get_parents):
1066 """Create a new instance.
1068 :param local_heads: Heads to start search with
1069 :param get_parents: Function for finding the parents of a SHA1.
1071 self.heads = set(local_heads)
1072 self.get_parents = get_parents
1076 """Ack that a revision and its ancestors are present in the source."""
1077 ancestors = set([sha])
1079 # stop if we run out of heads to remove
1083 self.heads.remove(a)
1085 # collect all ancestors
1086 new_ancestors = set()
1088 ps = self.parents.get(a)
1090 new_ancestors.update(ps)
1091 self.parents[a] = None
1093 # no more ancestors; stop
1094 if not new_ancestors:
1097 ancestors = new_ancestors
1100 """Iterate over ancestors of heads in the target."""
1102 ret = self.heads.pop()
1103 ps = self.get_parents(ret)
1104 self.parents[ret] = ps
1105 self.heads.update([p for p in ps if not p in self.parents])