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.

930 lines
36KB

  1. # -*- coding: utf-8 -*-
  2. # test_index.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 io import BytesIO
  8. import os
  9. from stat import (
  10. S_ISLNK,
  11. ST_MODE
  12. )
  13. import tempfile
  14. from unittest import skipIf
  15. from git import (
  16. IndexFile,
  17. Repo,
  18. BlobFilter,
  19. UnmergedEntriesError,
  20. Tree,
  21. Object,
  22. Diff,
  23. GitCommandError,
  24. CheckoutError,
  25. )
  26. from git.compat import string_types, is_win, PY3
  27. from git.exc import (
  28. HookExecutionError,
  29. InvalidGitRepositoryError
  30. )
  31. from git.index.fun import hook_path
  32. from git.index.typ import (
  33. BaseIndexEntry,
  34. IndexEntry
  35. )
  36. from git.objects import Blob
  37. from git.test.lib import (
  38. TestBase,
  39. fixture_path,
  40. fixture,
  41. with_rw_repo
  42. )
  43. from git.test.lib import with_rw_directory
  44. from git.util import Actor, rmtree
  45. from git.util import HIDE_WINDOWS_KNOWN_ERRORS, hex_to_bin
  46. from gitdb.base import IStream
  47. import os.path as osp
  48. from git.cmd import Git
  49. HOOKS_SHEBANG = "#!/usr/bin/env sh\n"
  50. @skipIf(HIDE_WINDOWS_KNOWN_ERRORS, "TODO: fix hooks execution on Windows: #703")
  51. def _make_hook(git_dir, name, content, make_exec=True):
  52. """A helper to create a hook"""
  53. hp = hook_path(name, git_dir)
  54. hpd = osp.dirname(hp)
  55. if not osp.isdir(hpd):
  56. os.mkdir(hpd)
  57. with open(hp, "wt") as fp:
  58. fp.write(HOOKS_SHEBANG + content)
  59. if make_exec:
  60. os.chmod(hp, 0o744)
  61. return hp
  62. class TestIndex(TestBase):
  63. def __init__(self, *args):
  64. super(TestIndex, self).__init__(*args)
  65. self._reset_progress()
  66. def _assert_fprogress(self, entries):
  67. self.assertEqual(len(entries), len(self._fprogress_map))
  68. for path, call_count in self._fprogress_map.items(): # @UnusedVariable
  69. self.assertEqual(call_count, 2)
  70. # END for each item in progress map
  71. self._reset_progress()
  72. def _fprogress(self, path, done, item):
  73. self._fprogress_map.setdefault(path, 0)
  74. curval = self._fprogress_map[path]
  75. if curval == 0:
  76. assert not done
  77. if curval == 1:
  78. assert done
  79. self._fprogress_map[path] = curval + 1
  80. def _fprogress_add(self, path, done, item):
  81. """Called as progress func - we keep track of the proper
  82. call order"""
  83. assert item is not None
  84. self._fprogress(path, done, item)
  85. def _reset_progress(self):
  86. # maps paths to the count of calls
  87. self._fprogress_map = {}
  88. def _assert_entries(self, entries):
  89. for entry in entries:
  90. assert isinstance(entry, BaseIndexEntry)
  91. assert not osp.isabs(entry.path)
  92. assert "\\" not in entry.path
  93. # END for each entry
  94. def test_index_file_base(self):
  95. # read from file
  96. index = IndexFile(self.rorepo, fixture_path("index"))
  97. assert index.entries
  98. assert index.version > 0
  99. # test entry
  100. entry = next(iter(index.entries.values()))
  101. for attr in ("path", "ctime", "mtime", "dev", "inode", "mode", "uid",
  102. "gid", "size", "binsha", "hexsha", "stage"):
  103. getattr(entry, attr)
  104. # END for each method
  105. # test update
  106. entries = index.entries
  107. assert isinstance(index.update(), IndexFile)
  108. assert entries is not index.entries
  109. # test stage
  110. index_merge = IndexFile(self.rorepo, fixture_path("index_merge"))
  111. self.assertEqual(len(index_merge.entries), 106)
  112. assert len([e for e in index_merge.entries.values() if e.stage != 0])
  113. # write the data - it must match the original
  114. tmpfile = tempfile.mktemp()
  115. index_merge.write(tmpfile)
  116. with open(tmpfile, 'rb') as fp:
  117. self.assertEqual(fp.read(), fixture("index_merge"))
  118. os.remove(tmpfile)
  119. def _cmp_tree_index(self, tree, index):
  120. # fail unless both objects contain the same paths and blobs
  121. if isinstance(tree, str):
  122. tree = self.rorepo.commit(tree).tree
  123. blist = []
  124. for blob in tree.traverse(predicate=lambda e, d: e.type == "blob", branch_first=False):
  125. assert (blob.path, 0) in index.entries
  126. blist.append(blob)
  127. # END for each blob in tree
  128. if len(blist) != len(index.entries):
  129. iset = {k[0] for k in index.entries.keys()}
  130. bset = {b.path for b in blist}
  131. raise AssertionError("CMP Failed: Missing entries in index: %s, missing in tree: %s" %
  132. (bset - iset, iset - bset))
  133. # END assertion message
  134. @with_rw_repo('0.1.6')
  135. def test_index_lock_handling(self, rw_repo):
  136. def add_bad_blob():
  137. rw_repo.index.add([Blob(rw_repo, b'f' * 20, 'bad-permissions', 'foo')])
  138. try:
  139. ## 1st fail on purpose adding into index.
  140. add_bad_blob()
  141. except Exception as ex:
  142. msg_py3 = "required argument is not an integer"
  143. msg_py2 = "cannot convert argument to integer"
  144. assert msg_py2 in str(ex) or msg_py3 in str(ex)
  145. ## 2nd time should not fail due to stray lock file
  146. try:
  147. add_bad_blob()
  148. except Exception as ex:
  149. assert "index.lock' could not be obtained" not in str(ex)
  150. @with_rw_repo('0.1.6')
  151. def test_index_file_from_tree(self, rw_repo):
  152. common_ancestor_sha = "5117c9c8a4d3af19a9958677e45cda9269de1541"
  153. cur_sha = "4b43ca7ff72d5f535134241e7c797ddc9c7a3573"
  154. other_sha = "39f85c4358b7346fee22169da9cad93901ea9eb9"
  155. # simple index from tree
  156. base_index = IndexFile.from_tree(rw_repo, common_ancestor_sha)
  157. assert base_index.entries
  158. self._cmp_tree_index(common_ancestor_sha, base_index)
  159. # merge two trees - its like a fast-forward
  160. two_way_index = IndexFile.from_tree(rw_repo, common_ancestor_sha, cur_sha)
  161. assert two_way_index.entries
  162. self._cmp_tree_index(cur_sha, two_way_index)
  163. # merge three trees - here we have a merge conflict
  164. three_way_index = IndexFile.from_tree(rw_repo, common_ancestor_sha, cur_sha, other_sha)
  165. assert len([e for e in three_way_index.entries.values() if e.stage != 0])
  166. # ITERATE BLOBS
  167. merge_required = lambda t: t[0] != 0
  168. merge_blobs = list(three_way_index.iter_blobs(merge_required))
  169. assert merge_blobs
  170. assert merge_blobs[0][0] in (1, 2, 3)
  171. assert isinstance(merge_blobs[0][1], Blob)
  172. # test BlobFilter
  173. prefix = 'lib/git'
  174. for stage, blob in base_index.iter_blobs(BlobFilter([prefix])): # @UnusedVariable
  175. assert blob.path.startswith(prefix)
  176. # writing a tree should fail with an unmerged index
  177. self.failUnlessRaises(UnmergedEntriesError, three_way_index.write_tree)
  178. # removed unmerged entries
  179. unmerged_blob_map = three_way_index.unmerged_blobs()
  180. assert unmerged_blob_map
  181. # pick the first blob at the first stage we find and use it as resolved version
  182. three_way_index.resolve_blobs(l[0][1] for l in unmerged_blob_map.values())
  183. tree = three_way_index.write_tree()
  184. assert isinstance(tree, Tree)
  185. num_blobs = 0
  186. for blob in tree.traverse(predicate=lambda item, d: item.type == "blob"):
  187. assert (blob.path, 0) in three_way_index.entries
  188. num_blobs += 1
  189. # END for each blob
  190. self.assertEqual(num_blobs, len(three_way_index.entries))
  191. @with_rw_repo('0.1.6')
  192. def test_index_merge_tree(self, rw_repo):
  193. # A bit out of place, but we need a different repo for this:
  194. self.assertNotEqual(self.rorepo, rw_repo)
  195. self.assertEqual(len({self.rorepo, self.rorepo, rw_repo, rw_repo}), 2)
  196. # SINGLE TREE MERGE
  197. # current index is at the (virtual) cur_commit
  198. next_commit = "4c39f9da792792d4e73fc3a5effde66576ae128c"
  199. parent_commit = rw_repo.head.commit.parents[0]
  200. manifest_key = IndexFile.entry_key('MANIFEST.in', 0)
  201. manifest_entry = rw_repo.index.entries[manifest_key]
  202. rw_repo.index.merge_tree(next_commit)
  203. # only one change should be recorded
  204. assert manifest_entry.binsha != rw_repo.index.entries[manifest_key].binsha
  205. rw_repo.index.reset(rw_repo.head)
  206. self.assertEqual(rw_repo.index.entries[manifest_key].binsha, manifest_entry.binsha)
  207. # FAKE MERGE
  208. #############
  209. # Add a change with a NULL sha that should conflict with next_commit. We
  210. # pretend there was a change, but we do not even bother adding a proper
  211. # sha for it ( which makes things faster of course )
  212. manifest_fake_entry = BaseIndexEntry((manifest_entry[0], b"\0" * 20, 0, manifest_entry[3]))
  213. # try write flag
  214. self._assert_entries(rw_repo.index.add([manifest_fake_entry], write=False))
  215. # add actually resolves the null-hex-sha for us as a feature, but we can
  216. # edit the index manually
  217. assert rw_repo.index.entries[manifest_key].binsha != Object.NULL_BIN_SHA
  218. # must operate on the same index for this ! Its a bit problematic as
  219. # it might confuse people
  220. index = rw_repo.index
  221. index.entries[manifest_key] = IndexEntry.from_base(manifest_fake_entry)
  222. index.write()
  223. self.assertEqual(rw_repo.index.entries[manifest_key].hexsha, Diff.NULL_HEX_SHA)
  224. # write an unchanged index ( just for the fun of it )
  225. rw_repo.index.write()
  226. # a three way merge would result in a conflict and fails as the command will
  227. # not overwrite any entries in our index and hence leave them unmerged. This is
  228. # mainly a protection feature as the current index is not yet in a tree
  229. self.failUnlessRaises(GitCommandError, index.merge_tree, next_commit, base=parent_commit)
  230. # the only way to get the merged entries is to safe the current index away into a tree,
  231. # which is like a temporary commit for us. This fails as well as the NULL sha deos not
  232. # have a corresponding object
  233. # NOTE: missing_ok is not a kwarg anymore, missing_ok is always true
  234. # self.failUnlessRaises(GitCommandError, index.write_tree)
  235. # if missing objects are okay, this would work though ( they are always okay now )
  236. # As we can't read back the tree with NULL_SHA, we rather set it to something else
  237. index.entries[manifest_key] = IndexEntry(manifest_entry[:1] + (hex_to_bin('f' * 40),) + manifest_entry[2:])
  238. tree = index.write_tree()
  239. # now make a proper three way merge with unmerged entries
  240. unmerged_tree = IndexFile.from_tree(rw_repo, parent_commit, tree, next_commit)
  241. unmerged_blobs = unmerged_tree.unmerged_blobs()
  242. self.assertEqual(len(unmerged_blobs), 1)
  243. self.assertEqual(list(unmerged_blobs.keys())[0], manifest_key[0])
  244. @with_rw_repo('0.1.6')
  245. def test_index_file_diffing(self, rw_repo):
  246. # default Index instance points to our index
  247. index = IndexFile(rw_repo)
  248. assert index.path is not None
  249. assert len(index.entries)
  250. # write the file back
  251. index.write()
  252. # could sha it, or check stats
  253. # test diff
  254. # resetting the head will leave the index in a different state, and the
  255. # diff will yield a few changes
  256. cur_head_commit = rw_repo.head.reference.commit
  257. rw_repo.head.reset('HEAD~6', index=True, working_tree=False)
  258. # diff against same index is 0
  259. diff = index.diff()
  260. self.assertEqual(len(diff), 0)
  261. # against HEAD as string, must be the same as it matches index
  262. diff = index.diff('HEAD')
  263. self.assertEqual(len(diff), 0)
  264. # against previous head, there must be a difference
  265. diff = index.diff(cur_head_commit)
  266. assert len(diff)
  267. # we reverse the result
  268. adiff = index.diff(str(cur_head_commit), R=True)
  269. odiff = index.diff(cur_head_commit, R=False) # now its not reversed anymore
  270. assert adiff != odiff
  271. self.assertEqual(odiff, diff) # both unreversed diffs against HEAD
  272. # against working copy - its still at cur_commit
  273. wdiff = index.diff(None)
  274. assert wdiff != adiff
  275. assert wdiff != odiff
  276. # against something unusual
  277. self.failUnlessRaises(ValueError, index.diff, int)
  278. # adjust the index to match an old revision
  279. cur_branch = rw_repo.active_branch
  280. cur_commit = cur_branch.commit
  281. rev_head_parent = 'HEAD~1'
  282. assert index.reset(rev_head_parent) is index
  283. self.assertEqual(cur_branch, rw_repo.active_branch)
  284. self.assertEqual(cur_commit, rw_repo.head.commit)
  285. # there must be differences towards the working tree which is in the 'future'
  286. assert index.diff(None)
  287. # reset the working copy as well to current head,to pull 'back' as well
  288. new_data = b"will be reverted"
  289. file_path = osp.join(rw_repo.working_tree_dir, "CHANGES")
  290. with open(file_path, "wb") as fp:
  291. fp.write(new_data)
  292. index.reset(rev_head_parent, working_tree=True)
  293. assert not index.diff(None)
  294. self.assertEqual(cur_branch, rw_repo.active_branch)
  295. self.assertEqual(cur_commit, rw_repo.head.commit)
  296. with open(file_path, 'rb') as fp:
  297. assert fp.read() != new_data
  298. # test full checkout
  299. test_file = osp.join(rw_repo.working_tree_dir, "CHANGES")
  300. with open(test_file, 'ab') as fd:
  301. fd.write(b"some data")
  302. rval = index.checkout(None, force=True, fprogress=self._fprogress)
  303. assert 'CHANGES' in list(rval)
  304. self._assert_fprogress([None])
  305. assert osp.isfile(test_file)
  306. os.remove(test_file)
  307. rval = index.checkout(None, force=False, fprogress=self._fprogress)
  308. assert 'CHANGES' in list(rval)
  309. self._assert_fprogress([None])
  310. assert osp.isfile(test_file)
  311. # individual file
  312. os.remove(test_file)
  313. rval = index.checkout(test_file, fprogress=self._fprogress)
  314. self.assertEqual(list(rval)[0], 'CHANGES')
  315. self._assert_fprogress([test_file])
  316. assert osp.exists(test_file)
  317. # checking out non-existing file throws
  318. self.failUnlessRaises(CheckoutError, index.checkout, "doesnt_exist_ever.txt.that")
  319. self.failUnlessRaises(CheckoutError, index.checkout, paths=["doesnt/exist"])
  320. # checkout file with modifications
  321. append_data = b"hello"
  322. with open(test_file, "ab") as fp:
  323. fp.write(append_data)
  324. try:
  325. index.checkout(test_file)
  326. except CheckoutError as e:
  327. self.assertEqual(len(e.failed_files), 1)
  328. self.assertEqual(e.failed_files[0], osp.basename(test_file))
  329. self.assertEqual(len(e.failed_files), len(e.failed_reasons))
  330. self.assertIsInstance(e.failed_reasons[0], string_types)
  331. self.assertEqual(len(e.valid_files), 0)
  332. with open(test_file, 'rb') as fd:
  333. s = fd.read()
  334. self.assertTrue(s.endswith(append_data), s)
  335. else:
  336. raise AssertionError("Exception CheckoutError not thrown")
  337. # if we force it it should work
  338. index.checkout(test_file, force=True)
  339. assert not open(test_file, 'rb').read().endswith(append_data)
  340. # checkout directory
  341. rmtree(osp.join(rw_repo.working_tree_dir, "lib"))
  342. rval = index.checkout('lib')
  343. assert len(list(rval)) > 1
  344. def _count_existing(self, repo, files):
  345. """
  346. Returns count of files that actually exist in the repository directory.
  347. """
  348. existing = 0
  349. basedir = repo.working_tree_dir
  350. for f in files:
  351. existing += osp.isfile(osp.join(basedir, f))
  352. # END for each deleted file
  353. return existing
  354. # END num existing helper
  355. @skipIf(HIDE_WINDOWS_KNOWN_ERRORS and Git.is_cygwin(),
  356. """FIXME: File "C:\\projects\\gitpython\\git\\test\\test_index.py", line 642, in test_index_mutation
  357. self.assertEqual(fd.read(), link_target)
  358. AssertionError: '!<symlink>\xff\xfe/\x00e\x00t\x00c\x00/\x00t\x00h\x00a\x00t\x00\x00\x00'
  359. != '/etc/that'
  360. """)
  361. @with_rw_repo('0.1.6')
  362. def test_index_mutation(self, rw_repo):
  363. index = rw_repo.index
  364. num_entries = len(index.entries)
  365. cur_head = rw_repo.head
  366. uname = u"Thomas Müller"
  367. umail = "sd@company.com"
  368. with rw_repo.config_writer() as writer:
  369. writer.set_value("user", "name", uname)
  370. writer.set_value("user", "email", umail)
  371. self.assertEqual(writer.get_value("user", "name"), uname)
  372. # remove all of the files, provide a wild mix of paths, BaseIndexEntries,
  373. # IndexEntries
  374. def mixed_iterator():
  375. count = 0
  376. for entry in index.entries.values():
  377. type_id = count % 4
  378. if type_id == 0: # path
  379. yield entry.path
  380. elif type_id == 1: # blob
  381. yield Blob(rw_repo, entry.binsha, entry.mode, entry.path)
  382. elif type_id == 2: # BaseIndexEntry
  383. yield BaseIndexEntry(entry[:4])
  384. elif type_id == 3: # IndexEntry
  385. yield entry
  386. else:
  387. raise AssertionError("Invalid Type")
  388. count += 1
  389. # END for each entry
  390. # END mixed iterator
  391. deleted_files = index.remove(mixed_iterator(), working_tree=False)
  392. assert deleted_files
  393. self.assertEqual(self._count_existing(rw_repo, deleted_files), len(deleted_files))
  394. self.assertEqual(len(index.entries), 0)
  395. # reset the index to undo our changes
  396. index.reset()
  397. self.assertEqual(len(index.entries), num_entries)
  398. # remove with working copy
  399. deleted_files = index.remove(mixed_iterator(), working_tree=True)
  400. assert deleted_files
  401. self.assertEqual(self._count_existing(rw_repo, deleted_files), 0)
  402. # reset everything
  403. index.reset(working_tree=True)
  404. self.assertEqual(self._count_existing(rw_repo, deleted_files), len(deleted_files))
  405. # invalid type
  406. self.failUnlessRaises(TypeError, index.remove, [1])
  407. # absolute path
  408. deleted_files = index.remove([osp.join(rw_repo.working_tree_dir, "lib")], r=True)
  409. assert len(deleted_files) > 1
  410. self.failUnlessRaises(ValueError, index.remove, ["/doesnt/exists"])
  411. # TEST COMMITTING
  412. # commit changed index
  413. cur_commit = cur_head.commit
  414. commit_message = u"commit default head by Frèderic Çaufl€"
  415. new_commit = index.commit(commit_message, head=False)
  416. assert cur_commit != new_commit
  417. self.assertEqual(new_commit.author.name, uname)
  418. self.assertEqual(new_commit.author.email, umail)
  419. self.assertEqual(new_commit.committer.name, uname)
  420. self.assertEqual(new_commit.committer.email, umail)
  421. self.assertEqual(new_commit.message, commit_message)
  422. self.assertEqual(new_commit.parents[0], cur_commit)
  423. self.assertEqual(len(new_commit.parents), 1)
  424. self.assertEqual(cur_head.commit, cur_commit)
  425. # commit with other actor
  426. cur_commit = cur_head.commit
  427. my_author = Actor(u"Frèderic Çaufl€", "author@example.com")
  428. my_committer = Actor(u"Committing Frèderic Çaufl€", "committer@example.com")
  429. commit_actor = index.commit(commit_message, author=my_author, committer=my_committer)
  430. assert cur_commit != commit_actor
  431. self.assertEqual(commit_actor.author.name, u"Frèderic Çaufl€")
  432. self.assertEqual(commit_actor.author.email, "author@example.com")
  433. self.assertEqual(commit_actor.committer.name, u"Committing Frèderic Çaufl€")
  434. self.assertEqual(commit_actor.committer.email, "committer@example.com")
  435. self.assertEqual(commit_actor.message, commit_message)
  436. self.assertEqual(commit_actor.parents[0], cur_commit)
  437. self.assertEqual(len(new_commit.parents), 1)
  438. self.assertEqual(cur_head.commit, commit_actor)
  439. self.assertEqual(cur_head.log()[-1].actor, my_committer)
  440. # commit with author_date and commit_date
  441. cur_commit = cur_head.commit
  442. commit_message = u"commit with dates by Avinash Sajjanshetty"
  443. new_commit = index.commit(commit_message, author_date="2006-04-07T22:13:13", commit_date="2005-04-07T22:13:13")
  444. assert cur_commit != new_commit
  445. print(new_commit.authored_date, new_commit.committed_date)
  446. self.assertEqual(new_commit.message, commit_message)
  447. self.assertEqual(new_commit.authored_date, 1144447993)
  448. self.assertEqual(new_commit.committed_date, 1112911993)
  449. # same index, no parents
  450. commit_message = "index without parents"
  451. commit_no_parents = index.commit(commit_message, parent_commits=[], head=True)
  452. self.assertEqual(commit_no_parents.message, commit_message)
  453. self.assertEqual(len(commit_no_parents.parents), 0)
  454. self.assertEqual(cur_head.commit, commit_no_parents)
  455. # same index, multiple parents
  456. commit_message = "Index with multiple parents\n commit with another line"
  457. commit_multi_parent = index.commit(commit_message, parent_commits=(commit_no_parents, new_commit))
  458. self.assertEqual(commit_multi_parent.message, commit_message)
  459. self.assertEqual(len(commit_multi_parent.parents), 2)
  460. self.assertEqual(commit_multi_parent.parents[0], commit_no_parents)
  461. self.assertEqual(commit_multi_parent.parents[1], new_commit)
  462. self.assertEqual(cur_head.commit, commit_multi_parent)
  463. # re-add all files in lib
  464. # get the lib folder back on disk, but get an index without it
  465. index.reset(new_commit.parents[0], working_tree=True).reset(new_commit, working_tree=False)
  466. lib_file_path = osp.join("lib", "git", "__init__.py")
  467. assert (lib_file_path, 0) not in index.entries
  468. assert osp.isfile(osp.join(rw_repo.working_tree_dir, lib_file_path))
  469. # directory
  470. entries = index.add(['lib'], fprogress=self._fprogress_add)
  471. self._assert_entries(entries)
  472. self._assert_fprogress(entries)
  473. assert len(entries) > 1
  474. # glob
  475. entries = index.reset(new_commit).add([osp.join('lib', 'git', '*.py')], fprogress=self._fprogress_add)
  476. self._assert_entries(entries)
  477. self._assert_fprogress(entries)
  478. self.assertEqual(len(entries), 14)
  479. # same file
  480. entries = index.reset(new_commit).add(
  481. [osp.join(rw_repo.working_tree_dir, 'lib', 'git', 'head.py')] * 2, fprogress=self._fprogress_add)
  482. self._assert_entries(entries)
  483. self.assertEqual(entries[0].mode & 0o644, 0o644)
  484. # would fail, test is too primitive to handle this case
  485. # self._assert_fprogress(entries)
  486. self._reset_progress()
  487. self.assertEqual(len(entries), 2)
  488. # missing path
  489. self.failUnlessRaises(OSError, index.reset(new_commit).add, ['doesnt/exist/must/raise'])
  490. # blob from older revision overrides current index revision
  491. old_blob = new_commit.parents[0].tree.blobs[0]
  492. entries = index.reset(new_commit).add([old_blob], fprogress=self._fprogress_add)
  493. self._assert_entries(entries)
  494. self._assert_fprogress(entries)
  495. self.assertEqual(index.entries[(old_blob.path, 0)].hexsha, old_blob.hexsha)
  496. self.assertEqual(len(entries), 1)
  497. # mode 0 not allowed
  498. null_hex_sha = Diff.NULL_HEX_SHA
  499. null_bin_sha = b"\0" * 20
  500. self.failUnlessRaises(ValueError, index.reset(
  501. new_commit).add, [BaseIndexEntry((0, null_bin_sha, 0, "doesntmatter"))])
  502. # add new file
  503. new_file_relapath = "my_new_file"
  504. self._make_file(new_file_relapath, "hello world", rw_repo)
  505. entries = index.reset(new_commit).add(
  506. [BaseIndexEntry((0o10644, null_bin_sha, 0, new_file_relapath))], fprogress=self._fprogress_add)
  507. self._assert_entries(entries)
  508. self._assert_fprogress(entries)
  509. self.assertEqual(len(entries), 1)
  510. self.assertNotEquals(entries[0].hexsha, null_hex_sha)
  511. # add symlink
  512. if not is_win:
  513. for target in ('/etc/nonexisting', '/etc/passwd', '/etc'):
  514. basename = "my_real_symlink"
  515. link_file = osp.join(rw_repo.working_tree_dir, basename)
  516. os.symlink(target, link_file)
  517. entries = index.reset(new_commit).add([link_file], fprogress=self._fprogress_add)
  518. self._assert_entries(entries)
  519. self._assert_fprogress(entries)
  520. self.assertEqual(len(entries), 1)
  521. self.assertTrue(S_ISLNK(entries[0].mode))
  522. self.assertTrue(S_ISLNK(index.entries[index.entry_key("my_real_symlink", 0)].mode))
  523. # we expect only the target to be written
  524. self.assertEqual(index.repo.odb.stream(entries[0].binsha).read().decode('ascii'), target)
  525. os.remove(link_file)
  526. # end for each target
  527. # END real symlink test
  528. # add fake symlink and assure it checks-our as symlink
  529. fake_symlink_relapath = "my_fake_symlink"
  530. link_target = "/etc/that"
  531. fake_symlink_path = self._make_file(fake_symlink_relapath, link_target, rw_repo)
  532. fake_entry = BaseIndexEntry((0o120000, null_bin_sha, 0, fake_symlink_relapath))
  533. entries = index.reset(new_commit).add([fake_entry], fprogress=self._fprogress_add)
  534. self._assert_entries(entries)
  535. self._assert_fprogress(entries)
  536. assert entries[0].hexsha != null_hex_sha
  537. self.assertEqual(len(entries), 1)
  538. self.assertTrue(S_ISLNK(entries[0].mode))
  539. # assure this also works with an alternate method
  540. full_index_entry = IndexEntry.from_base(BaseIndexEntry((0o120000, entries[0].binsha, 0, entries[0].path)))
  541. entry_key = index.entry_key(full_index_entry)
  542. index.reset(new_commit)
  543. assert entry_key not in index.entries
  544. index.entries[entry_key] = full_index_entry
  545. index.write()
  546. index.update() # force reread of entries
  547. new_entry = index.entries[entry_key]
  548. assert S_ISLNK(new_entry.mode)
  549. # a tree created from this should contain the symlink
  550. tree = index.write_tree()
  551. assert fake_symlink_relapath in tree
  552. index.write() # flush our changes for the checkout
  553. # checkout the fakelink, should be a link then
  554. assert not S_ISLNK(os.stat(fake_symlink_path)[ST_MODE])
  555. os.remove(fake_symlink_path)
  556. index.checkout(fake_symlink_path)
  557. # on windows we will never get symlinks
  558. if is_win:
  559. # simlinks should contain the link as text ( which is what a
  560. # symlink actually is )
  561. with open(fake_symlink_path, 'rt') as fd:
  562. self.assertEqual(fd.read(), link_target)
  563. else:
  564. self.assertTrue(S_ISLNK(os.lstat(fake_symlink_path)[ST_MODE]))
  565. # TEST RENAMING
  566. def assert_mv_rval(rval):
  567. for source, dest in rval:
  568. assert not osp.exists(source) and osp.exists(dest)
  569. # END for each renamed item
  570. # END move assertion utility
  571. self.failUnlessRaises(ValueError, index.move, ['just_one_path'])
  572. # file onto existing file
  573. files = ['AUTHORS', 'LICENSE']
  574. self.failUnlessRaises(GitCommandError, index.move, files)
  575. # again, with force
  576. assert_mv_rval(index.move(files, f=True))
  577. # files into directory - dry run
  578. paths = ['LICENSE', 'VERSION', 'doc']
  579. rval = index.move(paths, dry_run=True)
  580. self.assertEqual(len(rval), 2)
  581. assert osp.exists(paths[0])
  582. # again, no dry run
  583. rval = index.move(paths)
  584. assert_mv_rval(rval)
  585. # dir into dir
  586. rval = index.move(['doc', 'test'])
  587. assert_mv_rval(rval)
  588. # TEST PATH REWRITING
  589. ######################
  590. count = [0]
  591. def rewriter(entry):
  592. rval = str(count[0])
  593. count[0] += 1
  594. return rval
  595. # END rewriter
  596. def make_paths():
  597. # two existing ones, one new one
  598. yield 'CHANGES'
  599. yield 'ez_setup.py'
  600. yield index.entries[index.entry_key('README', 0)]
  601. yield index.entries[index.entry_key('.gitignore', 0)]
  602. for fid in range(3):
  603. fname = 'newfile%i' % fid
  604. with open(fname, 'wb') as fd:
  605. fd.write(b"abcd")
  606. yield Blob(rw_repo, Blob.NULL_BIN_SHA, 0o100644, fname)
  607. # END for each new file
  608. # END path producer
  609. paths = list(make_paths())
  610. self._assert_entries(index.add(paths, path_rewriter=rewriter))
  611. for filenum in range(len(paths)):
  612. assert index.entry_key(str(filenum), 0) in index.entries
  613. # TEST RESET ON PATHS
  614. ######################
  615. arela = "aa"
  616. brela = "bb"
  617. afile = self._make_file(arela, "adata", rw_repo)
  618. bfile = self._make_file(brela, "bdata", rw_repo)
  619. akey = index.entry_key(arela, 0)
  620. bkey = index.entry_key(brela, 0)
  621. keys = (akey, bkey)
  622. absfiles = (afile, bfile)
  623. files = (arela, brela)
  624. for fkey in keys:
  625. assert fkey not in index.entries
  626. index.add(files, write=True)
  627. nc = index.commit("2 files committed", head=False)
  628. for fkey in keys:
  629. assert fkey in index.entries
  630. # just the index
  631. index.reset(paths=(arela, afile))
  632. assert akey not in index.entries
  633. assert bkey in index.entries
  634. # now with working tree - files on disk as well as entries must be recreated
  635. rw_repo.head.commit = nc
  636. for absfile in absfiles:
  637. os.remove(absfile)
  638. index.reset(working_tree=True, paths=files)
  639. for fkey in keys:
  640. assert fkey in index.entries
  641. for absfile in absfiles:
  642. assert osp.isfile(absfile)
  643. @with_rw_repo('HEAD')
  644. def test_compare_write_tree(self, rw_repo):
  645. # write all trees and compare them
  646. # its important to have a few submodules in there too
  647. max_count = 25
  648. count = 0
  649. for commit in rw_repo.head.commit.traverse():
  650. if count >= max_count:
  651. break
  652. count += 1
  653. index = rw_repo.index.reset(commit)
  654. orig_tree = commit.tree
  655. self.assertEqual(index.write_tree(), orig_tree)
  656. # END for each commit
  657. def test_index_new(self):
  658. B = self.rorepo.tree("6d9b1f4f9fa8c9f030e3207e7deacc5d5f8bba4e")
  659. H = self.rorepo.tree("25dca42bac17d511b7e2ebdd9d1d679e7626db5f")
  660. M = self.rorepo.tree("e746f96bcc29238b79118123028ca170adc4ff0f")
  661. for args in ((B,), (B, H), (B, H, M)):
  662. index = IndexFile.new(self.rorepo, *args)
  663. assert isinstance(index, IndexFile)
  664. # END for each arg tuple
  665. @with_rw_repo('HEAD', bare=True)
  666. def test_index_bare_add(self, rw_bare_repo):
  667. # Something is wrong after cloning to a bare repo, reading the
  668. # property rw_bare_repo.working_tree_dir will return '/tmp'
  669. # instead of throwing the Exception we are expecting. This is
  670. # a quick hack to make this test fail when expected.
  671. assert rw_bare_repo.working_tree_dir is None
  672. assert rw_bare_repo.bare
  673. contents = b'This is a BytesIO file'
  674. filesize = len(contents)
  675. fileobj = BytesIO(contents)
  676. filename = 'my-imaginary-file'
  677. istream = rw_bare_repo.odb.store(
  678. IStream(Blob.type, filesize, fileobj))
  679. entry = BaseIndexEntry((0o100644, istream.binsha, 0, filename))
  680. try:
  681. rw_bare_repo.index.add([entry])
  682. except AssertionError:
  683. self.fail("Adding to the index of a bare repo is not allowed.")
  684. # Adding using a path should still require a non-bare repository.
  685. asserted = False
  686. path = osp.join('git', 'test', 'test_index.py')
  687. try:
  688. rw_bare_repo.index.add([path])
  689. except InvalidGitRepositoryError:
  690. asserted = True
  691. assert asserted, "Adding using a filename is not correctly asserted."
  692. @skipIf(HIDE_WINDOWS_KNOWN_ERRORS and not PY3, r"""
  693. FIXME: File "C:\projects\gitpython\git\util.py", line 125, in to_native_path_linux
  694. return path.replace('\\', '/')
  695. UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 0: ordinal not in range(128)""")
  696. @with_rw_directory
  697. def test_add_utf8P_path(self, rw_dir):
  698. # NOTE: fp is not a Unicode object in python 2 (which is the source of the problem)
  699. fp = osp.join(rw_dir, 'ø.txt')
  700. with open(fp, 'wb') as fs:
  701. fs.write(u'content of ø'.encode('utf-8'))
  702. r = Repo.init(rw_dir)
  703. r.index.add([fp])
  704. r.index.commit('Added orig and prestable')
  705. @with_rw_directory
  706. def test_add_a_file_with_wildcard_chars(self, rw_dir):
  707. # see issue #407
  708. fp = osp.join(rw_dir, '[.exe')
  709. with open(fp, "wb") as f:
  710. f.write(b'something')
  711. r = Repo.init(rw_dir)
  712. r.index.add([fp])
  713. r.index.commit('Added [.exe')
  714. def test__to_relative_path_at_root(self):
  715. root = osp.abspath(os.sep)
  716. class Mocked(object):
  717. bare = False
  718. git_dir = root
  719. working_tree_dir = root
  720. repo = Mocked()
  721. path = os.path.join(root, 'file')
  722. index = IndexFile(repo)
  723. rel = index._to_relative_path(path)
  724. self.assertEqual(rel, os.path.relpath(path, root))
  725. @with_rw_repo('HEAD', bare=True)
  726. def test_pre_commit_hook_success(self, rw_repo):
  727. index = rw_repo.index
  728. _make_hook(
  729. index.repo.git_dir,
  730. 'pre-commit',
  731. "exit 0"
  732. )
  733. index.commit("This should not fail")
  734. @with_rw_repo('HEAD', bare=True)
  735. def test_pre_commit_hook_fail(self, rw_repo):
  736. index = rw_repo.index
  737. hp = _make_hook(
  738. index.repo.git_dir,
  739. 'pre-commit',
  740. "echo stdout; echo stderr 1>&2; exit 1"
  741. )
  742. try:
  743. index.commit("This should fail")
  744. except HookExecutionError as err:
  745. if is_win:
  746. self.assertIsInstance(err.status, OSError)
  747. self.assertEqual(err.command, [hp])
  748. self.assertEqual(err.stdout, '')
  749. self.assertEqual(err.stderr, '')
  750. assert str(err)
  751. else:
  752. self.assertEqual(err.status, 1)
  753. self.assertEqual(err.command, [hp])
  754. self.assertEqual(err.stdout, "\n stdout: 'stdout\n'")
  755. self.assertEqual(err.stderr, "\n stderr: 'stderr\n'")
  756. assert str(err)
  757. else:
  758. raise AssertionError("Should have caught a HookExecutionError")
  759. @with_rw_repo('HEAD', bare=True)
  760. def test_commit_msg_hook_success(self, rw_repo):
  761. commit_message = u"commit default head by Frèderic Çaufl€"
  762. from_hook_message = u"from commit-msg"
  763. index = rw_repo.index
  764. _make_hook(
  765. index.repo.git_dir,
  766. 'commit-msg',
  767. 'echo -n " {}" >> "$1"'.format(from_hook_message)
  768. )
  769. new_commit = index.commit(commit_message)
  770. self.assertEqual(new_commit.message, u"{} {}".format(commit_message, from_hook_message))
  771. @with_rw_repo('HEAD', bare=True)
  772. def test_commit_msg_hook_fail(self, rw_repo):
  773. index = rw_repo.index
  774. hp = _make_hook(
  775. index.repo.git_dir,
  776. 'commit-msg',
  777. "echo stdout; echo stderr 1>&2; exit 1"
  778. )
  779. try:
  780. index.commit("This should fail")
  781. except HookExecutionError as err:
  782. if is_win:
  783. self.assertIsInstance(err.status, OSError)
  784. self.assertEqual(err.command, [hp])
  785. self.assertEqual(err.stdout, '')
  786. self.assertEqual(err.stderr, '')
  787. assert str(err)
  788. else:
  789. self.assertEqual(err.status, 1)
  790. self.assertEqual(err.command, [hp])
  791. self.assertEqual(err.stdout, "\n stdout: 'stdout\n'")
  792. self.assertEqual(err.stderr, "\n stderr: 'stderr\n'")
  793. assert str(err)
  794. else:
  795. raise AssertionError("Should have cought a HookExecutionError")