StrictDoc Documentation
strictdoc/server/routers/other_router.py
Source file coverage
Path:
strictdoc/server/routers/other_router.py
Lines:
321
Non-empty lines:
289
Non-empty lines covered with requirements:
289 / 289 (100.0%)
Functions:
4
Functions covered by requirements:
4 / 4 (100.0%)
1
"""
2
@relation(SDOC-SRS-111, scope=file)
3
"""
4
 
5
import os
6
import urllib
7
from contextlib import ExitStack
8
from copy import deepcopy
9
from datetime import datetime
10
from typing import Optional
11
 
12
from fastapi import APIRouter
13
from starlette.responses import HTMLResponse, Response
14
 
15
from strictdoc import __version__
16
from strictdoc.core.project_config import ProjectConfig
17
from strictdoc.export.html.document_type import DocumentType
18
from strictdoc.export.html.html_templates import HTMLTemplates
19
from strictdoc.export.html.renderers.link_renderer import LinkRenderer
20
from strictdoc.features.diff_and_changelog.change_container import (
21
    ChangeContainer,
22
)
23
from strictdoc.features.diff_and_changelog.change_generator import (
24
    ChangeGenerator,
25
)
26
from strictdoc.features.diff_and_changelog.diff_screen_results_view_object import (
27
    DiffScreenResultsViewObject,
28
)
29
from strictdoc.features.diff_and_changelog.diff_screen_view_object import (
30
    DiffScreenViewObject,
31
)
32
from strictdoc.features.diff_and_changelog.git_client import GitClient
33
from strictdoc.server.helpers.hierarchical_rw_lock_manager import (
34
    HierarchicalRWLockManager,
35
)
36
from strictdoc.server.routers.main_router import (
37
    HTTP_STATUS_BAD_REQUEST,
38
    HTTP_STATUS_PRECONDITION_FAILED,
39
)
40
 
41
 
42
def create_other_router(
43
    project_config: ProjectConfig,
44
    *,
45
    lock_manager: HierarchicalRWLockManager,
46
) -> APIRouter:
47
    router = APIRouter()
48
 
49
    html_templates = HTMLTemplates.create(
50
        project_config=project_config,
51
        enable_caching=False,
52
        strictdoc_last_update=datetime.today(),
53
    )
54
 
55
    @router.get("/diff")
56
    def get_git_diff(
57
        left_revision: Optional[str] = None,
58
        right_revision: Optional[str] = None,
59
        tab: Optional[str] = None,
60
    ) -> Response:
61
        if not project_config.is_activated_diff():
62
            return Response(
63
                content="The DIFF feature is not activated in the project config.",
64
                status_code=HTTP_STATUS_PRECONDITION_FAILED,
65
            )
66
        if tab is not None:
67
            if tab not in ("diff", "changelog"):
68
                return Response(
69
                    content="The tab= parameter must be either 'diff' or 'changelog'.",
70
                    status_code=HTTP_STATUS_BAD_REQUEST,
71
                )
72
        else:
73
            tab = "diff"
74
 
75
        error_message: Optional[str] = None
76
 
77
        if (
78
            left_revision is not None
79
            and len(left_revision) > 0
80
            and right_revision is not None
81
            and len(right_revision) > 0
82
        ):
83
            git_client = GitClient(".")
84
            try:
85
                if left_revision != "HEAD+":
86
                    git_client.check_revision(left_revision)
87
                else:
88
                    raise LookupError(
89
                        "Left revision argument 'HEAD+' is not supported. "
90
                        "'HEAD+' can only be used as a right revision argument."
91
                    )
92
 
93
                if right_revision != "HEAD+":
94
                    git_client.check_revision(right_revision)
95
            except LookupError as exception_:
96
                error_message = exception_.args[0]
97
        elif (left_revision is not None and len(left_revision) > 0) or (
98
            right_revision is not None and len(right_revision) > 0
99
        ):
100
            error_message = "Valid Git revisions must be provided."
101
        else:
102
            # In the case when both revisions are empty, we load the starting
103
            # diff page.
104
            pass
105
 
106
        view_object = DiffScreenViewObject(
107
            project_config=project_config,
108
            results=False,
109
            left_revision=left_revision,
110
            right_revision=right_revision,
111
            error_message=error_message,
112
            tab=tab,
113
        )
114
        output = view_object.render_screen(html_templates.jinja_environment())
115
        status_code = 200 if error_message is None else 422
116
        return HTMLResponse(content=output, status_code=status_code)
117
 
118
    @router.get("/diff_result")
119
    def get_git_diff_result(
120
        left_revision: Optional[str] = None,
121
        right_revision: Optional[str] = None,
122
        tab: Optional[str] = None,
123
    ) -> Response:
124
        if not project_config.is_activated_diff():
125
            return Response(
126
                content="The DIFF feature is not activated in the project config.",
127
                status_code=HTTP_STATUS_PRECONDITION_FAILED,
128
            )
129
        if tab is not None and tab not in ("diff", "changelog"):
130
            return Response(
131
                content="The tab= parameter must be either 'diff' or 'changelog'.",
132
                status_code=HTTP_STATUS_PRECONDITION_FAILED,
133
            )
134
        elif tab is None:
135
            tab = "diff"
136
 
137
        left_revision_resolved = None
138
        right_revision_resolved = None
139
 
140
        results = False
141
        error_message: Optional[str] = None
142
 
143
        if (
144
            left_revision is not None
145
            and len(left_revision) > 0
146
            and right_revision is not None
147
            and len(right_revision) > 0
148
        ):
149
            git_client = GitClient(".")
150
            try:
151
                if left_revision != "HEAD+":
152
                    left_revision_resolved = git_client.check_revision(
153
                        left_revision
154
                    )
155
                else:
156
                    raise LookupError(
157
                        "Left revision argument 'HEAD+' is not supported. "
158
                        "'HEAD+' can only be used as a right revision argument."
159
                    )
160
 
161
                if right_revision == "HEAD+":
162
                    right_revision_resolved = "HEAD+"
163
                else:
164
                    right_revision_resolved = git_client.check_revision(
165
                        right_revision
166
                    )
167
 
168
                results = True
169
            except LookupError as exception_:
170
                error_message = exception_.args[0]
171
        elif (left_revision is not None and len(left_revision) > 0) or (
172
            right_revision is not None and len(right_revision) > 0
173
        ):
174
            error_message = "Valid Git revisions must be provided."
175
        else:
176
            # In the case when both revisions are empty, we load the starting
177
            # diff page.
178
            pass
179
 
180
        path_to_template = (
181
            "features/diff_and_changelog/frame_changelog_result.jinja"
182
            if tab == "changelog"
183
            else "features/diff_and_changelog/frame_diff_result.jinja"
184
        )
185
        template = html_templates.jinja_environment().get_template(
186
            path_to_template
187
        )
188
 
189
        link_renderer = LinkRenderer(
190
            root_path="", static_path=project_config.dir_for_sdoc_assets
191
        )
192
 
193
        left_revision_urlencoded = (
194
            urllib.parse.quote(left_revision)
195
            if left_revision is not None
196
            else ""
197
        )
198
        right_revision_urlencoded = (
199
            urllib.parse.quote(right_revision)
200
            if right_revision is not None
201
            else ""
202
        )
203
 
204
        if not results:
205
            output = template.render(
206
                project_config=project_config,
207
                document_type=DocumentType.DOCUMENT.value,
208
                link_document_type=DocumentType.DOCUMENT.value,
209
                strictdoc_version=__version__,
210
                link_renderer=link_renderer,
211
                results=False,
212
                left_revision=left_revision,
213
                left_revision_urlencoded=left_revision_urlencoded,
214
                right_revision=right_revision,
215
                right_revision_urlencoded=right_revision_urlencoded,
216
                error_message=error_message,
217
            )
218
            status_code = 200 if error_message is None else 422
219
            return HTMLResponse(content=output, status_code=status_code)
220
 
221
        assert left_revision_resolved is not None
222
        assert right_revision_resolved is not None
223
 
224
        left_revision_tags = git_client.get_tags_for_revision(
225
            left_revision_resolved
226
        )
227
        right_revision_tags = (
228
            git_client.get_tags_for_revision(right_revision_resolved)
229
            if right_revision_resolved != "HEAD+"
230
            else []
231
        )
232
 
233
        def open_git_client_for_revision(revision: str) -> GitClient:
234
            if revision == "HEAD+":
235
                # Serialize HEAD+ snapshot creation with main router writes.
236
                with lock_manager.acquire_global_write():
237
                    return exit_stack.enter_context(
238
                        GitClient.create_cached_repo_from_local_copy(
239
                            revision, project_config
240
                        )
241
                    )
242
            return exit_stack.enter_context(
243
                GitClient.create_cached_repo_from_local_copy(
244
                    revision, project_config
245
                )
246
            )
247
 
248
        with ExitStack() as exit_stack:
249
            git_client_lhs = open_git_client_for_revision(
250
                left_revision_resolved
251
            )
252
            git_client_rhs = open_git_client_for_revision(
253
                right_revision_resolved
254
            )
255
 
256
            project_config_copy_lhs: ProjectConfig = deepcopy(project_config)
257
            assert project_config_copy_lhs.input_paths is not None
258
            project_config_copy_rhs: ProjectConfig = deepcopy(project_config)
259
            assert project_config_copy_rhs.input_paths is not None
260
 
261
            export_input_rel_path = os.path.relpath(
262
                project_config_copy_lhs.input_paths[0], os.getcwd()
263
            )
264
            export_input_abs_path = os.path.join(
265
                git_client_lhs.path_to_git_root, export_input_rel_path
266
            )
267
            project_config_copy_lhs.input_paths = [export_input_abs_path]
268
 
269
            export_input_rel_path = os.path.relpath(
270
                project_config_copy_rhs.input_paths[0], os.getcwd()
271
            )
272
            export_input_abs_path = os.path.join(
273
                git_client_rhs.path_to_git_root, export_input_rel_path
274
            )
275
            project_config_copy_rhs.input_paths = [export_input_abs_path]
276
 
277
            change_container: ChangeContainer = ChangeGenerator.generate(
278
                lhs_project_config=project_config_copy_lhs,
279
                rhs_project_config=project_config_copy_rhs,
280
            )
281
 
282
            assert (
283
                change_container.traceability_index_lhs.document_tree
284
                is not None
285
            )
286
            assert (
287
                change_container.traceability_index_rhs.document_tree
288
                is not None
289
            )
290
            # Unlike change_generator.py's static export, left_revision/
291
            # right_revision here must stay the raw expressions the user
292
            # submitted (e.g. "HEAD^"): the form/turbo-frame/nav-tab links
293
            # resubmit these exact values back to this route, so replacing
294
            # them with a "<expression> (<short-hash>)" display string would
295
            # break re-diffing from the results screen.
296
            view_object = DiffScreenResultsViewObject(
297
                project_config=project_config,
298
                change_container=change_container,
299
                document_tree_lhs=change_container.traceability_index_lhs.document_tree,
300
                document_tree_rhs=change_container.traceability_index_rhs.document_tree,
301
                documents_iterator_lhs=change_container.documents_iterator_lhs,
302
                documents_iterator_rhs=change_container.documents_iterator_rhs,
303
                left_revision=left_revision,
304
                right_revision=right_revision,
305
                left_revision_tags=left_revision_tags,
306
                right_revision_tags=right_revision_tags,
307
                lhs_stats=change_container.lhs_stats,
308
                rhs_stats=change_container.rhs_stats,
309
                change_stats=change_container.change_stats,
310
                traceability_index_lhs=change_container.traceability_index_lhs,
311
                traceability_index_rhs=change_container.traceability_index_rhs,
312
                tab=tab,
313
            )
314
            output = template.render(view_object=view_object)
315
 
316
        return HTMLResponse(
317
            content=output,
318
            status_code=200,
319
        )
320
 
321
    return router