test_attrs.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  1. # test_attrs.py -- tests for gitattributes
  2. # Copyright (C) 2019-2020 Collabora Ltd
  3. # Copyright (C) 2019-2020 Andrej Shadura <andrew.shadura@collabora.co.uk>
  4. #
  5. # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
  6. # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
  7. # General Public License as public by the Free Software Foundation; version 2.0
  8. # or (at your option) any later version. You can redistribute it and/or
  9. # modify it under the terms of either of these two licenses.
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. #
  17. # You should have received a copy of the licenses; if not, see
  18. # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
  19. # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
  20. # License, Version 2.0.
  21. #
  22. """Tests for gitattributes parsing and matching."""
  23. import os
  24. import tempfile
  25. from io import BytesIO
  26. from dulwich.attrs import (
  27. GitAttributes,
  28. Pattern,
  29. _parse_attr,
  30. match_path,
  31. parse_git_attributes,
  32. parse_gitattributes_file,
  33. read_gitattributes,
  34. )
  35. from . import TestCase
  36. class ParseAttrTests(TestCase):
  37. """Test the _parse_attr function."""
  38. def test_parse_set_attr(self):
  39. """Test parsing a set attribute."""
  40. name, value = _parse_attr(b"text")
  41. self.assertEqual(name, b"text")
  42. self.assertEqual(value, True)
  43. def test_parse_unset_attr(self):
  44. """Test parsing an unset attribute."""
  45. name, value = _parse_attr(b"-text")
  46. self.assertEqual(name, b"text")
  47. self.assertEqual(value, False)
  48. def test_parse_unspecified_attr(self):
  49. """Test parsing an unspecified attribute."""
  50. name, value = _parse_attr(b"!text")
  51. self.assertEqual(name, b"text")
  52. self.assertEqual(value, None)
  53. def test_parse_value_attr(self):
  54. """Test parsing an attribute with a value."""
  55. name, value = _parse_attr(b"diff=python")
  56. self.assertEqual(name, b"diff")
  57. self.assertEqual(value, b"python")
  58. def test_parse_value_with_equals(self):
  59. """Test parsing an attribute value containing equals."""
  60. name, value = _parse_attr(b"filter=foo=bar")
  61. self.assertEqual(name, b"filter")
  62. self.assertEqual(value, b"foo=bar")
  63. class ParseGitAttributesTests(TestCase):
  64. """Test the parse_git_attributes function."""
  65. def test_parse_empty(self):
  66. """Test parsing empty file."""
  67. attrs = list(parse_git_attributes(BytesIO(b"")))
  68. self.assertEqual(attrs, [])
  69. def test_parse_comments(self):
  70. """Test parsing file with comments."""
  71. content = b"""# This is a comment
  72. # Another comment
  73. """
  74. attrs = list(parse_git_attributes(BytesIO(content)))
  75. self.assertEqual(attrs, [])
  76. def test_parse_single_pattern(self):
  77. """Test parsing single pattern."""
  78. content = b"*.txt text"
  79. attrs = list(parse_git_attributes(BytesIO(content)))
  80. self.assertEqual(len(attrs), 1)
  81. pattern, attributes = attrs[0]
  82. self.assertEqual(pattern, b"*.txt")
  83. self.assertEqual(attributes, {b"text": True})
  84. def test_parse_multiple_attributes(self):
  85. """Test parsing pattern with multiple attributes."""
  86. content = b"*.jpg -text -diff binary"
  87. attrs = list(parse_git_attributes(BytesIO(content)))
  88. self.assertEqual(len(attrs), 1)
  89. pattern, attributes = attrs[0]
  90. self.assertEqual(pattern, b"*.jpg")
  91. self.assertEqual(attributes, {b"text": False, b"diff": False, b"binary": True})
  92. def test_parse_attributes_with_values(self):
  93. """Test parsing attributes with values."""
  94. content = b"*.c filter=indent diff=cpp text"
  95. attrs = list(parse_git_attributes(BytesIO(content)))
  96. self.assertEqual(len(attrs), 1)
  97. pattern, attributes = attrs[0]
  98. self.assertEqual(pattern, b"*.c")
  99. self.assertEqual(
  100. attributes, {b"filter": b"indent", b"diff": b"cpp", b"text": True}
  101. )
  102. def test_parse_multiple_patterns(self):
  103. """Test parsing multiple patterns."""
  104. content = b"""*.txt text
  105. *.jpg -text binary
  106. *.py diff=python
  107. """
  108. attrs = list(parse_git_attributes(BytesIO(content)))
  109. self.assertEqual(len(attrs), 3)
  110. # First pattern
  111. pattern, attributes = attrs[0]
  112. self.assertEqual(pattern, b"*.txt")
  113. self.assertEqual(attributes, {b"text": True})
  114. # Second pattern
  115. pattern, attributes = attrs[1]
  116. self.assertEqual(pattern, b"*.jpg")
  117. self.assertEqual(attributes, {b"text": False, b"binary": True})
  118. # Third pattern
  119. pattern, attributes = attrs[2]
  120. self.assertEqual(pattern, b"*.py")
  121. self.assertEqual(attributes, {b"diff": b"python"})
  122. def test_parse_git_lfs_example(self):
  123. """Test parsing Git LFS example from docstring."""
  124. content = b"""*.tar.* filter=lfs diff=lfs merge=lfs -text
  125. # store signatures in Git
  126. *.tar.*.asc -filter -diff merge=binary -text
  127. # store .dsc verbatim
  128. *.dsc -filter !diff merge=binary !text
  129. """
  130. attrs = list(parse_git_attributes(BytesIO(content)))
  131. self.assertEqual(len(attrs), 3)
  132. # LFS pattern
  133. pattern, attributes = attrs[0]
  134. self.assertEqual(pattern, b"*.tar.*")
  135. self.assertEqual(
  136. attributes,
  137. {b"filter": b"lfs", b"diff": b"lfs", b"merge": b"lfs", b"text": False},
  138. )
  139. # Signatures pattern
  140. pattern, attributes = attrs[1]
  141. self.assertEqual(pattern, b"*.tar.*.asc")
  142. self.assertEqual(
  143. attributes,
  144. {b"filter": False, b"diff": False, b"merge": b"binary", b"text": False},
  145. )
  146. # .dsc pattern
  147. pattern, attributes = attrs[2]
  148. self.assertEqual(pattern, b"*.dsc")
  149. self.assertEqual(
  150. attributes,
  151. {b"filter": False, b"diff": None, b"merge": b"binary", b"text": None},
  152. )
  153. class PatternTests(TestCase):
  154. """Test the Pattern class."""
  155. def test_exact_match(self):
  156. """Test exact filename matching without path."""
  157. pattern = Pattern(b"README.txt")
  158. self.assertTrue(pattern.match(b"README.txt"))
  159. self.assertFalse(pattern.match(b"readme.txt"))
  160. # Patterns without slashes match at any level
  161. self.assertTrue(pattern.match(b"src/README.txt"))
  162. def test_wildcard_extension(self):
  163. """Test wildcard extension matching."""
  164. pattern = Pattern(b"*.txt")
  165. self.assertTrue(pattern.match(b"file.txt"))
  166. self.assertTrue(pattern.match(b"README.txt"))
  167. self.assertTrue(pattern.match(b"src/doc.txt"))
  168. self.assertFalse(pattern.match(b"file.txt.bak"))
  169. self.assertFalse(pattern.match(b"file.md"))
  170. def test_wildcard_in_name(self):
  171. """Test wildcard in filename."""
  172. pattern = Pattern(b"test_*.py")
  173. self.assertTrue(pattern.match(b"test_foo.py"))
  174. self.assertTrue(pattern.match(b"test_bar.py"))
  175. self.assertTrue(pattern.match(b"src/test_baz.py"))
  176. self.assertFalse(pattern.match(b"test.py"))
  177. self.assertFalse(pattern.match(b"tests.py"))
  178. def test_question_mark(self):
  179. """Test question mark matching."""
  180. pattern = Pattern(b"file?.txt")
  181. self.assertTrue(pattern.match(b"file1.txt"))
  182. self.assertTrue(pattern.match(b"fileA.txt"))
  183. self.assertFalse(pattern.match(b"file.txt"))
  184. self.assertFalse(pattern.match(b"file10.txt"))
  185. def test_character_class(self):
  186. """Test character class matching."""
  187. pattern = Pattern(b"file[0-9].txt")
  188. self.assertTrue(pattern.match(b"file0.txt"))
  189. self.assertTrue(pattern.match(b"file5.txt"))
  190. self.assertTrue(pattern.match(b"file9.txt"))
  191. self.assertFalse(pattern.match(b"fileA.txt"))
  192. self.assertFalse(pattern.match(b"file10.txt"))
  193. def test_negated_character_class(self):
  194. """Test negated character class."""
  195. pattern = Pattern(b"file[!0-9].txt")
  196. self.assertTrue(pattern.match(b"fileA.txt"))
  197. self.assertTrue(pattern.match(b"file_.txt"))
  198. self.assertFalse(pattern.match(b"file0.txt"))
  199. self.assertFalse(pattern.match(b"file5.txt"))
  200. def test_directory_pattern(self):
  201. """Test pattern with directory."""
  202. pattern = Pattern(b"src/*.py")
  203. self.assertTrue(pattern.match(b"src/foo.py"))
  204. self.assertTrue(pattern.match(b"src/bar.py"))
  205. self.assertFalse(pattern.match(b"foo.py"))
  206. self.assertFalse(pattern.match(b"src/sub/foo.py"))
  207. self.assertFalse(pattern.match(b"other/foo.py"))
  208. def test_double_asterisk(self):
  209. """Test double asterisk matching."""
  210. pattern = Pattern(b"**/foo.txt")
  211. self.assertTrue(pattern.match(b"foo.txt"))
  212. self.assertTrue(pattern.match(b"src/foo.txt"))
  213. self.assertTrue(pattern.match(b"src/sub/foo.txt"))
  214. self.assertTrue(pattern.match(b"a/b/c/foo.txt"))
  215. def test_double_asterisk_middle(self):
  216. """Test double asterisk in middle."""
  217. pattern = Pattern(b"src/**/foo.txt")
  218. self.assertTrue(pattern.match(b"src/foo.txt"))
  219. self.assertTrue(pattern.match(b"src/sub/foo.txt"))
  220. self.assertTrue(pattern.match(b"src/a/b/foo.txt"))
  221. self.assertFalse(pattern.match(b"foo.txt"))
  222. self.assertFalse(pattern.match(b"other/foo.txt"))
  223. def test_leading_slash(self):
  224. """Test pattern with leading slash."""
  225. pattern = Pattern(b"/README.txt")
  226. self.assertTrue(pattern.match(b"README.txt"))
  227. self.assertTrue(pattern.match(b"/README.txt"))
  228. self.assertFalse(pattern.match(b"src/README.txt"))
  229. class MatchPathTests(TestCase):
  230. """Test the match_path function."""
  231. def test_no_matches(self):
  232. """Test when no patterns match."""
  233. patterns = [
  234. (Pattern(b"*.txt"), {b"text": True}),
  235. (Pattern(b"*.jpg"), {b"binary": True}),
  236. ]
  237. attrs = match_path(patterns, b"file.py")
  238. self.assertEqual(attrs, {})
  239. def test_single_match(self):
  240. """Test single pattern match."""
  241. patterns = [
  242. (Pattern(b"*.txt"), {b"text": True}),
  243. (Pattern(b"*.jpg"), {b"binary": True}),
  244. ]
  245. attrs = match_path(patterns, b"README.txt")
  246. self.assertEqual(attrs, {b"text": True})
  247. def test_multiple_matches_override(self):
  248. """Test that later patterns override earlier ones."""
  249. patterns = [
  250. (Pattern(b"*"), {b"text": True}),
  251. (Pattern(b"*.jpg"), {b"text": False, b"binary": True}),
  252. ]
  253. attrs = match_path(patterns, b"image.jpg")
  254. self.assertEqual(attrs, {b"text": False, b"binary": True})
  255. def test_unspecified_removes_attribute(self):
  256. """Test that unspecified (None) removes attributes."""
  257. patterns = [
  258. (Pattern(b"*"), {b"text": True, b"diff": True}),
  259. (Pattern(b"*.bin"), {b"text": None, b"binary": True}),
  260. ]
  261. attrs = match_path(patterns, b"file.bin")
  262. self.assertEqual(attrs, {b"diff": True, b"binary": True})
  263. # 'text' should be removed
  264. self.assertNotIn(b"text", attrs)
  265. class FileOperationsTests(TestCase):
  266. """Test file operations."""
  267. def test_parse_gitattributes_file(self):
  268. """Test parsing a gitattributes file."""
  269. with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
  270. f.write(b"*.txt text\n")
  271. f.write(b"*.jpg -text binary\n")
  272. temp_path = f.name
  273. self.addCleanup(os.unlink, temp_path)
  274. patterns = parse_gitattributes_file(temp_path)
  275. self.assertEqual(len(patterns), 2)
  276. # Check first pattern
  277. pattern, attrs = patterns[0]
  278. self.assertEqual(pattern.pattern, b"*.txt")
  279. self.assertEqual(attrs, {b"text": True})
  280. # Check second pattern
  281. pattern, attrs = patterns[1]
  282. self.assertEqual(pattern.pattern, b"*.jpg")
  283. self.assertEqual(attrs, {b"text": False, b"binary": True})
  284. def test_read_gitattributes(self):
  285. """Test reading gitattributes from a directory."""
  286. with tempfile.TemporaryDirectory() as tmpdir:
  287. # Create .gitattributes file
  288. attrs_path = os.path.join(tmpdir, ".gitattributes")
  289. with open(attrs_path, "wb") as f:
  290. f.write(b"*.py diff=python\n")
  291. patterns = read_gitattributes(tmpdir)
  292. self.assertEqual(len(patterns), 1)
  293. pattern, attrs = patterns[0]
  294. self.assertEqual(pattern.pattern, b"*.py")
  295. self.assertEqual(attrs, {b"diff": b"python"})
  296. def test_read_gitattributes_missing(self):
  297. """Test reading gitattributes when file doesn't exist."""
  298. with tempfile.TemporaryDirectory() as tmpdir:
  299. patterns = read_gitattributes(tmpdir)
  300. self.assertEqual(patterns, [])
  301. class GitAttributesTests(TestCase):
  302. """Test the GitAttributes class."""
  303. def test_empty_gitattributes(self):
  304. """Test GitAttributes with no patterns."""
  305. ga = GitAttributes()
  306. attrs = ga.match_path(b"file.txt")
  307. self.assertEqual(attrs, {})
  308. self.assertEqual(len(ga), 0)
  309. def test_gitattributes_with_patterns(self):
  310. """Test GitAttributes with patterns."""
  311. patterns = [
  312. (Pattern(b"*.txt"), {b"text": True}),
  313. (Pattern(b"*.jpg"), {b"binary": True, b"text": False}),
  314. ]
  315. ga = GitAttributes(patterns)
  316. # Test matching .txt file
  317. attrs = ga.match_path(b"README.txt")
  318. self.assertEqual(attrs, {b"text": True})
  319. # Test matching .jpg file
  320. attrs = ga.match_path(b"image.jpg")
  321. self.assertEqual(attrs, {b"binary": True, b"text": False})
  322. # Test non-matching file
  323. attrs = ga.match_path(b"script.py")
  324. self.assertEqual(attrs, {})
  325. self.assertEqual(len(ga), 2)
  326. def test_add_patterns(self):
  327. """Test adding patterns to GitAttributes."""
  328. ga = GitAttributes()
  329. self.assertEqual(len(ga), 0)
  330. # Add patterns
  331. ga.add_patterns(
  332. [
  333. (Pattern(b"*.py"), {b"diff": b"python"}),
  334. (Pattern(b"*.md"), {b"text": True}),
  335. ]
  336. )
  337. self.assertEqual(len(ga), 2)
  338. attrs = ga.match_path(b"test.py")
  339. self.assertEqual(attrs, {b"diff": b"python"})
  340. def test_iteration(self):
  341. """Test iterating over patterns."""
  342. patterns = [
  343. (Pattern(b"*.txt"), {b"text": True}),
  344. (Pattern(b"*.jpg"), {b"binary": True}),
  345. ]
  346. ga = GitAttributes(patterns)
  347. collected = list(ga)
  348. self.assertEqual(len(collected), 2)
  349. self.assertEqual(collected[0][0].pattern, b"*.txt")
  350. self.assertEqual(collected[1][0].pattern, b"*.jpg")
  351. def test_from_file(self):
  352. """Test creating GitAttributes from file."""
  353. with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
  354. f.write(b"*.txt text\n")
  355. f.write(b"*.bin -text binary\n")
  356. temp_path = f.name
  357. self.addCleanup(os.unlink, temp_path)
  358. ga = GitAttributes.from_file(temp_path)
  359. self.assertEqual(len(ga), 2)
  360. attrs = ga.match_path(b"file.txt")
  361. self.assertEqual(attrs, {b"text": True})
  362. attrs = ga.match_path(b"file.bin")
  363. self.assertEqual(attrs, {b"text": False, b"binary": True})
  364. def test_from_path(self):
  365. """Test creating GitAttributes from directory path."""
  366. with tempfile.TemporaryDirectory() as tmpdir:
  367. # Create .gitattributes file
  368. attrs_path = os.path.join(tmpdir, ".gitattributes")
  369. with open(attrs_path, "wb") as f:
  370. f.write(b"*.py diff=python\n")
  371. f.write(b"*.rs diff=rust\n")
  372. ga = GitAttributes.from_path(tmpdir)
  373. self.assertEqual(len(ga), 2)
  374. attrs = ga.match_path(b"main.py")
  375. self.assertEqual(attrs, {b"diff": b"python"})
  376. attrs = ga.match_path(b"lib.rs")
  377. self.assertEqual(attrs, {b"diff": b"rust"})
  378. def test_set_attribute(self):
  379. """Test setting attributes."""
  380. ga = GitAttributes()
  381. # Set attribute for a new pattern
  382. ga.set_attribute(b"*.txt", b"text", True)
  383. attrs = ga.match_path(b"file.txt")
  384. self.assertEqual(attrs, {b"text": True})
  385. # Update existing pattern
  386. ga.set_attribute(b"*.txt", b"diff", b"plain")
  387. attrs = ga.match_path(b"file.txt")
  388. self.assertEqual(attrs, {b"text": True, b"diff": b"plain"})
  389. # Unset attribute
  390. ga.set_attribute(b"*.txt", b"binary", False)
  391. attrs = ga.match_path(b"file.txt")
  392. self.assertEqual(attrs, {b"text": True, b"diff": b"plain", b"binary": False})
  393. # Remove attribute (unspecified)
  394. ga.set_attribute(b"*.txt", b"text", None)
  395. attrs = ga.match_path(b"file.txt")
  396. self.assertEqual(attrs, {b"diff": b"plain", b"binary": False})
  397. def test_remove_pattern(self):
  398. """Test removing patterns."""
  399. patterns = [
  400. (Pattern(b"*.txt"), {b"text": True}),
  401. (Pattern(b"*.jpg"), {b"binary": True}),
  402. (Pattern(b"*.py"), {b"diff": b"python"}),
  403. ]
  404. ga = GitAttributes(patterns)
  405. self.assertEqual(len(ga), 3)
  406. # Remove middle pattern
  407. ga.remove_pattern(b"*.jpg")
  408. self.assertEqual(len(ga), 2)
  409. # Check remaining patterns
  410. attrs = ga.match_path(b"file.txt")
  411. self.assertEqual(attrs, {b"text": True})
  412. attrs = ga.match_path(b"image.jpg")
  413. self.assertEqual(attrs, {}) # No match anymore
  414. attrs = ga.match_path(b"script.py")
  415. self.assertEqual(attrs, {b"diff": b"python"})
  416. def test_to_bytes_empty(self):
  417. """Test converting empty GitAttributes to bytes."""
  418. ga = GitAttributes()
  419. content = ga.to_bytes()
  420. self.assertEqual(content, b"")
  421. def test_to_bytes_single_pattern(self):
  422. """Test converting single pattern to bytes."""
  423. ga = GitAttributes()
  424. ga.set_attribute(b"*.txt", b"text", True)
  425. content = ga.to_bytes()
  426. self.assertEqual(content, b"*.txt text\n")
  427. def test_to_bytes_multiple_attributes(self):
  428. """Test converting pattern with multiple attributes to bytes."""
  429. ga = GitAttributes()
  430. ga.set_attribute(b"*.jpg", b"text", False)
  431. ga.set_attribute(b"*.jpg", b"diff", False)
  432. ga.set_attribute(b"*.jpg", b"binary", True)
  433. content = ga.to_bytes()
  434. # Attributes should be sorted
  435. self.assertEqual(content, b"*.jpg binary -diff -text\n")
  436. def test_to_bytes_multiple_patterns(self):
  437. """Test converting multiple patterns to bytes."""
  438. ga = GitAttributes()
  439. ga.set_attribute(b"*.txt", b"text", True)
  440. ga.set_attribute(b"*.jpg", b"binary", True)
  441. ga.set_attribute(b"*.jpg", b"text", False)
  442. content = ga.to_bytes()
  443. expected = b"*.txt text\n*.jpg binary -text\n"
  444. self.assertEqual(content, expected)
  445. def test_to_bytes_with_values(self):
  446. """Test converting attributes with values to bytes."""
  447. ga = GitAttributes()
  448. ga.set_attribute(b"*.c", b"filter", b"indent")
  449. ga.set_attribute(b"*.c", b"diff", b"cpp")
  450. ga.set_attribute(b"*.c", b"text", True)
  451. content = ga.to_bytes()
  452. # Attributes should be sorted
  453. self.assertEqual(content, b"*.c diff=cpp filter=indent text\n")
  454. def test_to_bytes_unspecified(self):
  455. """Test converting unspecified attributes to bytes."""
  456. ga = GitAttributes()
  457. ga.set_attribute(b"*.bin", b"text", None)
  458. ga.set_attribute(b"*.bin", b"diff", None)
  459. content = ga.to_bytes()
  460. # Unspecified attributes use !
  461. self.assertEqual(content, b"*.bin !diff !text\n")
  462. def test_write_to_file(self):
  463. """Test writing GitAttributes to file."""
  464. ga = GitAttributes()
  465. ga.set_attribute(b"*.txt", b"text", True)
  466. ga.set_attribute(b"*.jpg", b"binary", True)
  467. ga.set_attribute(b"*.jpg", b"text", False)
  468. with tempfile.NamedTemporaryFile(delete=False) as f:
  469. temp_path = f.name
  470. try:
  471. ga.write_to_file(temp_path)
  472. # Read back the file
  473. with open(temp_path, "rb") as f:
  474. content = f.read()
  475. expected = b"*.txt text\n*.jpg binary -text\n"
  476. self.assertEqual(content, expected)
  477. finally:
  478. os.unlink(temp_path)
  479. def test_write_to_file_string_path(self):
  480. """Test writing GitAttributes to file with string path."""
  481. ga = GitAttributes()
  482. ga.set_attribute(b"*.py", b"diff", b"python")
  483. with tempfile.NamedTemporaryFile(delete=False) as f:
  484. temp_path = f.name
  485. try:
  486. # Pass string path instead of bytes
  487. ga.write_to_file(temp_path)
  488. # Read back the file
  489. with open(temp_path, "rb") as f:
  490. content = f.read()
  491. self.assertEqual(content, b"*.py diff=python\n")
  492. finally:
  493. os.unlink(temp_path)
  494. def test_roundtrip_gitattributes(self):
  495. """Test reading and writing gitattributes preserves content."""
  496. original_content = b"""*.txt text
  497. *.jpg -text binary
  498. *.c filter=indent diff=cpp
  499. *.bin !text !diff
  500. """
  501. with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
  502. f.write(original_content)
  503. temp_path = f.name
  504. try:
  505. # Read the file
  506. ga = GitAttributes.from_file(temp_path)
  507. # Write to a new file
  508. with tempfile.NamedTemporaryFile(delete=False) as f2:
  509. temp_path2 = f2.name
  510. ga.write_to_file(temp_path2)
  511. # The content should be equivalent (though order might differ for attributes)
  512. ga2 = GitAttributes.from_file(temp_path2)
  513. # Compare patterns
  514. patterns1 = list(ga)
  515. patterns2 = list(ga2)
  516. self.assertEqual(len(patterns1), len(patterns2))
  517. for (p1, attrs1), (p2, attrs2) in zip(patterns1, patterns2):
  518. self.assertEqual(p1.pattern, p2.pattern)
  519. self.assertEqual(attrs1, attrs2)
  520. os.unlink(temp_path2)
  521. finally:
  522. os.unlink(temp_path)