objects.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732
  1. # objects.py -- Access to base git objects
  2. # Copyright (C) 2007 James Westby <jw+debian@jameswestby.net>
  3. # Copyright (C) 2008-2009 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 or (at your option) a later version 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. """Access to base git objects."""
  20. import binascii
  21. from cStringIO import (
  22. StringIO,
  23. )
  24. import mmap
  25. import os
  26. import stat
  27. import time
  28. import zlib
  29. from dulwich.errors import (
  30. NotBlobError,
  31. NotCommitError,
  32. NotTreeError,
  33. )
  34. from dulwich.file import GitFile
  35. from dulwich.misc import (
  36. make_sha,
  37. )
  38. # Header fields for commits
  39. _TREE_HEADER = "tree"
  40. _PARENT_HEADER = "parent"
  41. _AUTHOR_HEADER = "author"
  42. _COMMITTER_HEADER = "committer"
  43. _ENCODING_HEADER = "encoding"
  44. # Header fields for objects
  45. _OBJECT_HEADER = "object"
  46. _TYPE_HEADER = "type"
  47. _TAG_HEADER = "tag"
  48. _TAGGER_HEADER = "tagger"
  49. S_IFGITLINK = 0160000
  50. def S_ISGITLINK(m):
  51. return (stat.S_IFMT(m) == S_IFGITLINK)
  52. def _decompress(string):
  53. dcomp = zlib.decompressobj()
  54. dcomped = dcomp.decompress(string)
  55. dcomped += dcomp.flush()
  56. return dcomped
  57. def sha_to_hex(sha):
  58. """Takes a string and returns the hex of the sha within"""
  59. hexsha = binascii.hexlify(sha)
  60. assert len(hexsha) == 40, "Incorrect length of sha1 string: %d" % hexsha
  61. return hexsha
  62. def hex_to_sha(hex):
  63. """Takes a hex sha and returns a binary sha"""
  64. assert len(hex) == 40, "Incorrent length of hexsha: %s" % hex
  65. return binascii.unhexlify(hex)
  66. def serializable_property(name, docstring=None):
  67. def set(obj, value):
  68. obj._ensure_parsed()
  69. setattr(obj, "_"+name, value)
  70. obj._needs_serialization = True
  71. def get(obj):
  72. obj._ensure_parsed()
  73. return getattr(obj, "_"+name)
  74. return property(get, set, doc=docstring)
  75. def object_class(type):
  76. """Get the object class corresponding to the given type.
  77. :param type: Either a type name string or a numeric type.
  78. :return: The ShaFile subclass corresponding to the given type.
  79. """
  80. return _TYPE_MAP[type]
  81. class ShaFile(object):
  82. """A git SHA file."""
  83. @classmethod
  84. def _parse_legacy_object(cls, map):
  85. """Parse a legacy object, creating it and setting object._text"""
  86. text = _decompress(map)
  87. object = None
  88. for cls in OBJECT_CLASSES:
  89. if text.startswith(cls.type_name):
  90. object = cls()
  91. text = text[len(cls.type_name):]
  92. break
  93. assert object is not None, "%s is not a known object type" % text[:9]
  94. assert text[0] == ' ', "%s is not a space" % text[0]
  95. text = text[1:]
  96. size = 0
  97. i = 0
  98. while text[0] >= '0' and text[0] <= '9':
  99. if i > 0 and size == 0:
  100. raise AssertionError("Size is not in canonical format")
  101. size = (size * 10) + int(text[0])
  102. text = text[1:]
  103. i += 1
  104. object._size = size
  105. assert text[0] == "\0", "Size not followed by null"
  106. text = text[1:]
  107. object.set_raw_string(text)
  108. return object
  109. def as_legacy_object_chunks(self):
  110. compobj = zlib.compressobj()
  111. yield compobj.compress(self._header())
  112. for chunk in self.as_raw_chunks():
  113. yield compobj.compress(chunk)
  114. yield compobj.flush()
  115. def as_legacy_object(self):
  116. return "".join(self.as_legacy_object_chunks())
  117. def as_raw_chunks(self):
  118. if self._needs_serialization:
  119. self._chunked_text = self._serialize()
  120. self._needs_serialization = False
  121. return self._chunked_text
  122. def as_raw_string(self):
  123. return "".join(self.as_raw_chunks())
  124. def __str__(self):
  125. return self.as_raw_string()
  126. def __hash__(self):
  127. return hash(self.id)
  128. def as_pretty_string(self):
  129. return self.as_raw_string()
  130. def _ensure_parsed(self):
  131. if self._needs_parsing:
  132. self._deserialize(self._chunked_text)
  133. self._needs_parsing = False
  134. def set_raw_string(self, text):
  135. if type(text) != str:
  136. raise TypeError(text)
  137. self.set_raw_chunks([text])
  138. def set_raw_chunks(self, chunks):
  139. self._chunked_text = chunks
  140. self._sha = None
  141. self._needs_parsing = True
  142. self._needs_serialization = False
  143. @classmethod
  144. def _parse_object(cls, map):
  145. """Parse a new style object , creating it and setting object._text"""
  146. used = 0
  147. byte = ord(map[used])
  148. used += 1
  149. type_num = (byte >> 4) & 7
  150. try:
  151. object = object_class(type_num)()
  152. except KeyError:
  153. raise AssertionError("Not a known type: %d" % type_num)
  154. while (byte & 0x80) != 0:
  155. byte = ord(map[used])
  156. used += 1
  157. raw = map[used:]
  158. object.set_raw_string(_decompress(raw))
  159. return object
  160. @classmethod
  161. def _parse_file(cls, map):
  162. word = (ord(map[0]) << 8) + ord(map[1])
  163. if ord(map[0]) == 0x78 and (word % 31) == 0:
  164. return cls._parse_legacy_object(map)
  165. else:
  166. return cls._parse_object(map)
  167. def __init__(self):
  168. """Don't call this directly"""
  169. self._sha = None
  170. def _deserialize(self, chunks):
  171. raise NotImplementedError(self._deserialize)
  172. def _serialize(self):
  173. raise NotImplementedError(self._serialize)
  174. @classmethod
  175. def from_file(cls, filename):
  176. """Get the contents of a SHA file on disk"""
  177. size = os.path.getsize(filename)
  178. f = GitFile(filename, 'rb')
  179. try:
  180. map = mmap.mmap(f.fileno(), size, access=mmap.ACCESS_READ)
  181. shafile = cls._parse_file(map)
  182. return shafile
  183. finally:
  184. f.close()
  185. @staticmethod
  186. def from_raw_string(type_num, string):
  187. """Creates an object of the indicated type from the raw string given.
  188. :param type_num: The numeric type of the object.
  189. :param string: The raw uncompressed contents.
  190. """
  191. obj = object_class(type_num)()
  192. obj.set_raw_string(string)
  193. return obj
  194. @staticmethod
  195. def from_raw_chunks(type_num, chunks):
  196. """Creates an object of the indicated type from the raw chunks given.
  197. :param type_num: The numeric type of the object.
  198. :param chunks: An iterable of the raw uncompressed contents.
  199. """
  200. obj = object_class(type_num)()
  201. obj.set_raw_chunks(chunks)
  202. return obj
  203. @classmethod
  204. def from_string(cls, string):
  205. """Create a blob from a string."""
  206. obj = cls()
  207. obj.set_raw_string(string)
  208. return obj
  209. def _header(self):
  210. return "%s %lu\0" % (self.type_name, self.raw_length())
  211. def raw_length(self):
  212. """Returns the length of the raw string of this object."""
  213. ret = 0
  214. for chunk in self.as_raw_chunks():
  215. ret += len(chunk)
  216. return ret
  217. def _make_sha(self):
  218. ret = make_sha()
  219. ret.update(self._header())
  220. for chunk in self.as_raw_chunks():
  221. ret.update(chunk)
  222. return ret
  223. def sha(self):
  224. """The SHA1 object that is the name of this object."""
  225. if self._needs_serialization or self._sha is None:
  226. self._sha = self._make_sha()
  227. return self._sha
  228. @property
  229. def id(self):
  230. return self.sha().hexdigest()
  231. def get_type(self):
  232. return self.type_num
  233. def set_type(self, type):
  234. self.type_num = type
  235. # DEPRECATED: use type_num or type_name as needed.
  236. type = property(get_type, set_type)
  237. def __repr__(self):
  238. return "<%s %s>" % (self.__class__.__name__, self.id)
  239. def __ne__(self, other):
  240. return self.id != other.id
  241. def __eq__(self, other):
  242. """Return true if the sha of the two objects match.
  243. The __le__ etc methods aren't overriden as they make no sense,
  244. certainly at this level.
  245. """
  246. return self.id == other.id
  247. class Blob(ShaFile):
  248. """A Git Blob object."""
  249. type_name = 'blob'
  250. type_num = 3
  251. def __init__(self):
  252. super(Blob, self).__init__()
  253. self._chunked_text = []
  254. self._needs_parsing = False
  255. self._needs_serialization = False
  256. def _get_data(self):
  257. return self.as_raw_string()
  258. def _set_data(self, data):
  259. self.set_raw_string(data)
  260. data = property(_get_data, _set_data,
  261. "The text contained within the blob object.")
  262. def _get_chunked(self):
  263. return self._chunked_text
  264. def _set_chunked(self, chunks):
  265. self._chunked_text = chunks
  266. chunked = property(_get_chunked, _set_chunked,
  267. "The text within the blob object, as chunks (not necessarily lines).")
  268. @classmethod
  269. def from_file(cls, filename):
  270. blob = ShaFile.from_file(filename)
  271. if not isinstance(blob, cls):
  272. raise NotBlobError(filename)
  273. return blob
  274. class Tag(ShaFile):
  275. """A Git Tag object."""
  276. type_name = 'tag'
  277. type_num = 4
  278. def __init__(self):
  279. super(Tag, self).__init__()
  280. self._needs_parsing = False
  281. self._needs_serialization = True
  282. @classmethod
  283. def from_file(cls, filename):
  284. tag = ShaFile.from_file(filename)
  285. if not isinstance(tag, cls):
  286. raise NotTagError(filename)
  287. return tag
  288. @classmethod
  289. def from_string(cls, string):
  290. """Create a blob from a string."""
  291. shafile = cls()
  292. shafile.set_raw_string(string)
  293. return shafile
  294. def _serialize(self):
  295. chunks = []
  296. chunks.append("%s %s\n" % (_OBJECT_HEADER, self._object_sha))
  297. chunks.append("%s %s\n" % (_TYPE_HEADER, self._object_class.type_name))
  298. chunks.append("%s %s\n" % (_TAG_HEADER, self._name))
  299. if self._tagger:
  300. if self._tag_time is None:
  301. chunks.append("%s %s\n" % (_TAGGER_HEADER, self._tagger))
  302. else:
  303. chunks.append("%s %s %d %s\n" % (
  304. _TAGGER_HEADER, self._tagger, self._tag_time,
  305. format_timezone(self._tag_timezone)))
  306. chunks.append("\n") # To close headers
  307. chunks.append(self._message)
  308. return chunks
  309. def _deserialize(self, chunks):
  310. """Grab the metadata attached to the tag"""
  311. self._tagger = None
  312. f = StringIO("".join(chunks))
  313. for l in f:
  314. l = l.rstrip("\n")
  315. if l == "":
  316. break # empty line indicates end of headers
  317. (field, value) = l.split(" ", 1)
  318. if field == _OBJECT_HEADER:
  319. self._object_sha = value
  320. elif field == _TYPE_HEADER:
  321. self._object_class = object_class(value)
  322. elif field == _TAG_HEADER:
  323. self._name = value
  324. elif field == _TAGGER_HEADER:
  325. try:
  326. sep = value.index("> ")
  327. except ValueError:
  328. self._tagger = value
  329. self._tag_time = None
  330. self._tag_timezone = None
  331. else:
  332. self._tagger = value[0:sep+1]
  333. (timetext, timezonetext) = value[sep+2:].rsplit(" ", 1)
  334. try:
  335. self._tag_time = int(timetext)
  336. except ValueError: #Not a unix timestamp
  337. self._tag_time = time.strptime(timetext)
  338. self._tag_timezone = parse_timezone(timezonetext)
  339. else:
  340. raise AssertionError("Unknown field %s" % field)
  341. self._message = f.read()
  342. def _get_object(self):
  343. """Get the object pointed to by this tag.
  344. :return: tuple of (object class, sha).
  345. """
  346. self._ensure_parsed()
  347. return (self._object_class, self._object_sha)
  348. def _set_object(self, value):
  349. self._ensure_parsed()
  350. (self._object_class, self._object_sha) = value
  351. self._needs_serialization = True
  352. object = property(_get_object, _set_object)
  353. name = serializable_property("name", "The name of this tag")
  354. tagger = serializable_property("tagger",
  355. "Returns the name of the person who created this tag")
  356. tag_time = serializable_property("tag_time",
  357. "The creation timestamp of the tag. As the number of seconds since the epoch")
  358. tag_timezone = serializable_property("tag_timezone",
  359. "The timezone that tag_time is in.")
  360. message = serializable_property("message", "The message attached to this tag")
  361. def parse_tree(text):
  362. """Parse a tree text.
  363. :param text: Serialized text to parse
  364. :return: Dictionary with names as keys, (mode, sha) tuples as values
  365. """
  366. ret = {}
  367. count = 0
  368. l = len(text)
  369. while count < l:
  370. mode_end = text.index(' ', count)
  371. mode = int(text[count:mode_end], 8)
  372. name_end = text.index('\0', mode_end)
  373. name = text[mode_end+1:name_end]
  374. count = name_end+21
  375. sha = text[name_end+1:count]
  376. ret[name] = (mode, sha_to_hex(sha))
  377. return ret
  378. def serialize_tree(items):
  379. """Serialize the items in a tree to a text.
  380. :param items: Sorted iterable over (name, mode, sha) tuples
  381. :return: Serialized tree text as chunks
  382. """
  383. for name, mode, hexsha in items:
  384. yield "%04o %s\0%s" % (mode, name, hex_to_sha(hexsha))
  385. def sorted_tree_items(entries):
  386. """Iterate over a tree entries dictionary in the order in which
  387. the items would be serialized.
  388. :param entries: Dictionary mapping names to (mode, sha) tuples
  389. :return: Iterator over (name, mode, sha)
  390. """
  391. def cmp_entry((name1, value1), (name2, value2)):
  392. if stat.S_ISDIR(value1[0]):
  393. name1 += "/"
  394. if stat.S_ISDIR(value2[0]):
  395. name2 += "/"
  396. return cmp(name1, name2)
  397. for name, entry in sorted(entries.iteritems(), cmp=cmp_entry):
  398. yield name, entry[0], entry[1]
  399. class Tree(ShaFile):
  400. """A Git tree object"""
  401. type_name = 'tree'
  402. type_num = 2
  403. def __init__(self):
  404. super(Tree, self).__init__()
  405. self._entries = {}
  406. self._needs_parsing = False
  407. self._needs_serialization = True
  408. @classmethod
  409. def from_file(cls, filename):
  410. tree = ShaFile.from_file(filename)
  411. if not isinstance(tree, cls):
  412. raise NotTreeError(filename)
  413. return tree
  414. def __contains__(self, name):
  415. self._ensure_parsed()
  416. return name in self._entries
  417. def __getitem__(self, name):
  418. self._ensure_parsed()
  419. return self._entries[name]
  420. def __setitem__(self, name, value):
  421. assert isinstance(value, tuple)
  422. assert len(value) == 2
  423. self._ensure_parsed()
  424. self._entries[name] = value
  425. self._needs_serialization = True
  426. def __delitem__(self, name):
  427. self._ensure_parsed()
  428. del self._entries[name]
  429. self._needs_serialization = True
  430. def __len__(self):
  431. self._ensure_parsed()
  432. return len(self._entries)
  433. def add(self, mode, name, hexsha):
  434. assert type(mode) == int
  435. assert type(name) == str
  436. assert type(hexsha) == str
  437. self._ensure_parsed()
  438. self._entries[name] = mode, hexsha
  439. self._needs_serialization = True
  440. def entries(self):
  441. """Return a list of tuples describing the tree entries"""
  442. self._ensure_parsed()
  443. # The order of this is different from iteritems() for historical
  444. # reasons
  445. return [
  446. (mode, name, hexsha) for (name, mode, hexsha) in self.iteritems()]
  447. def iteritems(self):
  448. """Iterate over all entries in the order in which they would be
  449. serialized.
  450. :return: Iterator over (name, mode, sha) tuples
  451. """
  452. self._ensure_parsed()
  453. return sorted_tree_items(self._entries)
  454. def _deserialize(self, chunks):
  455. """Grab the entries in the tree"""
  456. self._entries = parse_tree("".join(chunks))
  457. def _serialize(self):
  458. return list(serialize_tree(self.iteritems()))
  459. def as_pretty_string(self):
  460. text = []
  461. for name, mode, hexsha in self.iteritems():
  462. if mode & stat.S_IFDIR:
  463. kind = "tree"
  464. else:
  465. kind = "blob"
  466. text.append("%04o %s %s\t%s\n" % (mode, kind, hexsha, name))
  467. return "".join(text)
  468. def parse_timezone(text):
  469. offset = int(text)
  470. signum = (offset < 0) and -1 or 1
  471. offset = abs(offset)
  472. hours = int(offset / 100)
  473. minutes = (offset % 100)
  474. return signum * (hours * 3600 + minutes * 60)
  475. def format_timezone(offset):
  476. if offset % 60 != 0:
  477. raise ValueError("Unable to handle non-minute offset.")
  478. sign = (offset < 0) and '-' or '+'
  479. offset = abs(offset)
  480. return '%c%02d%02d' % (sign, offset / 3600, (offset / 60) % 60)
  481. class Commit(ShaFile):
  482. """A git commit object"""
  483. type_name = 'commit'
  484. type_num = 1
  485. def __init__(self):
  486. super(Commit, self).__init__()
  487. self._parents = []
  488. self._encoding = None
  489. self._needs_parsing = False
  490. self._needs_serialization = True
  491. self._extra = {}
  492. @classmethod
  493. def from_file(cls, filename):
  494. commit = ShaFile.from_file(filename)
  495. if not isinstance(commit, cls):
  496. raise NotCommitError(filename)
  497. return commit
  498. def _deserialize(self, chunks):
  499. self._parents = []
  500. self._extra = []
  501. self._author = None
  502. f = StringIO("".join(chunks))
  503. for l in f:
  504. l = l.rstrip("\n")
  505. if l == "":
  506. # Empty line indicates end of headers
  507. break
  508. (field, value) = l.split(" ", 1)
  509. if field == _TREE_HEADER:
  510. self._tree = value
  511. elif field == _PARENT_HEADER:
  512. self._parents.append(value)
  513. elif field == _AUTHOR_HEADER:
  514. self._author, timetext, timezonetext = value.rsplit(" ", 2)
  515. self._author_time = int(timetext)
  516. self._author_timezone = parse_timezone(timezonetext)
  517. elif field == _COMMITTER_HEADER:
  518. self._committer, timetext, timezonetext = value.rsplit(" ", 2)
  519. self._commit_time = int(timetext)
  520. self._commit_timezone = parse_timezone(timezonetext)
  521. elif field == _ENCODING_HEADER:
  522. self._encoding = value
  523. else:
  524. self._extra.append((field, value))
  525. self._message = f.read()
  526. def _serialize(self):
  527. chunks = []
  528. chunks.append("%s %s\n" % (_TREE_HEADER, self._tree))
  529. for p in self._parents:
  530. chunks.append("%s %s\n" % (_PARENT_HEADER, p))
  531. chunks.append("%s %s %s %s\n" % (
  532. _AUTHOR_HEADER, self._author, str(self._author_time),
  533. format_timezone(self._author_timezone)))
  534. chunks.append("%s %s %s %s\n" % (
  535. _COMMITTER_HEADER, self._committer, str(self._commit_time),
  536. format_timezone(self._commit_timezone)))
  537. if self.encoding:
  538. chunks.append("%s %s\n" % (_ENCODING_HEADER, self.encoding))
  539. for k, v in self.extra:
  540. if "\n" in k or "\n" in v:
  541. raise AssertionError("newline in extra data: %r -> %r" % (k, v))
  542. chunks.append("%s %s\n" % (k, v))
  543. chunks.append("\n") # There must be a new line after the headers
  544. chunks.append(self._message)
  545. return chunks
  546. tree = serializable_property("tree", "Tree that is the state of this commit")
  547. def _get_parents(self):
  548. """Return a list of parents of this commit."""
  549. self._ensure_parsed()
  550. return self._parents
  551. def _set_parents(self, value):
  552. """Set a list of parents of this commit."""
  553. self._ensure_parsed()
  554. self._needs_serialization = True
  555. self._parents = value
  556. parents = property(_get_parents, _set_parents)
  557. def _get_extra(self):
  558. """Return extra settings of this commit."""
  559. self._ensure_parsed()
  560. return self._extra
  561. extra = property(_get_extra)
  562. author = serializable_property("author",
  563. "The name of the author of the commit")
  564. committer = serializable_property("committer",
  565. "The name of the committer of the commit")
  566. message = serializable_property("message",
  567. "The commit message")
  568. commit_time = serializable_property("commit_time",
  569. "The timestamp of the commit. As the number of seconds since the epoch.")
  570. commit_timezone = serializable_property("commit_timezone",
  571. "The zone the commit time is in")
  572. author_time = serializable_property("author_time",
  573. "The timestamp the commit was written. as the number of seconds since the epoch.")
  574. author_timezone = serializable_property("author_timezone",
  575. "Returns the zone the author time is in.")
  576. encoding = serializable_property("encoding",
  577. "Encoding of the commit message.")
  578. OBJECT_CLASSES = (
  579. Commit,
  580. Tree,
  581. Blob,
  582. Tag,
  583. )
  584. _TYPE_MAP = {}
  585. for cls in OBJECT_CLASSES:
  586. _TYPE_MAP[cls.type_name] = cls
  587. _TYPE_MAP[cls.type_num] = cls
  588. try:
  589. # Try to import C versions
  590. from dulwich._objects import parse_tree, sorted_tree_items
  591. except ImportError:
  592. pass