Skip to content

Prefer system git by default with fall back to JGit#382

Open
mkurz wants to merge 5 commits into
sbt:mainfrom
mkurz:system-git-first-backends
Open

Prefer system git by default with fall back to JGit#382
mkurz wants to merge 5 commits into
sbt:mainfrom
mkurz:system-git-first-backends

Conversation

@mkurz

@mkurz mkurz commented Jul 2, 2026

Copy link
Copy Markdown
Member

Problem

sbt-git currently defaults read-only metadata operations to JGit. That is problematic for linked Git worktrees when the plugin is used with JGit 5.x, because JGit 5 does not support that repository layout. This shows up in downstream plugins such as sbt-dynver sbt-ci-release when an sbt project is checked out as a linked worktree.

JGit 7 has worktree support, but it requires Java 17. sbt-git still publishes an sbt 1 artifact that must remain Java 8 compatible, so upgrading JGit unconditionally is not viable.

Related context:

Changes

This changes the default backend selection to prefer the system git executable for both read-only metadata and explicit git operations.

The new default is SystemGitFirst:

  • use system git when it is available and usable
  • fall back to JGit when system git is unavailable
  • keep deterministic modes for users who want to force either backend

New public helpers:

  • useSystemGitFirstForReads
  • useSystemGitOnlyForReads
  • useJGitOnlyForReads
  • useSystemGitFirstForOperations
  • useSystemGitOnlyForOperations
  • useJGitOnlyForOperations

The older helpers are kept as deprecated aliases:
(IMO the old helper names were also confusing: useJGit only affected explicit git operations, not all sbt-git reads, while useReadableConsoleGit controlled only read-only metadata access. The new read/operation-specific helpers make that split explicit.)

  • useJGit
  • useReadableConsoleGit

The JGit dependency is also split by artifact:

  • sbt 1 / Scala 2.12 keeps JGit 5
  • sbt 2 / Scala 3 uses JGit 7

That preserves the Java 8 floor for sbt 1 while allowing the sbt 2 artifact to benefit from JGit 7 behavior.

Compatibility

  • Existing useJGit still works, but is deprecated and now maps to useJGitOnlyForOperations, which matches its old practical scope.
  • Existing useReadableConsoleGit still works, but is deprecated and forces system git for reads.
  • Existing keys like git.gitHeadCommit, git.gitCurrentBranch, git.gitUncommittedChanges, git.runner, and the sbt git command remain available.
  • sbt 1 keeps JGit 5 and Java 8 compatibility.
  • sbt 2 gets JGit 7, which is compatible with the Java 17 floor there.

Tests

Added coverage for:

  • backend selection logic
  • system-git-unavailable fallback to JGit
  • linked worktree reads through the default system-git-first path
  • linked worktree reads through JGit-only on sbt 2 / JGit 7

Verified with:

sbt -Dsbt.server.forcestart=true "++2.12.21" test "++3.8.4" test
sbt -Dsbt.server.forcestart=true "++2.12.21" "scripted git-versioning/system-git-worktree git-versioning/jgit-only-sbt2-worktree"
sbt -Dsbt.server.forcestart=true "++3.8.4" "scripted git-versioning/system-git-worktree git-versioning/jgit-only-sbt2-worktree"
git diff --check main...HEAD

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.
@mkurz

mkurz commented Jul 2, 2026

Copy link
Copy Markdown
Member Author

Now that people use AI agents, which includes workflows that check out worktrees, we should finally make sbt-git handle git worktrees reliable.

Using command line git by default will cover 98% of the uses cases reliable, and even for the rest we will have a fallback to jgit.
This PR now also pull in jgit v7 (that has worktree support) when used with sbt 2.
So that only leaves the very edges case unsupported:#

  • Using a worktree and
  • using sbt 1 and
  • not having system git command available

will fail. But even in that case, things can be fixed in application code by upgrading jgit to v7 manually in project/project/plugins.sbt and sbt-git will then fall back to it and things work.

@mkurz mkurz changed the title Prefer system git by default Prefer system git by default with fall back to JGit Jul 2, 2026
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.
@mkurz

mkurz commented Jul 2, 2026

Copy link
Copy Markdown
Member Author

Ahh test fail...

Switching the default read backend to the system git command uncovered a few places where the existing console reader did not match the JGit behavior that sbt-git exposed before.

The commands themselves were valid, but their semantics differed in important edge cases: git status -s counted untracked files as dirty, empty command output was parsed as Seq(""), HEAD-dependent commands failed in repositories without commits, and branch detection did not handle unborn repositories the same way JGit did.

The follow-up commit keeps the system git reader compatible with the previous public behavior while still allowing system git to be the default.

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.
@mkurz

mkurz commented Jul 2, 2026

Copy link
Copy Markdown
Member Author

I also tightened the existing console read backend in a separate commit.

Making system git the preferred read backend means this implementation is now on the main path, so I audited the remaining places where it did not match the behavior users previously got through JGit. The fixes are small but important: branch reads now use Git's ref formatting instead of parsing decorated git branch output, all git describe --match patterns are passed through, commit dates now match JGit's formatting, and remoteOrigin keeps the same "origin" fallback as the JGit backend.

I added unit tests for these cases so the system git backend has coverage for the public read API behavior we rely on.

@mkurz mkurz requested a review from eed3si9n July 2, 2026 13:48
@eed3si9n

eed3si9n commented Jul 2, 2026

Copy link
Copy Markdown
Member

Nice. Thanks for the contribution!

* normally through PATH. JGit means the Java implementation on the plugin
* classpath.
*/
sealed trait GitBackend extends Product with Serializable

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might fall into "who want to force either backend" naïvely, but my gut feel is that:

  1. We should either use system git (aka console git) or JGit, no halfway
  2. Current regime uses GitRunner's class inheritance (?) to denote the different backend, so having this sealed trait feels duplicated. Especially we if go with system vs JGit dichotomy all we need is Boolean?

@mkurz mkurz Jul 6, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. We should either use system git (aka console git) or JGit, no halfway

I see the concern, but I would like to keep the meaning of SystemGitFirst because it preserves the low-friction setup story: for most users sbt-git will use the installed git command, but a build can still load in environments where only Java/sbt are available but no git command.

For read operations, sbt-git currently defaults to JGit, and that generally works fine, especially for sbt-dynver sbt-ci-release, which is probably how many users encounter sbt-git in the first place. The main reason to prefer system git now is that JGit 5 does not support linked worktrees, while JGit 7 does but requires Java 17. Since the sbt 1 artifact still supports Java 8, system git is the only backend that can make worktrees work out of the box there.

The fallback to JGit keeps existing non-worktree environments working when the git executable is not available. The known remaining edge case is: sbt 1, linked worktree, no system git, and no manual JGit 7 override.

That matters for projects like Play Framework where the contributor story is ideally "install Java and sbt and start hacking." Most developers will have system git, but some environments may not: minimal containers, restricted CI images, machines where Git is accessed through an IDE/UI integration, or setups where git is installed outside PATH.

  1. Current regime uses GitRunner's class inheritance (?) to denote the different backend, so having this sealed trait feels duplicated. Especially we if go with system vs JGit dichotomy all we need is Boolean?

I agree that we should avoid unnecessary backend complexity. I think there are two possible directions:

First, if we want to give users explicit control, I do not think this is just a Boolean. There are three useful user intents:

  • SystemGitFirst: prefer system git, fall back to JGit
  • SystemGitOnly: require system git and fail if unavailable
  • JGitOnly: never shell out

The first one is the default for developer experience. The Only modes are for users or CI setups that want deterministic behavior.

Second, we could decide not to expose backend choice at all and just make SystemGitFirst the fixed behavior.

My preference is still the first option. Even if we removed the explicit JGitOnly / SystemGitOnly cases, we would still have the same concepts because the legacy helpers already express them: useJGit means "use JGit, do not shell out", and useReadableConsoleGit means "use system git for reads." Keeping the enum cases makes those existing intents explicit instead of encoding them as special cases elsewhere.

Comment on lines 280 to 281
@deprecated("System git is now preferred for reads by default. Use useSystemGitOnlyForReads to force it.", "2.0.0")
def useReadableConsoleGit: Setting[?] = ThisBuild / useConsoleForROGit := true

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you mentioned the flag situation requires cognitive tax on the user. Given the long history of sbt-git, spiritually useReadableConsoleGit users have already bought into "just use git", so I think it's ok to leave this setting alone as Nil or make it an alias of useSystemGitOnlyForReads, but without the punishment of deprecation.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. I removed the deprecation and kept this as a compatibility helper.

I chose to preserve the existing useConsoleForROGit flag behavior instead of making it a direct alias to useSystemGitOnlyForReads, so existing users keep the same semantics and do not get a warning for a setting that already expresses the intended “use system git for reads” behavior.

Comment on lines +276 to +277
@deprecated("Use useJGitOnlyForOperations instead.", "2.0.0")
def useJGit: Setting[?] = useJGitOnlyForOperations

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar for useJGit, I think the original intent of ThisBuild / gitRunner := JGitRunner is that JGit will be used for all operations.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I changed useJGit to express the broader legacy intent: it now selects JGit for both read-only metadata and explicit git operations.

That does change the helper from a single Setting[?] to Seq[Setting[?]], but the behavior is much closer to what users reasonably expect from a setting named useJGit.

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.
@mkurz mkurz requested a review from eed3si9n July 6, 2026 11:35
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.
@mkurz

mkurz commented Jul 6, 2026

Copy link
Copy Markdown
Member Author

Whoa, I just realized something while testing this against the Play 2.9.x branch (checked out in a linked worktree):

The linked-worktree failure is not actually because Play's versioning path uses sbt-git. Play gets its version from sbt-dynver, and sbt-dynver shells out to git directly already. My whole assumption all the time was that sbt-dynver uses sbt-git, which is does not!

The reason sbt-git is involved is that if a project uses sbt-ci-release, that one brings sbt-git onto the plugin classpath!
However, it turns out sbt-ci-release does not even need sbt-git anymore (!!!), so removing it as a dependency fixes the linked worktree problems:

I still think this PR is useful, because any build that has sbt-git on the plugin classpath can hit the same problem. But the Play case reveals that most projects just fail because sbt-ci-release unnecessarily pulls in sbt-git!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Default to command-line git, instead of JGit

2 participants