From fc2567a6d1bd7c9124cdbb1687b3797361fba319 Mon Sep 17 00:00:00 2001
From: Alessandro Abrami <alessandro.abrami@elettra.eu>
Date: Tue, 8 Jun 2021 09:13:02 +0200
Subject: [PATCH] 2021/06/08: BRANCH(withFilters): 	aggiunta property:
 RilterRpDistance; attributo: attr_FilterTrackEnable_read

---
 src/PespCCDStage.cpp             | 75 +++++++++++++++++++++++++++++---
 src/PespCCDStage.h               | 13 ++++++
 src/PespCCDStage.xmi             | 19 ++++++--
 src/PespCCDStageClass.cpp        | 41 ++++++++++++++++-
 src/PespCCDStageClass.h          | 15 +++++++
 src/PespCCDStageDynAttrUtils.cpp |  4 +-
 src/PespCCDStageStateMachine.cpp | 20 +++++++++
 7 files changed, 175 insertions(+), 12 deletions(-)

diff --git a/src/PespCCDStage.cpp b/src/PespCCDStage.cpp
index 49143d0..ba2809d 100644
--- a/src/PespCCDStage.cpp
+++ b/src/PespCCDStage.cpp
@@ -75,6 +75,7 @@
 //  Pitch                  |  Tango::DevDouble	Scalar
 //  BeamOnYagDisplacement  |  Tango::DevDouble	Scalar
 //  UpdateOnDisplacement   |  Tango::DevBoolean	Scalar
+//  FilterTrackEnable      |  Tango::DevBoolean	Scalar
 //================================================================
 
 namespace PespCCDStage_ns
@@ -201,6 +202,7 @@ void PespCCDStage::delete_device()
 	delete[] attr_Pitch_read;
 	delete[] attr_BeamOnYagDisplacement_read;
 	delete[] attr_UpdateOnDisplacement_read;
+	delete[] attr_FilterTrackEnable_read;
 }
 
 //--------------------------------------------------------
@@ -304,6 +306,7 @@ typedef struct {
 	attr_Pitch_read = new Tango::DevDouble[1];
 	attr_BeamOnYagDisplacement_read = new Tango::DevDouble[1];
 	attr_UpdateOnDisplacement_read = new Tango::DevBoolean[1];
+	attr_FilterTrackEnable_read = new Tango::DevBoolean[1];
 	//	No longer if mandatory property not set. 
 	if (mandatoryNotDefined)
 		return;
@@ -379,10 +382,14 @@ typedef struct {
 
 		//===============================================================
 		// - - - - FILTERS
+		// FF
 		//
         	//p_stage2flt = new Stage2Filter(std::string dev, axis_nick, itpp::vec R /*pto in  xOz*/, double df);
 
-       		p_stage2flt = new PespStage_ns::Stage2Filter(this, "tango://srv-tango-padres-01:20000/pos_mag/mover1d/flt_mrc_pos_mag.01", "X", __cr, double(40));
+		if (filterRpDistance > 0 )
+       			p_stage2flt = new PespStage_ns::Stage2Filter(this, "tango://srv-tango-padres-01:20000/pos_mag/mover1d/flt_mrc_pos_mag.01", "X", __cr, filterRpDistance);
+		else
+			p_stage2flt = NULL;
 		//===============================================================
 	} else {
 		pstage = NULL;
@@ -632,6 +639,7 @@ void PespCCDStage::get_device_property()
 	dev_prop.push_back(Tango::DbDatum("StageType"));
 	dev_prop.push_back(Tango::DbDatum("YagsCenterOffsets"));
 	dev_prop.push_back(Tango::DbDatum("RotationCenterXZ"));
+	dev_prop.push_back(Tango::DbDatum("FilterRpDistance"));
 
 	//	is there at least one property to be read ?
 	if (dev_prop.size()>0)
@@ -755,6 +763,17 @@ void PespCCDStage::get_device_property()
 		//	And try to extract RotationCenterXZ value from database
 		if (dev_prop[i].is_empty()==false)	dev_prop[i]  >>  rotationCenterXZ;
 
+		//	Try to initialize FilterRpDistance from class property
+		cl_prop = ds_class->get_class_property(dev_prop[++i].name);
+		if (cl_prop.is_empty()==false)	cl_prop  >>  filterRpDistance;
+		else {
+			//	Try to initialize FilterRpDistance from default device value
+			def_prop = ds_class->get_default_device_property(dev_prop[i].name);
+			if (def_prop.is_empty()==false)	def_prop  >>  filterRpDistance;
+		}
+		//	And try to extract FilterRpDistance value from database
+		if (dev_prop[i].is_empty()==false)	dev_prop[i]  >>  filterRpDistance;
+
 	}
 
 	/*----- PROTECTED REGION ID(PespCCDStage::get_device_property_after) ENABLED START -----*/
@@ -780,9 +799,7 @@ void PespCCDStage::check_mandatory_property(Tango::DbDatum &class_prop, Tango::D
 			tms << "\' is mandatory but not defined in database";
 		else
 			tms << "\' is mandatory but cannot be defined without database";
-		string	status(get_status());
-		status += tms.str();
-		set_status(status);
+		append_status(tms.str());
 		mandatoryNotDefined = true;
 		/*----- PROTECTED REGION ID(PespCCDStage::check_mandatory_property) ENABLED START -----*/
 		cerr << tms.str() << " for " << device_name << endl;
@@ -803,10 +820,9 @@ void PespCCDStage::always_executed_hook()
 	DEBUG_STREAM << "PespCCDStage::always_executed_hook()  " << device_name << endl;
 	if (mandatoryNotDefined)
 	{
-		string	status(get_status());
 		Tango::Except::throw_exception(
 					(const char *)"PROPERTY_NOT_SET",
-					status.c_str(),
+					get_status().c_str(),
 					(const char *)"PespCCDStage::always_executed_hook()");
 	}
 	/*----- PROTECTED REGION ID(PespCCDStage::always_executed_hook) ENABLED START -----*/
@@ -932,6 +948,7 @@ void PespCCDStage::write_FocusX(Tango::WAttribute &attr)
 
 	//FF
 	itpp::vec Focus = newXZ;
+	if ( (p_stage2flt != NULL) && attr_FilterTrackEnable_read[0] )
 	if (!p_stage2flt->correct_position(Focus)){
       		INFO_STREAM << "PespCCDStage::write_FocusX() error on filters"<< endl;
 	}
@@ -999,6 +1016,7 @@ void PespCCDStage::write_FocusZ(Tango::WAttribute &attr)
 
 	//FF
 	itpp::vec Focus = newXZ;
+	if ( (p_stage2flt != NULL) && attr_FilterTrackEnable_read[0] )
 	if (!p_stage2flt->correct_position(Focus)){
       		INFO_STREAM << "PespCCDStage::write_FocusZ() error on filters"<< endl;
 	}
@@ -1071,6 +1089,7 @@ void PespCCDStage::write_FocusDistance(Tango::WAttribute &attr)
 
 	//FF
 	itpp::vec Focus = newXZ;
+	if ( (p_stage2flt != NULL) && attr_FilterTrackEnable_read[0] )
 	if (!p_stage2flt->correct_position(Focus)){
       		INFO_STREAM << "PespCCDStage::write_FocusDistance() error on filters"<< endl;
 	}
@@ -1142,6 +1161,7 @@ void PespCCDStage::write_FocusAngle(Tango::WAttribute &attr)
 
 	//FF
 	itpp::vec Focus = newXZ;
+	if ( (p_stage2flt != NULL) && attr_FilterTrackEnable_read[0] )
 	if (!p_stage2flt->correct_position(Focus)){
       		INFO_STREAM << "PespCCDStage::write_FocusDistance() error on filters"<< endl;
 	}
@@ -1245,6 +1265,7 @@ void PespCCDStage::write_BeamOnYagDisplacement(Tango::WAttribute &attr)
 
 	//FF
 	itpp::vec Focus = oldXZ;
+	if ( (p_stage2flt != NULL) && attr_FilterTrackEnable_read[0] )
 	if (!p_stage2flt->correct_position(Focus)){
       		INFO_STREAM << "PespCCDStage::write_FocusDistance() error on filters"<< endl;
 	}
@@ -1292,6 +1313,46 @@ void PespCCDStage::write_UpdateOnDisplacement(Tango::WAttribute &attr)
 	
 	/*----- PROTECTED REGION END -----*/	//	PespCCDStage::write_UpdateOnDisplacement
 }
+//--------------------------------------------------------
+/**
+ *	Read attribute FilterTrackEnable related method
+ *	Description: 
+ *
+ *	Data type:	Tango::DevBoolean
+ *	Attr type:	Scalar
+ */
+//--------------------------------------------------------
+void PespCCDStage::read_FilterTrackEnable(Tango::Attribute &attr)
+{
+	DEBUG_STREAM << "PespCCDStage::read_FilterTrackEnable(Tango::Attribute &attr) entering... " << endl;
+	/*----- PROTECTED REGION ID(PespCCDStage::read_FilterTrackEnable) ENABLED START -----*/
+	//	Set the attribute value
+	attr.set_value(attr_FilterTrackEnable_read);
+	
+	/*----- PROTECTED REGION END -----*/	//	PespCCDStage::read_FilterTrackEnable
+}
+//--------------------------------------------------------
+/**
+ *	Write attribute FilterTrackEnable related method
+ *	Description: 
+ *
+ *	Data type:	Tango::DevBoolean
+ *	Attr type:	Scalar
+ */
+//--------------------------------------------------------
+void PespCCDStage::write_FilterTrackEnable(Tango::WAttribute &attr)
+{
+	DEBUG_STREAM << "PespCCDStage::write_FilterTrackEnable(Tango::WAttribute &attr) entering... " << endl;
+	//	Retrieve write value
+	Tango::DevBoolean	w_val;
+	attr.get_write_value(w_val);
+	/*----- PROTECTED REGION ID(PespCCDStage::write_FilterTrackEnable) ENABLED START -----*/
+
+	// FF
+	attr_FilterTrackEnable_read[0] = w_val;
+	
+	/*----- PROTECTED REGION END -----*/	//	PespCCDStage::write_FilterTrackEnable
+}
 
 //--------------------------------------------------------
 /**
@@ -1407,6 +1468,7 @@ void PespCCDStage::write_YagSelector(Tango::WAttribute &attr)
 	//FF
 	if (attr_UpdateOnSelector && !first_write_YagSelector){
 		itpp::vec Focus = oldXZ;
+		if ( (p_stage2flt != NULL) && attr_FilterTrackEnable_read[0] )
 		if (!p_stage2flt->correct_position(Focus)){
       			INFO_STREAM << "PespCCDStage::write_FocusDistance() error on filters"<< endl;
 		}
@@ -1547,6 +1609,7 @@ Tango::DevBoolean PespCCDStage::set_angle_distance(const Tango::DevVarDoubleArra
 
 	//FF
 	itpp::vec Focus = newXZ;
+	if ( (p_stage2flt != NULL) && attr_FilterTrackEnable_read[0] )
 	if (!p_stage2flt->correct_position(Focus)){
       		INFO_STREAM << "PespCCDStage::write_FocusDistance() error on filters"<< endl;
 	}
diff --git a/src/PespCCDStage.h b/src/PespCCDStage.h
index c7d77bf..4e2fab9 100644
--- a/src/PespCCDStage.h
+++ b/src/PespCCDStage.h
@@ -150,6 +150,8 @@ public:
 	vector<Tango::DevDouble>	yagsCenterOffsets;
 	//	RotationCenterXZ:	Centro di rotazione (del naso) in coord. xOz
 	vector<Tango::DevDouble>	rotationCenterXZ;
+	//	FilterRpDistance:	Distance between filter axis and rotation point Rp (near ccd)
+	Tango::DevDouble	filterRpDistance;
 
 	bool	mandatoryNotDefined;
 
@@ -163,6 +165,7 @@ public:
 	Tango::DevDouble	*attr_Pitch_read;
 	Tango::DevDouble	*attr_BeamOnYagDisplacement_read;
 	Tango::DevBoolean	*attr_UpdateOnDisplacement_read;
+	Tango::DevBoolean	*attr_FilterTrackEnable_read;
 
 //	Constructors and destructors
 public:
@@ -313,6 +316,16 @@ public:
 	virtual void read_UpdateOnDisplacement(Tango::Attribute &attr);
 	virtual void write_UpdateOnDisplacement(Tango::WAttribute &attr);
 	virtual bool is_UpdateOnDisplacement_allowed(Tango::AttReqType type);
+/**
+ *	Attribute FilterTrackEnable related methods
+ *	Description: 
+ *
+ *	Data type:	Tango::DevBoolean
+ *	Attr type:	Scalar
+ */
+	virtual void read_FilterTrackEnable(Tango::Attribute &attr);
+	virtual void write_FilterTrackEnable(Tango::WAttribute &attr);
+	virtual bool is_FilterTrackEnable_allowed(Tango::AttReqType type);
 
 //	Dynamic attribute methods
 public:
diff --git a/src/PespCCDStage.xmi b/src/PespCCDStage.xmi
index e7306f7..0afac97 100644
--- a/src/PespCCDStage.xmi
+++ b/src/PespCCDStage.xmi
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="ASCII"?>
 <pogoDsl:PogoSystem xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:pogoDsl="http://www.esrf.fr/tango/pogo/PogoDsl">
-  <classes name="PespCCDStage" pogoRevision="9.4">
-    <description description="" title="" sourcePath="/home/abrami/devel-T9/fermi/servers/pespccdstage/src" language="Cpp" filestogenerate="XMI   file,Code files,Protected Regions" license="GPL" hasMandatoryProperty="true" hasConcreteProperty="true" hasAbstractCommand="false" hasAbstractAttribute="false">
+  <classes name="PespCCDStage" pogoRevision="9.6">
+    <description description="" title="" sourcePath="/home/alessandro.abrami/devel-git/cs/ds/pespccdstage/src" language="Cpp" filestogenerate="XMI   file,Code files,Protected Regions" license="GPL" hasMandatoryProperty="true" hasConcreteProperty="true" hasAbstractCommand="false" hasAbstractAttribute="false">
       <inheritances classname="Device_Impl" sourcePath=""/>
       <identification contact="at elettra.trieste.it - alessandro.abrami" author="alessandro.abrami" emailDomain="elettra.trieste.it" classFamily="BeamlineComponents" siteSpecific="" platform="All Platforms" bus="Not Applicable" manufacturer="none" reference=""/>
     </description>
@@ -41,6 +41,11 @@
       <type xsi:type="pogoDsl:DoubleVectorType"/>
       <status abstract="false" inherited="false" concrete="true" concreteHere="true"/>
     </deviceProperties>
+    <deviceProperties name="FilterRpDistance" description="Distance between filter axis and rotation point Rp (near ccd)">
+      <type xsi:type="pogoDsl:DoubleType"/>
+      <status abstract="false" inherited="false" concrete="true" concreteHere="true"/>
+      <DefaultPropValue>-1</DefaultPropValue>
+    </deviceProperties>
     <commands name="State" description="This command gets the device state (stored in its device_state data member) and returns it to the caller." execMethod="dev_state" displayLevel="OPERATOR" polledPeriod="0">
       <argin description="none">
         <type xsi:type="pogoDsl:VoidType"/>
@@ -150,6 +155,14 @@
       <status abstract="false" inherited="false" concrete="true" concreteHere="true"/>
       <properties description="" label="" unit="" standardUnit="" displayUnit="" format="" maxValue="" minValue="" maxAlarm="" minAlarm="" maxWarning="" minWarning="" deltaTime="" deltaValue=""/>
     </attributes>
+    <attributes name="FilterTrackEnable" attType="Scalar" rwType="READ_WRITE" displayLevel="EXPERT" polledPeriod="0" maxX="" maxY="" memorized="true" memorizedAtInit="true" allocReadMember="true" isDynamic="false">
+      <dataType xsi:type="pogoDsl:BooleanType"/>
+      <changeEvent fire="false" libCheckCriteria="false"/>
+      <archiveEvent fire="false" libCheckCriteria="false"/>
+      <dataReadyEvent fire="false" libCheckCriteria="true"/>
+      <status abstract="false" inherited="false" concrete="true" concreteHere="true"/>
+      <properties description="" label="" unit="" standardUnit="" displayUnit="" format="" maxValue="" minValue="" maxAlarm="" minAlarm="" maxWarning="" minWarning="" deltaTime="" deltaValue=""/>
+    </attributes>
     <dynamicAttributes name="YagSelector" attType="Scalar" rwType="READ_WRITE" displayLevel="OPERATOR" polledPeriod="0" maxX="" maxY="" memorized="true" memorizedAtInit="true" allocReadMember="true" isDynamic="true">
       <dataType xsi:type="pogoDsl:UShortType"/>
       <changeEvent fire="false" libCheckCriteria="false"/>
@@ -166,6 +179,6 @@
       <status abstract="false" inherited="false" concrete="true" concreteHere="true"/>
       <properties description="" label="" unit="" standardUnit="" displayUnit="" format="" maxValue="" minValue="" maxAlarm="" minAlarm="" maxWarning="" minWarning="" deltaTime="" deltaValue=""/>
     </dynamicAttributes>
-    <preferences docHome="./doc_html" makefileHome="/usr/local/tango-9.2.5a/share/pogo/preferences"/>
+    <preferences docHome="./doc_html" makefileHome="/usr/local/tango-9.3.3/share/pogo/preferences"/>
   </classes>
 </pogoDsl:PogoSystem>
diff --git a/src/PespCCDStageClass.cpp b/src/PespCCDStageClass.cpp
index 3db9c46..7515b5d 100644
--- a/src/PespCCDStageClass.cpp
+++ b/src/PespCCDStageClass.cpp
@@ -400,6 +400,20 @@ void PespCCDStageClass::set_default_property()
 	}
 	else
 		add_wiz_dev_prop(prop_name, prop_desc);
+	prop_name = "FilterRpDistance";
+	prop_desc = "Distance between filter axis and rotation point Rp (near ccd)";
+	prop_def  = "-1";
+	vect_data.clear();
+	vect_data.push_back("-1");
+	if (prop_def.length()>0)
+	{
+		Tango::DbDatum	data(prop_name);
+		data << vect_data ;
+		dev_def_prop.push_back(data);
+		add_wiz_dev_prop(prop_name, prop_desc,  prop_def);
+	}
+	else
+		add_wiz_dev_prop(prop_name, prop_desc);
 }
 
 //--------------------------------------------------------
@@ -704,6 +718,31 @@ void PespCCDStageClass::attribute_factory(vector<Tango::Attr *> &att_list)
 	updateondisplacement->set_memorized_init(true);
 	att_list.push_back(updateondisplacement);
 
+	//	Attribute : FilterTrackEnable
+	FilterTrackEnableAttrib	*filtertrackenable = new FilterTrackEnableAttrib();
+	Tango::UserDefaultAttrProp	filtertrackenable_prop;
+	//	description	not set for FilterTrackEnable
+	//	label	not set for FilterTrackEnable
+	//	unit	not set for FilterTrackEnable
+	//	standard_unit	not set for FilterTrackEnable
+	//	display_unit	not set for FilterTrackEnable
+	//	format	not set for FilterTrackEnable
+	//	max_value	not set for FilterTrackEnable
+	//	min_value	not set for FilterTrackEnable
+	//	max_alarm	not set for FilterTrackEnable
+	//	min_alarm	not set for FilterTrackEnable
+	//	max_warning	not set for FilterTrackEnable
+	//	min_warning	not set for FilterTrackEnable
+	//	delta_t	not set for FilterTrackEnable
+	//	delta_val	not set for FilterTrackEnable
+	
+	filtertrackenable->set_default_properties(filtertrackenable_prop);
+	//	Not Polled
+	filtertrackenable->set_disp_level(Tango::EXPERT);
+	filtertrackenable->set_memorized();
+	filtertrackenable->set_memorized_init(true);
+	att_list.push_back(filtertrackenable);
+
 
 	//	Create a list of static attributes
 	create_static_attribute_list(get_class_attr()->get_attr_list());
@@ -854,7 +893,7 @@ void PespCCDStageClass::erase_dynamic_attributes(const Tango::DevVarStringArray
 
 //--------------------------------------------------------
 /**
- *	Method      : PespCCDStageClass::get_attr_by_name()
+ *	Method      : PespCCDStageClass::get_attr_object_by_name()
  *	Description : returns Tango::Attr * object found by name
  */
 //--------------------------------------------------------
diff --git a/src/PespCCDStageClass.h b/src/PespCCDStageClass.h
index 6d58356..9ba53e6 100644
--- a/src/PespCCDStageClass.h
+++ b/src/PespCCDStageClass.h
@@ -170,6 +170,21 @@ public:
 		{return (static_cast<PespCCDStage *>(dev))->is_UpdateOnDisplacement_allowed(ty);}
 };
 
+//	Attribute FilterTrackEnable class definition
+class FilterTrackEnableAttrib: public Tango::Attr
+{
+public:
+	FilterTrackEnableAttrib():Attr("FilterTrackEnable",
+			Tango::DEV_BOOLEAN, Tango::READ_WRITE) {};
+	~FilterTrackEnableAttrib() {};
+	virtual void read(Tango::DeviceImpl *dev,Tango::Attribute &att)
+		{(static_cast<PespCCDStage *>(dev))->read_FilterTrackEnable(att);}
+	virtual void write(Tango::DeviceImpl *dev,Tango::WAttribute &att)
+		{(static_cast<PespCCDStage *>(dev))->write_FilterTrackEnable(att);}
+	virtual bool is_allowed(Tango::DeviceImpl *dev,Tango::AttReqType ty)
+		{return (static_cast<PespCCDStage *>(dev))->is_FilterTrackEnable_allowed(ty);}
+};
+
 
 //=========================================
 //	Define classes for dynamic attributes
diff --git a/src/PespCCDStageDynAttrUtils.cpp b/src/PespCCDStageDynAttrUtils.cpp
index 6ea7156..849fd4c 100644
--- a/src/PespCCDStageDynAttrUtils.cpp
+++ b/src/PespCCDStageDynAttrUtils.cpp
@@ -106,7 +106,7 @@ void PespCCDStage::add_YagSelector_dynamic_attribute(string attname)
 //--------------------------------------------------------
 void PespCCDStage::remove_YagSelector_dynamic_attribute(string attname)
 {
-	remove_attribute(attname, true);
+	remove_attribute(attname, true, Tango::Util::instance()->_UseDb);
 	map<string,Tango::DevUShort>::iterator ite;
     if ((ite=YagSelector_data.find(attname))!=YagSelector_data.end())
     {
@@ -164,7 +164,7 @@ void PespCCDStage::add_UpdateOnSelector_dynamic_attribute(string attname)
 //--------------------------------------------------------
 void PespCCDStage::remove_UpdateOnSelector_dynamic_attribute(string attname)
 {
-	remove_attribute(attname, true);
+	remove_attribute(attname, true, Tango::Util::instance()->_UseDb);
 	map<string,Tango::DevBoolean>::iterator ite;
     if ((ite=UpdateOnSelector_data.find(attname))!=UpdateOnSelector_data.end())
     {
diff --git a/src/PespCCDStageStateMachine.cpp b/src/PespCCDStageStateMachine.cpp
index aade630..7f464bb 100644
--- a/src/PespCCDStageStateMachine.cpp
+++ b/src/PespCCDStageStateMachine.cpp
@@ -197,6 +197,26 @@ bool PespCCDStage::is_UpdateOnDisplacement_allowed(TANGO_UNUSED(Tango::AttReqTyp
 	return true;
 }
 
+//--------------------------------------------------------
+/**
+ *	Method      : PespCCDStage::is_FilterTrackEnable_allowed()
+ *	Description : Execution allowed for FilterTrackEnable attribute
+ */
+//--------------------------------------------------------
+bool PespCCDStage::is_FilterTrackEnable_allowed(TANGO_UNUSED(Tango::AttReqType type))
+{
+	//	Not any excluded states for FilterTrackEnable attribute in Write access.
+	/*----- PROTECTED REGION ID(PespCCDStage::FilterTrackEnableStateAllowed_WRITE) ENABLED START -----*/
+	
+	/*----- PROTECTED REGION END -----*/	//	PespCCDStage::FilterTrackEnableStateAllowed_WRITE
+
+	//	Not any excluded states for FilterTrackEnable attribute in read access.
+	/*----- PROTECTED REGION ID(PespCCDStage::FilterTrackEnableStateAllowed_READ) ENABLED START -----*/
+	
+	/*----- PROTECTED REGION END -----*/	//	PespCCDStage::FilterTrackEnableStateAllowed_READ
+	return true;
+}
+
 //--------------------------------------------------------
 /**
  *	Method      : PespCCDStage::is_YagSelector_allowed()
-- 
GitLab