diff --git a/.gitignore b/.gitignore index cd12c72a9..25cf069ab 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ tests/unit_server/**/output/** ### LIT/FileCheck integration tests' artifacts. ### tests/integration/**/Output/** +# This helps to gitignore fake git repositories that are created by the integration tests. tests/integration/**/output/** tests/integration/**/sandbox/ .lit_test_times.txt diff --git a/strictdoc/export/html/generators/document.py b/strictdoc/export/html/generators/document.py index 4cfb5b9b9..4ac8e5d81 100644 --- a/strictdoc/export/html/generators/document.py +++ b/strictdoc/export/html/generators/document.py @@ -7,6 +7,7 @@ ) from strictdoc.export.html.html_templates import HTMLTemplates from strictdoc.export.html.renderers.link_renderer import LinkRenderer +from strictdoc.helpers.git_client import GitClient class DocumentHTMLGenerator: @@ -17,6 +18,7 @@ def export( traceability_index, markup_renderer, link_renderer: LinkRenderer, + git_client: GitClient, standalone: bool, html_templates: HTMLTemplates, ): @@ -27,6 +29,7 @@ def export( project_config=project_config, link_renderer=link_renderer, markup_renderer=markup_renderer, + git_client=git_client, standalone=standalone, ) return view_object.render_screen(html_templates.jinja_environment()) diff --git a/strictdoc/export/html/generators/document_deep_trace.py b/strictdoc/export/html/generators/document_deep_trace.py index 0f3ba2e86..f393b325c 100644 --- a/strictdoc/export/html/generators/document_deep_trace.py +++ b/strictdoc/export/html/generators/document_deep_trace.py @@ -6,6 +6,7 @@ ) from strictdoc.export.html.html_templates import HTMLTemplates from strictdoc.export.html.renderers.link_renderer import LinkRenderer +from strictdoc.helpers.git_client import GitClient class DocumentDeepTraceHTMLGenerator: @@ -16,6 +17,7 @@ def export_deep( traceability_index, markup_renderer, link_renderer: LinkRenderer, + git_client: GitClient, html_templates: HTMLTemplates, ): view_object = DocumentScreenViewObject( @@ -25,6 +27,7 @@ def export_deep( project_config=project_config, link_renderer=link_renderer, markup_renderer=markup_renderer, + git_client=git_client, standalone=False, ) return view_object.render_screen(html_templates.jinja_environment()) diff --git a/strictdoc/export/html/generators/document_pdf.py b/strictdoc/export/html/generators/document_pdf.py index c28113c57..02ef57217 100644 --- a/strictdoc/export/html/generators/document_pdf.py +++ b/strictdoc/export/html/generators/document_pdf.py @@ -7,6 +7,7 @@ ) from strictdoc.export.html.html_templates import HTMLTemplates from strictdoc.export.html.renderers.link_renderer import LinkRenderer +from strictdoc.helpers.git_client import GitClient class DocumentHTML2PDFGenerator: @@ -17,6 +18,7 @@ def export( traceability_index, markup_renderer, link_renderer: LinkRenderer, + git_client: GitClient, standalone: bool, html_templates: HTMLTemplates, ): @@ -27,6 +29,7 @@ def export( project_config=project_config, link_renderer=link_renderer, markup_renderer=markup_renderer, + git_client=git_client, standalone=standalone, ) return view_object.render_screen(html_templates.jinja_environment()) diff --git a/strictdoc/export/html/generators/document_table.py b/strictdoc/export/html/generators/document_table.py index 95f9f4b69..aea17c721 100644 --- a/strictdoc/export/html/generators/document_table.py +++ b/strictdoc/export/html/generators/document_table.py @@ -6,6 +6,7 @@ ) from strictdoc.export.html.html_templates import HTMLTemplates from strictdoc.export.html.renderers.link_renderer import LinkRenderer +from strictdoc.helpers.git_client import GitClient class DocumentTableHTMLGenerator: @@ -16,6 +17,7 @@ def export( traceability_index, markup_renderer, link_renderer: LinkRenderer, + git_client: GitClient, html_templates: HTMLTemplates, ): view_object = DocumentScreenViewObject( @@ -25,6 +27,7 @@ def export( project_config=project_config, link_renderer=link_renderer, markup_renderer=markup_renderer, + git_client=git_client, standalone=False, ) return view_object.render_screen(html_templates.jinja_environment()) diff --git a/strictdoc/export/html/generators/document_trace.py b/strictdoc/export/html/generators/document_trace.py index b3210b892..3de49edb9 100644 --- a/strictdoc/export/html/generators/document_trace.py +++ b/strictdoc/export/html/generators/document_trace.py @@ -6,6 +6,7 @@ ) from strictdoc.export.html.html_templates import HTMLTemplates from strictdoc.export.html.renderers.link_renderer import LinkRenderer +from strictdoc.helpers.git_client import GitClient class DocumentTraceHTMLGenerator: @@ -16,6 +17,7 @@ def export( traceability_index, markup_renderer, link_renderer: LinkRenderer, + git_client: GitClient, html_templates: HTMLTemplates, ): view_object = DocumentScreenViewObject( @@ -25,6 +27,7 @@ def export( project_config=project_config, link_renderer=link_renderer, markup_renderer=markup_renderer, + git_client=git_client, standalone=False, ) return view_object.render_screen(html_templates.jinja_environment()) diff --git a/strictdoc/export/html/generators/view_objects/document_screen_view_object.py b/strictdoc/export/html/generators/view_objects/document_screen_view_object.py index 0775bc9ed..2fc1714ed 100644 --- a/strictdoc/export/html/generators/view_objects/document_screen_view_object.py +++ b/strictdoc/export/html/generators/view_objects/document_screen_view_object.py @@ -19,6 +19,8 @@ from strictdoc.export.html.html_templates import JinjaEnvironment from strictdoc.export.html.renderers.link_renderer import LinkRenderer from strictdoc.export.html.renderers.markup_renderer import MarkupRenderer +from strictdoc.helpers.git_client import GitClient +from strictdoc.helpers.string import interpolate_at_pattern_lazy from strictdoc.server.helpers.turbo import render_turbo_stream @@ -33,6 +35,7 @@ def __init__( project_config: ProjectConfig, link_renderer: LinkRenderer, markup_renderer: MarkupRenderer, + git_client: GitClient, standalone: bool, ): self.document_type: DocumentType = document_type @@ -42,6 +45,7 @@ def __init__( self.project_config: ProjectConfig = project_config self.link_renderer: LinkRenderer = link_renderer self.markup_renderer: MarkupRenderer = markup_renderer + self.git_client: GitClient = git_client self.standalone: bool = standalone self.document_iterator = self.traceability_index.get_document_iterator( self.document @@ -190,6 +194,21 @@ def render_update_document_content_with_moved_node( ) return output + def render_document_version(self) -> Optional[str]: + if self.document.config.version is None: + return None + + def resolver(variable_name): + if variable_name == "GIT_VERSION": + return self.git_client.get_commit_hash() + elif variable_name == "GIT_BRANCH": + return self.git_client.get_branch() + return variable_name + + return interpolate_at_pattern_lazy( + self.document.config.version, resolver + ) + def is_empty_tree(self) -> bool: return self.document_tree_iterator.is_empty_tree() diff --git a/strictdoc/export/html/html_generator.py b/strictdoc/export/html/html_generator.py index 4ea8a57e6..173c92c63 100644 --- a/strictdoc/export/html/html_generator.py +++ b/strictdoc/export/html/html_generator.py @@ -44,6 +44,7 @@ from strictdoc.export.html.renderers.markup_renderer import MarkupRenderer from strictdoc.export.html.tools.html_embedded import HTMLEmbedder from strictdoc.helpers.file_system import sync_dir +from strictdoc.helpers.git_client import GitClient from strictdoc.helpers.paths import SDocRelativePath from strictdoc.helpers.timing import measure_performance @@ -54,6 +55,7 @@ def __init__( ): self.project_config: ProjectConfig = project_config self.html_templates = html_templates + self.git_client: GitClient = GitClient(commit_hash=None) def export_complete_tree( self, @@ -326,6 +328,7 @@ def export_single_document( traceability_index, markup_renderer, link_renderer, + git_client=self.git_client, standalone=False, html_templates=self.html_templates, ) @@ -346,7 +349,8 @@ def export_single_document( traceability_index, markup_renderer, link_renderer, - self.html_templates, + git_client=self.git_client, + html_templates=self.html_templates, ) document_out_file = document_meta.get_html_table_path() with open(document_out_file, "w", encoding="utf8") as file: @@ -365,7 +369,8 @@ def export_single_document( traceability_index, markup_renderer, link_renderer, - self.html_templates, + git_client=self.git_client, + html_templates=self.html_templates, ) document_out_file = document_meta.get_html_traceability_path() with open(document_out_file, "w", encoding="utf8") as file: @@ -384,7 +389,8 @@ def export_single_document( traceability_index, markup_renderer, link_renderer, - self.html_templates, + git_client=self.git_client, + html_templates=self.html_templates, ) document_out_file = document_meta.get_html_deep_traceability_path() with open(document_out_file, "w", encoding="utf8") as file: @@ -401,6 +407,7 @@ def export_single_document( traceability_index, markup_renderer, link_renderer, + git_client=self.git_client, standalone=False, html_templates=self.html_templates, ) @@ -418,6 +425,7 @@ def export_single_document( traceability_index, markup_renderer, link_renderer, + git_client=self.git_client, standalone=True, html_templates=self.html_templates, ) diff --git a/strictdoc/export/html/templates/components/node_field/document_meta/index.jinja b/strictdoc/export/html/templates/components/node_field/document_meta/index.jinja index 4908958cf..1d45af98a 100644 --- a/strictdoc/export/html/templates/components/node_field/document_meta/index.jinja +++ b/strictdoc/export/html/templates/components/node_field/document_meta/index.jinja @@ -8,10 +8,12 @@ {%- endwith -%} {%- endif -%} - {%- if view_object.document.config.version -%} + + {% set document_version_ = view_object.render_document_version() %} + {%- if document_version_ is not none -%} VERSION: - {%- with field_content = view_object.document.config.version %} + {%- with field_content = document_version_ %} {%- include "components/field/index.jinja" -%} {%- endwith -%} diff --git a/strictdoc/export/html2pdf/html2pdf_generator.py b/strictdoc/export/html2pdf/html2pdf_generator.py index e7fa493f4..54688e5c4 100644 --- a/strictdoc/export/html2pdf/html2pdf_generator.py +++ b/strictdoc/export/html2pdf/html2pdf_generator.py @@ -14,6 +14,7 @@ from strictdoc.export.html.renderers.markup_renderer import MarkupRenderer from strictdoc.export.html2pdf.pdf_print_driver import PDFPrintDriver from strictdoc.helpers.exception import StrictDocException +from strictdoc.helpers.git_client import GitClient from strictdoc.helpers.timing import measure_performance @@ -29,6 +30,8 @@ def export_tree( if not project_config.is_activated_html2pdf(): raise StrictDocException("HTML2PDF feature is not enabled") + git_client: GitClient = GitClient(commit_hash=None) + path_to_output_pdf_html_dir = os.path.join(output_html2pdf_root, "html") path_to_output_pdf_pdf_dir = os.path.join(output_html2pdf_root, "pdf") @@ -64,6 +67,7 @@ def export_tree( traceability_index, markup_renderer, link_renderer, + git_client=git_client, standalone=False, html_templates=html_templates, ) diff --git a/strictdoc/helpers/git_client.py b/strictdoc/helpers/git_client.py index aac1a9fc0..a6944778c 100644 --- a/strictdoc/helpers/git_client.py +++ b/strictdoc/helpers/git_client.py @@ -7,6 +7,7 @@ class GitClient: def __init__(self, commit_hash: Optional[str]): self._commit_hash: Optional[str] = commit_hash + self._branch: Optional[str] = None @staticmethod def create(): @@ -26,4 +27,43 @@ def create(): return GitClient(commit_hash=commit_hash) def get_commit_hash(self) -> Optional[str]: + if self._commit_hash is not None: + return self._commit_hash + try: + process_result = subprocess.run( + ["git", "describe", "--always", "--tags"], + cwd=os.getcwd(), + capture_output=True, + text=True, + check=False, + ) + if process_result.returncode != 0: + return "N/A" + commit_hash = process_result.stdout + except subprocess.CalledProcessError: + commit_hash = "N/A" + except FileNotFoundError: + commit_hash = "Git not available" + self._commit_hash = commit_hash.strip() return self._commit_hash + + def get_branch(self) -> Optional[str]: + if self._branch is not None: + return self._branch + try: + process_result = subprocess.run( + ["git", "rev-parse", "--abbrev-ref", "HEAD"], + cwd=os.getcwd(), + capture_output=True, + text=True, + check=False, + ) + if process_result.returncode != 0: + return "N/A" + branch = process_result.stdout + except subprocess.CalledProcessError: + branch = "N/A" + except FileNotFoundError: + branch = "Git not available" + self._branch = branch.strip() + return self._branch diff --git a/strictdoc/helpers/string.py b/strictdoc/helpers/string.py index 3bc5fcfa3..cc835f5ee 100644 --- a/strictdoc/helpers/string.py +++ b/strictdoc/helpers/string.py @@ -90,3 +90,13 @@ def create_safe_document_file_name(string) -> str: def ensure_newline(text: str) -> str: return text.rstrip() + "\n" + + +def interpolate_at_pattern_lazy(template: str, value_resolver) -> str: + pattern = r"@(\w+)" + + def replace_variable(match): + variable_name = match.group(1) + return value_resolver(variable_name) + + return re.sub(pattern, replace_variable, template) diff --git a/strictdoc/server/routers/main_router.py b/strictdoc/server/routers/main_router.py index 8effa00ca..c97010460 100644 --- a/strictdoc/server/routers/main_router.py +++ b/strictdoc/server/routers/main_router.py @@ -221,6 +221,7 @@ def requirement__show_full(reference_mid: str): project_config=project_config, link_renderer=link_renderer, markup_renderer=markup_renderer, + git_client=html_generator.git_client, standalone=False, ) output = env().render_template_as_markup( @@ -448,6 +449,7 @@ async def create_section(request: Request): project_config=project_config, link_renderer=link_renderer, markup_renderer=markup_renderer, + git_client=html_generator.git_client, standalone=False, ) @@ -603,6 +605,7 @@ async def put_update_section(request: Request): project_config=project_config, link_renderer=link_renderer, markup_renderer=markup_renderer, + git_client=html_generator.git_client, standalone=False, ) output = env().render_template_as_markup( @@ -663,6 +666,7 @@ def cancel_edit_section(section_mid: str): project_config=project_config, link_renderer=link_renderer, markup_renderer=markup_renderer, + git_client=html_generator.git_client, standalone=False, ) output = env().render_template_as_markup( @@ -964,6 +968,7 @@ async def create_requirement(request: Request): project_config=project_config, link_renderer=link_renderer, markup_renderer=markup_renderer, + git_client=html_generator.git_client, standalone=False, ) @@ -1191,6 +1196,7 @@ async def document__update_requirement(request: Request): project_config=project_config, link_renderer=link_renderer, markup_renderer=markup_renderer, + git_client=html_generator.git_client, standalone=False, ) @@ -1256,6 +1262,7 @@ def cancel_edit_requirement(requirement_mid: str): project_config=project_config, link_renderer=link_renderer, markup_renderer=markup_renderer, + git_client=html_generator.git_client, standalone=False, ) return HTMLResponse( @@ -1360,6 +1367,7 @@ def delete_section( project_config=project_config, link_renderer=link_renderer, markup_renderer=markup_renderer, + git_client=html_generator.git_client, standalone=False, ) output = env().render_template_as_markup( @@ -1459,6 +1467,7 @@ def delete_requirement( project_config=project_config, link_renderer=link_renderer, markup_renderer=markup_renderer, + git_client=html_generator.git_client, standalone=False, ) output = env().render_template_as_markup( @@ -1563,6 +1572,7 @@ async def move_node(request: Request): project_config=project_config, link_renderer=link_renderer, markup_renderer=markup_renderer, + git_client=html_generator.git_client, standalone=False, ) return HTMLResponse( @@ -1951,6 +1961,7 @@ async def document__save_edit_config(request: Request): project_config=project_config, link_renderer=link_renderer, markup_renderer=markup_renderer, + git_client=html_generator.git_client, standalone=False, ) html_output = env().render_template_as_markup( @@ -2035,6 +2046,7 @@ async def document__save_included_document(request: Request): project_config=project_config, link_renderer=link_renderer, markup_renderer=markup_renderer, + git_client=html_generator.git_client, standalone=False, ) return HTMLResponse( @@ -2071,6 +2083,7 @@ def document__cancel_edit_config(document_mid: str): project_config=project_config, link_renderer=link_renderer, markup_renderer=markup_renderer, + git_client=html_generator.git_client, standalone=False, ) output = env().render_template_as_markup( @@ -2116,6 +2129,7 @@ def document__cancel_edit_included_document(document_mid: str): project_config=project_config, link_renderer=link_renderer, markup_renderer=markup_renderer, + git_client=html_generator.git_client, standalone=False, ) output = env().render_template_as_markup( @@ -2224,6 +2238,7 @@ async def document__save_grammar(request: Request): project_config=project_config, link_renderer=link_renderer, markup_renderer=markup_renderer, + git_client=html_generator.git_client, standalone=False, ) output = ( @@ -2366,6 +2381,7 @@ async def document__save_grammar_element(request: Request): project_config=project_config, link_renderer=link_renderer, markup_renderer=markup_renderer, + git_client=html_generator.git_client, standalone=False, ) output = ( @@ -2573,6 +2589,7 @@ def get_export_html2pdf(document_mid: str): # noqa: ARG001 export_action.traceability_index, markup_renderer, link_renderer, + git_client=html_generator.git_client, standalone=False, html_templates=html_templates, ) diff --git a/tasks.py b/tasks.py index b31979eeb..162467e16 100644 --- a/tasks.py +++ b/tasks.py @@ -4,7 +4,9 @@ import os import re import sys +import tempfile from enum import Enum +from pathlib import Path from typing import Dict, Optional if not hasattr(inspect, "getargspec"): @@ -23,6 +25,8 @@ # properly. sys.stdout = open(1, "w", encoding="utf-8", closefd=False, buffering=1) +STRICTDOC_TMP_DIR = os.path.join(tempfile.gettempdir(), "strictdoc_tmp_dir") + # To prevent all tasks from building to the same virtual environment. # All values correspond to the configuration in the tox.ini config file. @@ -96,8 +100,9 @@ def clean(context): @task def clean_itest_artifacts(context): # https://unix.stackexchange.com/a/689930/77389 - find_command = """ - git clean -dfX tests/integration/ + find_command = f""" + git clean -dfX tests/integration/ && + rm -rf {STRICTDOC_TMP_DIR} """ # The command sometimes exits with 1 even if the files are deleted. # warn=True ensures that the execution continues. @@ -311,6 +316,8 @@ def test_integration( ): clean_itest_artifacts(context) + Path(STRICTDOC_TMP_DIR).mkdir(exist_ok=True) + cwd = os.getcwd() if strictdoc is None: @@ -345,6 +352,7 @@ def test_integration( itest_command = f""" lit --param STRICTDOC_EXEC="{strictdoc_exec}" + --param STRICTDOC_TMP_DIR="{STRICTDOC_TMP_DIR}" {html2pdf_param} {chromedriver_param} -v diff --git a/tests/integration/features/html/frontpage_document_meta/11_git_version/Output b/tests/integration/features/html/frontpage_document_meta/11_git_version/Output new file mode 160000 index 000000000..43b6f42f4 --- /dev/null +++ b/tests/integration/features/html/frontpage_document_meta/11_git_version/Output @@ -0,0 +1 @@ +Subproject commit 43b6f42f488af304d6640d47b4b38ed9a8eecf7e diff --git a/tests/integration/features/html/frontpage_document_meta/11_git_version/input.sdoc b/tests/integration/features/html/frontpage_document_meta/11_git_version/input.sdoc new file mode 100644 index 000000000..5b37a1f62 --- /dev/null +++ b/tests/integration/features/html/frontpage_document_meta/11_git_version/input.sdoc @@ -0,0 +1,3 @@ +[DOCUMENT] +TITLE: Hello world doc +VERSION: @GIT_VERSION, @GIT_BRANCH diff --git a/tests/integration/features/html/frontpage_document_meta/11_git_version/test.itest b/tests/integration/features/html/frontpage_document_meta/11_git_version/test.itest new file mode 100644 index 000000000..91115d34d --- /dev/null +++ b/tests/integration/features/html/frontpage_document_meta/11_git_version/test.itest @@ -0,0 +1,41 @@ +REQUIRES: PLATFORM_IS_NOT_WINDOWS + +# This is a rare case where an integration test has to create a Git repository. +# Creating a repository outside the strictdoc folder to not cause Git to report +# that there are embedded Git repositories. +RUN: mkdir -p /tmp/strictdoc_itests +RUN: THIS_TMP_DIR=$(mktemp -d %STRICTDOC_TMP_DIR/XXXXXX); cd $THIS_TMP_DIR +RUN: THIS_TMP_DIR_NAME=$(basename "$THIS_TMP_DIR") +RUN: echo $THIS_TMP_DIR + +# FIXME: This is not cleaned automatically. + +RUN: cp %S/input.sdoc $THIS_TMP_DIR/ + +RUN: git init +RUN: %strictdoc export . --output-dir Output +RUN: %cat "$THIS_TMP_DIR/Output/html/$THIS_TMP_DIR_NAME/input.html" | filecheck %s --dump-input=fail --check-prefix CHECK-1 +CHECK-1: N/A, N/A + +RUN: git config user.name "Test User" +RUN: git config user.email "test@example.com" +RUN: git add . +RUN: git commit -m "Initial commit" +RUN: rm -rf Output/ +RUN: %strictdoc export . --output-dir Output +RUN: %cat "$THIS_TMP_DIR/Output/html/$THIS_TMP_DIR_NAME/input.html" | filecheck %s --dump-input=fail --check-prefix CHECK-2 +CHECK-2: {{[a-f0-9]{7}}}, main + +RUN: git tag v1.0 +RUN: rm -rf Output/ +RUN: %strictdoc export . --output-dir Output +RUN: %cat "$THIS_TMP_DIR/Output/html/$THIS_TMP_DIR_NAME/input.html" | filecheck %s --dump-input=fail --check-prefix CHECK-3 +CHECK-3: v1.0, main + +RUN: touch foo +RUN: git add . +RUN: git commit -m "Second commit" +RUN: rm -rf Output/ +RUN: %strictdoc export . --output-dir Output +RUN: %cat "$THIS_TMP_DIR/Output/html/$THIS_TMP_DIR_NAME/input.html" | filecheck %s --dump-input=fail --check-prefix CHECK-4 +CHECK-4: v1.0-1-g{{[a-f0-9]{7}}}, main diff --git a/tests/integration/lit.cfg b/tests/integration/lit.cfg index 8a60a3b3b..a599e23f3 100644 --- a/tests/integration/lit.cfg +++ b/tests/integration/lit.cfg @@ -13,10 +13,13 @@ current_dir = os.getcwd() strictdoc_exec = lit_config.params['STRICTDOC_EXEC'] assert(strictdoc_exec) +config.substitutions.append(('%STRICTDOC_TMP_DIR', lit_config.params['STRICTDOC_TMP_DIR'])) + # NOTE: All substitutions work for the RUN: statements but they don't for CHECK:. # That's how LLVM LIT works. config.substitutions.append(('%THIS_TEST_FOLDER', '$(basename "%S")')) + config.substitutions.append(('%strictdoc_root', current_dir)) config.substitutions.append(('%strictdoc', strictdoc_exec)) diff --git a/tests/unit/strictdoc/helpers/test_string.py b/tests/unit/strictdoc/helpers/test_string.py index 817eef00f..2303e327b 100644 --- a/tests/unit/strictdoc/helpers/test_string.py +++ b/tests/unit/strictdoc/helpers/test_string.py @@ -1,4 +1,5 @@ from strictdoc.helpers.string import ( + interpolate_at_pattern_lazy, is_safe_alphanumeric_string, sanitize_html_form_field, ) @@ -61,3 +62,15 @@ def test_is_safe_alphanumeric_string(): assert is_safe_alphanumeric_string("docs/document.ext.sdoc") is True assert is_safe_alphanumeric_string("docs/docs2/document.sdoc") is True assert is_safe_alphanumeric_string("docs/document1.sdoc") is True + + +def test_interpolate_at_pattern_lazy(): + def resolver(variable_name): + if variable_name == "GIT_VERSION": + return "abcd123" + elif variable_name == "GIT_BRANCH": + return "main" + return variable_name + + result = interpolate_at_pattern_lazy("@GIT_VERSION, @GIT_BRANCH", resolver) + assert result == "abcd123, main"