From 61561e77ef271a7a64fda41c68bd37e2be2cf04a Mon Sep 17 00:00:00 2001
From: joanvallve <jvallve@iri.upc.edu>
Date: Thu, 16 Jan 2025 14:41:17 +0100
Subject: [PATCH] improved Buffer class and added BufferVectorComposite

---
 CMakeLists.txt                           |   1 +
 include/core/processor/buffer.h          |  63 ++++++-----
 src/processor/buffer.cpp                 |  38 +++++++
 src/processor/processor_base.cpp         |   4 +-
 src/processor/processor_loop_closure.cpp |   4 +-
 test/CMakeLists.txt                      |   3 +
 test/gtest_buffer.cpp                    | 134 +++++++++++++++++++++++
 test/gtest_buffer_frame.cpp              | 113 ++++++-------------
 8 files changed, 249 insertions(+), 111 deletions(-)
 create mode 100644 src/processor/buffer.cpp
 create mode 100644 test/gtest_buffer.cpp

diff --git a/CMakeLists.txt b/CMakeLists.txt
index dab498ba7..ea35abb22 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -147,6 +147,7 @@ SET(SRCS
   # problem
   src/problem/problem.cpp
   # processor
+  src/processor/buffer.cpp
   src/processor/motion_buffer.cpp
   src/processor/motion_provider.cpp
   src/processor/processor_base.cpp
diff --git a/include/core/processor/buffer.h b/include/core/processor/buffer.h
index f652476bd..d675d0185 100644
--- a/include/core/processor/buffer.h
+++ b/include/core/processor/buffer.h
@@ -25,6 +25,7 @@
 
 namespace wolf
 {
+
 /** \brief Buffer for arbitrary type objects
  *
  * Object and functions to manage a buffer of objects.
@@ -36,8 +37,8 @@ class Buffer
     typedef typename std::map<TimeStamp, T>::iterator       Iterator;       // buffer iterator
     typedef typename std::map<TimeStamp, T>::const_iterator ConstIterator;  // buffer iterator
 
-    Buffer(){};
-    ~Buffer(void){};
+    Buffer() {};
+    ~Buffer(void) {};
 
     /**\brief Select an element from the buffer
      *
@@ -122,22 +123,6 @@ class Buffer
     std::map<TimeStamp, T> container_;  // Main buffer container
 };
 
-/** \brief Buffer of Frames
- *
- * Object and functions to manage a buffer of FrameBasePtr objects.
- */
-class BufferFrame : public Buffer<FrameBasePtr>
-{
-};
-
-/** \brief Buffer of Captures
- *
- * Object and functions to manage a buffer of CaptureBasePtr objects.
- */
-class BufferCapture : public Buffer<CaptureBasePtr>
-{
-};
-
 }  // namespace wolf
 
 #include "core/common/time_stamp.h"
@@ -221,7 +206,7 @@ inline typename Buffer<T>::Iterator Buffer<T>::selectIterator(const TimeStamp& _
 template <typename T>
 inline T Buffer<T>::select(const TimeStamp& _time_stamp, const double& _time_tolerance) const
 {
-    if (container_.empty()) return nullptr;
+    if (container_.empty()) return T();  // nullprt in case of T being a pointer
 
     auto it = selectIterator(_time_stamp, _time_tolerance);
 
@@ -232,14 +217,14 @@ inline T Buffer<T>::select(const TimeStamp& _time_stamp, const double& _time_tol
         return it->second;
     }
 
-    return nullptr;
+    return T();  // nullprt in case of T being a pointer
 }
 
 template <typename T>
 inline T Buffer<T>::selectFirstBefore(const TimeStamp& _time_stamp, const double& _time_tolerance) const
 {
     // There is no element
-    if (container_.empty()) return nullptr;
+    if (container_.empty()) return T();  // nullprt in case of T being a pointer
 
     // Checking on begin() since elements are ordered in time
     // Return first element if is older than time stamp
@@ -249,14 +234,14 @@ inline T Buffer<T>::selectFirstBefore(const TimeStamp& _time_stamp, const double
     if (checkTimeTolerance(container_.begin()->first, _time_stamp, _time_tolerance)) return container_.begin()->second;
 
     // otherwise return nullptr (no element before the provided ts or within the tolerance was found)
-    return nullptr;
+    return T();  // nullprt in case of T being a pointer
 }
 
 template <typename T>
 inline T Buffer<T>::selectLastAfter(const TimeStamp& _time_stamp, const double& _time_tolerance) const
 {
     // There is no element
-    if (container_.empty()) return nullptr;
+    if (container_.empty()) return T();  // nullprt in case of T being a pointer
 
     // Checking on rbegin() since elements are ordered in time
     // Return last element if is newer than time stamp
@@ -267,14 +252,14 @@ inline T Buffer<T>::selectLastAfter(const TimeStamp& _time_stamp, const double&
         return container_.rbegin()->second;
 
     // otherwise return nullptr (no element after the provided ts or within the tolerance was found)
-    return nullptr;
+    return T();  // nullprt in case of T being a pointer
 }
 
 template <typename T>
 inline T Buffer<T>::selectFirst() const
 {
     // There is no element
-    if (container_.empty()) return nullptr;
+    if (container_.empty()) return T();  // nullprt in case of T being a pointer
 
     // Returning first map element
     return container_.begin()->second;
@@ -284,7 +269,7 @@ template <typename T>
 inline T Buffer<T>::selectLast() const
 {
     // There is no element
-    if (container_.empty()) return nullptr;
+    if (container_.empty()) return T();  // nullprt in case of T being a pointer
 
     // Returning last map element
     return container_.rbegin()->second;
@@ -362,4 +347,30 @@ inline bool Buffer<T>::checkTimeTolerance(const TimeStamp& _time_stamp1,
     return pass;
 }
 
+// SPECIFIC BUFFERS
+// fwd declarations
+class VectorComposite;
+
+/** \brief Buffer of Frames
+ */
+class BufferFrame : public Buffer<FrameBasePtr>
+{
+  public:
+    void emplace(FrameBasePtr _frm);
+};
+
+/** \brief Buffer of Captures
+ */
+class BufferCapture : public Buffer<CaptureBasePtr>
+{
+  public:
+    void emplace(CaptureBasePtr _cap);
+};
+
+/** \brief Buffer of VectorComposite
+ */
+class BufferVectorComposite : public Buffer<VectorComposite>
+{
+};
+
 }  // namespace wolf
diff --git a/src/processor/buffer.cpp b/src/processor/buffer.cpp
new file mode 100644
index 000000000..8e79ca399
--- /dev/null
+++ b/src/processor/buffer.cpp
@@ -0,0 +1,38 @@
+// WOLF - Copyright (C) 2020,2021,2022,2023,2024,2025
+// Institut de Robòtica i Informàtica Industrial, CSIC-UPC.
+// Authors: Joan Solà Ortega (jsola@iri.upc.edu) and
+// Joan Vallvé Navarro (jvallve@iri.upc.edu)
+// All rights reserved.
+//
+// This file is part of WOLF: http://www.iri.upc.edu/wolf
+// WOLF is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+#include "core/processor/buffer.h"
+#include "core/frame/frame_base.h"
+#include "core/capture/capture_base.h"
+
+namespace wolf
+{
+
+void BufferFrame::emplace(FrameBasePtr _frm)
+{
+    Buffer<FrameBasePtr>::emplace(_frm->getTimeStamp(), _frm);
+}
+
+void BufferCapture::emplace(CaptureBasePtr _cap)
+{
+    Buffer<CaptureBasePtr>::emplace(_cap->getTimeStamp(), _cap);
+}
+
+}  // namespace wolf
diff --git a/src/processor/processor_base.cpp b/src/processor/processor_base.cpp
index 78d716785..10b3e1a58 100644
--- a/src/processor/processor_base.cpp
+++ b/src/processor/processor_base.cpp
@@ -62,7 +62,7 @@ void ProcessorBase::keyFrameCallback(FrameBasePtr _keyframe)
     startKFProfiling();
 
     // asking if frame should be stored
-    if (storeKeyFrame(_keyframe)) buffer_frame_.emplace(_keyframe->getTimeStamp(), _keyframe);
+    if (storeKeyFrame(_keyframe)) buffer_frame_.emplace(_keyframe);
 
     // asking if frame should be processed
     if (triggerInKeyFrame(_keyframe)) processKeyFrame(_keyframe);
@@ -111,7 +111,7 @@ void ProcessorBase::captureCallback(CaptureBasePtr _capture)
         getProblem()->applyFirstFrameOptions(_capture->getTimeStamp());
 
     // asking if capture should be stored
-    if (storeCapture(_capture)) buffer_capture_.emplace(_capture->getTimeStamp(), _capture);
+    if (storeCapture(_capture)) buffer_capture_.emplace(_capture);
 
     // asking if capture should be processed
     if (triggerInCapture(_capture)) processCapture(_capture);
diff --git a/src/processor/processor_loop_closure.cpp b/src/processor/processor_loop_closure.cpp
index 26a655362..4596c718a 100644
--- a/src/processor/processor_loop_closure.cpp
+++ b/src/processor/processor_loop_closure.cpp
@@ -71,7 +71,7 @@ void ProcessorLoopClosure::processCapture(CaptureBasePtr _capture)
     }
     // CASE 3:
     WOLF_DEBUG("CASE 3");
-    buffer_capture_.emplace(_capture->getTimeStamp(), _capture);
+    buffer_capture_.emplace(_capture);
 }
 
 void ProcessorLoopClosure::processKeyFrame(FrameBasePtr _keyframe)
@@ -125,7 +125,7 @@ void ProcessorLoopClosure::processKeyFrame(FrameBasePtr _keyframe)
         WOLF_DEBUG("CASE 3");
 
         // store frame
-        buffer_frame_.emplace(_keyframe->getTimeStamp(), _keyframe);
+        buffer_frame_.emplace(_keyframe);
 
         return;
     }
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 31836743b..c6e89aadf 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -41,6 +41,9 @@ wolf_add_gtest(gtest_example gtest_example.cpp)          #
 
 # ------------------ First Base classes ------------------
 
+# Buffer
+wolf_add_gtest(gtest_buffer gtest_buffer.cpp)
+
 # BufferFrame
 wolf_add_gtest(gtest_buffer_frame gtest_buffer_frame.cpp)
 
diff --git a/test/gtest_buffer.cpp b/test/gtest_buffer.cpp
new file mode 100644
index 000000000..2cbd795f8
--- /dev/null
+++ b/test/gtest_buffer.cpp
@@ -0,0 +1,134 @@
+// WOLF - Copyright (C) 2020,2021,2022,2023,2024,2025
+// Institut de Robòtica i Informàtica Industrial, CSIC-UPC.
+// Authors: Joan Solà Ortega (jsola@iri.upc.edu) and
+// Joan Vallvé Navarro (jvallve@iri.upc.edu)
+// All rights reserved.
+//
+// This file is part of WOLF: http://www.iri.upc.edu/wolf
+// WOLF is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+#include "core/utils/utils_gtest.h"
+#include "core/processor/buffer.h"
+
+using namespace wolf;
+
+TEST(BufferTest, ConstructorInitializesCorrectly)
+{
+    Buffer<int> buffer;
+
+    // Add assertions to verify the initial state of the buffer
+    ASSERT_EQ(buffer.size(), 0);
+    ASSERT_TRUE(buffer.empty());
+}
+
+TEST(BufferTest, emplace)
+{
+    Buffer<int> buffer;
+    buffer.emplace(TimeStamp(10), 10);  // Assuming emplace is a method of Buffer
+    ASSERT_EQ(buffer.size(), 1);
+    ASSERT_EQ(buffer.getContainer().at(TimeStamp(10)), 10);
+}
+
+TEST(BufferTest, ClearEmptiesBuffer)
+{
+    Buffer<int> buffer;
+    buffer.emplace(TimeStamp(10), 10);
+    buffer.clear();
+    ASSERT_EQ(buffer.size(), 0);
+    ASSERT_TRUE(buffer.empty());
+}
+
+TEST(BufferTest, SelectFirstReturnsCorrectValue)
+{
+    Buffer<int> buffer;
+    buffer.emplace(TimeStamp(10), 10);
+    buffer.emplace(TimeStamp(20), 20);
+    ASSERT_EQ(buffer.selectFirst(), 10);
+}
+
+TEST(BufferTest, SelectLastReturnsCorrectValue)
+{
+    Buffer<int> buffer;
+    buffer.emplace(TimeStamp(10), 10);
+    buffer.emplace(TimeStamp(20), 20);
+    ASSERT_EQ(buffer.selectLast(), 20);
+}
+
+TEST(BufferTest, SelectFirstBeforeReturnsCorrectValue)
+{
+    Buffer<int> buffer;
+    buffer.emplace(TimeStamp(10), 10);
+    buffer.emplace(TimeStamp(20), 20);
+    ASSERT_EQ(buffer.selectFirstBefore(TimeStamp(15), 5), 10);
+    ASSERT_EQ(buffer.selectFirstBefore(TimeStamp(5), 1), 0);  // Assuming 0 is the default value for int
+}
+
+TEST(BufferTest, SelectLastAfterReturnsCorrectValue)
+{
+    Buffer<int> buffer;
+    buffer.emplace(TimeStamp(10), 10);
+    buffer.emplace(TimeStamp(20), 20);
+    ASSERT_EQ(buffer.selectLastAfter(TimeStamp(15), 5), 20);
+    ASSERT_EQ(buffer.selectLastAfter(TimeStamp(25), 1), 0);  // Assuming 0 is the default value for int
+}
+
+TEST(BufferTest, SelectReturnsCorrectValue)
+{
+    Buffer<int> buffer;
+    buffer.emplace(TimeStamp(10), 10);
+    buffer.emplace(TimeStamp(20), 20);
+    ASSERT_EQ(buffer.select(TimeStamp(11), 5), 10);
+    ASSERT_EQ(buffer.select(TimeStamp(11), 100), 10);
+    ASSERT_EQ(buffer.select(TimeStamp(16), 5), 20);
+    ASSERT_EQ(buffer.select(TimeStamp(16), 100), 20);
+    ASSERT_EQ(buffer.select(TimeStamp(25), 4), 0);  // Assuming 0 is the default value for int
+}
+
+TEST(BufferTest, RemoveUpToRemovesCorrectElements)
+{
+    Buffer<int> buffer;
+    buffer.emplace(TimeStamp(10), 10);
+    buffer.emplace(TimeStamp(20), 20);
+    buffer.removeUpTo(TimeStamp(15));
+    ASSERT_EQ(buffer.size(), 1);
+    ASSERT_EQ(buffer.selectFirst(), 20);
+}
+
+TEST(BufferTest, RemoveUpToLowerRemovesCorrectElements)
+{
+    Buffer<int> buffer;
+    buffer.emplace(TimeStamp(10), 10);
+    buffer.emplace(TimeStamp(20), 20);
+    buffer.removeUpToLower(TimeStamp(20));
+    ASSERT_EQ(buffer.size(), 1);
+    ASSERT_EQ(buffer.selectFirst(), 20);
+}
+
+TEST(BufferTest, CheckTimeToleranceWorksCorrectly)
+{
+    ASSERT_TRUE(Buffer<int>::checkTimeTolerance(TimeStamp(10), TimeStamp(15), 5));
+    ASSERT_FALSE(Buffer<int>::checkTimeTolerance(TimeStamp(10), TimeStamp(20), 5));
+}
+
+TEST(BufferTest, DoubleCheckTimeToleranceWorksCorrectly)
+{
+    ASSERT_TRUE(Buffer<int>::doubleCheckTimeTolerance(TimeStamp(10), 5, TimeStamp(15), 5));
+    ASSERT_FALSE(Buffer<int>::doubleCheckTimeTolerance(TimeStamp(10), 5, TimeStamp(20), 5));
+}
+
+int main(int argc, char **argv)
+{
+    ::testing::InitGoogleTest(&argc, argv);
+    return RUN_ALL_TESTS();
+}
\ No newline at end of file
diff --git a/test/gtest_buffer_frame.cpp b/test/gtest_buffer_frame.cpp
index 8e23b962e..7020522f5 100644
--- a/test/gtest_buffer_frame.cpp
+++ b/test/gtest_buffer_frame.cpp
@@ -20,14 +20,10 @@
 
 #include "core/utils/utils_gtest.h"
 
-#include "core/processor/processor_base.h"
-#include "core/capture/capture_void.h"
+#include "core/processor/buffer.h"
+#include "core/frame/frame_base.h"
 #include "core/problem/problem.h"
 
-// STL
-#include <iterator>
-#include <iostream>
-
 using namespace wolf;
 using namespace Eigen;
 
@@ -61,16 +57,16 @@ TEST_F(BufferFrameTest, empty)
 
 TEST_F(BufferFrameTest, emplace)
 {
-    buffer_kf.emplace(10, f10);
+    buffer_kf.emplace(f10);
     ASSERT_EQ(buffer_kf.size(), (unsigned int)1);
-    buffer_kf.emplace(20, f20);
+    buffer_kf.emplace(f20);
     ASSERT_EQ(buffer_kf.size(), (unsigned int)2);
 }
 
 TEST_F(BufferFrameTest, clear)
 {
-    buffer_kf.emplace(10, f10);
-    buffer_kf.emplace(20, f20);
+    buffer_kf.emplace(f10);
+    buffer_kf.emplace(f20);
     ASSERT_EQ(buffer_kf.size(), (unsigned int)2);
     buffer_kf.clear();
     ASSERT_TRUE(buffer_kf.empty());
@@ -79,80 +75,35 @@ TEST_F(BufferFrameTest, clear)
 TEST_F(BufferFrameTest, doubleCheckTimeTolerance)
 {
     buffer_kf.clear();
-    buffer_kf.emplace(10, f10);
-    buffer_kf.emplace(20, f20);
+    buffer_kf.emplace(f10);
+    buffer_kf.emplace(f20);
     // min time tolerance  > diff between time stamps. It should return true
     ASSERT_TRUE(buffer_kf.doubleCheckTimeTolerance(TimeStamp(10), 20, TimeStamp(20), 20));
     // min time tolerance  < diff between time stamps. It should return true
     ASSERT_FALSE(buffer_kf.doubleCheckTimeTolerance(TimeStamp(10), 1, TimeStamp(20), 20));
 }
 
-// TEST_F(BufferFrameTest, select)
-//{
-//    // Evaluation using two packs (p1,p2)
-//    // with different time tolerances (tp1,tp2)
-//    // using a query pack (q) with also different time tolerances
-//    // depending on these tolerances we will get one (p1) or the other (p2)
-//    // packages from the buffer (res).
-//    // This can be summarized in the table hereafter:
-//    //
-//    //  p1 p2 q | resulting pack time stamp
-//    // --------------------------------
-//    //  2  2  2 | nullptr
-//    //  2  2  5 | nullptr
-//    //  2  2  7 | nullptr
-//    //  2  7  2 | nullptr
-//    //  2  7  5 | 20
-//    //  2  7  7 | 20
-//    //  7  2  2 | nullptr
-//    //  7  2  5 | nullptr
-//    //  7  2  7 | 10
-//    //  7  7  2 | nullptr
-//    //  7  7  5 | 20
-//    //  7  7  7 | 20
-//
-//    buffer_kf.clear();
-//
-//    // input packages
-//    std::vector<int> p1 = {2, 7}; // Pack 1 time tolerances
-//    std::vector<int> p2 = {2, 7}; // Pack 2 time tolerances
-//    std::vector<int> q = {2, 5, 7}; // Query pack time tolerances
-//
-//    // Solution matrix
-//    Eigen::VectorXi res = Eigen::VectorXi::Zero(12);
-//    res(4) = 20;
-//    res(5) = 20;
-//    res(8) = 10;
-//    res(10) = 20;
-//    res(11) = 20;
-//
-//    // test
-//    for (unsigned int ip1=0;ip1<p1.size();++ip1)
-//    {
-//        for (unsigned int ip2=0;ip2<p2.size();++ip2)
-//        {
-//            buffer_kf.emplace(f10, p1[ip1]);
-//            buffer_kf.emplace(f20, p2[ip2]);
-//            for (unsigned int iq=0;iq<q.size();++iq)
-//            {
-//                PackKeyFramePtr packQ = buffer_kf.selectPack(16, q[iq]);
-//                if (packQ!=nullptr)
-//                {
-//                    ASSERT_EQ(packQ->key_frame->getTimeStamp(),res(ip1*6+ip2*3+iq));
-//                }
-//            }
-//            buffer_kf.clear();
-//        }
-//    }
-//}
+TEST_F(BufferFrameTest, select)
+{
+    buffer_kf.emplace(f10);
+    buffer_kf.emplace(f20);
+    buffer_kf.emplace(f21);
+    buffer_kf.emplace(f28);
+
+    ASSERT_EQ(buffer_kf.select(16, 1), nullptr);
+    ASSERT_EQ(buffer_kf.select(10, 1), f10);
+    ASSERT_EQ(buffer_kf.select(10, 0), f10);
+    ASSERT_EQ(buffer_kf.select(11, 1), f10);
+    ASSERT_EQ(buffer_kf.select(20.1, 0.1), f20);
+}
 
 TEST_F(BufferFrameTest, selectFirstBefore)
 {
     buffer_kf.clear();
 
-    buffer_kf.emplace(10, f10);
-    buffer_kf.emplace(20, f20);
-    buffer_kf.emplace(21, f21);
+    buffer_kf.emplace(f10);
+    buffer_kf.emplace(f20);
+    buffer_kf.emplace(f21);
 
     // input time stamps
     std::vector<TimeStamp> q_ts = {9.5, 9.995, 10.005, 19.5, 20.5, 21.5};
@@ -192,16 +143,16 @@ TEST_F(BufferFrameTest, selectFirstBefore)
 
     for (int i = 0; i < 3; i++)
     {
-        FrameBasePtr packQ;
+        FrameBasePtr frame;
         int          j = 0;
         for (auto ts : q_ts)
         {
-            packQ = buffer_kf.selectFirstBefore(ts, tt);
-            if (packQ) res(i, j) = packQ->getTimeStamp().get();
+            frame = buffer_kf.selectFirstBefore(ts, tt);
+            if (frame) res(i, j) = frame->getTimeStamp().get();
 
             j++;
         }
-        buffer_kf.removeUpTo(packQ->getTimeStamp());
+        buffer_kf.removeUpTo(frame->getTimeStamp());
     }
 
     ASSERT_MATRIX_APPROX(res, truth, 1e-6);
@@ -212,9 +163,9 @@ TEST_F(BufferFrameTest, removeUpTo)
     // Small time tolerance for all test asserts
     double tt = 0.1;
     buffer_kf.clear();
-    buffer_kf.emplace(10, f10);
-    buffer_kf.emplace(20, f20);
-    buffer_kf.emplace(21, f21);
+    buffer_kf.emplace(f10);
+    buffer_kf.emplace(f20);
+    buffer_kf.emplace(f21);
 
     // it should remove f20 and f10, thus size should be 1 after removal
     // Specifically, only f21 should remain
@@ -226,7 +177,7 @@ TEST_F(BufferFrameTest, removeUpTo)
 
     // Chech removal of an imprecise time stamp
     // Specifically, only f28 should remain
-    buffer_kf.emplace(28, f28);
+    buffer_kf.emplace(f28);
     ASSERT_EQ(buffer_kf.size(), (unsigned int)2);
     FrameBasePtr f22 = std::make_shared<FrameBase>(TimeStamp(22), TypeComposite(), VectorComposite());
     buffer_kf.removeUpTo(f22->getTimeStamp());
-- 
GitLab