Skip to content

Commit

Permalink
feat(vcs): Add Git-specific configuration options for submodule handling
Browse files Browse the repository at this point in the history
For large repositories with many layers of nested Git submodules, the
checkout 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 the commit history when checking out
the projects.

Signed-off-by: Wolfgang Klenk <wolfgang.klenk2@bosch.com>
Signed-off-by: Sebastian Schuberth <sebastian@doubleopen.org>
  • Loading branch information
wkl3nk authored and sschuberth committed Jan 13, 2025
1 parent 16c6f11 commit e4b94b1
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ private const val REPO_REV_FOR_VERSION = "371b23f37da064687518bace268d607a92ecbe
private const val REPO_PATH_FOR_VERSION = "specs"

class GitDownloadFunTest : StringSpec() {
private val git = Git()
private val git = Git.Factory().create()
private lateinit var outputDir: File

override suspend fun beforeTest(testCase: TestCase) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ private val tags = mapOf(
)

class GitFunTest : WordSpec({
val git = Git()
val git = Git.Factory().create()
val vcsInfo = VcsInfo(
type = VcsType.GIT,
url = "https://github.com/oss-review-toolkit/ort-test-data-git.git",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import org.ossreviewtoolkit.model.VcsInfo
import org.ossreviewtoolkit.model.VcsType

class GitWorkingTreeFunTest : StringSpec({
val git = Git()
val git = Git.Factory().create()
val repoDir = tempdir()
val vcsInfo = VcsInfo(
type = VcsType.GIT,
Expand Down
59 changes: 45 additions & 14 deletions plugins/version-control-systems/git/src/main/kotlin/Git.kt
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,7 @@ import org.ossreviewtoolkit.utils.ort.showStackTrace
import org.semver4j.RangesList
import org.semver4j.RangesListFactory

// TODO: Make this configurable.
const val GIT_HISTORY_DEPTH = 50
const val DEFAULT_HISTORY_DEPTH = 50

// Replace prefixes of Git submodule repository URLs.
private val REPOSITORY_URL_PREFIX_REPLACEMENTS = listOf(
Expand All @@ -85,7 +84,16 @@ object GitCommand : CommandLineTool {
override fun displayName(): String = "Git"
}

class Git internal constructor() : VersionControlSystem(GitCommand) {
/**
* This class provides functionality for interacting with Git repositories, utilizing either
* [JGit][org.eclipse.jgit.api.Git] or the Git CLI for executing operations.
*
* The following [configuration options][GitConfig] are available:
* - *historyDepth*: Depth of the commit history to fetch. Defaults to [DEFAULT_HISTORY_DEPTH].
* - *updateNestedSubmodules*: Whether nested submodules should be updated, or if only top-level submodules should be
* considered. Defaults to true.
*/
class Git internal constructor(private val config: GitConfig) : VersionControlSystem(GitCommand) {
companion object {
init {
// Make sure that JGit uses the exact same authentication information as ORT itself. This addresses
Expand Down Expand Up @@ -117,9 +125,14 @@ class Git internal constructor() : VersionControlSystem(GitCommand) {
}
}

class Factory : VersionControlSystemFactory<Unit>(VcsType.GIT.toString(), 100) {
override fun create(config: Unit) = Git()
override fun parseConfig(options: Options, secrets: Options) = Unit
class Factory : VersionControlSystemFactory<GitConfig>(VcsType.GIT.toString(), 100) {
override fun create(config: GitConfig) = Git(config)

override fun parseConfig(options: Options, secrets: Options) =
GitConfig(
historyDepth = options["historyDepth"]?.toIntOrNull() ?: DEFAULT_HISTORY_DEPTH,
updateNestedSubmodules = options["updateNestedSubmodules"]?.toBooleanStrictOrNull() ?: true
)
}

override val type = VcsType.GIT.toString()
Expand Down Expand Up @@ -190,15 +203,19 @@ class Git internal constructor() : VersionControlSystem(GitCommand) {
}
}

/**
* Update the [workingTree] using the [git] instance to a specific [revision] without updating any submodules. The
* configured [historyDepth][GitConfig.historyDepth] is respected.
*/
private fun updateWorkingTreeWithoutSubmodules(
workingTree: WorkingTree,
git: org.eclipse.jgit.api.Git,
revision: String
): Result<String> =
runCatching {
logger.info { "Trying to fetch only revision '$revision' with depth limited to $GIT_HISTORY_DEPTH." }
logger.info { "Trying to fetch only revision '$revision' with depth limited to ${config.historyDepth}." }

val fetch = git.fetch().setDepth(GIT_HISTORY_DEPTH)
val fetch = git.fetch().setDepth(config.historyDepth)

// See https://git-scm.com/docs/gitrevisions#_specifying_revisions for how Git resolves ambiguous
// names. In particular, tag names have higher precedence than branch names.
Expand All @@ -216,13 +233,13 @@ class Git internal constructor() : VersionControlSystem(GitCommand) {
it.showStackTrace()

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

git.fetch().setDepth(GIT_HISTORY_DEPTH).setTagOpt(TagOpt.FETCH_TAGS).call()
git.fetch().setDepth(config.historyDepth).setTagOpt(TagOpt.FETCH_TAGS).call()
}.recoverCatching {
it.showStackTrace()

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

git.fetch().setUnshallow(true).setTagOpt(TagOpt.FETCH_TAGS).call()
Expand Down Expand Up @@ -278,23 +295,37 @@ class Git internal constructor() : VersionControlSystem(GitCommand) {
revision
}

/**
* Initialize and / or update the submodules in [workingTree], limiting the commit history to the configured
* [historyDepth][GitConfig.historyDepth]. If [updateNestedSubmodules][GitConfig.updateNestedSubmodules] is true,
* submodules are initialized / updated recursively.
*/
private fun updateSubmodules(workingTree: WorkingTree) {
if (!workingTree.getRootPath().resolve(".gitmodules").isFile) return

val recursive = "--recursive".takeIf { config.updateNestedSubmodules }

val insteadOf = REPOSITORY_URL_PREFIX_REPLACEMENTS.map { (prefix, replacement) ->
"url.$replacement.insteadOf $prefix"
}

runCatching {
// TODO: Migrate this to JGit once https://bugs.eclipse.org/bugs/show_bug.cgi?id=580731 is implemented.
workingTree.runGit("submodule", "update", "--init", "--recursive", "--depth", "$GIT_HISTORY_DEPTH")
val updateArgs = listOfNotNull(
"submodule", "update", "--init", recursive, "--depth", "${config.historyDepth}"
)
workingTree.runGit(*updateArgs.toTypedArray())

insteadOf.forEach {
workingTree.runGit("submodule", "foreach", "--recursive", "git config $it")
val foreachArgs = listOfNotNull(
"submodule", "foreach", recursive, "git config $it"
)
workingTree.runGit(*foreachArgs.toTypedArray())
}
}.recover {
// As Git's dumb HTTP transport does not support shallow capabilities, also try to not limit the depth.
workingTree.runGit("submodule", "update", "--recursive")
val updateArgs = listOfNotNull("submodule", "update", recursive)
workingTree.runGit(*updateArgs.toTypedArray())
}
}

Expand Down
28 changes: 28 additions & 0 deletions plugins/version-control-systems/git/src/main/kotlin/GitConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright (C) 2025 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* License-Filename: LICENSE
*/

package org.ossreviewtoolkit.plugins.versioncontrolsystems.git

/** Git-specific [org.ossreviewtoolkit.downloader.VersionControlSystem] configuration. */
data class GitConfig(
/** Depth of the commit history to fetch. */
val historyDepth: Int,
/** Whether nested submodules should be updated, or if only top-level submodules should be considered. */
val updateNestedSubmodules: Boolean
)
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import org.ossreviewtoolkit.utils.ort.requestPasswordAuthentication

class GitTest : WordSpec({
// Make sure that the initialization logic runs.
val git = Git()
val git = Git.Factory().create()

var originalCredentialsProvider: CredentialsProvider? = null
var originalAuthenticator: Authenticator? = null
Expand Down

0 comments on commit e4b94b1

Please sign in to comment.