From 5e49328129cb072df5a167404c16b6a3bf698fd5 Mon Sep 17 00:00:00 2001
From: cramakri <cramakri>
Date: Wed, 11 Jul 2012 09:16:51 +0000
Subject: [PATCH] BIS-21 SP-177 : Add a wrapper that catches assertions in
 hardlink makers

SVN: 26071
---
 ...sertionCatchingImmutableCopierWrapper.java |  74 ++++++
 .../filesystem/AbstractHardlinkMakerTest.java | 210 ++++++++++++++++++
 .../FastRecursiveHardLinkMakerTest.java       |  54 +++++
 .../RecursiveHardLinkMakerTest.java           | 166 +-------------
 .../filesystem/TestBigStructureCreator.java   | 163 ++++++++++++++
 5 files changed, 504 insertions(+), 163 deletions(-)
 create mode 100644 common/source/java/ch/systemsx/cisd/common/filesystem/AssertionCatchingImmutableCopierWrapper.java
 create mode 100644 common/sourceTest/java/ch/systemsx/cisd/common/filesystem/AbstractHardlinkMakerTest.java
 create mode 100644 common/sourceTest/java/ch/systemsx/cisd/common/filesystem/FastRecursiveHardLinkMakerTest.java
 create mode 100644 common/sourceTest/java/ch/systemsx/cisd/common/filesystem/TestBigStructureCreator.java

diff --git a/common/source/java/ch/systemsx/cisd/common/filesystem/AssertionCatchingImmutableCopierWrapper.java b/common/source/java/ch/systemsx/cisd/common/filesystem/AssertionCatchingImmutableCopierWrapper.java
new file mode 100644
index 00000000000..2b04e08a57b
--- /dev/null
+++ b/common/source/java/ch/systemsx/cisd/common/filesystem/AssertionCatchingImmutableCopierWrapper.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2012 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.common.filesystem;
+
+import java.io.File;
+
+import ch.systemsx.cisd.common.exceptions.Status;
+
+/**
+ * A wrapper on an {@link IImmutableCopier} that catches assertions and returns a failure status if
+ * any are encountered.
+ * 
+ * @author Chandrasekhar Ramakrishnan
+ */
+public class AssertionCatchingImmutableCopierWrapper implements IImmutableCopier
+{
+    private final IImmutableCopier wrapped;
+
+    /**
+     * Create a wrapper on copier
+     * 
+     * @param copier The copier to wrap.
+     */
+    public AssertionCatchingImmutableCopierWrapper(IImmutableCopier copier)
+    {
+        this.wrapped = copier;
+    }
+
+    @Override
+    public Status copyImmutably(File source, File destinationDirectory, String nameOrNull)
+    {
+        Status result;
+        try
+        {
+            result = wrapped.copyImmutably(source, destinationDirectory, nameOrNull);
+        } catch (AssertionError e)
+        {
+            result = Status.createError(e.getMessage());
+        }
+
+        return result;
+    }
+
+    @Override
+    public Status copyImmutably(File source, File destinationDirectory, String nameOrNull,
+            CopyModeExisting mode)
+    {
+        Status result;
+        try
+        {
+            result = wrapped.copyImmutably(source, destinationDirectory, nameOrNull, mode);
+        } catch (AssertionError e)
+        {
+            result = Status.createError(e.getMessage());
+        }
+
+        return result;
+    }
+
+}
diff --git a/common/sourceTest/java/ch/systemsx/cisd/common/filesystem/AbstractHardlinkMakerTest.java b/common/sourceTest/java/ch/systemsx/cisd/common/filesystem/AbstractHardlinkMakerTest.java
new file mode 100644
index 00000000000..26f8dc1e2f5
--- /dev/null
+++ b/common/sourceTest/java/ch/systemsx/cisd/common/filesystem/AbstractHardlinkMakerTest.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2012 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.common.filesystem;
+
+import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertFalse;
+import static org.testng.AssertJUnit.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import ch.systemsx.cisd.common.collections.CollectionIO;
+import ch.systemsx.cisd.common.logging.LogInitializer;
+
+/**
+ * The abstract superclass of tests for the various hardlink maker implementations.
+ * 
+ * @author Chandrasekhar Ramakrishnan
+ */
+public abstract class AbstractHardlinkMakerTest
+{
+    protected static final File unitTestRootDirectory = new File("targets" + File.separator
+            + "unit-test-wd");
+
+    protected static final File workingDirectory = new File(unitTestRootDirectory,
+            FastRecursiveHardLinkMakerTest.class.getSimpleName());
+
+    protected static final File outputDir = new File(workingDirectory, "output");
+
+    protected static File createFile(File directory, String name) throws IOException
+    {
+        final File file = new File(directory, name);
+        file.createNewFile();
+        assert file.isFile();
+        CollectionIO
+                .writeIterable(file, Arrays.asList("test line 1", "test line 2", "test line 3"));
+        file.deleteOnExit();
+        return file;
+    }
+
+    @BeforeClass
+    public void init()
+    {
+        LogInitializer.init();
+        unitTestRootDirectory.mkdirs();
+        assert unitTestRootDirectory.isDirectory();
+    }
+
+    @BeforeMethod
+    public void setUp()
+    {
+        FileUtilities.deleteRecursively(workingDirectory);
+        outputDir.mkdirs();
+    }
+
+    @AfterClass
+    public void clean()
+    {
+        FileUtilities.deleteRecursively(outputDir);
+    }
+
+    private static File createDirectory(File directory, String name) throws IOException
+    {
+        final File file = new File(directory, name);
+        file.mkdir();
+        assert file.isDirectory();
+        file.deleteOnExit();
+        return file;
+    }
+
+    private static File mkPath(File parent, String... subdirs)
+    {
+        File file = parent;
+        for (String subdir : subdirs)
+        {
+            file = new File(file, subdir);
+        }
+        return file;
+    }
+
+    private static void assertFileExists(File file)
+    {
+        assert file.isFile();
+    }
+
+    private static void assertStructureExists(File destinationDir)
+    {
+        assertFileExists(mkPath(destinationDir, "dir1", "dir1a", "file1a"));
+        assertFileExists(mkPath(destinationDir, "dir1", "dir1b", "file1b"));
+        assertFileExists(mkPath(destinationDir, "dir2", "file4"));
+        assertFileExists(mkPath(destinationDir, "file2"));
+        assertFileExists(mkPath(destinationDir, "file3"));
+    }
+
+    private void createStructure(File inputDir) throws IOException
+    {
+        final File dir1 = createDirectory(inputDir, "dir1");
+        final File dir1a = createDirectory(dir1, "dir1a");
+        final File dir1b = createDirectory(dir1, "dir1b");
+        final File dir2 = createDirectory(inputDir, "dir2");
+        createFile(dir1a, "file1a");
+        createFile(dir1b, "file1b");
+        createFile(inputDir, "file2");
+        createFile(inputDir, "file3");
+        createFile(dir2, "file4");
+    }
+
+    @Test(groups =
+        { "requires_unix" })
+    public void testCopyWithHardLinks() throws IOException
+    {
+        File inputDir = createDirectory(workingDirectory, "resource-to-copy");
+        createStructure(inputDir);
+        assertTrue(createHardLinkCopier().copyImmutably(inputDir, outputDir, null).isOK());
+        File newInput = new File(outputDir, inputDir.getName());
+
+        assertStructureExists(newInput);
+        boolean deleted = FileUtilities.deleteRecursively(inputDir);
+        assert deleted;
+        assertStructureExists(newInput);
+    }
+
+    private static void assertFilesIdentical(File file1, File file2)
+    {
+        List<String> list1 = CollectionIO.readList(file1);
+        List<String> list2 = CollectionIO.readList(file2);
+        assertEquals(list1, list2);
+    }
+
+    @Test(groups =
+        { "requires_unix" })
+    public void testCopyFile() throws IOException
+    {
+        File src = createFile(workingDirectory, "fileXXX");
+        assertFileExists(src);
+
+        assertTrue(createHardLinkCopier().copyImmutably(src, outputDir, null).isOK());
+        File dest = new File(outputDir, src.getName());
+        assertFileExists(dest);
+
+        modifyDest(dest);
+        assertFilesIdentical(src, dest);
+    }
+
+    private static void modifyDest(File file)
+    {
+        List<String> list = Arrays.asList("new line 1", "new line 2");
+        CollectionIO.writeIterable(file, list);
+    }
+
+    /**
+     *
+     *
+     */
+    public AbstractHardlinkMakerTest()
+    {
+        super();
+    }
+
+    @Test(groups =
+        { "requires_unix" })
+    public void testDeleteWhileCopying() throws IOException
+    {
+        TestBigStructureCreator creator =
+                createBigStructureCreator(new File(workingDirectory, "big-structure"));
+        final File src = creator.createBigStructure();
+        assertTrue(creator.verifyStructure());
+        creator.deleteBigStructureAsync();
+        IImmutableCopier copier =
+                new AssertionCatchingImmutableCopierWrapper(createHardLinkCopier());
+        assertFalse(copier.copyImmutably(src, outputDir, null).isOK());
+        File dest = new File(outputDir, src.getName());
+
+        TestBigStructureCreator structureCopy = new TestBigStructureCreator(dest);
+        assertFalse("Big structure was partially copied", structureCopy.verifyStructure());
+        assertFalse("Original was not partially deleted", creator.verifyStructure());
+
+    }
+
+    /**
+     * Construct a TestBigStructureCreator. Subclasses may override.
+     */
+    protected TestBigStructureCreator createBigStructureCreator(File root)
+    {
+        return new TestBigStructureCreator(root);
+    }
+
+    protected abstract IImmutableCopier createHardLinkCopier();
+}
\ No newline at end of file
diff --git a/common/sourceTest/java/ch/systemsx/cisd/common/filesystem/FastRecursiveHardLinkMakerTest.java b/common/sourceTest/java/ch/systemsx/cisd/common/filesystem/FastRecursiveHardLinkMakerTest.java
new file mode 100644
index 00000000000..fde9aa107b0
--- /dev/null
+++ b/common/sourceTest/java/ch/systemsx/cisd/common/filesystem/FastRecursiveHardLinkMakerTest.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2007 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.common.filesystem;
+
+import static org.testng.AssertJUnit.assertFalse;
+import static org.testng.AssertJUnit.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.testng.annotations.Test;
+
+/**
+ * Test cases for the {@link FastRecursiveHardLinkMaker}.
+ * <p>
+ * More or less a duplicate of {@link RecursiveHardLinkMakerTest}.
+ * 
+ * @author Chandrasekhar Ramakrishnan
+ */
+public class FastRecursiveHardLinkMakerTest extends AbstractHardlinkMakerTest
+{
+
+    @Override
+    protected TestBigStructureCreator createBigStructureCreator(File root)
+    {
+        int[] numberOfFolders =
+            { 100, 10 };
+        int[] numberOfFiles =
+            { 1, 10, 10 };
+        return new TestBigStructureCreator(root, numberOfFolders, numberOfFiles);
+    }
+
+    @Override
+    protected IImmutableCopier createHardLinkCopier()
+    {
+        IImmutableCopier copier = FastRecursiveHardLinkMaker.tryCreate();
+        assert copier != null;
+        return copier;
+    }
+}
diff --git a/common/sourceTest/java/ch/systemsx/cisd/common/filesystem/RecursiveHardLinkMakerTest.java b/common/sourceTest/java/ch/systemsx/cisd/common/filesystem/RecursiveHardLinkMakerTest.java
index dc1e21fa2d0..6212f7682c7 100644
--- a/common/sourceTest/java/ch/systemsx/cisd/common/filesystem/RecursiveHardLinkMakerTest.java
+++ b/common/sourceTest/java/ch/systemsx/cisd/common/filesystem/RecursiveHardLinkMakerTest.java
@@ -16,179 +16,19 @@
 
 package ch.systemsx.cisd.common.filesystem;
 
-import static org.testng.AssertJUnit.*;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.List;
-
-import org.testng.annotations.AfterClass;
-import org.testng.annotations.BeforeClass;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-import ch.systemsx.cisd.common.collections.CollectionIO;
-import ch.systemsx.cisd.common.filesystem.FileUtilities;
-import ch.systemsx.cisd.common.filesystem.HardLinkMaker;
-import ch.systemsx.cisd.common.filesystem.IImmutableCopier;
-import ch.systemsx.cisd.common.filesystem.RecursiveHardLinkMaker;
-import ch.systemsx.cisd.common.logging.LogInitializer;
-
 /**
  * Test cases for the {@link RecursiveHardLinkMaker}.
  * 
  * @author Tomasz Pylak
  */
-public class RecursiveHardLinkMakerTest
+public class RecursiveHardLinkMakerTest extends AbstractHardlinkMakerTest
 {
-    private static final File unitTestRootDirectory =
-            new File("targets" + File.separator + "unit-test-wd");
-
-    private static final File workingDirectory =
-            new File(unitTestRootDirectory, RecursiveHardLinkMakerTest.class.getSimpleName());
-
-    private static final File outputDir = new File(workingDirectory, "output");
-
-    @BeforeClass
-    public void init()
-    {
-        LogInitializer.init();
-        unitTestRootDirectory.mkdirs();
-        assert unitTestRootDirectory.isDirectory();
-    }
-
-    @BeforeMethod
-    public void setUp()
-    {
-        FileUtilities.deleteRecursively(workingDirectory);
-        outputDir.mkdirs();
-    }
-
-    @AfterClass
-    public void clean()
-    {
-        FileUtilities.deleteRecursively(outputDir);
-    }
-
-    private static File createFile(File directory, String name) throws IOException
-    {
-        final File file = new File(directory, name);
-        file.createNewFile();
-        assert file.isFile();
-        CollectionIO
-                .writeIterable(file, Arrays.asList("test line 1", "test line 2", "test line 3"));
-        file.deleteOnExit();
-        return file;
-    }
-
-    private static File createDirectory(File directory, String name) throws IOException
-    {
-        final File file = new File(directory, name);
-        file.mkdir();
-        assert file.isDirectory();
-        file.deleteOnExit();
-        return file;
-    }
-
-    private static File mkPath(File parent, String... subdirs)
-    {
-        File file = parent;
-        for (String subdir : subdirs)
-        {
-            file = new File(file, subdir);
-        }
-        return file;
-    }
-
-    private static void assertFileExists(File file)
-    {
-        assert file.isFile();
-    }
-
-    private static IImmutableCopier createHardLinkCopier()
+    @Override
+    protected IImmutableCopier createHardLinkCopier()
     {
         IImmutableCopier copier = RecursiveHardLinkMaker.tryCreate(HardLinkMaker.tryCreate());
         assert copier != null;
         return copier;
     }
 
-    // ------------------------------------- test 1
-
-    // creates following structure
-    // dir1
-    // ---dir1a
-    // ------file1a
-    // ---dir1b
-    // ------file1b
-    // dir2
-    // ---file4
-    // file2
-    // file3
-    private void createStructure(File inputDir) throws IOException
-    {
-        final File dir1 = createDirectory(inputDir, "dir1");
-        final File dir1a = createDirectory(dir1, "dir1a");
-        final File dir1b = createDirectory(dir1, "dir1b");
-        final File dir2 = createDirectory(inputDir, "dir2");
-        createFile(dir1a, "file1a");
-        createFile(dir1b, "file1b");
-        createFile(inputDir, "file2");
-        createFile(inputDir, "file3");
-        createFile(dir2, "file4");
-    }
-
-    @Test(groups =
-        { "requires_unix" })
-    public void testCopyWithHardLinks() throws IOException
-    {
-        File inputDir = createDirectory(workingDirectory, "resource-to-copy");
-        createStructure(inputDir);
-        assertTrue(createHardLinkCopier().copyImmutably(inputDir, outputDir, null).isOK());
-        File newInput = new File(outputDir, inputDir.getName());
-
-        assertStructureExists(newInput);
-        boolean deleted = FileUtilities.deleteRecursively(inputDir);
-        assert deleted;
-        assertStructureExists(newInput);
-    }
-
-    private static void assertStructureExists(File destinationDir)
-    {
-        assertFileExists(mkPath(destinationDir, "dir1", "dir1a", "file1a"));
-        assertFileExists(mkPath(destinationDir, "dir1", "dir1b", "file1b"));
-        assertFileExists(mkPath(destinationDir, "dir2", "file4"));
-        assertFileExists(mkPath(destinationDir, "file2"));
-        assertFileExists(mkPath(destinationDir, "file3"));
-    }
-
-    // ------------------------------------- test 2
-
-    @Test(groups =
-        { "requires_unix" })
-    public void testCopyFile() throws IOException
-    {
-        File src = createFile(workingDirectory, "fileXXX");
-        assertFileExists(src);
-
-        assertTrue(createHardLinkCopier().copyImmutably(src, outputDir, null).isOK());
-        File dest = new File(outputDir, src.getName());
-        assertFileExists(dest);
-
-        modifyDest(dest);
-        assertFilesIdentical(src, dest);
-    }
-
-    private static void assertFilesIdentical(File file1, File file2)
-    {
-        List<String> list1 = CollectionIO.readList(file1);
-        List<String> list2 = CollectionIO.readList(file2);
-        assertEquals(list1, list2);
-    }
-
-    private static void modifyDest(File file)
-    {
-        List<String> list = Arrays.asList("new line 1", "new line 2");
-        CollectionIO.writeIterable(file, list);
-    }
 }
diff --git a/common/sourceTest/java/ch/systemsx/cisd/common/filesystem/TestBigStructureCreator.java b/common/sourceTest/java/ch/systemsx/cisd/common/filesystem/TestBigStructureCreator.java
new file mode 100644
index 00000000000..0b54b70de66
--- /dev/null
+++ b/common/sourceTest/java/ch/systemsx/cisd/common/filesystem/TestBigStructureCreator.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2012 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.common.filesystem;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+
+import ch.systemsx.cisd.common.collections.CollectionIO;
+
+/**
+ * Create a large file/folder structure that will take some time to copy.
+ * 
+ * @author Chandrasekhar Ramakrishnan
+ */
+public class TestBigStructureCreator
+{
+    private final File root;
+
+    private final int[] numberOfFoldersPerLevel;
+
+    private final int[] numberOfFilesPerFolder;
+
+    /**
+     * Create a larger structure that will take a few seconds to copy.
+     */
+    public static File createBigStructure(File directory, String name) throws IOException
+    {
+        final File root = new File(directory, name);
+        TestBigStructureCreator creator = new TestBigStructureCreator(root);
+        return creator.createBigStructure();
+    }
+
+    public TestBigStructureCreator(File root)
+    {
+        this(root, new int[]
+            { 100, 2 }, new int[]
+            { 0, 0, 10 });
+    }
+
+    public TestBigStructureCreator(File root, int[] numberOfFoldersPerLevel,
+            int[] numberOfFilesPerFolder)
+    {
+        this.root = root;
+        this.root.mkdir();
+        assert numberOfFilesPerFolder.length == numberOfFoldersPerLevel.length + 1;
+        this.numberOfFoldersPerLevel = numberOfFoldersPerLevel;
+        this.numberOfFilesPerFolder = numberOfFilesPerFolder;
+    }
+
+    /**
+     * Create a structure.
+     */
+    public File createBigStructure() throws IOException
+    {
+        return createStructure(root, 0);
+    }
+
+    /**
+     * Delete the structure asynchronously.
+     */
+    public void deleteBigStructureAsync()
+    {
+        Runnable deleter = new Runnable()
+            {
+                @Override
+                public void run()
+                {
+                    try
+                    {
+                        Thread.sleep(100);
+                    } catch (InterruptedException e)
+                    {
+                    }
+                    System.out.println("Deleting source");
+                    FileUtilities.deleteRecursively(root);
+                }
+
+            };
+
+        Thread deleterThread = new Thread(deleter);
+        deleterThread.start();
+    }
+
+    /**
+     * Verify that the structure is complete.
+     */
+    public boolean verifyStructure()
+    {
+        return verifyStructure(root, 0);
+    }
+
+    private File createStructure(File localRoot, int depth) throws IOException
+    {
+        final int maxDepth = numberOfFoldersPerLevel.length;
+        for (int i = 0; i < numberOfFilesPerFolder[depth]; ++i)
+        {
+            File file = new File(localRoot, "File-" + i);
+            file.createNewFile();
+            CollectionIO.writeIterable(file,
+                    Arrays.asList("test line 1", "test line 2", "test line 3"));
+        }
+        if (maxDepth == depth)
+        {
+            return localRoot;
+        }
+
+        for (int i = 0; i < numberOfFoldersPerLevel[depth]; ++i)
+        {
+            File folder = new File(localRoot, "Folder-" + i);
+            folder.mkdir();
+            createStructure(folder, depth + 1);
+        }
+
+        return localRoot;
+    }
+
+    private boolean verifyStructure(File localRoot, int depth)
+    {
+        final int maxDepth = numberOfFoldersPerLevel.length;
+        for (int i = 0; i < numberOfFilesPerFolder[depth]; ++i)
+        {
+            File file = new File(localRoot, "File-" + i);
+            if (false == file.exists())
+            {
+                return false;
+            }
+        }
+        if (maxDepth == depth)
+        {
+            return true;
+        }
+
+        for (int i = 0; i < numberOfFoldersPerLevel[depth]; ++i)
+        {
+            File folder = new File(localRoot, "Folder-" + i);
+            if (false == folder.exists())
+            {
+                return false;
+            }
+            if (false == verifyStructure(folder, depth + 1))
+            {
+                return false;
+            }
+        }
+
+        return true;
+    }
+}
-- 
GitLab