codeplain 0.1.0__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.
- codeplain-0.1.0.dist-info/METADATA +142 -0
- codeplain-0.1.0.dist-info/RECORD +51 -0
- codeplain-0.1.0.dist-info/WHEEL +5 -0
- codeplain-0.1.0.dist-info/entry_points.txt +2 -0
- codeplain-0.1.0.dist-info/licenses/LICENSE +21 -0
- codeplain-0.1.0.dist-info/top_level.txt +36 -0
- codeplain_REST_api.py +370 -0
- config/__init__.py +2 -0
- config/system_config.yaml +27 -0
- file_utils.py +316 -0
- git_utils.py +304 -0
- hash_key.py +29 -0
- plain2code.py +218 -0
- plain2code_arguments.py +286 -0
- plain2code_console.py +107 -0
- plain2code_exceptions.py +45 -0
- plain2code_nodes.py +108 -0
- plain2code_read_config.py +74 -0
- plain2code_state.py +75 -0
- plain2code_utils.py +56 -0
- plain_spec.py +360 -0
- render_machine/actions/analyze_specification_ambiguity.py +50 -0
- render_machine/actions/base_action.py +19 -0
- render_machine/actions/commit_conformance_tests_changes.py +46 -0
- render_machine/actions/commit_implementation_code_changes.py +22 -0
- render_machine/actions/create_dist.py +26 -0
- render_machine/actions/exit_with_error.py +22 -0
- render_machine/actions/fix_conformance_test.py +121 -0
- render_machine/actions/fix_unit_tests.py +57 -0
- render_machine/actions/prepare_repositories.py +50 -0
- render_machine/actions/prepare_testing_environment.py +30 -0
- render_machine/actions/refactor_code.py +48 -0
- render_machine/actions/render_conformance_tests.py +169 -0
- render_machine/actions/render_functional_requirement.py +69 -0
- render_machine/actions/run_conformance_tests.py +44 -0
- render_machine/actions/run_unit_tests.py +38 -0
- render_machine/actions/summarize_conformance_tests.py +34 -0
- render_machine/code_renderer.py +50 -0
- render_machine/conformance_test_helpers.py +68 -0
- render_machine/implementation_code_helpers.py +20 -0
- render_machine/render_context.py +280 -0
- render_machine/render_types.py +36 -0
- render_machine/render_utils.py +92 -0
- render_machine/state_machine_config.py +408 -0
- render_machine/states.py +52 -0
- render_machine/triggers.py +27 -0
- standard_template_library/__init__.py +1 -0
- standard_template_library/golang-console-app-template.plain +36 -0
- standard_template_library/python-console-app-template.plain +32 -0
- standard_template_library/typescript-react-app-template.plain +22 -0
- system_config.py +49 -0
file_utils.py
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from liquid2 import Environment, FileSystemLoader, StrictUndefined
|
|
6
|
+
from liquid2.exceptions import UndefinedError
|
|
7
|
+
|
|
8
|
+
import plain_spec
|
|
9
|
+
from plain2code_nodes import Plain2CodeIncludeTag, Plain2CodeLoaderMixin
|
|
10
|
+
|
|
11
|
+
BINARY_FILE_EXTENSIONS = [".pyc"]
|
|
12
|
+
|
|
13
|
+
# Dictionary mapping of file extensions to type names
|
|
14
|
+
FILE_EXTENSION_MAPPING = {
|
|
15
|
+
"": "plaintext",
|
|
16
|
+
".py": "python",
|
|
17
|
+
".txt": "plaintext",
|
|
18
|
+
".md": "markdown",
|
|
19
|
+
".ts": "typescript",
|
|
20
|
+
".tsx": "typescript",
|
|
21
|
+
".js": "javascript",
|
|
22
|
+
".html": "HTML",
|
|
23
|
+
".css": "CSS",
|
|
24
|
+
".scss": "SASS/SCSS",
|
|
25
|
+
".java": "java",
|
|
26
|
+
".cpp": "C++",
|
|
27
|
+
".c": "C",
|
|
28
|
+
".cs": "C#",
|
|
29
|
+
".php": "PHP",
|
|
30
|
+
".rb": "Ruby",
|
|
31
|
+
".go": "Go",
|
|
32
|
+
".rs": "Rust",
|
|
33
|
+
".swift": "Swift",
|
|
34
|
+
".kt": "Kotlin",
|
|
35
|
+
".sql": "SQL",
|
|
36
|
+
".json": "JSON",
|
|
37
|
+
".xml": "XML",
|
|
38
|
+
".yaml": "YAML",
|
|
39
|
+
".yml": "YAML", # YAML has two common extensions
|
|
40
|
+
".sh": "Shell Script",
|
|
41
|
+
".bat": "Batch File",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_file_type(file_name):
|
|
46
|
+
|
|
47
|
+
# Extract the file extension
|
|
48
|
+
ext = Path(file_name).suffix.lower() # Convert to lowercase to handle case-insensitive matching
|
|
49
|
+
|
|
50
|
+
# Use the dictionary to get the file type, defaulting to 'unknown' if the extension is not found
|
|
51
|
+
return FILE_EXTENSION_MAPPING.get(ext, "unknown")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def list_all_text_files(directory):
|
|
55
|
+
all_files = []
|
|
56
|
+
for root, dirs, files in os.walk(directory, topdown=True):
|
|
57
|
+
# Skip .git directory
|
|
58
|
+
if ".git" in dirs:
|
|
59
|
+
dirs.remove(".git")
|
|
60
|
+
|
|
61
|
+
modified_root = os.path.relpath(root, directory)
|
|
62
|
+
if modified_root == ".":
|
|
63
|
+
modified_root = ""
|
|
64
|
+
|
|
65
|
+
for filename in files:
|
|
66
|
+
if not any(filename.endswith(ending) for ending in BINARY_FILE_EXTENSIONS):
|
|
67
|
+
try:
|
|
68
|
+
with open(os.path.join(root, filename), "rb") as f:
|
|
69
|
+
f.read().decode("utf-8")
|
|
70
|
+
except UnicodeDecodeError:
|
|
71
|
+
print(f"WARNING! Not listing {filename} in {root}. File is not a text file. Skipping it.")
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
all_files.append(os.path.join(modified_root, filename))
|
|
75
|
+
|
|
76
|
+
return all_files
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def list_folders_in_directory(directory):
|
|
80
|
+
# List all items in the directory
|
|
81
|
+
items = os.listdir(directory)
|
|
82
|
+
|
|
83
|
+
# Filter out the folders
|
|
84
|
+
folders = [item for item in items if os.path.isdir(os.path.join(directory, item))]
|
|
85
|
+
|
|
86
|
+
return folders
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# delete a folder and all its subfolders and files
|
|
90
|
+
def delete_folder(folder_name):
|
|
91
|
+
if os.path.exists(folder_name):
|
|
92
|
+
shutil.rmtree(folder_name)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def delete_files_and_subfolders(directory):
|
|
96
|
+
total_files_deleted = 0
|
|
97
|
+
total_folders_deleted = 0
|
|
98
|
+
|
|
99
|
+
# Walk the directory in reverse order (bottom-up)
|
|
100
|
+
for root, dirs, files in os.walk(directory, topdown=False):
|
|
101
|
+
# Delete files
|
|
102
|
+
for file in files:
|
|
103
|
+
file_path = os.path.join(root, file)
|
|
104
|
+
os.remove(file_path)
|
|
105
|
+
total_files_deleted += 1
|
|
106
|
+
|
|
107
|
+
# Delete directories
|
|
108
|
+
for dir_ in dirs:
|
|
109
|
+
dir_path = os.path.join(root, dir_)
|
|
110
|
+
os.rmdir(dir_path)
|
|
111
|
+
total_folders_deleted += 1
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def copy_file(source_path, destination_path):
|
|
115
|
+
# Ensure the destination directory exists
|
|
116
|
+
os.makedirs(os.path.dirname(destination_path), exist_ok=True)
|
|
117
|
+
|
|
118
|
+
# Open the source file in read-binary ('rb') mode
|
|
119
|
+
with open(source_path, "rb") as source_file:
|
|
120
|
+
# Open the destination file in write-binary ('wb') mode
|
|
121
|
+
with open(destination_path, "wb") as destination_file:
|
|
122
|
+
# Copy the content from source to destination
|
|
123
|
+
while True:
|
|
124
|
+
# Read a chunk of the source file
|
|
125
|
+
chunk = source_file.read(4096) # Reading in chunks of 4KB
|
|
126
|
+
if not chunk:
|
|
127
|
+
break # End of file reached
|
|
128
|
+
# Write the chunk to the destination file
|
|
129
|
+
destination_file.write(chunk)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def add_current_path_if_no_path(filename):
|
|
133
|
+
# Extract the base name of the file (ignoring any path information)
|
|
134
|
+
basename = os.path.basename(filename)
|
|
135
|
+
|
|
136
|
+
# Compare the basename to the original filename
|
|
137
|
+
# If they are the same, there was no path information in the filename
|
|
138
|
+
if basename == filename:
|
|
139
|
+
# Prepend the current working directory
|
|
140
|
+
return os.path.join(os.getcwd(), filename)
|
|
141
|
+
# If the basename and the original filename differ, path information was present
|
|
142
|
+
return filename
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def get_existing_files_content(build_folder, existing_files):
|
|
146
|
+
existing_files_content = {}
|
|
147
|
+
for file_name in existing_files:
|
|
148
|
+
with open(os.path.join(build_folder, file_name), "rb") as f:
|
|
149
|
+
content = f.read()
|
|
150
|
+
try:
|
|
151
|
+
existing_files_content[file_name] = content.decode("utf-8")
|
|
152
|
+
except UnicodeDecodeError:
|
|
153
|
+
print(f"WARNING! Error loading {file_name}. File is not a text file. Skipping it.")
|
|
154
|
+
|
|
155
|
+
return existing_files_content
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def store_response_files(target_folder, response_files, existing_files):
|
|
159
|
+
for file_name in response_files:
|
|
160
|
+
full_file_name = os.path.join(target_folder, file_name)
|
|
161
|
+
|
|
162
|
+
if response_files[file_name] is None:
|
|
163
|
+
# None content indicates that the file should be deleted.
|
|
164
|
+
if os.path.exists(full_file_name):
|
|
165
|
+
os.remove(full_file_name)
|
|
166
|
+
existing_files.remove(file_name)
|
|
167
|
+
else:
|
|
168
|
+
print(f"WARNING! Cannot delete file! File {full_file_name} does not exist.")
|
|
169
|
+
|
|
170
|
+
continue
|
|
171
|
+
|
|
172
|
+
os.makedirs(os.path.dirname(full_file_name), exist_ok=True)
|
|
173
|
+
|
|
174
|
+
with open(full_file_name, "w") as f:
|
|
175
|
+
f.write(response_files[file_name])
|
|
176
|
+
|
|
177
|
+
if file_name not in existing_files:
|
|
178
|
+
existing_files.append(file_name)
|
|
179
|
+
|
|
180
|
+
return existing_files
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def load_linked_resources(template_dirs: list[str], resources_list):
|
|
184
|
+
linked_resources = {}
|
|
185
|
+
|
|
186
|
+
for resource in resources_list:
|
|
187
|
+
resource_found = False
|
|
188
|
+
for template_dir in template_dirs:
|
|
189
|
+
file_name = resource["target"]
|
|
190
|
+
if file_name in linked_resources:
|
|
191
|
+
continue
|
|
192
|
+
|
|
193
|
+
full_file_name = os.path.join(template_dir, file_name)
|
|
194
|
+
if not os.path.isfile(full_file_name):
|
|
195
|
+
continue
|
|
196
|
+
|
|
197
|
+
with open(full_file_name, "rb") as f:
|
|
198
|
+
content = f.read()
|
|
199
|
+
try:
|
|
200
|
+
linked_resources[file_name] = content.decode("utf-8")
|
|
201
|
+
except UnicodeDecodeError:
|
|
202
|
+
print(
|
|
203
|
+
f"WARNING! Error loading {resource['text']} ({resource['target']}). File is not a text file. Skipping it."
|
|
204
|
+
)
|
|
205
|
+
resource_found = True
|
|
206
|
+
if not resource_found:
|
|
207
|
+
raise FileNotFoundError(
|
|
208
|
+
f"""
|
|
209
|
+
Resource file {resource['target']} not found. Resource files are searched in the following order (highest to lowest precedence):
|
|
210
|
+
|
|
211
|
+
1. The directory containing your .plain file
|
|
212
|
+
2. The directory specified by --template-dir (if provided)
|
|
213
|
+
3. The built-in 'standard_template_library' directory
|
|
214
|
+
|
|
215
|
+
Please ensure that the resource exists in one of these locations, or specify the correct --template-dir if using custom templates.
|
|
216
|
+
"""
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
return linked_resources
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class TrackingFileSystemLoader(Plain2CodeLoaderMixin, FileSystemLoader):
|
|
223
|
+
def __init__(self, *args, **kwargs):
|
|
224
|
+
super().__init__(*args, **kwargs)
|
|
225
|
+
self.loaded_templates = {}
|
|
226
|
+
|
|
227
|
+
def get_source(self, environment, template_name, **kwargs):
|
|
228
|
+
source = super().get_source(environment, template_name, **kwargs)
|
|
229
|
+
self.loaded_templates[template_name] = source.source
|
|
230
|
+
return source
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def get_loaded_templates(source_path, plain_source):
|
|
234
|
+
# Render the plain source with Liquid templating engine
|
|
235
|
+
# to identify the templates that are being loaded
|
|
236
|
+
|
|
237
|
+
liquid_loader = TrackingFileSystemLoader(source_path)
|
|
238
|
+
liquid_env = Environment(loader=liquid_loader, undefined=StrictUndefined)
|
|
239
|
+
liquid_env.tags["include"] = Plain2CodeIncludeTag(liquid_env)
|
|
240
|
+
|
|
241
|
+
liquid_env.filters["code_variable"] = plain_spec.code_variable_liquid_filter
|
|
242
|
+
liquid_env.filters["prohibited_chars"] = plain_spec.prohibited_chars_liquid_filter
|
|
243
|
+
|
|
244
|
+
plain_source_template = liquid_env.from_string(plain_source)
|
|
245
|
+
try:
|
|
246
|
+
plain_source = plain_source_template.render()
|
|
247
|
+
except UndefinedError as e:
|
|
248
|
+
raise Exception(f"Undefined liquid variable: {str(e)}")
|
|
249
|
+
|
|
250
|
+
return plain_source, liquid_loader.loaded_templates
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def update_build_folder_with_rendered_files(build_folder, existing_files, response_files):
|
|
254
|
+
changed_files = set()
|
|
255
|
+
changed_files.update(response_files.keys())
|
|
256
|
+
|
|
257
|
+
existing_files = store_response_files(build_folder, response_files, existing_files)
|
|
258
|
+
|
|
259
|
+
return existing_files, changed_files
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def copy_folder_content(source_folder, destination_folder, ignore_folders=None):
|
|
263
|
+
"""
|
|
264
|
+
Recursively copy all files and folders from source_folder to destination_folder.
|
|
265
|
+
Uses shutil.copytree which handles all edge cases including permissions and symlinks.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
source_folder: Source directory to copy from
|
|
269
|
+
destination_folder: Destination directory to copy to
|
|
270
|
+
ignore_folders: List of folder names to ignore during copy (default: empty list)
|
|
271
|
+
"""
|
|
272
|
+
if ignore_folders is None:
|
|
273
|
+
ignore_folders = []
|
|
274
|
+
|
|
275
|
+
ignore_func = (
|
|
276
|
+
(lambda dir, files: [f for f in files if f in ignore_folders]) if ignore_folders else None # noqa: U100,U101
|
|
277
|
+
)
|
|
278
|
+
shutil.copytree(source_folder, destination_folder, dirs_exist_ok=True, ignore=ignore_func)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def get_template_directories(plain_file_path, custom_template_dir=None, default_template_dir=None) -> list[str]:
|
|
282
|
+
"""Set up template search directories with specific precedence order.
|
|
283
|
+
|
|
284
|
+
The order of directories in the returned list determines template loading precedence.
|
|
285
|
+
Earlier indices (lower numbers) have higher precedence - the first matching template found will be used.
|
|
286
|
+
|
|
287
|
+
Precedence order (highest to lowest):
|
|
288
|
+
1. Directory containing the plain file - for project-specific template overrides
|
|
289
|
+
2. Custom template directory (if provided) - for shared custom templates
|
|
290
|
+
3. Default template directory - for standard/fallback templates
|
|
291
|
+
"""
|
|
292
|
+
template_dirs = [
|
|
293
|
+
os.path.dirname(plain_file_path), # Highest precedence - directory containing plain file
|
|
294
|
+
]
|
|
295
|
+
|
|
296
|
+
if custom_template_dir:
|
|
297
|
+
template_dirs.append(os.path.abspath(custom_template_dir)) # Second highest - custom template dir
|
|
298
|
+
|
|
299
|
+
if default_template_dir:
|
|
300
|
+
# Add standard template directory last - lowest precedence
|
|
301
|
+
template_dirs.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), default_template_dir))
|
|
302
|
+
|
|
303
|
+
return template_dirs
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def copy_folder_to_output(source_folder, output_folder):
|
|
307
|
+
"""Copy source folder contents directly to the specified output folder."""
|
|
308
|
+
# Create output folder if it doesn't exist
|
|
309
|
+
os.makedirs(output_folder, exist_ok=True)
|
|
310
|
+
|
|
311
|
+
# If output folder exists, clean it first to ensure clean copy
|
|
312
|
+
if os.path.exists(output_folder):
|
|
313
|
+
delete_files_and_subfolders(output_folder)
|
|
314
|
+
|
|
315
|
+
# Copy source folder contents directly to output folder (excluding .git)
|
|
316
|
+
copy_folder_content(source_folder, output_folder, ignore_folders=[".git"])
|
git_utils.py
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Optional, Union
|
|
3
|
+
|
|
4
|
+
from git import Repo
|
|
5
|
+
|
|
6
|
+
import file_utils
|
|
7
|
+
|
|
8
|
+
FUNCTIONAL_REQUIREMENT_IMPLEMENTED_COMMIT_MESSAGE = (
|
|
9
|
+
"[Codeplain] Implemented code and unit tests for functional requirement {}"
|
|
10
|
+
)
|
|
11
|
+
REFACTORED_CODE_COMMIT_MESSAGE = "[Codeplain] Refactored code after implementing functional requirement {}"
|
|
12
|
+
CONFORMANCE_TESTS_PASSED_COMMIT_MESSAGE = (
|
|
13
|
+
"[Codeplain] Fixed issues in the implementation code identified during conformance testing"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# Following messages are used as checkpoints in the git history
|
|
17
|
+
# Changing them will break backwards compatibility so change them with care
|
|
18
|
+
FUNCTIONAL_REQUIREMENT_FINISHED_COMMIT_MESSAGE = "[Codeplain] Functional requirement ID (FRID):{} fully implemented"
|
|
19
|
+
INITIAL_COMMIT_MESSAGE = "[Codeplain] Initial commit"
|
|
20
|
+
BASE_FOLDER_COMMIT_MESSAGE = "[Codeplain] Initialize build with Base Folder content"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
RENDERED_FRID_MESSAGE = "Changes related to Functional requirement ID (FRID): {}"
|
|
24
|
+
RENDER_ID_MESSAGE = "Render ID: {}"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class InvalidGitRepositoryError(Exception):
|
|
28
|
+
"""Raised when the git repository is in an invalid state."""
|
|
29
|
+
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def init_git_repo(path_to_repo: Union[str, os.PathLike]) -> Repo:
|
|
34
|
+
"""
|
|
35
|
+
Initializes a new git repository in the given path.
|
|
36
|
+
If folder does not exist, it creates it.
|
|
37
|
+
If the folder already exists, it deletes the content of the folder.
|
|
38
|
+
"""
|
|
39
|
+
if os.path.isdir(path_to_repo):
|
|
40
|
+
file_utils.delete_files_and_subfolders(path_to_repo)
|
|
41
|
+
else:
|
|
42
|
+
os.makedirs(path_to_repo)
|
|
43
|
+
|
|
44
|
+
repo = Repo.init(path_to_repo)
|
|
45
|
+
repo.git.commit("--allow-empty", "-m", INITIAL_COMMIT_MESSAGE)
|
|
46
|
+
|
|
47
|
+
return repo
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def is_dirty(repo_path: Union[str, os.PathLike]) -> bool:
|
|
51
|
+
"""Checks if the repository is dirty."""
|
|
52
|
+
repo = Repo(repo_path)
|
|
53
|
+
return repo.is_dirty(untracked_files=True)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def add_all_files_and_commit(
|
|
57
|
+
repo_path: Union[str, os.PathLike], commit_message: str, frid: Optional[str] = None, render_id: Optional[str] = None
|
|
58
|
+
) -> Repo:
|
|
59
|
+
"""Adds all files to the git repository and commits them."""
|
|
60
|
+
repo = Repo(repo_path)
|
|
61
|
+
repo.git.add(".")
|
|
62
|
+
|
|
63
|
+
message = f"{commit_message}"
|
|
64
|
+
|
|
65
|
+
if frid or render_id:
|
|
66
|
+
message += "\n\n" + "-" * 80
|
|
67
|
+
|
|
68
|
+
if frid:
|
|
69
|
+
message += f"\n\n{RENDERED_FRID_MESSAGE.format(frid)}"
|
|
70
|
+
if render_id:
|
|
71
|
+
message += f"\n\n{RENDER_ID_MESSAGE.format(render_id)}"
|
|
72
|
+
|
|
73
|
+
# Check if there are any changes to commit
|
|
74
|
+
if not repo.is_dirty(untracked_files=True):
|
|
75
|
+
repo.git.commit("--allow-empty", "-m", message)
|
|
76
|
+
else:
|
|
77
|
+
repo.git.commit("-m", message)
|
|
78
|
+
|
|
79
|
+
return repo
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def revert_changes(repo_path: Union[str, os.PathLike]) -> Repo:
|
|
83
|
+
"""Reverts all changes made since the last commit."""
|
|
84
|
+
repo = Repo(repo_path)
|
|
85
|
+
repo.git.reset("--hard")
|
|
86
|
+
repo.git.clean("-xdf")
|
|
87
|
+
return repo
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def revert_to_commit_with_frid(repo_path: Union[str, os.PathLike], frid: Optional[str] = None) -> Repo:
|
|
91
|
+
"""
|
|
92
|
+
Finds commit with given frid mentioned in the commit message and reverts the branch to it.
|
|
93
|
+
|
|
94
|
+
If frid argument is not provided (None), repo is reverted to the initial state. In case the base folder doesn't exist,
|
|
95
|
+
code is reverted to the initial repo commit. Otherwise, the repo is reverted to the base folder commit.
|
|
96
|
+
|
|
97
|
+
It is expected that the repo has at least one commit related to provided frid if frid is not None.
|
|
98
|
+
In case the frid related commit is not found, an exception is raised.
|
|
99
|
+
"""
|
|
100
|
+
repo = Repo(repo_path)
|
|
101
|
+
|
|
102
|
+
commit = _get_commit(repo, frid)
|
|
103
|
+
|
|
104
|
+
if not commit:
|
|
105
|
+
raise InvalidGitRepositoryError("Git repository is in an invalid state. Relevant commit could not be found.")
|
|
106
|
+
|
|
107
|
+
repo.git.reset("--hard", commit)
|
|
108
|
+
repo.git.clean("-xdf")
|
|
109
|
+
return repo
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def checkout_commit_with_frid(repo_path: Union[str, os.PathLike], frid: Optional[str] = None) -> Repo:
|
|
113
|
+
"""
|
|
114
|
+
Finds commit with given frid mentioned in the commit message and checks out that commit.
|
|
115
|
+
|
|
116
|
+
If frid argument is not provided (None), repo is checked out to the initial state. In case the base folder doesn't exist,
|
|
117
|
+
code is checked out to the initial repo commit. Otherwise, the repo is checked out to the base folder commit.
|
|
118
|
+
|
|
119
|
+
It is expected that the repo has at least one commit related to provided frid if frid is not None.
|
|
120
|
+
In case the frid related commit is not found, an exception is raised.
|
|
121
|
+
"""
|
|
122
|
+
repo = Repo(repo_path)
|
|
123
|
+
|
|
124
|
+
commit = _get_commit(repo, frid)
|
|
125
|
+
|
|
126
|
+
if not commit:
|
|
127
|
+
raise InvalidGitRepositoryError("Git repository is in an invalid state. Relevant commit could not be found.")
|
|
128
|
+
|
|
129
|
+
repo.git.checkout(commit)
|
|
130
|
+
return repo
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def checkout_previous_branch(repo_path: Union[str, os.PathLike]) -> Repo:
|
|
134
|
+
"""
|
|
135
|
+
Checks out the previous branch using 'git checkout -'.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
repo_path (str | os.PathLike): Path to the git repository
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Repo: The git repository object
|
|
142
|
+
"""
|
|
143
|
+
repo = Repo(repo_path)
|
|
144
|
+
repo.git.checkout("-")
|
|
145
|
+
return repo
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _get_diff_dict(diff_output: str) -> dict:
|
|
149
|
+
diff_dict = {}
|
|
150
|
+
current_file = None
|
|
151
|
+
current_diff_lines = []
|
|
152
|
+
|
|
153
|
+
lines = diff_output.split("\n")
|
|
154
|
+
i = 0
|
|
155
|
+
|
|
156
|
+
while i < len(lines):
|
|
157
|
+
line = lines[i]
|
|
158
|
+
|
|
159
|
+
if line.startswith("diff --git"):
|
|
160
|
+
# Save previous file's diff if exists
|
|
161
|
+
if current_file and current_diff_lines:
|
|
162
|
+
diff_dict[current_file] = "\n".join(current_diff_lines)
|
|
163
|
+
|
|
164
|
+
# Extract file name from diff --git line
|
|
165
|
+
parts = line.split(" ")
|
|
166
|
+
if len(parts) >= 4:
|
|
167
|
+
# Get the b/ path (new file path)
|
|
168
|
+
current_file = parts[3][2:] if parts[3].startswith("b/") else parts[3]
|
|
169
|
+
current_diff_lines = []
|
|
170
|
+
|
|
171
|
+
# Skip the diff --git line
|
|
172
|
+
i += 1
|
|
173
|
+
|
|
174
|
+
# Skip the index line if it exists
|
|
175
|
+
while i < len(lines) and (
|
|
176
|
+
lines[i].startswith("index ")
|
|
177
|
+
or lines[i].startswith("new file mode ")
|
|
178
|
+
or lines[i].startswith("deleted file mode ")
|
|
179
|
+
):
|
|
180
|
+
i += 1
|
|
181
|
+
|
|
182
|
+
continue
|
|
183
|
+
|
|
184
|
+
# Add all other lines to current diff
|
|
185
|
+
if current_file is not None:
|
|
186
|
+
current_diff_lines.append(line)
|
|
187
|
+
|
|
188
|
+
i += 1
|
|
189
|
+
|
|
190
|
+
# Don't forget the last file
|
|
191
|
+
if current_file and current_diff_lines:
|
|
192
|
+
diff_dict[current_file] = "\n".join(current_diff_lines)
|
|
193
|
+
|
|
194
|
+
return diff_dict
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def diff(repo_path: Union[str, os.PathLike], previous_frid: str = None) -> dict:
|
|
198
|
+
"""
|
|
199
|
+
Get the git diff between the current code state and the previous frid using git's native diff command.
|
|
200
|
+
If previous_frid is not provided, we try to find the commit related to the copy of the base folder.
|
|
201
|
+
Removes the 'diff --git' and 'index' lines to get clean unified diff format.
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
repo_path (str | os.PathLike): Path to the git repository
|
|
206
|
+
previous_frid (str): Functional requirement ID (FRID) of the previous commit
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
dict: Dictionary with file names as keys and their clean diff strings as values
|
|
210
|
+
"""
|
|
211
|
+
repo = Repo(repo_path)
|
|
212
|
+
|
|
213
|
+
commit = _get_commit(repo, previous_frid)
|
|
214
|
+
|
|
215
|
+
# Add all files to the index to get a clean diff
|
|
216
|
+
repo.git.add("-N", ".")
|
|
217
|
+
|
|
218
|
+
# Get the raw git diff output, excluding .pyc files
|
|
219
|
+
diff_output = repo.git.diff(commit, "--text", ":!*.pyc")
|
|
220
|
+
|
|
221
|
+
if not diff_output:
|
|
222
|
+
return {}
|
|
223
|
+
|
|
224
|
+
return _get_diff_dict(diff_output)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _get_commit(repo: Repo, frid: Optional[str]) -> str:
|
|
228
|
+
if frid:
|
|
229
|
+
commit = _get_commit_with_frid(repo, frid)
|
|
230
|
+
else:
|
|
231
|
+
commit = _get_base_folder_commit(repo)
|
|
232
|
+
if not commit:
|
|
233
|
+
commit = _get_initial_commit(repo)
|
|
234
|
+
|
|
235
|
+
return commit
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _get_commit_with_frid(repo: Repo, frid: str) -> str:
|
|
239
|
+
"""Finds commit with given frid mentioned in the commit message."""
|
|
240
|
+
commit = _get_commit_with_message(repo, FUNCTIONAL_REQUIREMENT_FINISHED_COMMIT_MESSAGE.format(frid))
|
|
241
|
+
if not commit:
|
|
242
|
+
raise InvalidGitRepositoryError(f"No commit with frid {frid} found.")
|
|
243
|
+
return commit
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _get_base_folder_commit(repo: Repo) -> str:
|
|
247
|
+
"""Finds commit related to copy of the base folder."""
|
|
248
|
+
return _get_commit_with_message(repo, BASE_FOLDER_COMMIT_MESSAGE)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _get_initial_commit(repo: Repo) -> str:
|
|
252
|
+
"""Finds initial commit."""
|
|
253
|
+
return _get_commit_with_message(repo, INITIAL_COMMIT_MESSAGE)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _get_commit_with_message(repo: Repo, message: str) -> str:
|
|
257
|
+
"""Finds commit with given message."""
|
|
258
|
+
escaped_message = message.replace("[", "\\[").replace("]", "\\]")
|
|
259
|
+
|
|
260
|
+
return repo.git.rev_list(repo.active_branch.name, "--grep", escaped_message, "-n", "1")
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def get_implementation_code_diff(repo_path: Union[str, os.PathLike], frid: str, previous_frid: str) -> dict:
|
|
264
|
+
repo = Repo(repo_path)
|
|
265
|
+
|
|
266
|
+
implementation_commit = _get_commit_with_message(repo, REFACTORED_CODE_COMMIT_MESSAGE.format(frid))
|
|
267
|
+
if not implementation_commit:
|
|
268
|
+
implementation_commit = _get_commit_with_message(
|
|
269
|
+
repo, FUNCTIONAL_REQUIREMENT_IMPLEMENTED_COMMIT_MESSAGE.format(frid)
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
previous_frid_commit = _get_commit(repo, previous_frid)
|
|
273
|
+
|
|
274
|
+
# Get the raw git diff output, excluding .pyc files
|
|
275
|
+
diff_output = repo.git.diff(previous_frid_commit, implementation_commit, "--text", ":!*.pyc")
|
|
276
|
+
|
|
277
|
+
if not diff_output:
|
|
278
|
+
return {}
|
|
279
|
+
|
|
280
|
+
return _get_diff_dict(diff_output)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def get_fixed_implementation_code_diff(repo_path: Union[str, os.PathLike], frid: str) -> dict:
|
|
284
|
+
repo = Repo(repo_path)
|
|
285
|
+
|
|
286
|
+
implementation_commit = _get_commit_with_message(repo, REFACTORED_CODE_COMMIT_MESSAGE.format(frid))
|
|
287
|
+
if not implementation_commit:
|
|
288
|
+
implementation_commit = _get_commit_with_message(
|
|
289
|
+
repo, FUNCTIONAL_REQUIREMENT_IMPLEMENTED_COMMIT_MESSAGE.format(frid)
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
conformance_tests_passed_commit = _get_commit_with_message(
|
|
293
|
+
repo, CONFORMANCE_TESTS_PASSED_COMMIT_MESSAGE.format(frid)
|
|
294
|
+
)
|
|
295
|
+
if not conformance_tests_passed_commit:
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
# Get the raw git diff output, excluding .pyc files
|
|
299
|
+
diff_output = repo.git.diff(implementation_commit, conformance_tests_passed_commit, "--text", ":!*.pyc")
|
|
300
|
+
|
|
301
|
+
if not diff_output:
|
|
302
|
+
return {}
|
|
303
|
+
|
|
304
|
+
return _get_diff_dict(diff_output)
|
hash_key.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def hash_api_key(api_key):
|
|
6
|
+
"""Hash the provided API key using SHA-256 and return the hash as a hex string."""
|
|
7
|
+
try:
|
|
8
|
+
hash_object = hashlib.sha256(api_key.encode())
|
|
9
|
+
hex_dig = hash_object.hexdigest()
|
|
10
|
+
return hex_dig
|
|
11
|
+
except Exception as e:
|
|
12
|
+
error_message = f"An error occurred while hashing the API key: {str(e)}"
|
|
13
|
+
raise Exception(error_message)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
if __name__ == "__main__":
|
|
17
|
+
if len(sys.argv) != 2:
|
|
18
|
+
print("Error: Exactly one argument must be provided for the API key.")
|
|
19
|
+
print(f"Usage: python {sys.argv[0]} <api_key>")
|
|
20
|
+
sys.exit(1)
|
|
21
|
+
|
|
22
|
+
api_key = sys.argv[1]
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
hashed_key = hash_api_key(api_key)
|
|
26
|
+
print(f"Hashed API Key: {hashed_key}")
|
|
27
|
+
except Exception as e:
|
|
28
|
+
print(f"Error: {str(e)}")
|
|
29
|
+
sys.exit(1)
|