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.

294 lines
10KB

  1. from io import BytesIO
  2. from stat import S_IFDIR, S_IFREG, S_IFLNK
  3. from os import stat
  4. import os.path as osp
  5. from unittest import skipIf, SkipTest
  6. from git import Git
  7. from git.compat import PY3
  8. from git.index import IndexFile
  9. from git.index.fun import (
  10. aggressive_tree_merge
  11. )
  12. from git.objects.fun import (
  13. traverse_tree_recursive,
  14. traverse_trees_recursive,
  15. tree_to_stream,
  16. tree_entries_from_data,
  17. )
  18. from git.repo.fun import (
  19. find_worktree_git_dir
  20. )
  21. from git.test.lib import (
  22. assert_true,
  23. TestBase,
  24. with_rw_repo,
  25. with_rw_directory
  26. )
  27. from git.util import bin_to_hex, cygpath, join_path_native
  28. from gitdb.base import IStream
  29. from gitdb.typ import str_tree_type
  30. class TestFun(TestBase):
  31. def _assert_index_entries(self, entries, trees):
  32. index = IndexFile.from_tree(self.rorepo, *[self.rorepo.tree(bin_to_hex(t).decode('ascii')) for t in trees])
  33. assert entries
  34. assert len(index.entries) == len(entries)
  35. for entry in entries:
  36. assert (entry.path, entry.stage) in index.entries
  37. # END assert entry matches fully
  38. def test_aggressive_tree_merge(self):
  39. # head tree with additions, removals and modification compared to its predecessor
  40. odb = self.rorepo.odb
  41. HC = self.rorepo.commit("6c1faef799095f3990e9970bc2cb10aa0221cf9c")
  42. H = HC.tree
  43. B = HC.parents[0].tree
  44. # entries from single tree
  45. trees = [H.binsha]
  46. self._assert_index_entries(aggressive_tree_merge(odb, trees), trees)
  47. # from multiple trees
  48. trees = [B.binsha, H.binsha]
  49. self._assert_index_entries(aggressive_tree_merge(odb, trees), trees)
  50. # three way, no conflict
  51. tree = self.rorepo.tree
  52. B = tree("35a09c0534e89b2d43ec4101a5fb54576b577905")
  53. H = tree("4fe5cfa0e063a8d51a1eb6f014e2aaa994e5e7d4")
  54. M = tree("1f2b19de3301e76ab3a6187a49c9c93ff78bafbd")
  55. trees = [B.binsha, H.binsha, M.binsha]
  56. self._assert_index_entries(aggressive_tree_merge(odb, trees), trees)
  57. # three-way, conflict in at least one file, both modified
  58. B = tree("a7a4388eeaa4b6b94192dce67257a34c4a6cbd26")
  59. H = tree("f9cec00938d9059882bb8eabdaf2f775943e00e5")
  60. M = tree("44a601a068f4f543f73fd9c49e264c931b1e1652")
  61. trees = [B.binsha, H.binsha, M.binsha]
  62. self._assert_index_entries(aggressive_tree_merge(odb, trees), trees)
  63. # too many trees
  64. self.failUnlessRaises(ValueError, aggressive_tree_merge, odb, trees * 2)
  65. def mktree(self, odb, entries):
  66. """create a tree from the given tree entries and safe it to the database"""
  67. sio = BytesIO()
  68. tree_to_stream(entries, sio.write)
  69. sio.seek(0)
  70. istream = odb.store(IStream(str_tree_type, len(sio.getvalue()), sio))
  71. return istream.binsha
  72. @with_rw_repo('0.1.6')
  73. def test_three_way_merge(self, rwrepo):
  74. def mkfile(name, sha, executable=0):
  75. return (sha, S_IFREG | 0o644 | executable * 0o111, name)
  76. def mkcommit(name, sha):
  77. return (sha, S_IFDIR | S_IFLNK, name)
  78. def assert_entries(entries, num_entries, has_conflict=False):
  79. assert len(entries) == num_entries
  80. assert has_conflict == (len([e for e in entries if e.stage != 0]) > 0)
  81. mktree = self.mktree
  82. shaa = b"\1" * 20
  83. shab = b"\2" * 20
  84. shac = b"\3" * 20
  85. odb = rwrepo.odb
  86. # base tree
  87. bfn = 'basefile'
  88. fbase = mkfile(bfn, shaa)
  89. tb = mktree(odb, [fbase])
  90. # non-conflicting new files, same data
  91. fa = mkfile('1', shab)
  92. th = mktree(odb, [fbase, fa])
  93. fb = mkfile('2', shac)
  94. tm = mktree(odb, [fbase, fb])
  95. # two new files, same base file
  96. trees = [tb, th, tm]
  97. assert_entries(aggressive_tree_merge(odb, trees), 3)
  98. # both delete same file, add own one
  99. fa = mkfile('1', shab)
  100. th = mktree(odb, [fa])
  101. fb = mkfile('2', shac)
  102. tm = mktree(odb, [fb])
  103. # two new files
  104. trees = [tb, th, tm]
  105. assert_entries(aggressive_tree_merge(odb, trees), 2)
  106. # same file added in both, differently
  107. fa = mkfile('1', shab)
  108. th = mktree(odb, [fa])
  109. fb = mkfile('1', shac)
  110. tm = mktree(odb, [fb])
  111. # expect conflict
  112. trees = [tb, th, tm]
  113. assert_entries(aggressive_tree_merge(odb, trees), 2, True)
  114. # same file added, different mode
  115. fa = mkfile('1', shab)
  116. th = mktree(odb, [fa])
  117. fb = mkcommit('1', shab)
  118. tm = mktree(odb, [fb])
  119. # expect conflict
  120. trees = [tb, th, tm]
  121. assert_entries(aggressive_tree_merge(odb, trees), 2, True)
  122. # same file added in both
  123. fa = mkfile('1', shab)
  124. th = mktree(odb, [fa])
  125. fb = mkfile('1', shab)
  126. tm = mktree(odb, [fb])
  127. # expect conflict
  128. trees = [tb, th, tm]
  129. assert_entries(aggressive_tree_merge(odb, trees), 1)
  130. # modify same base file, differently
  131. fa = mkfile(bfn, shab)
  132. th = mktree(odb, [fa])
  133. fb = mkfile(bfn, shac)
  134. tm = mktree(odb, [fb])
  135. # conflict, 3 versions on 3 stages
  136. trees = [tb, th, tm]
  137. assert_entries(aggressive_tree_merge(odb, trees), 3, True)
  138. # change mode on same base file, by making one a commit, the other executable
  139. # no content change ( this is totally unlikely to happen in the real world )
  140. fa = mkcommit(bfn, shaa)
  141. th = mktree(odb, [fa])
  142. fb = mkfile(bfn, shaa, executable=1)
  143. tm = mktree(odb, [fb])
  144. # conflict, 3 versions on 3 stages, because of different mode
  145. trees = [tb, th, tm]
  146. assert_entries(aggressive_tree_merge(odb, trees), 3, True)
  147. for is_them in range(2):
  148. # only we/they change contents
  149. fa = mkfile(bfn, shab)
  150. th = mktree(odb, [fa])
  151. trees = [tb, th, tb]
  152. if is_them:
  153. trees = [tb, tb, th]
  154. entries = aggressive_tree_merge(odb, trees)
  155. assert len(entries) == 1 and entries[0].binsha == shab
  156. # only we/they change the mode
  157. fa = mkcommit(bfn, shaa)
  158. th = mktree(odb, [fa])
  159. trees = [tb, th, tb]
  160. if is_them:
  161. trees = [tb, tb, th]
  162. entries = aggressive_tree_merge(odb, trees)
  163. assert len(entries) == 1 and entries[0].binsha == shaa and entries[0].mode == fa[1]
  164. # one side deletes, the other changes = conflict
  165. fa = mkfile(bfn, shab)
  166. th = mktree(odb, [fa])
  167. tm = mktree(odb, [])
  168. trees = [tb, th, tm]
  169. if is_them:
  170. trees = [tb, tm, th]
  171. # as one is deleted, there are only 2 entries
  172. assert_entries(aggressive_tree_merge(odb, trees), 2, True)
  173. # END handle ours, theirs
  174. def _assert_tree_entries(self, entries, num_trees):
  175. for entry in entries:
  176. assert len(entry) == num_trees
  177. paths = {e[2] for e in entry if e}
  178. # only one path per set of entries
  179. assert len(paths) == 1
  180. # END verify entry
  181. def test_tree_traversal(self):
  182. # low level tree tarversal
  183. odb = self.rorepo.odb
  184. H = self.rorepo.tree('29eb123beb1c55e5db4aa652d843adccbd09ae18') # head tree
  185. M = self.rorepo.tree('e14e3f143e7260de9581aee27e5a9b2645db72de') # merge tree
  186. B = self.rorepo.tree('f606937a7a21237c866efafcad33675e6539c103') # base tree
  187. B_old = self.rorepo.tree('1f66cfbbce58b4b552b041707a12d437cc5f400a') # old base tree
  188. # two very different trees
  189. entries = traverse_trees_recursive(odb, [B_old.binsha, H.binsha], '')
  190. self._assert_tree_entries(entries, 2)
  191. oentries = traverse_trees_recursive(odb, [H.binsha, B_old.binsha], '')
  192. assert len(oentries) == len(entries)
  193. self._assert_tree_entries(oentries, 2)
  194. # single tree
  195. is_no_tree = lambda i, d: i.type != 'tree'
  196. entries = traverse_trees_recursive(odb, [B.binsha], '')
  197. assert len(entries) == len(list(B.traverse(predicate=is_no_tree)))
  198. self._assert_tree_entries(entries, 1)
  199. # two trees
  200. entries = traverse_trees_recursive(odb, [B.binsha, H.binsha], '')
  201. self._assert_tree_entries(entries, 2)
  202. # tree trees
  203. entries = traverse_trees_recursive(odb, [B.binsha, H.binsha, M.binsha], '')
  204. self._assert_tree_entries(entries, 3)
  205. def test_tree_traversal_single(self):
  206. max_count = 50
  207. count = 0
  208. odb = self.rorepo.odb
  209. for commit in self.rorepo.commit("29eb123beb1c55e5db4aa652d843adccbd09ae18").traverse():
  210. if count >= max_count:
  211. break
  212. count += 1
  213. entries = traverse_tree_recursive(odb, commit.tree.binsha, '')
  214. assert entries
  215. # END for each commit
  216. @with_rw_directory
  217. def test_linked_worktree_traversal(self, rw_dir):
  218. """Check that we can identify a linked worktree based on a .git file"""
  219. git = Git(rw_dir)
  220. if git.version_info[:3] < (2, 5, 1):
  221. raise SkipTest("worktree feature unsupported")
  222. rw_master = self.rorepo.clone(join_path_native(rw_dir, 'master_repo'))
  223. branch = rw_master.create_head('aaaaaaaa')
  224. worktree_path = join_path_native(rw_dir, 'worktree_repo')
  225. if Git.is_cygwin():
  226. worktree_path = cygpath(worktree_path)
  227. rw_master.git.worktree('add', worktree_path, branch.name)
  228. dotgit = osp.join(worktree_path, ".git")
  229. statbuf = stat(dotgit)
  230. assert_true(statbuf.st_mode & S_IFREG)
  231. gitdir = find_worktree_git_dir(dotgit)
  232. self.assertIsNotNone(gitdir)
  233. statbuf = stat(gitdir)
  234. assert_true(statbuf.st_mode & S_IFDIR)
  235. @skipIf(PY3, 'odd types returned ... maybe figure it out one day')
  236. def test_tree_entries_from_data_with_failing_name_decode_py2(self):
  237. r = tree_entries_from_data(b'100644 \x9f\0aaa')
  238. assert r == [('aaa', 33188, u'\udc9f')], r
  239. @skipIf(not PY3, 'odd types returned ... maybe figure it out one day')
  240. def test_tree_entries_from_data_with_failing_name_decode_py3(self):
  241. r = tree_entries_from_data(b'100644 \x9f\0aaa')
  242. assert r == [(b'aaa', 33188, '\udc9f')], r