From 4f6d27e84ba89999a878c06d8c7bf9cff1c237a9 Mon Sep 17 00:00:00 2001
From: Sebastian Wolf <65733509+phanlezz@users.noreply.github.com>
Date: Mon, 2 Dec 2024 12:08:58 +0100
Subject: [PATCH 1/5] feat(analysis): add core structure for large merge, WIP
(#3743)
---
README.md | 2 +-
.../codecharta/filter/mergefilter/FatMerge.kt | 55 +++
.../filter/mergefilter/MergeFilter.kt | 49 ++-
.../filter/mergefilter/mimo/Mimo.kt | 4 +-
.../filter/mergefilter/MergeFilterTest.kt | 66 +++-
.../duplicate/testProject.beta.cc.json | 323 ++++++++++++++++++
.../resources/fatMerge/testEdges1.cc.json | 128 +++++++
.../fatMerge/testProject.alpha.cc.json | 323 ++++++++++++++++++
8 files changed, 943 insertions(+), 7 deletions(-)
create mode 100644 analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/FatMerge.kt
create mode 100644 analysis/filter/MergeFilter/src/test/resources/fatMerge/duplicate/testProject.beta.cc.json
create mode 100644 analysis/filter/MergeFilter/src/test/resources/fatMerge/testEdges1.cc.json
create mode 100644 analysis/filter/MergeFilter/src/test/resources/fatMerge/testProject.alpha.cc.json
diff --git a/README.md b/README.md
index 17e5ac6e4b..e9e17e3821 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
Latest Release:
- Analysis 1.129.0 | Visualization 1.131.2
+ Analysis 1.129.0 | Visualization 1.131.2
[comment]: ##################################################################################
[comment]:
diff --git a/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/FatMerge.kt b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/FatMerge.kt
new file mode 100644
index 0000000000..98da63bf20
--- /dev/null
+++ b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/FatMerge.kt
@@ -0,0 +1,55 @@
+package de.maibornwolff.codecharta.filter.mergefilter
+
+import de.maibornwolff.codecharta.model.BlacklistItem
+import de.maibornwolff.codecharta.model.Edge
+import de.maibornwolff.codecharta.model.Node
+import de.maibornwolff.codecharta.model.NodeType
+import de.maibornwolff.codecharta.model.Project
+
+class FatMerge {
+ companion object {
+ fun packageProjectInto(project: Project, prefix: String): Project {
+ val modifiedProject = Project(
+ project.projectName,
+ moveNodesIntoFolder(project.rootNode, prefix),
+ project.apiVersion,
+ addFolderToEdgePaths(project.edges, prefix),
+ project.attributeTypes,
+ project.attributeDescriptors,
+ addFolderToBlackListPaths(project.blacklist, prefix)
+ )
+ return modifiedProject
+ }
+
+ private fun moveNodesIntoFolder(root: Node, folderName: String): List {
+ val newRoot = Node(
+ name = folderName,
+ type = NodeType.Folder,
+ children = root.children
+ )
+ val mutableRoot = root.toMutableNode()
+ mutableRoot.children = mutableSetOf(newRoot.toMutableNode())
+ return listOf(mutableRoot.toNode())
+ }
+
+ private fun addFolderToEdgePaths(edges: List, folderName: String): List {
+ edges.forEach {
+ it.fromNodeName = insertFolderIntoPath(it.fromNodeName, folderName)
+ it.toNodeName = insertFolderIntoPath(it.toNodeName, folderName)
+ }
+ return edges
+ }
+
+ private fun addFolderToBlackListPaths(blacklist: List, folderName: String): List {
+ blacklist.forEach {
+ it.path = insertFolderIntoPath(it.path, folderName)
+ }
+ return blacklist
+ }
+
+ private fun insertFolderIntoPath(path: String, folderName: String): String {
+ require(Regex("^/root/").matches(path))
+ return path.replace(Regex("^/root/"), "/root/$folderName/")
+ }
+ }
+}
diff --git a/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilter.kt b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilter.kt
index cded7f991a..abfaf7669a 100644
--- a/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilter.kt
+++ b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilter.kt
@@ -58,6 +58,12 @@ class MergeFilter(
)
private var levenshteinDistance = 3
+ @CommandLine.Option(
+ names = ["--fat"],
+ description = ["Merges input files into on file divided into project sub-folder as defined per prefix of each input file"]
+ )
+ private var fatMerge = false
+
override val name = NAME
override val description = DESCRIPTION
@@ -94,8 +100,11 @@ class MergeFilter(
if (mimo) {
processMimoMerge(sourceFiles, nodeMergerStrategy)
+ } else if (fatMerge) {
+ processFatMerge(sourceFiles, nodeMergerStrategy)
} else {
val projects = readInputFiles(sourceFiles)
+
if (!continueIfIncompatibleProjects(projects)) return null
val mergedProject = ProjectMerger(projects, nodeMergerStrategy).merge()
@@ -155,8 +164,9 @@ class MergeFilter(
return@forEach
}
- val projects = readInputFiles(confirmedFileList)
- if (projects.size <= 1) {
+ val projectsFileNamePairs = readInputFilesKeepFileNames(confirmedFileList)
+ val projects = projectsFileNamePairs.map { it.second }
+ if (projectsFileNamePairs.size <= 1) {
Logger.warn { "After deserializing there were one or less projects. Continue with next group" }
return@forEach
}
@@ -164,7 +174,7 @@ class MergeFilter(
if (!continueIfIncompatibleProjects(projects)) return@forEach
val mergedProject = ProjectMerger(projects, nodeMergerStrategy).merge()
- val outputFilePrefix = Mimo.retrieveGroupName(confirmedFileList)
+ val outputFilePrefix = Mimo.retrieveGroupName(projectsFileNamePairs.map { it.first })
ProjectSerializer.serializeToFileOrStream(mergedProject, "$outputFilePrefix.merge.cc.json", output, compress)
Logger.info {
"Merged files with prefix '$outputFilePrefix' into" +
@@ -173,6 +183,27 @@ class MergeFilter(
}
}
+ private fun processFatMerge(sourceFiles: List, nodeMergerStrategy: NodeMergerStrategy) {
+ val projectsFileNamePairs = readInputFilesKeepFileNames(sourceFiles)
+ val fileNameList = projectsFileNamePairs.map { it.first }
+
+ require(fileNameList.size > 1) {
+ Logger.warn { "One or less projects in input, merging aborted." }
+ }
+
+ require(fileNameList.groupingBy { it.substringBefore(".") }.eachCount().all {it.value == 1}) {
+ Logger.warn { "Make sure that the input prefixes across all input files are unique!" }
+ }
+
+ val packagedProjects : MutableList = mutableListOf()
+ projectsFileNamePairs.forEach {
+ packagedProjects.add(FatMerge.packageProjectInto(it.second, it.first.substringBefore(".")))
+ }
+
+ val mergedProject = ProjectMerger(packagedProjects, nodeMergerStrategy).merge()
+ ProjectSerializer.serializeToFileOrStream(mergedProject, outputFile, output, compress)
+ }
+
private fun readInputFiles(files: List): List {
return files.mapNotNull {
val input = it.inputStream()
@@ -184,4 +215,16 @@ class MergeFilter(
}
}
}
+
+ private fun readInputFilesKeepFileNames(files: List): List> {
+ return files.mapNotNull {
+ try {
+ Pair(it.name, ProjectDeserializer.deserializeProject(it.inputStream()))
+ } catch (e: Exception) {
+ Logger.warn { "${it.name} is not a valid project file and will be skipped." }
+ null
+ }
+
+ }
+ }
}
diff --git a/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/mimo/Mimo.kt b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/mimo/Mimo.kt
index bca25bb8c9..e263fc04da 100644
--- a/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/mimo/Mimo.kt
+++ b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/mimo/Mimo.kt
@@ -51,8 +51,8 @@ class Mimo {
}
}
- fun retrieveGroupName(files: List): String {
- val filePrefixes = files.map { it.name.substringBefore(".") }.toSet()
+ fun retrieveGroupName(files: List): String {
+ val filePrefixes = files.map { it.substringBefore(".") }.toSet()
if (filePrefixes.size == 1) return filePrefixes.first()
return ParserDialog.askForMimoPrefix(filePrefixes)
}
diff --git a/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt b/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt
index e5ca7fdad9..85eba25ac9 100644
--- a/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt
+++ b/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt
@@ -243,7 +243,7 @@ class MergeFilterTest {
@Nested
@DisplayName("MimoModeTests")
inner class MimoModeTest {
- val testFile1Path = "src/test/resources/test.json"
+ private val testFile1Path = "src/test/resources/test.json"
val testProjectPathA = "src/test/resources/mimoFileSelection/testProject.alpha.cc.json"
val testProjectPathB = "src/test/resources/mimoFileSelection/testProject.beta.cc.json"
val testProjectPathC = "src/test/resources/mimoFileSelection/testProjectX.notIncl.cc.json"
@@ -493,4 +493,68 @@ class MergeFilterTest {
outputFile.deleteOnExit()
}
}
+
+ @Nested
+ @DisplayName("FatMergeTests")
+ inner class FatMergeTest {
+ private val fatMergeTestFolder = "src/test/resources/fatMerge"
+ private val testFilePath1 = "$fatMergeTestFolder/testEdges1.cc.json"
+ private val testFilePath2 = "$fatMergeTestFolder/testProject.alpha.cc.json"
+ private val testFilePathDuplicate = "$fatMergeTestFolder/duplicate/testProject.beta.cc.json"
+
+ @Test
+ fun `should warn about invalid files during fat merge and abort`() {
+ System.setErr(PrintStream(errContent))
+ CommandLine(MergeFilter()).execute(
+ "src/test/resources/invalid.cc.json",
+ "--fat"
+ ).toString()
+ System.setErr(originalErr)
+
+ assertThat(errContent.toString()).contains("Input invalid files/folders for MergeFilter, stopping execution...")
+ }
+
+ @Test
+ fun `should exit when prefixes are not unique`() {
+ System.setErr(PrintStream(errContent))
+ CommandLine(MergeFilter()).execute(
+ testFilePath1, testFilePath2, testFilePathDuplicate,
+ "--fat"
+ ).toString()
+ System.setErr(originalErr)
+
+ assertThat(errContent.toString()).contains("Make sure that the input prefixes across all input files are unique!")
+ }
+
+ @Test
+ fun `should cancel on single file input`() {
+ System.setErr(PrintStream(errContent))
+ CommandLine(MergeFilter()).execute(
+ testFilePath1,
+ "--fat"
+ ).toString()
+ System.setErr(originalErr)
+
+ assertThat(errContent.toString()).contains("One or less projects in input, merging aborted.")
+ }
+
+ @Test
+ fun `should merge all projects into one file each packaged into a subfolder with input file's prefix`() {
+ System.setOut(PrintStream(outContent))
+ System.setErr(PrintStream(errContent))
+ CommandLine(MergeFilter()).execute(
+ testFilePath1, testFilePath2,
+ "--fat"
+ ).toString()
+ System.setErr(originalErr)
+ System.setOut(originalOut)
+ assertThat(errContent.toString()).contains("Input invalid files/folders for MergeFilter, stopping execution...")
+ assertThat(originalOut.toString()).contains("")
+ }
+
+ @Test
+ fun `should output into specified file`() {
+
+ }
+ }
}
diff --git a/analysis/filter/MergeFilter/src/test/resources/fatMerge/duplicate/testProject.beta.cc.json b/analysis/filter/MergeFilter/src/test/resources/fatMerge/duplicate/testProject.beta.cc.json
new file mode 100644
index 0000000000..e6dd62ac91
--- /dev/null
+++ b/analysis/filter/MergeFilter/src/test/resources/fatMerge/duplicate/testProject.beta.cc.json
@@ -0,0 +1,323 @@
+{
+ "projectName": "SourceMonitorImporter",
+ "apiVersion": "1.0",
+ "nodes": [
+ {
+ "name": "root",
+ "type": "Folder",
+ "children": [
+ {
+ "name": "src",
+ "type": "Folder",
+ "children": [
+ {
+ "name": "main",
+ "type": "Folder",
+ "children": [
+ {
+ "name": "java",
+ "type": "Folder",
+ "children": [
+ {
+ "name": "de",
+ "type": "Folder",
+ "children": [
+ {
+ "name": "mwea",
+ "type": "Folder",
+ "children": [
+ {
+ "name": "codecharta",
+ "type": "Folder",
+ "children": [
+ {
+ "name": "importer",
+ "type": "Folder",
+ "children": [
+ {
+ "name": "sourcemon",
+ "type": "Folder",
+ "children": [
+ {
+ "name": "SourceMonCsvConverter.java",
+ "type": "File",
+ "attributes": {
+ "AdditionalAttributeForTest": 0.0,
+ "Classes and Interfaces": 1.0,
+ "Statements at block level 9": 0.0,
+ "Maximum Block Depth": 4.0,
+ "Statements at block level 8": 0.0,
+ "Statements at block level 7": 0.0,
+ "Percent Lines with Comments": 0.0,
+ "Average Complexity*": 2.0,
+ "Statements at block level 6": 0.0,
+ "Statements at block level 5": 0.0,
+ "Statements at block level 4": 1.0,
+ "Statements at block level 3": 4.0,
+ "Line Number of Most Complex Method*": 81.0,
+ "Statements at block level 2": 30.0,
+ "Maximum Complexity*": 4.0,
+ "Statements at block level 1": 15.0,
+ "Statements at block level 0": 14.0,
+ "Line Number of Deepest Block": 85.0,
+ "Methods per Class": 6.0,
+ "Percent Branch Statements": 7.8,
+ "Average Statements per Method": 5.83,
+ "Statements": 64.0,
+ "Average Block Depth": 1.42,
+ "Method Call Statements": 38.0
+ }
+ },
+ {
+ "name": "SourcemonCsvImporter.java",
+ "type": "File",
+ "attributes": {
+ "Lines*": 25.0,
+ "Classes and Interfaces": 1.0,
+ "Statements at block level 9": 0.0,
+ "Maximum Block Depth": 4.0,
+ "Statements at block level 8": 0.0,
+ "Statements at block level 7": 0.0,
+ "Percent Lines with Comments": 0.0,
+ "Average Complexity*": 3.0,
+ "Statements at block level 6": 0.0,
+ "Statements at block level 5": 0.0,
+ "Statements at block level 4": 1.0,
+ "Statements at block level 3": 3.0,
+ "Line Number of Most Complex Method*": 18.0,
+ "Statements at block level 2": 7.0,
+ "Maximum Complexity*": 4.0,
+ "Statements at block level 1": 2.0,
+ "Statements at block level 0": 6.0,
+ "Line Number of Deepest Block": 24.0,
+ "Methods per Class": 2.0,
+ "Percent Branch Statements": 21.1,
+ "Average Statements per Method": 5.5,
+ "Statements": 19.0,
+ "Average Block Depth": 1.53,
+ "Method Call Statements": 9.0
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "json",
+ "type": "Folder",
+ "children": [
+ {
+ "name": "JsonAccessor.java",
+ "type": "File",
+ "attributes": {
+ "Lines*": 15.0,
+ "Classes and Interfaces": 1.0,
+ "Statements at block level 9": 0.0,
+ "Maximum Block Depth": 2.0,
+ "Statements at block level 8": 0.0,
+ "Statements at block level 7": 0.0,
+ "Percent Lines with Comments": 0.0,
+ "Average Complexity*": 1.0,
+ "Statements at block level 6": 0.0,
+ "Statements at block level 5": 0.0,
+ "Statements at block level 4": 0.0,
+ "Statements at block level 3": 0.0,
+ "Line Number of Most Complex Method*": 14.0,
+ "Statements at block level 2": 2.0,
+ "Maximum Complexity*": 1.0,
+ "Statements at block level 1": 1.0,
+ "Statements at block level 0": 10.0,
+ "Line Number of Deepest Block": 15.0,
+ "Methods per Class": 1.0,
+ "Percent Branch Statements": 0.0,
+ "Average Statements per Method": 2.0,
+ "Statements": 13.0,
+ "Average Block Depth": 0.38,
+ "Method Call Statements": 2.0
+ }
+ }
+ ]
+ },
+ {
+ "name": "model",
+ "type": "Folder",
+ "children": [
+ {
+ "name": "Node.java",
+ "type": "File",
+ "attributes": {
+ "Lines*": 34.0,
+ "Classes and Interfaces": 1.0,
+ "Statements at block level 9": 0.0,
+ "Maximum Block Depth": 2.0,
+ "Statements at block level 8": 0.0,
+ "Statements at block level 7": 0.0,
+ "Percent Lines with Comments": 0.0,
+ "Average Complexity*": 1.0,
+ "Statements at block level 6": 0.0,
+ "Statements at block level 5": 0.0,
+ "Statements at block level 4": 0.0,
+ "Statements at block level 3": 0.0,
+ "Line Number of Most Complex Method*": 11.0,
+ "Statements at block level 2": 10.0,
+ "Maximum Complexity*": 1.0,
+ "Statements at block level 1": 12.0,
+ "Statements at block level 0": 3.0,
+ "Line Number of Deepest Block": 12.0,
+ "Methods per Class": 8.0,
+ "Percent Branch Statements": 0.0,
+ "Average Statements per Method": 1.25,
+ "Statements": 25.0,
+ "Average Block Depth": 1.28,
+ "Method Call Statements": 2.0
+ }
+ },
+ {
+ "name": "NodeType.java",
+ "type": "File",
+ "attributes": {
+ "Lines*": 4.0,
+ "Classes and Interfaces": 1.0,
+ "Statements at block level 9": 0.0,
+ "Maximum Block Depth": 0.0,
+ "Statements at block level 8": 0.0,
+ "Statements at block level 7": 0.0,
+ "Percent Lines with Comments": 0.0,
+ "Average Complexity*": 0.0,
+ "Statements at block level 6": 0.0,
+ "Statements at block level 5": 0.0,
+ "Statements at block level 4": 0.0,
+ "Statements at block level 3": 0.0,
+ "Statements at block level 2": 0.0,
+ "Maximum Complexity*": 0.0,
+ "Statements at block level 1": 0.0,
+ "Statements at block level 0": 2.0,
+ "Line Number of Deepest Block": 1.0,
+ "Methods per Class": 0.0,
+ "Percent Branch Statements": 0.0,
+ "Average Statements per Method": 0.0,
+ "Statements": 2.0,
+ "Average Block Depth": 0.0,
+ "Method Call Statements": 0.0
+ }
+ },
+ {
+ "name": "Project.java",
+ "type": "File",
+ "attributes": {
+ "Lines*": 17.0,
+ "Classes and Interfaces": 1.0,
+ "Statements at block level 9": 0.0,
+ "Maximum Block Depth": 2.0,
+ "Statements at block level 8": 0.0,
+ "Statements at block level 7": 0.0,
+ "Percent Lines with Comments": 0.0,
+ "Average Complexity*": 1.0,
+ "Statements at block level 6": 0.0,
+ "Statements at block level 5": 0.0,
+ "Statements at block level 4": 0.0,
+ "Statements at block level 3": 0.0,
+ "Line Number of Most Complex Method*": 10.0,
+ "Statements at block level 2": 3.0,
+ "Maximum Complexity*": 1.0,
+ "Statements at block level 1": 6.0,
+ "Statements at block level 0": 4.0,
+ "Line Number of Deepest Block": 11.0,
+ "Methods per Class": 3.0,
+ "Percent Branch Statements": 0.0,
+ "Average Statements per Method": 1.0,
+ "Statements": 13.0,
+ "Average Block Depth": 0.92,
+ "Method Call Statements": 0.0
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "test",
+ "type": "Folder",
+ "children": [
+ {
+ "name": "java",
+ "type": "Folder",
+ "children": [
+ {
+ "name": "de",
+ "type": "Folder",
+ "children": [
+ {
+ "name": "mwea",
+ "type": "Folder",
+ "children": [
+ {
+ "name": "codecharta",
+ "type": "Folder",
+ "children": [
+ {
+ "name": "importer",
+ "type": "Folder",
+ "children": [
+ {
+ "name": "sourcemon",
+ "type": "Folder",
+ "children": [
+ {
+ "name": "SourceMonCsvConverterTest.java",
+ "type": "File",
+ "attributes": {
+ "Lines*": 18.0,
+ "Classes and Interfaces": 1.0,
+ "Statements at block level 9": 0.0,
+ "Maximum Block Depth": 2.0,
+ "Statements at block level 8": 0.0,
+ "Statements at block level 7": 0.0,
+ "Percent Lines with Comments": 0.0,
+ "Average Complexity*": 1.0,
+ "Statements at block level 6": 0.0,
+ "Statements at block level 5": 0.0,
+ "Statements at block level 4": 0.0,
+ "Statements at block level 3": 0.0,
+ "Line Number of Most Complex Method*": 13.0,
+ "Statements at block level 2": 4.0,
+ "Maximum Complexity*": 1.0,
+ "Statements at block level 1": 3.0,
+ "Statements at block level 0": 7.0,
+ "Line Number of Deepest Block": 14.0,
+ "Methods per Class": 2.0,
+ "Percent Branch Statements": 0.0,
+ "Average Statements per Method": 2.0,
+ "Statements": 14.0,
+ "Average Block Depth": 0.79,
+ "Method Call Statements": 5.0
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/analysis/filter/MergeFilter/src/test/resources/fatMerge/testEdges1.cc.json b/analysis/filter/MergeFilter/src/test/resources/fatMerge/testEdges1.cc.json
new file mode 100644
index 0000000000..8bd4746be1
--- /dev/null
+++ b/analysis/filter/MergeFilter/src/test/resources/fatMerge/testEdges1.cc.json
@@ -0,0 +1,128 @@
+{
+ "projectName": "Sample Project 1",
+ "apiVersion": "1.0",
+ "nodes": [
+ {
+ "name": "root",
+ "type": "Folder",
+ "attributes": {},
+ "children": [
+ {
+ "name": "file1",
+ "type": "File",
+ "attributes": {
+ "number_of_commits": 160.0,
+ "range_of_weeks_with_commits": 68.0,
+ "successive_weeks_with_commits": 10.0,
+ "weeks_with_commits": 51.0,
+ "highly_coupled_files": 0.0,
+ "median_coupled_files": 2.0,
+ "abs_coupled_churn": 33700.0,
+ "avg_code_churn": 3.0
+ },
+ "link": "",
+ "children": []
+ },
+ {
+ "name": "file4",
+ "type": "File",
+ "attributes": {
+ "number_of_authors": 22.0,
+ "number_of_commits": 130.0,
+ "range_of_weeks_with_commits": 18.0,
+ "successive_weeks_with_commits": 20.0,
+ "weeks_with_commits": 61.0,
+ "highly_coupled_files": 1.0,
+ "median_coupled_files": 4.0,
+ "abs_coupled_churn": 22000.0,
+ "avg_code_churn": 4.0
+ },
+ "link": "",
+ "children": []
+ },
+ {
+ "name": "visualization",
+ "type": "Folder",
+ "attributes": {},
+ "link": "",
+ "children": [
+ {
+ "name": "file2",
+ "type": "File",
+ "attributes": {
+ "number_of_authors": 12.0,
+ "number_of_commits": 120.0,
+ "range_of_weeks_with_commits": 30.0,
+ "successive_weeks_with_commits": 20.0,
+ "weeks_with_commits": 76.0,
+ "highly_coupled_files": 0.0,
+ "median_coupled_files": 2.0,
+ "abs_coupled_churn": 33700.0,
+ "avg_code_churn": 7.0
+ },
+ "link": "",
+ "children": []
+ },
+ {
+ "name": "file3",
+ "type": "File",
+ "attributes": {
+ "number_of_authors": 40.0,
+ "number_of_commits": 230.0,
+ "range_of_weeks_with_commits": 50.0,
+ "successive_weeks_with_commits": 80.0,
+ "weeks_with_commits": 10.0,
+ "highly_coupled_files": 5.0,
+ "median_coupled_files": 2.0,
+ "abs_coupled_churn": 44000.0,
+ "avg_code_churn": 4.0
+ },
+ "link": "",
+ "children": []
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "edges": [
+ {
+ "fromNodeName": "/root/file1",
+ "toNodeName": "/root/visualization/file2",
+ "attributes": {
+ "pairingRate": 90,
+ "avgCommits": 5,
+ "anotherMetric": 25
+ }
+ },
+ {
+ "fromNodeName": "/root/visualization/file2",
+ "toNodeName": "/root/visualization/file3",
+ "attributes": {
+ "pairingRate": 80,
+ "avgCommits": 3
+ }
+ }
+ ],
+ "attributeTypes": {
+ "nodes": {
+ "attributeA": "absolute",
+ "attributeB": "absolute",
+ "attributeZ": "absolute"
+ },
+ "edges": {
+ "attributeD": "absolute",
+ "attributeE": "relative"
+ }
+ },
+ "blacklist": [
+ {
+ "path": "/root/file1",
+ "type": "hide"
+ },
+ {
+ "path": "/root/visualization/file2",
+ "type": "exclude"
+ }
+ ]
+}
diff --git a/analysis/filter/MergeFilter/src/test/resources/fatMerge/testProject.alpha.cc.json b/analysis/filter/MergeFilter/src/test/resources/fatMerge/testProject.alpha.cc.json
new file mode 100644
index 0000000000..ad84fc1762
--- /dev/null
+++ b/analysis/filter/MergeFilter/src/test/resources/fatMerge/testProject.alpha.cc.json
@@ -0,0 +1,323 @@
+{
+ "projectName": "SourceMonitorImporter",
+ "apiVersion": "1.0",
+ "nodes": [
+ {
+ "name": "root",
+ "type": "Folder",
+ "children": [
+ {
+ "name": "src",
+ "type": "Folder",
+ "children": [
+ {
+ "name": "main",
+ "type": "Folder",
+ "children": [
+ {
+ "name": "java",
+ "type": "Folder",
+ "children": [
+ {
+ "name": "de",
+ "type": "Folder",
+ "children": [
+ {
+ "name": "mwea",
+ "type": "Folder",
+ "children": [
+ {
+ "name": "codecharta",
+ "type": "Folder",
+ "children": [
+ {
+ "name": "importer",
+ "type": "Folder",
+ "children": [
+ {
+ "name": "sourcemon",
+ "type": "Folder",
+ "children": [
+ {
+ "name": "SourceMonCsvConverter.java",
+ "type": "File",
+ "attributes": {
+ "Lines*": 80.0,
+ "Classes and Interfaces": 1.0,
+ "Statements at block level 9": 0.0,
+ "Maximum Block Depth": 4.0,
+ "Statements at block level 8": 0.0,
+ "Statements at block level 7": 0.0,
+ "Percent Lines with Comments": 0.0,
+ "Average Complexity*": 2.0,
+ "Statements at block level 6": 0.0,
+ "Statements at block level 5": 0.0,
+ "Statements at block level 4": 1.0,
+ "Statements at block level 3": 4.0,
+ "Line Number of Most Complex Method*": 81.0,
+ "Statements at block level 2": 30.0,
+ "Maximum Complexity*": 4.0,
+ "Statements at block level 1": 15.0,
+ "Statements at block level 0": 14.0,
+ "Line Number of Deepest Block": 85.0,
+ "Methods per Class": 6.0,
+ "Percent Branch Statements": 7.8,
+ "Average Statements per Method": 5.83,
+ "Statements": 64.0,
+ "Average Block Depth": 1.42,
+ "Method Call Statements": 38.0
+ }
+ },
+ {
+ "name": "SourcemonCsvImporter.java",
+ "type": "File",
+ "attributes": {
+ "Lines*": 25.0,
+ "Classes and Interfaces": 1.0,
+ "Statements at block level 9": 0.0,
+ "Maximum Block Depth": 4.0,
+ "Statements at block level 8": 0.0,
+ "Statements at block level 7": 0.0,
+ "Percent Lines with Comments": 0.0,
+ "Average Complexity*": 3.0,
+ "Statements at block level 6": 0.0,
+ "Statements at block level 5": 0.0,
+ "Statements at block level 4": 1.0,
+ "Statements at block level 3": 3.0,
+ "Line Number of Most Complex Method*": 18.0,
+ "Statements at block level 2": 7.0,
+ "Maximum Complexity*": 4.0,
+ "Statements at block level 1": 2.0,
+ "Statements at block level 0": 6.0,
+ "Line Number of Deepest Block": 24.0,
+ "Methods per Class": 2.0,
+ "Percent Branch Statements": 21.1,
+ "Average Statements per Method": 5.5,
+ "Statements": 19.0,
+ "Average Block Depth": 1.53,
+ "Method Call Statements": 9.0
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "json",
+ "type": "Folder",
+ "children": [
+ {
+ "name": "JsonAccessor.java",
+ "type": "File",
+ "attributes": {
+ "Lines*": 15.0,
+ "Classes and Interfaces": 1.0,
+ "Statements at block level 9": 0.0,
+ "Maximum Block Depth": 2.0,
+ "Statements at block level 8": 0.0,
+ "Statements at block level 7": 0.0,
+ "Percent Lines with Comments": 0.0,
+ "Average Complexity*": 1.0,
+ "Statements at block level 6": 0.0,
+ "Statements at block level 5": 0.0,
+ "Statements at block level 4": 0.0,
+ "Statements at block level 3": 0.0,
+ "Line Number of Most Complex Method*": 14.0,
+ "Statements at block level 2": 2.0,
+ "Maximum Complexity*": 1.0,
+ "Statements at block level 1": 1.0,
+ "Statements at block level 0": 10.0,
+ "Line Number of Deepest Block": 15.0,
+ "Methods per Class": 1.0,
+ "Percent Branch Statements": 0.0,
+ "Average Statements per Method": 2.0,
+ "Statements": 13.0,
+ "Average Block Depth": 0.38,
+ "Method Call Statements": 2.0
+ }
+ }
+ ]
+ },
+ {
+ "name": "model",
+ "type": "Folder",
+ "children": [
+ {
+ "name": "Node.java",
+ "type": "File",
+ "attributes": {
+ "Lines*": 34.0,
+ "Classes and Interfaces": 1.0,
+ "Statements at block level 9": 0.0,
+ "Maximum Block Depth": 2.0,
+ "Statements at block level 8": 0.0,
+ "Statements at block level 7": 0.0,
+ "Percent Lines with Comments": 0.0,
+ "Average Complexity*": 1.0,
+ "Statements at block level 6": 0.0,
+ "Statements at block level 5": 0.0,
+ "Statements at block level 4": 0.0,
+ "Statements at block level 3": 0.0,
+ "Line Number of Most Complex Method*": 11.0,
+ "Statements at block level 2": 10.0,
+ "Maximum Complexity*": 1.0,
+ "Statements at block level 1": 12.0,
+ "Statements at block level 0": 3.0,
+ "Line Number of Deepest Block": 12.0,
+ "Methods per Class": 8.0,
+ "Percent Branch Statements": 0.0,
+ "Average Statements per Method": 1.25,
+ "Statements": 25.0,
+ "Average Block Depth": 1.28,
+ "Method Call Statements": 2.0
+ }
+ },
+ {
+ "name": "NodeType.java",
+ "type": "File",
+ "attributes": {
+ "Lines*": 4.0,
+ "Classes and Interfaces": 1.0,
+ "Statements at block level 9": 0.0,
+ "Maximum Block Depth": 0.0,
+ "Statements at block level 8": 0.0,
+ "Statements at block level 7": 0.0,
+ "Percent Lines with Comments": 0.0,
+ "Average Complexity*": 0.0,
+ "Statements at block level 6": 0.0,
+ "Statements at block level 5": 0.0,
+ "Statements at block level 4": 0.0,
+ "Statements at block level 3": 0.0,
+ "Statements at block level 2": 0.0,
+ "Maximum Complexity*": 0.0,
+ "Statements at block level 1": 0.0,
+ "Statements at block level 0": 2.0,
+ "Line Number of Deepest Block": 1.0,
+ "Methods per Class": 0.0,
+ "Percent Branch Statements": 0.0,
+ "Average Statements per Method": 0.0,
+ "Statements": 2.0,
+ "Average Block Depth": 0.0,
+ "Method Call Statements": 0.0
+ }
+ },
+ {
+ "name": "Project.java",
+ "type": "File",
+ "attributes": {
+ "Lines*": 17.0,
+ "Classes and Interfaces": 1.0,
+ "Statements at block level 9": 0.0,
+ "Maximum Block Depth": 2.0,
+ "Statements at block level 8": 0.0,
+ "Statements at block level 7": 0.0,
+ "Percent Lines with Comments": 0.0,
+ "Average Complexity*": 1.0,
+ "Statements at block level 6": 0.0,
+ "Statements at block level 5": 0.0,
+ "Statements at block level 4": 0.0,
+ "Statements at block level 3": 0.0,
+ "Line Number of Most Complex Method*": 10.0,
+ "Statements at block level 2": 3.0,
+ "Maximum Complexity*": 1.0,
+ "Statements at block level 1": 6.0,
+ "Statements at block level 0": 4.0,
+ "Line Number of Deepest Block": 11.0,
+ "Methods per Class": 3.0,
+ "Percent Branch Statements": 0.0,
+ "Average Statements per Method": 1.0,
+ "Statements": 13.0,
+ "Average Block Depth": 0.92,
+ "Method Call Statements": 0.0
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "test",
+ "type": "Folder",
+ "children": [
+ {
+ "name": "java",
+ "type": "Folder",
+ "children": [
+ {
+ "name": "de",
+ "type": "Folder",
+ "children": [
+ {
+ "name": "mwea",
+ "type": "Folder",
+ "children": [
+ {
+ "name": "codecharta",
+ "type": "Folder",
+ "children": [
+ {
+ "name": "importer",
+ "type": "Folder",
+ "children": [
+ {
+ "name": "sourcemon",
+ "type": "Folder",
+ "children": [
+ {
+ "name": "SourceMonCsvConverterTest.java",
+ "type": "File",
+ "attributes": {
+ "Lines*": 18.0,
+ "Classes and Interfaces": 1.0,
+ "Statements at block level 9": 0.0,
+ "Maximum Block Depth": 2.0,
+ "Statements at block level 8": 0.0,
+ "Statements at block level 7": 0.0,
+ "Percent Lines with Comments": 0.0,
+ "Average Complexity*": 1.0,
+ "Statements at block level 6": 0.0,
+ "Statements at block level 5": 0.0,
+ "Statements at block level 4": 0.0,
+ "Statements at block level 3": 0.0,
+ "Line Number of Most Complex Method*": 13.0,
+ "Statements at block level 2": 4.0,
+ "Maximum Complexity*": 1.0,
+ "Statements at block level 1": 3.0,
+ "Statements at block level 0": 7.0,
+ "Line Number of Deepest Block": 14.0,
+ "Methods per Class": 2.0,
+ "Percent Branch Statements": 0.0,
+ "Average Statements per Method": 2.0,
+ "Statements": 14.0,
+ "Average Block Depth": 0.79,
+ "Method Call Statements": 5.0
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
From d8155c599fca0f1c66eba5e32288c9d15cbbaa59 Mon Sep 17 00:00:00 2001
From: Sebastian Wolf <65733509+phanlezz@users.noreply.github.com>
Date: Tue, 3 Dec 2024 09:28:08 +0100
Subject: [PATCH 2/5] feat(analysis): apply new name (#3743)
---
.../mergefilter/{FatMerge.kt => LargeMerge.kt} | 2 +-
.../filter/mergefilter/MergeFilter.kt | 17 ++++++++---------
.../filter/mergefilter/MergeFilterTest.kt | 18 ++++++++++--------
.../duplicate/testProject.beta.cc.json | 0
.../testEdges1.cc.json | 0
.../testProject.alpha.cc.json | 0
6 files changed, 19 insertions(+), 18 deletions(-)
rename analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/{FatMerge.kt => LargeMerge.kt} (99%)
rename analysis/filter/MergeFilter/src/test/resources/{fatMerge => largeMerge}/duplicate/testProject.beta.cc.json (100%)
rename analysis/filter/MergeFilter/src/test/resources/{fatMerge => largeMerge}/testEdges1.cc.json (100%)
rename analysis/filter/MergeFilter/src/test/resources/{fatMerge => largeMerge}/testProject.alpha.cc.json (100%)
diff --git a/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/FatMerge.kt b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/LargeMerge.kt
similarity index 99%
rename from analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/FatMerge.kt
rename to analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/LargeMerge.kt
index 98da63bf20..a956f74720 100644
--- a/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/FatMerge.kt
+++ b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/LargeMerge.kt
@@ -6,7 +6,7 @@ import de.maibornwolff.codecharta.model.Node
import de.maibornwolff.codecharta.model.NodeType
import de.maibornwolff.codecharta.model.Project
-class FatMerge {
+class LargeMerge {
companion object {
fun packageProjectInto(project: Project, prefix: String): Project {
val modifiedProject = Project(
diff --git a/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilter.kt b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilter.kt
index abfaf7669a..b2a09ed87c 100644
--- a/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilter.kt
+++ b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilter.kt
@@ -59,10 +59,10 @@ class MergeFilter(
private var levenshteinDistance = 3
@CommandLine.Option(
- names = ["--fat"],
+ names = ["--large"],
description = ["Merges input files into on file divided into project sub-folder as defined per prefix of each input file"]
)
- private var fatMerge = false
+ private var largeMerge = false
override val name = NAME
override val description = DESCRIPTION
@@ -100,8 +100,8 @@ class MergeFilter(
if (mimo) {
processMimoMerge(sourceFiles, nodeMergerStrategy)
- } else if (fatMerge) {
- processFatMerge(sourceFiles, nodeMergerStrategy)
+ } else if (largeMerge) {
+ processLargeMerge(sourceFiles, nodeMergerStrategy)
} else {
val projects = readInputFiles(sourceFiles)
@@ -183,7 +183,7 @@ class MergeFilter(
}
}
- private fun processFatMerge(sourceFiles: List, nodeMergerStrategy: NodeMergerStrategy) {
+ private fun processLargeMerge(sourceFiles: List, nodeMergerStrategy: NodeMergerStrategy) {
val projectsFileNamePairs = readInputFilesKeepFileNames(sourceFiles)
val fileNameList = projectsFileNamePairs.map { it.first }
@@ -191,13 +191,13 @@ class MergeFilter(
Logger.warn { "One or less projects in input, merging aborted." }
}
- require(fileNameList.groupingBy { it.substringBefore(".") }.eachCount().all {it.value == 1}) {
+ require(fileNameList.groupingBy { it.substringBefore(".") }.eachCount().all { it.value == 1 }) {
Logger.warn { "Make sure that the input prefixes across all input files are unique!" }
}
- val packagedProjects : MutableList = mutableListOf()
+ val packagedProjects: MutableList = mutableListOf()
projectsFileNamePairs.forEach {
- packagedProjects.add(FatMerge.packageProjectInto(it.second, it.first.substringBefore(".")))
+ packagedProjects.add(LargeMerge.packageProjectInto(it.second, it.first.substringBefore(".")))
}
val mergedProject = ProjectMerger(packagedProjects, nodeMergerStrategy).merge()
@@ -224,7 +224,6 @@ class MergeFilter(
Logger.warn { "${it.name} is not a valid project file and will be skipped." }
null
}
-
}
}
}
diff --git a/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt b/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt
index 85eba25ac9..ba594ca4cb 100644
--- a/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt
+++ b/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt
@@ -497,7 +497,7 @@ class MergeFilterTest {
@Nested
@DisplayName("FatMergeTests")
inner class FatMergeTest {
- private val fatMergeTestFolder = "src/test/resources/fatMerge"
+ private val fatMergeTestFolder = "src/test/resources/largeMerge"
private val testFilePath1 = "$fatMergeTestFolder/testEdges1.cc.json"
private val testFilePath2 = "$fatMergeTestFolder/testProject.alpha.cc.json"
private val testFilePathDuplicate = "$fatMergeTestFolder/duplicate/testProject.beta.cc.json"
@@ -507,7 +507,7 @@ class MergeFilterTest {
System.setErr(PrintStream(errContent))
CommandLine(MergeFilter()).execute(
"src/test/resources/invalid.cc.json",
- "--fat"
+ "--large"
).toString()
System.setErr(originalErr)
@@ -518,8 +518,10 @@ class MergeFilterTest {
fun `should exit when prefixes are not unique`() {
System.setErr(PrintStream(errContent))
CommandLine(MergeFilter()).execute(
- testFilePath1, testFilePath2, testFilePathDuplicate,
- "--fat"
+ testFilePath1,
+ testFilePath2,
+ testFilePathDuplicate,
+ "--large"
).toString()
System.setErr(originalErr)
@@ -531,7 +533,7 @@ class MergeFilterTest {
System.setErr(PrintStream(errContent))
CommandLine(MergeFilter()).execute(
testFilePath1,
- "--fat"
+ "--large"
).toString()
System.setErr(originalErr)
@@ -543,8 +545,9 @@ class MergeFilterTest {
System.setOut(PrintStream(outContent))
System.setErr(PrintStream(errContent))
CommandLine(MergeFilter()).execute(
- testFilePath1, testFilePath2,
- "--fat"
+ testFilePath1,
+ testFilePath2,
+ "--large"
).toString()
System.setErr(originalErr)
System.setOut(originalOut)
@@ -554,7 +557,6 @@ class MergeFilterTest {
@Test
fun `should output into specified file`() {
-
}
}
}
diff --git a/analysis/filter/MergeFilter/src/test/resources/fatMerge/duplicate/testProject.beta.cc.json b/analysis/filter/MergeFilter/src/test/resources/largeMerge/duplicate/testProject.beta.cc.json
similarity index 100%
rename from analysis/filter/MergeFilter/src/test/resources/fatMerge/duplicate/testProject.beta.cc.json
rename to analysis/filter/MergeFilter/src/test/resources/largeMerge/duplicate/testProject.beta.cc.json
diff --git a/analysis/filter/MergeFilter/src/test/resources/fatMerge/testEdges1.cc.json b/analysis/filter/MergeFilter/src/test/resources/largeMerge/testEdges1.cc.json
similarity index 100%
rename from analysis/filter/MergeFilter/src/test/resources/fatMerge/testEdges1.cc.json
rename to analysis/filter/MergeFilter/src/test/resources/largeMerge/testEdges1.cc.json
diff --git a/analysis/filter/MergeFilter/src/test/resources/fatMerge/testProject.alpha.cc.json b/analysis/filter/MergeFilter/src/test/resources/largeMerge/testProject.alpha.cc.json
similarity index 100%
rename from analysis/filter/MergeFilter/src/test/resources/fatMerge/testProject.alpha.cc.json
rename to analysis/filter/MergeFilter/src/test/resources/largeMerge/testProject.alpha.cc.json
From 8ed1597ccdc73d9d62b2a218e747ef80f8859655 Mon Sep 17 00:00:00 2001
From: Sebastian Wolf <65733509+phanlezz@users.noreply.github.com>
Date: Tue, 3 Dec 2024 12:02:54 +0100
Subject: [PATCH 3/5] feat(analysis): add test to verify output in file (#3743)
---
.../filter/mergefilter/LargeMerge.kt | 1 -
.../filter/mergefilter/MergeFilterTest.kt | 33 ++++++++++++++++---
2 files changed, 29 insertions(+), 5 deletions(-)
diff --git a/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/LargeMerge.kt b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/LargeMerge.kt
index a956f74720..49442485d7 100644
--- a/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/LargeMerge.kt
+++ b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/LargeMerge.kt
@@ -48,7 +48,6 @@ class LargeMerge {
}
private fun insertFolderIntoPath(path: String, folderName: String): String {
- require(Regex("^/root/").matches(path))
return path.replace(Regex("^/root/"), "/root/$folderName/")
}
}
diff --git a/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt b/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt
index ba594ca4cb..0a641e021f 100644
--- a/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt
+++ b/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt
@@ -5,6 +5,7 @@ import com.github.kinquirer.components.promptCheckboxObject
import com.github.kinquirer.components.promptList
import com.github.kinquirer.core.Choice
import de.maibornwolff.codecharta.filter.mergefilter.MergeFilter.Companion.main
+import de.maibornwolff.codecharta.serialization.ProjectDeserializer
import de.maibornwolff.codecharta.tools.interactiveparser.InputType
import de.maibornwolff.codecharta.util.InputHelper
import io.mockk.every
@@ -543,20 +544,44 @@ class MergeFilterTest {
@Test
fun `should merge all projects into one file each packaged into a subfolder with input file's prefix`() {
System.setOut(PrintStream(outContent))
- System.setErr(PrintStream(errContent))
CommandLine(MergeFilter()).execute(
testFilePath1,
testFilePath2,
"--large"
).toString()
- System.setErr(originalErr)
System.setOut(originalOut)
- assertThat(errContent.toString()).contains("Input invalid files/folders for MergeFilter, stopping execution...")
- assertThat(originalOut.toString()).contains("")
+ val outputString = outContent.toString()
+ assertThat(outputString).contains("testProject", "testEdges1")
+ assertThat(outputString).contains("SourceMonCsvConverter", "number_of_commits")
+ assertThat(
+ outputString
+ ).contains("/root/testEdges1/visualization/file2", "/root/testEdges1/visualization/file3", "/root/testEdges1/file1")
}
@Test
fun `should output into specified file`() {
+ val outPutFilePath = "$fatMergeTestFolder/largeOutputToFile.cc.json"
+ CommandLine(MergeFilter()).execute(
+ testFilePath1,
+ testFilePath2,
+ "--large",
+ "-o=$outPutFilePath",
+ "-nc"
+ ).toString()
+ val outPutFile = File(outPutFilePath)
+ assertThat(outPutFile).exists()
+ val project = ProjectDeserializer.deserializeProject(outPutFile.inputStream())
+ val projectInput1 = ProjectDeserializer.deserializeProject(File(testFilePath1).inputStream())
+ val projectInput2 = ProjectDeserializer.deserializeProject(File(testFilePath2).inputStream())
+ assertThat(project.sizeOfEdges()).isEqualTo(2)
+ assertThat(project.sizeOfBlacklist()).isEqualTo(2)
+ assertThat(project.edges.toString()).contains("/root/testEdges1/visualization/file2", "/root/testEdges1/visualization/file3")
+ assertThat(project.rootNode.children.size).isEqualTo(2)
+ val outputProject1 = project.rootNode.children.first { it.name == "testEdges1" }
+ val outputProject2 = project.rootNode.children.first { it.name == "testProject" }
+ assertThat(outputProject1.children.toString()).isEqualTo(projectInput1.rootNode.children.toString())
+ assertThat(outputProject2.children.toString()).isEqualTo(projectInput2.rootNode.children.toString())
+ outPutFile.deleteOnExit()
}
}
}
From 53abd5d79e2e9eca4d59760efb2668240622ca54 Mon Sep 17 00:00:00 2001
From: Sebastian Wolf <65733509+phanlezz@users.noreply.github.com>
Date: Tue, 3 Dec 2024 14:27:47 +0100
Subject: [PATCH 4/5] feat(analysis): add dialog option for large and -o for
mimo (#3743)
Also updates the MergeFilter README (copied into gh-pages), updates tests for dialog and adds test for output file during mimo
---
analysis/CHANGELOG.md | 9 ++
analysis/filter/MergeFilter/README.md | 45 ++++++----
.../filter/mergefilter/MergeFilter.kt | 6 +-
.../filter/mergefilter/ParserDialog.kt | 42 ++++++---
.../filter/mergefilter/mimo/Mimo.kt | 10 +++
.../filter/mergefilter/MergeFilterTest.kt | 47 +++++++++-
.../filter/mergefilter/ParserDialogTest.kt | 86 ++++++++++++++++---
gh-pages/_docs/07-filter/02-merge-filter.md | 45 ++++++----
8 files changed, 228 insertions(+), 62 deletions(-)
diff --git a/analysis/CHANGELOG.md b/analysis/CHANGELOG.md
index 37b0517958..0a9d529893 100644
--- a/analysis/CHANGELOG.md
+++ b/analysis/CHANGELOG.md
@@ -7,8 +7,17 @@ and this project adheres to [Semantic Versioning](http://semver.org/)
## [unreleased] (Added 🚀 | Changed | Removed 🗑 | Fixed 🐞 | Chore 👨💻 👩💻)
+### Added 🚀
+
+- Add a new `--large` flat to the MergeFilter that merges projects into one file each in its own subfolder depending on the input file's dot-prefix name [#3841](https://github.com/MaibornWolff/codecharta/pull/3841)
+- Add the ability to the MergeFilter to specify the output file during `--mimo` operation [#3841](https://github.com/MaibornWolff/codecharta/pull/3841)
+
## [1.129.0] - 2024-11-29
+### Added 🚀
+
+- Add a new `--mimo` flag to the MergeFilter that merges multiple project files into multiple output files depending on the input file's dot-prefix name [#3800](https://github.com/MaibornWolff/codecharta/pull/3800)
+
### Changed
- Moved the structure print functionality to a new tool called 'inspection' [#3826](https://github.com/MaibornWolff/codecharta/pull/3826)
diff --git a/analysis/filter/MergeFilter/README.md b/analysis/filter/MergeFilter/README.md
index 82c8980b3e..acc6680f15 100644
--- a/analysis/filter/MergeFilter/README.md
+++ b/analysis/filter/MergeFilter/README.md
@@ -14,19 +14,20 @@ Both strategies will merge the unique list entries for `attributeTypes` and `bla
## Usage and Parameters
-| Parameters | Description |
-|---------------------------------|----------------------------------------------------------------------|
-| `FILE` | files to merge |
-| `-a, --add-missing` | [Leaf Merging Strategy] enable adding missing nodes to reference |
-| `-h, --help` | displays help and exits |
-| `--ignore-case` | ignores case when checking node names |
-| `--leaf` | use leaf merging strategy |
-| `-nc, --not-compressed` | save uncompressed output File |
-| `-o, --outputFile=` | output File (or empty for stdout; ignored in [MIMO mode])) |
-| `--recursive` | use recursive merging strategy (default) |
-| `--mimo` | merge multiple files with the same prefix into multiple output files |
-| `-ld, --levenshtein-distance` | [MIMO mode] levenshtein distance for name match suggestions |
-| `-f` | force merge non-overlapping modules at the top-level structure |
+| Parameters | Description |
+|---------------------------------|----------------------------------------------------------------------------------|
+| `FILE` | files to merge |
+| `-a, --add-missing` | [Leaf Merging Strategy] enable adding missing nodes to reference |
+| `-h, --help` | displays help and exits |
+| `--ignore-case` | ignores case when checking node names |
+| `--leaf` | use leaf merging strategy |
+| `-nc, --not-compressed` | save uncompressed output File |
+| `-o, --outputFile=` | output File (or empty for stdout; [MIMO mode] output folder)) |
+| `--recursive` | use recursive merging strategy (default) |
+| `--mimo` | merge multiple files with the same prefix into multiple output files |
+| `-ld, --levenshtein-distance` | [MIMO mode] levenshtein distance for name match suggestions |
+| `-f` | force merge non-overlapping modules at the top-level structure |
+| `--large` | merge multiple project files into one output file, separated by their dot-prefix |
```
Usage: ccsh merge [-ah] [--ignore-case] [--leaf] [-nc] [--recursive]
@@ -53,10 +54,24 @@ This last example inputs the folder foo, which will result in all project files
ccsh merge myProjectFolder/ --mimo -ld 0 -f
```
-## MIMO - Multiply Inputs Multiple Outputs
+## MIMO Merge - Multiply Inputs Multiple Outputs
Matches multiple `cc.json` files based on their prefix (e.g. **myProject**.git.cc.json). Tries to match project names with typos and asks which to add to the output.
If you want to use this in a CI/CD pipeline environment you may find it useful to specify `-ld` and `-f` to not prompt any user input.
The output file name follows the following schema: `myProject.merge.cc.json`.
-> IMPORTANT: Output is always the current working directory.
+## Large Merge
+
+Merges multiple `.cc.json` files into one projects, but separates them into sub-folders, with names defined through the dot-prefixes of the input files:
+
+```
+ccsh merge aa.cc.json bb.cc.json cc.cc.json --large -o myOutputFile -nc
+# myOutputFile.cc.json:
+# - root
+# - - aa
+# - - - *
+# - - bb
+# - - - *
+# - - cc
+# - - - *
+```
diff --git a/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilter.kt b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilter.kt
index b2a09ed87c..6d5fb8c9f2 100644
--- a/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilter.kt
+++ b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilter.kt
@@ -175,10 +175,12 @@ class MergeFilter(
val mergedProject = ProjectMerger(projects, nodeMergerStrategy).merge()
val outputFilePrefix = Mimo.retrieveGroupName(projectsFileNamePairs.map { it.first })
- ProjectSerializer.serializeToFileOrStream(mergedProject, "$outputFilePrefix.merge.cc.json", output, compress)
+ val outputFileName = "$outputFilePrefix.merge.cc.json"
+ val outputFilePath = Mimo.assembleOutputFilePath(outputFile, outputFileName)
+ ProjectSerializer.serializeToFileOrStream(mergedProject, outputFilePath, output, compress)
Logger.info {
"Merged files with prefix '$outputFilePrefix' into" +
- " '$outputFilePrefix.merge.cc.json${if (compress) ".gz" else ""}'"
+ " '$outputFileName${if (compress) ".gz" else ""}'"
}
}
}
diff --git a/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/ParserDialog.kt b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/ParserDialog.kt
index 36db9fd2f1..d77aee4b1b 100644
--- a/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/ParserDialog.kt
+++ b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/ParserDialog.kt
@@ -20,24 +20,32 @@ class ParserDialog {
inputFolderName = getInputFileName("cc.json", InputType.FOLDER)
} while (!InputHelper.isInputValidAndNotNull(arrayOf(File(inputFolderName)), canInputContainFolders = true))
- val isMimoMode = KInquirer.promptConfirm(
- message = "Do you want to use MIMO mode? (multiple inputs multiple outputs)",
- default = false
+ val defaultMerge = "Default merging..."
+ val mimoMerge = "Mimo Merge"
+ val largeMerge = "Large Merge"
+ val mergeMode = KInquirer.promptList(
+ message = "Do you want to use a special merge mode?",
+ choices = listOf(defaultMerge, mimoMerge, largeMerge)
)
- var outputFileName = ""
+ val outputFileName: String
val isCompressed: Boolean
var levenshteinDistance = 0
- if (isMimoMode) {
- levenshteinDistance = KInquirer.promptInputNumber(
- message = "Select Levenshtein Distance for name match suggestions (0 for no suggestions)",
- default = "3"
- ).toInt()
+ if (mergeMode == mimoMerge) {
+ outputFileName = KInquirer.promptInput(
+ message = "What is the output folder path?",
+ hint = "Uses the current working directory if empty"
+ )
isCompressed = KInquirer.promptConfirm(
message = "Do you want to compress the output file(s)?",
default = true
)
+
+ levenshteinDistance = KInquirer.promptInputNumber(
+ message = "Select Levenshtein Distance for name match suggestions (0 for no suggestions)",
+ default = "3"
+ ).toInt()
} else {
outputFileName =
KInquirer.promptInput(
@@ -82,18 +90,24 @@ class ParserDialog {
"--recursive=${!leafFlag}",
"--leaf=$leafFlag",
"--ignore-case=$ignoreCase",
- "--not-compressed=$isCompressed"
+ "--not-compressed=$isCompressed",
+ "--output-file=$outputFileName"
)
- if (isMimoMode) {
+ if (mergeMode == mimoMerge) {
return basicMergeConfig + listOf(
"--mimo=true",
"--levenshtein-distance=$levenshteinDistance"
)
}
- return basicMergeConfig + listOf(
- "--output-file=$outputFileName"
- )
+
+ if (mergeMode == largeMerge) {
+ return basicMergeConfig + listOf(
+ "--large=true"
+ )
+ }
+
+ return basicMergeConfig
}
fun askForceMerge(): Boolean {
diff --git a/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/mimo/Mimo.kt b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/mimo/Mimo.kt
index e263fc04da..4fa3374654 100644
--- a/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/mimo/Mimo.kt
+++ b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/mimo/Mimo.kt
@@ -83,5 +83,15 @@ class Mimo {
return cost[rhsLength]
}
+
+ fun assembleOutputFilePath(filePath: String?, fileName: String): String {
+ return if (filePath.isNullOrEmpty()) {
+ fileName
+ } else if (File(filePath).isDirectory) {
+ "${File(filePath).path}/$fileName"
+ } else {
+ throw IllegalArgumentException("Please specify a folder for MIMO output or nothing")
+ }
+ }
}
}
diff --git a/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt b/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt
index 0a641e021f..99159cc39b 100644
--- a/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt
+++ b/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt
@@ -493,11 +493,54 @@ class MergeFilterTest {
outputFile.deleteOnExit()
}
+
+ @Test
+ fun `should merge two projects with output at the given location`() {
+ val prefix = "testProject"
+ val outputFolder = "src/test/resources"
+
+ System.setErr(PrintStream(errContent))
+ CommandLine(MergeFilter()).execute(
+ testProjectPathA,
+ testProjectPathB,
+ "--mimo",
+ "--levenshtein-distance=0",
+ "-o=$outputFolder"
+ ).toString()
+
+ System.setErr(originalErr)
+
+ val outputFileName = "$prefix.merge.cc.json.gz"
+ val outputFile = File("$outputFolder/$outputFileName")
+
+ assertThat(errContent.toString()).contains("Merged files with prefix '$prefix' into '$outputFileName'")
+ assertThat(outputFile).exists()
+
+ outputFile.deleteOnExit()
+ }
+
+ @Test
+ fun `should throw error if output-file is not a folder`() {
+ val invalidOutputPath = "src/test/resources/invalid.cc.json"
+
+ System.setErr(PrintStream(errContent))
+ CommandLine(MergeFilter()).execute(
+ testProjectPathA,
+ testProjectPathB,
+ "--mimo",
+ "--levenshtein-distance=0",
+ "-o=$invalidOutputPath"
+ ).toString()
+
+ System.setErr(originalErr)
+
+ assertThat(errContent.toString()).contains("Please specify a folder for MIMO output or nothing")
+ }
}
@Nested
- @DisplayName("FatMergeTests")
- inner class FatMergeTest {
+ @DisplayName("LargeMergeTests")
+ inner class LargeMergeTest {
private val fatMergeTestFolder = "src/test/resources/largeMerge"
private val testFilePath1 = "$fatMergeTestFolder/testEdges1.cc.json"
private val testFilePath2 = "$fatMergeTestFolder/testProject.alpha.cc.json"
diff --git a/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/ParserDialogTest.kt b/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/ParserDialogTest.kt
index 209056a447..3e383871a1 100644
--- a/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/ParserDialogTest.kt
+++ b/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/ParserDialogTest.kt
@@ -26,14 +26,15 @@ class ParserDialogTest {
}
@Test
- fun `should output correct arguments and skip questions (leaf, no mimo)`() {
+ fun `should output correct arguments and skip questions (leaf, no mimo or large)`() {
// given
val inputFolderName = "folder"
val outputFileName = "sampleOutputFile"
val compress = false
val addMissing = true
val ignoreCase = false
- val mimo = false
+
+ val mergeMode = "Default merging..."
// set indirectly
val recursive = false
@@ -51,11 +52,11 @@ class ParserDialogTest {
mockkStatic("com.github.kinquirer.components.ConfirmKt")
every {
KInquirer.promptConfirm(any(), any())
- } returns mimo andThen compress andThen addMissing andThen ignoreCase
+ } returns compress andThen addMissing andThen ignoreCase
mockkStatic("com.github.kinquirer.components.ListKt")
every {
KInquirer.promptList(any(), any(), any(), any(), any())
- } returns "Leaf Merging Strategy"
+ } returns mergeMode andThen "Leaf Merging Strategy"
// when
val parserArguments = ParserDialog.collectParserArgs()
@@ -77,14 +78,15 @@ class ParserDialogTest {
}
@Test
- fun `should output correct arguments and skip questions (recursive, no mimo)`() {
+ fun `should output correct arguments and skip questions (recursive, no mimo or large)`() {
// given
val inputFolderName = "folder"
val outputFileName = "sampleOutputFile"
val compress = true
val addMissing = false
val ignoreCase = false
- val mimo = false
+
+ val mergeMode = "Default merging..."
// set indirectly
val recursive = true
@@ -102,11 +104,11 @@ class ParserDialogTest {
mockkStatic("com.github.kinquirer.components.ConfirmKt")
every {
KInquirer.promptConfirm(any(), any())
- } returns mimo andThen compress andThen ignoreCase
+ } returns compress andThen ignoreCase
mockkStatic("com.github.kinquirer.components.ListKt")
every {
KInquirer.promptList(any(), any(), any(), any(), any())
- } returns "Recursive Merging Strategy"
+ } returns mergeMode andThen "Recursive Merging Strategy"
// when
val parserArguments = ParserDialog.collectParserArgs()
@@ -128,15 +130,16 @@ class ParserDialogTest {
}
@Test
- fun `should output correct arguments when mimo`() {
+ fun `should output correct arguments when using Mimo Merge`() {
// given
val inputFolderName = "folder"
val addMissing = false
val ignoreCase = true
- val mimo = true
val levenshteinDistance = BigDecimal(3)
val compress = true
+ val mergeMode = "Mimo Merge"
+
// set indirectly
val recursive = true
val leaf = false
@@ -149,7 +152,7 @@ class ParserDialogTest {
mockkStatic("com.github.kinquirer.components.InputKt")
every {
KInquirer.promptInput(any(), any(), any())
- } returns inputFolderName
+ } returns inputFolderName andThen ""
every {
KInquirer.promptInputNumber(any(), any(), any(), any())
} returns levenshteinDistance
@@ -157,11 +160,11 @@ class ParserDialogTest {
mockkStatic("com.github.kinquirer.components.ConfirmKt")
every {
KInquirer.promptConfirm(any(), any())
- } returns mimo andThen compress andThen ignoreCase
+ } returns compress andThen ignoreCase
mockkStatic("com.github.kinquirer.components.ListKt")
every {
KInquirer.promptList(any(), any(), any(), any(), any())
- } returns "Recursive Merging Strategy"
+ } returns mergeMode andThen "Recursive Merging Strategy"
// when
val parserArguments = ParserDialog.collectParserArgs()
@@ -174,15 +177,70 @@ class ParserDialogTest {
it.name
}
).isEqualTo(listOf(inputFolderName))
+ Assertions.assertThat(parseResult.matchedOption("output-file").getValue()).isEqualTo("")
Assertions.assertThat(parseResult.matchedOption("not-compressed").getValue()).isEqualTo(compress)
Assertions.assertThat(parseResult.matchedOption("add-missing").getValue()).isEqualTo(addMissing)
Assertions.assertThat(parseResult.matchedOption("recursive").getValue()).isEqualTo(recursive)
Assertions.assertThat(parseResult.matchedOption("leaf").getValue()).isEqualTo(leaf)
Assertions.assertThat(parseResult.matchedOption("ignore-case").getValue()).isEqualTo(ignoreCase)
- Assertions.assertThat(parseResult.matchedOption("mimo").getValue()).isEqualTo(mimo)
+ Assertions.assertThat(parseResult.matchedOption("mimo").getValue()).isTrue()
Assertions.assertThat(parseResult.matchedOption("levenshtein-distance").getValue()).isEqualTo(levenshteinDistance.toInt())
}
+ @Test
+ fun `should output correct arguments when using Large Merge`() {
+ // given
+ val inputFolderName = "folder"
+ val outputFileName = "sampleOutputFile"
+ val addMissing = false
+ val ignoreCase = true
+ val compress = true
+
+ val mergeMode = "Large Merge"
+
+ // set indirectly
+ val recursive = true
+ val leaf = false
+
+ mockkObject(InputHelper)
+ every {
+ InputHelper.isInputValidAndNotNull(any(), any())
+ } returns true
+
+ mockkStatic("com.github.kinquirer.components.InputKt")
+ every {
+ KInquirer.promptInput(any(), any(), any())
+ } returns inputFolderName andThen outputFileName
+
+ mockkStatic("com.github.kinquirer.components.ConfirmKt")
+ every {
+ KInquirer.promptConfirm(any(), any())
+ } returns compress andThen ignoreCase
+ mockkStatic("com.github.kinquirer.components.ListKt")
+ every {
+ KInquirer.promptList(any(), any(), any(), any(), any())
+ } returns mergeMode andThen "Recursive Merging Strategy"
+
+ // when
+ val parserArguments = ParserDialog.collectParserArgs()
+ val commandLine = CommandLine(MergeFilter())
+ val parseResult = commandLine.parseArgs(*parserArguments.toTypedArray())
+
+ // then
+ Assertions.assertThat(
+ parseResult.matchedPositional(0).getValue>().map {
+ it.name
+ }
+ ).isEqualTo(listOf(inputFolderName))
+ Assertions.assertThat(parseResult.matchedOption("output-file").getValue()).isEqualTo(outputFileName)
+ Assertions.assertThat(parseResult.matchedOption("not-compressed").getValue()).isEqualTo(compress)
+ Assertions.assertThat(parseResult.matchedOption("add-missing").getValue()).isEqualTo(addMissing)
+ Assertions.assertThat(parseResult.matchedOption("recursive").getValue()).isEqualTo(recursive)
+ Assertions.assertThat(parseResult.matchedOption("leaf").getValue()).isEqualTo(leaf)
+ Assertions.assertThat(parseResult.matchedOption("ignore-case").getValue()).isEqualTo(ignoreCase)
+ Assertions.assertThat(parseResult.matchedOption("large").getValue()).isTrue()
+ }
+
@Test
fun `should prompt user twice for input file when first input file is invalid`() { // given
val invalidInputFolderName = ""
diff --git a/gh-pages/_docs/07-filter/02-merge-filter.md b/gh-pages/_docs/07-filter/02-merge-filter.md
index 6639c51c52..b7aa286aba 100644
--- a/gh-pages/_docs/07-filter/02-merge-filter.md
+++ b/gh-pages/_docs/07-filter/02-merge-filter.md
@@ -21,19 +21,20 @@ Both strategies will merge the unique list entries for `attributeTypes` and `bla
## Usage and Parameters
-| Parameters | Description |
-|---------------------------------|----------------------------------------------------------------------|
-| `FILE` | files to merge |
-| `-a, --add-missing` | [Leaf Merging Strategy] enable adding missing nodes to reference |
-| `-h, --help` | displays help and exits |
-| `--ignore-case` | ignores case when checking node names |
-| `--leaf` | use leaf merging strategy |
-| `-nc, --not-compressed` | save uncompressed output File |
-| `-o, --outputFile=` | output File (or empty for stdout; ignored in [MIMO mode])) |
-| `--recursive` | use recursive merging strategy (default) |
-| `--mimo` | merge multiple files with the same prefix into multiple output files |
-| `-ld, --levenshtein-distance` | [MIMO mode] levenshtein distance for name match suggestions |
-| `-f` | force merge non-overlapping modules at the top-level structure |
+| Parameters | Description |
+|---------------------------------|----------------------------------------------------------------------------------|
+| `FILE` | files to merge |
+| `-a, --add-missing` | [Leaf Merging Strategy] enable adding missing nodes to reference |
+| `-h, --help` | displays help and exits |
+| `--ignore-case` | ignores case when checking node names |
+| `--leaf` | use leaf merging strategy |
+| `-nc, --not-compressed` | save uncompressed output File |
+| `-o, --outputFile=` | output File (or empty for stdout; [MIMO mode] output folder)) |
+| `--recursive` | use recursive merging strategy (default) |
+| `--mimo` | merge multiple files with the same prefix into multiple output files |
+| `-ld, --levenshtein-distance` | [MIMO mode] levenshtein distance for name match suggestions |
+| `-f` | force merge non-overlapping modules at the top-level structure |
+| `--large` | merge multiple project files into one output file, separated by their dot-prefix |
```
Usage: ccsh merge [-ah] [--ignore-case] [--leaf] [-nc] [--recursive]
@@ -60,10 +61,24 @@ This last example inputs the folder foo, which will result in all project files
ccsh merge myProjectFolder/ --mimo -ld 0 -f
```
-## MIMO - Multiply Inputs Multiple Outputs
+## MIMO Merge - Multiply Inputs Multiple Outputs
Matches multiple `cc.json` files based on their prefix (e.g. **myProject**.git.cc.json). Tries to match project names with typos and asks which to add to the output.
If you want to use this in a CI/CD pipeline environment you may find it useful to specify `-ld` and `-f` to not prompt any user input.
The output file name follows the following schema: `myProject.merge.cc.json`.
-> IMPORTANT: Output is always the current working directory.
+## Large Merge
+
+Merges multiple `.cc.json` files into one projects, but separates them into sub-folders, with names defined through the dot-prefixes of the input files:
+
+```
+ccsh merge aa.cc.json bb.cc.json cc.cc.json --large -o myOutputFile -nc
+# myOutputFile.cc.json:
+# - root
+# - - aa
+# - - - *
+# - - bb
+# - - - *
+# - - cc
+# - - - *
+```
From 40df04b50e148792fe6a2241f1c0fcc28cf5140b Mon Sep 17 00:00:00 2001
From: Sebastian Wolf <65733509+phanlezz@users.noreply.github.com>
Date: Thu, 5 Dec 2024 14:04:28 +0100
Subject: [PATCH 5/5] feat(analysis): implement review notes (#3743)
---
analysis/CHANGELOG.md | 2 +-
.../filter/mergefilter/LargeMerge.kt | 28 ++--
.../filter/mergefilter/MergeFilter.kt | 2 +-
.../filter/mergefilter/MergeFilterTest.kt | 16 +++
.../duplicate/customRootFailure.cc.json | 128 ++++++++++++++++++
5 files changed, 164 insertions(+), 12 deletions(-)
create mode 100644 analysis/filter/MergeFilter/src/test/resources/largeMerge/duplicate/customRootFailure.cc.json
diff --git a/analysis/CHANGELOG.md b/analysis/CHANGELOG.md
index 0a9d529893..1f6851b6ca 100644
--- a/analysis/CHANGELOG.md
+++ b/analysis/CHANGELOG.md
@@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/)
### Added 🚀
-- Add a new `--large` flat to the MergeFilter that merges projects into one file each in its own subfolder depending on the input file's dot-prefix name [#3841](https://github.com/MaibornWolff/codecharta/pull/3841)
+- Add a new `--large` flag to the MergeFilter that merges projects into one file each in its own subfolder depending on the input file's dot-prefix name [#3841](https://github.com/MaibornWolff/codecharta/pull/3841)
- Add the ability to the MergeFilter to specify the output file during `--mimo` operation [#3841](https://github.com/MaibornWolff/codecharta/pull/3841)
## [1.129.0] - 2024-11-29
diff --git a/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/LargeMerge.kt b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/LargeMerge.kt
index 49442485d7..dcc7712eda 100644
--- a/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/LargeMerge.kt
+++ b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/LargeMerge.kt
@@ -8,7 +8,7 @@ import de.maibornwolff.codecharta.model.Project
class LargeMerge {
companion object {
- fun packageProjectInto(project: Project, prefix: String): Project {
+ fun wrapProjectInFolder(project: Project, prefix: String): Project {
val modifiedProject = Project(
project.projectName,
moveNodesIntoFolder(project.rootNode, prefix),
@@ -22,32 +22,40 @@ class LargeMerge {
}
private fun moveNodesIntoFolder(root: Node, folderName: String): List {
- val newRoot = Node(
+ val folderNode = Node(
name = folderName,
type = NodeType.Folder,
children = root.children
)
val mutableRoot = root.toMutableNode()
- mutableRoot.children = mutableSetOf(newRoot.toMutableNode())
+ mutableRoot.children = mutableSetOf(folderNode.toMutableNode())
return listOf(mutableRoot.toNode())
}
private fun addFolderToEdgePaths(edges: List, folderName: String): List {
- edges.forEach {
- it.fromNodeName = insertFolderIntoPath(it.fromNodeName, folderName)
- it.toNodeName = insertFolderIntoPath(it.toNodeName, folderName)
+ return edges.map { edge ->
+ Edge(
+ fromNodeName = insertFolderIntoPath(edge.fromNodeName, folderName),
+ toNodeName = insertFolderIntoPath(edge.toNodeName, folderName),
+ attributes = edge.attributes
+ )
}
- return edges
}
private fun addFolderToBlackListPaths(blacklist: List, folderName: String): List {
- blacklist.forEach {
- it.path = insertFolderIntoPath(it.path, folderName)
+ return blacklist.map { item ->
+ BlacklistItem(
+ path = insertFolderIntoPath(item.path, folderName),
+ type = item.type
+ )
}
- return blacklist
}
private fun insertFolderIntoPath(path: String, folderName: String): String {
+ val rootRegex = Regex("^/root/")
+ require(rootRegex.containsMatchIn(path)) {
+ "Input project structure doesn't have '/root/' as a base folder. If that's intended open an issue."
+ }
return path.replace(Regex("^/root/"), "/root/$folderName/")
}
}
diff --git a/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilter.kt b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilter.kt
index 6d5fb8c9f2..5afdcefbb6 100644
--- a/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilter.kt
+++ b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilter.kt
@@ -199,7 +199,7 @@ class MergeFilter(
val packagedProjects: MutableList = mutableListOf()
projectsFileNamePairs.forEach {
- packagedProjects.add(LargeMerge.packageProjectInto(it.second, it.first.substringBefore(".")))
+ packagedProjects.add(LargeMerge.wrapProjectInFolder(it.second, it.first.substringBefore(".")))
}
val mergedProject = ProjectMerger(packagedProjects, nodeMergerStrategy).merge()
diff --git a/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt b/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt
index 99159cc39b..dc5a7db54e 100644
--- a/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt
+++ b/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt
@@ -626,5 +626,21 @@ class MergeFilterTest {
assertThat(outputProject2.children.toString()).isEqualTo(projectInput2.rootNode.children.toString())
outPutFile.deleteOnExit()
}
+
+ @Test
+ fun `should throw error if input project does not contain a strict root node`() {
+ val customRootProject = "$fatMergeTestFolder/duplicate/customRootFailure.cc.json"
+ System.setErr(PrintStream(errContent))
+ CommandLine(MergeFilter()).execute(
+ testFilePath1,
+ testFilePath2,
+ customRootProject,
+ "--large"
+ ).toString()
+ System.setErr(originalErr)
+ assertThat(
+ errContent.toString()
+ ).contains("Input project structure doesn't have '/root/' as a base folder. If that's intended open an issue.")
+ }
}
}
diff --git a/analysis/filter/MergeFilter/src/test/resources/largeMerge/duplicate/customRootFailure.cc.json b/analysis/filter/MergeFilter/src/test/resources/largeMerge/duplicate/customRootFailure.cc.json
new file mode 100644
index 0000000000..fae0f5ee94
--- /dev/null
+++ b/analysis/filter/MergeFilter/src/test/resources/largeMerge/duplicate/customRootFailure.cc.json
@@ -0,0 +1,128 @@
+{
+ "projectName": "Sample Project 1",
+ "apiVersion": "1.0",
+ "nodes": [
+ {
+ "name": "customRoot",
+ "type": "Folder",
+ "attributes": {},
+ "children": [
+ {
+ "name": "file1",
+ "type": "File",
+ "attributes": {
+ "number_of_commits": 160.0,
+ "range_of_weeks_with_commits": 68.0,
+ "successive_weeks_with_commits": 10.0,
+ "weeks_with_commits": 51.0,
+ "highly_coupled_files": 0.0,
+ "median_coupled_files": 2.0,
+ "abs_coupled_churn": 33700.0,
+ "avg_code_churn": 3.0
+ },
+ "link": "",
+ "children": []
+ },
+ {
+ "name": "file4",
+ "type": "File",
+ "attributes": {
+ "number_of_authors": 22.0,
+ "number_of_commits": 130.0,
+ "range_of_weeks_with_commits": 18.0,
+ "successive_weeks_with_commits": 20.0,
+ "weeks_with_commits": 61.0,
+ "highly_coupled_files": 1.0,
+ "median_coupled_files": 4.0,
+ "abs_coupled_churn": 22000.0,
+ "avg_code_churn": 4.0
+ },
+ "link": "",
+ "children": []
+ },
+ {
+ "name": "visualization",
+ "type": "Folder",
+ "attributes": {},
+ "link": "",
+ "children": [
+ {
+ "name": "file2",
+ "type": "File",
+ "attributes": {
+ "number_of_authors": 12.0,
+ "number_of_commits": 120.0,
+ "range_of_weeks_with_commits": 30.0,
+ "successive_weeks_with_commits": 20.0,
+ "weeks_with_commits": 76.0,
+ "highly_coupled_files": 0.0,
+ "median_coupled_files": 2.0,
+ "abs_coupled_churn": 33700.0,
+ "avg_code_churn": 7.0
+ },
+ "link": "",
+ "children": []
+ },
+ {
+ "name": "file3",
+ "type": "File",
+ "attributes": {
+ "number_of_authors": 40.0,
+ "number_of_commits": 230.0,
+ "range_of_weeks_with_commits": 50.0,
+ "successive_weeks_with_commits": 80.0,
+ "weeks_with_commits": 10.0,
+ "highly_coupled_files": 5.0,
+ "median_coupled_files": 2.0,
+ "abs_coupled_churn": 44000.0,
+ "avg_code_churn": 4.0
+ },
+ "link": "",
+ "children": []
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "edges": [
+ {
+ "fromNodeName": "/customRoot/file1",
+ "toNodeName": "/customRoot/visualization/file2",
+ "attributes": {
+ "pairingRate": 90,
+ "avgCommits": 5,
+ "anotherMetric": 25
+ }
+ },
+ {
+ "fromNodeName": "/customRoot/visualization/file2",
+ "toNodeName": "/customRoot/visualization/file3",
+ "attributes": {
+ "pairingRate": 80,
+ "avgCommits": 3
+ }
+ }
+ ],
+ "attributeTypes": {
+ "nodes": {
+ "attributeA": "absolute",
+ "attributeB": "absolute",
+ "attributeZ": "absolute"
+ },
+ "edges": {
+ "attributeD": "absolute",
+ "attributeE": "relative"
+ }
+ },
+ "blacklist": [
+ {
+ "path": "/customRoot/file1",
+ "type": "hide"
+ },
+ {
+ "path": "/customRoot/visualization/file2",
+ "type": "exclude"
+ }
+ ]
+}