You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

403 lines
15KB

  1. # -*- coding: utf-8 -*-
  2. # test_commit.py
  3. # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
  4. #
  5. # This module is part of GitPython and is released under
  6. # the BSD License: http://www.opensource.org/licenses/bsd-license.php
  7. from __future__ import print_function
  8. from datetime import datetime
  9. from io import BytesIO
  10. import re
  11. import sys
  12. import time
  13. from git import (
  14. Commit,
  15. Actor,
  16. )
  17. from git import Repo
  18. from git.compat import (
  19. string_types,
  20. text_type
  21. )
  22. from git.objects.util import tzoffset, utc
  23. from git.repo.fun import touch
  24. from git.test.lib import (
  25. TestBase,
  26. assert_equal,
  27. assert_not_equal,
  28. with_rw_repo,
  29. fixture_path,
  30. StringProcessAdapter
  31. )
  32. from git.test.lib import with_rw_directory
  33. from gitdb import IStream
  34. import os.path as osp
  35. try:
  36. from unittest.mock import Mock
  37. except ImportError:
  38. from mock import Mock
  39. def assert_commit_serialization(rwrepo, commit_id, print_performance_info=False):
  40. """traverse all commits in the history of commit identified by commit_id and check
  41. if the serialization works.
  42. :param print_performance_info: if True, we will show how fast we are"""
  43. ns = 0 # num serializations
  44. nds = 0 # num deserializations
  45. st = time.time()
  46. for cm in rwrepo.commit(commit_id).traverse():
  47. nds += 1
  48. # assert that we deserialize commits correctly, hence we get the same
  49. # sha on serialization
  50. stream = BytesIO()
  51. cm._serialize(stream)
  52. ns += 1
  53. streamlen = stream.tell()
  54. stream.seek(0)
  55. istream = rwrepo.odb.store(IStream(Commit.type, streamlen, stream))
  56. assert_equal(istream.hexsha, cm.hexsha.encode('ascii'))
  57. nc = Commit(rwrepo, Commit.NULL_BIN_SHA, cm.tree,
  58. cm.author, cm.authored_date, cm.author_tz_offset,
  59. cm.committer, cm.committed_date, cm.committer_tz_offset,
  60. cm.message, cm.parents, cm.encoding)
  61. assert_equal(nc.parents, cm.parents)
  62. stream = BytesIO()
  63. nc._serialize(stream)
  64. ns += 1
  65. streamlen = stream.tell()
  66. stream.seek(0)
  67. # reuse istream
  68. istream.size = streamlen
  69. istream.stream = stream
  70. istream.binsha = None
  71. nc.binsha = rwrepo.odb.store(istream).binsha
  72. # if it worked, we have exactly the same contents !
  73. assert_equal(nc.hexsha, cm.hexsha)
  74. # END check commits
  75. elapsed = time.time() - st
  76. if print_performance_info:
  77. print("Serialized %i and deserialized %i commits in %f s ( (%f, %f) commits / s"
  78. % (ns, nds, elapsed, ns / elapsed, nds / elapsed), file=sys.stderr)
  79. # END handle performance info
  80. class TestCommit(TestBase):
  81. def test_bake(self):
  82. commit = self.rorepo.commit('2454ae89983a4496a445ce347d7a41c0bb0ea7ae')
  83. # commits have no dict
  84. self.failUnlessRaises(AttributeError, setattr, commit, 'someattr', 1)
  85. commit.author # bake
  86. assert_equal("Sebastian Thiel", commit.author.name)
  87. assert_equal("byronimo@gmail.com", commit.author.email)
  88. self.assertEqual(commit.author, commit.committer)
  89. assert isinstance(commit.authored_date, int) and isinstance(commit.committed_date, int)
  90. assert isinstance(commit.author_tz_offset, int) and isinstance(commit.committer_tz_offset, int)
  91. self.assertEqual(commit.message, "Added missing information to docstrings of commit and stats module\n")
  92. def test_stats(self):
  93. commit = self.rorepo.commit('33ebe7acec14b25c5f84f35a664803fcab2f7781')
  94. stats = commit.stats
  95. def check_entries(d):
  96. assert isinstance(d, dict)
  97. for key in ("insertions", "deletions", "lines"):
  98. assert key in d
  99. # END assertion helper
  100. assert stats.files
  101. assert stats.total
  102. check_entries(stats.total)
  103. assert "files" in stats.total
  104. for filepath, d in stats.files.items(): # @UnusedVariable
  105. check_entries(d)
  106. # END for each stated file
  107. # assure data is parsed properly
  108. michael = Actor._from_string("Michael Trier <mtrier@gmail.com>")
  109. self.assertEqual(commit.author, michael)
  110. self.assertEqual(commit.committer, michael)
  111. self.assertEqual(commit.authored_date, 1210193388)
  112. self.assertEqual(commit.committed_date, 1210193388)
  113. self.assertEqual(commit.author_tz_offset, 14400, commit.author_tz_offset)
  114. self.assertEqual(commit.committer_tz_offset, 14400, commit.committer_tz_offset)
  115. self.assertEqual(commit.message, "initial project\n")
  116. def test_unicode_actor(self):
  117. # assure we can parse unicode actors correctly
  118. name = u"Üäöß ÄußÉ"
  119. self.assertEqual(len(name), 9)
  120. special = Actor._from_string(u"%s <something@this.com>" % name)
  121. self.assertEqual(special.name, name)
  122. assert isinstance(special.name, text_type)
  123. def test_traversal(self):
  124. start = self.rorepo.commit("a4d06724202afccd2b5c54f81bcf2bf26dea7fff")
  125. first = self.rorepo.commit("33ebe7acec14b25c5f84f35a664803fcab2f7781")
  126. p0 = start.parents[0]
  127. p1 = start.parents[1]
  128. p00 = p0.parents[0]
  129. p10 = p1.parents[0]
  130. # basic branch first, depth first
  131. dfirst = start.traverse(branch_first=False)
  132. bfirst = start.traverse(branch_first=True)
  133. self.assertEqual(next(dfirst), p0)
  134. self.assertEqual(next(dfirst), p00)
  135. self.assertEqual(next(bfirst), p0)
  136. self.assertEqual(next(bfirst), p1)
  137. self.assertEqual(next(bfirst), p00)
  138. self.assertEqual(next(bfirst), p10)
  139. # at some point, both iterations should stop
  140. self.assertEqual(list(bfirst)[-1], first)
  141. stoptraverse = self.rorepo.commit("254d04aa3180eb8b8daf7b7ff25f010cd69b4e7d").traverse(as_edge=True)
  142. self.assertEqual(len(next(stoptraverse)), 2)
  143. # ignore self
  144. self.assertEqual(next(start.traverse(ignore_self=False)), start)
  145. # depth
  146. self.assertEqual(len(list(start.traverse(ignore_self=False, depth=0))), 1)
  147. # prune
  148. self.assertEqual(next(start.traverse(branch_first=1, prune=lambda i, d: i == p0)), p1)
  149. # predicate
  150. self.assertEqual(next(start.traverse(branch_first=1, predicate=lambda i, d: i == p1)), p1)
  151. # traversal should stop when the beginning is reached
  152. self.failUnlessRaises(StopIteration, next, first.traverse())
  153. # parents of the first commit should be empty ( as the only parent has a null
  154. # sha )
  155. self.assertEqual(len(first.parents), 0)
  156. def test_iteration(self):
  157. # we can iterate commits
  158. all_commits = Commit.list_items(self.rorepo, self.rorepo.head)
  159. assert all_commits
  160. self.assertEqual(all_commits, list(self.rorepo.iter_commits()))
  161. # this includes merge commits
  162. mcomit = self.rorepo.commit('d884adc80c80300b4cc05321494713904ef1df2d')
  163. assert mcomit in all_commits
  164. # we can limit the result to paths
  165. ltd_commits = list(self.rorepo.iter_commits(paths='CHANGES'))
  166. assert ltd_commits and len(ltd_commits) < len(all_commits)
  167. # show commits of multiple paths, resulting in a union of commits
  168. less_ltd_commits = list(Commit.iter_items(self.rorepo, 'master', paths=('CHANGES', 'AUTHORS')))
  169. assert len(ltd_commits) < len(less_ltd_commits)
  170. def test_iter_items(self):
  171. # pretty not allowed
  172. self.failUnlessRaises(ValueError, Commit.iter_items, self.rorepo, 'master', pretty="raw")
  173. def test_rev_list_bisect_all(self):
  174. """
  175. 'git rev-list --bisect-all' returns additional information
  176. in the commit header. This test ensures that we properly parse it.
  177. """
  178. revs = self.rorepo.git.rev_list('933d23bf95a5bd1624fbcdf328d904e1fa173474',
  179. first_parent=True,
  180. bisect_all=True)
  181. commits = Commit._iter_from_process_or_stream(self.rorepo, StringProcessAdapter(revs.encode('ascii')))
  182. expected_ids = (
  183. '7156cece3c49544abb6bf7a0c218eb36646fad6d',
  184. '1f66cfbbce58b4b552b041707a12d437cc5f400a',
  185. '33ebe7acec14b25c5f84f35a664803fcab2f7781',
  186. '933d23bf95a5bd1624fbcdf328d904e1fa173474'
  187. )
  188. for sha1, commit in zip(expected_ids, commits):
  189. assert_equal(sha1, commit.hexsha)
  190. @with_rw_directory
  191. def test_ambiguous_arg_iteration(self, rw_dir):
  192. rw_repo = Repo.init(osp.join(rw_dir, 'test_ambiguous_arg'))
  193. path = osp.join(rw_repo.working_tree_dir, 'master')
  194. touch(path)
  195. rw_repo.index.add([path])
  196. rw_repo.index.commit('initial commit')
  197. list(rw_repo.iter_commits(rw_repo.head.ref)) # should fail unless bug is fixed
  198. def test_count(self):
  199. self.assertEqual(self.rorepo.tag('refs/tags/0.1.5').commit.count(), 143)
  200. def test_list(self):
  201. # This doesn't work anymore, as we will either attempt getattr with bytes, or compare 20 byte string
  202. # with actual 20 byte bytes. This usage makes no sense anyway
  203. assert isinstance(Commit.list_items(self.rorepo, '0.1.5', max_count=5)[
  204. '5117c9c8a4d3af19a9958677e45cda9269de1541'], Commit)
  205. def test_str(self):
  206. commit = Commit(self.rorepo, Commit.NULL_BIN_SHA)
  207. assert_equal(Commit.NULL_HEX_SHA, str(commit))
  208. def test_repr(self):
  209. commit = Commit(self.rorepo, Commit.NULL_BIN_SHA)
  210. assert_equal('<git.Commit "%s">' % Commit.NULL_HEX_SHA, repr(commit))
  211. def test_equality(self):
  212. commit1 = Commit(self.rorepo, Commit.NULL_BIN_SHA)
  213. commit2 = Commit(self.rorepo, Commit.NULL_BIN_SHA)
  214. commit3 = Commit(self.rorepo, "\1" * 20)
  215. assert_equal(commit1, commit2)
  216. assert_not_equal(commit2, commit3)
  217. def test_iter_parents(self):
  218. # should return all but ourselves, even if skip is defined
  219. c = self.rorepo.commit('0.1.5')
  220. for skip in (0, 1):
  221. piter = c.iter_parents(skip=skip)
  222. first_parent = next(piter)
  223. assert first_parent != c
  224. self.assertEqual(first_parent, c.parents[0])
  225. # END for each
  226. def test_name_rev(self):
  227. name_rev = self.rorepo.head.commit.name_rev
  228. assert isinstance(name_rev, string_types)
  229. @with_rw_repo('HEAD', bare=True)
  230. def test_serialization(self, rwrepo):
  231. # create all commits of our repo
  232. assert_commit_serialization(rwrepo, '0.1.6')
  233. def test_serialization_unicode_support(self):
  234. self.assertEqual(Commit.default_encoding.lower(), 'utf-8')
  235. # create a commit with unicode in the message, and the author's name
  236. # Verify its serialization and deserialization
  237. cmt = self.rorepo.commit('0.1.6')
  238. assert isinstance(cmt.message, text_type) # it automatically decodes it as such
  239. assert isinstance(cmt.author.name, text_type) # same here
  240. cmt.message = u"üäêèß"
  241. self.assertEqual(len(cmt.message), 5)
  242. cmt.author.name = u"äüß"
  243. self.assertEqual(len(cmt.author.name), 3)
  244. cstream = BytesIO()
  245. cmt._serialize(cstream)
  246. cstream.seek(0)
  247. assert len(cstream.getvalue())
  248. ncmt = Commit(self.rorepo, cmt.binsha)
  249. ncmt._deserialize(cstream)
  250. self.assertEqual(cmt.author.name, ncmt.author.name)
  251. self.assertEqual(cmt.message, ncmt.message)
  252. # actually, it can't be printed in a shell as repr wants to have ascii only
  253. # it appears
  254. cmt.author.__repr__()
  255. def test_invalid_commit(self):
  256. cmt = self.rorepo.commit()
  257. with open(fixture_path('commit_invalid_data'), 'rb') as fd:
  258. cmt._deserialize(fd)
  259. self.assertEqual(cmt.author.name, u'E.Azer Ko�o�o�oculu', cmt.author.name)
  260. self.assertEqual(cmt.author.email, 'azer@kodfabrik.com', cmt.author.email)
  261. def test_gpgsig(self):
  262. cmt = self.rorepo.commit()
  263. with open(fixture_path('commit_with_gpgsig'), 'rb') as fd:
  264. cmt._deserialize(fd)
  265. fixture_sig = """-----BEGIN PGP SIGNATURE-----
  266. Version: GnuPG v1.4.11 (GNU/Linux)
  267. iQIcBAABAgAGBQJRk8zMAAoJEG5mS6x6i9IjsTEP/0v2Wx/i7dqyKban6XMIhVdj
  268. uI0DycfXqnCCZmejidzeao+P+cuK/ZAA/b9fU4MtwkDm2USvnIOrB00W0isxsrED
  269. sdv6uJNa2ybGjxBolLrfQcWutxGXLZ1FGRhEvkPTLMHHvVriKoNFXcS7ewxP9MBf
  270. NH97K2wauqA+J4BDLDHQJgADCOmLrGTAU+G1eAXHIschDqa6PZMH5nInetYZONDh
  271. 3SkOOv8VKFIF7gu8X7HC+7+Y8k8U0TW0cjlQ2icinwCc+KFoG6GwXS7u/VqIo1Yp
  272. Tack6sxIdK7NXJhV5gAeAOMJBGhO0fHl8UUr96vGEKwtxyZhWf8cuIPOWLk06jA0
  273. g9DpLqmy/pvyRfiPci+24YdYRBua/vta+yo/Lp85N7Hu/cpIh+q5WSLvUlv09Dmo
  274. TTTG8Hf6s3lEej7W8z2xcNZoB6GwXd8buSDU8cu0I6mEO9sNtAuUOHp2dBvTA6cX
  275. PuQW8jg3zofnx7CyNcd3KF3nh2z8mBcDLgh0Q84srZJCPRuxRcp9ylggvAG7iaNd
  276. XMNvSK8IZtWLkx7k3A3QYt1cN4y1zdSHLR2S+BVCEJea1mvUE+jK5wiB9S4XNtKm
  277. BX/otlTa8pNE3fWYBxURvfHnMY4i3HQT7Bc1QjImAhMnyo2vJk4ORBJIZ1FTNIhJ
  278. JzJMZDRLQLFvnzqZuCjE
  279. =przd
  280. -----END PGP SIGNATURE-----"""
  281. self.assertEqual(cmt.gpgsig, fixture_sig)
  282. cmt.gpgsig = "<test\ndummy\nsig>"
  283. assert cmt.gpgsig != fixture_sig
  284. cstream = BytesIO()
  285. cmt._serialize(cstream)
  286. assert re.search(r"^gpgsig <test\n dummy\n sig>$", cstream.getvalue().decode('ascii'), re.MULTILINE)
  287. self.assert_gpgsig_deserialization(cstream)
  288. cstream.seek(0)
  289. cmt.gpgsig = None
  290. cmt._deserialize(cstream)
  291. self.assertEqual(cmt.gpgsig, "<test\ndummy\nsig>")
  292. cmt.gpgsig = None
  293. cstream = BytesIO()
  294. cmt._serialize(cstream)
  295. assert not re.search(r"^gpgsig ", cstream.getvalue().decode('ascii'), re.MULTILINE)
  296. def assert_gpgsig_deserialization(self, cstream):
  297. assert 'gpgsig' in 'precondition: need gpgsig'
  298. class RepoMock:
  299. def __init__(self, bytestr):
  300. self.bytestr = bytestr
  301. @property
  302. def odb(self):
  303. class ODBMock:
  304. def __init__(self, bytestr):
  305. self.bytestr = bytestr
  306. def stream(self, *args):
  307. stream = Mock(spec_set=['read'], return_value=self.bytestr)
  308. stream.read.return_value = self.bytestr
  309. return ('binsha', 'typename', 'size', stream)
  310. return ODBMock(self.bytestr)
  311. repo_mock = RepoMock(cstream.getvalue())
  312. for field in Commit.__slots__:
  313. c = Commit(repo_mock, b'x' * 20)
  314. assert getattr(c, field) is not None
  315. def test_datetimes(self):
  316. commit = self.rorepo.commit('4251bd5')
  317. self.assertEqual(commit.authored_date, 1255018625)
  318. self.assertEqual(commit.committed_date, 1255026171)
  319. self.assertEqual(commit.authored_datetime,
  320. datetime(2009, 10, 8, 18, 17, 5, tzinfo=tzoffset(-7200)), commit.authored_datetime) # noqa
  321. self.assertEqual(commit.authored_datetime,
  322. datetime(2009, 10, 8, 16, 17, 5, tzinfo=utc), commit.authored_datetime)
  323. self.assertEqual(commit.committed_datetime,
  324. datetime(2009, 10, 8, 20, 22, 51, tzinfo=tzoffset(-7200)))
  325. self.assertEqual(commit.committed_datetime,
  326. datetime(2009, 10, 8, 18, 22, 51, tzinfo=utc), commit.committed_datetime)