Skip to content

Commit 1cf9421

Browse files
committed
feat(vcs): Add Git-specific configuration options for submodule handling
For large repositories with many layers of nested Git submodules, the download process can be very time-consuming and often results in duplicate projects in the tree of nested submodules. This feature introduces configuration options to limit the recursive checkout of nested Git submodules to the first layer, optimizing performance and reducing redundancy. Additionally, it also allows to limit the depth of commit history to fetch when downloading the projects. Signed-off-by: Wolfgang Klenk <wolfgang.klenk2@bosch.com>
1 parent 6534363 commit 1cf9421

File tree

2 files changed

+174
-15
lines changed

2 files changed

+174
-15
lines changed

plugins/version-control-systems/git/src/main/kotlin/Git.kt

Lines changed: 90 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ import org.ossreviewtoolkit.downloader.VersionControlSystem
4848
import org.ossreviewtoolkit.downloader.WorkingTree
4949
import org.ossreviewtoolkit.model.VcsInfo
5050
import org.ossreviewtoolkit.model.VcsType
51+
import org.ossreviewtoolkit.plugins.versioncontrolsystems.git.OptionKey.Companion.getOrDefault
52+
import org.ossreviewtoolkit.plugins.versioncontrolsystems.git.OptionKey.HISTORY_DEPTH
53+
import org.ossreviewtoolkit.plugins.versioncontrolsystems.git.OptionKey.UPDATE_NESTED_SUBMODULES
5154
import org.ossreviewtoolkit.utils.common.CommandLineTool
5255
import org.ossreviewtoolkit.utils.common.Options
5356
import org.ossreviewtoolkit.utils.common.Os
@@ -60,14 +63,50 @@ import org.ossreviewtoolkit.utils.ort.showStackTrace
6063
import org.semver4j.RangesList
6164
import org.semver4j.RangesListFactory
6265

63-
// TODO: Make this configurable.
64-
const val GIT_HISTORY_DEPTH = 50
65-
6666
// Replace prefixes of Git submodule repository URLs.
6767
private val REPOSITORY_URL_PREFIX_REPLACEMENTS = listOf(
6868
"git://" to "https://"
6969
)
7070

71+
enum class OptionKey(val key: String, val defaultValue: String, val deprecated: Boolean = false) {
72+
// Git-specific configuration option for the depth of commit history to fetch
73+
HISTORY_DEPTH("historyDepth", "50"),
74+
75+
// Git-specific configuration option to define if nested submodules should be updated, or if only the
76+
// submodules on the top-level should be initialized, updated, and cloned.
77+
UPDATE_NESTED_SUBMODULES("updateNestedSubmodules", "true"),
78+
79+
// Example for deprecating a configuration option
80+
DO_NOT_USE("doNotUse", "some-value", deprecated = true);
81+
82+
companion object {
83+
private val map = entries.associateBy(OptionKey::key)
84+
private val validKeys: Set<String> get() = map.keys
85+
86+
fun validate(options: Options): OptionsValidationResult {
87+
val unknownKeys = options.keys - OptionKey.validKeys
88+
val deprecatedKeys = entries.filter { it.deprecated }.map { it.key }.toSet()
89+
val usedDeprecatedKeys = options.keys.intersect(deprecatedKeys)
90+
91+
return OptionsValidationResult(
92+
isSuccess = unknownKeys.isEmpty(),
93+
errors = unknownKeys.map { "Unknown Git-specific configuration key: '$it'" },
94+
warnings = usedDeprecatedKeys.map {
95+
"Git-specific configuration key '$it' is deprecated and may be removed in future versions."
96+
}
97+
)
98+
}
99+
100+
fun getOrDefault(options: Options, key: OptionKey): String = options[key.key] ?: key.defaultValue
101+
}
102+
}
103+
104+
data class OptionsValidationResult(
105+
val isSuccess: Boolean,
106+
val errors: List<String> = emptyList(),
107+
val warnings: List<String> = emptyList()
108+
)
109+
71110
object GitCommand : CommandLineTool {
72111
private val versionRegex = Regex("[Gg]it [Vv]ersion (?<version>[\\d.a-z-]+)(\\s.+)?")
73112

@@ -178,9 +217,24 @@ class Git : VersionControlSystem(GitCommand) {
178217
Git(this).use { git ->
179218
logger.info { "Updating working tree from ${workingTree.getRemoteUrl()}." }
180219

181-
updateWorkingTreeWithoutSubmodules(workingTree, git, revision).mapCatching {
220+
val optionsValidationResult = OptionKey.validate(options)
221+
optionsValidationResult.warnings.forEach { logger.warn { it } }
222+
optionsValidationResult.warnings.forEach { logger.error { it } }
223+
require(optionsValidationResult.isSuccess) {
224+
optionsValidationResult.errors.joinToString()
225+
}
226+
227+
val historyDepth = getOrDefault(options, HISTORY_DEPTH).toInt()
228+
updateWorkingTreeWithoutSubmodules(workingTree, git, revision, historyDepth).mapCatching {
182229
// In case this throws the exception gets encapsulated as a failure.
183-
if (recursive) updateSubmodules(workingTree)
230+
if (recursive) {
231+
val updateNestedSubmodules = getOrDefault(options, UPDATE_NESTED_SUBMODULES).toBoolean()
232+
updateSubmodules(
233+
workingTree,
234+
recursive = updateNestedSubmodules,
235+
historyDepth = historyDepth
236+
)
237+
}
184238

185239
revision
186240
}
@@ -190,12 +244,13 @@ class Git : VersionControlSystem(GitCommand) {
190244
private fun updateWorkingTreeWithoutSubmodules(
191245
workingTree: WorkingTree,
192246
git: Git,
193-
revision: String
247+
revision: String,
248+
historyDepth: Int
194249
): Result<String> =
195250
runCatching {
196-
logger.info { "Trying to fetch only revision '$revision' with depth limited to $GIT_HISTORY_DEPTH." }
251+
logger.info { "Trying to fetch only revision '$revision' with depth limited to $historyDepth." }
197252

198-
val fetch = git.fetch().setDepth(GIT_HISTORY_DEPTH)
253+
val fetch = git.fetch().setDepth(historyDepth)
199254

200255
// See https://git-scm.com/docs/gitrevisions#_specifying_revisions for how Git resolves ambiguous
201256
// names. In particular, tag names have higher precedence than branch names.
@@ -213,13 +268,13 @@ class Git : VersionControlSystem(GitCommand) {
213268
it.showStackTrace()
214269

215270
logger.info { "Could not fetch only revision '$revision': ${it.collectMessages()}" }
216-
logger.info { "Falling back to fetching all refs with depth limited to $GIT_HISTORY_DEPTH." }
271+
logger.info { "Falling back to fetching all refs with depth limited to $historyDepth." }
217272

218-
git.fetch().setDepth(GIT_HISTORY_DEPTH).setTagOpt(TagOpt.FETCH_TAGS).call()
273+
git.fetch().setDepth(historyDepth).setTagOpt(TagOpt.FETCH_TAGS).call()
219274
}.recoverCatching {
220275
it.showStackTrace()
221276

222-
logger.info { "Could not fetch with only a depth of $GIT_HISTORY_DEPTH: ${it.collectMessages()}" }
277+
logger.info { "Could not fetch with only a depth of $historyDepth: ${it.collectMessages()}" }
223278
logger.info { "Falling back to fetch everything including tags." }
224279

225280
git.fetch().setUnshallow(true).setTagOpt(TagOpt.FETCH_TAGS).call()
@@ -274,7 +329,14 @@ class Git : VersionControlSystem(GitCommand) {
274329
revision
275330
}
276331

277-
private fun updateSubmodules(workingTree: WorkingTree) {
332+
/**
333+
* Initialize, update, and clone all the submodules in a working tree.
334+
*
335+
* If [recursive] is set to true, then the operations are not only performed on the
336+
* submodules in the top-level of the working tree, but also on the submodules of the submodules, and so on.
337+
* If [recursive] is set to false, only the submodules on the top-level are initialized, updated, and cloned.
338+
*/
339+
private fun updateSubmodules(workingTree: WorkingTree, recursive: Boolean, historyDepth: Int) {
278340
if (!workingTree.getRootPath().resolve(".gitmodules").isFile) return
279341

280342
val insteadOf = REPOSITORY_URL_PREFIX_REPLACEMENTS.map { (prefix, replacement) ->
@@ -283,14 +345,27 @@ class Git : VersionControlSystem(GitCommand) {
283345

284346
runCatching {
285347
// TODO: Migrate this to JGit once https://bugs.eclipse.org/bugs/show_bug.cgi?id=580731 is implemented.
286-
workingTree.runGit("submodule", "update", "--init", "--recursive", "--depth", "$GIT_HISTORY_DEPTH")
348+
val updateArgs = mutableListOf("submodule", "update", "--init", "--depth", "$historyDepth").apply {
349+
if (recursive) { add("--recursive") }
350+
}
351+
352+
workingTree.runGit(*updateArgs.toTypedArray())
287353

288354
insteadOf.forEach {
289-
workingTree.runGit("submodule", "foreach", "--recursive", "git config $it")
355+
val foreachArgs = mutableListOf("submodule", "foreach").apply {
356+
if (recursive) { add("--recursive") }
357+
add("git config $it")
358+
}
359+
360+
workingTree.runGit(*foreachArgs.toTypedArray())
290361
}
291362
}.recover {
292363
// As Git's dumb HTTP transport does not support shallow capabilities, also try to not limit the depth.
293-
workingTree.runGit("submodule", "update", "--recursive")
364+
val fallbackArgs = mutableListOf("submodule", "update").apply {
365+
if (recursive) { add("--recursive") }
366+
}
367+
368+
workingTree.runGit(*fallbackArgs.toTypedArray())
294369
}
295370
}
296371

plugins/version-control-systems/git/src/test/kotlin/GitTest.kt

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ import org.eclipse.jgit.transport.CredentialItem
4242
import org.eclipse.jgit.transport.CredentialsProvider
4343
import org.eclipse.jgit.transport.URIish
4444

45+
import org.ossreviewtoolkit.plugins.versioncontrolsystems.git.OptionKey.Companion.getOrDefault
46+
import org.ossreviewtoolkit.plugins.versioncontrolsystems.git.OptionKey.HISTORY_DEPTH
47+
import org.ossreviewtoolkit.plugins.versioncontrolsystems.git.OptionKey.UPDATE_NESTED_SUBMODULES
48+
import org.ossreviewtoolkit.utils.common.Options
4549
import org.ossreviewtoolkit.utils.ort.requestPasswordAuthentication
4650

4751
class GitTest : WordSpec({
@@ -151,6 +155,86 @@ class GitTest : WordSpec({
151155
credentialProvider.isInteractive shouldBe false
152156
}
153157
}
158+
159+
"The validation of git-specific configuration options" should {
160+
"succeed if valid configuration options are provided" {
161+
val options: Options = mapOf(
162+
"historyDepth" to "1",
163+
"updateNestedSubmodules" to "false"
164+
)
165+
166+
val result = OptionKey.validate(options)
167+
168+
result.isSuccess shouldBe true
169+
result.errors shouldBe emptyList()
170+
result.warnings shouldBe emptyList()
171+
172+
val historyDepth: Int = getOrDefault(options, HISTORY_DEPTH).toInt()
173+
historyDepth shouldBe 1
174+
175+
val updateNestedSubmodules: Boolean = getOrDefault(options, UPDATE_NESTED_SUBMODULES).toBoolean()
176+
updateNestedSubmodules shouldBe false
177+
}
178+
179+
"fail if invalid configuration options are provided" {
180+
val options: Options = mapOf(
181+
"historyDepth" to "1",
182+
"updateNestedSubmodules" to "false",
183+
"invalidOption" to "value"
184+
)
185+
186+
val result = OptionKey.validate(options)
187+
188+
result.isSuccess shouldBe false
189+
result.errors.count() shouldBe 1
190+
result.warnings shouldBe emptyList()
191+
}
192+
193+
"return deprecated warning for deprecated configuration options" {
194+
val options: Options = mapOf(
195+
"historyDepth" to "1",
196+
"updateNestedSubmodules" to "false",
197+
"doNotUse" to "true"
198+
)
199+
200+
val result = OptionKey.validate(options)
201+
202+
result.isSuccess shouldBe true
203+
result.errors shouldBe emptyList()
204+
result.warnings.count() shouldBe 1
205+
206+
val historyDepth: Int = getOrDefault(options, HISTORY_DEPTH).toInt()
207+
historyDepth shouldBe 1
208+
209+
val updateNestedSubmodules: Boolean = getOrDefault(options, UPDATE_NESTED_SUBMODULES).toBoolean()
210+
updateNestedSubmodules shouldBe false
211+
}
212+
}
213+
214+
"The helper for getting configuration options" should {
215+
"return the correct values if the options are set" {
216+
val options: Options = mapOf(
217+
"historyDepth" to "1",
218+
"updateNestedSubmodules" to "false"
219+
)
220+
221+
val historyDepth: Int = getOrDefault(options, HISTORY_DEPTH).toInt()
222+
historyDepth shouldBe 1
223+
224+
val updateNestedSubmodules: Boolean = getOrDefault(options, UPDATE_NESTED_SUBMODULES).toBoolean()
225+
updateNestedSubmodules shouldBe false
226+
}
227+
228+
"return the default value if the option is not set" {
229+
val options: Options = emptyMap()
230+
231+
val historyDepth: Int = getOrDefault(options, HISTORY_DEPTH).toInt()
232+
historyDepth shouldBe 50
233+
234+
val updateNestedSubmodules: Boolean = getOrDefault(options, UPDATE_NESTED_SUBMODULES).toBoolean()
235+
updateNestedSubmodules shouldBe true
236+
}
237+
}
154238
})
155239

156240
private val TestUri = URIish(URI.create("https://www.example.org:8080/foo").toURL())

0 commit comments

Comments
 (0)