Repository: JuliaVersionControl/Git.jl
Pull request #55: Add OpenSSH_jll as a direct dependency, and force our Git to use OpenSSH_jll (instead of the system `ssh`)
Author: login: DilumAluthge
Created: 2025-05-07T13:13:34Z | Merged: 2025-05-08T10:30:28Z

Fixes #51, fixes https://github.com/julia-actions/julia-docdeploy/issues/40.

Linked issue #51: Git.jl is having issues with a container image using a different version of OpenSSL/OpenSSH
See https://github.com/JuliaDocs/Documenter.jl/issues/2357

I am having issues with using Documenter.jl switching to Git.jl. Using an `opensuse/tumbleweed` image causes a

```
Initialized empty Git repository in /tmp/jl_307B6d/.git/
OpenSSL version mismatch. Built against 30100030, you have 300000c0
fatal: Could not read from remote repository.
```

but switching to a `julia` image (based on a debian image) seems to fix it.
Linked issue comments: user: login: tamasgal
created_at: 2025-05-06T11:47:54Z
body: This is now happening again using the official Julia Docker images (started to fail a couple of days ago in our CI for all Julia projects): https://github.com/JuliaDocs/Documenter.jl/issues/2698
author_association: NONE
user: login: giordano
created_at: 2025-05-06T22:21:34Z
body: It goes without saying that a minimal reproducer would increase the chance someone will ever have a look. I can't reproduce issues locally with what I tried so I don't know what to say.
author_association: MEMBER
user: login: mortenpi
created_at: 2025-05-06T22:40:47Z
body: I think I have a Git.jl MWE with the official 1.11 Julia Docker image:

```
$ docker run -it julia:1.11 bash

root@9acce645cf2a:/# apt update && apt instally -y ssh

root@9acce645cf2a:/# julia

(@v1.11) pkg> add Git
 ...
  [d7ba0133] + Git v1.3.1

julia> using Git

julia> run(`$(Git.git()) clone git@github.com:JuliaDocs/Documenter.jl.git`)
Cloning into 'Documenter.jl'...
OpenSSL version mismatch. Built against 300000f0, you have 30500000
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
```

Note: I obviously don't have any SSH keys in the Docker container, so it should fail to clone regardless. But the OpenSSL mismatch message is definitely showing up.

For completeness, the host OS is `Pop!_OS 22.04 LTS` (so Linux).
author_association: COLLABORATOR
user: login: giordano
created_at: 2025-05-06T23:01:20Z
body: As far as I understand, the error message comes from OpenSSH: https://github.com/PowerShell/openssh-portable/blob/bdacf9868fe9e8024a5312861b7aaa6faba1821f/entropy.c#L71-L72.  I'm not sure how that comes into play here, since we don't directly use it in `Git_jll`: https://github.com/JuliaPackaging/Yggdrasil/blob/d09d9fc74ffaa3d23260a238fb06524486921f2e/G/Git/build_tarballs.jl#L120-L128.  But that'd explain why system packages are involved.
author_association: MEMBER
user: login: giordano
created_at: 2025-05-06T23:10:22Z
body: ```julia-repl
julia> using Git

julia> run(`$(Git.git()) clone --depth=1 git@github.com:JuliaDocs/Documenter.jl.git`);
Cloning into 'Documenter.jl'...
OpenSSL version mismatch. Built against 30000020, you have 30500000
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
ERROR: [...]

Stacktrace:
 [1] pipeline_error
   @ ./process.jl:598 [inlined]
 [2] run(::Cmd; wait::Bool)
   @ Base ./process.jl:513
 [3] run(::Cmd)
   @ Base ./process.jl:510
 [4] top-level scope
   @ REPL[26]:1

julia> using OpenSSH_jll

julia> _, path_value = split(Git.git().env[findfirst(startswith("PATH="), Git.git().env)], '=');

julia> openssh_bindir = dirname(OpenSSH_jll.ssh_path);

julia> git_ = addenv(Git.git(), "PATH" => "$(openssh_bindir):$(path_value)");

julia> run(`$(git_) clone --depth=1 git@github.com:JuliaDocs/Documenter.jl.git`);
Cloning into 'Documenter.jl'...
remote: Enumerating objects: 459, done.
remote: Counting objects: 100% (459/459), done.
remote: Compressing objects: 100% (415/415), done.
remote: Total 459 (delta 30), reused 328 (delta 14), pack-reused 0 (from 0)
Receiving objects: 100% (459/459), 1.08 MiB | 4.27 MiB/s, done.
Resolving deltas: 100% (30/30), done.
```
author_association: MEMBER
user: login: mortenpi
created_at: 2025-05-06T23:13:09Z
body: It defaults to the system's `ssh` binary, right? I explicitly had to install the `ssh` package for it to work at all. But it looks like the system `ssh` is linking against the Julia OpenSSH, right? Even though we'd want it to link against the system OpenSSL? Is a `LD_LIBRARY_PATH` from the JLL stuff leaking to the subprograms?

Should we force `git` to use OpenSSH_jll in Git.jl?
author_association: COLLABORATOR
user: login: giordano
created_at: 2025-05-06T23:15:54Z
body: > Should we force git to user OpenSSH_jll in Git.jl?

Yeah, that's what I did above.
author_association: MEMBER
user: login: mortenpi
created_at: 2025-05-06T23:18:34Z
body: > > Should we force git to user OpenSSH_jll in Git.jl?
> 
> Yeah, that's what I did above.

I meant do you think we should patch Git.jl the same way? I.e. add OpenSSH_jll as a dependency to it and tag a new version?
author_association: COLLABORATOR
user: login: giordano
created_at: 2025-05-06T23:20:54Z
body: Probably, yes.  We do something similar in BinaryBuilderBase: https://github.com/JuliaPackaging/BinaryBuilderBase.jl/blob/45acaa5c78a091ef75e5daa5de7a8676600509a5/src/Prefix.jl#L302-L326 (I think that's slightly cleaner than the env splitting I did above, that was a quick test)
author_association: MEMBER

Diff:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4321f55..20c6d0f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -2,8 +2,6 @@ name: CI
 
 on:
   pull_request:
-    branches:
-      - master
   push:
     branches:
       - master
@@ -61,7 +59,15 @@ jobs:
           arch: ${{ matrix.arch }}
       - uses: julia-actions/cache@v2
       - uses: julia-actions/julia-buildpkg@v1
+      - name: On Linux and Windows, ssh-keyscan github.com and store in known-hosts
+        shell: bash
+        run: |
+          mkdir -p ~/.ssh
+          ssh-keyscan github.com >> ~/.ssh/known_hosts
+        if: runner.os == 'Linux' || runner.os == 'Windows'
       - uses: julia-actions/julia-runtest@v1
+        env:
+          CI_READONLY_DEPLOYKEY_FOR_CI_TESTSUITE_PRIVATEKEY: ${{ secrets.CI_READONLY_DEPLOYKEY_FOR_CI_TESTSUITE_PRIVATEKEY }}
       - uses: julia-actions/julia-processcoverage@v1
       - uses: codecov/codecov-action@v5
         with:
diff --git a/Project.toml b/Project.toml
index 3ebb083..2619f85 100644
--- a/Project.toml
+++ b/Project.toml
@@ -1,19 +1,21 @@
 name = "Git"
 uuid = "d7ba0133-e1db-5d97-8f8c-041e4b3a1eb2"
+version = "1.4.0"
 authors = ["Dilum Aluthge", "contributors"]
-version = "1.3.1"
 
 [deps]
 Git_jll = "f8c6e375-362e-5223-8a59-34ff63f689eb"
+JLLWrappers = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210"
+OpenSSH_jll = "9bd350c2-7e96-507f-8002-3f2e150b4e1b"
 
 [compat]
 Git_jll = "2.44"
 JLLWrappers = "1.1"
+OpenSSH_jll = "9, 10"
 julia = "1.6"
 
 [extras]
-JLLWrappers = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210"
 Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
 
 [targets]
-test = ["JLLWrappers", "Test"]
+test = ["Test"]
diff --git a/src/git_function.jl b/src/git_function.jl
index 867f697..1e34733 100644
--- a/src/git_function.jl
+++ b/src/git_function.jl
@@ -1,3 +1,6 @@
+using OpenSSH_jll: OpenSSH_jll
+using JLLWrappers: pathsep, LIBPATH_env
+
 """
     git()
 
@@ -18,8 +21,8 @@ julia> run(git(["clone", "https://github.com/JuliaRegistries/General"]))
 to bypass the parsing of the command string.
 """
 function git(; adjust_PATH::Bool = true, adjust_LIBPATH::Bool = true)
-    @static if Sys.iswindows()
-        return Git_jll.git(; adjust_PATH, adjust_LIBPATH)::Cmd
+    git_cmd = @static if Sys.iswindows()
+        Git_jll.git(; adjust_PATH, adjust_LIBPATH)::Cmd
     else
         root = Git_jll.artifact_dir
 
@@ -45,8 +48,26 @@ function git(; adjust_PATH::Bool = true, adjust_LIBPATH::Bool = true)
         end
 
         original_cmd = Git_jll.git(; adjust_PATH, adjust_LIBPATH)::Cmd
-        return addenv(original_cmd, env_mapping...)::Cmd
+        addenv(original_cmd, env_mapping...)::Cmd
     end
+
+    # Use OpenSSH from the JLL: <https://github.com/JuliaVersionControl/Git.jl/issues/51>.
+    if !Sys.iswindows() && OpenSSH_jll.is_available()
+        path = split(get(ENV, "PATH", ""), pathsep)
+        libpath = split(get(ENV, LIBPATH_env, ""), pathsep)
+
+        path = vcat(dirname(OpenSSH_jll.ssh_path), path)
+        libpath = vcat(OpenSSH_jll.LIBPATH_list, libpath)
+        path = vcat(dirname(Git_jll.git_path), path)
+        libpath = vcat(Git_jll.LIBPATH_list, libpath)
+
+        unique!(filter!(!isempty, path))
+        unique!(filter!(!isempty, libpath))
+
+        git_cmd = addenv(git_cmd, "PATH" => join(path, pathsep), LIBPATH_env => join(libpath, pathsep))
+    end
+
+    return git_cmd
 end
 
 function git(args::AbstractVector{<:AbstractString}; kwargs...)
diff --git a/test/runtests.jl b/test/runtests.jl
index 5095d81..6c539a2 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -71,3 +71,39 @@ end
         @test dir1_log == dir2_log
     end; end
 end
+
+# https://github.com/JuliaVersionControl/Git.jl/issues/51
+@testset "OpenSSH integration" begin
+    is_ci = parse(Bool, strip(get(ENV, "CI", "false")))
+    is_gha = parse(Bool, strip(get(ENV, "GITHUB_ACTIONS", "false")))
+    if is_ci && is_gha
+        @info "This is GitHub Actions CI, so running the OpenSSH test..."
+        mktempdir() do sshprivkeydir
+            privkey_filepath = joinpath(sshprivkeydir, "my_private_key")
+            open(privkey_filepath, "w") do io
+                ssh_privkey = ENV["CI_READONLY_DEPLOYKEY_FOR_CI_TESTSUITE_PRIVATEKEY"]
+                println(io, ssh_privkey)
+            end # open
+            # We need to chmod our private key to 600, or SSH will ignore it.
+            chmod(privkey_filepath, 0o600)
+
+            # ssh_verbose = "-vvv" # comment this line back out when you are finished debugging
+            ssh_verbose = "" # uncomment this line when you are finished debugging
+
+            withenv("GIT_SSH_COMMAND" => "ssh $(ssh_verbose) -i \"$(privkey_filepath)\"") do
+                withtempdir() do workdir
+                    @test !isdir("Git.jl")
+                    @test !isfile(joinpath("Git.jl", "Project.toml"))
+                    # We use `run()` so that we can see the stdout and stderr in the CI logs:
+                    proc = run(`$(git()) clone --depth=1 git@github.com:JuliaVersionControl/Git.jl.git`)
+                    @test success(proc)
+                    @test isdir("Git.jl")
+                    @test isfile(joinpath("Git.jl", "Project.toml"))
+                end # withtempdir/workdir
+            end # withenv
+        end # withtempdir/sshprivkeydir
+    else
+        # Mark this test as skipped if we are not running in CI
+        @test_skip false
+    end # if
+end # testset


Reviews:
user: login: giordano
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T14:04:42Z
user: login: giordano
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T14:05:14Z
user: login: DilumAluthge
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T14:06:00Z
user: login: DilumAluthge
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T14:09:43Z
user: login: DilumAluthge
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T15:59:10Z
user: login: giordano
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T16:22:53Z
user: login: giordano
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T16:23:45Z
user: login: DilumAluthge
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T09:07:38Z
user: login: DilumAluthge
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T09:08:22Z

Review comments:
diff_hunk: @@ -71,3 +71,29 @@ end
         @test dir1_log == dir2_log
     end; end
 end
+
+# https://github.com/JuliaVersionControl/Git.jl/issues/51
+
+@testset "OpenSSH integration" begin
+    is_ci = parse(Bool, strip(get(ENV, "CI", "false")))
+    if is_ci
+        withtempdir() do sshprivkeydir
+            privkey_filepath = joinpath(sshprivkeydir, "my_private_key")
+            open(privkey_filepath, "w") do io
+                ssh_privkey = ENV["CI_READONLY_DEPLOYKEY_FOR_CI_TESTSUITE_PRIVATEKEY"]
+                println(io, ssh_privkey)
+            end # open
+            withenv("GIT_SSH_COMMAND" => "ssh -i \"$(privkey_filepath)\"") do
+                withtempdir() do workdir
+                    cd(workdir) do
path: test/runtests.jl
user: login: giordano
body: ```suggestion
```
created_at: 2025-05-07T14:04:41Z
author_association: MEMBER
diff_hunk: @@ -71,3 +71,29 @@ end
         @test dir1_log == dir2_log
     end; end
 end
+
+# https://github.com/JuliaVersionControl/Git.jl/issues/51
+
+@testset "OpenSSH integration" begin
+    is_ci = parse(Bool, strip(get(ENV, "CI", "false")))
+    if is_ci
+        withtempdir() do sshprivkeydir
path: test/runtests.jl
user: login: giordano
body: ```suggestion
        mktempdir() do sshprivkeydir
```
created_at: 2025-05-07T14:05:14Z
author_association: MEMBER
diff_hunk: @@ -71,3 +71,29 @@ end
         @test dir1_log == dir2_log
     end; end
 end
+
+# https://github.com/JuliaVersionControl/Git.jl/issues/51
+
+@testset "OpenSSH integration" begin
+    is_ci = parse(Bool, strip(get(ENV, "CI", "false")))
+    if is_ci
+        withtempdir() do sshprivkeydir
+            privkey_filepath = joinpath(sshprivkeydir, "my_private_key")
+            open(privkey_filepath, "w") do io
+                ssh_privkey = ENV["CI_READONLY_DEPLOYKEY_FOR_CI_TESTSUITE_PRIVATEKEY"]
+                println(io, ssh_privkey)
+            end # open
+            withenv("GIT_SSH_COMMAND" => "ssh -i \"$(privkey_filepath)\"") do
+                withtempdir() do workdir
+                    cd(workdir) do
path: test/runtests.jl
user: login: DilumAluthge
body: Ah, I forgot that our own `withtempdir()` function does the `cd()` for us. I'll fix that.
created_at: 2025-05-07T14:05:56Z
author_association: MEMBER
diff_hunk: @@ -71,3 +71,29 @@ end
         @test dir1_log == dir2_log
     end; end
 end
+
+# https://github.com/JuliaVersionControl/Git.jl/issues/51
+
+@testset "OpenSSH integration" begin
+    is_ci = parse(Bool, strip(get(ENV, "CI", "false")))
+    if is_ci
+        withtempdir() do sshprivkeydir
+            privkey_filepath = joinpath(sshprivkeydir, "my_private_key")
+            open(privkey_filepath, "w") do io
+                ssh_privkey = ENV["CI_READONLY_DEPLOYKEY_FOR_CI_TESTSUITE_PRIVATEKEY"]
+                println(io, ssh_privkey)
+            end # open
+            withenv("GIT_SSH_COMMAND" => "ssh -i \"$(privkey_filepath)\"") do
+                withtempdir() do workdir
+                    cd(workdir) do
path: test/runtests.jl
user: login: DilumAluthge
body: Fixed in 7c53abcc3b9d300a4191251aa2df8180cc5d5432
created_at: 2025-05-07T14:09:43Z
author_association: MEMBER
diff_hunk: @@ -71,3 +71,39 @@ end
         @test dir1_log == dir2_log
     end; end
 end
+
+# https://github.com/JuliaVersionControl/Git.jl/issues/51
+@testset "OpenSSH integration" begin
+    is_ci = parse(Bool, strip(get(ENV, "CI", "false")))
+    is_gha = parse(Bool, strip(get(ENV, "GITHUB_ACTIONS", "false")))
+    if is_ci && is_gha
+        @info "This is GitHub Actions CI, so running the OpenSSH test..."
+        mktempdir() do sshprivkeydir
+            privkey_filepath = joinpath(sshprivkeydir, "my_private_key")
+            open(privkey_filepath, "w") do io
+                ssh_privkey = ENV["CI_READONLY_DEPLOYKEY_FOR_CI_TESTSUITE_PRIVATEKEY"]
+                println(io, ssh_privkey)
+            end # open
+            # We need to chmod our private key to 600, or SSH will ignore it.
+            chmod(privkey_filepath, 0o600)
+
+            ssh_verbose = "-vvv" # comment this line back out when you are finished debugging
+            # ssh_verbose = "" # uncomment this line when you are finished debugging
path: test/runtests.jl
user: login: DilumAluthge
body: TODO once we have finished debugging CI in this PR:

```suggestion
            # ssh_verbose = "-vvv" # comment this line back out when you are finished debugging
            ssh_verbose = "" # uncomment this line when you are finished debugging
```
created_at: 2025-05-07T15:59:06Z
author_association: MEMBER
diff_hunk: @@ -61,7 +59,17 @@ jobs:
           arch: ${{ matrix.arch }}
       - uses: julia-actions/cache@v2
       - uses: julia-actions/julia-buildpkg@v1
+      - uses: dcarbone/install-jq-action@v3
+        if: runner.os == 'Linux' || runner.os == 'Windows'
+      - name: On Linux and Windows, add Github.com known hosts (from GitHub API over HTTPS)
+        shell: bash
+        run: |
+          mkdir -p ~/.ssh
+          curl --silent https://api.github.com/meta | jq --raw-output '"github.com "+.ssh_keys[]' >> ~/.ssh/known_hosts
+        if: runner.os == 'Linux' || runner.os == 'Windows'
path: .github/workflows/ci.yml
user: login: giordano
body: Isn't [`ssh-keyscan github.com >> ~/.ssh/known_hosts`](https://github.com/JuliaPackaging/BinaryBuilderBase.jl/blob/45acaa5c78a091ef75e5daa5de7a8676600509a5/.github/workflows/ci.yml#L118C11-L118C55) sufficient?
created_at: 2025-05-07T16:22:53Z
author_association: MEMBER
diff_hunk: @@ -45,8 +48,26 @@ function git(; adjust_PATH::Bool = true, adjust_LIBPATH::Bool = true)
         end
 
         original_cmd = Git_jll.git(; adjust_PATH, adjust_LIBPATH)::Cmd
-        return addenv(original_cmd, env_mapping...)::Cmd
+        addenv(original_cmd, env_mapping...)::Cmd
     end
+
+    # Use OpenSSH from the JLL: <https://github.com/JuliaVersionControl/Git.jl/issues/51>.
+    if !Sys.iswindows() && OpenSSH_jll.is_available()
path: src/git_function.jl
user: login: giordano
body: At this point this can be moved to the non-Windows branch above.
created_at: 2025-05-07T16:23:45Z
author_association: MEMBER
diff_hunk: @@ -61,7 +59,17 @@ jobs:
           arch: ${{ matrix.arch }}
       - uses: julia-actions/cache@v2
       - uses: julia-actions/julia-buildpkg@v1
+      - uses: dcarbone/install-jq-action@v3
+        if: runner.os == 'Linux' || runner.os == 'Windows'
+      - name: On Linux and Windows, add Github.com known hosts (from GitHub API over HTTPS)
+        shell: bash
+        run: |
+          mkdir -p ~/.ssh
+          curl --silent https://api.github.com/meta | jq --raw-output '"github.com "+.ssh_keys[]' >> ~/.ssh/known_hosts
+        if: runner.os == 'Linux' || runner.os == 'Windows'
path: .github/workflows/ci.yml
user: login: DilumAluthge
body: ```suggestion
      - name: On Linux and Windows, ssh-keyscan github.com and store in known-hosts
        shell: bash
        run: |
          mkdir -p ~/.ssh
          ssh-keyscan github.com >> ~/.ssh/known_hosts
        if: runner.os == 'Linux' || runner.os == 'Windows'
```
created_at: 2025-05-08T09:07:34Z
author_association: MEMBER
diff_hunk: @@ -61,7 +59,17 @@ jobs:
           arch: ${{ matrix.arch }}
       - uses: julia-actions/cache@v2
       - uses: julia-actions/julia-buildpkg@v1
+      - uses: dcarbone/install-jq-action@v3
+        if: runner.os == 'Linux' || runner.os == 'Windows'
+      - name: On Linux and Windows, add Github.com known hosts (from GitHub API over HTTPS)
+        shell: bash
+        run: |
+          mkdir -p ~/.ssh
+          curl --silent https://api.github.com/meta | jq --raw-output '"github.com "+.ssh_keys[]' >> ~/.ssh/known_hosts
+        if: runner.os == 'Linux' || runner.os == 'Windows'
path: .github/workflows/ci.yml
user: login: DilumAluthge
body: Done in 472fdffadfb605a02c7e43485bb935213a00b6e3
created_at: 2025-05-08T09:08:22Z
author_association: MEMBER

PR comments:
user: login: DilumAluthge
created_at: 2025-05-07T13:18:07Z
body: cc: @mortenpi
author_association: MEMBER
user: login: DilumAluthge
created_at: 2025-05-07T13:23:22Z
body: Some of the Cirrus CI jobs are passing in the package test stage, but then failing in the Codecov submission. That should go away once we merge #56.
author_association: MEMBER
user: login: DilumAluthge
created_at: 2025-05-07T13:26:37Z
body: The FreeBSD Cirrus CI failure will (hopefully) be fixed by #57.
author_association: MEMBER
user: login: giordano
created_at: 2025-05-07T13:30:01Z
body: Locally I already had
```patch
diff --git a/Project.toml b/Project.toml
index 3ebb083..512cffa 100644
--- a/Project.toml
+++ b/Project.toml
@@ -1,19 +1,21 @@
 name = "Git"
 uuid = "d7ba0133-e1db-5d97-8f8c-041e4b3a1eb2"
+version = "1.3.2"
 authors = ["Dilum Aluthge", "contributors"]
-version = "1.3.1"
 
 [deps]
 Git_jll = "f8c6e375-362e-5223-8a59-34ff63f689eb"
+JLLWrappers = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210"
+OpenSSH_jll = "9bd350c2-7e96-507f-8002-3f2e150b4e1b"
 
 [compat]
 Git_jll = "2.44"
 JLLWrappers = "1.1"
+OpenSSH_jll = "9, 10"
 julia = "1.6"
 
 [extras]
-JLLWrappers = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210"
 Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
 
 [targets]
-test = ["JLLWrappers", "Test"]
+test = ["Test"]
diff --git a/src/git_function.jl b/src/git_function.jl
index 867f697..a218928 100644
--- a/src/git_function.jl
+++ b/src/git_function.jl
@@ -1,3 +1,6 @@
+using OpenSSH_jll: OpenSSH_jll
+using JLLWrappers: pathsep, LIBPATH_env
+
 """
     git()
 
@@ -18,8 +21,8 @@ julia> run(git(["clone", "https://github.com/JuliaRegistries/General"]))
 to bypass the parsing of the command string.
 """
 function git(; adjust_PATH::Bool = true, adjust_LIBPATH::Bool = true)
-    @static if Sys.iswindows()
-        return Git_jll.git(; adjust_PATH, adjust_LIBPATH)::Cmd
+    git_cmd = @static if Sys.iswindows()
+        Git_jll.git(; adjust_PATH, adjust_LIBPATH)::Cmd
     else
         root = Git_jll.artifact_dir
 
@@ -45,8 +48,24 @@ function git(; adjust_PATH::Bool = true, adjust_LIBPATH::Bool = true)
         end
 
         original_cmd = Git_jll.git(; adjust_PATH, adjust_LIBPATH)::Cmd
-        return addenv(original_cmd, env_mapping...)::Cmd
+        addenv(original_cmd, env_mapping...)::Cmd
     end
+
+    # Use OpenSSH from the JLL: <https://github.com/JuliaVersionControl/Git.jl/issues/51>.
+    if OpenSSH_jll.is_available()
+        path = split(get(ENV, "PATH", ""), pathsep)
+        libpath = split(get(ENV, LIBPATH_env, ""), pathsep)
+
+        path = vcat(dirname(OpenSSH_jll.ssh_path), path)
+        libpath = vcat(OpenSSH_jll.LIBPATH_list, libpath)
+
+        unique!(filter!(!isempty, path))
+        unique!(filter!(!isempty, libpath))
+
+        git_cmd = addenv(git_cmd, "PATH" => join(path, pathsep), LIBPATH_env => join(libpath, pathsep))
+    end
+
+    return git_cmd
 end
 
 function git(args::AbstractVector{<:AbstractString}; kwargs...)
```
which looks cleaner to me, but I was trying to figure out how to test this (always needs a key pair, which we can't ensure)
author_association: MEMBER
user: login: DilumAluthge
created_at: 2025-05-07T13:34:52Z
body: I would probably bump the minor version number, because this feels a bit like a new feature to me, but other than that LGTM.

For testing, can we just generate a new SSH keypair on the fly, and then discard it at the end of our tests? Or does it need to be an SSH keypair that is known to GitHub?
author_association: MEMBER
user: login: giordano
created_at: 2025-05-07T13:35:42Z
body: > Or does it need to be an SSH keypair that is known to GitHub?

I presume the latter.
author_association: MEMBER
user: login: DilumAluthge
created_at: 2025-05-07T13:37:35Z
body: We could add a test that we only do on CI (skip locally).

We can generate a new SSH keypair, and add it to this repo as a deploy key with read-only permissions, and add the private key as a GitHub Actions secret. And then have a CI-only test that uses this SSH deploy key to clone this repo?
author_association: MEMBER
user: login: DilumAluthge
created_at: 2025-05-07T13:38:38Z
body: BTW, your diff applies this "prepend OpenSSH_jll" behavior to both Windows and non-Windows. Is this necessary? I was assuming we could just bypass this whole thing on Windows, and just apply this patch for the non-Windows codepath.
author_association: MEMBER
user: login: giordano
created_at: 2025-05-07T13:39:58Z
body: >  Is this necessary? 

I don't see why Windows should be any different (besides the fact it's slightly less likely users may have `ssh` lying around in `PATH`)

> We can generate a new SSH keypair, and add it to this repo as a deploy key with read-only permissions, and add the private key as a GitHub Actions secret. And then have a CI-only test that uses this SSH deploy key to clone this repo?

That could work.
author_association: MEMBER
user: login: DilumAluthge
created_at: 2025-05-07T13:40:27Z
body: @giordano You can force-push your version to my branch, and take over this PR (or you can open a new PR if you've already pushed your branch).
author_association: MEMBER
user: login: giordano
created_at: 2025-05-07T13:46:39Z
body: The test I had in mind was
```julia
@testset "OpenSSH integration" begin
    if # something....
        withtempdir() do dir
            @test !isdir("Git.jl")
            @test !isfile(joinpath("Git.jl", "Project.toml"))
            @test success(`$(git()) clone --depth=1 git@github.com:JuliaVersionControl/Git.jl.git`)
            @test isdir("Git.jl")
            @test isfile(joinpath("Git.jl", "Project.toml"))
        end
    end
end
```
Would you be able to finish up the testing part?
author_association: MEMBER
user: login: DilumAluthge
created_at: 2025-05-07T13:47:55Z
body: > I don't see why Windows should be any different (besides the fact it's slightly less likely users may have `ssh` lying around in `PATH`)

My thinking is:

1. We (Yggdrasil) don't build Git binaries for Git_jll on Windows (we just re-ship the tarballs from upstream https://github.com/git-for-windows/git/releases, if I understand correctly). So on Windows, there's no reason that the Git binaries would be built against our OpenSSH_jll.
2. If I understand correctly, Git for Windows ships with its own SSH client (as part of the Git Bash that is shipped with Git for Windows). So I assume that the Git for Windows binaries are automatically configured out-of-the-box to use their own SSH client.
author_association: MEMBER
user: login: DilumAluthge
created_at: 2025-05-07T13:48:32Z
body: I just checked on a Windows machine that I have. When I open Git Bash, the first SSH client in the PATH is the SSH that ships built-in to Git Bash.
author_association: MEMBER
user: login: DilumAluthge
created_at: 2025-05-07T13:51:59Z
body: > Would you be able to finish up the testing part?

Yep. I will create the deploy key and push the tests shortly.
author_association: MEMBER
user: login: DilumAluthge
created_at: 2025-05-07T14:03:39Z
body: I have:
1. Created the keypair, added the public key as deploy key (read-only), added the private key as secret.
2. Pushed a commit with the tests.
author_association: MEMBER
user: login: DilumAluthge
created_at: 2025-05-07T14:04:58Z
body: Ubuntu CI is failing with:

```
/home/runner/.julia/artifacts/9bfc066f45d9299c72b5f6928c8618c066904c83/bin/git: error while loading shared libraries: libiconv.so.2: cannot open shared object file: No such file or directory
```
author_association: MEMBER
user: login: giordano
created_at: 2025-05-07T14:20:24Z
body: A read-only key can also be added here (as long as it's read-only), to make testing more straightforward everywhere.  We do that in BBB for example: https://github.com/JuliaPackaging/BinaryBuilderBase.jl/blob/45acaa5c78a091ef75e5daa5de7a8676600509a5/test/id_ecdsa_deploy_helloworld_c_jll_read_only.
author_association: MEMBER
user: login: DilumAluthge
created_at: 2025-05-07T14:23:17Z
body: Hmmm. My understanding was that GitHub would detect and revoke SSH private keys that get checked into public repos, even if the SSH keypair is just a deploy key with read-only access.
author_association: MEMBER
user: login: DilumAluthge
created_at: 2025-05-07T14:24:16Z
body: I'd probably create a separate test-only repo, just to be safe, and have the deploy key be for that separate repo.
author_association: MEMBER
user: login: DilumAluthge
created_at: 2025-05-07T14:45:24Z
body: @giordano Can you look into https://github.com/JuliaVersionControl/Git.jl/pull/55#issuecomment-2858679781? IIUC, on Windows we're just shipping upstream binaries, so those binaries won't be linked against our own OpenSSH_jll anyway, right?
author_association: MEMBER
user: login: giordano
created_at: 2025-05-07T14:47:08Z
body: That's right, but we still don't want to require users to have `ssh` installed, no?
author_association: MEMBER
user: login: DilumAluthge
created_at: 2025-05-07T14:50:02Z
body: > That's right, but we still don't want to require users to have `ssh` installed, no?

But doesn't Git for Windows come with SSH?

I'm looking inside https://github.com/git-for-windows/git/releases/download/v2.49.0.windows.1/Git-2.49.0-64-bit.tar.bz2, and in `./usr/bin/`, I see `ssh.exe`.
author_association: MEMBER
user: login: giordano
created_at: 2025-05-07T14:52:18Z
body: > But doesn't Git for Windows come with SSH?

I mean, this package doesn't require anyone to have Git installed either.
author_association: MEMBER
user: login: DilumAluthge
created_at: 2025-05-07T14:53:46Z
body: I'm not sure I follow. When you install Git.jl, you get Git_jll.jl, which on Windows comes with the entire contents of https://github.com/git-for-windows/git/releases/download/v2.49.0.windows.1/Git-2.49.0-64-bit.tar.bz2, right? So installing Git.jl on Windows will automatically give you all of https://github.com/git-for-windows/git/releases/download/v2.49.0.windows.1/Git-2.49.0-64-bit.tar.bz2?
author_association: MEMBER
user: login: DilumAluthge
created_at: 2025-05-07T14:54:46Z
body: That is, the user doesn't need anything installed, because on Windows we are providing the entirety of git-for-windows, right?
author_association: MEMBER
user: login: giordano
created_at: 2025-05-07T14:57:44Z
body: I don't see `ssh.exe` in `/bin` in https://github.com/JuliaBinaryWrappers/Git_jll.jl/releases/download/Git-v2.49.0%2B0/Git.v2.49.0.x86_64-w64-mingw32.tar.gz, so I don't understand how that should be picked up.
author_association: MEMBER
user: login: DilumAluthge
created_at: 2025-05-07T14:59:28Z
body: Ah, I see, so maybe we aren't including it when we re-bundle the upstream Git for Windows?
author_association: MEMBER
user: login: DilumAluthge
created_at: 2025-05-07T14:59:49Z
body: > I don't see `ssh.exe` in `/bin`

I do see it in `/usr/bin` FWIW
author_association: MEMBER
user: login: DilumAluthge
created_at: 2025-05-07T15:00:22Z
body: On a separate note, on Julia 1.6, Windows tests are failing:

> IOError: could not spawn setenv(`'C:\Users\runneradmin\.julia\artifacts\9148fbb39a57f9ad39d560c68427a823b4a8afb1\bin\git.exe' clone --quiet [https://github.com/JuliaVersionControl/Git.jl`,[](https://github.com/JuliaVersionControl/Git.jl%60,[)" ...

And this is a non-SSH URL.
author_association: MEMBER
user: login: DilumAluthge
created_at: 2025-05-07T15:00:53Z
body: Of note, Windows is passing on Julia 1.11, but failing on Julia 1.6.
author_association: MEMBER
user: login: codecov[bot]
created_at: 2025-05-07T15:02:21Z
body: ## [Codecov](https://app.codecov.io/gh/JuliaVersionControl/Git.jl/pull/55?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=JuliaVersionControl) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 96.96%. Comparing base [(`f818f5a`)](https://app.codecov.io/gh/JuliaVersionControl/Git.jl/commit/f818f5a2521531d4f697e1d295b4baca70053d08?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=JuliaVersionControl) to head [(`472fdff`)](https://app.codecov.io/gh/JuliaVersionControl/Git.jl/commit/472fdffadfb605a02c7e43485bb935213a00b6e3?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=JuliaVersionControl).
> Report is 1 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master      #55      +/-   ##
==========================================
+ Coverage   95.45%   96.96%   +1.51%     
==========================================
  Files           1        1              
  Lines          22       33      +11     
==========================================
+ Hits           21       32      +11     
  Misses          1        1              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/JuliaVersionControl/Git.jl/pull/55?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=JuliaVersionControl).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=JuliaVersionControl).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: giordano
created_at: 2025-05-07T15:03:03Z
body: > I do see it in /usr/bin FWIW

I have no idea how the `git` executable finds `ssh.exe`.  It's not in the same directory, unless it somehow sets `PATH` internally or manually reference the relative path to that one I don't think that executable is ever called at any point.  Existing != being called.
author_association: MEMBER
user: login: DilumAluthge
created_at: 2025-05-07T15:51:33Z
body: Alright, so I did some testing on a Windows machine, and here is what I found.

First, I opened Task Manager, and I made sure that there were no `ssh` processes running.

Then, I opened Julia, and did `using Git: git` (where I have Git.jl version v1.3.1, with Julia 1.11.5).

Then I did:

```julia
using Git: git

run(`$(git()) clone git@github.com:JuliaRegistries/General.git`)
```

I don't have any `known_hosts` file yet, so this second line hangs and waits for me to accept or reject the fingerprint for `github.com`. I leave the Julia REPL as-is, and I go back to Task Manager. I'm now able to see the `ssh` process running. In Task Manager, when I pull up the details for that `ssh` process, here's what I see:

Screenshot 1 of 3:

<img width="50%" alt="1" src="https://github.com/user-attachments/assets/51f13cae-fc13-4e6d-9dd6-1009aa7db0bb" />


Screenshot 2 of 3:

<img width="50%" alt="2" src="https://github.com/user-attachments/assets/1d6d26c9-ad8b-4a4f-a463-63cad76eb432" />


Screenshot 3 of 3 (I'm hovering over the "path" so that the full path is shown):

<img width="50%" alt="3" src="https://github.com/user-attachments/assets/8c0bc152-2062-477b-b6f1-c633514d4297" />

So, from this testing, it looks to me that when I use Git.jl (latest registered version) on Windows, by default it will use the `ssh.exe` that is bundled inside `./usr/bin` inside our artifacts.

I assume Git-for-Windows does some magic to make sure that their own SSH gets picked up, instead of a system SSH.
author_association: MEMBER
user: login: fingolfin
created_at: 2025-05-16T06:20:55Z
body: Could we get this into a release?
author_association: NONE
user: login: giordano
created_at: 2025-05-16T07:16:15Z
body: It's in [`1.4.0`](https://github.com/JuliaRegistries/General/pull/130516), isn't it?
author_association: MEMBER
user: login: fingolfin
created_at: 2025-05-16T08:14:56Z
body: Sorry I only looked at https://github.com/JuliaVersionControl/Git.jl/releases and it is not listed there. So I assume TagBot integration here is broken?

Indeed, your commit https://github.com/JuliaVersionControl/Git.jl/commit/fc28591b8126c9681725dcc7b837f0caa817d405 likely broke it. Indeed, the `permissions` block has to be commented out. And then the TagBot documentation ought to be adjusted to not suggest to people to use this, as it doesn't work (I found this out the hard work when I recently worked on https://github.com/JuliaCollections/DataStructures.jl )
author_association: NONE
user: login: giordano
created_at: 2025-05-16T08:17:04Z
body: > And then the TagBot documentation ought to be adjusted to not suggest to people to use this, as it doesn't work

Yeah, well, I copied from there, if that's wrong it's broken for everyone.
author_association: MEMBER
user: login: fingolfin
created_at: 2025-05-16T09:55:20Z
body: it is indeed broken for "everyone". I'll report to TagBot
author_association: NONE
user: login: fingolfin
created_at: 2025-05-16T09:57:10Z
body: ah actually [I did report it in March](https://github.com/JuliaRegistries/TagBot/issues/388) (phew! I felt bad because I thought "why the heck did I not report this when I run into it). 

Unfortunately I got no reactions by anyone "in charge". I have no idea who that would be ... :-(
author_association: NONE