@@ -48,6 +48,9 @@ import org.ossreviewtoolkit.downloader.VersionControlSystem
4848import org.ossreviewtoolkit.downloader.WorkingTree
4949import org.ossreviewtoolkit.model.VcsInfo
5050import 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
5154import org.ossreviewtoolkit.utils.common.CommandLineTool
5255import org.ossreviewtoolkit.utils.common.Options
5356import org.ossreviewtoolkit.utils.common.Os
@@ -60,14 +63,50 @@ import org.ossreviewtoolkit.utils.ort.showStackTrace
6063import org.semver4j.RangesList
6164import 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.
6767private 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+
71110object 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
0 commit comments