pygit2 1.15.1__tar.gz → 1.16.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. {pygit2-1.15.1 → pygit2-1.16.0}/AUTHORS.md +4 -1
  2. {pygit2-1.15.1 → pygit2-1.16.0}/CHANGELOG.md +22 -0
  3. {pygit2-1.15.1 → pygit2-1.16.0}/PKG-INFO +6 -7
  4. {pygit2-1.15.1 → pygit2-1.16.0}/README.md +1 -3
  5. {pygit2-1.15.1 → pygit2-1.16.0}/SPONSORS.md +3 -4
  6. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/_build.py +1 -1
  7. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/_pygit2.pyi +2 -2
  8. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/callbacks.py +6 -6
  9. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/decl/callbacks.h +1 -1
  10. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/decl/repository.h +1 -0
  11. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/decl/types.h +11 -0
  12. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/enums.py +2 -2
  13. pygit2-1.16.0/pygit2/py.typed +1 -0
  14. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/repository.py +53 -0
  15. {pygit2-1.15.1 → pygit2-1.16.0}/pyproject.toml +1 -1
  16. {pygit2-1.15.1 → pygit2-1.16.0}/setup.py +6 -5
  17. {pygit2-1.15.1 → pygit2-1.16.0}/src/options.c +26 -69
  18. {pygit2-1.15.1 → pygit2-1.16.0}/src/options.h +6 -0
  19. {pygit2-1.15.1 → pygit2-1.16.0}/src/pygit2.c +2 -0
  20. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_repository.py +68 -0
  21. {pygit2-1.15.1 → pygit2-1.16.0}/COPYING +0 -0
  22. {pygit2-1.15.1 → pygit2-1.16.0}/Makefile +0 -0
  23. {pygit2-1.15.1 → pygit2-1.16.0}/build.sh +0 -0
  24. {pygit2-1.15.1 → pygit2-1.16.0}/build_tag.py +0 -0
  25. {pygit2-1.15.1 → pygit2-1.16.0}/mypy-stubtest.ini +0 -0
  26. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/__init__.py +0 -0
  27. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/_run.py +0 -0
  28. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/blame.py +0 -0
  29. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/blob.py +0 -0
  30. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/branches.py +0 -0
  31. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/config.py +0 -0
  32. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/credentials.py +0 -0
  33. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/decl/attr.h +0 -0
  34. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/decl/blame.h +0 -0
  35. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/decl/buffer.h +0 -0
  36. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/decl/checkout.h +0 -0
  37. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/decl/clone.h +0 -0
  38. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/decl/commit.h +0 -0
  39. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/decl/common.h +0 -0
  40. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/decl/config.h +0 -0
  41. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/decl/describe.h +0 -0
  42. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/decl/diff.h +0 -0
  43. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/decl/errors.h +0 -0
  44. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/decl/graph.h +0 -0
  45. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/decl/index.h +0 -0
  46. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/decl/indexer.h +0 -0
  47. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/decl/merge.h +0 -0
  48. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/decl/net.h +0 -0
  49. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/decl/oid.h +0 -0
  50. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/decl/pack.h +0 -0
  51. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/decl/proxy.h +0 -0
  52. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/decl/refspec.h +0 -0
  53. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/decl/remote.h +0 -0
  54. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/decl/revert.h +0 -0
  55. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/decl/stash.h +0 -0
  56. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/decl/strarray.h +0 -0
  57. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/decl/submodule.h +0 -0
  58. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/decl/transport.h +0 -0
  59. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/errors.py +0 -0
  60. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/ffi.py +0 -0
  61. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/filter.py +0 -0
  62. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/index.py +0 -0
  63. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/legacyenums.py +0 -0
  64. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/packbuilder.py +0 -0
  65. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/references.py +0 -0
  66. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/refspec.py +0 -0
  67. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/remotes.py +0 -0
  68. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/settings.py +0 -0
  69. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/submodules.py +0 -0
  70. {pygit2-1.15.1 → pygit2-1.16.0}/pygit2/utils.py +0 -0
  71. {pygit2-1.15.1 → pygit2-1.16.0}/pytest.ini +0 -0
  72. {pygit2-1.15.1 → pygit2-1.16.0}/requirements-test.txt +0 -0
  73. {pygit2-1.15.1 → pygit2-1.16.0}/requirements.txt +0 -0
  74. {pygit2-1.15.1 → pygit2-1.16.0}/setup.cfg +0 -0
  75. {pygit2-1.15.1 → pygit2-1.16.0}/src/blob.c +0 -0
  76. {pygit2-1.15.1 → pygit2-1.16.0}/src/branch.c +0 -0
  77. {pygit2-1.15.1 → pygit2-1.16.0}/src/branch.h +0 -0
  78. {pygit2-1.15.1 → pygit2-1.16.0}/src/commit.c +0 -0
  79. {pygit2-1.15.1 → pygit2-1.16.0}/src/diff.c +0 -0
  80. {pygit2-1.15.1 → pygit2-1.16.0}/src/diff.h +0 -0
  81. {pygit2-1.15.1 → pygit2-1.16.0}/src/error.c +0 -0
  82. {pygit2-1.15.1 → pygit2-1.16.0}/src/error.h +0 -0
  83. {pygit2-1.15.1 → pygit2-1.16.0}/src/filter.c +0 -0
  84. {pygit2-1.15.1 → pygit2-1.16.0}/src/filter.h +0 -0
  85. {pygit2-1.15.1 → pygit2-1.16.0}/src/mailmap.c +0 -0
  86. {pygit2-1.15.1 → pygit2-1.16.0}/src/mailmap.h +0 -0
  87. {pygit2-1.15.1 → pygit2-1.16.0}/src/note.c +0 -0
  88. {pygit2-1.15.1 → pygit2-1.16.0}/src/note.h +0 -0
  89. {pygit2-1.15.1 → pygit2-1.16.0}/src/object.c +0 -0
  90. {pygit2-1.15.1 → pygit2-1.16.0}/src/object.h +0 -0
  91. {pygit2-1.15.1 → pygit2-1.16.0}/src/odb.c +0 -0
  92. {pygit2-1.15.1 → pygit2-1.16.0}/src/odb.h +0 -0
  93. {pygit2-1.15.1 → pygit2-1.16.0}/src/odb_backend.c +0 -0
  94. {pygit2-1.15.1 → pygit2-1.16.0}/src/odb_backend.h +0 -0
  95. {pygit2-1.15.1 → pygit2-1.16.0}/src/oid.c +0 -0
  96. {pygit2-1.15.1 → pygit2-1.16.0}/src/oid.h +0 -0
  97. {pygit2-1.15.1 → pygit2-1.16.0}/src/patch.c +0 -0
  98. {pygit2-1.15.1 → pygit2-1.16.0}/src/patch.h +0 -0
  99. {pygit2-1.15.1 → pygit2-1.16.0}/src/refdb.c +0 -0
  100. {pygit2-1.15.1 → pygit2-1.16.0}/src/refdb.h +0 -0
  101. {pygit2-1.15.1 → pygit2-1.16.0}/src/refdb_backend.c +0 -0
  102. {pygit2-1.15.1 → pygit2-1.16.0}/src/refdb_backend.h +0 -0
  103. {pygit2-1.15.1 → pygit2-1.16.0}/src/reference.c +0 -0
  104. {pygit2-1.15.1 → pygit2-1.16.0}/src/reference.h +0 -0
  105. {pygit2-1.15.1 → pygit2-1.16.0}/src/repository.c +0 -0
  106. {pygit2-1.15.1 → pygit2-1.16.0}/src/repository.h +0 -0
  107. {pygit2-1.15.1 → pygit2-1.16.0}/src/revspec.c +0 -0
  108. {pygit2-1.15.1 → pygit2-1.16.0}/src/revspec.h +0 -0
  109. {pygit2-1.15.1 → pygit2-1.16.0}/src/signature.c +0 -0
  110. {pygit2-1.15.1 → pygit2-1.16.0}/src/signature.h +0 -0
  111. {pygit2-1.15.1 → pygit2-1.16.0}/src/stash.c +0 -0
  112. {pygit2-1.15.1 → pygit2-1.16.0}/src/tag.c +0 -0
  113. {pygit2-1.15.1 → pygit2-1.16.0}/src/tree.c +0 -0
  114. {pygit2-1.15.1 → pygit2-1.16.0}/src/tree.h +0 -0
  115. {pygit2-1.15.1 → pygit2-1.16.0}/src/treebuilder.c +0 -0
  116. {pygit2-1.15.1 → pygit2-1.16.0}/src/treebuilder.h +0 -0
  117. {pygit2-1.15.1 → pygit2-1.16.0}/src/types.h +0 -0
  118. {pygit2-1.15.1 → pygit2-1.16.0}/src/utils.c +0 -0
  119. {pygit2-1.15.1 → pygit2-1.16.0}/src/utils.h +0 -0
  120. {pygit2-1.15.1 → pygit2-1.16.0}/src/walker.c +0 -0
  121. {pygit2-1.15.1 → pygit2-1.16.0}/src/walker.h +0 -0
  122. {pygit2-1.15.1 → pygit2-1.16.0}/src/wildmatch.c +0 -0
  123. {pygit2-1.15.1 → pygit2-1.16.0}/src/wildmatch.h +0 -0
  124. {pygit2-1.15.1 → pygit2-1.16.0}/src/worktree.c +0 -0
  125. {pygit2-1.15.1 → pygit2-1.16.0}/src/worktree.h +0 -0
  126. {pygit2-1.15.1 → pygit2-1.16.0}/test/__init__.py +0 -0
  127. {pygit2-1.15.1 → pygit2-1.16.0}/test/conftest.py +0 -0
  128. {pygit2-1.15.1 → pygit2-1.16.0}/test/data/barerepo.zip +0 -0
  129. {pygit2-1.15.1 → pygit2-1.16.0}/test/data/binaryfilerepo.zip +0 -0
  130. {pygit2-1.15.1 → pygit2-1.16.0}/test/data/blameflagsrepo.zip +0 -0
  131. {pygit2-1.15.1 → pygit2-1.16.0}/test/data/dirtyrepo.zip +0 -0
  132. {pygit2-1.15.1 → pygit2-1.16.0}/test/data/emptyrepo.zip +0 -0
  133. {pygit2-1.15.1 → pygit2-1.16.0}/test/data/encoding.zip +0 -0
  134. {pygit2-1.15.1 → pygit2-1.16.0}/test/data/gpgsigned.zip +0 -0
  135. {pygit2-1.15.1 → pygit2-1.16.0}/test/data/submodulerepo.zip +0 -0
  136. {pygit2-1.15.1 → pygit2-1.16.0}/test/data/testrepo.zip +0 -0
  137. {pygit2-1.15.1 → pygit2-1.16.0}/test/data/testrepoformerging.zip +0 -0
  138. {pygit2-1.15.1 → pygit2-1.16.0}/test/data/testrepopacked.zip +0 -0
  139. {pygit2-1.15.1 → pygit2-1.16.0}/test/data/trailerrepo.zip +0 -0
  140. {pygit2-1.15.1 → pygit2-1.16.0}/test/data/utf8branchrepo.zip +0 -0
  141. {pygit2-1.15.1 → pygit2-1.16.0}/test/keys/pygit2_empty +0 -0
  142. {pygit2-1.15.1 → pygit2-1.16.0}/test/keys/pygit2_empty.pub +0 -0
  143. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_apply_diff.py +0 -0
  144. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_archive.py +0 -0
  145. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_attributes.py +0 -0
  146. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_blame.py +0 -0
  147. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_blob.py +0 -0
  148. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_branch.py +0 -0
  149. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_branch_empty.py +0 -0
  150. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_cherrypick.py +0 -0
  151. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_commit.py +0 -0
  152. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_commit_gpg.py +0 -0
  153. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_commit_trailer.py +0 -0
  154. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_config.py +0 -0
  155. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_credentials.py +0 -0
  156. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_describe.py +0 -0
  157. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_diff.py +0 -0
  158. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_diff_binary.py +0 -0
  159. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_filter.py +0 -0
  160. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_index.py +0 -0
  161. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_mailmap.py +0 -0
  162. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_merge.py +0 -0
  163. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_note.py +0 -0
  164. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_object.py +0 -0
  165. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_odb.py +0 -0
  166. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_odb_backend.py +0 -0
  167. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_oid.py +0 -0
  168. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_options.py +0 -0
  169. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_packbuilder.py +0 -0
  170. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_patch.py +0 -0
  171. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_patch_encoding.py +0 -0
  172. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_refdb_backend.py +0 -0
  173. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_refs.py +0 -0
  174. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_remote.py +0 -0
  175. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_remote_prune.py +0 -0
  176. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_remote_utf8.py +0 -0
  177. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_repository_bare.py +0 -0
  178. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_repository_custom.py +0 -0
  179. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_repository_empty.py +0 -0
  180. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_revparse.py +0 -0
  181. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_revwalk.py +0 -0
  182. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_signature.py +0 -0
  183. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_status.py +0 -0
  184. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_submodule.py +0 -0
  185. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_tag.py +0 -0
  186. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_tree.py +0 -0
  187. {pygit2-1.15.1 → pygit2-1.16.0}/test/test_treebuilder.py +0 -0
  188. {pygit2-1.15.1 → pygit2-1.16.0}/test/utils.py +0 -0
@@ -113,6 +113,7 @@ Authors:
113
113
  Kevin KIN-FOO
114
114
  Marcel Waldvogel
115
115
  Masud Rahman
116
+ Mathieu Parent
116
117
  Michael Sondergaard
117
118
  Natanael Arndt
118
119
  Ondřej Nový
@@ -184,16 +185,17 @@ Authors:
184
185
  Lukas Berk
185
186
  Martin von Zweigbergk
186
187
  Mathieu Bridon
187
- Mathieu Parent
188
188
  Mathieu Pillard
189
189
  Matthaus Woolard
190
190
  Maxwell G
191
191
  Michał Górny
192
+ Na'aman Hirschfeld
192
193
  Nicolás Sanguinetti
193
194
  Nikita Kartashov
194
195
  Nikolai Zujev
195
196
  Nils Philippsen
196
197
  Noah Fontes
198
+ Or Hayat
197
199
  Óscar San José
198
200
  Patrick Lühne
199
201
  Paul Wagland
@@ -215,6 +217,7 @@ Authors:
215
217
  Steven Winfield
216
218
  Tad Hardesty
217
219
  Timo Röhling
220
+ Victor Florea
218
221
  Vladimir Rutsky
219
222
  Wim Jeantine-Glenn
220
223
  Yu Jianjian
@@ -1,3 +1,25 @@
1
+ # 1.16.0 (2024-10-11)
2
+
3
+ - Add support for Python 3.13
4
+
5
+ - Drop support for Python 3.9
6
+
7
+ - New `Repository.hashfile(...)`
8
+ [#1298](https://github.com/libgit2/pygit2/pull/1298)
9
+
10
+ - New `Option.GET_MWINDOW_FILE_LIMIT` and `Option.SET_MWINDOW_FILE_LIMIT`
11
+ [#1312](https://github.com/libgit2/pygit2/pull/1312)
12
+
13
+ - Fix overriding `certificate_check(...)` callback via argument to `RemoteCallbacks(...)`
14
+ [#1321](https://github.com/libgit2/pygit2/pull/1321)
15
+
16
+ - Add py.typed
17
+ [#1310](https://github.com/libgit2/pygit2/pull/1310)
18
+
19
+ - Fix `discover_repository(...)` annotation
20
+ [#1313](https://github.com/libgit2/pygit2/pull/1313)
21
+
22
+
1
23
  # 1.15.1 (2024-07-07)
2
24
 
3
25
  - New `Repository.revert(...)`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pygit2
3
- Version: 1.15.1
3
+ Version: 1.16.0
4
4
  Summary: Python bindings for libgit2.
5
5
  Home-page: https://github.com/libgit2/pygit2
6
6
  Maintainer: J. David Ibáñez
@@ -14,23 +14,24 @@ Classifier: Development Status :: 5 - Production/Stable
14
14
  Classifier: Intended Audience :: Developers
15
15
  Classifier: Programming Language :: Python
16
16
  Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.9
18
17
  Classifier: Programming Language :: Python :: 3.10
19
18
  Classifier: Programming Language :: Python :: 3.11
20
19
  Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
21
  Classifier: Programming Language :: Python :: Implementation :: PyPy
22
22
  Classifier: Programming Language :: Python :: Implementation :: CPython
23
23
  Classifier: Topic :: Software Development :: Version Control
24
- Requires-Python: >=3.9
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.10
25
26
  Description-Content-Type: text/markdown
26
27
  License-File: COPYING
27
28
  License-File: AUTHORS.md
28
- Requires-Dist: cffi>=1.16.0
29
+ Requires-Dist: cffi>=1.17.0
29
30
 
30
31
  # pygit2 - libgit2 bindings in Python
31
32
 
32
33
  Bindings to the libgit2 shared library, implements Git plumbing.
33
- Supports Python 3.9 to 3.12 and PyPy3 7.3+
34
+ Supports Python 3.10 to 3.13 and PyPy3 7.3+
34
35
 
35
36
  [![image](https://github.com/libgit2/pygit2/actions/workflows/tests.yml/badge.svg)](https://github.com/libgit2/pygit2/actions/workflows/tests.yml)
36
37
 
@@ -47,8 +48,6 @@ Supports Python 3.9 to 3.12 and PyPy3 7.3+
47
48
 
48
49
  ## Sponsors
49
50
 
50
- - [Microsoft](https://github.com/microsoft)
51
-
52
51
  Add your name and link here, [become a
53
52
  sponsor](https://github.com/sponsors/jdavid).
54
53
 
@@ -1,7 +1,7 @@
1
1
  # pygit2 - libgit2 bindings in Python
2
2
 
3
3
  Bindings to the libgit2 shared library, implements Git plumbing.
4
- Supports Python 3.9 to 3.12 and PyPy3 7.3+
4
+ Supports Python 3.10 to 3.13 and PyPy3 7.3+
5
5
 
6
6
  [![image](https://github.com/libgit2/pygit2/actions/workflows/tests.yml/badge.svg)](https://github.com/libgit2/pygit2/actions/workflows/tests.yml)
7
7
 
@@ -18,8 +18,6 @@ Supports Python 3.9 to 3.12 and PyPy3 7.3+
18
18
 
19
19
  ## Sponsors
20
20
 
21
- - [Microsoft](https://github.com/microsoft)
22
-
23
21
  Add your name and link here, [become a
24
22
  sponsor](https://github.com/sponsors/jdavid).
25
23
 
@@ -1,12 +1,11 @@
1
1
  Friends of pygit2:
2
2
 
3
- - [Microsoft](https://github.com/microsoft)
4
-
5
- Add your name to the list,
6
- [become a friend of pygit2](https://github.com/sponsors/jdavid).
3
+ - Add your name to the list,
4
+ [become a friend of pygit2](https://github.com/sponsors/jdavid).
7
5
 
8
6
  Past sponsors:
9
7
 
8
+ - [Microsoft](https://github.com/microsoft)
10
9
  - [Iterative](https://iterative.ai/)
11
10
  - [SourceHut](https://sourcehut.org)
12
11
  - [GitHub](https://github.com/github)
@@ -34,7 +34,7 @@ from pathlib import Path
34
34
  #
35
35
  # The version number of pygit2
36
36
  #
37
- __version__ = '1.15.1'
37
+ __version__ = '1.16.0'
38
38
 
39
39
 
40
40
  #
@@ -1,6 +1,6 @@
1
1
  from typing import Iterator, Literal, Optional, overload
2
2
  from io import IOBase
3
- from . import Index, Submodule
3
+ from . import Index
4
4
  from .enums import (
5
5
  ApplyLocation,
6
6
  BranchType,
@@ -558,7 +558,7 @@ class Worktree:
558
558
 
559
559
  def discover_repository(
560
560
  path: str, across_fs: bool = False, ceiling_dirs: str = ...
561
- ) -> str: ...
561
+ ) -> str | None: ...
562
562
  def hash(data: bytes) -> Oid: ...
563
563
  def hashfile(path: str) -> Oid: ...
564
564
  def init_file_backend(path: str, flags: int = 0) -> object: ...
@@ -113,12 +113,12 @@ class RemoteCallbacks(Payload):
113
113
  RemoteCallbacks(certificate=certificate).
114
114
  """
115
115
 
116
- def __init__(self, credentials=None, certificate=None):
116
+ def __init__(self, credentials=None, certificate_check=None):
117
117
  super().__init__()
118
118
  if credentials is not None:
119
119
  self.credentials = credentials
120
- if certificate is not None:
121
- self.certificate = certificate
120
+ if certificate_check is not None:
121
+ self.certificate_check = certificate_check
122
122
 
123
123
  def sideband_progress(self, string):
124
124
  """
@@ -344,7 +344,7 @@ def git_fetch_options(payload, opts=None):
344
344
  opts.callbacks.transfer_progress = C._transfer_progress_cb
345
345
  opts.callbacks.update_tips = C._update_tips_cb
346
346
  opts.callbacks.credentials = C._credentials_cb
347
- opts.callbacks.certificate_check = C._certificate_cb
347
+ opts.callbacks.certificate_check = C._certificate_check_cb
348
348
  # Payload
349
349
  handle = ffi.new_handle(payload)
350
350
  opts.callbacks.payload = handle
@@ -368,7 +368,7 @@ def git_push_options(payload, opts=None):
368
368
  opts.callbacks.transfer_progress = C._transfer_progress_cb
369
369
  opts.callbacks.update_tips = C._update_tips_cb
370
370
  opts.callbacks.credentials = C._credentials_cb
371
- opts.callbacks.certificate_check = C._certificate_cb
371
+ opts.callbacks.certificate_check = C._certificate_check_cb
372
372
  opts.callbacks.push_update_reference = C._push_update_reference_cb
373
373
  # Payload
374
374
  handle = ffi.new_handle(payload)
@@ -457,7 +457,7 @@ def libgit2_callback_void(f):
457
457
 
458
458
 
459
459
  @libgit2_callback
460
- def _certificate_cb(cert_i, valid, host, data):
460
+ def _certificate_check_cb(cert_i, valid, host, data):
461
461
  # We want to simulate what should happen if libgit2 supported pass-through
462
462
  # for this callback. For SSH, 'valid' is always False, because it doesn't
463
463
  # look at known_hosts, but we do want to let it through in order to do what
@@ -1,4 +1,4 @@
1
- extern "Python" int _certificate_cb(
1
+ extern "Python" int _certificate_check_cb(
2
2
  git_cert *cert,
3
3
  int valid,
4
4
  const char *host,
@@ -83,6 +83,7 @@ int git_repository_set_head_detached(
83
83
  git_repository* repo,
84
84
  const git_oid* commitish);
85
85
 
86
+ int git_repository_hashfile(git_oid *out, git_repository *repo, const char *path, git_object_t type, const char *as_path);
86
87
  int git_repository_ident(const char **name, const char **email, const git_repository *repo);
87
88
  int git_repository_set_ident(git_repository *repo, const char *name, const char *email);
88
89
  int git_repository_index(git_index **out, git_repository *repo);
@@ -57,3 +57,14 @@ typedef enum {
57
57
  GIT_SUBMODULE_IGNORE_DIRTY = 3,
58
58
  GIT_SUBMODULE_IGNORE_ALL = 4,
59
59
  } git_submodule_ignore_t;
60
+
61
+ typedef enum {
62
+ GIT_OBJECT_ANY = ...,
63
+ GIT_OBJECT_INVALID = ...,
64
+ GIT_OBJECT_COMMIT = ...,
65
+ GIT_OBJECT_TREE = ...,
66
+ GIT_OBJECT_BLOB = ...,
67
+ GIT_OBJECT_TAG = ...,
68
+ GIT_OBJECT_OFS_DELTA = ...,
69
+ GIT_OBJECT_REF_DELTA = ...,
70
+ } git_object_t;
@@ -979,8 +979,8 @@ class Option(IntEnum):
979
979
  SET_PACK_MAX_OBJECTS = _pygit2.GIT_OPT_SET_PACK_MAX_OBJECTS
980
980
  DISABLE_PACK_KEEP_FILE_CHECKS = _pygit2.GIT_OPT_DISABLE_PACK_KEEP_FILE_CHECKS
981
981
  # ENABLE_HTTP_EXPECT_CONTINUE = _pygit2.GIT_OPT_ENABLE_HTTP_EXPECT_CONTINUE
982
- # GET_MWINDOW_FILE_LIMIT = _pygit2.GIT_OPT_GET_MWINDOW_FILE_LIMIT
983
- # SET_MWINDOW_FILE_LIMIT = _pygit2.GIT_OPT_SET_MWINDOW_FILE_LIMIT
982
+ GET_MWINDOW_FILE_LIMIT = _pygit2.GIT_OPT_GET_MWINDOW_FILE_LIMIT
983
+ SET_MWINDOW_FILE_LIMIT = _pygit2.GIT_OPT_SET_MWINDOW_FILE_LIMIT
984
984
  # SET_ODB_PACKED_PRIORITY = _pygit2.GIT_OPT_SET_ODB_PACKED_PRIORITY
985
985
  # SET_ODB_LOOSE_PRIORITY = _pygit2.GIT_OPT_SET_ODB_LOOSE_PRIORITY
986
986
  # GET_EXTENSIONS = _pygit2.GIT_OPT_GET_EXTENSIONS
@@ -0,0 +1 @@
1
+ # python type marker, see: https://peps.python.org/pep-0561/
@@ -51,6 +51,7 @@ from .enums import (
51
51
  MergeFavor,
52
52
  MergeFileFlag,
53
53
  MergeFlag,
54
+ ObjectType,
54
55
  RepositoryOpenFlag,
55
56
  RepositoryState,
56
57
  )
@@ -129,6 +130,58 @@ class BaseRepository(_Repository):
129
130
 
130
131
  return builder.written_objects_count
131
132
 
133
+ def hashfile(
134
+ self,
135
+ path: str,
136
+ object_type: ObjectType = ObjectType.BLOB,
137
+ as_path: typing.Optional[str] = None,
138
+ ):
139
+ """Calculate the hash of a file using repository filtering rules.
140
+
141
+ If you simply want to calculate the hash of a file on disk with no filters,
142
+ you can just use `pygit2.hashfile()`. However, if you want to hash a file
143
+ in the repository and you want to apply filtering rules (e.g. crlf filters)
144
+ before generating the SHA, then use this function.
145
+
146
+ Note: if the repository has `core.safecrlf` set to fail and the filtering
147
+ triggers that failure, then this function will raise an error and not
148
+ calculate the hash of the file.
149
+
150
+ Returns: Output value of calculated SHA (Oid)
151
+
152
+ Parameters:
153
+
154
+ path
155
+ Path to file on disk whose contents should be hashed. This may be
156
+ an absolute path or a relative path, in which case it will be treated
157
+ as a path within the working directory.
158
+
159
+ object_type
160
+ The object type to hash (e.g. enums.ObjectType.BLOB)
161
+
162
+ as_path
163
+ The path to use to look up filtering rules. If this is an empty string
164
+ then no filters will be applied when calculating the hash.
165
+ If this is `None` and the `path` parameter is a file within the
166
+ repository's working directory, then the `path` will be used.
167
+ """
168
+ c_path = to_bytes(path)
169
+
170
+ if as_path is None:
171
+ c_as_path = ffi.NULL
172
+ else:
173
+ c_as_path = to_bytes(as_path)
174
+
175
+ c_oid = ffi.new('git_oid *')
176
+
177
+ err = C.git_repository_hashfile(
178
+ c_oid, self._repo, c_path, int(object_type), c_as_path
179
+ )
180
+ check_error(err)
181
+
182
+ oid = Oid(raw=bytes(ffi.buffer(c_oid.id)[:]))
183
+ return oid
184
+
132
185
  def __iter__(self):
133
186
  return iter(self.odb)
134
187
 
@@ -25,7 +25,7 @@ environment = {LIBGIT2_VERSION="1.8.1", LIBSSH2_VERSION="1.11.0", OPENSSL_VERSIO
25
25
  repair-wheel-command = "DYLD_LIBRARY_PATH=/Users/runner/work/pygit2/pygit2/ci/lib delocate-wheel --require-archs {delocate_archs} -w {dest_dir} {wheel}"
26
26
 
27
27
  [tool.ruff]
28
- target-version = "py39" # oldest supported Python version
28
+ target-version = "py310" # oldest supported Python version
29
29
  fix = true
30
30
  extend-exclude = [
31
31
  ".cache",
@@ -75,13 +75,14 @@ classifiers = [
75
75
  'Intended Audience :: Developers',
76
76
  'Programming Language :: Python',
77
77
  'Programming Language :: Python :: 3',
78
- 'Programming Language :: Python :: 3.9',
79
78
  'Programming Language :: Python :: 3.10',
80
79
  'Programming Language :: Python :: 3.11',
81
80
  'Programming Language :: Python :: 3.12',
81
+ 'Programming Language :: Python :: 3.13',
82
82
  'Programming Language :: Python :: Implementation :: PyPy',
83
83
  'Programming Language :: Python :: Implementation :: CPython',
84
84
  'Topic :: Software Development :: Version Control',
85
+ 'Typing :: Typed',
85
86
  ]
86
87
 
87
88
  __dir__ = Path(__file__).parent
@@ -144,15 +145,15 @@ setup(
144
145
  long_description=long_description,
145
146
  long_description_content_type='text/markdown',
146
147
  packages=['pygit2'],
147
- package_data={'pygit2': ['decl/*.h', '*.pyi']},
148
+ package_data={'pygit2': ['decl/*.h', '*.pyi', 'py.typed']},
148
149
  zip_safe=False,
149
150
  cmdclass=cmdclass,
150
151
  cffi_modules=['pygit2/_run.py:ffi'],
151
152
  ext_modules=ext_modules,
152
153
  # Requirements
153
- python_requires='>=3.9',
154
- setup_requires=['cffi>=1.16.0'],
155
- install_requires=['cffi>=1.16.0'],
154
+ python_requires='>=3.10',
155
+ setup_requires=['cffi>=1.17.0'],
156
+ install_requires=['cffi>=1.17.0'],
156
157
  # URLs
157
158
  url='https://github.com/libgit2/pygit2',
158
159
  project_urls={
@@ -73,85 +73,46 @@ option(PyObject *self, PyObject *args)
73
73
 
74
74
  switch (option) {
75
75
 
76
+ case GIT_OPT_GET_MWINDOW_FILE_LIMIT:
77
+ case GIT_OPT_GET_MWINDOW_MAPPED_LIMIT:
76
78
  case GIT_OPT_GET_MWINDOW_SIZE:
77
79
  {
78
- size_t size;
80
+ size_t value;
79
81
 
80
- error = git_libgit2_opts(GIT_OPT_GET_MWINDOW_SIZE, &size);
82
+ error = git_libgit2_opts(option, &value);
81
83
  if (error < 0)
82
84
  return Error_set(error);
83
85
 
84
- return PyLong_FromSize_t(size);
86
+ return PyLong_FromSize_t(value);
85
87
  }
86
88
 
89
+ case GIT_OPT_SET_MWINDOW_FILE_LIMIT:
90
+ case GIT_OPT_SET_MWINDOW_MAPPED_LIMIT:
87
91
  case GIT_OPT_SET_MWINDOW_SIZE:
88
92
  {
89
- size_t size;
90
- PyObject *py_size;
91
-
92
- py_size = PyTuple_GetItem(args, 1);
93
- if (!py_size)
93
+ PyObject *py_value = PyTuple_GetItem(args, 1);
94
+ if (!py_value)
94
95
  return NULL;
95
96
 
96
- if (!PyLong_Check(py_size))
97
- return Error_type_error(
98
- "size should be an integer, got %.200s", py_size);
97
+ if (!PyLong_Check(py_value))
98
+ return Error_type_error("expected integer, got %.200s", py_value);
99
99
 
100
- size = PyLong_AsSize_t(py_size);
101
- error = git_libgit2_opts(GIT_OPT_SET_MWINDOW_SIZE, size);
100
+ size_t value = PyLong_AsSize_t(py_value);
101
+ error = git_libgit2_opts(option, value);
102
102
  if (error < 0)
103
103
  return Error_set(error);
104
104
 
105
105
  Py_RETURN_NONE;
106
106
  }
107
107
 
108
- case GIT_OPT_GET_MWINDOW_MAPPED_LIMIT:
109
- {
110
- size_t limit;
111
-
112
- error = git_libgit2_opts(GIT_OPT_GET_MWINDOW_MAPPED_LIMIT, &limit);
113
- if (error < 0)
114
- return Error_set(error);
115
-
116
- return PyLong_FromSize_t(limit);
117
- }
118
-
119
- case GIT_OPT_SET_MWINDOW_MAPPED_LIMIT:
120
- {
121
- size_t limit;
122
- PyObject *py_limit;
123
-
124
- py_limit = PyTuple_GetItem(args, 1);
125
- if (!py_limit)
126
- return NULL;
127
-
128
- if (PyLong_Check(py_limit)) {
129
- limit = PyLong_AsSize_t(py_limit);
130
- } else if (PyLong_Check(py_limit)) {
131
- limit = PyLong_AsSize_t(py_limit);
132
- } else {
133
- return Error_type_error(
134
- "limit should be an integer, got %.200s", py_limit);
135
- }
136
-
137
- error = git_libgit2_opts(GIT_OPT_SET_MWINDOW_MAPPED_LIMIT, limit);
138
- if (error < 0)
139
- return Error_set(error);
140
-
141
- Py_RETURN_NONE;
142
- }
143
-
144
108
  case GIT_OPT_GET_SEARCH_PATH:
145
109
  {
146
- PyObject *py_level;
147
-
148
- py_level = PyTuple_GetItem(args, 1);
110
+ PyObject *py_level = PyTuple_GetItem(args, 1);
149
111
  if (!py_level)
150
112
  return NULL;
151
113
 
152
114
  if (!PyLong_Check(py_level))
153
- return Error_type_error(
154
- "level should be an integer, got %.200s", py_level);
115
+ return Error_type_error("level should be an integer, got %.200s", py_level);
155
116
 
156
117
  return get_search_path(PyLong_AsLong(py_level));
157
118
  }
@@ -173,7 +134,7 @@ option(PyObject *self, PyObject *args)
173
134
  if (!path)
174
135
  return NULL;
175
136
 
176
- int err = git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, PyLong_AsLong(py_level), path);
137
+ int err = git_libgit2_opts(option, PyLong_AsLong(py_level), path);
177
138
  if (err < 0)
178
139
  return Error_set(err);
179
140
 
@@ -200,8 +161,7 @@ option(PyObject *self, PyObject *args)
200
161
 
201
162
  object_type = PyLong_AsLong(py_object_type);
202
163
  limit = PyLong_AsSize_t(py_limit);
203
- error = git_libgit2_opts(
204
- GIT_OPT_SET_CACHE_OBJECT_LIMIT, object_type, limit);
164
+ error = git_libgit2_opts(option, object_type, limit);
205
165
 
206
166
  if (error < 0)
207
167
  return Error_set(error);
@@ -223,7 +183,7 @@ option(PyObject *self, PyObject *args)
223
183
  "max_size should be an integer, got %.200s", py_max_size);
224
184
 
225
185
  max_size = PyLong_AsSize_t(py_max_size);
226
- error = git_libgit2_opts(GIT_OPT_SET_CACHE_MAX_SIZE, max_size);
186
+ error = git_libgit2_opts(option, max_size);
227
187
  if (error < 0)
228
188
  return Error_set(error);
229
189
 
@@ -236,7 +196,7 @@ option(PyObject *self, PyObject *args)
236
196
  size_t allowed;
237
197
  PyObject* tup = PyTuple_New(2);
238
198
 
239
- error = git_libgit2_opts(GIT_OPT_GET_CACHED_MEMORY, &current, &allowed);
199
+ error = git_libgit2_opts(option, &current, &allowed);
240
200
  if (error < 0)
241
201
  return Error_set(error);
242
202
 
@@ -275,7 +235,7 @@ option(PyObject *self, PyObject *args)
275
235
  if (PyUnicode_Check(py_dir) || PyBytes_Check(py_dir))
276
236
  dir_path = pgit_borrow_fsdefault(py_dir, &tvalue_dir);
277
237
 
278
- err = git_libgit2_opts(GIT_OPT_SET_SSL_CERT_LOCATIONS, file_path, dir_path);
238
+ err = git_libgit2_opts(option, file_path, dir_path);
279
239
  Py_XDECREF(tvalue_file);
280
240
  Py_XDECREF(tvalue_dir);
281
241
 
@@ -302,18 +262,15 @@ option(PyObject *self, PyObject *args)
302
262
  case GIT_OPT_DISABLE_PACK_KEEP_FILE_CHECKS:
303
263
  case GIT_OPT_SET_OWNER_VALIDATION:
304
264
  {
305
- PyObject *py_enabled;
306
- int enabled;
307
-
308
- py_enabled = PyTuple_GetItem(args, 1);
309
- if (!py_enabled)
265
+ PyObject *py_value = PyTuple_GetItem(args, 1);
266
+ if (!py_value)
310
267
  return NULL;
311
268
 
312
- if (!PyLong_Check(py_enabled))
313
- return Error_type_error("expected integer, got %.200s", py_enabled);
269
+ if (!PyLong_Check(py_value))
270
+ return Error_type_error("expected integer, got %.200s", py_value);
314
271
 
315
- enabled = PyLong_AsSize_t(py_enabled);
316
- error = git_libgit2_opts(option, enabled);
272
+ int value = PyLong_AsSize_t(py_value);
273
+ error = git_libgit2_opts(option, value);
317
274
  if (error < 0)
318
275
  return Error_set(error);
319
276
 
@@ -52,6 +52,12 @@ PyDoc_STRVAR(option__doc__,
52
52
  "GIT_OPT_SET_MWINDOW_SIZE, size\n"
53
53
  " Set the maximum mmap window size.\n"
54
54
  "\n"
55
+ "GIT_OPT_GET_MWINDOW_FILE_LIMIT\n"
56
+ " Get the maximum number of files that will be mapped at any time by the library.\n"
57
+ "\n"
58
+ "GIT_OPT_SET_MWINDOW_FILE_LIMIT, size\n"
59
+ " Set the maximum number of files that can be mapped at any time by the library. The default (0) is unlimited.\n"
60
+ "\n"
55
61
  "GIT_OPT_GET_OWNER_VALIDATION\n"
56
62
  " Gets the owner validation setting for repository directories.\n"
57
63
  "\n"
@@ -504,6 +504,8 @@ PyInit__pygit2(void)
504
504
  ADD_CONSTANT_INT(m, GIT_OPT_DISABLE_PACK_KEEP_FILE_CHECKS);
505
505
  ADD_CONSTANT_INT(m, GIT_OPT_GET_OWNER_VALIDATION);
506
506
  ADD_CONSTANT_INT(m, GIT_OPT_SET_OWNER_VALIDATION);
507
+ ADD_CONSTANT_INT(m, GIT_OPT_GET_MWINDOW_FILE_LIMIT);
508
+ ADD_CONSTANT_INT(m, GIT_OPT_SET_MWINDOW_FILE_LIMIT);
507
509
 
508
510
  /* Exceptions */
509
511
  ADD_EXC(m, GitError, NULL);
@@ -898,3 +898,71 @@ def test_is_shallow(testrepo):
898
898
  f.write('abcdef0123456789abcdef0123456789abcdef00\n')
899
899
 
900
900
  assert testrepo.is_shallow
901
+
902
+
903
+ def test_repository_hashfile(testrepo):
904
+ original_hash = testrepo.index['hello.txt'].id
905
+
906
+ # Test simple use
907
+ h = testrepo.hashfile('hello.txt')
908
+ assert h == original_hash
909
+
910
+ # Test absolute path
911
+ # For best results on Windows, pass a pure POSIX path. (See https://github.com/libgit2/libgit2/issues/6825)
912
+ absolute_path = Path(testrepo.workdir, 'hello.txt')
913
+ absolute_path = absolute_path.as_posix() # Windows compatibility
914
+ h = testrepo.hashfile(str(absolute_path))
915
+ assert h == original_hash
916
+
917
+ # Test missing path
918
+ with pytest.raises(KeyError):
919
+ testrepo.hashfile('missing-file')
920
+
921
+ # Test invalid object type
922
+ with pytest.raises(pygit2.GitError):
923
+ testrepo.hashfile('hello.txt', ObjectType.OFS_DELTA)
924
+
925
+
926
+ def test_repository_hashfile_filter(testrepo):
927
+ original_hash = testrepo.index['hello.txt'].id
928
+
929
+ with open(Path(testrepo.workdir, 'hello.txt'), 'rb') as f:
930
+ original_text = f.read()
931
+
932
+ crlf_data = original_text.replace(b'\n', b'\r\n')
933
+ crlf_hash = utils.gen_blob_sha1(crlf_data)
934
+ assert crlf_hash != original_hash
935
+
936
+ # Write hellocrlf.txt as a copy of hello.txt with CRLF line endings
937
+ with open(Path(testrepo.workdir, 'hellocrlf.txt'), 'wb') as f:
938
+ f.write(crlf_data)
939
+
940
+ # Set up a CRLF filter
941
+ testrepo.config['core.autocrlf'] = True
942
+ with open(Path(testrepo.workdir, '.gitattributes'), 'wt') as f:
943
+ f.write('*.txt text\n*.bin binary\n\n')
944
+
945
+ # By default, hellocrlf.txt should have the same hash as the original,
946
+ # due to core.autocrlf=True
947
+ h = testrepo.hashfile('hellocrlf.txt')
948
+ assert h == original_hash
949
+
950
+ # Treat absolute path with filters.
951
+ # For best results on Windows, pass a pure POSIX path. (See https://github.com/libgit2/libgit2/issues/6825)
952
+ absolute_path = Path(testrepo.workdir, 'hellocrlf.txt')
953
+ absolute_path = absolute_path.as_posix() # Windows compatibility
954
+ h = testrepo.hashfile(str(absolute_path))
955
+ assert h == original_hash
956
+
957
+ # Bypass filters
958
+ h = testrepo.hashfile('hellocrlf.txt', as_path='')
959
+ assert h == crlf_hash
960
+
961
+ # Bypass filters via .gitattributes
962
+ h = testrepo.hashfile('hellocrlf.txt', as_path='foobar.bin')
963
+ assert h == crlf_hash
964
+
965
+ # If core.safecrlf=fail, hashing a non-CRLF file will fail
966
+ testrepo.config['core.safecrlf'] = 'fail'
967
+ with pytest.raises(pygit2.GitError):
968
+ h = testrepo.hashfile('hello.txt')
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes