codeplain 0.1.8__py3-none-any.whl → 0.2.1__py3-none-any.whl

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.
@@ -1,32 +1,31 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeplain
3
- Version: 0.1.8
3
+ Version: 0.2.1
4
4
  Summary: Transform plain language specifications into working code
5
+ License-File: LICENSE
5
6
  Classifier: Environment :: Console
6
7
  Classifier: Intended Audience :: Developers
7
8
  Classifier: Operating System :: OS Independent
8
9
  Classifier: Topic :: Software Development :: Code Generators
9
10
  Requires-Python: >=3.11
10
- Description-Content-Type: text/markdown
11
- License-File: LICENSE
12
- Requires-Dist: python-liquid2==0.3.0
11
+ Requires-Dist: gitpython==3.1.42
13
12
  Requires-Dist: mistletoe==1.3.0
13
+ Requires-Dist: networkx==3.6.1
14
+ Requires-Dist: python-frontmatter==1.1.0
15
+ Requires-Dist: python-liquid2==0.3.0
16
+ Requires-Dist: pyyaml==6.0.2
14
17
  Requires-Dist: requests==2.32.3
18
+ Requires-Dist: rich==14.2.0
19
+ Requires-Dist: textual==1.0.0
15
20
  Requires-Dist: tiktoken==0.12.0
16
- Requires-Dist: PyYAML==6.0.2
17
- Requires-Dist: gitpython==3.1.42
18
21
  Requires-Dist: transitions==0.9.3
19
- Requires-Dist: textual==1.0.0
20
- Requires-Dist: rich==14.2.0
21
- Requires-Dist: python-frontmatter==1.1.0
22
- Requires-Dist: networkx==3.6.1
23
22
  Provides-Extra: dev
24
- Requires-Dist: pytest==8.3.5; extra == "dev"
25
- Requires-Dist: flake8==7.0.0; extra == "dev"
26
- Requires-Dist: black==24.2.0; extra == "dev"
27
- Requires-Dist: isort==5.13.2; extra == "dev"
28
- Requires-Dist: mypy==1.11.2; extra == "dev"
29
- Dynamic: license-file
23
+ Requires-Dist: black==24.2.0; extra == 'dev'
24
+ Requires-Dist: flake8==7.0.0; extra == 'dev'
25
+ Requires-Dist: isort==5.13.2; extra == 'dev'
26
+ Requires-Dist: mypy==1.11.2; extra == 'dev'
27
+ Requires-Dist: pytest==8.3.5; extra == 'dev'
28
+ Description-Content-Type: text/markdown
30
29
 
31
30
  # Codeplain plain2code renderer
32
31
 
@@ -1,5 +1,6 @@
1
1
  codeplain_REST_api.py,sha256=qfzUw9v0-NKII4ibw3vqSBrCeU2WP4RcrrEti43F3zs,18400
2
2
  concept_utils.py,sha256=vwSY4-FmxyqaDBxhntYzqG8t9pXvAWwiULP0DYQI32o,7905
3
+ diff_utils.py,sha256=AjiQlqo5pRos_8hVXZo5yBurl5BzSrTMGrQv4dCtRCg,1198
3
4
  event_bus.py,sha256=sduIR3bgIbxAbLhwKd8Gx9YN9gzaeqy9-mNupS04aKw,1759
4
5
  file_utils.py,sha256=4BIxzsteZQOaK-efkvQcoaIfYydsQNFR6elpsxJgXLQ,11591
5
6
  git_utils.py,sha256=gTRps6RIzJJkyy9amaDxP38FPoxYulZViBWr9V0IPQg,12414
@@ -21,9 +22,10 @@ plain_modules.py,sha256=iDqqamtix5KahMC_v-vfQ7yndugmqtBW1z6XxTB4x6w,4876
21
22
  plain_spec.py,sha256=zC-VOb_UJOs8OxtEiwQJuonw7Lkmbi7YHyFvvCvUZNo,13529
22
23
  spinner.py,sha256=Ro6Gd9Przf-whuHqPRY6HwI0T57yJjyNPbhDbigZKZE,2471
23
24
  system_config.py,sha256=mgHLn-CRHLO9Y9vKyI_eFBreY_YhFad-ctZgBYp-rIg,1777
24
- codeplain-0.1.8.dist-info/licenses/LICENSE,sha256=wsFi5dpbJurnRNfBj8q2RCcF3ryrmdRIfxc3lPcmc4c,1069
25
25
  config/__init__.py,sha256=beYSsJWmBNHDP5rYmVDouqgEeP3t1lkkepbXJ-oq0F8,37
26
26
  config/system_config.yaml,sha256=bB5Th5jxgXFyaIvceUPID1ReebMMXsyMibV4gtu9sWQ,1148
27
+ docs/generate_cli.py,sha256=xJmiihtdgqEDBYt_wBsRbniZvIfq7Q6tjAsi3SjoMJg,531
28
+ examples/example_hello_world_python/harness_tests/hello_world_display/test_hello_world.py,sha256=dwTowrHiVKKbrDv21v8xJC30Q57AXZkQasdGOO5JsBE,470
27
29
  render_machine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
30
  render_machine/code_renderer.py,sha256=jOoYivkZzCuGG947YlNPlvD1Dpd2xTWVK8pwfF_OqDU,2957
29
31
  render_machine/conformance_test_helpers.py,sha256=6Ru7Dh24SLNIKWfz_sP5hE_EmWpvk6nfgN1T1yaCZus,2887
@@ -57,6 +59,14 @@ standard_template_library/golang-console-app-template.plain,sha256=M3gE1wZ6o_F-H
57
59
  standard_template_library/python-console-app-template.plain,sha256=ofIJIkNXScGFTx2w8mY3lP4KmRml2GQv2EiEoXlbqqs,1005
58
60
  standard_template_library/typescript-react-app-boilerplate.plain,sha256=6LFxhEOzWnDHdVpFj-XBTq6s4_2dy7LKV3lVkCzv8i4,114
59
61
  standard_template_library/typescript-react-app-template.plain,sha256=SG1cBMVt1ncMRK4cOGOyVVj0MmsQTmF4MI67IBn1lFg,380
62
+ tests/__init__.py,sha256=Wk73Io62J15BtlLVIzxmASDWaaJkQLevS4BLK5LDAQg,16
63
+ tests/conftest.py,sha256=QZcp08htUlJGgmHDlRWFgsWXZ8o8IBWTD5QqJaUMlU8,790
64
+ tests/test_git_utils.py,sha256=Knc4R2ZkHYVj9iPFKhb0_ewaad_mxPrakOn6gpziHMc,13138
65
+ tests/test_imports.py,sha256=cdw7u93xNFD064sjwwEWy4oLHhJNlt1-DRDYhHjymFw,2078
66
+ tests/test_plainfile.py,sha256=N3f59hpxOO5zULdBtmXZM6KO2PKADoOL5d3WxNTUYXg,11106
67
+ tests/test_plainfileparser.py,sha256=9B9r2jEeUKDMhqb4TJEPK7aatYl1op1z92GDAD4fJ20,18685
68
+ tests/test_plainspec.py,sha256=ouS899oejStu8u1D3399y5NAxcKlNbLcinNoxZUp59M,2437
69
+ tests/test_requires.py,sha256=u8wY-UI7Ryat84htaI0yFXpl-PjX7G90EnMKM35S6No,1182
60
70
  tui/__init__.py,sha256=_fy7FowY0RkWdn4kC7XV2jgvQPCj-2gE8fNUb-Mny0A,60
61
71
  tui/components.py,sha256=l7f-jLk0sj9WM8z2IpaHksmSyytXe_bl2GBdHASoIg4,13901
62
72
  tui/models.py,sha256=KMjlWubmPD41GA-BjgiqkaV9v7tLJ0fuWKBDPmZj4JA,1493
@@ -64,8 +74,8 @@ tui/plain2code_tui.py,sha256=7agVU0NnaCuF1LuOSEmjx-XfEOQ6HHr1HlWe_b1Tses,11695
64
74
  tui/state_handlers.py,sha256=HbjgaV-9xGhp3E-3X114zOqPkeNcCjT-R1PbVRxVdso,12674
65
75
  tui/styles.css,sha256=Umm2TLePmywizZGV4Nd8UezZRiK5pFyibYRbpRvGqbs,3056
66
76
  tui/widget_helpers.py,sha256=VJorEM2PjRBzN-jIDmKJPolFgo2d8-2NmTumgC5xeNo,5229
67
- codeplain-0.1.8.dist-info/METADATA,sha256=6cCO9MBjIFzFB4xwk0DzkYOsG9teb9irU43B_oW9kic,4300
68
- codeplain-0.1.8.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
69
- codeplain-0.1.8.dist-info/entry_points.txt,sha256=oDZkBqu9WhtZApb_K6ia8-fn9aojwmAsgnKELceX5T4,46
70
- codeplain-0.1.8.dist-info/top_level.txt,sha256=L9DFaIGhmYbauBxKOmNEl7cc-eYfRAw5XWYaM7B1bcw,620
71
- codeplain-0.1.8.dist-info/RECORD,,
77
+ codeplain-0.2.1.dist-info/METADATA,sha256=UmRjTWg_BVVg4NqJRvOi_Rb6ldLR4EwZDU4X0TpNI3w,4278
78
+ codeplain-0.2.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
79
+ codeplain-0.2.1.dist-info/entry_points.txt,sha256=oDZkBqu9WhtZApb_K6ia8-fn9aojwmAsgnKELceX5T4,46
80
+ codeplain-0.2.1.dist-info/licenses/LICENSE,sha256=wsFi5dpbJurnRNfBj8q2RCcF3ryrmdRIfxc3lPcmc4c,1069
81
+ codeplain-0.2.1.dist-info/RECORD,,
@@ -1,5 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.10.2)
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
-
diff_utils.py ADDED
@@ -0,0 +1,32 @@
1
+ from difflib import unified_diff
2
+
3
+
4
+ def get_unified_diff(filename: str, existing_file_content: str, response_file_content: str) -> str:
5
+ diff = unified_diff(
6
+ existing_file_content.splitlines(keepends=True),
7
+ response_file_content.splitlines(keepends=True),
8
+ fromfile=filename,
9
+ tofile=filename,
10
+ )
11
+
12
+ return "".join(diff)
13
+
14
+
15
+ def get_code_diff(response_files: dict[str, str], existing_files_content: dict[str, str]) -> dict[str, str]:
16
+ code_diff: dict[str, str] = {}
17
+ for file_name in response_files:
18
+ if file_name in existing_files_content and existing_files_content[file_name]:
19
+ if response_files[file_name]:
20
+ unified_diff_result = get_unified_diff(
21
+ file_name,
22
+ existing_files_content[file_name],
23
+ response_files[file_name],
24
+ )
25
+ if unified_diff_result and unified_diff_result.strip():
26
+ code_diff[file_name] = unified_diff_result
27
+ else:
28
+ code_diff[file_name] = f"File {file_name} was deleted."
29
+ else:
30
+ code_diff[file_name] = response_files[file_name]
31
+
32
+ return code_diff
docs/generate_cli.py ADDED
@@ -0,0 +1,20 @@
1
+ import os
2
+ import sys
3
+
4
+ from plain2code_arguments import create_parser
5
+
6
+ # Add the parent directory to the path so we can import plain2code_arguments
7
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
8
+
9
+
10
+ # Get the parser and generate help text
11
+ parser = create_parser()
12
+ help_text = parser.format_help()
13
+
14
+ # Create markdown
15
+ md = "# Plain2Code CLI Reference\n\n```text\n" + help_text + "\n```"
16
+
17
+ # Run generate_cli.py in the docs folder
18
+
19
+ with open("plain2code_cli.md", "w", encoding="utf-8") as f:
20
+ f.write(md)
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import subprocess
4
+ import unittest
5
+
6
+
7
+ class TestHelloWorld(unittest.TestCase):
8
+ def test_hello_world_output(self):
9
+ # Run the hello_world.py script and capture its output
10
+ result = subprocess.run(["python3", "hello_world.py"], capture_output=True, text=True)
11
+
12
+ # Check if the output matches the expected string
13
+ self.assertEqual(result.stdout.strip(), "hello, world")
14
+
15
+
16
+ if __name__ == "__main__":
17
+ unittest.main()
tests/__init__.py ADDED
@@ -0,0 +1 @@
1
+ # Tests package
tests/conftest.py ADDED
@@ -0,0 +1,34 @@
1
+ import os
2
+
3
+ import pytest
4
+
5
+ os.environ["MASTER_KEY"] = "test-master-key"
6
+ os.environ["GOOGLE_API_KEY"] = "test-google-api-key"
7
+
8
+
9
+ @pytest.fixture
10
+ def test_data_path():
11
+ """Returns the path to the test data directory."""
12
+ return os.path.dirname(__file__)
13
+
14
+
15
+ @pytest.fixture
16
+ def load_test_data(test_data_path):
17
+ """Returns a function to load test data files."""
18
+
19
+ def _load(file_name):
20
+ file_path = os.path.join(test_data_path, file_name)
21
+ with open(file_path, "r") as file:
22
+ return file.read()
23
+
24
+ return _load
25
+
26
+
27
+ @pytest.fixture
28
+ def get_test_data_path(test_data_path):
29
+ """Returns a function to get the path to a test data subfolder."""
30
+
31
+ def _get_path(subfolder_name):
32
+ return os.path.join(test_data_path, subfolder_name)
33
+
34
+ return _get_path
@@ -0,0 +1,458 @@
1
+ import os
2
+ import tempfile
3
+ from pathlib import Path
4
+ from textwrap import dedent
5
+
6
+ import pytest
7
+ from git import Repo
8
+
9
+ from git_utils import (
10
+ BASE_FOLDER_COMMIT_MESSAGE,
11
+ FUNCTIONAL_REQUIREMENT_FINISHED_COMMIT_MESSAGE,
12
+ REFACTORED_CODE_COMMIT_MESSAGE,
13
+ add_all_files_and_commit,
14
+ diff,
15
+ init_git_repo,
16
+ revert_changes,
17
+ revert_to_commit_with_frid,
18
+ )
19
+
20
+
21
+ @pytest.fixture
22
+ def temp_repo():
23
+ """Create a temporary git repository for testing."""
24
+ with tempfile.TemporaryDirectory() as temp_dir:
25
+ init_git_repo(temp_dir)
26
+
27
+ # Create and commit initial file
28
+ file_path = Path(temp_dir) / "test.txt"
29
+ file_path.write_text("initial content\nline2\nline3\n")
30
+
31
+ repo = Repo(temp_dir)
32
+ repo.index.add(["test.txt"])
33
+ add_all_files_and_commit(temp_dir, FUNCTIONAL_REQUIREMENT_FINISHED_COMMIT_MESSAGE.format("1.1"))
34
+
35
+ yield temp_dir
36
+
37
+
38
+ @pytest.fixture
39
+ def empty_repo():
40
+ """Create an empty git repository for testing."""
41
+ with tempfile.TemporaryDirectory() as temp_dir:
42
+ init_git_repo(temp_dir)
43
+ yield temp_dir
44
+
45
+
46
+ def test_empty_diff(temp_repo):
47
+ """Test diff when there are no changes."""
48
+ result = diff(temp_repo, "1.1")
49
+ assert result == {}
50
+
51
+
52
+ def test_single_file_change(temp_repo):
53
+ """Test diff with a single file change."""
54
+ repo = Repo(temp_repo)
55
+
56
+ # Modify the file
57
+ file_path = Path(temp_repo) / "test.txt"
58
+ file_path.write_text("modified content\nline2\nline3\n")
59
+ repo.index.add(["test.txt"])
60
+ repo.index.commit("Modified test.txt")
61
+
62
+ # Get diff
63
+ result = diff(temp_repo, "1.1")
64
+
65
+ assert "test.txt" in result
66
+ expected_diff = dedent(
67
+ """
68
+ --- a/test.txt
69
+ +++ b/test.txt
70
+ @@ -1,3 +1,3 @@
71
+ -initial content
72
+ +modified content
73
+ line2
74
+ line3
75
+ """
76
+ ).strip()
77
+ assert result["test.txt"] == expected_diff
78
+
79
+
80
+ def test_multiple_file_changes(temp_repo):
81
+ """Test diff with multiple file changes."""
82
+ repo = Repo(temp_repo)
83
+
84
+ # Create and commit second file
85
+ file2_path = Path(temp_repo) / "file2.txt"
86
+ file2_path.write_text("file2 initial\nline2\n")
87
+
88
+ add_all_files_and_commit(temp_repo, "Added file2.txt", None, "1.2")
89
+
90
+ # Modify both files
91
+ file1_path = Path(temp_repo) / "test.txt"
92
+ file1_path.write_text("file1 modified\nline2\n")
93
+
94
+ file2_path.write_text("file2 modified\nline2")
95
+
96
+ # Get diff
97
+ result = diff(temp_repo, "1.1")
98
+
99
+ # Check first file
100
+ assert "test.txt" in result
101
+ expected_diff1 = dedent(
102
+ """
103
+ --- a/test.txt
104
+ +++ b/test.txt
105
+ @@ -1,3 +1,2 @@
106
+ -initial content
107
+ +file1 modified
108
+ line2
109
+ -line3
110
+ """
111
+ ).strip()
112
+ assert result["test.txt"] == expected_diff1
113
+
114
+ # Check second file
115
+ assert "file2.txt" in result
116
+ expected_diff2 = dedent(
117
+ """
118
+ --- /dev/null
119
+ +++ b/file2.txt
120
+ @@ -0,0 +1,2 @@
121
+ +file2 modified
122
+ +line2
123
+ \
124
+ """
125
+ ).strip()
126
+ assert result["file2.txt"] == expected_diff2
127
+
128
+
129
+ def test_multiple_commits_diff(temp_repo):
130
+ """Test diff with multiple commits."""
131
+ repo = Repo(temp_repo)
132
+
133
+ file1_path = Path(temp_repo) / "test.txt"
134
+
135
+ # Create and commit second file
136
+ file2_path = Path(temp_repo) / "file2.txt"
137
+ file2_path.write_text("file2 frid1.1 refactored version\nline2\n")
138
+
139
+ add_all_files_and_commit(temp_repo, REFACTORED_CODE_COMMIT_MESSAGE.format("1.1"), None, "1.1")
140
+ add_all_files_and_commit(temp_repo, FUNCTIONAL_REQUIREMENT_FINISHED_COMMIT_MESSAGE.format("1.1"), None, "1.1")
141
+
142
+ file1_path.write_text("file1 frid1.2 version\nline2\n")
143
+ file2_path.write_text("file2 frid1.2 version\nline2\n")
144
+
145
+ add_all_files_and_commit(temp_repo, "implemented frid 1.2", None, "1.2")
146
+
147
+ file1_path.write_text("file1 frid1.2 refactored version\nline2\n")
148
+
149
+ add_all_files_and_commit(temp_repo, REFACTORED_CODE_COMMIT_MESSAGE.format("1.2"), None, "1.2")
150
+ add_all_files_and_commit(temp_repo, FUNCTIONAL_REQUIREMENT_FINISHED_COMMIT_MESSAGE.format("1.2"), None, "1.2")
151
+
152
+ file3_path = Path(temp_repo) / "file3.txt"
153
+ file3_path.write_text("file3 frid1.2 new file\nline2\n")
154
+
155
+ # Get diff
156
+ result = diff(temp_repo, "1.1")
157
+
158
+ # Check first file
159
+ assert "test.txt" in result
160
+ expected_diff1 = dedent(
161
+ """
162
+ --- a/test.txt
163
+ +++ b/test.txt
164
+ @@ -1,3 +1,2 @@
165
+ -initial content
166
+ +file1 frid1.2 refactored version
167
+ line2
168
+ -line3
169
+ """
170
+ ).strip()
171
+ assert result["test.txt"] == expected_diff1
172
+
173
+ # Check second file
174
+ assert "file2.txt" in result
175
+ expected_diff2 = dedent(
176
+ """
177
+ --- a/file2.txt
178
+ +++ b/file2.txt
179
+ @@ -1,2 +1,2 @@
180
+ -file2 frid1.1 refactored version
181
+ +file2 frid1.2 version
182
+ line2
183
+ """
184
+ ).strip()
185
+ assert result["file2.txt"] == expected_diff2
186
+
187
+ # Check third file
188
+ assert "file3.txt" in result
189
+ expected_diff3 = dedent(
190
+ """
191
+ --- /dev/null
192
+ +++ b/file3.txt
193
+ @@ -0,0 +1,2 @@
194
+ +file3 frid1.2 new file
195
+ +line2
196
+ """
197
+ ).strip()
198
+ assert result["file3.txt"] == expected_diff3
199
+
200
+
201
+ def test_diff_without_previous_frid_and_no_base_folder(empty_repo):
202
+ """Test diff without previous frid and no base folder."""
203
+ # Create a new file without committing
204
+ file_path = Path(empty_repo) / "new.txt"
205
+ file_path.write_text("new file content\nline2\n")
206
+ add_all_files_and_commit(empty_repo, "First commit")
207
+
208
+ # create one more file
209
+ file_path = Path(empty_repo) / "new2.txt"
210
+ file_path.write_text("new file content\nline2\n")
211
+
212
+ # Get diff
213
+ result = diff(empty_repo)
214
+
215
+ assert "new.txt" in result
216
+ expected_diff = dedent(
217
+ """
218
+ --- /dev/null
219
+ +++ b/new.txt
220
+ @@ -0,0 +1,2 @@
221
+ +new file content
222
+ +line2
223
+ """
224
+ ).strip()
225
+ assert result["new.txt"] == expected_diff
226
+
227
+ assert "new2.txt" in result
228
+ expected_diff2 = dedent(
229
+ """
230
+ --- /dev/null
231
+ +++ b/new2.txt
232
+ @@ -0,0 +1,2 @@
233
+ +new file content
234
+ +line2
235
+ """
236
+ ).strip()
237
+ assert result["new2.txt"] == expected_diff2
238
+
239
+
240
+ def test_diff_without_previous_frid_and_base_folder(temp_repo):
241
+ """Test diff without previous frid and base folder."""
242
+ # Create a commit for the base folder
243
+ file_path = Path(temp_repo) / "new.txt"
244
+ file_path.write_text("base folder content\nline2\n")
245
+ add_all_files_and_commit(temp_repo, BASE_FOLDER_COMMIT_MESSAGE)
246
+
247
+ # update the file
248
+ file_path.write_text("updated base folder content\nline2\n")
249
+
250
+ # Get diff
251
+ result = diff(temp_repo)
252
+
253
+ assert "new.txt" in result
254
+ expected_diff = dedent(
255
+ """
256
+ --- a/new.txt
257
+ +++ b/new.txt
258
+ @@ -1,2 +1,2 @@
259
+ -base folder content
260
+ +updated base folder content
261
+ line2
262
+ """
263
+ ).strip()
264
+ assert result["new.txt"] == expected_diff
265
+
266
+
267
+ def test_new_file(temp_repo):
268
+ """Test diff with a new untracked file."""
269
+ repo = Repo(temp_repo)
270
+
271
+ # Create a new file without committing
272
+ file_path = Path(temp_repo) / "new.txt"
273
+ file_path.write_text("new file content\nline2\n")
274
+
275
+ # Get diff
276
+ result = diff(temp_repo, "1.1")
277
+
278
+ assert "new.txt" in result
279
+ expected_diff = dedent(
280
+ """
281
+ --- /dev/null
282
+ +++ b/new.txt
283
+ @@ -0,0 +1,2 @@
284
+ +new file content
285
+ +line2
286
+ """
287
+ ).strip()
288
+ assert result["new.txt"] == expected_diff
289
+
290
+
291
+ def test_deleted_file(temp_repo):
292
+ """Test diff with a deleted file."""
293
+ repo = Repo(temp_repo)
294
+
295
+ # Delete the file
296
+ file_path = Path(temp_repo) / "test.txt"
297
+ file_path.unlink()
298
+
299
+ # Get diff
300
+ result = diff(temp_repo, "1.1")
301
+
302
+ assert "test.txt" in result
303
+ expected_diff = dedent(
304
+ """
305
+ --- a/test.txt
306
+ +++ /dev/null
307
+ @@ -1,3 +0,0 @@
308
+ -initial content
309
+ -line2
310
+ -line3
311
+ """
312
+ ).strip()
313
+ assert result["test.txt"] == expected_diff
314
+
315
+
316
+ def test_init_clean_repo(empty_repo):
317
+ """Test initializing a clean git repository."""
318
+ repo = Repo(empty_repo)
319
+
320
+ # Verify it's a git repository
321
+ assert repo.git_dir is not None
322
+ assert repo.git_dir.endswith(".git")
323
+
324
+ # Verify there is only one commit
325
+ assert len(list(repo.iter_commits())) == 1
326
+ assert "[Codeplain] Initial module commit" in list(repo.iter_commits())[0].message
327
+
328
+
329
+ def test_add_all_files_and_commit(temp_repo):
330
+ """Test adding all files and committing them."""
331
+ # Create some test files
332
+ file1_path = Path(temp_repo) / "file1.txt"
333
+ file1_path.write_text("content1")
334
+ file2_path = Path(temp_repo) / "file2.txt"
335
+ file2_path.write_text("content2")
336
+
337
+ # Add and commit files
338
+ repo = add_all_files_and_commit(temp_repo, "Test commit", None, "FR123", "render-id")
339
+
340
+ # Verify commit was created
341
+ commits = list(repo.iter_commits())
342
+ assert len(commits) == 3 # initial commit + first FRID commit + test commit
343
+
344
+ # Verify commit message
345
+ commit = commits[0]
346
+ assert "Test commit" in commit.message
347
+ assert "Changes related to Functional requirement ID (FRID): FR123" in commit.message
348
+ assert "Render ID: render-id" in commit.message
349
+
350
+ # Verify files were committed
351
+ tree = commit.tree
352
+ assert "file1.txt" in tree
353
+ assert "file2.txt" in tree
354
+
355
+ file2_path.write_text("content2 modified")
356
+ repo = add_all_files_and_commit(temp_repo, "Commit changes on existing file", None, "FR4")
357
+ commits = list(repo.iter_commits())
358
+ assert len(commits) == 4
359
+ assert "Changes related to Functional requirement ID (FRID): FR4" in commits[0].message
360
+
361
+ repo = add_all_files_and_commit(temp_repo, "Empty commit", None, "FR5")
362
+ commits = list(repo.iter_commits())
363
+ assert len(commits) == 5
364
+ assert "Changes related to Functional requirement ID (FRID): FR5" in commits[0].message
365
+
366
+
367
+ def test_revert_changes(temp_repo):
368
+ """Test reverting changes in the repository."""
369
+ # Create and commit initial file
370
+ file_path = Path(temp_repo) / "test.txt"
371
+ file_path.write_text("initial content")
372
+ repo = add_all_files_and_commit(temp_repo, "Initial commit", None, "FR123")
373
+
374
+ # Modify the file
375
+ file_path.write_text("modified content")
376
+
377
+ # Verify the file was modified
378
+ assert file_path.read_text() == "modified content"
379
+
380
+ # Revert changes
381
+ repo = revert_changes(temp_repo)
382
+
383
+ # Verify the file was reverted
384
+ assert file_path.read_text() == "initial content"
385
+
386
+ # Verify working directory is clean
387
+ assert not repo.is_dirty()
388
+
389
+
390
+ def test_revert_to_commit_with_frid(temp_repo):
391
+ """Test reverting to a specific commit with FRID."""
392
+ # Create and commit first version
393
+ file_path = Path(temp_repo) / "test.txt"
394
+ file_path.write_text("version 1")
395
+ repo = add_all_files_and_commit(
396
+ temp_repo, FUNCTIONAL_REQUIREMENT_FINISHED_COMMIT_MESSAGE.format("FR123"), None, "FR123"
397
+ )
398
+
399
+ # Create and commit second version
400
+ file_path.write_text("version 2")
401
+ repo = add_all_files_and_commit(
402
+ temp_repo, FUNCTIONAL_REQUIREMENT_FINISHED_COMMIT_MESSAGE.format("FR456"), None, "FR456"
403
+ )
404
+
405
+ # Create and commit third version
406
+ file_path.write_text("version 3")
407
+ repo = add_all_files_and_commit(
408
+ temp_repo, FUNCTIONAL_REQUIREMENT_FINISHED_COMMIT_MESSAGE.format("FR789"), None, "FR789"
409
+ )
410
+
411
+ # Revert to FR123
412
+ repo = revert_to_commit_with_frid(temp_repo, "FR123")
413
+
414
+ # Verify we're back at version 1
415
+ assert file_path.read_text() == "version 1"
416
+
417
+ # Verify working directory is clean
418
+ assert not repo.is_dirty()
419
+
420
+ # Test error case - non-existent FRID
421
+ with pytest.raises(Exception) as exc_info:
422
+ revert_to_commit_with_frid(temp_repo, "NONEXISTENT")
423
+ assert "No commit with frid NONEXISTENT found" in str(exc_info.value)
424
+
425
+
426
+ def test_revert_to_commit_with_frid_and_base_folder(temp_repo):
427
+ """Test reverting to base folder."""
428
+ # Create a commit for the base folder
429
+ file_path = Path(temp_repo) / "new.txt"
430
+ file_path.write_text("base folder content\nline1\n")
431
+ add_all_files_and_commit(temp_repo, BASE_FOLDER_COMMIT_MESSAGE)
432
+
433
+ # create another commit
434
+ file_path.write_text("changed file content\nline2\n")
435
+ add_all_files_and_commit(temp_repo, "Another commit")
436
+
437
+ # revert to base folder
438
+ repo = revert_to_commit_with_frid(temp_repo, None)
439
+
440
+ # verify the file was reverted
441
+ assert file_path.read_text() == "base folder content\nline1\n"
442
+
443
+
444
+ def test_revert_to_base_folder_no_commit(temp_repo):
445
+ """Test reverting to base folder."""
446
+ # Create a commit for the base folder
447
+ file_path = Path(temp_repo) / "new.txt"
448
+ file_path.write_text("some content\n")
449
+ add_all_files_and_commit(temp_repo, "FRID", 123)
450
+
451
+ # revert initial commit
452
+ repo = revert_to_commit_with_frid(temp_repo, None)
453
+
454
+ # verify the repo doesn't have any commits and that the file doesn't exist
455
+ assert not repo.is_dirty()
456
+ assert repo.active_branch.name == "main"
457
+ assert len(list(repo.iter_commits())) == 1 # initial commit
458
+ assert not file_path.exists()