From a296157f58a5fa31861daa359b2ef8998d06c8a6 Mon Sep 17 00:00:00 2001 From: Jochen Becher Date: Tue, 16 Apr 2024 13:17:14 +0200 Subject: ModelEditor: Add dialog for adding related elements Change-Id: Iae832885278472bb42a588fc97967ae5ffad6b71 Reviewed-by: Leena Miettinen Reviewed-by: Alessandro Portale --- src/libs/modelinglib/CMakeLists.txt | 2 + src/libs/modelinglib/modelinglib.qbs | 3 + .../qmt/diagram_scene/items/objectitem.cpp | 6 - .../model_widgets_ui/addrelatedelementsdialog.cpp | 378 +++++++++++++++++++++ .../model_widgets_ui/addrelatedelementsdialog.h | 48 +++ .../model_widgets_ui/addrelatedelementsdialog.ui | 190 +++++++++++ .../qmt/tasks/diagramscenecontroller.cpp | 54 ++- .../modelinglib/qmt/tasks/diagramscenecontroller.h | 7 +- 8 files changed, 678 insertions(+), 10 deletions(-) create mode 100644 src/libs/modelinglib/qmt/model_widgets_ui/addrelatedelementsdialog.cpp create mode 100644 src/libs/modelinglib/qmt/model_widgets_ui/addrelatedelementsdialog.h create mode 100644 src/libs/modelinglib/qmt/model_widgets_ui/addrelatedelementsdialog.ui (limited to 'src/libs/modelinglib') diff --git a/src/libs/modelinglib/CMakeLists.txt b/src/libs/modelinglib/CMakeLists.txt index dcc810eb211..afea6c16994 100644 --- a/src/libs/modelinglib/CMakeLists.txt +++ b/src/libs/modelinglib/CMakeLists.txt @@ -131,6 +131,8 @@ add_qtc_library(Modeling qmt/model_ui/stereotypescontroller.cpp qmt/model_ui/stereotypescontroller.h qmt/model_ui/treemodel.cpp qmt/model_ui/treemodel.h qmt/model_ui/treemodelmanager.cpp qmt/model_ui/treemodelmanager.h + qmt/model_widgets_ui/addrelatedelementsdialog.h qmt/model_widgets_ui/addrelatedelementsdialog.cpp + qmt/model_widgets_ui/addrelatedelementsdialog.ui qmt/model_widgets_ui/classmembersedit.cpp qmt/model_widgets_ui/classmembersedit.h qmt/model_widgets_ui/modeltreeview.cpp qmt/model_widgets_ui/modeltreeview.h qmt/model_widgets_ui/palettebox.cpp qmt/model_widgets_ui/palettebox.h diff --git a/src/libs/modelinglib/modelinglib.qbs b/src/libs/modelinglib/modelinglib.qbs index 409928d3cbf..d4edfadf09f 100644 --- a/src/libs/modelinglib/modelinglib.qbs +++ b/src/libs/modelinglib/modelinglib.qbs @@ -247,6 +247,9 @@ QtcLibrary { "model_ui/treemodel.h", "model_ui/treemodelmanager.cpp", "model_ui/treemodelmanager.h", + "model_widgets_ui/addrelatedelementsdialog.h", + "model_widgets_ui/addrelatedelementsdialog.cpp", + "model_widgets_ui/addrelatedelementsdialog.ui", "model_widgets_ui/classmembersedit.cpp", "model_widgets_ui/classmembersedit.h", "model_widgets_ui/modeltreeview.cpp", diff --git a/src/libs/modelinglib/qmt/diagram_scene/items/objectitem.cpp b/src/libs/modelinglib/qmt/diagram_scene/items/objectitem.cpp index 5d747a6391f..60009e5f500 100644 --- a/src/libs/modelinglib/qmt/diagram_scene/items/objectitem.cpp +++ b/src/libs/modelinglib/qmt/diagram_scene/items/objectitem.cpp @@ -1092,7 +1092,6 @@ void ObjectItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) layoutMenu.addAction(new ContextMenuAction(Tr::tr("Equal Vertical Space"), "sameVBorderDistance", &alignMenu)); layoutMenu.setEnabled(m_diagramSceneModel->hasMultiObjectsSelection()); menu.addMenu(&layoutMenu); - menu.addAction(new ContextMenuAction(Tr::tr("Add Related Elements"), "addRelatedElements", &menu)); QAction *selectedAction = menu.exec(event->screenPos()); if (selectedAction) { @@ -1145,11 +1144,6 @@ void ObjectItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) align(IAlignable::AlignHeight, "height"); } else if (action->id() == "sameSize") { align(IAlignable::AlignSize, "size"); - } else if (action->id() == "addRelatedElements") { - DSelection selection = m_diagramSceneModel->selectedElements(); - if (selection.isEmpty()) - selection.append(m_object->uid(), m_diagramSceneModel->diagram()->uid()); - m_diagramSceneModel->diagramSceneController()->addRelatedElements(selection, m_diagramSceneModel->diagram()); } } } diff --git a/src/libs/modelinglib/qmt/model_widgets_ui/addrelatedelementsdialog.cpp b/src/libs/modelinglib/qmt/model_widgets_ui/addrelatedelementsdialog.cpp new file mode 100644 index 00000000000..35bfeda6c7d --- /dev/null +++ b/src/libs/modelinglib/qmt/model_widgets_ui/addrelatedelementsdialog.cpp @@ -0,0 +1,378 @@ +// Copyright (C) 2018 Jochen Becher +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "addrelatedelementsdialog.h" +#include "ui_addrelatedelementsdialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + + +namespace qmt { + +namespace { + +enum class RelationType { + Any, + Dependency, + Association, + Inheritance, + Connection +}; + +enum class RelationDirection { + Any, + Outgoing, + Incoming, + Bidirectional +}; + +enum class ElementType { + Any, + Package, + Component, + Class, + Diagram, + Item, +}; + +class Filter : public qmt::MVoidConstVisitor { +public: + + void setRelationType(RelationType newRelationType) + { + m_relationType = newRelationType; + } + + void setRelationTypeId(const QString &newRelationTypeId) + { + m_relationTypeId = newRelationTypeId; + } + + void setRelationDirection(RelationDirection newRelationDirection) + { + m_relationDirection = newRelationDirection; + } + + void setRelationStereotypes(const QStringList &newRelationStereotypes) + { + m_relationStereotypes = newRelationStereotypes; + } + + void setElementType(ElementType newElementType) + { + m_elementType = newElementType; + } + + void setElementStereotypes(const QStringList &newElementStereotypes) + { + m_elementStereotypes = newElementStereotypes; + } + + void setObject(const qmt::DObject *dobject, const qmt::MObject *mobject) + { + m_dobject = dobject; + m_mobject = mobject; + } + + void setRelation(const qmt::MRelation *relation) + { + m_relation = relation; + } + + bool keep() const { return m_keep; } + + void reset() + { + m_dobject = nullptr; + m_mobject = nullptr; + m_relation = nullptr; + m_keep = true; + } + + // MConstVisitor interface + void visitMObject(const qmt::MObject *object) override + { + if (!m_elementStereotypes.isEmpty()) { + const QStringList stereotypes = object->stereotypes(); + bool containsElementStereotype = std::any_of( + stereotypes.constBegin(), stereotypes.constEnd(), + [&](const QString &s) { return m_elementStereotypes.contains(s); }); + if (!containsElementStereotype) { + m_keep = false; + return; + } + } + } + + void visitMPackage(const qmt::MPackage *package) override + { + if (m_elementType == ElementType::Any || m_elementType == ElementType::Package) + qmt::MVoidConstVisitor::visitMPackage(package); + else + m_keep = false; + } + + void visitMClass(const qmt::MClass *klass) override + { + if (m_elementType == ElementType::Any || m_elementType == ElementType::Class) + qmt::MVoidConstVisitor::visitMClass(klass); + else + m_keep = false; + } + + void visitMComponent(const qmt::MComponent *component) override + { + if (m_elementType == ElementType::Any || m_elementType == ElementType::Component) + qmt::MVoidConstVisitor::visitMComponent(component); + else + m_keep = false; + } + + void visitMDiagram(const qmt::MDiagram *diagram) override + { + if (m_elementType == ElementType::Any || m_elementType == ElementType::Diagram) + qmt::MVoidConstVisitor::visitMDiagram(diagram); + else + m_keep = false; + } + + void visitMItem(const qmt::MItem *item) override + { + if (m_elementType == ElementType::Any || m_elementType == ElementType::Item) + qmt::MVoidConstVisitor::visitMItem(item); + else + m_keep = false; + } + + void visitMRelation(const qmt::MRelation *relation) override + { + if (!m_relationStereotypes.isEmpty()) { + const QStringList relationStereotypes = relation->stereotypes(); + bool containsRelationStereotype = std::any_of( + relationStereotypes.constBegin(), relationStereotypes.constEnd(), + [&](const QString &s) { return m_relationStereotypes.contains(s); }); + if (!containsRelationStereotype) { + m_keep = false; + return; + } + } + } + + void visitMDependency(const qmt::MDependency *dependency) override + { + if (m_relationDirection != RelationDirection::Any) { + bool keep = false; + if (m_relationDirection == RelationDirection::Outgoing) { + if (dependency->direction() == qmt::MDependency::AToB && dependency->endAUid() == m_mobject->uid()) + keep = true; + else if (dependency->direction() == qmt::MDependency::BToA && dependency->endBUid() == m_mobject->uid()) + keep = true; + } else if (m_relationDirection == RelationDirection::Incoming) { + if (dependency->direction() == qmt::MDependency::AToB && dependency->endBUid() == m_mobject->uid()) + keep = true; + else if (dependency->direction() == qmt::MDependency::BToA && dependency->endAUid() == m_mobject->uid()) + keep = true; + } else if (m_relationDirection == RelationDirection::Bidirectional) { + if (dependency->direction() == qmt::MDependency::Bidirectional) + keep = true; + } + m_keep = keep; + if (!keep) + return; + } + if (m_relationType == RelationType::Any || m_relationType == RelationType::Dependency) + qmt::MVoidConstVisitor::visitMDependency(dependency); + else + m_keep = false; + } + + bool testDirection(const qmt::MRelation *relation) + { + if (m_relationDirection != RelationDirection::Any) { + bool keep = false; + if (m_relationDirection == RelationDirection::Outgoing) { + if (relation->endAUid() == m_mobject->uid()) + keep = true; + } else if (m_relationDirection == RelationDirection::Incoming) { + if (relation->endBUid() == m_mobject->uid()) + keep = true; + } + m_keep = keep; + if (!keep) + return false; + } + return true; + } + + void visitMInheritance(const qmt::MInheritance *inheritance) override + { + if (!testDirection(inheritance)) + return; + if (m_relationType == RelationType::Any || m_relationType == RelationType::Inheritance) + qmt::MVoidConstVisitor::visitMInheritance(inheritance); + else + m_keep = false; + } + + void visitMAssociation(const qmt::MAssociation *association) override + { + if (!testDirection(association)) + return; + if (m_relationType == RelationType::Any || m_relationType == RelationType::Association) + qmt::MVoidConstVisitor::visitMAssociation(association); + else + m_keep = false; + } + + void visitMConnection(const qmt::MConnection *connection) override + { + if (!testDirection(connection)) + return; + if (m_relationType == RelationType::Any || m_relationType == RelationType::Connection) + qmt::MVoidConstVisitor::visitMConnection(connection); + else + m_keep = false; + } + +private: + RelationType m_relationType = RelationType::Any; + QString m_relationTypeId; + RelationDirection m_relationDirection = RelationDirection::Any; + QStringList m_relationStereotypes; + ElementType m_elementType = ElementType::Any; + QStringList m_elementStereotypes; + const qmt::DObject *m_dobject = nullptr; + const qmt::MObject *m_mobject = nullptr; + const qmt::MRelation *m_relation = nullptr; + bool m_keep = true; +}; +} // namespace + +class AddRelatedElementsDialog::Private { +public: + qmt::DiagramSceneController *m_diagramSceneController = nullptr; + qmt::DSelection m_selection; + qmt::Uid m_diagramUid; + QStringListModel m_relationTypeModel; + QStringListModel m_relationDirectionModel; + QStringListModel m_relationStereotypesModel; + QStringListModel m_elementTypeModel; + QStringListModel m_elementStereotypesModel; + Filter m_filter; +}; + +AddRelatedElementsDialog::AddRelatedElementsDialog(QWidget *parent) : + QDialog(parent), + ui(new Ui::AddRelatedElementsDialog), + d(new Private) +{ + ui->setupUi(this); + connect(ui->RelationTypeCombobox, &QComboBox::currentIndexChanged, this, &AddRelatedElementsDialog::updateNumberOfElements); + connect(ui->DirectionCombobox, &QComboBox::currentIndexChanged, this, &AddRelatedElementsDialog::updateNumberOfElements); + connect(ui->StereotypesCombobox, &QComboBox::currentTextChanged, this, &AddRelatedElementsDialog::updateNumberOfElements); + connect(ui->ElementTypeComboBox, &QComboBox::currentIndexChanged, this, &AddRelatedElementsDialog::updateNumberOfElements); + connect(ui->ElementStereotypesCombobox, &QComboBox::currentTextChanged, this, &AddRelatedElementsDialog::updateNumberOfElements); + connect(this, &QDialog::accepted, this, &AddRelatedElementsDialog::onAccepted); +} + +AddRelatedElementsDialog::~AddRelatedElementsDialog() +{ + delete d; + delete ui; +} + +void AddRelatedElementsDialog::setDiagramSceneController(qmt::DiagramSceneController *diagramSceneController) +{ + d->m_diagramSceneController = diagramSceneController; +} + +void AddRelatedElementsDialog::setElements(const qmt::DSelection &selection, qmt::MDiagram *diagram) +{ + d->m_selection = selection; + d->m_diagramUid = diagram->uid(); + QStringList relationTypes = {"Any", "Dependency", "Association", "Inheritance"}; + d->m_relationTypeModel.setStringList(relationTypes); + ui->RelationTypeCombobox->setModel(&d->m_relationTypeModel); + QStringList relationDirections = {"Any", "Outgoing (->)", "Incoming (<-)", "Bidirectional (<->)"}; + d->m_relationDirectionModel.setStringList(relationDirections); + ui->DirectionCombobox->setModel(&d->m_relationDirectionModel); + QStringList relationStereotypes = { }; + d->m_relationStereotypesModel.setStringList(relationStereotypes); + ui->StereotypesCombobox->setModel(&d->m_relationStereotypesModel); + QStringList elementTypes = {"Any", "Package", "Component", "Class", "Diagram", "Item"}; + d->m_elementTypeModel.setStringList(elementTypes); + ui->ElementTypeComboBox->setModel(&d->m_elementTypeModel); + QStringList elementStereotypes = { }; + d->m_elementStereotypesModel.setStringList(elementStereotypes); + ui->ElementStereotypesCombobox->setModel(&d->m_elementStereotypesModel); + updateNumberOfElements(); +} + +void AddRelatedElementsDialog::onAccepted() +{ + qmt::MDiagram *diagram = d->m_diagramSceneController->modelController()->findElement(d->m_diagramUid); + if (diagram) { + updateFilter(); + d->m_diagramSceneController->addRelatedElements( + d->m_selection, diagram, + [this](qmt::DObject *dobject, qmt::MObject *mobject, qmt::MRelation *relation) -> bool + { + return this->filter(dobject, mobject, relation); + }); + } +} + +void AddRelatedElementsDialog::updateFilter() +{ + d->m_filter.setRelationType((RelationType) ui->RelationTypeCombobox->currentIndex()); + d->m_filter.setRelationDirection((RelationDirection) ui->DirectionCombobox->currentIndex()); + d->m_filter.setRelationStereotypes(ui->StereotypesCombobox->currentText().split(',', Qt::SkipEmptyParts)); + d->m_filter.setElementType((ElementType) ui->ElementTypeComboBox->currentIndex()); + d->m_filter.setElementStereotypes(ui->ElementStereotypesCombobox->currentText().split(',', Qt::SkipEmptyParts)); +} + +bool AddRelatedElementsDialog::filter(qmt::DObject *dobject, qmt::MObject *mobject, qmt::MRelation *relation) +{ + d->m_filter.reset(); + d->m_filter.setObject(dobject, mobject); + d->m_filter.setRelation(relation); + relation->accept(&d->m_filter); + if (!d->m_filter.keep()) + return false; + qmt::MObject *targetObject = nullptr; + if (relation->endAUid() != mobject->uid()) + targetObject = d->m_diagramSceneController->modelController()->findObject(relation->endAUid()); + else if (relation->endBUid() != mobject->uid()) + targetObject = d->m_diagramSceneController->modelController()->findObject(relation->endBUid()); + if (!targetObject) + return false; + targetObject->accept(&d->m_filter); + return d->m_filter.keep(); +} + +void AddRelatedElementsDialog::updateNumberOfElements() +{ + qmt::MDiagram *diagram = d->m_diagramSceneController->modelController()->findElement(d->m_diagramUid); + if (diagram) { + updateFilter(); + ui->NumberOfMatchingElementsValue->setText(QString::number(d->m_diagramSceneController->countRelatedElements( + d->m_selection, diagram, + [this](qmt::DObject *dobject, qmt::MObject *mobject, qmt::MRelation *relation) -> bool + { + return this->filter(dobject, mobject, relation); + }))); + } +} + +} // namespace qmt diff --git a/src/libs/modelinglib/qmt/model_widgets_ui/addrelatedelementsdialog.h b/src/libs/modelinglib/qmt/model_widgets_ui/addrelatedelementsdialog.h new file mode 100644 index 00000000000..1c7edb1d574 --- /dev/null +++ b/src/libs/modelinglib/qmt/model_widgets_ui/addrelatedelementsdialog.h @@ -0,0 +1,48 @@ +// Copyright (C) 2018 Jochen Becher +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include "qmt/infrastructure/qmt_global.h" + +#include + +namespace qmt { + +class DSelection; +class DObject; +class MObject; +class MDiagram; +class MRelation; +class DiagramSceneController; +} + +namespace Ui { +class AddRelatedElementsDialog; +} + +namespace qmt { + +class QMT_EXPORT AddRelatedElementsDialog : public QDialog +{ + Q_OBJECT + class Private; + +public: + explicit AddRelatedElementsDialog(QWidget *parent = nullptr); + ~AddRelatedElementsDialog(); + + void setDiagramSceneController(qmt::DiagramSceneController *diagramSceneController); + void setElements(const qmt::DSelection &selection, qmt::MDiagram *diagram); + +private: + void onAccepted(); + void updateFilter(); + bool filter(qmt::DObject *dobject, qmt::MObject *mobject, qmt::MRelation *relation); + void updateNumberOfElements(); + + Ui::AddRelatedElementsDialog *ui = nullptr; + Private *d = nullptr; +}; + +} // namespace qmt diff --git a/src/libs/modelinglib/qmt/model_widgets_ui/addrelatedelementsdialog.ui b/src/libs/modelinglib/qmt/model_widgets_ui/addrelatedelementsdialog.ui new file mode 100644 index 00000000000..0dc2cf04ca4 --- /dev/null +++ b/src/libs/modelinglib/qmt/model_widgets_ui/addrelatedelementsdialog.ui @@ -0,0 +1,190 @@ + + + AddRelatedElementsDialog + + + + 0 + 0 + + + + + 500 + 0 + + + + Dialog + + + + + + Relation Attributes + + + + + + + + Type + + + + + + + + + + Direction + + + + + + + + + + Stereotypes + + + + + + + true + + + + + + + + + + + + Other Element Attributes + + + + + + + + Type + + + + + + + Stereotypes + + + + + + + true + + + + + + + false + + + + + + + + + + + + + + Number of matching elements: + + + + + + + 0 + + + + + + + + + Qt::Vertical + + + QSizePolicy::MinimumExpanding + + + + 0 + 0 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + AddRelatedElementsDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + AddRelatedElementsDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/libs/modelinglib/qmt/tasks/diagramscenecontroller.cpp b/src/libs/modelinglib/qmt/tasks/diagramscenecontroller.cpp index 00aacf9c824..51f414f66e5 100644 --- a/src/libs/modelinglib/qmt/tasks/diagramscenecontroller.cpp +++ b/src/libs/modelinglib/qmt/tasks/diagramscenecontroller.cpp @@ -421,7 +421,44 @@ void DiagramSceneController::dropNewModelElement(MObject *modelObject, MPackage emit newElementCreated(element, diagram); } -void DiagramSceneController::addRelatedElements(const DSelection &selection, MDiagram *diagram) +int DiagramSceneController::countRelatedElements(const DSelection &selection, MDiagram *diagram, std::function filter) +{ + int counter = 0; + const QList indices = selection.indices(); + for (const DSelection::Index &index : indices) { + DElement *delement = m_diagramController->findElement(index.elementKey(), diagram); + QMT_ASSERT(delement, return 0); + DObject *dobject = dynamic_cast(delement); + if (dobject && dobject->modelUid().isValid()) { + MObject *mobject = m_modelController->findElement(delement->modelUid()); + if (mobject) { + const QList relations = m_modelController->findRelationsOfObject(mobject); + QList filteredRelations; + const QList *relationsList = nullptr; + if (filter) { + for (MRelation *relation : relations) { + if (filter(dobject, mobject, relation)) + filteredRelations.append(relation); + } + relationsList = &filteredRelations; + } else { + relationsList = &relations; + } + for (MRelation *relation : *relationsList) { + if (relation->endAUid() != mobject->uid()) + ++counter; + else if (relation->endBUid() != mobject->uid()) + ++counter; + } + } + } + } + return counter; +} + +void DiagramSceneController::addRelatedElements( + const DSelection &selection, MDiagram *diagram, + std::function filter) { m_diagramController->undoController()->beginMergeSequence(Tr::tr("Add Related Element")); const QList indices = selection.indices(); @@ -435,8 +472,19 @@ void DiagramSceneController::addRelatedElements(const DSelection &selection, MDi qreal dAngle = 360.0 / 11.5; qreal dRadius = 100.0; const QList relations = m_modelController->findRelationsOfObject(mobject); + QList filteredRelations; + const QList *relationsList = nullptr; + if (filter) { + for (MRelation *relation : relations) { + if (filter(dobject, mobject, relation)) + filteredRelations.append(relation); + } + relationsList = &filteredRelations; + } else { + relationsList = &relations; + } int count = 0; - for (MRelation *relation : relations) { + for (MRelation *relation : *relationsList) { if (relation->endAUid() != mobject->uid() || relation->endBUid() != mobject->uid()) ++count; } @@ -446,7 +494,7 @@ void DiagramSceneController::addRelatedElements(const DSelection &selection, MDi } qreal radius = 200.0; qreal angle = 0.0; - for (MRelation *relation : relations) { + for (MRelation *relation : *relationsList) { QPointF pos(dobject->pos()); pos += QPointF(radius * sin(angle / 180 * M_PI), -radius * cos(angle / 180 * M_PI)); bool added = false; diff --git a/src/libs/modelinglib/qmt/tasks/diagramscenecontroller.h b/src/libs/modelinglib/qmt/tasks/diagramscenecontroller.h index a062fa10c4e..14e704a1929 100644 --- a/src/libs/modelinglib/qmt/tasks/diagramscenecontroller.h +++ b/src/libs/modelinglib/qmt/tasks/diagramscenecontroller.h @@ -86,7 +86,12 @@ public: DElement *topMostElementAtPos, const QPointF &pos, MDiagram *diagram, const QPoint &viewPos, const QSize &viewSize); void dropNewModelElement(MObject *modelObject, MPackage *parentPackage, const QPointF &pos, MDiagram *diagram); - void addRelatedElements(const DSelection &selection, MDiagram *diagram); + int countRelatedElements( + const DSelection &selection, MDiagram *diagram, + std::function filter); + void addRelatedElements( + const DSelection &selection, MDiagram *diagram, + std::function filter); MPackage *findSuitableParentPackage(DElement *topmostDiagramElement, MDiagram *diagram); MDiagram *findDiagramBySearchId(MPackage *package, const QString &diagramName); -- cgit v1.2.3