test_repository.py 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929
  1. # -*- coding: utf-8 -*-
  2. # test_repository.py -- tests for repository.py
  3. # Copyright (C) 2007 James Westby <jw+debian@jameswestby.net>
  4. #
  5. # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
  6. # General Public License as public by the Free Software Foundation; version 2.0
  7. # or (at your option) any later version. You can redistribute it and/or
  8. # modify it under the terms of either of these two licenses.
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. #
  16. # You should have received a copy of the licenses; if not, see
  17. # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
  18. # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
  19. # License, Version 2.0.
  20. #
  21. """Tests for the repository."""
  22. import locale
  23. import os
  24. import stat
  25. import shutil
  26. import sys
  27. import tempfile
  28. import warnings
  29. from dulwich import errors
  30. from dulwich.object_store import (
  31. tree_lookup_path,
  32. )
  33. from dulwich import objects
  34. from dulwich.config import Config
  35. from dulwich.errors import NotGitRepository
  36. from dulwich.repo import (
  37. Repo,
  38. MemoryRepo,
  39. )
  40. from dulwich.tests import (
  41. TestCase,
  42. skipIf,
  43. )
  44. from dulwich.tests.utils import (
  45. open_repo,
  46. tear_down_repo,
  47. setup_warning_catcher,
  48. )
  49. missing_sha = b'b91fa4d900e17e99b433218e988c4eb4a3e9a097'
  50. class CreateRepositoryTests(TestCase):
  51. def assertFileContentsEqual(self, expected, repo, path):
  52. f = repo.get_named_file(path)
  53. if not f:
  54. self.assertEqual(expected, None)
  55. else:
  56. with f:
  57. self.assertEqual(expected, f.read())
  58. def _check_repo_contents(self, repo, expect_bare):
  59. self.assertEqual(expect_bare, repo.bare)
  60. self.assertFileContentsEqual(
  61. b'Unnamed repository', repo, 'description')
  62. self.assertFileContentsEqual(
  63. b'', repo, os.path.join('info', 'exclude'))
  64. self.assertFileContentsEqual(None, repo, 'nonexistent file')
  65. barestr = b'bare = ' + str(expect_bare).lower().encode('ascii')
  66. with repo.get_named_file('config') as f:
  67. config_text = f.read()
  68. self.assertTrue(barestr in config_text, "%r" % config_text)
  69. expect_filemode = sys.platform != 'win32'
  70. barestr = b'filemode = ' + str(expect_filemode).lower().encode('ascii')
  71. with repo.get_named_file('config') as f:
  72. config_text = f.read()
  73. self.assertTrue(barestr in config_text, "%r" % config_text)
  74. def test_create_memory(self):
  75. repo = MemoryRepo.init_bare([], {})
  76. self._check_repo_contents(repo, True)
  77. def test_create_disk_bare(self):
  78. tmp_dir = tempfile.mkdtemp()
  79. self.addCleanup(shutil.rmtree, tmp_dir)
  80. repo = Repo.init_bare(tmp_dir)
  81. self.assertEqual(tmp_dir, repo._controldir)
  82. self._check_repo_contents(repo, True)
  83. def test_create_disk_non_bare(self):
  84. tmp_dir = tempfile.mkdtemp()
  85. self.addCleanup(shutil.rmtree, tmp_dir)
  86. repo = Repo.init(tmp_dir)
  87. self.assertEqual(os.path.join(tmp_dir, '.git'), repo._controldir)
  88. self._check_repo_contents(repo, False)
  89. def test_create_disk_non_bare_mkdir(self):
  90. tmp_dir = tempfile.mkdtemp()
  91. target_dir = os.path.join(tmp_dir, "target")
  92. self.addCleanup(shutil.rmtree, tmp_dir)
  93. repo = Repo.init(target_dir, mkdir=True)
  94. self.assertEqual(os.path.join(target_dir, '.git'), repo._controldir)
  95. self._check_repo_contents(repo, False)
  96. def test_create_disk_bare_mkdir(self):
  97. tmp_dir = tempfile.mkdtemp()
  98. target_dir = os.path.join(tmp_dir, "target")
  99. self.addCleanup(shutil.rmtree, tmp_dir)
  100. repo = Repo.init_bare(target_dir, mkdir=True)
  101. self.assertEqual(target_dir, repo._controldir)
  102. self._check_repo_contents(repo, True)
  103. class MemoryRepoTests(TestCase):
  104. def test_set_description(self):
  105. r = MemoryRepo.init_bare([], {})
  106. description = b"Some description"
  107. r.set_description(description)
  108. self.assertEqual(description, r.get_description())
  109. class RepositoryRootTests(TestCase):
  110. def mkdtemp(self):
  111. return tempfile.mkdtemp()
  112. def open_repo(self, name):
  113. temp_dir = self.mkdtemp()
  114. repo = open_repo(name, temp_dir)
  115. self.addCleanup(tear_down_repo, repo)
  116. return repo
  117. def test_simple_props(self):
  118. r = self.open_repo('a.git')
  119. self.assertEqual(r.controldir(), r.path)
  120. def test_setitem(self):
  121. r = self.open_repo('a.git')
  122. r[b"refs/tags/foo"] = b'a90fa2d900a17e99b433217e988c4eb4a2e9a097'
  123. self.assertEqual(b'a90fa2d900a17e99b433217e988c4eb4a2e9a097',
  124. r[b"refs/tags/foo"].id)
  125. def test_getitem_unicode(self):
  126. r = self.open_repo('a.git')
  127. test_keys = [
  128. (b'refs/heads/master', True),
  129. (b'a90fa2d900a17e99b433217e988c4eb4a2e9a097', True),
  130. (b'11' * 19 + b'--', False),
  131. ]
  132. for k, contained in test_keys:
  133. self.assertEqual(k in r, contained)
  134. # Avoid deprecation warning under Py3.2+
  135. if getattr(self, 'assertRaisesRegex', None):
  136. assertRaisesRegexp = self.assertRaisesRegex
  137. else:
  138. assertRaisesRegexp = self.assertRaisesRegexp
  139. for k, _ in test_keys:
  140. assertRaisesRegexp(
  141. TypeError, "'name' must be bytestring, not int",
  142. r.__getitem__, 12
  143. )
  144. def test_delitem(self):
  145. r = self.open_repo('a.git')
  146. del r[b'refs/heads/master']
  147. self.assertRaises(KeyError, lambda: r[b'refs/heads/master'])
  148. del r[b'HEAD']
  149. self.assertRaises(KeyError, lambda: r[b'HEAD'])
  150. self.assertRaises(ValueError, r.__delitem__, b'notrefs/foo')
  151. def test_get_refs(self):
  152. r = self.open_repo('a.git')
  153. self.assertEqual({
  154. b'HEAD': b'a90fa2d900a17e99b433217e988c4eb4a2e9a097',
  155. b'refs/heads/master': b'a90fa2d900a17e99b433217e988c4eb4a2e9a097',
  156. b'refs/tags/mytag': b'28237f4dc30d0d462658d6b937b08a0f0b6ef55a',
  157. b'refs/tags/mytag-packed':
  158. b'b0931cadc54336e78a1d980420e3268903b57a50',
  159. }, r.get_refs())
  160. def test_head(self):
  161. r = self.open_repo('a.git')
  162. self.assertEqual(r.head(), b'a90fa2d900a17e99b433217e988c4eb4a2e9a097')
  163. def test_get_object(self):
  164. r = self.open_repo('a.git')
  165. obj = r.get_object(r.head())
  166. self.assertEqual(obj.type_name, b'commit')
  167. def test_get_object_non_existant(self):
  168. r = self.open_repo('a.git')
  169. self.assertRaises(KeyError, r.get_object, missing_sha)
  170. def test_contains_object(self):
  171. r = self.open_repo('a.git')
  172. self.assertTrue(r.head() in r)
  173. def test_contains_ref(self):
  174. r = self.open_repo('a.git')
  175. self.assertTrue(b"HEAD" in r)
  176. def test_get_no_description(self):
  177. r = self.open_repo('a.git')
  178. self.assertIs(None, r.get_description())
  179. def test_get_description(self):
  180. r = self.open_repo('a.git')
  181. with open(os.path.join(r.path, 'description'), 'wb') as f:
  182. f.write(b"Some description")
  183. self.assertEqual(b"Some description", r.get_description())
  184. def test_set_description(self):
  185. r = self.open_repo('a.git')
  186. description = b"Some description"
  187. r.set_description(description)
  188. self.assertEqual(description, r.get_description())
  189. def test_contains_missing(self):
  190. r = self.open_repo('a.git')
  191. self.assertFalse(b"bar" in r)
  192. def test_get_peeled(self):
  193. # unpacked ref
  194. r = self.open_repo('a.git')
  195. tag_sha = b'28237f4dc30d0d462658d6b937b08a0f0b6ef55a'
  196. self.assertNotEqual(r[tag_sha].sha().hexdigest(), r.head())
  197. self.assertEqual(r.get_peeled(b'refs/tags/mytag'), r.head())
  198. # packed ref with cached peeled value
  199. packed_tag_sha = b'b0931cadc54336e78a1d980420e3268903b57a50'
  200. parent_sha = r[r.head()].parents[0]
  201. self.assertNotEqual(r[packed_tag_sha].sha().hexdigest(), parent_sha)
  202. self.assertEqual(r.get_peeled(b'refs/tags/mytag-packed'), parent_sha)
  203. # TODO: add more corner cases to test repo
  204. def test_get_peeled_not_tag(self):
  205. r = self.open_repo('a.git')
  206. self.assertEqual(r.get_peeled(b'HEAD'), r.head())
  207. def test_get_walker(self):
  208. r = self.open_repo('a.git')
  209. # include defaults to [r.head()]
  210. self.assertEqual(
  211. [e.commit.id for e in r.get_walker()],
  212. [r.head(), b'2a72d929692c41d8554c07f6301757ba18a65d91'])
  213. self.assertEqual(
  214. [e.commit.id for e in
  215. r.get_walker([b'2a72d929692c41d8554c07f6301757ba18a65d91'])],
  216. [b'2a72d929692c41d8554c07f6301757ba18a65d91'])
  217. self.assertEqual(
  218. [e.commit.id for e in
  219. r.get_walker(b'2a72d929692c41d8554c07f6301757ba18a65d91')],
  220. [b'2a72d929692c41d8554c07f6301757ba18a65d91'])
  221. def test_clone(self):
  222. r = self.open_repo('a.git')
  223. tmp_dir = self.mkdtemp()
  224. self.addCleanup(shutil.rmtree, tmp_dir)
  225. with r.clone(tmp_dir, mkdir=False) as t:
  226. self.assertEqual({
  227. b'HEAD': b'a90fa2d900a17e99b433217e988c4eb4a2e9a097',
  228. b'refs/remotes/origin/master':
  229. b'a90fa2d900a17e99b433217e988c4eb4a2e9a097',
  230. b'refs/heads/master':
  231. b'a90fa2d900a17e99b433217e988c4eb4a2e9a097',
  232. b'refs/tags/mytag':
  233. b'28237f4dc30d0d462658d6b937b08a0f0b6ef55a',
  234. b'refs/tags/mytag-packed':
  235. b'b0931cadc54336e78a1d980420e3268903b57a50',
  236. }, t.refs.as_dict())
  237. shas = [e.commit.id for e in r.get_walker()]
  238. self.assertEqual(shas, [t.head(),
  239. b'2a72d929692c41d8554c07f6301757ba18a65d91'])
  240. c = t.get_config()
  241. encoded_path = r.path
  242. if not isinstance(encoded_path, bytes):
  243. encoded_path = encoded_path.encode(sys.getfilesystemencoding())
  244. self.assertEqual(encoded_path,
  245. c.get((b'remote', b'origin'), b'url'))
  246. self.assertEqual(
  247. b'+refs/heads/*:refs/remotes/origin/*',
  248. c.get((b'remote', b'origin'), b'fetch'))
  249. def test_clone_no_head(self):
  250. temp_dir = self.mkdtemp()
  251. self.addCleanup(shutil.rmtree, temp_dir)
  252. repo_dir = os.path.join(os.path.dirname(__file__), 'data', 'repos')
  253. dest_dir = os.path.join(temp_dir, 'a.git')
  254. shutil.copytree(os.path.join(repo_dir, 'a.git'),
  255. dest_dir, symlinks=True)
  256. r = Repo(dest_dir)
  257. del r.refs[b"refs/heads/master"]
  258. del r.refs[b"HEAD"]
  259. t = r.clone(os.path.join(temp_dir, 'b.git'), mkdir=True)
  260. self.assertEqual({
  261. b'refs/tags/mytag': b'28237f4dc30d0d462658d6b937b08a0f0b6ef55a',
  262. b'refs/tags/mytag-packed':
  263. b'b0931cadc54336e78a1d980420e3268903b57a50',
  264. }, t.refs.as_dict())
  265. def test_clone_empty(self):
  266. """Test clone() doesn't crash if HEAD points to a non-existing ref.
  267. This simulates cloning server-side bare repository either when it is
  268. still empty or if user renames master branch and pushes private repo
  269. to the server.
  270. Non-bare repo HEAD always points to an existing ref.
  271. """
  272. r = self.open_repo('empty.git')
  273. tmp_dir = self.mkdtemp()
  274. self.addCleanup(shutil.rmtree, tmp_dir)
  275. r.clone(tmp_dir, mkdir=False, bare=True)
  276. def test_merge_history(self):
  277. r = self.open_repo('simple_merge.git')
  278. shas = [e.commit.id for e in r.get_walker()]
  279. self.assertEqual(shas, [b'5dac377bdded4c9aeb8dff595f0faeebcc8498cc',
  280. b'ab64bbdcc51b170d21588e5c5d391ee5c0c96dfd',
  281. b'4cffe90e0a41ad3f5190079d7c8f036bde29cbe6',
  282. b'60dacdc733de308bb77bb76ce0fb0f9b44c9769e',
  283. b'0d89f20333fbb1d2f3a94da77f4981373d8f4310'])
  284. def test_out_of_order_merge(self):
  285. """Test that revision history is ordered by date, not parent order."""
  286. r = self.open_repo('ooo_merge.git')
  287. shas = [e.commit.id for e in r.get_walker()]
  288. self.assertEqual(shas, [b'7601d7f6231db6a57f7bbb79ee52e4d462fd44d1',
  289. b'f507291b64138b875c28e03469025b1ea20bc614',
  290. b'fb5b0425c7ce46959bec94d54b9a157645e114f5',
  291. b'f9e39b120c68182a4ba35349f832d0e4e61f485c'])
  292. def test_get_tags_empty(self):
  293. r = self.open_repo('ooo_merge.git')
  294. self.assertEqual({}, r.refs.as_dict(b'refs/tags'))
  295. def test_get_config(self):
  296. r = self.open_repo('ooo_merge.git')
  297. self.assertIsInstance(r.get_config(), Config)
  298. def test_get_config_stack(self):
  299. r = self.open_repo('ooo_merge.git')
  300. self.assertIsInstance(r.get_config_stack(), Config)
  301. @skipIf(not getattr(os, 'symlink', None), 'Requires symlink support')
  302. def test_submodule(self):
  303. temp_dir = self.mkdtemp()
  304. self.addCleanup(shutil.rmtree, temp_dir)
  305. repo_dir = os.path.join(os.path.dirname(__file__), 'data', 'repos')
  306. shutil.copytree(os.path.join(repo_dir, 'a.git'),
  307. os.path.join(temp_dir, 'a.git'), symlinks=True)
  308. rel = os.path.relpath(os.path.join(repo_dir, 'submodule'), temp_dir)
  309. os.symlink(os.path.join(rel, 'dotgit'), os.path.join(temp_dir, '.git'))
  310. with Repo(temp_dir) as r:
  311. self.assertEqual(r.head(),
  312. b'a90fa2d900a17e99b433217e988c4eb4a2e9a097')
  313. def test_common_revisions(self):
  314. """
  315. This test demonstrates that ``find_common_revisions()`` actually
  316. returns common heads, not revisions; dulwich already uses
  317. ``find_common_revisions()`` in such a manner (see
  318. ``Repo.fetch_objects()``).
  319. """
  320. expected_shas = set([b'60dacdc733de308bb77bb76ce0fb0f9b44c9769e'])
  321. # Source for objects.
  322. r_base = self.open_repo('simple_merge.git')
  323. # Re-create each-side of the merge in simple_merge.git.
  324. #
  325. # Since the trees and blobs are missing, the repository created is
  326. # corrupted, but we're only checking for commits for the purpose of
  327. # this test, so it's immaterial.
  328. r1_dir = self.mkdtemp()
  329. self.addCleanup(shutil.rmtree, r1_dir)
  330. r1_commits = [b'ab64bbdcc51b170d21588e5c5d391ee5c0c96dfd', # HEAD
  331. b'60dacdc733de308bb77bb76ce0fb0f9b44c9769e',
  332. b'0d89f20333fbb1d2f3a94da77f4981373d8f4310']
  333. r2_dir = self.mkdtemp()
  334. self.addCleanup(shutil.rmtree, r2_dir)
  335. r2_commits = [b'4cffe90e0a41ad3f5190079d7c8f036bde29cbe6', # HEAD
  336. b'60dacdc733de308bb77bb76ce0fb0f9b44c9769e',
  337. b'0d89f20333fbb1d2f3a94da77f4981373d8f4310']
  338. r1 = Repo.init_bare(r1_dir)
  339. for c in r1_commits:
  340. r1.object_store.add_object(r_base.get_object(c))
  341. r1.refs[b'HEAD'] = r1_commits[0]
  342. r2 = Repo.init_bare(r2_dir)
  343. for c in r2_commits:
  344. r2.object_store.add_object(r_base.get_object(c))
  345. r2.refs[b'HEAD'] = r2_commits[0]
  346. # Finally, the 'real' testing!
  347. shas = r2.object_store.find_common_revisions(r1.get_graph_walker())
  348. self.assertEqual(set(shas), expected_shas)
  349. shas = r1.object_store.find_common_revisions(r2.get_graph_walker())
  350. self.assertEqual(set(shas), expected_shas)
  351. def test_shell_hook_pre_commit(self):
  352. if os.name != 'posix':
  353. self.skipTest('shell hook tests requires POSIX shell')
  354. pre_commit_fail = """#!/bin/sh
  355. exit 1
  356. """
  357. pre_commit_success = """#!/bin/sh
  358. exit 0
  359. """
  360. repo_dir = os.path.join(self.mkdtemp())
  361. self.addCleanup(shutil.rmtree, repo_dir)
  362. r = Repo.init(repo_dir)
  363. self.addCleanup(r.close)
  364. pre_commit = os.path.join(r.controldir(), 'hooks', 'pre-commit')
  365. with open(pre_commit, 'w') as f:
  366. f.write(pre_commit_fail)
  367. os.chmod(pre_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
  368. self.assertRaises(errors.CommitError, r.do_commit, 'failed commit',
  369. committer='Test Committer <test@nodomain.com>',
  370. author='Test Author <test@nodomain.com>',
  371. commit_timestamp=12345, commit_timezone=0,
  372. author_timestamp=12345, author_timezone=0)
  373. with open(pre_commit, 'w') as f:
  374. f.write(pre_commit_success)
  375. os.chmod(pre_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
  376. commit_sha = r.do_commit(
  377. b'empty commit',
  378. committer=b'Test Committer <test@nodomain.com>',
  379. author=b'Test Author <test@nodomain.com>',
  380. commit_timestamp=12395, commit_timezone=0,
  381. author_timestamp=12395, author_timezone=0)
  382. self.assertEqual([], r[commit_sha].parents)
  383. def test_shell_hook_commit_msg(self):
  384. if os.name != 'posix':
  385. self.skipTest('shell hook tests requires POSIX shell')
  386. commit_msg_fail = """#!/bin/sh
  387. exit 1
  388. """
  389. commit_msg_success = """#!/bin/sh
  390. exit 0
  391. """
  392. repo_dir = self.mkdtemp()
  393. self.addCleanup(shutil.rmtree, repo_dir)
  394. r = Repo.init(repo_dir)
  395. self.addCleanup(r.close)
  396. commit_msg = os.path.join(r.controldir(), 'hooks', 'commit-msg')
  397. with open(commit_msg, 'w') as f:
  398. f.write(commit_msg_fail)
  399. os.chmod(commit_msg, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
  400. self.assertRaises(errors.CommitError, r.do_commit, b'failed commit',
  401. committer=b'Test Committer <test@nodomain.com>',
  402. author=b'Test Author <test@nodomain.com>',
  403. commit_timestamp=12345, commit_timezone=0,
  404. author_timestamp=12345, author_timezone=0)
  405. with open(commit_msg, 'w') as f:
  406. f.write(commit_msg_success)
  407. os.chmod(commit_msg, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
  408. commit_sha = r.do_commit(
  409. b'empty commit',
  410. committer=b'Test Committer <test@nodomain.com>',
  411. author=b'Test Author <test@nodomain.com>',
  412. commit_timestamp=12395, commit_timezone=0,
  413. author_timestamp=12395, author_timezone=0)
  414. self.assertEqual([], r[commit_sha].parents)
  415. def test_shell_hook_post_commit(self):
  416. if os.name != 'posix':
  417. self.skipTest('shell hook tests requires POSIX shell')
  418. repo_dir = self.mkdtemp()
  419. self.addCleanup(shutil.rmtree, repo_dir)
  420. r = Repo.init(repo_dir)
  421. self.addCleanup(r.close)
  422. (fd, path) = tempfile.mkstemp(dir=repo_dir)
  423. os.close(fd)
  424. post_commit_msg = """#!/bin/sh
  425. rm """ + path + """
  426. """
  427. root_sha = r.do_commit(
  428. b'empty commit',
  429. committer=b'Test Committer <test@nodomain.com>',
  430. author=b'Test Author <test@nodomain.com>',
  431. commit_timestamp=12345, commit_timezone=0,
  432. author_timestamp=12345, author_timezone=0)
  433. self.assertEqual([], r[root_sha].parents)
  434. post_commit = os.path.join(r.controldir(), 'hooks', 'post-commit')
  435. with open(post_commit, 'wb') as f:
  436. f.write(post_commit_msg.encode(locale.getpreferredencoding()))
  437. os.chmod(post_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
  438. commit_sha = r.do_commit(
  439. b'empty commit',
  440. committer=b'Test Committer <test@nodomain.com>',
  441. author=b'Test Author <test@nodomain.com>',
  442. commit_timestamp=12345, commit_timezone=0,
  443. author_timestamp=12345, author_timezone=0)
  444. self.assertEqual([root_sha], r[commit_sha].parents)
  445. self.assertFalse(os.path.exists(path))
  446. post_commit_msg_fail = """#!/bin/sh
  447. exit 1
  448. """
  449. with open(post_commit, 'w') as f:
  450. f.write(post_commit_msg_fail)
  451. os.chmod(post_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
  452. warnings.simplefilter("always", UserWarning)
  453. self.addCleanup(warnings.resetwarnings)
  454. warnings_list, restore_warnings = setup_warning_catcher()
  455. self.addCleanup(restore_warnings)
  456. commit_sha2 = r.do_commit(
  457. b'empty commit',
  458. committer=b'Test Committer <test@nodomain.com>',
  459. author=b'Test Author <test@nodomain.com>',
  460. commit_timestamp=12345, commit_timezone=0,
  461. author_timestamp=12345, author_timezone=0)
  462. expected_warning = UserWarning(
  463. 'post-commit hook failed: Hook post-commit exited with '
  464. 'non-zero status',)
  465. for w in warnings_list:
  466. if (type(w) == type(expected_warning) and
  467. w.args == expected_warning.args):
  468. break
  469. else:
  470. raise AssertionError(
  471. 'Expected warning %r not in %r' %
  472. (expected_warning, warnings_list))
  473. self.assertEqual([commit_sha], r[commit_sha2].parents)
  474. def test_as_dict(self):
  475. def check(repo):
  476. self.assertEqual(
  477. repo.refs.subkeys(b'refs/tags'),
  478. repo.refs.subkeys(b'refs/tags/'))
  479. self.assertEqual(
  480. repo.refs.as_dict(b'refs/tags'),
  481. repo.refs.as_dict(b'refs/tags/'))
  482. self.assertEqual(
  483. repo.refs.as_dict(b'refs/heads'),
  484. repo.refs.as_dict(b'refs/heads/'))
  485. bare = self.open_repo('a.git')
  486. tmp_dir = self.mkdtemp()
  487. self.addCleanup(shutil.rmtree, tmp_dir)
  488. with bare.clone(tmp_dir, mkdir=False) as nonbare:
  489. check(nonbare)
  490. check(bare)
  491. def test_working_tree(self):
  492. temp_dir = tempfile.mkdtemp()
  493. self.addCleanup(shutil.rmtree, temp_dir)
  494. worktree_temp_dir = tempfile.mkdtemp()
  495. self.addCleanup(shutil.rmtree, worktree_temp_dir)
  496. r = Repo.init(temp_dir)
  497. self.addCleanup(r.close)
  498. root_sha = r.do_commit(
  499. b'empty commit',
  500. committer=b'Test Committer <test@nodomain.com>',
  501. author=b'Test Author <test@nodomain.com>',
  502. commit_timestamp=12345, commit_timezone=0,
  503. author_timestamp=12345, author_timezone=0)
  504. r.refs[b'refs/heads/master'] = root_sha
  505. w = Repo._init_new_working_directory(worktree_temp_dir, r)
  506. self.addCleanup(w.close)
  507. new_sha = w.do_commit(
  508. b'new commit',
  509. committer=b'Test Committer <test@nodomain.com>',
  510. author=b'Test Author <test@nodomain.com>',
  511. commit_timestamp=12345, commit_timezone=0,
  512. author_timestamp=12345, author_timezone=0)
  513. w.refs[b'HEAD'] = new_sha
  514. self.assertEqual(os.path.abspath(r.controldir()),
  515. os.path.abspath(w.commondir()))
  516. self.assertEqual(r.refs.keys(), w.refs.keys())
  517. self.assertNotEqual(r.head(), w.head())
  518. class BuildRepoRootTests(TestCase):
  519. """Tests that build on-disk repos from scratch.
  520. Repos live in a temp dir and are torn down after each test. They start with
  521. a single commit in master having single file named 'a'.
  522. """
  523. def get_repo_dir(self):
  524. return os.path.join(tempfile.mkdtemp(), 'test')
  525. def setUp(self):
  526. super(BuildRepoRootTests, self).setUp()
  527. self._repo_dir = self.get_repo_dir()
  528. os.makedirs(self._repo_dir)
  529. r = self._repo = Repo.init(self._repo_dir)
  530. self.addCleanup(tear_down_repo, r)
  531. self.assertFalse(r.bare)
  532. self.assertEqual(b'ref: refs/heads/master', r.refs.read_ref(b'HEAD'))
  533. self.assertRaises(KeyError, lambda: r.refs[b'refs/heads/master'])
  534. with open(os.path.join(r.path, 'a'), 'wb') as f:
  535. f.write(b'file contents')
  536. r.stage(['a'])
  537. commit_sha = r.do_commit(
  538. b'msg',
  539. committer=b'Test Committer <test@nodomain.com>',
  540. author=b'Test Author <test@nodomain.com>',
  541. commit_timestamp=12345, commit_timezone=0,
  542. author_timestamp=12345, author_timezone=0)
  543. self.assertEqual([], r[commit_sha].parents)
  544. self._root_commit = commit_sha
  545. def test_build_repo(self):
  546. r = self._repo
  547. self.assertEqual(b'ref: refs/heads/master', r.refs.read_ref(b'HEAD'))
  548. self.assertEqual(self._root_commit, r.refs[b'refs/heads/master'])
  549. expected_blob = objects.Blob.from_string(b'file contents')
  550. self.assertEqual(expected_blob.data, r[expected_blob.id].data)
  551. actual_commit = r[self._root_commit]
  552. self.assertEqual(b'msg', actual_commit.message)
  553. def test_commit_modified(self):
  554. r = self._repo
  555. with open(os.path.join(r.path, 'a'), 'wb') as f:
  556. f.write(b'new contents')
  557. r.stage(['a'])
  558. commit_sha = r.do_commit(
  559. b'modified a',
  560. committer=b'Test Committer <test@nodomain.com>',
  561. author=b'Test Author <test@nodomain.com>',
  562. commit_timestamp=12395, commit_timezone=0,
  563. author_timestamp=12395, author_timezone=0)
  564. self.assertEqual([self._root_commit], r[commit_sha].parents)
  565. a_mode, a_id = tree_lookup_path(r.get_object, r[commit_sha].tree, b'a')
  566. self.assertEqual(stat.S_IFREG | 0o644, a_mode)
  567. self.assertEqual(b'new contents', r[a_id].data)
  568. @skipIf(not getattr(os, 'symlink', None), 'Requires symlink support')
  569. def test_commit_symlink(self):
  570. r = self._repo
  571. os.symlink('a', os.path.join(r.path, 'b'))
  572. r.stage(['a', 'b'])
  573. commit_sha = r.do_commit(
  574. b'Symlink b',
  575. committer=b'Test Committer <test@nodomain.com>',
  576. author=b'Test Author <test@nodomain.com>',
  577. commit_timestamp=12395, commit_timezone=0,
  578. author_timestamp=12395, author_timezone=0)
  579. self.assertEqual([self._root_commit], r[commit_sha].parents)
  580. b_mode, b_id = tree_lookup_path(r.get_object, r[commit_sha].tree, b'b')
  581. self.assertTrue(stat.S_ISLNK(b_mode))
  582. self.assertEqual(b'a', r[b_id].data)
  583. def test_commit_deleted(self):
  584. r = self._repo
  585. os.remove(os.path.join(r.path, 'a'))
  586. r.stage(['a'])
  587. commit_sha = r.do_commit(
  588. b'deleted a',
  589. committer=b'Test Committer <test@nodomain.com>',
  590. author=b'Test Author <test@nodomain.com>',
  591. commit_timestamp=12395, commit_timezone=0,
  592. author_timestamp=12395, author_timezone=0)
  593. self.assertEqual([self._root_commit], r[commit_sha].parents)
  594. self.assertEqual([], list(r.open_index()))
  595. tree = r[r[commit_sha].tree]
  596. self.assertEqual([], list(tree.iteritems()))
  597. def test_commit_follows(self):
  598. r = self._repo
  599. r.refs.set_symbolic_ref(b'HEAD', b'refs/heads/bla')
  600. commit_sha = r.do_commit(
  601. b'commit with strange character',
  602. committer=b'Test Committer <test@nodomain.com>',
  603. author=b'Test Author <test@nodomain.com>',
  604. commit_timestamp=12395, commit_timezone=0,
  605. author_timestamp=12395, author_timezone=0,
  606. ref=b'HEAD')
  607. self.assertEqual(commit_sha, r[b'refs/heads/bla'].id)
  608. def test_commit_encoding(self):
  609. r = self._repo
  610. commit_sha = r.do_commit(
  611. b'commit with strange character \xee',
  612. committer=b'Test Committer <test@nodomain.com>',
  613. author=b'Test Author <test@nodomain.com>',
  614. commit_timestamp=12395, commit_timezone=0,
  615. author_timestamp=12395, author_timezone=0,
  616. encoding=b"iso8859-1")
  617. self.assertEqual(b"iso8859-1", r[commit_sha].encoding)
  618. def test_commit_config_identity(self):
  619. # commit falls back to the users' identity if it wasn't specified
  620. r = self._repo
  621. c = r.get_config()
  622. c.set((b"user", ), b"name", b"Jelmer")
  623. c.set((b"user", ), b"email", b"jelmer@apache.org")
  624. c.write_to_path()
  625. commit_sha = r.do_commit(b'message')
  626. self.assertEqual(
  627. b"Jelmer <jelmer@apache.org>",
  628. r[commit_sha].author)
  629. self.assertEqual(
  630. b"Jelmer <jelmer@apache.org>",
  631. r[commit_sha].committer)
  632. def test_commit_config_identity_in_memoryrepo(self):
  633. # commit falls back to the users' identity if it wasn't specified
  634. r = MemoryRepo.init_bare([], {})
  635. c = r.get_config()
  636. c.set((b"user", ), b"name", b"Jelmer")
  637. c.set((b"user", ), b"email", b"jelmer@apache.org")
  638. commit_sha = r.do_commit(b'message', tree=objects.Tree().id)
  639. self.assertEqual(
  640. b"Jelmer <jelmer@apache.org>",
  641. r[commit_sha].author)
  642. self.assertEqual(
  643. b"Jelmer <jelmer@apache.org>",
  644. r[commit_sha].committer)
  645. def test_commit_fail_ref(self):
  646. r = self._repo
  647. def set_if_equals(name, old_ref, new_ref):
  648. return False
  649. r.refs.set_if_equals = set_if_equals
  650. def add_if_new(name, new_ref):
  651. self.fail('Unexpected call to add_if_new')
  652. r.refs.add_if_new = add_if_new
  653. old_shas = set(r.object_store)
  654. self.assertRaises(errors.CommitError, r.do_commit, b'failed commit',
  655. committer=b'Test Committer <test@nodomain.com>',
  656. author=b'Test Author <test@nodomain.com>',
  657. commit_timestamp=12345, commit_timezone=0,
  658. author_timestamp=12345, author_timezone=0)
  659. new_shas = set(r.object_store) - old_shas
  660. self.assertEqual(1, len(new_shas))
  661. # Check that the new commit (now garbage) was added.
  662. new_commit = r[new_shas.pop()]
  663. self.assertEqual(r[self._root_commit].tree, new_commit.tree)
  664. self.assertEqual(b'failed commit', new_commit.message)
  665. def test_commit_branch(self):
  666. r = self._repo
  667. commit_sha = r.do_commit(
  668. b'commit to branch',
  669. committer=b'Test Committer <test@nodomain.com>',
  670. author=b'Test Author <test@nodomain.com>',
  671. commit_timestamp=12395, commit_timezone=0,
  672. author_timestamp=12395, author_timezone=0,
  673. ref=b"refs/heads/new_branch")
  674. self.assertEqual(self._root_commit, r[b"HEAD"].id)
  675. self.assertEqual(commit_sha, r[b"refs/heads/new_branch"].id)
  676. self.assertEqual([], r[commit_sha].parents)
  677. self.assertTrue(b"refs/heads/new_branch" in r)
  678. new_branch_head = commit_sha
  679. commit_sha = r.do_commit(
  680. b'commit to branch 2',
  681. committer=b'Test Committer <test@nodomain.com>',
  682. author=b'Test Author <test@nodomain.com>',
  683. commit_timestamp=12395, commit_timezone=0,
  684. author_timestamp=12395, author_timezone=0,
  685. ref=b"refs/heads/new_branch")
  686. self.assertEqual(self._root_commit, r[b"HEAD"].id)
  687. self.assertEqual(commit_sha, r[b"refs/heads/new_branch"].id)
  688. self.assertEqual([new_branch_head], r[commit_sha].parents)
  689. def test_commit_merge_heads(self):
  690. r = self._repo
  691. merge_1 = r.do_commit(
  692. b'commit to branch 2',
  693. committer=b'Test Committer <test@nodomain.com>',
  694. author=b'Test Author <test@nodomain.com>',
  695. commit_timestamp=12395, commit_timezone=0,
  696. author_timestamp=12395, author_timezone=0,
  697. ref=b"refs/heads/new_branch")
  698. commit_sha = r.do_commit(
  699. b'commit with merge',
  700. committer=b'Test Committer <test@nodomain.com>',
  701. author=b'Test Author <test@nodomain.com>',
  702. commit_timestamp=12395, commit_timezone=0,
  703. author_timestamp=12395, author_timezone=0,
  704. merge_heads=[merge_1])
  705. self.assertEqual(
  706. [self._root_commit, merge_1],
  707. r[commit_sha].parents)
  708. def test_commit_dangling_commit(self):
  709. r = self._repo
  710. old_shas = set(r.object_store)
  711. old_refs = r.get_refs()
  712. commit_sha = r.do_commit(
  713. b'commit with no ref',
  714. committer=b'Test Committer <test@nodomain.com>',
  715. author=b'Test Author <test@nodomain.com>',
  716. commit_timestamp=12395, commit_timezone=0,
  717. author_timestamp=12395, author_timezone=0,
  718. ref=None)
  719. new_shas = set(r.object_store) - old_shas
  720. # New sha is added, but no new refs
  721. self.assertEqual(1, len(new_shas))
  722. new_commit = r[new_shas.pop()]
  723. self.assertEqual(r[self._root_commit].tree, new_commit.tree)
  724. self.assertEqual([], r[commit_sha].parents)
  725. self.assertEqual(old_refs, r.get_refs())
  726. def test_commit_dangling_commit_with_parents(self):
  727. r = self._repo
  728. old_shas = set(r.object_store)
  729. old_refs = r.get_refs()
  730. commit_sha = r.do_commit(
  731. b'commit with no ref',
  732. committer=b'Test Committer <test@nodomain.com>',
  733. author=b'Test Author <test@nodomain.com>',
  734. commit_timestamp=12395, commit_timezone=0,
  735. author_timestamp=12395, author_timezone=0,
  736. ref=None, merge_heads=[self._root_commit])
  737. new_shas = set(r.object_store) - old_shas
  738. # New sha is added, but no new refs
  739. self.assertEqual(1, len(new_shas))
  740. new_commit = r[new_shas.pop()]
  741. self.assertEqual(r[self._root_commit].tree, new_commit.tree)
  742. self.assertEqual([self._root_commit], r[commit_sha].parents)
  743. self.assertEqual(old_refs, r.get_refs())
  744. def test_stage_absolute(self):
  745. r = self._repo
  746. os.remove(os.path.join(r.path, 'a'))
  747. self.assertRaises(ValueError, r.stage, [os.path.join(r.path, 'a')])
  748. def test_stage_deleted(self):
  749. r = self._repo
  750. os.remove(os.path.join(r.path, 'a'))
  751. r.stage(['a'])
  752. r.stage(['a']) # double-stage a deleted path
  753. def test_stage_directory(self):
  754. r = self._repo
  755. os.mkdir(os.path.join(r.path, 'c'))
  756. r.stage(['c'])
  757. self.assertEqual([b'a'], list(r.open_index()))
  758. @skipIf(sys.platform == 'win32' and sys.version_info[:2] >= (3, 6),
  759. 'tries to implicitly decode as utf8')
  760. def test_commit_no_encode_decode(self):
  761. r = self._repo
  762. repo_path_bytes = r.path.encode(sys.getfilesystemencoding())
  763. encodings = ('utf8', 'latin1')
  764. names = [u'À'.encode(encoding) for encoding in encodings]
  765. for name, encoding in zip(names, encodings):
  766. full_path = os.path.join(repo_path_bytes, name)
  767. with open(full_path, 'wb') as f:
  768. f.write(encoding.encode('ascii'))
  769. # These files are break tear_down_repo, so cleanup these files
  770. # ourselves.
  771. self.addCleanup(os.remove, full_path)
  772. r.stage(names)
  773. commit_sha = r.do_commit(
  774. b'Files with different encodings',
  775. committer=b'Test Committer <test@nodomain.com>',
  776. author=b'Test Author <test@nodomain.com>',
  777. commit_timestamp=12395, commit_timezone=0,
  778. author_timestamp=12395, author_timezone=0,
  779. ref=None, merge_heads=[self._root_commit])
  780. for name, encoding in zip(names, encodings):
  781. mode, id = tree_lookup_path(r.get_object, r[commit_sha].tree, name)
  782. self.assertEqual(stat.S_IFREG | 0o644, mode)
  783. self.assertEqual(encoding.encode('ascii'), r[id].data)
  784. def test_discover_intended(self):
  785. path = os.path.join(self._repo_dir, 'b/c')
  786. r = Repo.discover(path)
  787. self.assertEqual(r.head(), self._repo.head())
  788. def test_discover_isrepo(self):
  789. r = Repo.discover(self._repo_dir)
  790. self.assertEqual(r.head(), self._repo.head())
  791. def test_discover_notrepo(self):
  792. with self.assertRaises(NotGitRepository):
  793. Repo.discover('/')