Skip to content

Commit 1556a46

Browse files
author
Kamil Bielecki
committed
feat(scanner): Add branch name to FossID scan code
Add branch name (revision) to FossID scan code for easier identification of scanned source code. As FossID accepts maximum of 255 characters in scan code, branch name is trimmed to size, that is compliant with this constraint. Signed-off-by: Kamil Bielecki <[email protected]>
1 parent 833dac3 commit 1556a46

File tree

3 files changed

+245
-10
lines changed

3 files changed

+245
-10
lines changed

plugins/scanners/fossid/src/main/kotlin/FossId.kt

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,7 @@ class FossId internal constructor(
467467
val scanCodeAndId = if (existingScan == null) {
468468
logger.info { "No scan found for $url and revision $revision. Creating scan..." }
469469

470-
val scanCode = namingProvider.createScanCode(projectName)
470+
val scanCode = namingProvider.createScanCode(projectName = projectName, branch = revision)
471471
val newUrl = urlProvider.getUrl(url)
472472
val scanId = createScan(projectCode, scanCode, newUrl, revision)
473473

@@ -512,10 +512,6 @@ class FossId internal constructor(
512512
if (defaultBranch != null) "Default branch is '$defaultBranch'." else "There is no default remote branch."
513513
}
514514

515-
// If a scan for the default branch is created, put the default branch name in the scan code (the
516-
// FossIdNamingProvider must also have a scan pattern that makes use of it).
517-
val branchLabel = projectRevision.takeIf { defaultBranch == projectRevision }.orEmpty()
518-
519515
if (projectRevision == null) {
520516
logger.warn { "No project revision has been given." }
521517
} else {
@@ -547,13 +543,13 @@ class FossId internal constructor(
547543
logger.info {
548544
"No scan found for $mappedUrlWithoutCredentials and revision $revision. Creating origin scan..."
549545
}
550-
namingProvider.createScanCode(projectName, DeltaTag.ORIGIN, branchLabel)
546+
namingProvider.createScanCode(projectName, DeltaTag.ORIGIN, revision)
551547
} else {
552548
logger.info { "Scan '${existingScan.code}' found for $mappedUrlWithoutCredentials and revision $revision." }
553549
logger.info {
554550
"Existing scan has for reference(s): ${existingScan.comment.orEmpty()}. Creating delta scan..."
555551
}
556-
namingProvider.createScanCode(projectName, DeltaTag.DELTA, branchLabel)
552+
namingProvider.createScanCode(projectName, DeltaTag.DELTA, revision)
557553
}
558554

559555
val scanId = createScan(projectCode, scanCode, mappedUrl, revision, projectRevision.orEmpty())

plugins/scanners/fossid/src/main/kotlin/FossIdNamingProvider.kt

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import org.apache.logging.log4j.kotlin.logger
3737
* * **currentTimestamp**: The current time.
3838
* * **deltaTag** (scan code only): If delta scans is enabled, this qualifies the scan as an *origin* scan or a *delta*
3939
* scan.
40+
* * **branch**: branch name (revision) given to scan
4041
*/
4142
class FossIdNamingProvider(
4243
private val namingProjectPattern: String?,
@@ -46,6 +47,8 @@ class FossIdNamingProvider(
4647
companion object {
4748
@JvmStatic
4849
val FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss")
50+
51+
const val MAX_SCAN_CODE_LEN = 254
4952
}
5053

5154
fun createProjectCode(projectName: String): String =
@@ -57,16 +60,86 @@ class FossIdNamingProvider(
5760
} ?: projectName
5861

5962
fun createScanCode(projectName: String, deltaTag: FossId.DeltaTag? = null, branch: String = ""): String {
63+
return namingScanPattern?.let {
64+
createScanCodeForCustomPattern(namingScanPattern, projectName, deltaTag, branch)
65+
} ?: run {
66+
createScanCodeForDefaultPattern(projectName, deltaTag, branch)
67+
}
68+
}
69+
70+
private fun createScanCodeForDefaultPattern(
71+
projectName: String,
72+
deltaTag: FossId.DeltaTag? = null,
73+
branch: String = ""
74+
): String {
75+
val builtins = mutableMapOf("#projectName" to projectName)
6076
var defaultPattern = "#projectName_#currentTimestamp"
61-
val builtins = mutableMapOf("#projectName" to projectName, "#branch" to branch)
6277

6378
deltaTag?.let {
6479
defaultPattern += "_#deltaTag"
6580
builtins += "#deltaTag" to deltaTag.name.lowercase()
6681
}
6782

68-
val pattern = namingScanPattern ?: defaultPattern
69-
return replaceNamingConventionVariables(pattern, builtins, namingConventionVariables)
83+
if (branch.isNotBlank()) {
84+
val branchName = normalizeBranchName(branch, defaultPattern, builtins)
85+
defaultPattern += "_#branch"
86+
builtins += "#branch" to branchName
87+
}
88+
89+
return replaceNamingConventionVariables(defaultPattern, builtins, namingConventionVariables)
90+
}
91+
92+
private fun createScanCodeForCustomPattern(
93+
namingPattern: String,
94+
projectName: String,
95+
deltaTag: FossId.DeltaTag? = null,
96+
branch: String = ""
97+
): String {
98+
val builtins = mutableMapOf<String, String>()
99+
100+
namingPattern.contains("#projectName").let {
101+
builtins += "#projectName" to projectName
102+
}
103+
104+
namingPattern.contains("#deltaTag").let {
105+
if (deltaTag != null) {
106+
builtins += "#deltaTag" to deltaTag.name.lowercase()
107+
}
108+
}
109+
110+
namingPattern.contains("#branch").let {
111+
val namingPatternWithoutBranchPlaceholder = namingPattern.replace("#branch", "")
112+
builtins += "#branch" to normalizeBranchName(branch, namingPatternWithoutBranchPlaceholder, builtins)
113+
}
114+
115+
return replaceNamingConventionVariables(namingPattern, builtins, namingConventionVariables)
116+
}
117+
118+
/**
119+
* Replaces non-standard characters in branch name and trims its length to one that will not exceed
120+
* maximum length of FossID scan ID, when combined with the rest of variables
121+
*/
122+
private fun normalizeBranchName(
123+
branch: String,
124+
scanCodeNamingPattern: String,
125+
scanCodeVariables: Map<String, String>
126+
): String {
127+
val noBranchScanCode =
128+
replaceNamingConventionVariables(
129+
scanCodeNamingPattern,
130+
scanCodeVariables,
131+
namingConventionVariables
132+
)
133+
134+
if (noBranchScanCode.length >= MAX_SCAN_CODE_LEN) {
135+
throw IllegalArgumentException(
136+
"FossID scan code '$noBranchScanCode' is too long. " +
137+
"It must not exceed $MAX_SCAN_CODE_LEN characters. Please consider shorter naming scan pattern."
138+
)
139+
}
140+
141+
val maxBranchNameLength = MAX_SCAN_CODE_LEN - noBranchScanCode.length
142+
return branch.replace(Regex("[^a-zA-Z0-9-_]"), "_").take(maxBranchNameLength)
70143
}
71144

72145
/**
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/*
2+
* Copyright (C) 2024 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
* License-Filename: LICENSE
18+
*/
19+
20+
package org.ossreviewtoolkit.plugins.scanners.fossid
21+
22+
import io.kotest.assertions.throwables.shouldThrow
23+
import io.kotest.core.spec.style.WordSpec
24+
import io.kotest.core.test.TestCase
25+
import io.kotest.core.test.TestResult
26+
import io.kotest.matchers.equals.shouldBeEqual
27+
import io.kotest.matchers.ints.shouldBeLessThanOrEqual
28+
29+
import io.mockk.every
30+
import io.mockk.mockkStatic
31+
import io.mockk.unmockkAll
32+
33+
import java.time.LocalDateTime
34+
35+
class FossIdNamingProviderTest : WordSpec() {
36+
37+
override suspend fun afterEach(testCase: TestCase, result: TestResult) {
38+
unmockkAll()
39+
}
40+
41+
companion object {
42+
const val MAX_SCAN_CODE_LEN = 255
43+
}
44+
45+
init {
46+
"createScanCode" should {
47+
val namingProvider = FossIdNamingProvider(null, null, emptyMap())
48+
49+
val mockedDateTime = LocalDateTime.of(2024, 4, 1, 10, 0)
50+
val expectedTimestamp = "20240401_100000"
51+
52+
val longBranchName = "origin/feature/LOREM-123321_lorem-ipsum-dolor-sit-amet-consectetur-adipiscing-elit-" +
53+
"aliquam-laoreet-ac-nulla-in-bibendum-phasellus-sodales-vel-lorem-consequat-efficitur-morbi-viverra-a" +
54+
"ccumsan-libero-a-tincidunt-libero-venenatis-nec-nulla-facilisi-vestibulum-pharetra-finibus-mi-vitae-" +
55+
"luctus"
56+
57+
val longScanPattern = "#projectName_#currentTimestamp_lorem-ipsum-dolor-sit-amet-consectetur-adipiscing-e" +
58+
"lit-aliquam-laoreet-ac-nulla-in-bibendum-phasellus-sodales-vel-lorem-consequat-efficitur-morbi-viver" +
59+
"ra-accumsan-libero-a-tincidunt-libero-venenatis-nec-nulla-facilisi-vestibulum-pharetra-finibus-mi-vi" +
60+
"tae-luctus"
61+
62+
"create code without branch name, when it's empty" {
63+
mockkStatic(LocalDateTime::class) {
64+
every { LocalDateTime.now() } returns mockedDateTime
65+
66+
namingProvider.createScanCode(
67+
"example-project-name", null, ""
68+
) shouldBeEqual "example-project-name_$expectedTimestamp"
69+
}
70+
}
71+
72+
"create code with branch name" {
73+
mockkStatic(LocalDateTime::class) {
74+
every { LocalDateTime.now() } returns mockedDateTime
75+
76+
namingProvider.createScanCode(
77+
"example-project-name", null, "CODE-2233_Red-dots-added-to-layout"
78+
) shouldBeEqual "example-project-name_" + expectedTimestamp + "_CODE-2233_Red-dots-added-to-layout"
79+
}
80+
unmockkAll()
81+
}
82+
83+
"create code with branch name and delta tag" {
84+
mockkStatic(LocalDateTime::class) {
85+
every { LocalDateTime.now() } returns mockedDateTime
86+
87+
namingProvider.createScanCode(
88+
"example-project-name", FossId.DeltaTag.DELTA, "CODE-2233_Red-dots-added-to-layout"
89+
) shouldBeEqual "example-project-name_" + expectedTimestamp +
90+
"_delta_CODE-2233_Red-dots-added-to-layout"
91+
}
92+
unmockkAll()
93+
}
94+
95+
"remove all non-standard signs from branch name when creating code" {
96+
mockkStatic(LocalDateTime::class) {
97+
every { LocalDateTime.now() } returns mockedDateTime
98+
99+
namingProvider.createScanCode(
100+
"example-project-name", null, "feature/CODE-12%%$@@&^_SOME_*&^#!*text!!"
101+
) shouldBeEqual "example-project-name_" +
102+
expectedTimestamp + "_feature_CODE-12________SOME_______text__"
103+
}
104+
}
105+
106+
"truncate very long scan id to fit maximum length accepted by FossID (255 chars)" {
107+
mockkStatic(LocalDateTime::class) {
108+
every { LocalDateTime.now() } returns mockedDateTime
109+
110+
namingProvider.createScanCode(
111+
"example-project-name", FossId.DeltaTag.DELTA, longBranchName
112+
).length shouldBeLessThanOrEqual MAX_SCAN_CODE_LEN
113+
}
114+
}
115+
116+
"create code without branch name form custom naming pattern" {
117+
val customScanPattern = "#projectName_#currentTimestamp"
118+
119+
val namingProviderWithLongScanPattern = FossIdNamingProvider(null, customScanPattern, emptyMap())
120+
mockkStatic(LocalDateTime::class) {
121+
every { LocalDateTime.now() } returns mockedDateTime
122+
123+
namingProviderWithLongScanPattern.createScanCode(
124+
"example-project-name", null, ""
125+
) shouldBeEqual "example-project-name_20240401_100000"
126+
}
127+
}
128+
129+
"create code without branch name form custom naming pattern when branch name is provided" {
130+
val customScanPattern = "#projectName_#currentTimestamp"
131+
132+
val namingProviderWithLongScanPattern = FossIdNamingProvider(null, customScanPattern, emptyMap())
133+
mockkStatic(LocalDateTime::class) {
134+
every { LocalDateTime.now() } returns mockedDateTime
135+
136+
namingProviderWithLongScanPattern.createScanCode(
137+
"example-project-name", null, "feature/LOREM-3212"
138+
) shouldBeEqual "example-project-name_20240401_100000"
139+
}
140+
}
141+
142+
"create code without branch name form custom naming pattern when too long branch name is provided" {
143+
val customScanPattern = "#projectName_#currentTimestamp_#branch"
144+
val namingProviderWithLongScanPattern = FossIdNamingProvider(null, customScanPattern, emptyMap())
145+
mockkStatic(LocalDateTime::class) {
146+
every { LocalDateTime.now() } returns mockedDateTime
147+
148+
namingProviderWithLongScanPattern.createScanCode(
149+
"example-project-name", null, longBranchName
150+
).length shouldBeLessThanOrEqual MAX_SCAN_CODE_LEN
151+
}
152+
}
153+
154+
"throw an exception if scan code pattern is too long" {
155+
val namingProviderWithLongScanPattern = FossIdNamingProvider(null, longScanPattern, emptyMap())
156+
mockkStatic(LocalDateTime::class) {
157+
every { LocalDateTime.now() } returns mockedDateTime
158+
159+
shouldThrow<IllegalArgumentException> {
160+
namingProviderWithLongScanPattern.createScanCode("example-project-name", null, "")
161+
}
162+
}
163+
}
164+
}
165+
}
166+
}

0 commit comments

Comments
 (0)