diff --git a/model_diffsummary/__main__.py b/model_diffsummary/__main__.py index f15edb5..71b00d4 100644 --- a/model_diffsummary/__main__.py +++ b/model_diffsummary/__main__.py @@ -1,11 +1,20 @@ # Copyright DB Netz AG and contributors # SPDX-License-Identifier: Apache-2.0 """Main entry point into model_diffsummary.""" +from __future__ import annotations +import logging +import typing as t + +import capellambse import click +from capellambse import model as m +from capellambse.model import common as c import model_diffsummary +logger = logging.getLogger(__name__) + @click.command() @click.version_option( @@ -13,8 +22,129 @@ prog_name="Model-Diff Summary Tool", message="%(prog)s %(version)s", ) -def main(): - """Console script for model_diffsummary.""" +@click.argument("model", type=capellambse.cli_helpers.ModelInfoCLI()) +@click.argument("old_version") +@click.argument("new_version") +def main(model: dict[str, t.Any], old_version: str, new_version: str): + """Generate the diff summary between two model versions. + + The comparison result will be printed to stdout in YAML format. + """ + logging.basicConfig(level="DEBUG") + old_model = capellambse.MelodyModel(**model, revision=old_version) + new_model = capellambse.MelodyModel(**model, revision=new_version) + + result = _compare_all_objects(old_model, new_model) + + print(result) + + +def _compare_all_objects( + old: capellambse.MelodyModel, new: capellambse.MelodyModel +) -> t.Any: + result: t.Any = {"oa": {}, "la": {}, "pa": {}, "sa": {}} + for layer, objtype in ( + ("oa", m.oa.OperationalActivity), + ("oa", m.fa.FunctionalExchange), + ("oa", m.oa.OperationalCapability), + ("sa", m.ctx.SystemComponent), + ("sa", m.ctx.SystemFunction), + ("sa", m.ctx.Capability), + ): + type_result = _compare_object_type(old, new, objtype) + result[layer][objtype.__name__] = type_result + return result + + +def _compare_object_type( + old: capellambse.MelodyModel, + new: capellambse.MelodyModel, + obj_type: type[c.GenericElement], +) -> t.Any: + logging.debug("Collecting objects of type %s", obj_type.__name__) + changes = {} + + old_objs = old.search(obj_type) + new_objs = new.search(obj_type) + + old_uuids = {i.uuid for i in old_objs} + new_uuids = {i.uuid for i in new_objs} + + if created_uuids := new_uuids - old_uuids: + changes["created"] = [_obj2dict(new.by_uuid(i)) for i in created_uuids] + if deleted_uuids := old_uuids - new_uuids: + changes["deleted"] = [_obj2dict(old.by_uuid(i)) for i in deleted_uuids] + + for i in old_uuids & new_uuids: + if diff := _obj2diff(old.by_uuid(i), new.by_uuid(i)): + changes.setdefault("modified", []).append(diff) + + +def _obj2dict(obj: c.GenericElement) -> dict[str, t.Any]: + """Serialize a model object into a dict. + + This function is used for objects that were either created or + deleted, in which case all available attributes are serialized. + """ + result = {} + for attr in dir(type(obj)): + acc = getattr(type(obj), attr, None) + if isinstance(acc, c.AttributeProperty): + result[attr] = getattr(obj, attr) + return result + + +def _obj2diff( + old: c.GenericElement, new: c.GenericElement +) -> dict[str, t.Any] | None: + """Serialize the differences between the old and new object. + + This function is used for objects that were modified. Only the + attributes that were changed are serialized. + + The new (current) *name* is always serialized. If it didn't change, + it will not have the "previous" key. + """ + changes: t.Any = {} + for attr in dir(type(old)): + if not isinstance( + getattr(type(old), attr, None), + (c.AttributeProperty, c.AttrProxyAccessor, c.LinkAccessor), + ): + continue + + old_val = getattr(old, attr) + new_val = getattr(new, attr) + if isinstance(old_val, c.GenericElement): + if old_val.uuid != new_val.uuid: + changes[attr] = { + "previous": { + "uuid": old_val.uuid, + "display_name": old_val.name, + }, + "current": { + "uuid": new_val.uuid, + "display_name": new_val.name, + }, + } + elif isinstance(old_val, c.ElementList): + if [i.uuid for i in old_val] != [i.uuid for i in new_val]: + changes[attr] = { + "previous": [ + {"uuid": i.uuid, "display_name": i.name} + for i in old_val + ], + "current": [ + {"uuid": i.uuid, "display_name": i.name} + for i in new_val + ], + } + elif old_val != new_val: + changes[attr] = {"previous": old_val, "current": new_val} + + if not changes: + return None + return {"uuid": old.uuid, "display_name": new.name, "changes": changes} if __name__ == "__main__": diff --git a/notes.md b/notes.md new file mode 100644 index 0000000..7b15846 --- /dev/null +++ b/notes.md @@ -0,0 +1,91 @@ + + +End goal: + +- Commit-based "news feed" in collab manager +- Also viewable as standalone HTML report +- Run in pipeline for newest changes of each commit +- Run for any arbitrary two commits (e.g. tag BL_10 vs. tag BL_11) + +- Something like: python -m whatevermod --previous COMMITISH --current COMMITISH + +- Produces JSON or YAML report, which can be displayed interactively + +- Some tree structure, commit as tree root + + drill down into changed objects: + + Commit ABCDEF0 by Guy 1 on + + Changed diagrams: + > [SAB] System: 20 changes (click to expand...) + > ... + v [CDC] Mission + Introduced: + - Capability 1 + Removed: + - Capability 7 + > ... + + Changes on OA: + > OperationalActivity: 3 created, 5 deleted, 1 modified (click to expand...) + > ... + v OperationalCapability: + | Created: + | > New thing A (click to show details) + | v New thing B + | uuid: 13579BDF-... + | name: New thing B + | description:

...

+ | ... + | + | Deleted: + | - Old Name 1 (12345678-...) + | - Old Name 2 (9ABCDEF0-...) + | ... + | + | Modified: + | - Capsoup (2468ACE0-...) + | Name changed from X to Capsoup + + Changes on SA: + ... + + Changes on LA: + ... + + ------------------------------------ + + Changed objects: + - Function ABC + > new input: ... + > input deleted: ... + > name changed: ... + + > child change: + - ... + > ... + + Changed diagrams: + - Diagram XYZ + > Box "Function ABC" added + (only semantic changes, layout is ignored / summarized as "layout changes") + -> link to diagram cache, or left / right display, visual diff tool, w/e + + ... and other changes (which we don't know how to show yet) :) // (XML tree diff?) + + Commit 1234567 by Guy 2 on : Changed 200 objects and 20 diagrams (click to expand...) + +Objects of interest: + + For OA: OperationalActivity, FunctionalExchange, OperationalCapability + Diagrams (OA): COC, OAIB, OPD, OAB, O.MSM, O.STM(?), O.CDB (low-prio) + + For SA: SystemComponent (+ Parts) -> distinguish between actor/non-actor; + allocated SystemFunctions + Capabilities + Involvements + Diagrams (SA): CC, SDFB, MCB, SAB + +Start with OperationalActivity diff --git a/result-sample.yaml b/result-sample.yaml new file mode 100644 index 0000000..21a5e8c --- /dev/null +++ b/result-sample.yaml @@ -0,0 +1,66 @@ +# Copyright DB Netz AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +metadata: + model: + path: git+https://git.tech.rz.db.de/god/automated-train + old_revision: + hash: commit_A + author: Person A + date: 2023-09-01 09:30:00 + description: |- + Do work + + In this commit, work was done. + + Co-authored-by: Arufua + Signed-off-by: Executive C + new_revision: + hash: commit_B + author: Person B + date: 2023-09-28 14:00:00 + description: A couple minor changes + +diagrams: + oa: + created: + - Diagram 1 + - Diagram 2 + deleted: + - Diagram 3 + - Diagram 4 + changed: + - id: _ABCDEFGHI + name: Diagram 5 + layout_changes: true + introduced: + - id: 12345678-... + name: Operational Activity 1 + - id: 12345678-... + name: Operational Activity 2 + removed: + - id: 12345678-... + name: Operational Activity 3 + +objects: + oa: + OperationalActivity: + created: + - uuid: 13579BDF-... + name: New thing B + description:

...

# maybe use '!html' ? + deleted: + - uuid: 12345678-... + name: Old Name 1 + description:

...

+ modified: + - uuid: 2468ACE0-... + display_name: Capsoup # always the current name, for showing in the frontend + changes: + name: + previous: null + current: Capsoup + OperationalCapability: + ...: ... + + sa: diff --git a/testrun.sh b/testrun.sh new file mode 100755 index 0000000..582ebdc --- /dev/null +++ b/testrun.sh @@ -0,0 +1,5 @@ +#!/bin/sh -e +# Copyright DB Netz AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +exec python -m model_diffsummary autotrain 2ffa0e6e828378550cabec5ff32cf5b4a834d1fc HEAD