diff --git a/plugins/commands/compare/src/main/kotlin/CompareCommand.kt b/plugins/commands/compare/src/main/kotlin/CompareCommand.kt index 0d545e965a4f0..5b1dbca6bf2c6 100644 --- a/plugins/commands/compare/src/main/kotlin/CompareCommand.kt +++ b/plugins/commands/compare/src/main/kotlin/CompareCommand.kt @@ -43,11 +43,30 @@ import com.github.difflib.UnifiedDiffUtils import java.time.Instant import org.ossreviewtoolkit.model.OrtResult +import org.ossreviewtoolkit.model.PackageCuration +import org.ossreviewtoolkit.model.Repository +import org.ossreviewtoolkit.model.VcsInfo +import org.ossreviewtoolkit.model.config.Curations +import org.ossreviewtoolkit.model.config.Excludes +import org.ossreviewtoolkit.model.config.IssueResolution +import org.ossreviewtoolkit.model.config.LicenseChoices +import org.ossreviewtoolkit.model.config.LicenseFindingCuration +import org.ossreviewtoolkit.model.config.PackageConfiguration +import org.ossreviewtoolkit.model.config.PackageLicenseChoice +import org.ossreviewtoolkit.model.config.PackageManagerConfiguration +import org.ossreviewtoolkit.model.config.PathExclude +import org.ossreviewtoolkit.model.config.RepositoryAnalyzerConfiguration +import org.ossreviewtoolkit.model.config.RepositoryConfiguration +import org.ossreviewtoolkit.model.config.Resolutions +import org.ossreviewtoolkit.model.config.RuleViolationResolution +import org.ossreviewtoolkit.model.config.ScopeExclude +import org.ossreviewtoolkit.model.config.VulnerabilityResolution import org.ossreviewtoolkit.model.mapper import org.ossreviewtoolkit.plugins.commands.api.OrtCommand import org.ossreviewtoolkit.utils.common.expandTilde import org.ossreviewtoolkit.utils.common.getCommonParentFile import org.ossreviewtoolkit.utils.ort.Environment +import org.ossreviewtoolkit.utils.spdx.model.SpdxLicenseChoice class CompareCommand : OrtCommand( name = "compare", @@ -167,12 +186,142 @@ class CompareCommand : OrtCommand( throw ProgramResult(0) } + val diff = resultA.diff(resultB) + + echo(deserializer.writeValueAsString(diff)) + throw ProgramResult(1) } } } } +private fun OrtResult.diff(other: OrtResult) = + OrtResultDiff( + repositoryDiff = repository.diff(other.repository) + ) + +private fun Repository.diff(other: Repository): RepositoryDiff? = + if (this == other) { + null + } else { + RepositoryDiff( + vcsA = vcs.takeIf { it != other.vcs }, + vcsB = other.vcs.takeIf { it != vcs }, + vcsProcessedA = vcsProcessed.takeIf { it != other.vcsProcessed }, + vcsProcessedB = other.vcsProcessed.takeIf { it != vcsProcessed }, + nestedRepositoriesA = nestedRepositories.takeIf { it != other.nestedRepositories }, + nestedRepositoriesB = other.nestedRepositories.takeIf { it != nestedRepositories }, + configDiff = config.diff(other.config) + ) + } + +private fun RepositoryConfiguration.diff(other: RepositoryConfiguration): RepositoryConfigurationDiff? = + if (this == other) { + null + } else { + RepositoryConfigurationDiff( + analyzerConfigDiff = analyzer.diff(other.analyzer), + excludeDiff = excludes.diff(other.excludes), + resolutionsDiff = resolutions.diff(other.resolutions), + curationsDiff = curations.diff(other.curations), + packageConfigurationsA = (packageConfigurations - other.packageConfigurations.toSet()) + .takeUnless { it.isEmpty() }, + packageConfigurationsB = (other.packageConfigurations - packageConfigurations.toSet()) + .takeUnless { it.isEmpty() }, + licenseChoicesDiff = licenseChoices.diff(other.licenseChoices) + ) + } + +private fun Excludes.diff(other: Excludes): ExcludesDiff? = + if (this == other) { + null + } else { + ExcludesDiff( + pathsA = (paths - other.paths.toSet()).takeUnless { it.isEmpty() }, + pathsB = (other.paths - paths.toSet()).takeUnless { it.isEmpty() }, + scopesA = (scopes - other.scopes.toSet()).takeUnless { it.isEmpty() }, + scopesB = (other.scopes - scopes.toSet()).takeUnless { it.isEmpty() } + ) + } + +private fun Resolutions.diff(other: Resolutions): ResolutionsDiff? = + if (this == other) { + null + } else { + ResolutionsDiff( + issuesA = (issues - other.issues.toSet()).takeUnless { it.isEmpty() }, + issuesB = (other.issues - issues.toSet()).takeUnless { it.isEmpty() }, + ruleViolationsA = (ruleViolations - other.ruleViolations.toSet()).takeUnless { it.isEmpty() }, + ruleViolationsB = (other.ruleViolations - ruleViolations.toSet()).takeUnless { it.isEmpty() }, + vulnerabilitiesA = (vulnerabilities - other.vulnerabilities.toSet()).takeUnless { it.isEmpty() }, + vulnerabilitiesB = (other.vulnerabilities - vulnerabilities.toSet()).takeUnless { it.isEmpty() } + ) + } + +private fun Curations.diff(other: Curations): CurationsDiff? = + if (this == other) { + null + } else { + CurationsDiff( + packagesA = (packages - other.packages.toSet()).takeUnless { it.isEmpty() }, + packagesB = (other.packages - packages.toSet()).takeUnless { it.isEmpty() }, + licenseFindingsA = (licenseFindings - other.licenseFindings.toSet()).takeUnless { it.isEmpty() }, + licenseFindingsB = (other.licenseFindings - licenseFindings.toSet()).takeUnless { it.isEmpty() } + ) + } + +private fun LicenseChoices.diff(other: LicenseChoices): LicenseChoicesDiff? = + if (this == other) { + null + } else { + LicenseChoicesDiff( + repositoryLicenseChoicesA = (repositoryLicenseChoices - other.repositoryLicenseChoices.toSet()) + .takeUnless { it.isEmpty() }, + repositoryLicenseChoicesB = (other.repositoryLicenseChoices - repositoryLicenseChoices.toSet()) + .takeUnless { it.isEmpty() }, + packageLicenseChoicesA = (packageLicenseChoices - other.packageLicenseChoices.toSet()) + .takeUnless { it.isEmpty() }, + packageLicenseChoicesB = (other.packageLicenseChoices - packageLicenseChoices.toSet()) + .takeUnless { it.isEmpty() } + ) + } + +private fun RepositoryAnalyzerConfiguration?.diff(other: RepositoryAnalyzerConfiguration?): AnalyzerConfigurationDiff? { + if (this == other) return null + + return if (this == null) { + AnalyzerConfigurationDiff( + allowDynamicVersionsB = other?.allowDynamicVersions, + enabledPackageManagersB = other?.enabledPackageManagers, + disabledPackageManagersB = other?.disabledPackageManagers, + packageManagersB = other?.packageManagers, + skipExcludedB = other?.skipExcluded + ) + } else if (other == null) { + AnalyzerConfigurationDiff( + allowDynamicVersionsA = allowDynamicVersions, + enabledPackageManagersA = enabledPackageManagers, + disabledPackageManagersA = disabledPackageManagers, + packageManagersA = packageManagers, + skipExcludedA = skipExcluded + ) + } else { + AnalyzerConfigurationDiff( + allowDynamicVersionsA = allowDynamicVersions.takeIf { it != other.allowDynamicVersions }, + allowDynamicVersionsB = other.allowDynamicVersions.takeIf { it != allowDynamicVersions }, + enabledPackageManagersA = enabledPackageManagers.takeIf { it != other.enabledPackageManagers }, + enabledPackageManagersB = other.enabledPackageManagers.takeIf { it != enabledPackageManagers }, + disabledPackageManagersA = disabledPackageManagers.takeIf { it != other.disabledPackageManagers }, + disabledPackageManagersB = other.disabledPackageManagers.takeIf { it != disabledPackageManagers }, + packageManagersA = packageManagers.takeIf { it != other.packageManagers }, + packageManagersB = other.packageManagers.takeIf { it != packageManagers }, + skipExcludedA = skipExcluded.takeIf { it != other.skipExcluded }, + skipExcludedB = other.skipExcluded.takeIf { it != skipExcluded } + ) + } +} + private enum class CompareMethod { TEXT_DIFF, SEMANTIC_DIFF @@ -204,3 +353,70 @@ private fun Map.replaceIn(text: String) = entries.fold(text) { currentText, (from, to) -> currentText.replace(from, to) } + +private data class OrtResultDiff( + val repositoryDiff: RepositoryDiff? = null +) + +private data class RepositoryDiff( + val vcsA: VcsInfo? = null, + val vcsB: VcsInfo? = null, + val vcsProcessedA: VcsInfo? = null, + val vcsProcessedB: VcsInfo? = null, + val nestedRepositoriesA: Map? = null, + val nestedRepositoriesB: Map? = null, + val configDiff: RepositoryConfigurationDiff? = null +) + +private data class RepositoryConfigurationDiff( + val analyzerConfigDiff: AnalyzerConfigurationDiff? = null, + val excludeDiff: ExcludesDiff? = null, + val resolutionsDiff: ResolutionsDiff? = null, + val curationsDiff: CurationsDiff? = null, + val packageConfigurationsA: List? = null, + val packageConfigurationsB: List? = null, + val licenseChoicesDiff: LicenseChoicesDiff? = null +) + +private data class ExcludesDiff( + val pathsA: List? = null, + val pathsB: List? = null, + val scopesA: List? = null, + val scopesB: List? = null +) + +private data class ResolutionsDiff( + val issuesA: List? = null, + val issuesB: List? = null, + val ruleViolationsA: List? = null, + val ruleViolationsB: List? = null, + val vulnerabilitiesA: List? = null, + val vulnerabilitiesB: List? = null +) + +private data class CurationsDiff( + val packagesA: List? = null, + val packagesB: List? = null, + val licenseFindingsA: List? = null, + val licenseFindingsB: List? = null +) + +private data class LicenseChoicesDiff( + val repositoryLicenseChoicesA: List? = null, + val repositoryLicenseChoicesB: List? = null, + val packageLicenseChoicesA: List? = null, + val packageLicenseChoicesB: List? = null +) + +private data class AnalyzerConfigurationDiff( + val allowDynamicVersionsA: Boolean? = null, + val allowDynamicVersionsB: Boolean? = null, + val enabledPackageManagersA: List? = null, + val enabledPackageManagersB: List? = null, + val disabledPackageManagersA: List? = null, + val disabledPackageManagersB: List? = null, + val packageManagersA: Map? = null, + val packageManagersB: Map? = null, + val skipExcludedA: Boolean? = false, + val skipExcludedB: Boolean? = false +)