From b8efeb06b9abbb03692d0812407ecdb46f556798 Mon Sep 17 00:00:00 2001 From: Matthias Kurz Date: Wed, 1 Jul 2026 22:12:10 +0200 Subject: [PATCH 1/5] Prefer system git by default Add explicit backend settings for read-only metadata and git operations. Both now default to SystemGitFirst, which uses the system git executable when it is usable and falls back to JGit otherwise. Keep JGit 5 for the sbt 1 artifact and use JGit 7 for the sbt 2 artifact, where Java 17 is already required. This improves the JGit fallback and JGitOnly paths without raising the Java floor for sbt 1 users. Add SystemGitOnly and JGitOnly helpers for users who need deterministic behavior. Deprecate the older useJGit and useReadableConsoleGit aliases in favor of the new read and operation helpers. Keep the system git detection details internal. Cover the missing-system-git fallback and linked-worktree default paths with scripted tests. --- README.md | 46 +++--- build.sbt | 9 +- .../scala/com/github/sbt/git/GitBackend.scala | 26 ++++ .../scala/com/github/sbt/git/GitPlugin.scala | 133 ++++++++++++++++-- .../com/github/sbt/git/ReadableGit.scala | 9 +- .../jgit-only-sbt2-worktree/init.sh | 29 ++++ .../template-build/.gitignore | 4 + .../template-build/build.sbt | 39 +++++ .../template-build/project/plugins.sbt | 1 + .../jgit-only-sbt2-worktree/test | 6 + .../system-git-fallback/build.sbt | 14 ++ .../system-git-fallback/init.sh | 9 ++ .../project/TestHooks.scala | 8 ++ .../system-git-fallback/project/plugins.sbt | 1 + .../git-versioning/system-git-fallback/test | 2 + .../system-git-worktree/init.sh | 29 ++++ .../template-build/.gitignore | 4 + .../template-build/build.sbt | 30 ++++ .../template-build/project/plugins.sbt | 1 + .../git-versioning/system-git-worktree/test | 6 + .../com/github/sbt/git/SbtGitSuite.scala | 31 ++++ 21 files changed, 404 insertions(+), 33 deletions(-) create mode 100644 src/main/scala/com/github/sbt/git/GitBackend.scala create mode 100755 src/sbt-test/git-versioning/jgit-only-sbt2-worktree/init.sh create mode 100644 src/sbt-test/git-versioning/jgit-only-sbt2-worktree/template-build/.gitignore create mode 100644 src/sbt-test/git-versioning/jgit-only-sbt2-worktree/template-build/build.sbt create mode 100644 src/sbt-test/git-versioning/jgit-only-sbt2-worktree/template-build/project/plugins.sbt create mode 100644 src/sbt-test/git-versioning/jgit-only-sbt2-worktree/test create mode 100644 src/sbt-test/git-versioning/system-git-fallback/build.sbt create mode 100755 src/sbt-test/git-versioning/system-git-fallback/init.sh create mode 100644 src/sbt-test/git-versioning/system-git-fallback/project/TestHooks.scala create mode 100644 src/sbt-test/git-versioning/system-git-fallback/project/plugins.sbt create mode 100644 src/sbt-test/git-versioning/system-git-fallback/test create mode 100755 src/sbt-test/git-versioning/system-git-worktree/init.sh create mode 100644 src/sbt-test/git-versioning/system-git-worktree/template-build/.gitignore create mode 100644 src/sbt-test/git-versioning/system-git-worktree/template-build/build.sbt create mode 100644 src/sbt-test/git-versioning/system-git-worktree/template-build/project/plugins.sbt create mode 100644 src/sbt-test/git-versioning/system-git-worktree/test diff --git a/README.md b/README.md index 9bed22a..7370514 100644 --- a/README.md +++ b/README.md @@ -31,32 +31,44 @@ additionally, use one of the older README.md files: (https://github.com/sbt/sbt- JGit is a Java interface to git that allows some git operations to be performed in the JVM without invoking an external git executable. By default, -this plugin uses JGit for read-only operations such as inspecting HEAD; for -write operations, it assumes a git executable is present and on the PATH and -it uses that. +this plugin prefers the system `git` executable for both read-only metadata +such as inspecting HEAD and for explicit git operations such as clone, pull, +push, and the sbt `git` command. Read-only metadata falls back to JGit if +system git is not available or cannot read the build's Git metadata. Explicit +git operations fall back to JGit only when system git is not available. -In certain circumstances you may want to force the use of JGit or an -executable for both read-only and read-write operations; for example, if no -git executable is present (e.g. you use windows and you haven't installed git -or it's not on your PATH) you need to disable the console interface, or if -you rely on a git feature that JGit does not support (e.g. worktrees) you need -to disable the JGit interface. +In certain circumstances you may want deterministic behavior instead of the +default fallback. For example, CI can force system git so the build fails if +the executable is missing, while restricted environments can force JGit so the +plugin never shells out. -The following settings will force the use of only JGit or a git executable, -respectively: +The following settings control read-only operations: -* `useJGit` -* `useReadableConsoleGit` +* `useSystemGitFirstForReads` - prefer system git and fall back to JGit. This + is the default. +* `useSystemGitOnlyForReads` - use system git only. +* `useJGitOnlyForReads` - use JGit only. + +The following settings control explicit git operations: + +* `useSystemGitFirstForOperations` - prefer system git and fall back to JGit. + This is the default. +* `useSystemGitOnlyForOperations` - use system git only. +* `useJGitOnlyForOperations` - use JGit only. + +The older `useJGit` and `useReadableConsoleGit` helpers are deprecated aliases +for command-operation JGit and read-only system git behavior, respectively. These settings can be included in your project's `git.sbt` or in -`~/.sbt/1.0/git.sbt` -- for example, if no git executable is installed, -either file can have the following contents: +`~/.sbt/1.0/git.sbt` -- for example, to force JGit and prevent sbt-git from +shelling out, either file can have the following contents: - useJGit + useJGitOnlyForReads + useJGitOnlyForOperations Or you can `set` the appropriate setting in the sbt prompt: - > set useReadableConsoleGit + > set useSystemGitOnlyForReads [info] Reapplying settings... [info] Set current project to scala-arm (in build file:...) > session save diff --git a/build.sbt b/build.sbt index f673c9b..3f98b8b 100644 --- a/build.sbt +++ b/build.sbt @@ -15,8 +15,15 @@ crossScalaVersions := Seq(scala212, scala3) enablePlugins(GitVersioning, SbtPlugin) git.baseVersion := "1.0" +lazy val jgitVersion = Def.setting { + scalaBinaryVersion.value match { + case "2.12" => "5.13.5.202508271544-r" + case _ => "7.7.0.202606012155-r" + } +} + libraryDependencies ++= Seq( - "org.eclipse.jgit" % "org.eclipse.jgit" % "5.13.5.202508271544-r", + "org.eclipse.jgit" % "org.eclipse.jgit" % jgitVersion.value, "com.michaelpollmeier" % "versionsort" % "1.0.17", "org.scalameta" %% "munit" % "1.3.3" % Test ) diff --git a/src/main/scala/com/github/sbt/git/GitBackend.scala b/src/main/scala/com/github/sbt/git/GitBackend.scala new file mode 100644 index 0000000..beaea72 --- /dev/null +++ b/src/main/scala/com/github/sbt/git/GitBackend.scala @@ -0,0 +1,26 @@ +package com.github.sbt.git + +/** Selects which Git implementation sbt-git should use. + * + * System Git means the `git` executable available to the sbt process, + * normally through PATH. JGit means the Java implementation on the plugin + * classpath. + */ +sealed trait GitBackend extends Product with Serializable + +object GitBackend { + /** Prefer the system `git` executable and fall back to JGit if it is not + * available. + */ + case object SystemGitFirst extends GitBackend + + /** Always use the system `git` executable. Builds fail if it is not + * available or cannot run the requested operation. + */ + case object SystemGitOnly extends GitBackend + + /** Always use JGit. Builds fail if JGit cannot handle the requested + * repository layout or operation. + */ + case object JGitOnly extends GitBackend +} diff --git a/src/main/scala/com/github/sbt/git/GitPlugin.scala b/src/main/scala/com/github/sbt/git/GitPlugin.scala index 7f03d00..0c553a2 100644 --- a/src/main/scala/com/github/sbt/git/GitPlugin.scala +++ b/src/main/scala/com/github/sbt/git/GitPlugin.scala @@ -1,5 +1,7 @@ package com.github.sbt.git +import scala.util.Try + import sbt.* import Keys.* @@ -8,7 +10,6 @@ object SbtGit { object GitKeys { // Read-only git settings and values for use in other build settings. - // Note: These are all grabbed using jgit currently. val gitReader = SettingKey[ReadableGit]("git-reader", "This gives us a read-only view of the git repository.") val gitBranch = SettingKey[Option[String]]("git-branch", "Target branch of a git operation") val gitCurrentBranch = SettingKey[String]("git-current-branch", "The current branch for this project.") @@ -20,9 +21,24 @@ object SbtGit { val gitDescribedVersion = SettingKey[Option[String]]("git-described-version", "Version as returned by `git describe --tags`.") val gitUncommittedChanges = SettingKey[Boolean]("git-uncommitted-changes", "Whether there are uncommitted changes.") + @transient + val gitReadBackend = SettingKey[GitBackend]( + "git-read-backend", + "Selects the Git implementation used to read repository metadata such as HEAD, tags, branch, and dirty status." + ) + // A Mechanism to run Git directly. @transient val gitRunner = TaskKey[GitRunner]("git-runner", "The mechanism used to run git in the current build.") + @transient + val gitOperationBackend = SettingKey[GitBackend]( + "git-operation-backend", + "Selects the Git implementation used for explicit git operations such as clone, pull, push, and the sbt git command." + ) + private[sbt] val systemGitAvailableOverride = SettingKey[Option[Boolean]]( + "git-system-git-available-override", + "Internal test hook for overriding whether system git is detected." + ) // Keys associated with setting a version number. val useGitDescribe = SettingKey[Boolean]("use-git-describe", "Get version by calling `git describe` on the repository") @@ -44,8 +60,10 @@ object SbtGit { // The remote repository we're using. val gitRemoteRepo = SettingKey[String]("git-remote-repo", "The remote git repository associated with this project") - // Git worktree workaround - val useConsoleForROGit = SettingKey[Boolean]("console-ro-runner", "Whether to shell out to git for ro ops in the current build.") + val useConsoleForROGit = SettingKey[Boolean]( + "console-ro-runner", + "Deprecated compatibility setting that forces read-only operations to use the system git executable." + ) } object GitCommand { @@ -116,12 +134,26 @@ object SbtGit { // Build settings. import GitKeys.* def buildSettings = Seq( + gitReadBackend := GitBackend.SystemGitFirst, + gitOperationBackend := GitBackend.SystemGitFirst, + systemGitAvailableOverride := None, useConsoleForROGit := sys.env.contains("SBT_GIT_USE_CONSOLE_FOR_RO_GIT"), - gitReader := new DefaultReadableGit( - baseDirectory.value, - if (useConsoleForROGit.value) Some(new ConsoleGitReadableOnly(ConsoleGitRunner, file("."), sLog.value)) else None - ), - gitRunner := ConsoleGitRunner, + gitReader := { + val base = baseDirectory.value + val log = sLog.value + val available = detectedSystemGitAvailable(systemGitAvailableOverride.value, base, log) + val readable = available && isSystemGitReadable(base, log) + new DefaultReadableGit( + base, + if (useSystemGitForReads(useConsoleForROGit.value, gitReadBackend.value, readable)) + Some(new ConsoleGitReadableOnly(ConsoleGitRunner, base, log)) + else None + ) + }, + gitRunner := { + val available = detectedSystemGitAvailable(systemGitAvailableOverride.value, baseDirectory.value, sLog.value) + selectGitRunner(gitOperationBackend.value, available) + }, gitHeadCommit := gitReader.value.withGit(_.headCommitSha), gitHeadMessage := gitReader.value.withGit(_.headCommitMessage), gitHeadCommitDate := gitReader.value.withGit(_.headCommitDate), @@ -135,6 +167,29 @@ object SbtGit { ThisBuild / gitUncommittedChanges := gitReader.value.withGit(_.hasUncommittedChanges), scmInfo := parseScmInfo(gitReader.value.withGit(_.remoteOrigin)) ) + + private[sbt] def isSystemGitAvailable(dir: File, log: Logger): Boolean = + Try(ConsoleGitRunner("--version")(dir, log)).isSuccess + + private[sbt] def detectedSystemGitAvailable(overrideValue: Option[Boolean], dir: File, log: Logger): Boolean = + overrideValue.getOrElse(isSystemGitAvailable(dir, log)) + + private[sbt] def isSystemGitReadable(dir: File, log: Logger): Boolean = + Try(ConsoleGitRunner("rev-parse", "--git-dir")(dir, log)).isSuccess + + private[sbt] def useSystemGitForReads(forceSystemGit: Boolean, backend: GitBackend, systemGitAvailable: Boolean): Boolean = + forceSystemGit || useSystemGit(backend, systemGitAvailable) + + private[sbt] def selectGitRunner(backend: GitBackend, systemGitAvailable: Boolean): GitRunner = + if (useSystemGit(backend, systemGitAvailable)) ConsoleGitRunner else JGitRunner + + private[sbt] def useSystemGit(backend: GitBackend, systemGitAvailable: Boolean): Boolean = + backend match { + case GitBackend.SystemGitFirst => systemGitAvailable + case GitBackend.SystemGitOnly => true + case GitBackend.JGitOnly => false + } + private[sbt] def parseScmInfo(remoteOrigin: String): Option[ScmInfo] = { val user = """(?:[^@\/]+@)?""" val domain = """([^\/]+)""" @@ -175,10 +230,54 @@ object SbtGit { } ) + /** Use the system git executable for read-only repository metadata. + * + * This is deterministic: if system git is unavailable, the build fails + * instead of falling back to JGit. + */ + def useSystemGitOnlyForReads: Setting[?] = + ThisBuild / gitReadBackend := GitBackend.SystemGitOnly + + /** Use JGit for read-only repository metadata. + * + * This is deterministic: sbt-git will not shell out to system git for + * reads, even if JGit cannot handle the repository layout. + */ + def useJGitOnlyForReads: Setting[?] = + ThisBuild / gitReadBackend := GitBackend.JGitOnly + + /** Prefer system git for read-only repository metadata, falling back to JGit + * if system git is unavailable. This is the default. + */ + def useSystemGitFirstForReads: Setting[?] = + ThisBuild / gitReadBackend := GitBackend.SystemGitFirst + + /** Use the system git executable for explicit git operations. + * + * This is deterministic: if system git is unavailable, the build fails + * instead of falling back to JGit. + */ + def useSystemGitOnlyForOperations: Setting[?] = + ThisBuild / gitOperationBackend := GitBackend.SystemGitOnly + + /** Use JGit for explicit git operations such as clone, pull, push, and the + * sbt git command. + */ + def useJGitOnlyForOperations: Setting[?] = + ThisBuild / gitOperationBackend := GitBackend.JGitOnly + + /** Prefer system git for explicit git operations, falling back to JGit if + * system git is unavailable. This is the default. + */ + def useSystemGitFirstForOperations: Setting[?] = + ThisBuild / gitOperationBackend := GitBackend.SystemGitFirst + /** A Predefined setting to use JGit runner for git. */ - def useJGit: Setting[?] = ThisBuild / gitRunner := JGitRunner + @deprecated("Use useJGitOnlyForOperations instead.", "2.0.0") + def useJGit: Setting[?] = useJGitOnlyForOperations /** Setting to use console git for readable ops, to allow working with git worktrees */ + @deprecated("System git is now preferred for reads by default. Use useSystemGitOnlyForReads to force it.", "2.0.0") def useReadableConsoleGit: Setting[?] = ThisBuild / useConsoleForROGit := true /** Adapts the project prompt to show the current project name *and* the current git branch. */ @@ -267,7 +366,9 @@ object SbtGit { object git { val remoteRepo = GitKeys.gitRemoteRepo val branch = GitKeys.gitBranch + val readBackend = ThisBuild / GitKeys.gitReadBackend val runner = ThisBuild / GitKeys.gitRunner + val operationBackend = ThisBuild / GitKeys.gitOperationBackend val gitHeadCommit = ThisBuild / GitKeys.gitHeadCommit val gitHeadMessage = ThisBuild / GitKeys.gitHeadMessage val gitHeadCommitDate = ThisBuild / GitKeys.gitHeadCommitDate @@ -344,11 +445,21 @@ object GitPlugin extends AutoPlugin { // Note: In an attempt to pretend we are binary compatible, we current add this as an after thought. // In 1.0, we should deprecate/move the other means of getting these values. object autoImport { + type GitBackend = com.github.sbt.git.GitBackend + val GitBackend = com.github.sbt.git.GitBackend val git = SbtGit.git def versionWithGit = SbtGit.versionWithGit def versionProjectWithGit = SbtGit.versionProjectWithGit - def useJGit = SbtGit.useJGit - def useReadableConsoleGit = SbtGit.useReadableConsoleGit + def useSystemGitOnlyForReads = SbtGit.useSystemGitOnlyForReads + def useJGitOnlyForReads = SbtGit.useJGitOnlyForReads + def useSystemGitFirstForReads = SbtGit.useSystemGitFirstForReads + def useSystemGitOnlyForOperations = SbtGit.useSystemGitOnlyForOperations + def useJGitOnlyForOperations = SbtGit.useJGitOnlyForOperations + def useSystemGitFirstForOperations = SbtGit.useSystemGitFirstForOperations + @deprecated("Use useJGitOnlyForOperations instead.", "2.0.0") + def useJGit = SbtGit.useJGitOnlyForOperations + @deprecated("System git is now preferred for reads by default. Use useSystemGitOnlyForReads to force it.", "2.0.0") + def useReadableConsoleGit = SbtGit.useSystemGitOnlyForReads def showCurrentGitBranch = SbtGit.showCurrentGitBranch } override def buildSettings: Seq[Setting[?]] = SbtGit.buildSettings diff --git a/src/main/scala/com/github/sbt/git/ReadableGit.scala b/src/main/scala/com/github/sbt/git/ReadableGit.scala index 98d36a9..41f93ad 100644 --- a/src/main/scala/com/github/sbt/git/ReadableGit.scala +++ b/src/main/scala/com/github/sbt/git/ReadableGit.scala @@ -30,15 +30,16 @@ trait GitReadonlyInterface { def headCommitMessage: Option[String] } -/** Our default readable git uses JGit instead of a process-forking and reading, for speed/safety. However, we allow - * overriding, since JGit doesn't currently work with git worktrees - * */ +/** ReadableGit implementation backed by either the configured override, such + * as system git, or JGit as the fallback implementation. + */ final class DefaultReadableGit(base: sbt.File, gitOverride: Option[GitReadonlyInterface]) extends ReadableGit { // TODO - Should we cache git, or just create on each request? // For now, let's cache. private val git = gitOverride getOrElse JGit(base) /** Use the git read-only interface. */ def withGit[A](f: GitReadonlyInterface => A): A = - // JGit has concurrency issues so we synchronize access to it. + // JGit has concurrency issues, so keep access synchronized even when the + // configured backend is not JGit. synchronized(f(git)) } diff --git a/src/sbt-test/git-versioning/jgit-only-sbt2-worktree/init.sh b/src/sbt-test/git-versioning/jgit-only-sbt2-worktree/init.sh new file mode 100755 index 0000000..2325ead --- /dev/null +++ b/src/sbt-test/git-versioning/jgit-only-sbt2-worktree/init.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +set -eux + +# Build this scripted layout: +# +# . +# |-- main/ primary Git repository +# `-- project/ linked worktree containing the sbt build under test +# +# The directory name "project" is intentional. In sbt, `project/` is the +# meta-build of the scripted root. The scripted test runs `reload plugins` +# so subsequent commands execute from inside this linked worktree. +mkdir -p main + +cd main +git init +git config user.email "test@jsuereth.com" +git config user.name "Tester" +git commit --allow-empty --no-gpg-sign -m "Initial commit" +rm -rf ../project +git worktree add -b wt ../project +cd - + +cp -r template-build/. project/ +cd project +git add .gitignore build.sbt project/plugins.sbt +git commit --no-gpg-sign -m "Add scripted project" +echo "# dirty" >> .gitignore diff --git a/src/sbt-test/git-versioning/jgit-only-sbt2-worktree/template-build/.gitignore b/src/sbt-test/git-versioning/jgit-only-sbt2-worktree/template-build/.gitignore new file mode 100644 index 0000000..f2b6ab5 --- /dev/null +++ b/src/sbt-test/git-versioning/jgit-only-sbt2-worktree/template-build/.gitignore @@ -0,0 +1,4 @@ +target +project/project +project/target +*.class diff --git a/src/sbt-test/git-versioning/jgit-only-sbt2-worktree/template-build/build.sbt b/src/sbt-test/git-versioning/jgit-only-sbt2-worktree/template-build/build.sbt new file mode 100644 index 0000000..13ac864 --- /dev/null +++ b/src/sbt-test/git-versioning/jgit-only-sbt2-worktree/template-build/build.sbt @@ -0,0 +1,39 @@ +enablePlugins(GitVersioning) + +ThisBuild / com.github.sbt.git.SbtGit.GitKeys.gitReadBackend := { + if (sbtVersion.value.startsWith("2.")) com.github.sbt.git.GitBackend.JGitOnly + else com.github.sbt.git.GitBackend.SystemGitFirst +} + +val expectJGitOnlyBackend = + taskKey[Unit]("checks the forced JGit read backend in a linked worktree") + +expectJGitOnlyBackend := { + if (sbtVersion.value.startsWith("2.")) { + val expectedReadBackend = com.github.sbt.git.GitBackend.JGitOnly + val actualReadBackend = + (ThisBuild / com.github.sbt.git.SbtGit.GitKeys.gitReadBackend).value + assert( + actualReadBackend == expectedReadBackend, + s"Expected gitReadBackend=$expectedReadBackend, found $actualReadBackend" + ) + } +} + +val expectWorktreeReadableValues = + taskKey[Unit]("checks JGit readable git values in a linked worktree") + +expectWorktreeReadableValues := { + if (sbtVersion.value.startsWith("2.")) { + assert(git.gitHeadCommit.value.nonEmpty, "Expected gitHeadCommit to be defined") + assert( + git.gitHeadMessage.value.map(_.trim).contains("Add scripted project"), + s"Unexpected head message: ${git.gitHeadMessage.value}" + ) + assert( + git.gitCurrentBranch.value == "wt", + s"Unexpected current branch: ${git.gitCurrentBranch.value}" + ) + assert(git.gitUncommittedChanges.value, "Expected dirty linked worktree") + } +} diff --git a/src/sbt-test/git-versioning/jgit-only-sbt2-worktree/template-build/project/plugins.sbt b/src/sbt-test/git-versioning/jgit-only-sbt2-worktree/template-build/project/plugins.sbt new file mode 100644 index 0000000..5510398 --- /dev/null +++ b/src/sbt-test/git-versioning/jgit-only-sbt2-worktree/template-build/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("com.github.sbt" % "sbt-git" % sys.props("project.version")) diff --git a/src/sbt-test/git-versioning/jgit-only-sbt2-worktree/test b/src/sbt-test/git-versioning/jgit-only-sbt2-worktree/test new file mode 100644 index 0000000..4f5ef92 --- /dev/null +++ b/src/sbt-test/git-versioning/jgit-only-sbt2-worktree/test @@ -0,0 +1,6 @@ +$ exec ./init.sh +# Switch to the scripted root's meta-build. That meta-build is the generated +# project/ directory, which init.sh made a linked Git worktree. +> reload plugins +> expectJGitOnlyBackend +> expectWorktreeReadableValues diff --git a/src/sbt-test/git-versioning/system-git-fallback/build.sbt b/src/sbt-test/git-versioning/system-git-fallback/build.sbt new file mode 100644 index 0000000..aae6bd1 --- /dev/null +++ b/src/sbt-test/git-versioning/system-git-fallback/build.sbt @@ -0,0 +1,14 @@ +enablePlugins(GitVersioning) + +com.github.sbt.TestHooks.forceSystemGitUnavailable + +val expectJGitFallback = taskKey[Unit]("checks SystemGitFirst falls back to JGit when system git is unavailable") +expectJGitFallback := { + val readBackend = (ThisBuild / com.github.sbt.git.SbtGit.GitKeys.gitReadBackend).value + val operationBackend = (ThisBuild / com.github.sbt.git.SbtGit.GitKeys.gitOperationBackend).value + val runner = (ThisBuild / com.github.sbt.git.SbtGit.GitKeys.gitRunner).value + + assert(readBackend == com.github.sbt.git.GitBackend.SystemGitFirst, s"Unexpected read backend: $readBackend") + assert(operationBackend == com.github.sbt.git.GitBackend.SystemGitFirst, s"Unexpected operation backend: $operationBackend") + assert(runner == com.github.sbt.git.JGitRunner, s"Expected JGitRunner fallback, found $runner") +} diff --git a/src/sbt-test/git-versioning/system-git-fallback/init.sh b/src/sbt-test/git-versioning/system-git-fallback/init.sh new file mode 100755 index 0000000..e205c3d --- /dev/null +++ b/src/sbt-test/git-versioning/system-git-fallback/init.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -eux + +git init +git config user.email "test@jsuereth.com" +git config user.name "Tester" +git add build.sbt project/plugins.sbt +git commit --no-gpg-sign -m "Add scripted project" diff --git a/src/sbt-test/git-versioning/system-git-fallback/project/TestHooks.scala b/src/sbt-test/git-versioning/system-git-fallback/project/TestHooks.scala new file mode 100644 index 0000000..4e6a80c --- /dev/null +++ b/src/sbt-test/git-versioning/system-git-fallback/project/TestHooks.scala @@ -0,0 +1,8 @@ +package com.github.sbt + +import com.github.sbt.git.SbtGit + +object TestHooks { + val forceSystemGitUnavailable = + _root_.sbt.ThisBuild / SbtGit.GitKeys.systemGitAvailableOverride := Some(false) +} diff --git a/src/sbt-test/git-versioning/system-git-fallback/project/plugins.sbt b/src/sbt-test/git-versioning/system-git-fallback/project/plugins.sbt new file mode 100644 index 0000000..5510398 --- /dev/null +++ b/src/sbt-test/git-versioning/system-git-fallback/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("com.github.sbt" % "sbt-git" % sys.props("project.version")) diff --git a/src/sbt-test/git-versioning/system-git-fallback/test b/src/sbt-test/git-versioning/system-git-fallback/test new file mode 100644 index 0000000..4c3fd30 --- /dev/null +++ b/src/sbt-test/git-versioning/system-git-fallback/test @@ -0,0 +1,2 @@ +$ exec ./init.sh +> expectJGitFallback diff --git a/src/sbt-test/git-versioning/system-git-worktree/init.sh b/src/sbt-test/git-versioning/system-git-worktree/init.sh new file mode 100755 index 0000000..2325ead --- /dev/null +++ b/src/sbt-test/git-versioning/system-git-worktree/init.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +set -eux + +# Build this scripted layout: +# +# . +# |-- main/ primary Git repository +# `-- project/ linked worktree containing the sbt build under test +# +# The directory name "project" is intentional. In sbt, `project/` is the +# meta-build of the scripted root. The scripted test runs `reload plugins` +# so subsequent commands execute from inside this linked worktree. +mkdir -p main + +cd main +git init +git config user.email "test@jsuereth.com" +git config user.name "Tester" +git commit --allow-empty --no-gpg-sign -m "Initial commit" +rm -rf ../project +git worktree add -b wt ../project +cd - + +cp -r template-build/. project/ +cd project +git add .gitignore build.sbt project/plugins.sbt +git commit --no-gpg-sign -m "Add scripted project" +echo "# dirty" >> .gitignore diff --git a/src/sbt-test/git-versioning/system-git-worktree/template-build/.gitignore b/src/sbt-test/git-versioning/system-git-worktree/template-build/.gitignore new file mode 100644 index 0000000..f2b6ab5 --- /dev/null +++ b/src/sbt-test/git-versioning/system-git-worktree/template-build/.gitignore @@ -0,0 +1,4 @@ +target +project/project +project/target +*.class diff --git a/src/sbt-test/git-versioning/system-git-worktree/template-build/build.sbt b/src/sbt-test/git-versioning/system-git-worktree/template-build/build.sbt new file mode 100644 index 0000000..fc24c9d --- /dev/null +++ b/src/sbt-test/git-versioning/system-git-worktree/template-build/build.sbt @@ -0,0 +1,30 @@ +enablePlugins(GitVersioning) + +val expectSystemGitFirstBackend = + taskKey[Unit]("checks the default read backend in a linked worktree") + +expectSystemGitFirstBackend := { + val expectedReadBackend = com.github.sbt.git.GitBackend.SystemGitFirst + val actualReadBackend = + (ThisBuild / com.github.sbt.git.SbtGit.GitKeys.gitReadBackend).value + assert( + actualReadBackend == expectedReadBackend, + s"Expected gitReadBackend=$expectedReadBackend, found $actualReadBackend" + ) +} + +val expectWorktreeReadableValues = + taskKey[Unit]("checks readable git values in a linked worktree") + +expectWorktreeReadableValues := { + assert(git.gitHeadCommit.value.nonEmpty, "Expected gitHeadCommit to be defined") + assert( + git.gitHeadMessage.value.map(_.trim).contains("Add scripted project"), + s"Unexpected head message: ${git.gitHeadMessage.value}" + ) + assert( + git.gitCurrentBranch.value == "wt", + s"Unexpected current branch: ${git.gitCurrentBranch.value}" + ) + assert(git.gitUncommittedChanges.value, "Expected dirty linked worktree") +} diff --git a/src/sbt-test/git-versioning/system-git-worktree/template-build/project/plugins.sbt b/src/sbt-test/git-versioning/system-git-worktree/template-build/project/plugins.sbt new file mode 100644 index 0000000..5510398 --- /dev/null +++ b/src/sbt-test/git-versioning/system-git-worktree/template-build/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("com.github.sbt" % "sbt-git" % sys.props("project.version")) diff --git a/src/sbt-test/git-versioning/system-git-worktree/test b/src/sbt-test/git-versioning/system-git-worktree/test new file mode 100644 index 0000000..1dbec11 --- /dev/null +++ b/src/sbt-test/git-versioning/system-git-worktree/test @@ -0,0 +1,6 @@ +$ exec ./init.sh +# Switch to the scripted root's meta-build. That meta-build is the generated +# project/ directory, which init.sh made a linked Git worktree. +> reload plugins +> expectSystemGitFirstBackend +> expectWorktreeReadableValues diff --git a/src/test/scala/com/github/sbt/git/SbtGitSuite.scala b/src/test/scala/com/github/sbt/git/SbtGitSuite.scala index 2cc0f59..f959376 100644 --- a/src/test/scala/com/github/sbt/git/SbtGitSuite.scala +++ b/src/test/scala/com/github/sbt/git/SbtGitSuite.scala @@ -24,4 +24,35 @@ class SbtGitSuite extends munit.FunSuite { test("a https URL without the .git postfix") { assertEquals(SbtGit.parseScmInfo("https://github.com/akka/akka"), expectedScmInfo) } + + test("SystemGitFirst uses system git only when it is available") { + assert(SbtGit.useSystemGit(GitBackend.SystemGitFirst, systemGitAvailable = true)) + assertEquals(SbtGit.useSystemGit(GitBackend.SystemGitFirst, systemGitAvailable = false), false) + } + + test("SystemGitOnly always selects system git") { + assert(SbtGit.useSystemGit(GitBackend.SystemGitOnly, systemGitAvailable = true)) + assert(SbtGit.useSystemGit(GitBackend.SystemGitOnly, systemGitAvailable = false)) + } + + test("JGitOnly never selects system git") { + assertEquals(SbtGit.useSystemGit(GitBackend.JGitOnly, systemGitAvailable = true), false) + assertEquals(SbtGit.useSystemGit(GitBackend.JGitOnly, systemGitAvailable = false), false) + } + + test("legacy read-only console flag forces system git reads") { + assert(SbtGit.useSystemGitForReads(forceSystemGit = true, GitBackend.JGitOnly, systemGitAvailable = false)) + } + + test("system git availability override takes precedence over detection") { + assert(SbtGit.detectedSystemGitAvailable(Some(true), sbt.file("."), sbt.util.Logger.Null)) + assertEquals(SbtGit.detectedSystemGitAvailable(Some(false), sbt.file("."), sbt.util.Logger.Null), false) + } + + test("operation backend selects the expected runner") { + assertEquals(SbtGit.selectGitRunner(GitBackend.SystemGitFirst, systemGitAvailable = true), ConsoleGitRunner) + assertEquals(SbtGit.selectGitRunner(GitBackend.SystemGitFirst, systemGitAvailable = false), JGitRunner) + assertEquals(SbtGit.selectGitRunner(GitBackend.SystemGitOnly, systemGitAvailable = false), ConsoleGitRunner) + assertEquals(SbtGit.selectGitRunner(GitBackend.JGitOnly, systemGitAvailable = true), JGitRunner) + } } From 96a48a675ad3c9ed763b88b874b063d7a8da4fe8 Mon Sep 17 00:00:00 2001 From: Matthias Kurz Date: Thu, 2 Jul 2026 14:41:39 +0200 Subject: [PATCH 2/5] Align system git read semantics Keep the console read backend closer to the previous JGit behavior. It now handles unborn repositories without failing during project load, treats empty command output as an empty collection, and ignores untracked files when checking whether tracked sources are dirty. Add unit coverage for the unborn repository and untracked-file cases that caused Java 8 scripted failures after making system git the default read backend. --- .../sbt/git/ConsoleGitReadableOnly.scala | 32 ++++++++++----- .../com/github/sbt/git/SbtGitSuite.scala | 39 +++++++++++++++++++ 2 files changed, 61 insertions(+), 10 deletions(-) diff --git a/src/main/scala/com/github/sbt/git/ConsoleGitReadableOnly.scala b/src/main/scala/com/github/sbt/git/ConsoleGitReadableOnly.scala index 0df62b0..e904c15 100644 --- a/src/main/scala/com/github/sbt/git/ConsoleGitReadableOnly.scala +++ b/src/main/scala/com/github/sbt/git/ConsoleGitReadableOnly.scala @@ -5,28 +5,40 @@ import scala.util.Try import sbt.{File, Logger} class ConsoleGitReadableOnly(git: GitRunner, cwd: File, log: Logger) extends GitReadonlyInterface { - def branch: String = git("rev-parse", "--abbrev-ref", "HEAD")(cwd, log) + private def splitOutput(output: String): Seq[String] = + output.split("\\s+").toSeq.filter(_.nonEmpty) - def headCommitSha: Option[String] = Try(git("rev-parse", "HEAD")(cwd, log)).toOption + def branch: String = + Try(git("symbolic-ref", "--short", "-q", "HEAD")(cwd, log)) + .orElse(Try(git("rev-parse", "--abbrev-ref", "HEAD")(cwd, log))) + .getOrElse("") - def headCommitDate: Option[String] = Try(git("log", """--pretty="%cI"""", "-n", "1")(cwd, log)).toOption + def headCommitSha: Option[String] = Try(git("rev-parse", "--verify", "--quiet", "HEAD")(cwd, log)).toOption - def currentTags: Seq[String] = Try(git("tag", "--points-at", "HEAD")(cwd, log).split("\\s+").toSeq).getOrElse(Seq()) + def headCommitDate: Option[String] = + headCommitSha.flatMap(_ => Try(git("log", """--pretty="%cI"""", "-n", "1")(cwd, log)).toOption) - def describedVersion: Option[String] = Try(git("describe", "--tags")(cwd, log).split("\\s+").headOption).toOption.flatten + def currentTags: Seq[String] = + headCommitSha + .map(_ => Try(splitOutput(git("tag", "--points-at", "HEAD")(cwd, log))).getOrElse(Seq())) + .getOrElse(Seq()) + + def describedVersion: Option[String] = + headCommitSha.flatMap(_ => Try(splitOutput(git("describe", "--tags")(cwd, log)).headOption).toOption.flatten) override def describedVersion(patterns: Seq[String]): Option[String] = patterns.headOption.fold(describedVersion)(pat => - Try(git("describe", "--tags", "--match", pat)(cwd, log).split("\\s+").headOption).toOption.flatten + headCommitSha.flatMap(_ => Try(splitOutput(git("describe", "--tags", "--match", pat)(cwd, log)).headOption).toOption.flatten) ) - def hasUncommittedChanges: Boolean = Try(!git("status", "-s")(cwd, log).trim.isEmpty).getOrElse(true) + def hasUncommittedChanges: Boolean = Try(!git("status", "-s", "--untracked-files=no")(cwd, log).trim.isEmpty).getOrElse(true) - def branches: Seq[String] = Try(git("branch", "--list")(cwd, log).split("\\s+").toSeq).getOrElse(Seq()) + def branches: Seq[String] = Try(splitOutput(git("branch", "--list")(cwd, log))).getOrElse(Seq()) - def remoteBranches: Seq[String] = Try(git("branch", "-l", "--remotes")(cwd, log).split("\\s+").toSeq).getOrElse(Seq()) + def remoteBranches: Seq[String] = Try(splitOutput(git("branch", "-l", "--remotes")(cwd, log))).getOrElse(Seq()) def remoteOrigin: String = git("ls-remote", "--get-url", "origin")(cwd, log) - def headCommitMessage: Option[String] = Try(git("log", "--pretty=%s\n\n%b", "-n", "1")(cwd, log)).toOption + def headCommitMessage: Option[String] = + headCommitSha.flatMap(_ => Try(git("log", "--pretty=%B", "-n", "1")(cwd, log)).toOption) } diff --git a/src/test/scala/com/github/sbt/git/SbtGitSuite.scala b/src/test/scala/com/github/sbt/git/SbtGitSuite.scala index f959376..c218c5d 100644 --- a/src/test/scala/com/github/sbt/git/SbtGitSuite.scala +++ b/src/test/scala/com/github/sbt/git/SbtGitSuite.scala @@ -1,8 +1,12 @@ package com.github.sbt.git import sbt.ScmInfo +import sbt.IO +import sbt.io.syntax.* import sbt.url +import scala.sys.process.Process + class SbtGitSuite extends munit.FunSuite { val expectedScmInfo = Some( ScmInfo( @@ -55,4 +59,39 @@ class SbtGitSuite extends munit.FunSuite { assertEquals(SbtGit.selectGitRunner(GitBackend.SystemGitOnly, systemGitAvailable = false), ConsoleGitRunner) assertEquals(SbtGit.selectGitRunner(GitBackend.JGitOnly, systemGitAvailable = true), JGitRunner) } + + test("console reader handles a repository without commits") { + IO.withTemporaryDirectory { dir => + runGit(dir, "init") + + val reader = new ConsoleGitReadableOnly(ConsoleGitRunner, dir, sbt.util.Logger.Null) + assert(reader.branch.nonEmpty) + assertEquals(reader.headCommitSha, None) + assertEquals(reader.currentTags, Seq.empty) + assertEquals(reader.headCommitMessage, None) + } + } + + test("console reader dirty check ignores untracked files") { + IO.withTemporaryDirectory { dir => + runGit(dir, "init") + runGit(dir, "config", "user.email", "test@example.com") + runGit(dir, "config", "user.name", "Tester") + IO.write(dir / "README.md", "clean\n") + runGit(dir, "add", "README.md") + runGit(dir, "commit", "--no-gpg-sign", "-m", "Initial commit") + + val reader = new ConsoleGitReadableOnly(ConsoleGitRunner, dir, sbt.util.Logger.Null) + IO.write(dir / "build.sbt", "untracked\n") + assertEquals(reader.hasUncommittedChanges, false) + + IO.write(dir / "README.md", "dirty\n") + assert(reader.hasUncommittedChanges) + } + } + + private def runGit(dir: sbt.File, args: String*): Unit = { + val exitCode = Process("git" +: args, dir).! + assertEquals(exitCode, 0) + } } From 3ae60cfc92ec8241990767115dd4458fa855b844 Mon Sep 17 00:00:00 2001 From: Matthias Kurz Date: Thu, 2 Jul 2026 14:54:01 +0200 Subject: [PATCH 3/5] Tighten system git reader behavior Bring the console read-only backend closer to the behavior exposed by JGit. Use Git formatting options for branch names, pass all describe match patterns, avoid shell-style quotes in commit dates, and keep the same origin fallback when no remote is configured. Add unit coverage for these read operations so future changes to the system git backend keep the public read API stable. --- .../sbt/git/ConsoleGitReadableOnly.scala | 15 +++--- .../com/github/sbt/git/SbtGitSuite.scala | 52 +++++++++++++++++++ 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/src/main/scala/com/github/sbt/git/ConsoleGitReadableOnly.scala b/src/main/scala/com/github/sbt/git/ConsoleGitReadableOnly.scala index e904c15..0128992 100644 --- a/src/main/scala/com/github/sbt/git/ConsoleGitReadableOnly.scala +++ b/src/main/scala/com/github/sbt/git/ConsoleGitReadableOnly.scala @@ -8,6 +8,9 @@ class ConsoleGitReadableOnly(git: GitRunner, cwd: File, log: Logger) extends Git private def splitOutput(output: String): Seq[String] = output.split("\\s+").toSeq.filter(_.nonEmpty) + private def describeArgs(patterns: Seq[String]): Seq[String] = + Seq("describe", "--tags") ++ patterns.flatMap(Seq("--match", _)) + def branch: String = Try(git("symbolic-ref", "--short", "-q", "HEAD")(cwd, log)) .orElse(Try(git("rev-parse", "--abbrev-ref", "HEAD")(cwd, log))) @@ -16,7 +19,7 @@ class ConsoleGitReadableOnly(git: GitRunner, cwd: File, log: Logger) extends Git def headCommitSha: Option[String] = Try(git("rev-parse", "--verify", "--quiet", "HEAD")(cwd, log)).toOption def headCommitDate: Option[String] = - headCommitSha.flatMap(_ => Try(git("log", """--pretty="%cI"""", "-n", "1")(cwd, log)).toOption) + headCommitSha.flatMap(_ => Try(git("log", "--date=format:%Y-%m-%dT%H:%M:%S%z", "--pretty=%cd", "-n", "1")(cwd, log)).toOption) def currentTags: Seq[String] = headCommitSha @@ -27,17 +30,17 @@ class ConsoleGitReadableOnly(git: GitRunner, cwd: File, log: Logger) extends Git headCommitSha.flatMap(_ => Try(splitOutput(git("describe", "--tags")(cwd, log)).headOption).toOption.flatten) override def describedVersion(patterns: Seq[String]): Option[String] = - patterns.headOption.fold(describedVersion)(pat => - headCommitSha.flatMap(_ => Try(splitOutput(git("describe", "--tags", "--match", pat)(cwd, log)).headOption).toOption.flatten) + patterns.headOption.fold(describedVersion)(_ => + headCommitSha.flatMap(_ => Try(splitOutput(git(describeArgs(patterns)*)(cwd, log)).headOption).toOption.flatten) ) def hasUncommittedChanges: Boolean = Try(!git("status", "-s", "--untracked-files=no")(cwd, log).trim.isEmpty).getOrElse(true) - def branches: Seq[String] = Try(splitOutput(git("branch", "--list")(cwd, log))).getOrElse(Seq()) + def branches: Seq[String] = Try(splitOutput(git("branch", "--list", "--format=%(refname:short)")(cwd, log))).getOrElse(Seq()) - def remoteBranches: Seq[String] = Try(splitOutput(git("branch", "-l", "--remotes")(cwd, log))).getOrElse(Seq()) + def remoteBranches: Seq[String] = Try(splitOutput(git("branch", "--remotes", "--format=%(refname:short)")(cwd, log))).getOrElse(Seq()) - def remoteOrigin: String = git("ls-remote", "--get-url", "origin")(cwd, log) + def remoteOrigin: String = Try(git("ls-remote", "--get-url", "origin")(cwd, log)).getOrElse("origin") def headCommitMessage: Option[String] = headCommitSha.flatMap(_ => Try(git("log", "--pretty=%B", "-n", "1")(cwd, log)).toOption) diff --git a/src/test/scala/com/github/sbt/git/SbtGitSuite.scala b/src/test/scala/com/github/sbt/git/SbtGitSuite.scala index c218c5d..ccd3e2b 100644 --- a/src/test/scala/com/github/sbt/git/SbtGitSuite.scala +++ b/src/test/scala/com/github/sbt/git/SbtGitSuite.scala @@ -90,6 +90,58 @@ class SbtGitSuite extends munit.FunSuite { } } + test("console reader returns undecorated branch names") { + IO.withTemporaryDirectory { dir => + initCommittedRepo(dir) + runGit(dir, "branch", "feature") + runGit(dir, "update-ref", "refs/remotes/origin/main", "HEAD") + + val reader = new ConsoleGitReadableOnly(ConsoleGitRunner, dir, sbt.util.Logger.Null) + assert(reader.branches.contains("feature")) + assert(!reader.branches.contains("*")) + assert(reader.remoteBranches.contains("origin/main")) + assert(!reader.remoteBranches.contains("*")) + } + } + + test("console reader applies all describe match patterns") { + IO.withTemporaryDirectory { dir => + initCommittedRepo(dir) + runGit(dir, "tag", "release-1.0") + + val reader = new ConsoleGitReadableOnly(ConsoleGitRunner, dir, sbt.util.Logger.Null) + assertEquals(reader.describedVersion(Seq("ignored-*", "release-*")), Some("release-1.0")) + } + } + + test("console reader commit date is not shell quoted") { + IO.withTemporaryDirectory { dir => + initCommittedRepo(dir) + + val reader = new ConsoleGitReadableOnly(ConsoleGitRunner, dir, sbt.util.Logger.Null) + val date = reader.headCommitDate.getOrElse(fail("expected a commit date")) + assert(date.matches("""\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{4}""")) + } + } + + test("console reader falls back to origin when no origin remote is configured") { + IO.withTemporaryDirectory { dir => + initCommittedRepo(dir) + + val reader = new ConsoleGitReadableOnly(ConsoleGitRunner, dir, sbt.util.Logger.Null) + assertEquals(reader.remoteOrigin, "origin") + } + } + + private def initCommittedRepo(dir: sbt.File): Unit = { + runGit(dir, "init") + runGit(dir, "config", "user.email", "test@example.com") + runGit(dir, "config", "user.name", "Tester") + IO.write(dir / "README.md", "initial\n") + runGit(dir, "add", "README.md") + runGit(dir, "commit", "--no-gpg-sign", "-m", "Initial commit") + } + private def runGit(dir: sbt.File, args: String*): Unit = { val exitCode = Process("git" +: args, dir).! assertEquals(exitCode, 0) From 759bbb07402ce31bfba40651d369164b952db57b Mon Sep 17 00:00:00 2001 From: Matthias Kurz Date: Mon, 6 Jul 2026 12:54:21 +0200 Subject: [PATCH 4/5] Preserve legacy backend helpers Keep useReadableConsoleGit as a non-deprecated compatibility helper that preserves the existing read-only console flag. This avoids warning users who had already opted into system git reads. Make useJGit express the broader legacy intent by selecting JGit for both read-only metadata and explicit git operations. Add scripted coverage for both legacy helpers and update the README. --- README.md | 9 ++++--- .../scala/com/github/sbt/git/GitPlugin.scala | 13 ++++----- .../legacy-backend-helpers/build.sbt | 27 +++++++++++++++++++ .../project/plugins.sbt | 1 + .../legacy-backend-helpers/test | 1 + .../legacy-readable-console-helper/build.sbt | 16 +++++++++++ .../project/plugins.sbt | 1 + .../legacy-readable-console-helper/test | 1 + 8 files changed, 57 insertions(+), 12 deletions(-) create mode 100644 src/sbt-test/git-versioning/legacy-backend-helpers/build.sbt create mode 100644 src/sbt-test/git-versioning/legacy-backend-helpers/project/plugins.sbt create mode 100644 src/sbt-test/git-versioning/legacy-backend-helpers/test create mode 100644 src/sbt-test/git-versioning/legacy-readable-console-helper/build.sbt create mode 100644 src/sbt-test/git-versioning/legacy-readable-console-helper/project/plugins.sbt create mode 100644 src/sbt-test/git-versioning/legacy-readable-console-helper/test diff --git a/README.md b/README.md index 7370514..6872284 100644 --- a/README.md +++ b/README.md @@ -56,15 +56,16 @@ The following settings control explicit git operations: * `useSystemGitOnlyForOperations` - use system git only. * `useJGitOnlyForOperations` - use JGit only. -The older `useJGit` and `useReadableConsoleGit` helpers are deprecated aliases -for command-operation JGit and read-only system git behavior, respectively. +The older `useJGit` helper remains available and now selects JGit for both +read-only metadata and explicit git operations. The older +`useReadableConsoleGit` helper remains available as a compatibility alias for +forcing read-only metadata to use system git. These settings can be included in your project's `git.sbt` or in `~/.sbt/1.0/git.sbt` -- for example, to force JGit and prevent sbt-git from shelling out, either file can have the following contents: - useJGitOnlyForReads - useJGitOnlyForOperations + useJGit Or you can `set` the appropriate setting in the sbt prompt: diff --git a/src/main/scala/com/github/sbt/git/GitPlugin.scala b/src/main/scala/com/github/sbt/git/GitPlugin.scala index 0c553a2..6ad9026 100644 --- a/src/main/scala/com/github/sbt/git/GitPlugin.scala +++ b/src/main/scala/com/github/sbt/git/GitPlugin.scala @@ -272,12 +272,11 @@ object SbtGit { def useSystemGitFirstForOperations: Setting[?] = ThisBuild / gitOperationBackend := GitBackend.SystemGitFirst - /** A Predefined setting to use JGit runner for git. */ - @deprecated("Use useJGitOnlyForOperations instead.", "2.0.0") - def useJGit: Setting[?] = useJGitOnlyForOperations + /** Predefined settings to use JGit for reads and explicit git operations. */ + def useJGit: Seq[Setting[?]] = + Seq(useJGitOnlyForReads, useJGitOnlyForOperations) /** Setting to use console git for readable ops, to allow working with git worktrees */ - @deprecated("System git is now preferred for reads by default. Use useSystemGitOnlyForReads to force it.", "2.0.0") def useReadableConsoleGit: Setting[?] = ThisBuild / useConsoleForROGit := true /** Adapts the project prompt to show the current project name *and* the current git branch. */ @@ -456,10 +455,8 @@ object GitPlugin extends AutoPlugin { def useSystemGitOnlyForOperations = SbtGit.useSystemGitOnlyForOperations def useJGitOnlyForOperations = SbtGit.useJGitOnlyForOperations def useSystemGitFirstForOperations = SbtGit.useSystemGitFirstForOperations - @deprecated("Use useJGitOnlyForOperations instead.", "2.0.0") - def useJGit = SbtGit.useJGitOnlyForOperations - @deprecated("System git is now preferred for reads by default. Use useSystemGitOnlyForReads to force it.", "2.0.0") - def useReadableConsoleGit = SbtGit.useSystemGitOnlyForReads + def useJGit = SbtGit.useJGit + def useReadableConsoleGit = SbtGit.useReadableConsoleGit def showCurrentGitBranch = SbtGit.showCurrentGitBranch } override def buildSettings: Seq[Setting[?]] = SbtGit.buildSettings diff --git a/src/sbt-test/git-versioning/legacy-backend-helpers/build.sbt b/src/sbt-test/git-versioning/legacy-backend-helpers/build.sbt new file mode 100644 index 0000000..f9d030a --- /dev/null +++ b/src/sbt-test/git-versioning/legacy-backend-helpers/build.sbt @@ -0,0 +1,27 @@ +enablePlugins(GitVersioning) + +useJGit + +val checkUseJGit = taskKey[Unit]("checks the legacy useJGit helper") + +checkUseJGit := { + val readBackend = + (ThisBuild / com.github.sbt.git.SbtGit.GitKeys.gitReadBackend).value + val operationBackend = + (ThisBuild / com.github.sbt.git.SbtGit.GitKeys.gitOperationBackend).value + val runner = + (ThisBuild / com.github.sbt.git.SbtGit.GitKeys.gitRunner).value + + assert( + readBackend == com.github.sbt.git.GitBackend.JGitOnly, + s"Expected useJGit to set gitReadBackend=JGitOnly, found $readBackend" + ) + assert( + operationBackend == com.github.sbt.git.GitBackend.JGitOnly, + s"Expected useJGit to set gitOperationBackend=JGitOnly, found $operationBackend" + ) + assert( + runner == com.github.sbt.git.JGitRunner, + s"Expected useJGit to select JGitRunner, found $runner" + ) +} diff --git a/src/sbt-test/git-versioning/legacy-backend-helpers/project/plugins.sbt b/src/sbt-test/git-versioning/legacy-backend-helpers/project/plugins.sbt new file mode 100644 index 0000000..5510398 --- /dev/null +++ b/src/sbt-test/git-versioning/legacy-backend-helpers/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("com.github.sbt" % "sbt-git" % sys.props("project.version")) diff --git a/src/sbt-test/git-versioning/legacy-backend-helpers/test b/src/sbt-test/git-versioning/legacy-backend-helpers/test new file mode 100644 index 0000000..519bd27 --- /dev/null +++ b/src/sbt-test/git-versioning/legacy-backend-helpers/test @@ -0,0 +1 @@ +> checkUseJGit diff --git a/src/sbt-test/git-versioning/legacy-readable-console-helper/build.sbt b/src/sbt-test/git-versioning/legacy-readable-console-helper/build.sbt new file mode 100644 index 0000000..ae5aabe --- /dev/null +++ b/src/sbt-test/git-versioning/legacy-readable-console-helper/build.sbt @@ -0,0 +1,16 @@ +enablePlugins(GitVersioning) + +useReadableConsoleGit + +val checkUseReadableConsoleGit = + taskKey[Unit]("checks the legacy useReadableConsoleGit helper") + +checkUseReadableConsoleGit := { + val forceConsoleReads = + (ThisBuild / com.github.sbt.git.SbtGit.GitKeys.useConsoleForROGit).value + + assert( + forceConsoleReads, + "Expected useReadableConsoleGit to set the legacy console read flag" + ) +} diff --git a/src/sbt-test/git-versioning/legacy-readable-console-helper/project/plugins.sbt b/src/sbt-test/git-versioning/legacy-readable-console-helper/project/plugins.sbt new file mode 100644 index 0000000..5510398 --- /dev/null +++ b/src/sbt-test/git-versioning/legacy-readable-console-helper/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("com.github.sbt" % "sbt-git" % sys.props("project.version")) diff --git a/src/sbt-test/git-versioning/legacy-readable-console-helper/test b/src/sbt-test/git-versioning/legacy-readable-console-helper/test new file mode 100644 index 0000000..05e0734 --- /dev/null +++ b/src/sbt-test/git-versioning/legacy-readable-console-helper/test @@ -0,0 +1 @@ +> checkUseReadableConsoleGit From 13b31b072b0dee0f39b5b744e1fcfdf110c9da35 Mon Sep 17 00:00:00 2001 From: Matthias Kurz Date: Mon, 6 Jul 2026 13:58:16 +0200 Subject: [PATCH 5/5] Silence system git detection probes Run system git availability and readability checks through a silent probe instead of the normal ConsoleGitRunner path. These checks are expected to fail when git is missing or the build is not inside a git repository, so they should not log scary fatal messages during project load. Keep normal git command execution unchanged so real command failures still report stderr to the user. Add unit coverage for the non-git-directory readability case. --- .../com/github/sbt/git/ConsoleGitRunner.scala | 6 ++++++ .../scala/com/github/sbt/git/GitPlugin.scala | 20 +++++++++---------- .../com/github/sbt/git/SbtGitSuite.scala | 10 ++++++++-- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/main/scala/com/github/sbt/git/ConsoleGitRunner.scala b/src/main/scala/com/github/sbt/git/ConsoleGitRunner.scala index a713725..0c5abfb 100644 --- a/src/main/scala/com/github/sbt/git/ConsoleGitRunner.scala +++ b/src/main/scala/com/github/sbt/git/ConsoleGitRunner.scala @@ -39,6 +39,12 @@ object ConsoleGitRunner extends GitRunner { result } + private[sbt] def succeeds(args: String*)(cwd: File): Boolean = + Try { + IO.createDirectory(cwd) + Process(cmd ++ args, cwd, colorSupport*) ! ProcessLogger(_ => (), _ => ()) + }.toOption.contains(0) + override def toString = "git" // reduce log level for git process private class GitLogger(log: Logger) extends ProcessLogger { diff --git a/src/main/scala/com/github/sbt/git/GitPlugin.scala b/src/main/scala/com/github/sbt/git/GitPlugin.scala index 6ad9026..f43e32f 100644 --- a/src/main/scala/com/github/sbt/git/GitPlugin.scala +++ b/src/main/scala/com/github/sbt/git/GitPlugin.scala @@ -1,7 +1,5 @@ package com.github.sbt.git -import scala.util.Try - import sbt.* import Keys.* @@ -141,8 +139,8 @@ object SbtGit { gitReader := { val base = baseDirectory.value val log = sLog.value - val available = detectedSystemGitAvailable(systemGitAvailableOverride.value, base, log) - val readable = available && isSystemGitReadable(base, log) + val available = detectedSystemGitAvailable(systemGitAvailableOverride.value, base) + val readable = available && isSystemGitReadable(base) new DefaultReadableGit( base, if (useSystemGitForReads(useConsoleForROGit.value, gitReadBackend.value, readable)) @@ -151,7 +149,7 @@ object SbtGit { ) }, gitRunner := { - val available = detectedSystemGitAvailable(systemGitAvailableOverride.value, baseDirectory.value, sLog.value) + val available = detectedSystemGitAvailable(systemGitAvailableOverride.value, baseDirectory.value) selectGitRunner(gitOperationBackend.value, available) }, gitHeadCommit := gitReader.value.withGit(_.headCommitSha), @@ -168,14 +166,14 @@ object SbtGit { scmInfo := parseScmInfo(gitReader.value.withGit(_.remoteOrigin)) ) - private[sbt] def isSystemGitAvailable(dir: File, log: Logger): Boolean = - Try(ConsoleGitRunner("--version")(dir, log)).isSuccess + private[sbt] def isSystemGitAvailable(dir: File): Boolean = + ConsoleGitRunner.succeeds("--version")(dir) - private[sbt] def detectedSystemGitAvailable(overrideValue: Option[Boolean], dir: File, log: Logger): Boolean = - overrideValue.getOrElse(isSystemGitAvailable(dir, log)) + private[sbt] def detectedSystemGitAvailable(overrideValue: Option[Boolean], dir: File): Boolean = + overrideValue.getOrElse(isSystemGitAvailable(dir)) - private[sbt] def isSystemGitReadable(dir: File, log: Logger): Boolean = - Try(ConsoleGitRunner("rev-parse", "--git-dir")(dir, log)).isSuccess + private[sbt] def isSystemGitReadable(dir: File): Boolean = + ConsoleGitRunner.succeeds("rev-parse", "--git-dir")(dir) private[sbt] def useSystemGitForReads(forceSystemGit: Boolean, backend: GitBackend, systemGitAvailable: Boolean): Boolean = forceSystemGit || useSystemGit(backend, systemGitAvailable) diff --git a/src/test/scala/com/github/sbt/git/SbtGitSuite.scala b/src/test/scala/com/github/sbt/git/SbtGitSuite.scala index ccd3e2b..8a9b634 100644 --- a/src/test/scala/com/github/sbt/git/SbtGitSuite.scala +++ b/src/test/scala/com/github/sbt/git/SbtGitSuite.scala @@ -49,8 +49,14 @@ class SbtGitSuite extends munit.FunSuite { } test("system git availability override takes precedence over detection") { - assert(SbtGit.detectedSystemGitAvailable(Some(true), sbt.file("."), sbt.util.Logger.Null)) - assertEquals(SbtGit.detectedSystemGitAvailable(Some(false), sbt.file("."), sbt.util.Logger.Null), false) + assert(SbtGit.detectedSystemGitAvailable(Some(true), sbt.file("."))) + assertEquals(SbtGit.detectedSystemGitAvailable(Some(false), sbt.file(".")), false) + } + + test("system git readability probe handles non-git directories") { + IO.withTemporaryDirectory { dir => + assertEquals(SbtGit.isSystemGitReadable(dir), false) + } } test("operation backend selects the expected runner") {