index.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. # index.py -- File parser/writer for the git index file
  2. # Copyright (C) 2008-2009 Jelmer Vernooij <jelmer@samba.org>
  3. #
  4. # This program is free software; you can redistribute it and/or
  5. # modify it under the terms of the GNU General Public License
  6. # as published by the Free Software Foundation; version 2
  7. # of the License or (at your opinion) any later version of the license.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program; if not, write to the Free Software
  16. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
  17. # MA 02110-1301, USA.
  18. """Parser for the git index file format."""
  19. import os
  20. import stat
  21. import struct
  22. from dulwich.file import GitFile
  23. from dulwich.objects import (
  24. S_IFGITLINK,
  25. S_ISGITLINK,
  26. Tree,
  27. hex_to_sha,
  28. sha_to_hex,
  29. )
  30. from dulwich.pack import (
  31. SHA1Reader,
  32. SHA1Writer,
  33. )
  34. def pathsplit(path):
  35. """Split a /-delimited path into a directory part and a basename.
  36. :param path: The path to split.
  37. :return: Tuple with directory name and basename
  38. """
  39. try:
  40. (dirname, basename) = path.rsplit("/", 1)
  41. except ValueError:
  42. return ("", path)
  43. else:
  44. return (dirname, basename)
  45. def pathjoin(*args):
  46. """Join a /-delimited path.
  47. """
  48. return "/".join([p for p in args if p])
  49. def read_cache_time(f):
  50. """Read a cache time.
  51. :param f: File-like object to read from
  52. :return: Tuple with seconds and nanoseconds
  53. """
  54. return struct.unpack(">LL", f.read(8))
  55. def write_cache_time(f, t):
  56. """Write a cache time.
  57. :param f: File-like object to write to
  58. :param t: Time to write (as int, float or tuple with secs and nsecs)
  59. """
  60. if isinstance(t, int):
  61. t = (t, 0)
  62. elif isinstance(t, float):
  63. (secs, nsecs) = divmod(t, 1.0)
  64. t = (int(secs), int(nsecs * 1000000000))
  65. elif not isinstance(t, tuple):
  66. raise TypeError(t)
  67. f.write(struct.pack(">LL", *t))
  68. def read_cache_entry(f):
  69. """Read an entry from a cache file.
  70. :param f: File-like object to read from
  71. :return: tuple with: device, inode, mode, uid, gid, size, sha, flags
  72. """
  73. beginoffset = f.tell()
  74. ctime = read_cache_time(f)
  75. mtime = read_cache_time(f)
  76. (dev, ino, mode, uid, gid, size, sha, flags, ) = \
  77. struct.unpack(">LLLLLL20sH", f.read(20 + 4 * 6 + 2))
  78. name = f.read((flags & 0x0fff))
  79. # Padding:
  80. real_size = ((f.tell() - beginoffset + 8) & ~7)
  81. data = f.read((beginoffset + real_size) - f.tell())
  82. return (name, ctime, mtime, dev, ino, mode, uid, gid, size,
  83. sha_to_hex(sha), flags & ~0x0fff)
  84. def write_cache_entry(f, entry):
  85. """Write an index entry to a file.
  86. :param f: File object
  87. :param entry: Entry to write, tuple with:
  88. (name, ctime, mtime, dev, ino, mode, uid, gid, size, sha, flags)
  89. """
  90. beginoffset = f.tell()
  91. (name, ctime, mtime, dev, ino, mode, uid, gid, size, sha, flags) = entry
  92. write_cache_time(f, ctime)
  93. write_cache_time(f, mtime)
  94. flags = len(name) | (flags &~ 0x0fff)
  95. f.write(struct.pack(">LLLLLL20sH", dev, ino, mode, uid, gid, size, hex_to_sha(sha), flags))
  96. f.write(name)
  97. real_size = ((f.tell() - beginoffset + 8) & ~7)
  98. f.write("\0" * ((beginoffset + real_size) - f.tell()))
  99. def read_index(f):
  100. """Read an index file, yielding the individual entries."""
  101. header = f.read(4)
  102. if header != "DIRC":
  103. raise AssertionError("Invalid index file header: %r" % header)
  104. (version, num_entries) = struct.unpack(">LL", f.read(4 * 2))
  105. assert version in (1, 2)
  106. for i in range(num_entries):
  107. yield read_cache_entry(f)
  108. def read_index_dict(f):
  109. """Read an index file and return it as a dictionary.
  110. :param f: File object to read from
  111. """
  112. ret = {}
  113. for x in read_index(f):
  114. ret[x[0]] = tuple(x[1:])
  115. return ret
  116. def write_index(f, entries):
  117. """Write an index file.
  118. :param f: File-like object to write to
  119. :param entries: Iterable over the entries to write
  120. """
  121. f.write("DIRC")
  122. f.write(struct.pack(">LL", 2, len(entries)))
  123. for x in entries:
  124. write_cache_entry(f, x)
  125. def write_index_dict(f, entries):
  126. """Write an index file based on the contents of a dictionary.
  127. """
  128. entries_list = []
  129. for name in sorted(entries):
  130. entries_list.append((name,) + tuple(entries[name]))
  131. write_index(f, entries_list)
  132. def cleanup_mode(mode):
  133. """Cleanup a mode value.
  134. This will return a mode that can be stored in a tree object.
  135. :param mode: Mode to clean up.
  136. """
  137. if stat.S_ISLNK(mode):
  138. return stat.S_IFLNK
  139. elif stat.S_ISDIR(mode):
  140. return stat.S_IFDIR
  141. elif S_ISGITLINK(mode):
  142. return S_IFGITLINK
  143. ret = stat.S_IFREG | 0644
  144. ret |= (mode & 0111)
  145. return ret
  146. class Index(object):
  147. """A Git Index file."""
  148. def __init__(self, filename):
  149. """Open an index file.
  150. :param filename: Path to the index file
  151. """
  152. self._filename = filename
  153. self.clear()
  154. self.read()
  155. def __repr__(self):
  156. return "%s(%r)" % (self.__class__.__name__, self._filename)
  157. def write(self):
  158. """Write current contents of index to disk."""
  159. f = GitFile(self._filename, 'wb')
  160. try:
  161. f = SHA1Writer(f)
  162. write_index_dict(f, self._byname)
  163. finally:
  164. f.close()
  165. def read(self):
  166. """Read current contents of index from disk."""
  167. if not os.path.exists(self._filename):
  168. return
  169. f = GitFile(self._filename, 'rb')
  170. try:
  171. f = SHA1Reader(f)
  172. for x in read_index(f):
  173. self[x[0]] = tuple(x[1:])
  174. # FIXME: Additional data?
  175. f.read(os.path.getsize(self._filename)-f.tell()-20)
  176. f.check_sha()
  177. finally:
  178. f.close()
  179. def __len__(self):
  180. """Number of entries in this index file."""
  181. return len(self._byname)
  182. def __getitem__(self, name):
  183. """Retrieve entry by relative path.
  184. :return: tuple with (ctime, mtime, dev, ino, mode, uid, gid, size, sha, flags)
  185. """
  186. return self._byname[name]
  187. def __iter__(self):
  188. """Iterate over the paths in this index."""
  189. return iter(self._byname)
  190. def get_sha1(self, path):
  191. """Return the (git object) SHA1 for the object at a path."""
  192. return self[path][-2]
  193. def get_mode(self, path):
  194. """Return the POSIX file mode for the object at a path."""
  195. return self[path][-6]
  196. def iterblobs(self):
  197. """Iterate over path, sha, mode tuples for use with commit_tree."""
  198. for path in self:
  199. entry = self[path]
  200. yield path, entry[-2], cleanup_mode(entry[-6])
  201. def clear(self):
  202. """Remove all contents from this index."""
  203. self._byname = {}
  204. def __setitem__(self, name, x):
  205. assert isinstance(name, str)
  206. assert len(x) == 10
  207. # Remove the old entry if any
  208. self._byname[name] = x
  209. def __delitem__(self, name):
  210. assert isinstance(name, str)
  211. del self._byname[name]
  212. def iteritems(self):
  213. return self._byname.iteritems()
  214. def update(self, entries):
  215. for name, value in entries.iteritems():
  216. self[name] = value
  217. def changes_from_tree(self, object_store, tree, want_unchanged=False):
  218. """Find the differences between the contents of this index and a tree.
  219. :param object_store: Object store to use for retrieving tree contents
  220. :param tree: SHA1 of the root tree
  221. :param want_unchanged: Whether unchanged files should be reported
  222. :return: Iterator over tuples with (oldpath, newpath), (oldmode, newmode), (oldsha, newsha)
  223. """
  224. def lookup_entry(path):
  225. entry = self[path]
  226. return entry[-2], entry[-6]
  227. for (name, mode, sha) in changes_from_tree(self._byname.keys(),
  228. lookup_entry, object_store, tree,
  229. want_unchanged=want_unchanged):
  230. yield (name, mode, sha)
  231. def commit(self, object_store):
  232. """Create a new tree from an index.
  233. :param object_store: Object store to save the tree in
  234. :return: Root tree SHA
  235. """
  236. return commit_tree(object_store, self.iterblobs())
  237. def commit_tree(object_store, blobs):
  238. """Commit a new tree.
  239. :param object_store: Object store to add trees to
  240. :param blobs: Iterable over blob path, sha, mode entries
  241. :return: SHA1 of the created tree.
  242. """
  243. trees = {"": {}}
  244. def add_tree(path):
  245. if path in trees:
  246. return trees[path]
  247. dirname, basename = pathsplit(path)
  248. t = add_tree(dirname)
  249. assert isinstance(basename, str)
  250. newtree = {}
  251. t[basename] = newtree
  252. trees[path] = newtree
  253. return newtree
  254. for path, sha, mode in blobs:
  255. tree_path, basename = pathsplit(path)
  256. tree = add_tree(tree_path)
  257. tree[basename] = (mode, sha)
  258. def build_tree(path):
  259. tree = Tree()
  260. for basename, entry in trees[path].iteritems():
  261. if type(entry) == dict:
  262. mode = stat.S_IFDIR
  263. sha = build_tree(pathjoin(path, basename))
  264. else:
  265. (mode, sha) = entry
  266. tree.add(basename, mode, sha)
  267. object_store.add_object(tree)
  268. return tree.id
  269. return build_tree("")
  270. def commit_index(object_store, index):
  271. """Create a new tree from an index.
  272. :param object_store: Object store to save the tree in
  273. :param index: Index file
  274. :note: This function is deprecated, use index.commit() instead.
  275. :return: Root tree sha.
  276. """
  277. return commit_tree(object_store, index.iterblobs())
  278. def changes_from_tree(names, lookup_entry, object_store, tree,
  279. want_unchanged=False):
  280. """Find the differences between the contents of a tree and
  281. a working copy.
  282. :param names: Iterable of names in the working copy
  283. :param lookup_entry: Function to lookup an entry in the working copy
  284. :param object_store: Object store to use for retrieving tree contents
  285. :param tree: SHA1 of the root tree
  286. :param want_unchanged: Whether unchanged files should be reported
  287. :return: Iterator over tuples with (oldpath, newpath), (oldmode, newmode),
  288. (oldsha, newsha)
  289. """
  290. other_names = set(names)
  291. for (name, mode, sha) in object_store.iter_tree_contents(tree):
  292. try:
  293. (other_sha, other_mode) = lookup_entry(name)
  294. except KeyError:
  295. # Was removed
  296. yield ((name, None), (mode, None), (sha, None))
  297. else:
  298. other_names.remove(name)
  299. if (want_unchanged or other_sha != sha or other_mode != mode):
  300. yield ((name, name), (mode, other_mode), (sha, other_sha))
  301. # Mention added files
  302. for name in other_names:
  303. (other_sha, other_mode) = lookup_entry(name)
  304. yield ((None, name), (None, other_mode), (None, other_sha))
  305. def index_entry_from_stat(stat_val, hex_sha, flags, mode=None):
  306. """Create a new index entry from a stat value.
  307. :param stat_val: POSIX stat_result instance
  308. :param hex_sha: Hex sha of the object
  309. :param flags: Index flags
  310. """
  311. if mode is None:
  312. mode = cleanup_mode(stat_val.st_mode)
  313. return (stat_val.st_ctime, stat_val.st_mtime, stat_val.st_dev,
  314. stat_val.st_ino, mode, stat_val.st_uid,
  315. stat_val.st_gid, stat_val.st_size, hex_sha, flags)
  316. def build_index_from_tree(prefix, index_path, object_store, tree_id,
  317. honor_filemode=True):
  318. """Generate and materialize index from a tree
  319. :param tree_id: Tree to materialize
  320. :param prefix: Target dir for materialized index files
  321. :param index_path: Target path for generated index
  322. :param object_store: Non-empty object store holding tree contents
  323. :param honor_filemode: An optional flag to honor core.filemode setting in
  324. config file, default is core.filemode=True, change executable bit
  325. :note:: existing index is wiped and contents are not merged
  326. in a working dir. Suiteable only for fresh clones.
  327. """
  328. index = Index(index_path)
  329. for entry in object_store.iter_tree_contents(tree_id):
  330. full_path = os.path.join(prefix, entry.path)
  331. if not os.path.exists(os.path.dirname(full_path)):
  332. os.makedirs(os.path.dirname(full_path))
  333. # FIXME: Merge new index into working tree
  334. if stat.S_ISLNK(entry.mode):
  335. # FIXME: This will fail on Windows. What should we do instead?
  336. if os.path.exists(full_path):
  337. # Must delete symlink dest to overwrite
  338. os.remove(full_path)
  339. os.symlink(object_store[entry.sha].as_raw_string(), full_path)
  340. else:
  341. f = open(full_path, 'wb')
  342. try:
  343. # Write out file
  344. f.write(object_store[entry.sha].as_raw_string())
  345. finally:
  346. f.close()
  347. if honor_filemode:
  348. os.chmod(full_path, entry.mode)
  349. # Add file to index
  350. st = os.lstat(full_path)
  351. index[entry.path] = index_entry_from_stat(st, entry.sha, 0)
  352. index.write()