test_index.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797
  1. # test_index.py -- Tests for the git index
  2. # Copyright (C) 2008-2009 Jelmer Vernooij <jelmer@jelmer.uk>
  3. #
  4. # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
  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 index."""
  22. import os
  23. import shutil
  24. import stat
  25. import struct
  26. import sys
  27. import tempfile
  28. from io import BytesIO
  29. from dulwich.index import (
  30. Index,
  31. IndexEntry,
  32. SerializedIndexEntry,
  33. _fs_to_tree_path,
  34. _tree_to_fs_path,
  35. build_index_from_tree,
  36. cleanup_mode,
  37. commit_tree,
  38. get_unstaged_changes,
  39. index_entry_from_stat,
  40. read_index,
  41. read_index_dict,
  42. validate_path_element_default,
  43. validate_path_element_ntfs,
  44. write_cache_time,
  45. write_index,
  46. write_index_dict,
  47. )
  48. from dulwich.object_store import MemoryObjectStore
  49. from dulwich.objects import S_IFGITLINK, Blob, Commit, Tree
  50. from dulwich.repo import Repo
  51. from . import TestCase, skipIf
  52. def can_symlink() -> bool:
  53. """Return whether running process can create symlinks."""
  54. if sys.platform != "win32":
  55. # Platforms other than Windows should allow symlinks without issues.
  56. return True
  57. test_source = tempfile.mkdtemp()
  58. test_target = test_source + "can_symlink"
  59. try:
  60. os.symlink(test_source, test_target)
  61. except (NotImplementedError, OSError):
  62. return False
  63. return True
  64. class IndexTestCase(TestCase):
  65. datadir = os.path.join(os.path.dirname(__file__), "../testdata/indexes")
  66. def get_simple_index(self, name):
  67. return Index(os.path.join(self.datadir, name))
  68. class SimpleIndexTestCase(IndexTestCase):
  69. def test_len(self) -> None:
  70. self.assertEqual(1, len(self.get_simple_index("index")))
  71. def test_iter(self) -> None:
  72. self.assertEqual([b"bla"], list(self.get_simple_index("index")))
  73. def test_iter_skip_hash(self) -> None:
  74. self.assertEqual([b"bla"], list(self.get_simple_index("index_skip_hash")))
  75. def test_iterobjects(self) -> None:
  76. self.assertEqual(
  77. [(b"bla", b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", 33188)],
  78. list(self.get_simple_index("index").iterobjects()),
  79. )
  80. def test_getitem(self) -> None:
  81. self.assertEqual(
  82. IndexEntry(
  83. (1230680220, 0),
  84. (1230680220, 0),
  85. 2050,
  86. 3761020,
  87. 33188,
  88. 1000,
  89. 1000,
  90. 0,
  91. b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
  92. 0,
  93. 0,
  94. ),
  95. self.get_simple_index("index")[b"bla"],
  96. )
  97. def test_empty(self) -> None:
  98. i = self.get_simple_index("notanindex")
  99. self.assertEqual(0, len(i))
  100. self.assertFalse(os.path.exists(i._filename))
  101. def test_against_empty_tree(self) -> None:
  102. i = self.get_simple_index("index")
  103. changes = list(i.changes_from_tree(MemoryObjectStore(), None))
  104. self.assertEqual(1, len(changes))
  105. (oldname, newname), (oldmode, newmode), (oldsha, newsha) = changes[0]
  106. self.assertEqual(b"bla", newname)
  107. self.assertEqual(b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", newsha)
  108. class SimpleIndexWriterTestCase(IndexTestCase):
  109. def setUp(self) -> None:
  110. IndexTestCase.setUp(self)
  111. self.tempdir = tempfile.mkdtemp()
  112. def tearDown(self) -> None:
  113. IndexTestCase.tearDown(self)
  114. shutil.rmtree(self.tempdir)
  115. def test_simple_write(self) -> None:
  116. entries = [
  117. (
  118. SerializedIndexEntry(
  119. b"barbla",
  120. (1230680220, 0),
  121. (1230680220, 0),
  122. 2050,
  123. 3761020,
  124. 33188,
  125. 1000,
  126. 1000,
  127. 0,
  128. b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
  129. 0,
  130. 0,
  131. )
  132. )
  133. ]
  134. filename = os.path.join(self.tempdir, "test-simple-write-index")
  135. with open(filename, "wb+") as x:
  136. write_index(x, entries)
  137. with open(filename, "rb") as x:
  138. self.assertEqual(entries, list(read_index(x)))
  139. class ReadIndexDictTests(IndexTestCase):
  140. def setUp(self) -> None:
  141. IndexTestCase.setUp(self)
  142. self.tempdir = tempfile.mkdtemp()
  143. def tearDown(self) -> None:
  144. IndexTestCase.tearDown(self)
  145. shutil.rmtree(self.tempdir)
  146. def test_simple_write(self) -> None:
  147. entries = {
  148. b"barbla": IndexEntry(
  149. (1230680220, 0),
  150. (1230680220, 0),
  151. 2050,
  152. 3761020,
  153. 33188,
  154. 1000,
  155. 1000,
  156. 0,
  157. b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
  158. 0,
  159. 0,
  160. )
  161. }
  162. filename = os.path.join(self.tempdir, "test-simple-write-index")
  163. with open(filename, "wb+") as x:
  164. write_index_dict(x, entries)
  165. with open(filename, "rb") as x:
  166. self.assertEqual(entries, read_index_dict(x))
  167. class CommitTreeTests(TestCase):
  168. def setUp(self) -> None:
  169. super().setUp()
  170. self.store = MemoryObjectStore()
  171. def test_single_blob(self) -> None:
  172. blob = Blob()
  173. blob.data = b"foo"
  174. self.store.add_object(blob)
  175. blobs = [(b"bla", blob.id, stat.S_IFREG)]
  176. rootid = commit_tree(self.store, blobs)
  177. self.assertEqual(rootid, b"1a1e80437220f9312e855c37ac4398b68e5c1d50")
  178. self.assertEqual((stat.S_IFREG, blob.id), self.store[rootid][b"bla"])
  179. self.assertEqual({rootid, blob.id}, set(self.store._data.keys()))
  180. def test_nested(self) -> None:
  181. blob = Blob()
  182. blob.data = b"foo"
  183. self.store.add_object(blob)
  184. blobs = [(b"bla/bar", blob.id, stat.S_IFREG)]
  185. rootid = commit_tree(self.store, blobs)
  186. self.assertEqual(rootid, b"d92b959b216ad0d044671981196781b3258fa537")
  187. dirid = self.store[rootid][b"bla"][1]
  188. self.assertEqual(dirid, b"c1a1deb9788150829579a8b4efa6311e7b638650")
  189. self.assertEqual((stat.S_IFDIR, dirid), self.store[rootid][b"bla"])
  190. self.assertEqual((stat.S_IFREG, blob.id), self.store[dirid][b"bar"])
  191. self.assertEqual({rootid, dirid, blob.id}, set(self.store._data.keys()))
  192. class CleanupModeTests(TestCase):
  193. def assertModeEqual(self, expected, got) -> None:
  194. self.assertEqual(expected, got, f"{expected:o} != {got:o}")
  195. def test_file(self) -> None:
  196. self.assertModeEqual(0o100644, cleanup_mode(0o100000))
  197. def test_executable(self) -> None:
  198. self.assertModeEqual(0o100755, cleanup_mode(0o100711))
  199. self.assertModeEqual(0o100755, cleanup_mode(0o100700))
  200. def test_symlink(self) -> None:
  201. self.assertModeEqual(0o120000, cleanup_mode(0o120711))
  202. def test_dir(self) -> None:
  203. self.assertModeEqual(0o040000, cleanup_mode(0o40531))
  204. def test_submodule(self) -> None:
  205. self.assertModeEqual(0o160000, cleanup_mode(0o160744))
  206. class WriteCacheTimeTests(TestCase):
  207. def test_write_string(self) -> None:
  208. f = BytesIO()
  209. self.assertRaises(TypeError, write_cache_time, f, "foo")
  210. def test_write_int(self) -> None:
  211. f = BytesIO()
  212. write_cache_time(f, 434343)
  213. self.assertEqual(struct.pack(">LL", 434343, 0), f.getvalue())
  214. def test_write_tuple(self) -> None:
  215. f = BytesIO()
  216. write_cache_time(f, (434343, 21))
  217. self.assertEqual(struct.pack(">LL", 434343, 21), f.getvalue())
  218. def test_write_float(self) -> None:
  219. f = BytesIO()
  220. write_cache_time(f, 434343.000000021)
  221. self.assertEqual(struct.pack(">LL", 434343, 21), f.getvalue())
  222. class IndexEntryFromStatTests(TestCase):
  223. def test_simple(self) -> None:
  224. st = os.stat_result(
  225. (
  226. 16877,
  227. 131078,
  228. 64769,
  229. 154,
  230. 1000,
  231. 1000,
  232. 12288,
  233. 1323629595,
  234. 1324180496,
  235. 1324180496,
  236. )
  237. )
  238. entry = index_entry_from_stat(st, b"22" * 20)
  239. self.assertEqual(
  240. entry,
  241. IndexEntry(
  242. 1324180496,
  243. 1324180496,
  244. 64769,
  245. 131078,
  246. 16384,
  247. 1000,
  248. 1000,
  249. 12288,
  250. b"2222222222222222222222222222222222222222",
  251. 0,
  252. 0,
  253. ),
  254. )
  255. def test_override_mode(self) -> None:
  256. st = os.stat_result(
  257. (
  258. stat.S_IFREG + 0o644,
  259. 131078,
  260. 64769,
  261. 154,
  262. 1000,
  263. 1000,
  264. 12288,
  265. 1323629595,
  266. 1324180496,
  267. 1324180496,
  268. )
  269. )
  270. entry = index_entry_from_stat(st, b"22" * 20, mode=stat.S_IFREG + 0o755)
  271. self.assertEqual(
  272. entry,
  273. IndexEntry(
  274. 1324180496,
  275. 1324180496,
  276. 64769,
  277. 131078,
  278. 33261,
  279. 1000,
  280. 1000,
  281. 12288,
  282. b"2222222222222222222222222222222222222222",
  283. 0,
  284. 0,
  285. ),
  286. )
  287. class BuildIndexTests(TestCase):
  288. def assertReasonableIndexEntry(self, index_entry, mode, filesize, sha) -> None:
  289. self.assertEqual(index_entry.mode, mode) # mode
  290. self.assertEqual(index_entry.size, filesize) # filesize
  291. self.assertEqual(index_entry.sha, sha) # sha
  292. def assertFileContents(self, path, contents, symlink=False) -> None:
  293. if symlink:
  294. self.assertEqual(os.readlink(path), contents)
  295. else:
  296. with open(path, "rb") as f:
  297. self.assertEqual(f.read(), contents)
  298. def test_empty(self) -> None:
  299. repo_dir = tempfile.mkdtemp()
  300. self.addCleanup(shutil.rmtree, repo_dir)
  301. with Repo.init(repo_dir) as repo:
  302. tree = Tree()
  303. repo.object_store.add_object(tree)
  304. build_index_from_tree(
  305. repo.path, repo.index_path(), repo.object_store, tree.id
  306. )
  307. # Verify index entries
  308. index = repo.open_index()
  309. self.assertEqual(len(index), 0)
  310. # Verify no files
  311. self.assertEqual([".git"], os.listdir(repo.path))
  312. def test_git_dir(self) -> None:
  313. repo_dir = tempfile.mkdtemp()
  314. self.addCleanup(shutil.rmtree, repo_dir)
  315. with Repo.init(repo_dir) as repo:
  316. # Populate repo
  317. filea = Blob.from_string(b"file a")
  318. filee = Blob.from_string(b"d")
  319. tree = Tree()
  320. tree[b".git/a"] = (stat.S_IFREG | 0o644, filea.id)
  321. tree[b"c/e"] = (stat.S_IFREG | 0o644, filee.id)
  322. repo.object_store.add_objects([(o, None) for o in [filea, filee, tree]])
  323. build_index_from_tree(
  324. repo.path, repo.index_path(), repo.object_store, tree.id
  325. )
  326. # Verify index entries
  327. index = repo.open_index()
  328. self.assertEqual(len(index), 1)
  329. # filea
  330. apath = os.path.join(repo.path, ".git", "a")
  331. self.assertFalse(os.path.exists(apath))
  332. # filee
  333. epath = os.path.join(repo.path, "c", "e")
  334. self.assertTrue(os.path.exists(epath))
  335. self.assertReasonableIndexEntry(
  336. index[b"c/e"], stat.S_IFREG | 0o644, 1, filee.id
  337. )
  338. self.assertFileContents(epath, b"d")
  339. def test_nonempty(self) -> None:
  340. repo_dir = tempfile.mkdtemp()
  341. self.addCleanup(shutil.rmtree, repo_dir)
  342. with Repo.init(repo_dir) as repo:
  343. # Populate repo
  344. filea = Blob.from_string(b"file a")
  345. fileb = Blob.from_string(b"file b")
  346. filed = Blob.from_string(b"file d")
  347. tree = Tree()
  348. tree[b"a"] = (stat.S_IFREG | 0o644, filea.id)
  349. tree[b"b"] = (stat.S_IFREG | 0o644, fileb.id)
  350. tree[b"c/d"] = (stat.S_IFREG | 0o644, filed.id)
  351. repo.object_store.add_objects(
  352. [(o, None) for o in [filea, fileb, filed, tree]]
  353. )
  354. build_index_from_tree(
  355. repo.path, repo.index_path(), repo.object_store, tree.id
  356. )
  357. # Verify index entries
  358. index = repo.open_index()
  359. self.assertEqual(len(index), 3)
  360. # filea
  361. apath = os.path.join(repo.path, "a")
  362. self.assertTrue(os.path.exists(apath))
  363. self.assertReasonableIndexEntry(
  364. index[b"a"], stat.S_IFREG | 0o644, 6, filea.id
  365. )
  366. self.assertFileContents(apath, b"file a")
  367. # fileb
  368. bpath = os.path.join(repo.path, "b")
  369. self.assertTrue(os.path.exists(bpath))
  370. self.assertReasonableIndexEntry(
  371. index[b"b"], stat.S_IFREG | 0o644, 6, fileb.id
  372. )
  373. self.assertFileContents(bpath, b"file b")
  374. # filed
  375. dpath = os.path.join(repo.path, "c", "d")
  376. self.assertTrue(os.path.exists(dpath))
  377. self.assertReasonableIndexEntry(
  378. index[b"c/d"], stat.S_IFREG | 0o644, 6, filed.id
  379. )
  380. self.assertFileContents(dpath, b"file d")
  381. # Verify no extra files
  382. self.assertEqual([".git", "a", "b", "c"], sorted(os.listdir(repo.path)))
  383. self.assertEqual(["d"], sorted(os.listdir(os.path.join(repo.path, "c"))))
  384. @skipIf(not getattr(os, "sync", None), "Requires sync support")
  385. def test_norewrite(self) -> None:
  386. repo_dir = tempfile.mkdtemp()
  387. self.addCleanup(shutil.rmtree, repo_dir)
  388. with Repo.init(repo_dir) as repo:
  389. # Populate repo
  390. filea = Blob.from_string(b"file a")
  391. filea_path = os.path.join(repo_dir, "a")
  392. tree = Tree()
  393. tree[b"a"] = (stat.S_IFREG | 0o644, filea.id)
  394. repo.object_store.add_objects([(o, None) for o in [filea, tree]])
  395. # First Write
  396. build_index_from_tree(
  397. repo.path, repo.index_path(), repo.object_store, tree.id
  398. )
  399. # Use sync as metadata can be cached on some FS
  400. os.sync()
  401. mtime = os.stat(filea_path).st_mtime
  402. # Test Rewrite
  403. build_index_from_tree(
  404. repo.path, repo.index_path(), repo.object_store, tree.id
  405. )
  406. os.sync()
  407. self.assertEqual(mtime, os.stat(filea_path).st_mtime)
  408. # Modify content
  409. with open(filea_path, "wb") as fh:
  410. fh.write(b"test a")
  411. os.sync()
  412. mtime = os.stat(filea_path).st_mtime
  413. # Test rewrite
  414. build_index_from_tree(
  415. repo.path, repo.index_path(), repo.object_store, tree.id
  416. )
  417. os.sync()
  418. with open(filea_path, "rb") as fh:
  419. self.assertEqual(b"file a", fh.read())
  420. @skipIf(not can_symlink(), "Requires symlink support")
  421. def test_symlink(self) -> None:
  422. repo_dir = tempfile.mkdtemp()
  423. self.addCleanup(shutil.rmtree, repo_dir)
  424. with Repo.init(repo_dir) as repo:
  425. # Populate repo
  426. filed = Blob.from_string(b"file d")
  427. filee = Blob.from_string(b"d")
  428. tree = Tree()
  429. tree[b"c/d"] = (stat.S_IFREG | 0o644, filed.id)
  430. tree[b"c/e"] = (stat.S_IFLNK, filee.id) # symlink
  431. repo.object_store.add_objects([(o, None) for o in [filed, filee, tree]])
  432. build_index_from_tree(
  433. repo.path, repo.index_path(), repo.object_store, tree.id
  434. )
  435. # Verify index entries
  436. index = repo.open_index()
  437. # symlink to d
  438. epath = os.path.join(repo.path, "c", "e")
  439. self.assertTrue(os.path.exists(epath))
  440. self.assertReasonableIndexEntry(
  441. index[b"c/e"],
  442. stat.S_IFLNK,
  443. 0 if sys.platform == "win32" else 1,
  444. filee.id,
  445. )
  446. self.assertFileContents(epath, "d", symlink=True)
  447. def test_no_decode_encode(self) -> None:
  448. repo_dir = tempfile.mkdtemp()
  449. repo_dir_bytes = os.fsencode(repo_dir)
  450. self.addCleanup(shutil.rmtree, repo_dir)
  451. with Repo.init(repo_dir) as repo:
  452. # Populate repo
  453. file = Blob.from_string(b"foo")
  454. tree = Tree()
  455. latin1_name = "À".encode("latin1")
  456. try:
  457. latin1_path = os.path.join(repo_dir_bytes, latin1_name)
  458. except UnicodeDecodeError:
  459. self.skipTest("can not decode as latin1")
  460. utf8_name = "À".encode()
  461. utf8_path = os.path.join(repo_dir_bytes, utf8_name)
  462. tree[latin1_name] = (stat.S_IFREG | 0o644, file.id)
  463. tree[utf8_name] = (stat.S_IFREG | 0o644, file.id)
  464. repo.object_store.add_objects([(o, None) for o in [file, tree]])
  465. try:
  466. build_index_from_tree(
  467. repo.path, repo.index_path(), repo.object_store, tree.id
  468. )
  469. except OSError as e:
  470. if e.errno == 92 and sys.platform == "darwin":
  471. # Our filename isn't supported by the platform :(
  472. self.skipTest(f"can not write filename {e.filename!r}")
  473. else:
  474. raise
  475. except UnicodeDecodeError:
  476. # This happens e.g. with python3.6 on Windows.
  477. # It implicitly decodes using utf8, which doesn't work.
  478. self.skipTest("can not implicitly convert as utf8")
  479. # Verify index entries
  480. index = repo.open_index()
  481. self.assertIn(latin1_name, index)
  482. self.assertIn(utf8_name, index)
  483. self.assertTrue(os.path.exists(latin1_path))
  484. self.assertTrue(os.path.exists(utf8_path))
  485. def test_git_submodule(self) -> None:
  486. repo_dir = tempfile.mkdtemp()
  487. self.addCleanup(shutil.rmtree, repo_dir)
  488. with Repo.init(repo_dir) as repo:
  489. filea = Blob.from_string(b"file alalala")
  490. subtree = Tree()
  491. subtree[b"a"] = (stat.S_IFREG | 0o644, filea.id)
  492. c = Commit()
  493. c.tree = subtree.id
  494. c.committer = c.author = b"Somebody <somebody@example.com>"
  495. c.commit_time = c.author_time = 42342
  496. c.commit_timezone = c.author_timezone = 0
  497. c.parents = []
  498. c.message = b"Subcommit"
  499. tree = Tree()
  500. tree[b"c"] = (S_IFGITLINK, c.id)
  501. repo.object_store.add_objects([(o, None) for o in [tree]])
  502. build_index_from_tree(
  503. repo.path, repo.index_path(), repo.object_store, tree.id
  504. )
  505. # Verify index entries
  506. index = repo.open_index()
  507. self.assertEqual(len(index), 1)
  508. # filea
  509. apath = os.path.join(repo.path, "c/a")
  510. self.assertFalse(os.path.exists(apath))
  511. # dir c
  512. cpath = os.path.join(repo.path, "c")
  513. self.assertTrue(os.path.isdir(cpath))
  514. self.assertEqual(index[b"c"].mode, S_IFGITLINK) # mode
  515. self.assertEqual(index[b"c"].sha, c.id) # sha
  516. def test_git_submodule_exists(self) -> None:
  517. repo_dir = tempfile.mkdtemp()
  518. self.addCleanup(shutil.rmtree, repo_dir)
  519. with Repo.init(repo_dir) as repo:
  520. filea = Blob.from_string(b"file alalala")
  521. subtree = Tree()
  522. subtree[b"a"] = (stat.S_IFREG | 0o644, filea.id)
  523. c = Commit()
  524. c.tree = subtree.id
  525. c.committer = c.author = b"Somebody <somebody@example.com>"
  526. c.commit_time = c.author_time = 42342
  527. c.commit_timezone = c.author_timezone = 0
  528. c.parents = []
  529. c.message = b"Subcommit"
  530. tree = Tree()
  531. tree[b"c"] = (S_IFGITLINK, c.id)
  532. os.mkdir(os.path.join(repo_dir, "c"))
  533. repo.object_store.add_objects([(o, None) for o in [tree]])
  534. build_index_from_tree(
  535. repo.path, repo.index_path(), repo.object_store, tree.id
  536. )
  537. # Verify index entries
  538. index = repo.open_index()
  539. self.assertEqual(len(index), 1)
  540. # filea
  541. apath = os.path.join(repo.path, "c/a")
  542. self.assertFalse(os.path.exists(apath))
  543. # dir c
  544. cpath = os.path.join(repo.path, "c")
  545. self.assertTrue(os.path.isdir(cpath))
  546. self.assertEqual(index[b"c"].mode, S_IFGITLINK) # mode
  547. self.assertEqual(index[b"c"].sha, c.id) # sha
  548. class GetUnstagedChangesTests(TestCase):
  549. def test_get_unstaged_changes(self) -> None:
  550. """Unit test for get_unstaged_changes."""
  551. repo_dir = tempfile.mkdtemp()
  552. self.addCleanup(shutil.rmtree, repo_dir)
  553. with Repo.init(repo_dir) as repo:
  554. # Commit a dummy file then modify it
  555. foo1_fullpath = os.path.join(repo_dir, "foo1")
  556. with open(foo1_fullpath, "wb") as f:
  557. f.write(b"origstuff")
  558. foo2_fullpath = os.path.join(repo_dir, "foo2")
  559. with open(foo2_fullpath, "wb") as f:
  560. f.write(b"origstuff")
  561. repo.stage(["foo1", "foo2"])
  562. repo.do_commit(
  563. b"test status",
  564. author=b"author <email>",
  565. committer=b"committer <email>",
  566. )
  567. with open(foo1_fullpath, "wb") as f:
  568. f.write(b"newstuff")
  569. # modify access and modify time of path
  570. os.utime(foo1_fullpath, (0, 0))
  571. changes = get_unstaged_changes(repo.open_index(), repo_dir)
  572. self.assertEqual(list(changes), [b"foo1"])
  573. def test_get_unstaged_deleted_changes(self) -> None:
  574. """Unit test for get_unstaged_changes."""
  575. repo_dir = tempfile.mkdtemp()
  576. self.addCleanup(shutil.rmtree, repo_dir)
  577. with Repo.init(repo_dir) as repo:
  578. # Commit a dummy file then remove it
  579. foo1_fullpath = os.path.join(repo_dir, "foo1")
  580. with open(foo1_fullpath, "wb") as f:
  581. f.write(b"origstuff")
  582. repo.stage(["foo1"])
  583. repo.do_commit(
  584. b"test status",
  585. author=b"author <email>",
  586. committer=b"committer <email>",
  587. )
  588. os.unlink(foo1_fullpath)
  589. changes = get_unstaged_changes(repo.open_index(), repo_dir)
  590. self.assertEqual(list(changes), [b"foo1"])
  591. def test_get_unstaged_changes_removed_replaced_by_directory(self) -> None:
  592. """Unit test for get_unstaged_changes."""
  593. repo_dir = tempfile.mkdtemp()
  594. self.addCleanup(shutil.rmtree, repo_dir)
  595. with Repo.init(repo_dir) as repo:
  596. # Commit a dummy file then modify it
  597. foo1_fullpath = os.path.join(repo_dir, "foo1")
  598. with open(foo1_fullpath, "wb") as f:
  599. f.write(b"origstuff")
  600. repo.stage(["foo1"])
  601. repo.do_commit(
  602. b"test status",
  603. author=b"author <email>",
  604. committer=b"committer <email>",
  605. )
  606. os.remove(foo1_fullpath)
  607. os.mkdir(foo1_fullpath)
  608. changes = get_unstaged_changes(repo.open_index(), repo_dir)
  609. self.assertEqual(list(changes), [b"foo1"])
  610. @skipIf(not can_symlink(), "Requires symlink support")
  611. def test_get_unstaged_changes_removed_replaced_by_link(self) -> None:
  612. """Unit test for get_unstaged_changes."""
  613. repo_dir = tempfile.mkdtemp()
  614. self.addCleanup(shutil.rmtree, repo_dir)
  615. with Repo.init(repo_dir) as repo:
  616. # Commit a dummy file then modify it
  617. foo1_fullpath = os.path.join(repo_dir, "foo1")
  618. with open(foo1_fullpath, "wb") as f:
  619. f.write(b"origstuff")
  620. repo.stage(["foo1"])
  621. repo.do_commit(
  622. b"test status",
  623. author=b"author <email>",
  624. committer=b"committer <email>",
  625. )
  626. os.remove(foo1_fullpath)
  627. os.symlink(os.path.dirname(foo1_fullpath), foo1_fullpath)
  628. changes = get_unstaged_changes(repo.open_index(), repo_dir)
  629. self.assertEqual(list(changes), [b"foo1"])
  630. class TestValidatePathElement(TestCase):
  631. def test_default(self) -> None:
  632. self.assertTrue(validate_path_element_default(b"bla"))
  633. self.assertTrue(validate_path_element_default(b".bla"))
  634. self.assertFalse(validate_path_element_default(b".git"))
  635. self.assertFalse(validate_path_element_default(b".giT"))
  636. self.assertFalse(validate_path_element_default(b".."))
  637. self.assertTrue(validate_path_element_default(b"git~1"))
  638. def test_ntfs(self) -> None:
  639. self.assertTrue(validate_path_element_ntfs(b"bla"))
  640. self.assertTrue(validate_path_element_ntfs(b".bla"))
  641. self.assertFalse(validate_path_element_ntfs(b".git"))
  642. self.assertFalse(validate_path_element_ntfs(b".giT"))
  643. self.assertFalse(validate_path_element_ntfs(b".."))
  644. self.assertFalse(validate_path_element_ntfs(b"git~1"))
  645. class TestTreeFSPathConversion(TestCase):
  646. def test_tree_to_fs_path(self) -> None:
  647. tree_path = "délwíçh/foo".encode()
  648. fs_path = _tree_to_fs_path(b"/prefix/path", tree_path)
  649. self.assertEqual(
  650. fs_path,
  651. os.fsencode(os.path.join("/prefix/path", "délwíçh", "foo")),
  652. )
  653. def test_fs_to_tree_path_str(self) -> None:
  654. fs_path = os.path.join(os.path.join("délwíçh", "foo"))
  655. tree_path = _fs_to_tree_path(fs_path)
  656. self.assertEqual(tree_path, "délwíçh/foo".encode())
  657. def test_fs_to_tree_path_bytes(self) -> None:
  658. fs_path = os.path.join(os.fsencode(os.path.join("délwíçh", "foo")))
  659. tree_path = _fs_to_tree_path(fs_path)
  660. self.assertEqual(tree_path, "délwíçh/foo".encode())