/
githubmirror
/
julia
Обзор
Документация
Войти
/
githubmirror
/
julia
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
stdlib/LibGit2/test/libgit2-tests.jl
3 290 строк
139 KB
Jameson Nash
docs: fix grammar and clarity in comments and docstrings (#62083)
17 июн 2026, 05:57
Не верифицирован
17 июн 2026, 05:57
27858bb
Код
Авторство
О чём код?
# This file is a part of Julia. License is MIT: https://julialang.org/license module LibGit2Tests import LibGit2 using LibGit2_jll using Test using Random, Serialization, Sockets const BASE_TEST_PATH = joinpath(Sys.BINDIR, Base.DATAROOTDIR, "julia", "test") isdefined(Main, :ChallengePrompts) || @eval Main include(joinpath($(BASE_TEST_PATH), "testhelpers", "ChallengePrompts.jl")) using .Main.ChallengePrompts: challenge_prompt as basic_challenge_prompt challenge_prompt(code::Expr, challenges) = basic_challenge_prompt(code, challenges; pkgs=["LibGit2"]) challenge_prompt(cmd::Cmd, challenges) = basic_challenge_prompt(cmd, challenges) const LIBGIT2_MIN_VER = v"1.0.0" const LIBGIT2_HELPER_PATH = joinpath(@__DIR__, "libgit2-helpers.jl") const KEY_DIR = joinpath(@__DIR__, "keys") const HOME = Sys.iswindows() ? "USERPROFILE" : "HOME" # Environment variable name for home const GIT_INSTALLED = try success(`git --version`) catch false end function get_global_dir() buf = Ref(LibGit2.Buffer()) LibGit2.@check @ccall libgit2.git_libgit2_opts( LibGit2.Consts.GET_SEARCH_PATH::Cint; LibGit2.Consts.CONFIG_LEVEL_GLOBAL::Cint, buf::Ptr{LibGit2.Buffer})::Cint path = unsafe_string(buf[].ptr) LibGit2.free(buf) return path end function set_global_dir(dir) LibGit2.@check @ccall libgit2.git_libgit2_opts( LibGit2.Consts.SET_SEARCH_PATH::Cint; LibGit2.Consts.CONFIG_LEVEL_GLOBAL::Cint, dir::Cstring)::Cint return end function with_libgit2_temp_home(f) mktempdir() do tmphome oldpath = get_global_dir() set_global_dir(tmphome) try @test get_global_dir() == tmphome f(tmphome) finally set_global_dir(oldpath) end return end end ######### # TESTS # ######### @testset "Check library version" begin v = LibGit2.version() @test v.major == LIBGIT2_MIN_VER.major && v.minor >= LIBGIT2_MIN_VER.minor end @testset "Check library features" begin f = LibGit2.features() @test findfirst(isequal(LibGit2.Consts.FEATURE_SSH), f) !== nothing @test findfirst(isequal(LibGit2.Consts.FEATURE_HTTPS), f) !== nothing end @testset "OID" begin z = LibGit2.GitHash() @test LibGit2.iszero(z) @test z == zero(LibGit2.GitHash) @test z == LibGit2.GitHash(z) rs = string(z) rr = LibGit2.raw(z) @test z == LibGit2.GitHash(rr) @test z == LibGit2.GitHash(rs) @test z == LibGit2.GitHash(pointer(rr)) @test LibGit2.GitShortHash(z, 20) == LibGit2.GitShortHash(rs[1:20]) @test_throws ArgumentError LibGit2.GitHash(Ptr{UInt8}(C_NULL)) @test_throws ArgumentError LibGit2.GitHash(rand(UInt8, 2*LibGit2.OID_RAWSZ)) @test_throws ArgumentError LibGit2.GitHash("a") end @testset "StrArrayStruct" begin p = ["XXX","YYY"] a = Base.cconvert(Ptr{LibGit2.StrArrayStruct}, p) b = Base.unsafe_convert(Ptr{LibGit2.StrArrayStruct}, a) @test p == collect(unsafe_load(b)) @noinline gcuse(a) = a gcuse(a) end @testset "Signature" begin sig = LibGit2.Signature("AAA", "AAA@BBB.COM", round(time(); digits=0), 0) git_sig = convert(LibGit2.GitSignature, sig) sig2 = LibGit2.Signature(git_sig) close(git_sig) @test sig.name == sig2.name @test sig.email == sig2.email @test sig.time == sig2.time sig3 = LibGit2.Signature("AAA","AAA@BBB.COM") @test sig3.name == sig.name @test sig3.email == sig.email end @testset "Default config" begin with_libgit2_temp_home() do tmphome cfg = LibGit2.GitConfig() @test isa(cfg, LibGit2.GitConfig) @test LibGit2.getconfig("fake.property", "") == "" LibGit2.set!(cfg, "fake.property", "AAAA") @test LibGit2.getconfig("fake.property", "") == "AAAA" end end @testset "Trace" begin code = "import LibGit2; LibGit2.trace_set(LibGit2.Consts.TRACE_DEBUG); exit(LibGit2.trace_set(0))" run(`$(Base.julia_cmd()) --startup-file=no -e $code`) end # See #21872 and #21636 LibGit2.version() >= v"0.26.0" && Sys.isunix() && @testset "Default config with symlink" begin with_libgit2_temp_home() do tmphome write(joinpath(tmphome, "real_gitconfig"), "[fake]\n\tproperty = BBB") symlink(joinpath(tmphome, "real_gitconfig"), joinpath(tmphome, ".gitconfig")) cfg = LibGit2.GitConfig() @test isa(cfg, LibGit2.GitConfig) LibGit2.getconfig("fake.property", "") == "BBB" LibGit2.set!(cfg, "fake.property", "AAAA") LibGit2.getconfig("fake.property", "") == "AAAA" end end @testset "Git URL parsing" begin @testset "HTTPS URL" begin m = match(LibGit2.URL_REGEX, "https://user:pass@server.com:80/org/project.git") @test m[:scheme] == "https" @test m[:user] == "user" @test m[:password] == "pass" @test m[:host] == "server.com" @test m[:port] == "80" @test m[:path] == "org/project.git" end @testset "SSH URL" begin m = match(LibGit2.URL_REGEX, "ssh://user:pass@server:22/project.git") @test m[:scheme] == "ssh" @test m[:user] == "user" @test m[:password] == "pass" @test m[:host] == "server" @test m[:port] == "22" @test m[:path] == "project.git" end @testset "SSH URL, scp-like syntax" begin m = match(LibGit2.URL_REGEX, "user@server:project.git") @test m[:scheme] === nothing @test m[:user] == "user" @test m[:password] === nothing @test m[:host] == "server" @test m[:port] === nothing @test m[:path] == "project.git" end # scp-like syntax corner case. The SCP syntax does not support port so everything after # the colon is part of the path. @testset "scp-like syntax, no port" begin m = match(LibGit2.URL_REGEX, "server:1234/repo") @test m[:scheme] === nothing @test m[:user] === nothing @test m[:password] === nothing @test m[:host] == "server" @test m[:port] === nothing @test m[:path] == "1234/repo" end @testset "HTTPS URL, realistic" begin m = match(LibGit2.URL_REGEX, "https://github.com/JuliaLang/Example.jl.git") @test m[:scheme] == "https" @test m[:user] === nothing @test m[:password] === nothing @test m[:host] == "github.com" @test m[:port] === nothing @test m[:path] == "JuliaLang/Example.jl.git" end @testset "SSH URL, realistic" begin m = match(LibGit2.URL_REGEX, "git@github.com:JuliaLang/Example.jl.git") @test m[:scheme] === nothing @test m[:user] == "git" @test m[:password] === nothing @test m[:host] == "github.com" @test m[:port] === nothing @test m[:path] == "JuliaLang/Example.jl.git" end @testset "usernames with special characters" begin m = match(LibGit2.URL_REGEX, "user-name@hostname.com") @test m[:user] == "user-name" end @testset "HTTPS URL, no path" begin m = match(LibGit2.URL_REGEX, "https://user:pass@server.com:80") @test m[:path] === nothing end @testset "scp-like syntax, no path" begin m = match(LibGit2.URL_REGEX, "user@server:") @test m[:path] == "" m = match(LibGit2.URL_REGEX, "user@server") @test m[:path] === nothing end @testset "HTTPS URL, invalid path" begin m = match(LibGit2.URL_REGEX, "https://git@server:repo") @test m === nothing end # scp-like syntax should have a colon separating the hostname from the path @testset "scp-like syntax, invalid path" begin m = match(LibGit2.URL_REGEX, "git@server/repo") @test m === nothing end end @testset "Git URL formatting" begin @testset "HTTPS URL" begin url = LibGit2.git_url( scheme="https", username="user", host="server.com", port=80, path="org/project.git") @test url == "https://user@server.com:80/org/project.git" end @testset "SSH URL" begin url = LibGit2.git_url( scheme="ssh", username="user", host="server", port="22", path="project.git") @test url == "ssh://user@server:22/project.git" end @testset "SSH URL, scp-like syntax" begin url = LibGit2.git_url( username="user", host="server", path="project.git") @test url == "user@server:project.git" end @testset "HTTPS URL, realistic" begin url = LibGit2.git_url( scheme="https", host="github.com", path="JuliaLang/Example.jl.git") @test url == "https://github.com/JuliaLang/Example.jl.git" end @testset "SSH URL, realistic" begin url = LibGit2.git_url( username="git", host="github.com", path="JuliaLang/Example.jl.git") @test url == "git@github.com:JuliaLang/Example.jl.git" end @testset "HTTPS URL, no path" begin url = LibGit2.git_url( scheme="https", username="user", host="server.com", port="80") @test url == "https://user@server.com:80" end @testset "scp-like syntax, no path" begin url = LibGit2.git_url( username="user", host="server.com") @test url == "user@server.com" end @testset "HTTP URL, path includes slash prefix" begin url = LibGit2.git_url( scheme="http", host="server.com", path="/path") @test url == "http://server.com/path" end @testset "empty" begin @test_throws ArgumentError LibGit2.git_url() @test LibGit2.git_url(host="server.com") == "server.com" url = LibGit2.git_url( scheme="", username="", host="server.com", port="", path="") @test url == "server.com" end end @testset "Passphrase Required" begin @testset "missing file" begin @test !LibGit2.is_passphrase_required("") file = joinpath(KEY_DIR, "foobar") @test !isfile(file) @test !LibGit2.is_passphrase_required(file) end @testset "not private key" begin @test !LibGit2.is_passphrase_required(joinpath(KEY_DIR, "invalid.pub")) end @testset "private key, with passphrase" begin @test LibGit2.is_passphrase_required(joinpath(KEY_DIR, "valid-passphrase")) end @testset "private key, no passphrase" begin @test !LibGit2.is_passphrase_required(joinpath(KEY_DIR, "valid")) end end @testset "GitCredential" begin @testset "missing" begin str = "" cred = read!(IOBuffer(str), LibGit2.GitCredential()) @test cred == LibGit2.GitCredential() @test sprint(write, cred) == str Base.shred!(cred) end @testset "empty" begin str = """ protocol= host= path= username= password= """ cred = read!(IOBuffer(str), LibGit2.GitCredential()) @test cred == LibGit2.GitCredential("", "", "", "", "") @test sprint(write, cred) == str Base.shred!(cred) end @testset "input/output" begin str = """ protocol=https host=example.com username=alice password=***** """ expected_cred = LibGit2.GitCredential("https", "example.com", nothing, "alice", "*****") cred = read!(IOBuffer(str), LibGit2.GitCredential()) @test cred == expected_cred @test sprint(write, cred) == str Base.shred!(cred) Base.shred!(expected_cred) end @testset "extra newline" begin # The "Git for Windows" installer will also install the "Git Credential Manager for # Windows" (https://github.com/Microsoft/Git-Credential-Manager-for-Windows) (also # known as "manager" in the .gitconfig files). This credential manager returns an # additional newline when returning the results. str = """ protocol=https host=example.com path= username=bob password=***** """ expected_cred = LibGit2.GitCredential("https", "example.com", "", "bob", "*****") cred = read!(IOBuffer(str), LibGit2.GitCredential()) @test cred == expected_cred @test sprint(write, cred) * "\n" == str Base.shred!(cred) Base.shred!(expected_cred) end @testset "unknown attribute" begin str = """ protocol=https host=example.com attribute=value username=bob password=***** """ expected_cred = LibGit2.GitCredential("https", "example.com", nothing, "bob", "*****") expected_log = (:warn, "Unknown git credential attribute found: \"attribute\"") cred = @test_logs expected_log read!(IOBuffer(str), LibGit2.GitCredential()) @test cred == expected_cred Base.shred!(cred) Base.shred!(expected_cred) end @testset "use http path" begin cred = LibGit2.GitCredential("https", "example.com", "dir/file", "alice", "*****") expected = """ protocol=https host=example.com username=alice password=***** """ @test cred.use_http_path cred.use_http_path = false @test cred.path == "dir/file" @test sprint(write, cred) == expected Base.shred!(cred) end @testset "URL input/output" begin str = """ host=example.com password=bar url=https://a@b/c username=foo """ expected_str = """ protocol=https host=b path=c username=foo """ expected_cred = LibGit2.GitCredential("https", "b", "c", "foo", nothing) cred = read!(IOBuffer(str), LibGit2.GitCredential()) @test cred == expected_cred @test sprint(write, cred) == expected_str Base.shred!(cred) Base.shred!(expected_cred) end @testset "ismatch" begin # Equal cred = LibGit2.GitCredential("https", "github.com") @test LibGit2.ismatch("https://github.com", cred) Base.shred!(cred) # Credential hostname is different cred = LibGit2.GitCredential("https", "github.com") @test !LibGit2.ismatch("https://myhost", cred) Base.shred!(cred) # Credential is less specific than URL cred = LibGit2.GitCredential("https") @test !LibGit2.ismatch("https://github.com", cred) Base.shred!(cred) # Credential is more specific than URL cred = LibGit2.GitCredential("https", "github.com", "path", "user", "pass") @test LibGit2.ismatch("https://github.com", cred) Base.shred!(cred) # Credential needs to have an "" username to match cred = LibGit2.GitCredential("https", "github.com", nothing, "") @test LibGit2.ismatch("https://@github.com", cred) Base.shred!(cred) cred = LibGit2.GitCredential("https", "github.com", nothing, nothing) @test !LibGit2.ismatch("https://@github.com", cred) Base.shred!(cred) end @testset "GITHUB_REGEX" begin github_regex_test = function(url, user, repo) m = match(LibGit2.GITHUB_REGEX, url) @test m !== nothing @test m[1] == "$user/$repo" @test m[2] == user @test m[3] == repo end user = "User" repo = "Repo" github_regex_test("git@github.com/$user/$repo.git", user, repo) github_regex_test("https://github.com/$user/$repo.git", user, repo) github_regex_test("https://username@github.com/$user/$repo.git", user, repo) github_regex_test("ssh://git@github.com/$user/$repo.git", user, repo) github_regex_test("git@github.com/$user/$repo", user, repo) github_regex_test("https://github.com/$user/$repo", user, repo) github_regex_test("https://username@github.com/$user/$repo", user, repo) github_regex_test("ssh://git@github.com/$user/$repo", user, repo) @test !occursin(LibGit2.GITHUB_REGEX, "git@notgithub.com/$user/$repo.git") end @testset "UserPasswordCredential/url constructor" begin user_pass_cred = LibGit2.UserPasswordCredential("user", "*******") url = "https://github.com" expected_cred = LibGit2.GitCredential("https", "github.com", nothing, "user", "*******") cred = LibGit2.GitCredential(user_pass_cred, url) @test cred == expected_cred # Shredding the UserPasswordCredential shouldn't result in information being lost # inside of a GitCredential. Base.shred!(user_pass_cred) @test cred == expected_cred Base.shred!(cred) Base.shred!(expected_cred) end end mktempdir() do dir dir = realpath(dir) # test parameters repo_url = "https://github.com/JuliaLang/Example.jl" cache_repo = joinpath(dir, "Example") test_repo = joinpath(dir, "Example.Test") test_sig = LibGit2.Signature("TEST", "TEST@TEST.COM", round(time(); digits=0), 0) test_dir = "testdir" test_file = "$(test_dir)/testfile" config_file = "testconfig" commit_msg1 = randstring(10) commit_msg2 = randstring(10) commit_oid1 = LibGit2.GitHash() commit_oid2 = LibGit2.GitHash() commit_oid3 = LibGit2.GitHash() default_branch = LibGit2.getconfig("init.defaultBranch", "master") test_branch = "test_branch" test_branch2 = "test_branch_two" tag1 = "tag1" tag2 = "tag2" @testset "Configuration" begin LibGit2.with(LibGit2.GitConfig(joinpath(dir, config_file), LibGit2.Consts.CONFIG_LEVEL_APP)) do cfg @test_throws LibGit2.Error.GitError LibGit2.get(AbstractString, cfg, "tmp.str") @test isempty(LibGit2.get(cfg, "tmp.str", "")) == true LibGit2.set!(cfg, "tmp.str", "AAAA") LibGit2.set!(cfg, "tmp.int32", Int32(1)) LibGit2.set!(cfg, "tmp.int64", Int64(1)) LibGit2.set!(cfg, "tmp.bool", true) @test LibGit2.get(cfg, "tmp.str", "") == "AAAA" @test LibGit2.get(cfg, "tmp.int32", Int32(0)) == Int32(1) @test LibGit2.get(cfg, "tmp.int64", Int64(0)) == Int64(1) @test LibGit2.get(cfg, "tmp.bool", false) == true # Ordering of entries appears random when using `LibGit2.set!` count = 0 for entry in LibGit2.GitConfigIter(cfg, r"tmp.*") count += 1 name, value = unsafe_string(entry.name), unsafe_string(entry.value) if name == "tmp.str" @test value == "AAAA" elseif name == "tmp.int32" @test value == "1" elseif name == "tmp.int64" @test value == "1" elseif name == "tmp.bool" @test value == "true" else error("Found unexpected entry: $name") end show_str = sprint(show, entry) @test show_str == string("ConfigEntry(\"", name, "\", \"", value, "\")") end @test count == 4 end end @testset "Configuration Iteration" begin config_path = joinpath(dir, config_file) # Write config entries with duplicate names open(config_path, "a") do fp write(fp, """ [credential] helper = store username = julia [credential] helper = cache """) end LibGit2.with(LibGit2.GitConfig(config_path, LibGit2.Consts.CONFIG_LEVEL_APP)) do cfg # Will only see the last entry @test LibGit2.get(cfg, "credential.helper", "") == "cache" count = 0 for entry in LibGit2.GitConfigIter(cfg, "credential.helper") count += 1 name, value = unsafe_string(entry.name), unsafe_string(entry.value) @test name == "credential.helper" @test value == (count == 1 ? "store" : "cache") end @test count == 2 end end @testset "Initializing repository" begin @testset "with remote branch" begin LibGit2.with(LibGit2.init(cache_repo)) do repo @test isdir(cache_repo) @test LibGit2.path(repo) == LibGit2.posixpath(realpath(cache_repo)) @test isdir(joinpath(cache_repo, ".git")) # set a remote branch branch = "upstream" LibGit2.GitRemote(repo, branch, repo_url) |> close # test remote's representation in the repo's config config = joinpath(cache_repo, ".git", "config") lines = split(open(x->read(x, String), config, "r"), "\n") @test any(map(x->x == "[remote \"upstream\"]", lines)) LibGit2.with(LibGit2.get(LibGit2.GitRemote, repo, branch)) do remote # test various remote properties @test LibGit2.url(remote) == repo_url @test LibGit2.push_url(remote) == "" @test LibGit2.name(remote) == "upstream" @test isa(remote, LibGit2.GitRemote) # test showing a GitRemote object @test sprint(show, remote) == "GitRemote:\nRemote name: upstream url: $repo_url" end # test setting and getting the remote's URL @test LibGit2.isattached(repo) LibGit2.set_remote_url(repo, "upstream", "unknown") LibGit2.with(LibGit2.get(LibGit2.GitRemote, repo, branch)) do remote @test LibGit2.url(remote) == "unknown" @test LibGit2.push_url(remote) == "unknown" @test sprint(show, remote) == "GitRemote:\nRemote name: upstream url: unknown" end LibGit2.set_remote_url(cache_repo, "upstream", repo_url) LibGit2.with(LibGit2.get(LibGit2.GitRemote, repo, branch)) do remote @test LibGit2.url(remote) == repo_url @test LibGit2.push_url(remote) == repo_url @test sprint(show, remote) == "GitRemote:\nRemote name: upstream url: $repo_url" LibGit2.add_fetch!(repo, remote, "upstream") # test setting fetch and push refspecs @test LibGit2.fetch_refspecs(remote) == String["+refs/heads/*:refs/remotes/upstream/*"] LibGit2.add_push!(repo, remote, "refs/heads/master") end LibGit2.with(LibGit2.get(LibGit2.GitRemote, repo, branch)) do remote @test LibGit2.push_refspecs(remote) == String["refs/heads/master"] end # constructor with a refspec LibGit2.with(LibGit2.GitRemote(repo, "upstream2", repo_url, "upstream")) do remote @test sprint(show, remote) == "GitRemote:\nRemote name: upstream2 url: $repo_url" @test LibGit2.fetch_refspecs(remote) == String["upstream"] end LibGit2.with(LibGit2.GitRemoteAnon(repo, repo_url)) do remote @test LibGit2.url(remote) == repo_url @test LibGit2.push_url(remote) == "" @test LibGit2.name(remote) == "" @test isa(remote, LibGit2.GitRemote) end end end @testset "bare" begin path = joinpath(dir, "Example.Bare") LibGit2.with(LibGit2.init(path, true)) do repo @test isdir(path) @test LibGit2.path(repo) == LibGit2.posixpath(realpath(path)) @test isfile(joinpath(path, LibGit2.Consts.HEAD_FILE)) @test LibGit2.isattached(repo) end path = joinpath("garbagefakery", "Example.Bare") try LibGit2.GitRepo(path) error("unexpected") catch e @test typeof(e) == LibGit2.GitError @test startswith( lowercase(sprint(show, e)), lowercase("GitError(Code:ENOTFOUND, Class:OS, failed to resolve path")) end path = joinpath(dir, "Example.BareTwo") LibGit2.with(LibGit2.init(path, true)) do repo #just to see if this works LibGit2.cleanup(repo) end end end @testset "Cloning repository" begin function bare_repo_tests(repo, repo_path) @test isdir(repo_path) @test LibGit2.path(repo) == LibGit2.posixpath(realpath(repo_path)) @test isfile(joinpath(repo_path, LibGit2.Consts.HEAD_FILE)) @test LibGit2.isattached(repo) @test LibGit2.remotes(repo) == ["origin"] end @testset "bare" begin repo_path = joinpath(dir, "Example.Bare1") LibGit2.with(LibGit2.clone(cache_repo, repo_path, isbare = true)) do repo bare_repo_tests(repo, repo_path) end end @testset "bare with remote callback" begin repo_path = joinpath(dir, "Example.Bare2") LibGit2.with(LibGit2.clone(cache_repo, repo_path, isbare = true, remote_cb = LibGit2.mirror_cb())) do repo bare_repo_tests(repo, repo_path) LibGit2.with(LibGit2.get(LibGit2.GitRemote, repo, "origin")) do rmt @test LibGit2.fetch_refspecs(rmt)[1] == "+refs/*:refs/*" end end end @testset "normal" begin LibGit2.with(LibGit2.clone(cache_repo, test_repo)) do repo @test isdir(test_repo) @test LibGit2.path(repo) == LibGit2.posixpath(realpath(test_repo)) @test isdir(joinpath(test_repo, ".git")) @test LibGit2.workdir(repo) == LibGit2.path(repo)*"/" @test LibGit2.isattached(repo) @test LibGit2.isorphan(repo) repo_str = sprint(show, repo) @test repo_str == "LibGit2.GitRepo($(sprint(show,LibGit2.path(repo))))" end end @testset "credentials callback conflict" begin callbacks = LibGit2.Callbacks(:credentials => (C_NULL, 0)) cred_payload = LibGit2.CredentialPayload() @test_throws ArgumentError LibGit2.clone(cache_repo, test_repo, callbacks=callbacks, credentials=cred_payload) end @testset "shallow clone" begin @static if LibGit2.VERSION >= v"1.7.0" # Note: Shallow clones are not supported with local file:// transport # This is a limitation in libgit2 - shallow clones only work with # network protocols (http, https, git, ssh) # See online-tests.jl for tests with remote repositories # Test normal clone is not shallow normal_path = joinpath(dir, "Example.NotShallow") LibGit2.with(LibGit2.clone(cache_repo, normal_path)) do repo @test !LibGit2.isshallow(repo) end else # Test that depth parameter throws error on older libgit2 @test_throws ArgumentError LibGit2.clone(cache_repo, joinpath(dir, "Example.Shallow"), depth=1) end end end @testset "Update cache repository" begin @testset "with commits" begin repo = LibGit2.GitRepo(cache_repo) repo_dir = joinpath(cache_repo,test_dir) mkdir(repo_dir) repo_file = open(joinpath(cache_repo,test_file), "a") try # create commits println(repo_file, commit_msg1) flush(repo_file) LibGit2.add!(repo, test_file) @test LibGit2.iszero(commit_oid1) commit_oid1 = LibGit2.commit(repo, commit_msg1; author=test_sig, committer=test_sig) @test !LibGit2.iszero(commit_oid1) @test LibGit2.GitHash(LibGit2.head(cache_repo)) == commit_oid1 println(repo_file, randstring(10)) flush(repo_file) LibGit2.add!(repo, test_file) commit_oid3 = LibGit2.commit(repo, randstring(10); author=test_sig, committer=test_sig) println(repo_file, commit_msg2) flush(repo_file) LibGit2.add!(repo, test_file) @test LibGit2.iszero(commit_oid2) commit_oid2 = LibGit2.commit(repo, commit_msg2; author=test_sig, committer=test_sig) @test !LibGit2.iszero(commit_oid2) # test getting list of commit authors auths = LibGit2.authors(repo) @test length(auths) == 3 for auth in auths @test auth.name == test_sig.name @test auth.time == test_sig.time @test auth.email == test_sig.email end # check various commit properties - commit_oid1 happened before # commit_oid2, so it *is* an ancestor of commit_oid2 @test LibGit2.is_ancestor_of(string(commit_oid1), string(commit_oid2), repo) @test LibGit2.iscommit(string(commit_oid1), repo) @test !LibGit2.iscommit(string(commit_oid1)*"fake", repo) @test LibGit2.iscommit(string(commit_oid2), repo) # lookup commits LibGit2.with(LibGit2.GitCommit(repo, commit_oid1)) do cmt @test LibGit2.Consts.OBJECT(typeof(cmt)) == LibGit2.Consts.OBJ_COMMIT @test commit_oid1 == LibGit2.GitHash(cmt) short_oid1 = LibGit2.GitShortHash(string(commit_oid1)) @test string(commit_oid1) == string(short_oid1) @test cmp(commit_oid1, short_oid1) == 0 @test cmp(short_oid1, commit_oid1) == 0 @test !(short_oid1 < commit_oid1) # test showing ShortHash short_str = sprint(show, short_oid1) @test short_str == "GitShortHash(\"$(string(short_oid1))\")" short_oid2 = LibGit2.GitShortHash(cmt) @test startswith(string(commit_oid1), string(short_oid2)) LibGit2.with(LibGit2.GitCommit(repo, short_oid2)) do cmt2 @test commit_oid1 == LibGit2.GitHash(cmt2) end # check that the author and committer signatures are correct auth = LibGit2.author(cmt) @test isa(auth, LibGit2.Signature) @test auth.name == test_sig.name @test auth.time == test_sig.time @test auth.email == test_sig.email short_auth = LibGit2.author(LibGit2.GitCommit(repo, short_oid1)) @test short_auth.name == test_sig.name @test short_auth.time == test_sig.time @test short_auth.email == test_sig.email cmtr = LibGit2.committer(cmt) @test isa(cmtr, LibGit2.Signature) @test cmtr.name == test_sig.name @test cmtr.time == test_sig.time @test cmtr.email == test_sig.email @test LibGit2.message(cmt) == commit_msg1 # test that the parent is correct @test LibGit2.parentcount(cmt) == 0 LibGit2.with(LibGit2.GitCommit(repo, commit_oid3)) do cmt3 @test LibGit2.parentcount(cmt3) == 1 @test LibGit2.parent_id(cmt3, 1) == commit_oid1 @test LibGit2.GitHash(LibGit2.parent(cmt3, 1)) == commit_oid1 end # test showing the commit showstr = split(sprint(show, cmt), "\n") # the time of the commit will vary so just test the first two parts @test occursin("Git Commit:", showstr[1]) @test occursin("Commit Author: Name: TEST, Email: TEST@TEST.COM, Time:", showstr[2]) @test occursin("Committer: Name: TEST, Email: TEST@TEST.COM, Time:", showstr[3]) @test occursin("SHA:", showstr[4]) @test showstr[5] == "Message:" @test showstr[6] == commit_msg1 @test LibGit2.revcount(repo, string(commit_oid1), string(commit_oid3)) == (-1,0) blame = LibGit2.GitBlame(repo, test_file) @test LibGit2.counthunks(blame) == 3 @test_throws BoundsError getindex(blame, LibGit2.counthunks(blame)+1) @test_throws BoundsError getindex(blame, 0) sig = LibGit2.Signature(blame[1].orig_signature) @test sig.name == cmtr.name @test sig.email == cmtr.email show_strs = split(sprint(show, blame[1]), "\n") @test show_strs[1] == "GitBlameHunk:" @test show_strs[2] == "Original path: $test_file" @test show_strs[3] == "Lines in hunk: 1" @test show_strs[4] == "Final commit oid: $commit_oid1" @test show_strs[6] == "Original commit oid: $commit_oid1" @test length(show_strs) == 7 end finally close(repo) close(repo_file) end end @testset "with branch" begin LibGit2.with(LibGit2.GitRepo(cache_repo)) do repo brnch = LibGit2.branch(repo) LibGit2.with(LibGit2.head(repo)) do brref # various branch properties @test LibGit2.isbranch(brref) @test !LibGit2.isremote(brref) @test LibGit2.name(brref) == "refs/heads/$(default_branch)" @test LibGit2.shortname(brref) == default_branch @test LibGit2.ishead(brref) @test LibGit2.upstream(brref) === nothing # showing the GitReference to this branch show_strs = split(sprint(show, brref), "\n") @test show_strs[1] == "GitReference:" @test show_strs[2] == "Branch with name refs/heads/$(default_branch)" @test show_strs[3] == "Branch is HEAD." @test repo.ptr == LibGit2.repository(brref).ptr @test brnch == default_branch @test LibGit2.headname(repo) == default_branch # create a branch *without* setting its tip as HEAD LibGit2.branch!(repo, test_branch, string(commit_oid1), set_head=false) # null because we are looking for a REMOTE branch @test LibGit2.lookup_branch(repo, test_branch, true) === nothing # not nothing because we are now looking for a LOCAL branch LibGit2.with(LibGit2.lookup_branch(repo, test_branch, false)) do tbref @test LibGit2.shortname(tbref) == test_branch @test LibGit2.upstream(tbref) === nothing end @test LibGit2.lookup_branch(repo, test_branch2, true) === nothing # test deleting the branch LibGit2.branch!(repo, test_branch2; set_head=false) LibGit2.with(LibGit2.lookup_branch(repo, test_branch2, false)) do tbref @test LibGit2.shortname(tbref) == test_branch2 LibGit2.delete_branch(tbref) @test LibGit2.lookup_branch(repo, test_branch2, true) === nothing end # an invalid branch name propagates the underlying error # instead of a generic "cannot create branch" error err = try LibGit2.branch!(repo, "invalid name", string(commit_oid1), set_head=false, force=true) nothing catch ex ex end @test err isa LibGit2.Error.GitError @test err.code == LibGit2.Error.EINVALIDSPEC end branches = map(b->LibGit2.shortname(b[1]), LibGit2.GitBranchIter(repo)) @test default_branch in branches @test test_branch in branches end end @testset "with default configuration" begin LibGit2.with(LibGit2.GitRepo(cache_repo)) do repo try LibGit2.Signature(repo) catch ex # these test configure repo with new signature # in case when global one does not exist @test isa(ex, LibGit2.Error.GitError) == true cfg = LibGit2.GitConfig(repo) LibGit2.set!(cfg, "user.name", "AAAA") LibGit2.set!(cfg, "user.email", "BBBB@BBBB.COM") sig = LibGit2.Signature(repo) @test sig.name == "AAAA" @test sig.email == "BBBB@BBBB.COM" @test LibGit2.getconfig(repo, "user.name", "") == "AAAA" @test LibGit2.getconfig(cache_repo, "user.name", "") == "AAAA" end end end @testset "with tags" begin LibGit2.with(LibGit2.GitRepo(cache_repo)) do repo tags = LibGit2.tag_list(repo) @test length(tags) == 0 # create tag and extract it from a GitReference tag_oid1 = LibGit2.tag_create(repo, tag1, commit_oid1, sig=test_sig) @test !LibGit2.iszero(tag_oid1) tags = LibGit2.tag_list(repo) @test length(tags) == 1 @test tag1 in tags tag1ref = LibGit2.GitReference(repo, "refs/tags/$tag1") # because this is a reference to an OID @test isempty(LibGit2.fullname(tag1ref)) # test showing a GitReference to a GitTag, and the GitTag itself show_strs = split(sprint(show, tag1ref), "\n") @test show_strs[1] == "GitReference:" @test show_strs[2] == "Tag with name refs/tags/$tag1" tag1tag = LibGit2.peel(LibGit2.GitTag, tag1ref) @test LibGit2.name(tag1tag) == tag1 @test LibGit2.target(tag1tag) == commit_oid1 @test sprint(show, tag1tag) == "GitTag:\nTag name: $tag1 target: $commit_oid1" # peels to the commit the tag points to tag1cmt = LibGit2.peel(tag1ref) @test LibGit2.GitHash(tag1cmt) == commit_oid1 tag_oid2 = LibGit2.tag_create(repo, tag2, commit_oid2) @test !LibGit2.iszero(tag_oid2) tags = LibGit2.tag_list(repo) @test length(tags) == 2 @test tag2 in tags refs = LibGit2.ref_list(repo) @test refs == ["refs/heads/$(default_branch)", "refs/heads/test_branch", "refs/tags/tag1", "refs/tags/tag2"] # test deleting a tag LibGit2.tag_delete(repo, tag1) tags = LibGit2.tag_list(repo) @test length(tags) == 1 @test tag2 ∈ tags @test tag1 ∉ tags # test git describe functions applied to these GitTags description = LibGit2.GitDescribeResult(repo) fmtted_description = LibGit2.format(description) @test sprint(show, description) == "GitDescribeResult:\n$fmtted_description\n" @test fmtted_description == "tag2" description = LibGit2.GitDescribeResult(LibGit2.GitObject(repo, "HEAD")) fmtted_description = LibGit2.format(description) @test sprint(show, description) == "GitDescribeResult:\n$fmtted_description\n" @test fmtted_description == "tag2" end end @testset "status" begin LibGit2.with(LibGit2.GitRepo(cache_repo)) do repo status = LibGit2.GitStatus(repo) @test length(status) == 0 @test_throws BoundsError status[1] repo_file = open(joinpath(cache_repo,"statusfile"), "a") # create commits println(repo_file, commit_msg1) flush(repo_file) LibGit2.add!(repo, test_file) status = LibGit2.GitStatus(repo) @test length(status) != 0 @test_throws BoundsError status[0] @test_throws BoundsError status[length(status)+1] # we've added a file - show that it is new @test status[1].status == LibGit2.Consts.STATUS_WT_NEW close(repo_file) end end @testset "blobs" begin LibGit2.with(LibGit2.GitRepo(cache_repo)) do repo # this is slightly dubious, as it assumes the object has not been packed # could be replaced by another binary format hash_string = string(commit_oid1) blob_file = joinpath(cache_repo,".git/objects", hash_string[1:2], hash_string[3:end]) id = LibGit2.addblob!(repo, blob_file) blob = LibGit2.GitBlob(repo, id) @test LibGit2.isbinary(blob) len1 = length(blob) # test showing a GitBlob blob_show_strs = split(sprint(show, blob), "\n") @test blob_show_strs[1] == "GitBlob:" @test occursin("Blob id:", blob_show_strs[2]) @test blob_show_strs[3] == "Contents are binary." blob2 = LibGit2.GitBlob(repo, LibGit2.GitHash(blob)) @test LibGit2.isbinary(blob2) @test length(blob2) == len1 @test blob == blob2 @test blob !== blob2 # showing a text blob previews only the first 3 lines text_file = joinpath(dir, "long_text_blob") write(text_file, "l1\nl2\nl3\nl4\nl5\n") text_blob = LibGit2.GitBlob(repo, LibGit2.addblob!(repo, text_file)) @test sprint(show, text_blob) == "GitBlob:\nBlob id: $(LibGit2.GitHash(text_blob))\nContents:\nl1\nl2\nl3\n" # showing a text blob with fewer than 3 lines should not error short_file = joinpath(dir, "short_text_blob") write(short_file, "l1\n") short_blob = LibGit2.GitBlob(repo, LibGit2.addblob!(repo, short_file)) @test sprint(show, short_blob) == "GitBlob:\nBlob id: $(LibGit2.GitHash(short_blob))\nContents:\nl1\n\n" end end @testset "trees" begin LibGit2.with(LibGit2.GitRepo(cache_repo)) do repo @test_throws LibGit2.Error.GitError LibGit2.GitTree(repo, "HEAD") tree = LibGit2.GitTree(repo, "HEAD^{tree}") @test isa(tree, LibGit2.GitTree) @test isa(LibGit2.GitObject(repo, "HEAD^{tree}"), LibGit2.GitTree) @test LibGit2.Consts.OBJECT(typeof(tree)) == LibGit2.Consts.OBJ_TREE @test LibGit2.count(tree) == 1 # test showing the GitTree and its entries tree_str = sprint(show, tree) @test tree_str == "GitTree:\nOwner: $(LibGit2.repository(tree))\nNumber of entries: 1\n" @test_throws BoundsError tree[0] @test_throws BoundsError tree[2] tree_entry = tree[1] subtree = LibGit2.GitTree(tree_entry) @test_throws BoundsError subtree[0] @test_throws BoundsError subtree[2] tree_entry = subtree[1] @test LibGit2.filemode(tree_entry) == 33188 te_str = sprint(show, tree_entry) ref_te_str = "GitTreeEntry:\nEntry name: testfile\nEntry type: LibGit2.GitBlob\nEntry OID: " ref_te_str *= "$(LibGit2.entryid(tree_entry))\n" @test te_str == ref_te_str blob = LibGit2.GitBlob(tree_entry) blob_str = sprint(show, blob) blob_lines = split(LibGit2.content(blob), "\n") expected = "GitBlob:\nBlob id: $(LibGit2.GitHash(blob))\nContents:\n" * join([blob_lines[i] for i in 1:min(length(blob_lines), 3)], "\n") * "\n" @test blob_str == expected # tests for walking the tree and accessing objects @test tree[""] == tree @test tree["/"] == tree @test isa(tree[test_dir], LibGit2.GitTree) @test tree["$test_dir/"] == tree[test_dir] @test isa(tree[test_file], LibGit2.GitBlob) @test_throws KeyError tree["nonexistent"] # test workaround for git_tree_walk issue # https://github.com/libgit2/libgit2/issues/4693 ccall((:git_error_set_str, libgit2), Cvoid, (Cint, Cstring), Cint(LibGit2.Error.Invalid), "previous error") try # file needs to exist in tree in order to trigger the stop walk condition tree[test_file] catch err if isa(err, LibGit2.Error.GitError) && err.class == LibGit2.Error.Invalid @test false else rethrow() end end # Test GitTree constructor with GitHash tree1 = LibGit2.GitTree(repo, "HEAD^{tree}") tree_hash = LibGit2.GitHash(tree1) tree2 = LibGit2.GitTree(repo, tree_hash) @test isa(tree2, LibGit2.GitTree) @test LibGit2.GitHash(tree1) == LibGit2.GitHash(tree2) @test LibGit2.count(tree1) == LibGit2.count(tree2) end end @testset "diff" begin LibGit2.with(LibGit2.GitRepo(cache_repo)) do repo @test !LibGit2.isdirty(repo) @test !LibGit2.isdirty(repo, test_file) @test !LibGit2.isdirty(repo, "nonexistent") @test !LibGit2.isdiff(repo, "HEAD") @test !LibGit2.isdirty(repo, cached=true) @test !LibGit2.isdirty(repo, test_file, cached=true) @test !LibGit2.isdirty(repo, "nonexistent", cached=true) @test !LibGit2.isdiff(repo, "HEAD", cached=true) open(joinpath(cache_repo,test_file), "a") do f println(f, "zzzz") end @test LibGit2.isdirty(repo) @test LibGit2.isdirty(repo, test_file) @test !LibGit2.isdirty(repo, "nonexistent") @test LibGit2.isdiff(repo, "HEAD") @test !LibGit2.isdirty(repo, cached=true) @test !LibGit2.isdiff(repo, "HEAD", cached=true) LibGit2.add!(repo, test_file) @test LibGit2.isdirty(repo) @test LibGit2.isdiff(repo, "HEAD") @test LibGit2.isdirty(repo, cached=true) @test LibGit2.isdiff(repo, "HEAD", cached=true) tree = LibGit2.GitTree(repo, "HEAD^{tree}") # test properties of the diff_tree diff = LibGit2.diff_tree(repo, tree, "", cached=true) @test LibGit2.count(diff) == 1 @test_throws BoundsError diff[0] @test_throws BoundsError diff[2] @test LibGit2.Consts.DELTA_STATUS(diff[1].status) == LibGit2.Consts.DELTA_MODIFIED @test diff[1].nfiles == 2 # test showing a DiffDelta diff_strs = split(sprint(show, diff[1]), '\n') @test diff_strs[1] == "DiffDelta:" @test diff_strs[2] == "Status: DELTA_MODIFIED" @test diff_strs[3] == "Number of files: 2" @test diff_strs[4] == "Old file:" @test diff_strs[5] == "DiffFile:" @test occursin("Oid:", diff_strs[6]) @test occursin("Path:", diff_strs[7]) @test occursin("Size:", diff_strs[8]) @test isempty(diff_strs[9]) @test diff_strs[10] == "New file:" # test showing a GitDiff diff_strs = split(sprint(show, diff), '\n') @test diff_strs[1] == "GitDiff:" @test diff_strs[2] == "Number of deltas: 1" @test diff_strs[3] == "GitDiffStats:" @test diff_strs[4] == "Files changed: 1" @test diff_strs[5] == "Insertions: 1" @test diff_strs[6] == "Deletions: 0" LibGit2.commit(repo, "zzz") @test !LibGit2.isdirty(repo) @test !LibGit2.isdiff(repo, "HEAD") @test !LibGit2.isdirty(repo, cached=true) @test !LibGit2.isdiff(repo, "HEAD", cached=true) end LibGit2.with(LibGit2.GitRepo(cache_repo)) do repo diff_str = "diff --git a/test.txt b/test.txt\nindex 0000000..1111111 100644\n" @test_throws LibGit2.GitError LibGit2.GitDiff(diff_str) tree1 = LibGit2.GitTree(repo, "HEAD~1^{tree}") tree2 = LibGit2.GitTree(repo, "HEAD^{tree}") diff = LibGit2.diff_tree(repo, tree1, tree2) idx = LibGit2.apply_to_tree(repo, tree1, diff) @test idx isa LibGit2.GitIndex oid = LibGit2.write_tree_to!(repo, idx) @test oid isa LibGit2.GitHash close(idx) end end end function setup_clone_repo(cache_repo::AbstractString, path::AbstractString; name="AAAA", email="BBBB@BBBB.COM") repo = LibGit2.clone(cache_repo, path) LibGit2.fetch(repo) # need to set this for merges to succeed cfg = LibGit2.GitConfig(repo) LibGit2.set!(cfg, "user.name", name) LibGit2.set!(cfg, "user.email", email) return repo end # TO DO: add more tests for various merge # preference options function add_and_commit_file(repo, filenm, filecontent) open(joinpath(LibGit2.path(repo), filenm),"w") do f write(f, filecontent) end LibGit2.add!(repo, filenm) return LibGit2.commit(repo, "add $filenm") end @testset "Fastforward merges" begin LibGit2.with(setup_clone_repo(cache_repo, joinpath(dir, "Example.FF"))) do repo # Sets up a branch "branch/ff_a" which will be two commits ahead # of "master". It's possible to fast-forward merge "branch/ff_a" # into "master", which is the default behavior. oldhead = LibGit2.head_oid(repo) LibGit2.branch!(repo, "branch/ff_a") add_and_commit_file(repo, "ff_file1", "111\n") add_and_commit_file(repo, "ff_file2", "222\n") LibGit2.branch!(repo, "master") # switch back, now try to ff-merge the changes # from branch/a # set up the merge using GitAnnotated objects upst_ann = LibGit2.GitAnnotated(repo, "branch/ff_a") head_ann = LibGit2.GitAnnotated(repo, "master") # ff merge them @test LibGit2.merge!(repo, [upst_ann], true) @test LibGit2.is_ancestor_of(string(oldhead), string(LibGit2.head_oid(repo)), repo) # Repeat the process, but specifying a commit to merge in as opposed # to a branch name or GitAnnotated. oldhead = LibGit2.head_oid(repo) LibGit2.branch!(repo, "branch/ff_b") add_and_commit_file(repo, "ff_file3", "333\n") branchhead = add_and_commit_file(repo, "ff_file4", "444\n") LibGit2.branch!(repo, "master") # switch back, now try to ff-merge the changes # from branch/a using committish @test LibGit2.merge!(repo, committish=string(branchhead)) @test LibGit2.is_ancestor_of(string(oldhead), string(LibGit2.head_oid(repo)), repo) # Repeat the process, but specifying a branch name to merge in as opposed # to a commit or GitAnnotated. oldhead = LibGit2.head_oid(repo) LibGit2.branch!(repo, "branch/ff_c") add_and_commit_file(repo, "ff_file5", "555\n") branchhead = add_and_commit_file(repo, "ff_file6", "666\n") LibGit2.branch!(repo, "master") # switch back, now try to ff-merge the changes # from branch/ff_c using branch name @test LibGit2.merge!(repo, branch="refs/heads/branch/ff_c") @test LibGit2.is_ancestor_of(string(oldhead), string(LibGit2.head_oid(repo)), repo) LibGit2.branch!(repo, "branch/ff_d") branchhead = add_and_commit_file(repo, "ff_file7", "777\n") LibGit2.branch!(repo, "master") # switch back, now try to ff-merge the changes # from branch/a # set up the merge using GitAnnotated objects # from a fetchhead fh = LibGit2.fetchheads(repo) upst_ann = LibGit2.GitAnnotated(repo, fh[1]) @test LibGit2.merge!(repo, [upst_ann], true) @test LibGit2.is_ancestor_of(string(oldhead), string(LibGit2.head_oid(repo)), repo) end end @testset "Cherrypick" begin LibGit2.with(setup_clone_repo(cache_repo, joinpath(dir, "Example.Cherrypick"))) do repo # Create a commit on the new branch and cherry-pick it over to # master. Since the cherry-pick does *not* make a new commit on # master, we have to create our own commit of the dirty state. oldhead = LibGit2.head_oid(repo) LibGit2.branch!(repo, "branch/cherry_a") cmt_oid = add_and_commit_file(repo, "file1", "111\n") cmt = LibGit2.GitCommit(repo, cmt_oid) # switch back, try to cherrypick # from branch/cherry_a LibGit2.branch!(repo, "master") LibGit2.cherrypick(repo, cmt, options=LibGit2.CherrypickOptions()) cmt_oid2 = LibGit2.commit(repo, "add file1") @test isempty(LibGit2.diff_files(repo, "master", "branch/cherry_a")) end end @testset "Merges" begin LibGit2.with(setup_clone_repo(cache_repo, joinpath(dir, "Example.Merge"))) do repo oldhead = LibGit2.head_oid(repo) LibGit2.branch!(repo, "branch/merge_a") add_and_commit_file(repo, "file1", "111\n") # switch back, add a commit, try to merge # from branch/merge_a LibGit2.branch!(repo, default_branch) # test for showing a Reference to a non-HEAD branch brref = LibGit2.GitReference(repo, "refs/heads/branch/merge_a") @test LibGit2.name(brref) == "refs/heads/branch/merge_a" @test !LibGit2.ishead(brref) show_strs = split(sprint(show, brref), "\n") @test show_strs[1] == "GitReference:" @test show_strs[2] == "Branch with name refs/heads/branch/merge_a" @test show_strs[3] == "Branch is not HEAD." add_and_commit_file(repo, "file2", "222\n") upst_ann = LibGit2.GitAnnotated(repo, "branch/merge_a") head_ann = LibGit2.GitAnnotated(repo, default_branch) # (fail to) merge them because we can't fastforward @test_logs (:warn,"Cannot perform fast-forward merge") !LibGit2.merge!(repo, [upst_ann], true) # merge them now that we allow non-ff @test_logs (:info,"Review and commit merged changes") LibGit2.merge!(repo, [upst_ann], false) @test LibGit2.is_ancestor_of(string(oldhead), string(LibGit2.head_oid(repo)), repo) # go back to merge_a and rename a file LibGit2.branch!(repo, "branch/merge_b") mv(joinpath(LibGit2.path(repo),"file1"),joinpath(LibGit2.path(repo),"mvfile1")) LibGit2.add!(repo, "mvfile1") LibGit2.commit(repo, "move file1") LibGit2.branch!(repo, default_branch) upst_ann = LibGit2.GitAnnotated(repo, "branch/merge_b") rename_flag = Cint(0) rename_flag = LibGit2.toggle(rename_flag, Cint(0)) # turns on the find renames opt mos = LibGit2.MergeOptions(flags=rename_flag) @test_logs (:info,"Review and commit merged changes") LibGit2.merge!(repo, [upst_ann], merge_opts=mos) end end @testset "push" begin up_path = joinpath(dir, "Example.PushUp") up_repo = setup_clone_repo(cache_repo, up_path) our_repo = setup_clone_repo(cache_repo, joinpath(dir, "Example.Push")) try add_and_commit_file(our_repo, "file1", "111\n") if LibGit2.version() >= v"0.26.0" # See #21872, #21639 and #21597 # we cannot yet locally push to non-bare repos @test_throws LibGit2.GitError LibGit2.push(our_repo, remoteurl=up_path) end finally close(our_repo) close(up_repo) end @testset "credentials callback conflict" begin callbacks = LibGit2.Callbacks(:credentials => (C_NULL, 0)) cred_payload = LibGit2.CredentialPayload() LibGit2.with(LibGit2.GitRepo(joinpath(dir, "Example.Push"))) do repo @test_throws ArgumentError LibGit2.push(repo, callbacks=callbacks, credentials=cred_payload) end end end @testset "Show closed repo" begin # Make sure this doesn't crash buf = IOBuffer() Base.show(buf, LibGit2.with(identity, LibGit2.GitRepo(test_repo))) @test String(take!(buf)) == "LibGit2.GitRepo(<closed>)" end @testset "Fetch from cache repository" begin LibGit2.with(LibGit2.GitRepo(test_repo)) do repo # fetch changes @test LibGit2.fetch(repo) == 0 @test !isfile(joinpath(test_repo, test_file)) # ff merge them @test LibGit2.merge!(repo, fastforward=true) # because there was not any file we need to reset branch head_oid = LibGit2.head_oid(repo) new_head = LibGit2.reset!(repo, head_oid, LibGit2.Consts.RESET_HARD) @test isfile(joinpath(test_repo, test_file)) @test new_head == head_oid # GitAnnotated for a fetchhead fh_ann = LibGit2.GitAnnotated(repo, LibGit2.Consts.FETCH_HEAD) @test LibGit2.GitHash(fh_ann) == head_oid # Detach HEAD - no merge LibGit2.checkout!(repo, string(commit_oid3)) @test_throws LibGit2.Error.GitError LibGit2.merge!(repo, fastforward=true) # Switch to a branch without remote - no merge LibGit2.branch!(repo, test_branch) @test_throws LibGit2.Error.GitError LibGit2.merge!(repo, fastforward=true) # Set the username and email for the test_repo (needed for rebase) cfg = LibGit2.GitConfig(repo) LibGit2.set!(cfg, "user.name", "AAAA") LibGit2.set!(cfg, "user.email", "BBBB@BBBB.COM") # If upstream argument is empty, libgit2 will look for tracking # information. If the current branch isn't tracking any upstream # the rebase should fail. @test_throws LibGit2.GitError LibGit2.rebase!(repo) # Try rebasing on master instead newhead = LibGit2.rebase!(repo, default_branch) @test newhead == head_oid # Switch to the master branch LibGit2.branch!(repo, default_branch) fetch_heads = LibGit2.fetchheads(repo) @test fetch_heads[1].name == "refs/heads/$(default_branch)" @test fetch_heads[1].ismerge == true # we just merged master @test fetch_heads[2].name == "refs/heads/test_branch" @test fetch_heads[2].ismerge == false @test fetch_heads[3].name == "refs/tags/tag2" @test fetch_heads[3].ismerge == false for fh in fetch_heads @test fh.url == cache_repo fh_strs = split(sprint(show, fh), '\n') @test fh_strs[1] == "FetchHead:" @test fh_strs[2] == "Name: $(fh.name)" @test fh_strs[3] == "URL: $(fh.url)" @test fh_strs[5] == "Merged: $(fh.ismerge)" end end @testset "credentials callback conflict" begin callbacks = LibGit2.Callbacks(:credentials => (C_NULL, 0)) cred_payload = LibGit2.CredentialPayload() LibGit2.with(LibGit2.GitRepo(test_repo)) do repo @test_throws ArgumentError LibGit2.fetch(repo, callbacks=callbacks, credentials=cred_payload) end end end @testset "Examine test repository" begin @testset "files" begin @test readlines(joinpath(test_repo, test_file)) == readlines(joinpath(cache_repo, test_file)) end @testset "tags & branches" begin LibGit2.with(LibGit2.GitRepo(test_repo)) do repo # all tag in place tags = LibGit2.tag_list(repo) @test length(tags) == 1 @test tag2 in tags # all tag in place branches = map(b->LibGit2.shortname(b[1]), LibGit2.GitBranchIter(repo)) @test default_branch in branches @test test_branch in branches # issue #16337 LibGit2.with(LibGit2.GitReference(repo, "refs/tags/$tag2")) do tag2ref @test_throws LibGit2.Error.GitError LibGit2.upstream(tag2ref) end end end @testset "commits with revwalk" begin repo = LibGit2.GitRepo(test_repo) cache = LibGit2.GitRepo(cache_repo) try # test map with oid oids = LibGit2.with(LibGit2.GitRevWalker(repo)) do walker LibGit2.map((oid,repo)->(oid,repo), walker, oid=commit_oid1, by=LibGit2.Consts.SORT_TIME) end @test length(oids) == 1 # test map with range str_1 = string(commit_oid1) str_3 = string(commit_oid3) oids = LibGit2.with(LibGit2.GitRevWalker(repo)) do walker LibGit2.map((oid,repo)->(oid,repo), walker, range="$str_1..$str_3", by=LibGit2.Consts.SORT_TIME) end @test length(oids) == 1 test_oids = LibGit2.with(LibGit2.GitRevWalker(repo)) do walker LibGit2.map((oid,repo)->string(oid), walker, by = LibGit2.Consts.SORT_TIME) end cache_oids = LibGit2.with(LibGit2.GitRevWalker(cache)) do walker LibGit2.map((oid,repo)->string(oid), walker, by = LibGit2.Consts.SORT_TIME) end for i in eachindex(oids) @test cache_oids[i] == test_oids[i] end # test with specified oid LibGit2.with(LibGit2.GitRevWalker(repo)) do walker @test LibGit2.count((oid,repo)->(oid == commit_oid1), walker, oid=commit_oid1, by=LibGit2.Consts.SORT_TIME) == 1 end # test without specified oid LibGit2.with(LibGit2.GitRevWalker(repo)) do walker @test LibGit2.count((oid,repo)->(oid == commit_oid1), walker, by=LibGit2.Consts.SORT_TIME) == 1 end finally close(repo) close(cache) end end end @testset "Modify and reset repository" begin LibGit2.with(LibGit2.GitRepo(test_repo)) do repo # check index for file LibGit2.with(LibGit2.GitIndex(repo)) do idx i = findall(test_file, idx) @test i !== nothing idx_entry = idx[i] @test idx_entry !== nothing idx_entry_str = sprint(show, idx_entry) @test idx_entry_str == "IndexEntry($(string(idx_entry.id)))" @test LibGit2.stage(idx_entry) == 0 i = findall("zzz", idx) @test i === nothing idx_str = sprint(show, idx) @test idx_str == "GitIndex:\nRepository: $(LibGit2.repository(idx))\nNumber of elements: 1\n" LibGit2.remove!(repo, test_file) LibGit2.read!(repo) @test LibGit2.count(idx) == 0 LibGit2.add!(repo, test_file) LibGit2.update!(repo, test_file) @test LibGit2.count(idx) == 1 end # check non-existent file status st = LibGit2.status(repo, "XYZ") @test st === nothing # check file status st = LibGit2.status(repo, test_file) @test st !== nothing @test LibGit2.isset(st, LibGit2.Consts.STATUS_CURRENT) # modify file open(joinpath(test_repo, test_file), "a") do io write(io, 0x41) end # file modified but not staged st_mod = LibGit2.status(repo, test_file) @test !LibGit2.isset(st_mod, LibGit2.Consts.STATUS_INDEX_MODIFIED) @test LibGit2.isset(st_mod, LibGit2.Consts.STATUS_WT_MODIFIED) # stage file LibGit2.add!(repo, test_file) # modified file staged st_stg = LibGit2.status(repo, test_file) @test LibGit2.isset(st_stg, LibGit2.Consts.STATUS_INDEX_MODIFIED) @test !LibGit2.isset(st_stg, LibGit2.Consts.STATUS_WT_MODIFIED) # try to unstage to unknown commit @test_throws LibGit2.Error.GitError LibGit2.reset!(repo, "XYZ", test_file) # status should not change st_new = LibGit2.status(repo, test_file) @test st_new == st_stg # try to unstage to HEAD new_head = LibGit2.reset!(repo, LibGit2.Consts.HEAD_FILE, test_file) st_uns = LibGit2.status(repo, test_file) @test st_uns == st_mod # reset repo @test_throws LibGit2.Error.GitError LibGit2.reset!(repo, LibGit2.GitHash(), LibGit2.Consts.RESET_HARD) new_head = LibGit2.reset!(repo, LibGit2.head_oid(repo), LibGit2.Consts.RESET_HARD) open(joinpath(test_repo, test_file), "r") do io @test read(io)[end] != 0x41 end end end @testset "Modify remote" begin path = test_repo LibGit2.with(LibGit2.GitRepo(path)) do repo remote_name = "test" url = "https://test.com/repo" @test LibGit2.lookup_remote(repo, remote_name) === nothing for r in (repo, path) # Set just the fetch URL LibGit2.set_remote_fetch_url(r, remote_name, url) remote = LibGit2.lookup_remote(repo, remote_name) @test LibGit2.name(remote) == remote_name @test LibGit2.url(remote) == url @test LibGit2.push_url(remote) == "" LibGit2.remote_delete(repo, remote_name) @test LibGit2.lookup_remote(repo, remote_name) === nothing # Set just the push URL LibGit2.set_remote_push_url(r, remote_name, url) remote = LibGit2.lookup_remote(repo, remote_name) @test LibGit2.name(remote) == remote_name @test LibGit2.url(remote) == "" @test LibGit2.push_url(remote) == url LibGit2.remote_delete(repo, remote_name) @test LibGit2.lookup_remote(repo, remote_name) === nothing # Set the fetch and push URL LibGit2.set_remote_url(r, remote_name, url) remote = LibGit2.lookup_remote(repo, remote_name) @test LibGit2.name(remote) == remote_name @test LibGit2.url(remote) == url @test LibGit2.push_url(remote) == url LibGit2.remote_delete(repo, remote_name) @test LibGit2.lookup_remote(repo, remote_name) === nothing end # Invalid remote name @test_throws LibGit2.GitError LibGit2.set_remote_url(repo, "", url) @test_throws LibGit2.GitError LibGit2.set_remote_url(repo, remote_name, "") end end @testset "rebase" begin LibGit2.with(LibGit2.GitRepo(test_repo)) do repo LibGit2.branch!(repo, "branch/a") oldhead = LibGit2.head_oid(repo) add_and_commit_file(repo, "file1", "111\n") add_and_commit_file(repo, "file2", "222\n") LibGit2.branch!(repo, "branch/b") # squash last 2 commits new_head = LibGit2.reset!(repo, oldhead, LibGit2.Consts.RESET_SOFT) @test new_head == oldhead LibGit2.commit(repo, "squash file1 and file2") # add another file newhead = add_and_commit_file(repo, "file3", "333\n") @test LibGit2.diff_files(repo, "branch/a", "branch/b", filter=Set([LibGit2.Consts.DELTA_ADDED])) == ["file3"] @test LibGit2.diff_files(repo, "branch/a", "branch/b", filter=Set([LibGit2.Consts.DELTA_MODIFIED])) == [] # switch back and rebase LibGit2.branch!(repo, "branch/a") newnewhead = LibGit2.rebase!(repo, "branch/b") # issue #19624 @test newnewhead == newhead # add yet another file add_and_commit_file(repo, "file4", "444\n") # rebase with onto newhead = LibGit2.rebase!(repo, "branch/a", default_branch) newerhead = LibGit2.head_oid(repo) @test newerhead == newhead # add yet more files add_and_commit_file(repo, "file5", "555\n") pre_abort_head = add_and_commit_file(repo, "file6", "666\n") # Rebase type head_ann = LibGit2.GitAnnotated(repo, "branch/a") upst_ann = LibGit2.GitAnnotated(repo, default_branch) rb = LibGit2.GitRebase(repo, head_ann, upst_ann) @test_throws BoundsError rb[3] @test_throws BoundsError rb[0] rbo, _ = iterate(rb) rbo_str = sprint(show, rbo) @test rbo_str == "RebaseOperation($(string(rbo.id)))\nOperation type: REBASE_OPERATION_PICK\n" rb_str = sprint(show, rb) @test rb_str == "GitRebase:\nNumber: 2\nCurrently performing operation: 1\n" rbo = rb[2] rbo_str = sprint(show, rbo) @test rbo_str == "RebaseOperation($(string(rbo.id)))\nOperation type: REBASE_OPERATION_PICK\n" # test rebase abort LibGit2.abort(rb) @test LibGit2.head_oid(repo) == pre_abort_head end end @testset "merge" begin LibGit2.with(setup_clone_repo(cache_repo, joinpath(dir, "Example.simple_merge"))) do repo LibGit2.branch!(repo, "branch/merge_a") a_head = LibGit2.head_oid(repo) add_and_commit_file(repo, "merge_file1", "111\n") LibGit2.branch!(repo, default_branch) a_head_ann = LibGit2.GitAnnotated(repo, "branch/merge_a") # merge returns true if successful @test_logs (:info,"Review and commit merged changes") LibGit2.merge!(repo, [a_head_ann]) end end @testset "Transact test repository" begin LibGit2.with(LibGit2.GitRepo(test_repo)) do repo cp(joinpath(test_repo, test_file), joinpath(test_repo, "CCC")) cp(joinpath(test_repo, test_file), joinpath(test_repo, "AAA")) LibGit2.add!(repo, "AAA") @test_throws ErrorException LibGit2.transact(repo) do trepo mv(joinpath(test_repo, test_file), joinpath(test_repo, "BBB")) LibGit2.add!(trepo, "BBB") oid = LibGit2.commit(trepo, "test commit"; author=test_sig, committer=test_sig) error("Force recovery") end @test isfile(joinpath(test_repo, "AAA")) @test isfile(joinpath(test_repo, "CCC")) @test !isfile(joinpath(test_repo, "BBB")) @test isfile(joinpath(test_repo, test_file)) end end @testset "checkout_head" begin LibGit2.with(LibGit2.GitRepo(cache_repo)) do repo # modify file repo_file = open(joinpath(cache_repo,test_file), "a") println(repo_file, commit_msg1 * randstring(10)) close(repo_file) # and checkout HEAD once more LibGit2.checkout_head(repo, options=LibGit2.CheckoutOptions(checkout_strategy=LibGit2.Consts.CHECKOUT_FORCE)) @test LibGit2.headname(repo) == default_branch @test !LibGit2.isdirty(repo) end end @testset "checkout/headname" begin LibGit2.with(LibGit2.GitRepo(cache_repo)) do repo LibGit2.checkout!(repo, string(commit_oid1)) @test !LibGit2.isattached(repo) @test LibGit2.headname(repo) == "(detached from $(string(commit_oid1)[1:7]))" # the reflog message should end with the commit id (no stray paren) reflog_msg = split(last(readlines(joinpath(cache_repo, ".git", "logs", "HEAD"))), '\t')[2] @test endswith(reflog_msg, "to $(string(commit_oid1))") end end if Sys.isunix() @testset "checkout/proptest" begin LibGit2.with(LibGit2.GitRepo(test_repo)) do repo cp(joinpath(test_repo, test_file), joinpath(test_repo, "proptest")) LibGit2.add!(repo, "proptest") id1 = LibGit2.commit(repo, "test property change 1") # change in file permissions (#17610) chmod(joinpath(test_repo, "proptest"),0o744) LibGit2.add!(repo, "proptest") id2 = LibGit2.commit(repo, "test property change 2") LibGit2.checkout!(repo, string(id1)) @test !LibGit2.isdirty(repo) # change file to symlink (#18420) mv(joinpath(test_repo, "proptest"), joinpath(test_repo, "proptest2")) symlink(joinpath(test_repo, "proptest2"), joinpath(test_repo, "proptest")) LibGit2.add!(repo, "proptest", "proptest2") id3 = LibGit2.commit(repo, "test symlink change") LibGit2.checkout!(repo, string(id1)) @test !LibGit2.isdirty(repo) end end end @testset "Credentials" begin creds_user = "USER" creds_pass = Base.SecretBuffer("PASS") creds = LibGit2.UserPasswordCredential(creds_user, creds_pass) @test creds.user == creds_user @test creds.pass == creds_pass creds2 = LibGit2.UserPasswordCredential(creds_user, creds_pass) @test creds == creds2 sshcreds = LibGit2.SSHCredential(creds_user, creds_pass) @test sshcreds.user == creds_user @test sshcreds.pass == creds_pass @test sshcreds.prvkey == "" @test sshcreds.pubkey == "" sshcreds2 = LibGit2.SSHCredential(creds_user, creds_pass) @test sshcreds == sshcreds2 Base.shred!(creds) Base.shred!(creds2) Base.shred!(sshcreds) Base.shred!(sshcreds2) Base.shred!(creds_pass) end @testset "CachedCredentials" begin cache = LibGit2.CachedCredentials() url = "https://github.com/JuliaLang/Example.jl" cred_id = LibGit2.credential_identifier(url) cred = LibGit2.UserPasswordCredential("julia", "password") @test !haskey(cache, cred_id) password = Base.SecretBuffer("password") # Attempt to reject a credential which wasn't stored LibGit2.reject(cache, cred, url) @test !haskey(cache, cred_id) @test cred.user == "julia" @test cred.pass == password # Approve a credential which causes it to be stored LibGit2.approve(cache, cred, url) @test haskey(cache, cred_id) @test cache[cred_id] === cred # Approve the same credential again which does not overwrite LibGit2.approve(cache, cred, url) @test haskey(cache, cred_id) @test cache[cred_id] === cred # Overwrite an already cached credential dup_cred = deepcopy(cred) LibGit2.approve(cache, dup_cred, url) # Shreds overwritten `cred` @test haskey(cache, cred_id) @test cache[cred_id] === dup_cred @test cred.user != "julia" @test cred.pass != password @test dup_cred.user == "julia" @test dup_cred.pass == password cred = dup_cred # Reject an approved credential @test cache[cred_id] === cred LibGit2.reject(cache, cred, url) # Avoids shredding the credential passed in @test !haskey(cache, cred_id) @test cred.user == "julia" @test cred.pass == password # Reject and shred an approved credential dup_cred = deepcopy(cred) LibGit2.approve(cache, cred, url) LibGit2.reject(cache, dup_cred, url) # Shred `cred` but not passed in `dup_cred` @test !haskey(cache, cred_id) @test cred.user != "julia" @test cred.pass != password @test dup_cred.user == "julia" @test dup_cred.pass == password Base.shred!(dup_cred) Base.shred!(cache) Base.shred!(password) end @testset "Git credential username" begin @testset "fill username" begin config_path = joinpath(dir, config_file) isfile(config_path) && rm(config_path) LibGit2.with(LibGit2.GitConfig(config_path, LibGit2.Consts.CONFIG_LEVEL_APP)) do cfg # No credential settings should be set for these tests @test isempty(collect(LibGit2.GitConfigIter(cfg, r"credential.*"))) github_cred = LibGit2.GitCredential("https", "github.com") mygit_cred = LibGit2.GitCredential("https", "mygithost") # No credential settings in configuration. username = LibGit2.default_username(cfg, github_cred) @test username === nothing # Add a credential setting for a specific URL LibGit2.set!(cfg, "credential.https://github.com.username", "foo") username = LibGit2.default_username(cfg, github_cred) @test username == "foo" username = LibGit2.default_username(cfg, mygit_cred) @test username === nothing # Add a global credential setting after the URL specific setting. The first # setting to match will be the one that is used. LibGit2.set!(cfg, "credential.username", "bar") username = LibGit2.default_username(cfg, github_cred) @test username == "foo" username = LibGit2.default_username(cfg, mygit_cred) @test username == "bar" Base.shred!(github_cred) Base.shred!(mygit_cred) end end @testset "empty username" begin config_path = joinpath(dir, config_file) isfile(config_path) && rm(config_path) LibGit2.with(LibGit2.GitConfig(config_path, LibGit2.Consts.CONFIG_LEVEL_APP)) do cfg # No credential settings should be set for these tests @test isempty(collect(LibGit2.GitConfigIter(cfg, r"credential.*"))) # An empty username should count as being set LibGit2.set!(cfg, "credential.https://github.com.username", "") LibGit2.set!(cfg, "credential.username", "name") github_cred = LibGit2.GitCredential("https", "github.com") mygit_cred = LibGit2.GitCredential("https", "mygithost", "path") username = LibGit2.default_username(cfg, github_cred) @test username == "" username = LibGit2.default_username(cfg, mygit_cred) @test username == "name" Base.shred!(github_cred) Base.shred!(mygit_cred) end end end @testset "Git helpers useHttpPath" begin @testset "use_http_path" begin config_path = joinpath(dir, config_file) isfile(config_path) && rm(config_path) LibGit2.with(LibGit2.GitConfig(config_path, LibGit2.Consts.CONFIG_LEVEL_APP)) do cfg # No credential settings should be set for these tests @test isempty(collect(LibGit2.GitConfigIter(cfg, r"credential.*"))) github_cred = LibGit2.GitCredential("https", "github.com") mygit_cred = LibGit2.GitCredential("https", "mygithost") # No credential settings in configuration. @test !LibGit2.use_http_path(cfg, github_cred) @test !LibGit2.use_http_path(cfg, mygit_cred) # Add a credential setting for a specific URL LibGit2.set!(cfg, "credential.https://github.com.useHttpPath", "true") @test LibGit2.use_http_path(cfg, github_cred) @test !LibGit2.use_http_path(cfg, mygit_cred) # Invert the current settings. LibGit2.set!(cfg, "credential.useHttpPath", "true") LibGit2.set!(cfg, "credential.https://github.com.useHttpPath", "false") @test !LibGit2.use_http_path(cfg, github_cred) @test LibGit2.use_http_path(cfg, mygit_cred) Base.shred!(github_cred) Base.shred!(mygit_cred) end end end @testset "GitCredentialHelper" begin GitCredentialHelper = LibGit2.GitCredentialHelper GitCredential = LibGit2.GitCredential @testset "parse" begin @test parse(GitCredentialHelper, "!echo hello") == GitCredentialHelper(`echo hello`) @test parse(GitCredentialHelper, "/bin/bash") == GitCredentialHelper(`/bin/bash`) @test parse(GitCredentialHelper, "store") == GitCredentialHelper(`git credential-store`) end @testset "credential_helpers" begin config_path = joinpath(dir, config_file) # Note: LibGit2.set! doesn't allow us to set duplicates or ordering open(config_path, "w+") do fp write(fp, """ [credential] helper = !echo first [credential "https://mygithost"] helper = "" [credential] helper = !echo second """) # Git for Windows uses this config (see issue #45693) write(fp,""" [credential "helperselector"] selected = manager-core """) end LibGit2.with(LibGit2.GitConfig(config_path, LibGit2.Consts.CONFIG_LEVEL_APP)) do cfg expected = [ GitCredentialHelper(`echo first`), GitCredentialHelper(`echo second`), ] github_cred = GitCredential("https", "github.com") mygit_cred = GitCredential("https", "mygithost") @test LibGit2.credential_helpers(cfg, github_cred) == expected @test LibGit2.credential_helpers(cfg, mygit_cred) == expected[2:2] Base.shred!(github_cred) Base.shred!(mygit_cred) end end @testset "fill! with multiple helpers" begin # Requires `git` to be installed and available on the path. if GIT_INSTALLED config_path = joinpath(dir, config_file) empty_store = joinpath(dir, "empty-credentials") bob_store = joinpath(dir, "bob-credentials") alice_store = joinpath(dir, "alice-credentials") alice_alt_store = joinpath(dir, "alice-alt-credentials") touch(empty_store) write(bob_store, "https://bob:s3cre7@mygithost\n") write(alice_store, "https://alice:pa55w0rd@mygithost\n") write(alice_alt_store, "https://alice:0therpa55@mygithost\n") store_helper(path) = "store --file=$(replace(path, '\\' => '/'))" # helpers after one that returned nothing are still tried open(config_path, "w+") do fp write(fp, """ [credential] helper = $(store_helper(empty_store)) [credential] helper = $(store_helper(bob_store)) """) end LibGit2.with(LibGit2.GitConfig(config_path, LibGit2.Consts.CONFIG_LEVEL_APP)) do cfg cred = GitCredential("https", "mygithost") LibGit2.fill!(cfg, cred) @test LibGit2.isfilled(cred) @test cred.username == "bob" Base.shred!(cred) end # no more helpers are tried once the credential is filled open(config_path, "w+") do fp write(fp, """ [credential] helper = $(store_helper(alice_store)) [credential] helper = $(store_helper(alice_alt_store)) """) end LibGit2.with(LibGit2.GitConfig(config_path, LibGit2.Consts.CONFIG_LEVEL_APP)) do cfg cred = GitCredential("https", "mygithost") LibGit2.fill!(cfg, cred) @test LibGit2.isfilled(cred) @test cred.username == "alice" Base.shred!(Base.SecretBuffer("pa55w0rd")) do pw @test cred.password == pw end Base.shred!(cred) end end end @testset "approve/reject" begin # In order to use the "store" credential helper `git` needs to be installed and # on the path. if GIT_INSTALLED credential_path = joinpath(dir, ".git-credentials") isfile(credential_path) && rm(credential_path) # Requires `git` to be installed and available on the path. helper = parse(LibGit2.GitCredentialHelper, "store") # Set HOME to control where the .git-credentials file is written. # Note: In Cygwin environments `git` will use HOME instead of USERPROFILE. # Setting both environment variables ensures home was overridden. withenv("HOME" => dir, "USERPROFILE" => dir) do query = LibGit2.GitCredential("https", "mygithost") filled = LibGit2.GitCredential("https", "mygithost", nothing, "bob", "s3cre7") @test !isfile(credential_path) Base.shred!(LibGit2.fill!(helper, deepcopy(query))) do result @test result == query end LibGit2.approve(helper, filled) @test isfile(credential_path) Base.shred!(LibGit2.fill!(helper, deepcopy(query))) do result @test result == filled end LibGit2.reject(helper, filled) Base.shred!(LibGit2.fill!(helper, deepcopy(query))) do result @test result == query end Base.shred!(query) Base.shred!(filled) end end end @testset "approve/reject with path" begin # In order to use the "store" credential helper `git` needs to be installed and # on the path. if GIT_INSTALLED credential_path = joinpath(dir, ".git-credentials") isfile(credential_path) && rm(credential_path) # Requires `git` to be installed and available on the path. helper = parse(LibGit2.GitCredentialHelper, "store") # Set HOME to control where the .git-credentials file is written. # Note: In Cygwin environments `git` will use HOME instead of USERPROFILE. # Setting both environment variables ensures home was overridden. withenv("HOME" => dir, "USERPROFILE" => dir) do query = LibGit2.GitCredential("https", "mygithost") query_a = LibGit2.GitCredential("https", "mygithost", "a") query_b = LibGit2.GitCredential("https", "mygithost", "b") filled_a = LibGit2.GitCredential("https", "mygithost", "a", "alice", "1234") filled_b = LibGit2.GitCredential("https", "mygithost", "b", "bob", "s3cre7") function without_path(cred) c = deepcopy(cred) c.path = nothing c end filled_without_path_a = without_path(filled_a) filled_without_path_b = without_path(filled_b) @test !isfile(credential_path) Base.shred!(LibGit2.fill!(helper, deepcopy(query))) do result @test result == query end Base.shred!(LibGit2.fill!(helper, deepcopy(query_a))) do result @test result == query_a end Base.shred!(LibGit2.fill!(helper, deepcopy(query_b))) do result @test result == query_b end LibGit2.approve(helper, filled_a) @test isfile(credential_path) Base.shred!(LibGit2.fill!(helper, deepcopy(query))) do result @test result == filled_without_path_a end Base.shred!(LibGit2.fill!(helper, deepcopy(query_a))) do result @test result == filled_a end Base.shred!(LibGit2.fill!(helper, deepcopy(query_b))) do result @test result == query_b end LibGit2.approve(helper, filled_b) Base.shred!(LibGit2.fill!(helper, deepcopy(query))) do result @test result == filled_without_path_b end Base.shred!(LibGit2.fill!(helper, deepcopy(query_a))) do result @test result == filled_a end Base.shred!(LibGit2.fill!(helper, deepcopy(query_b))) do result @test result == filled_b end LibGit2.reject(helper, filled_b) Base.shred!(LibGit2.fill!(helper, deepcopy(query))) do result @test result == filled_without_path_a end Base.shred!(LibGit2.fill!(helper, deepcopy(query_a))) do result @test result == filled_a end Base.shred!(LibGit2.fill!(helper, deepcopy(query_b))) do result @test result == query_b end Base.shred!(query) Base.shred!(query_a) Base.shred!(query_b) Base.shred!(filled_a) Base.shred!(filled_b) Base.shred!(filled_without_path_a) Base.shred!(filled_without_path_b) end end end @testset "approve/reject with UserPasswordCredential" begin # In order to use the "store" credential helper `git` needs to be installed and # on the path. if GIT_INSTALLED config_path = joinpath(dir, config_file) isfile(config_path) && rm(config_path) credential_path = joinpath(dir, ".git-credentials") isfile(credential_path) && rm(credential_path) LibGit2.with(LibGit2.GitConfig(config_path, LibGit2.Consts.CONFIG_LEVEL_APP)) do cfg query = LibGit2.GitCredential("https", "mygithost") filled = LibGit2.GitCredential("https", "mygithost", nothing, "alice", "1234") user_pass_cred = LibGit2.UserPasswordCredential("alice", "1234") url = "https://mygithost" # Requires `git` to be installed and available on the path. LibGit2.set!(cfg, "credential.helper", "store --file \"$credential_path\"") helper = only(LibGit2.credential_helpers(cfg, query)) @test !isfile(credential_path) Base.shred!(LibGit2.fill!(helper, deepcopy(query))) do result @test result == query end LibGit2.approve(cfg, user_pass_cred, url) @test isfile(credential_path) Base.shred!(LibGit2.fill!(helper, deepcopy(query))) do result @test result == filled end LibGit2.reject(cfg, user_pass_cred, url) Base.shred!(LibGit2.fill!(helper, deepcopy(query))) do result @test result == query end Base.shred!(query) Base.shred!(filled) Base.shred!(user_pass_cred) end end end end # The following tests require that we can fake a TTY so that we can provide passwords # which use the `getpass` function. At the moment we can only fake this on UNIX based # systems. if Sys.isunix() git_ok = LibGit2.GitError( LibGit2.Error.None, LibGit2.Error.GIT_OK, "No errors") abort_prompt = LibGit2.GitError( LibGit2.Error.Callback, LibGit2.Error.EUSER, "Aborting, user cancelled credential request.") prompt_limit = LibGit2.GitError( LibGit2.Error.Callback, LibGit2.Error.EAUTH, "Aborting, maximum number of prompts reached.") incompatible_error = LibGit2.GitError( LibGit2.Error.Callback, LibGit2.Error.EAUTH, "The explicitly provided credential is incompatible with the requested " * "authentication methods.") exhausted_error = LibGit2.GitError( LibGit2.Error.Callback, LibGit2.Error.EAUTH, "All authentication methods have failed.") @testset "SSH credential prompt" begin url = "git@github.com:test/package.jl" username = "git" valid_key = joinpath(KEY_DIR, "valid") valid_cred = LibGit2.SSHCredential(username, "", valid_key, valid_key * ".pub") valid_p_key = joinpath(KEY_DIR, "valid-passphrase") passphrase = "secret" valid_p_cred = LibGit2.SSHCredential(username, passphrase, valid_p_key, valid_p_key * ".pub") invalid_key = joinpath(KEY_DIR, "invalid") function gen_ex(cred; username="git") url = username !== nothing && !isempty(username) ? "$username@" : "" url *= "github.com:test/package.jl" quote include($LIBGIT2_HELPER_PATH) credential_loop($cred, $url, $username) end end ssh_ex = gen_ex(valid_cred) ssh_p_ex = gen_ex(valid_p_cred) ssh_u_ex = gen_ex(valid_cred, username=nothing) # Note: We cannot use the default ~/.ssh/id_rsa for tests since we cannot be # sure a user will actually have these files. Instead we will use the ENV # variables to set the default values. # ENV credentials are valid withenv("SSH_KEY_PATH" => valid_key) do err, auth_attempts, p = challenge_prompt(ssh_ex, []) @test err == git_ok @test auth_attempts == 1 end # ENV credentials are valid but requires a passphrase withenv("SSH_KEY_PATH" => valid_p_key) do challenges = [ "Passphrase for $valid_p_key: " => "$passphrase\n", ] err, auth_attempts, p = challenge_prompt(ssh_p_ex, challenges) @test err == git_ok @test auth_attempts == 1 # User mistypes passphrase. # Note: In reality LibGit2 will raise an error upon using the invalid SSH # credentials. Since we don't control the internals of LibGit2 though they # could also just re-call the credential callback like they do for HTTP. challenges = [ "Passphrase for $valid_p_key: " => "foo\n", "Private key location for 'git@github.com' [$valid_p_key]: " => "\n", "Passphrase for $valid_p_key: " => "$passphrase\n", ] err, auth_attempts, p = challenge_prompt(ssh_p_ex, challenges) @test err == git_ok @test auth_attempts == 2 # User sends EOF in passphrase prompt which aborts the credential request challenges = [ "Passphrase for $valid_p_key: " => "\x04", ] err, auth_attempts, p = challenge_prompt(ssh_p_ex, challenges) @test err == abort_prompt @test auth_attempts == 1 # User provides an empty passphrase challenges = [ "Passphrase for $valid_p_key: " => "\n", ] err, auth_attempts, p = challenge_prompt(ssh_p_ex, challenges) @test err == abort_prompt @test auth_attempts == 1 end # ENV credential requiring passphrase withenv("SSH_KEY_PATH" => valid_p_key, "SSH_KEY_PASS" => passphrase) do err, auth_attempts, p = challenge_prompt(ssh_p_ex, []) @test err == git_ok @test auth_attempts == 1 end # Missing username withenv("SSH_KEY_PATH" => valid_key) do # User provides a valid username challenges = [ "Username for 'github.com': " => "$username\n", ] err, auth_attempts, p = challenge_prompt(ssh_u_ex, challenges) @test err == git_ok @test auth_attempts == 1 # User sends EOF in username prompt which aborts the credential request challenges = [ "Username for 'github.com': " => "\x04", ] err, auth_attempts, p = challenge_prompt(ssh_u_ex, challenges) @test err == abort_prompt @test auth_attempts == 1 # User provides an empty username challenges = [ "Username for 'github.com': " => "\n", "Username for 'github.com': " => "\x04", ] err, auth_attempts, p = challenge_prompt(ssh_u_ex, challenges) @test err == abort_prompt @test auth_attempts == 2 # User repeatedly chooses an invalid username challenges = [ "Username for 'github.com': " => "foo\n", "Username for 'github.com' [foo]: " => "\n", "Private key location for 'foo@github.com' [$valid_key]: " => "\n", "Username for 'github.com' [foo]: " => "\x04", # Need to manually abort ] err, auth_attempts, p = challenge_prompt(ssh_u_ex, challenges) @test err == abort_prompt @test auth_attempts == 3 # Credential callback is given an empty string in the `username_ptr` # instead of the C_NULL in the other missing username tests. ssh_user_empty_ex = gen_ex(valid_cred, username="") challenges = [ "Username for 'github.com': " => "$username\n", ] err, auth_attempts, p = challenge_prompt(ssh_user_empty_ex, challenges) @test err == git_ok @test auth_attempts == 1 end # Explicitly setting these env variables to be empty means the user will be # given a prompt with no defaults set. withenv("SSH_KEY_PATH" => nothing, "SSH_PUB_KEY_PATH" => nothing, "SSH_KEY_PASS" => nothing, HOME => dir) do # Set the USERPROFILE / HOME above to be a directory that does not contain # the "~/.ssh/id_rsa" file. If this file exists the credential callback # will default to use this private key instead of triggering a prompt. @test !isfile(joinpath(homedir(), ".ssh", "id_rsa")) # User provides valid credentials challenges = [ "Private key location for 'git@github.com': " => "$valid_key\n", ] err, auth_attempts, p = challenge_prompt(ssh_ex, challenges) @test err == git_ok @test auth_attempts == 1 # User provides valid credentials that requires a passphrase challenges = [ "Private key location for 'git@github.com': " => "$valid_p_key\n", "Passphrase for $valid_p_key: " => "$passphrase\n", ] err, auth_attempts, p = challenge_prompt(ssh_p_ex, challenges) @test err == git_ok @test auth_attempts == 1 # User sends EOF in private key prompt which aborts the credential request challenges = [ "Private key location for 'git@github.com': " => "\x04", ] err, auth_attempts, p = challenge_prompt(ssh_ex, challenges) @test err == abort_prompt @test auth_attempts == 1 # User provides an empty private key which triggers a re-prompt challenges = [ "Private key location for 'git@github.com': " => "\n", "Private key location for 'git@github.com': " => "\x04", ] err, auth_attempts, p = challenge_prompt(ssh_ex, challenges) @test err == abort_prompt @test auth_attempts == 2 # User provides an invalid private key until prompt limit reached. # Note: the prompt should not supply an invalid default. challenges = [ "Private key location for 'git@github.com': " => "foo\n", "Private key location for 'git@github.com' [foo]: " => "foo\n", "Private key location for 'git@github.com' [foo]: " => "foo\n", ] err, auth_attempts, p = challenge_prompt(ssh_ex, challenges) @test err == prompt_limit @test auth_attempts == 3 end # Explicitly setting these env variables to an existing but invalid key pair # means the user will be given a prompt with that defaults to the given values. withenv("SSH_KEY_PATH" => invalid_key, "SSH_PUB_KEY_PATH" => invalid_key * ".pub") do challenges = [ "Private key location for 'git@github.com' [$invalid_key]: " => "$valid_key\n", ] err, auth_attempts, p = challenge_prompt(ssh_ex, challenges) @test err == git_ok @test auth_attempts == 2 # User repeatedly chooses the default invalid private key until prompt limit reached challenges = [ "Private key location for 'git@github.com' [$invalid_key]: " => "\n", "Private key location for 'git@github.com' [$invalid_key]: " => "\n", "Private key location for 'git@github.com' [$invalid_key]: " => "\n", ] err, auth_attempts, p = challenge_prompt(ssh_ex, challenges) @test err == prompt_limit @test auth_attempts == 4 end # Explicitly set the public key ENV variable to a non-existent file. withenv("SSH_KEY_PATH" => valid_key, "SSH_PUB_KEY_PATH" => valid_key * ".public") do @test !isfile(ENV["SSH_PUB_KEY_PATH"]) challenges = [ # "Private key location for 'git@github.com' [$valid_key]: " => "\n" "Public key location for 'git@github.com' [$valid_key.public]: " => "$valid_key.pub\n" ] err, auth_attempts, p = challenge_prompt(ssh_ex, challenges) @test err == git_ok @test auth_attempts == 1 end # Explicitly set the public key ENV variable to a public key that doesn't match # the private key. withenv("SSH_KEY_PATH" => valid_key, "SSH_PUB_KEY_PATH" => invalid_key * ".pub") do @test isfile(ENV["SSH_PUB_KEY_PATH"]) challenges = [ "Private key location for 'git@github.com' [$valid_key]: " => "\n" "Public key location for 'git@github.com' [$invalid_key.pub]: " => "$valid_key.pub\n" ] err, auth_attempts, p = challenge_prompt(ssh_ex, challenges) @test err == git_ok @test auth_attempts == 2 end Base.shred!(valid_cred) Base.shred!(valid_p_cred) end @testset "SSH known host checking" begin CHECK_MATCH = LibGit2.Consts.LIBSSH2_KNOWNHOST_CHECK_MATCH CHECK_MISMATCH = LibGit2.Consts.LIBSSH2_KNOWNHOST_CHECK_MISMATCH CHECK_NOTFOUND = LibGit2.Consts.LIBSSH2_KNOWNHOST_CHECK_NOTFOUND CHECK_FAILURE = LibGit2.Consts.LIBSSH2_KNOWNHOST_CHECK_FAILURE # randomly generated hashes matching no hosts random_key = collect(reinterpret(Cchar, codeunits("\0\0\0\assh-rsa\0\0\0\x01#\0\0\0\x81\0¿\x95\xbe9\xfc9g\n:\xcf&\x06YA\xb5`\x97\xc13A\xbf;T+C\xc9Ut J>\xc5ҍ\xc4_S\x8a \xc1S\xeb\x15FH\xd2a\x04.D\xeeb\xac\x8f\xdb\xcc\xef\xc4l G\x9bR\xafp\x17s<=\x12\xab\x04ڳif\\A\x9ba0\xde%\xdei\x04\xc3\r\xb3\x81w\x88\xec\xc0f\x15A;AÝ\xc0r\xa1\u5fe\xd3\xf6)8\x8e\xa3\xcbc\xee\xdd\$\x04\x0f\xc1\xb4\x1f\xcc\xecK\xe0\x99"))) # hashes of the unique github.com fingerprint github_key = collect(reinterpret(Cchar, codeunits("\0\0\0\assh-rsa\0\0\0\x01#\0\0\x01\x01\0\xab`;\x85\x11\xa6vy\xbd\xb5@\xdb;\xd2\x03K\0J\xe96\xd0k\xe3\xd7`\xf0\x8f˪\xdbN\xb4\xedóǑ\xc7\n\xae\x9at\xc9Xi\xe4wD!«\xea\x92\xe5T0_8\xb5\xfdAK2\b\xe5t\xc37\xe3 \x93e\x18F,vRɋ1\xe1n}\xa6R;\xd2\0t*dD\xd8?\xcd^\x172\xd06sǷ\x81\x15UH{U\xf0\xc4IO8)\xec\xe6\x0f\x94%Z\x95˚\xf57\xd7\xfc\x8c\x7f\xe4\x9e\xf3\x18GN\xf2\x92\t\x92\x05\"e\xb0\xa0n\xa6mJ\x16\x7f\xd9\xf3\xa4\x8a\x1aJ0~\xc1\xea\xaaQI\xa9i\xa6\xac]V\xa5\xefb~Q}\x81\xfbdO[t\\OG\x8e\xcd\b*\x94\x92\xf7D\xaa\xd3&\xf7l\x8cM\xc9\x10\vƫyF\x1d&W\xcbo\x06\xde\xc9.kd\xa6V/\xf0\xe3 \x84\xea\x06\xce\x0e\xa9\xd3ZX;\xfb\0\xbaӌ\x9d\x19p<T\x98\x92\xe5\xaaxܕ\xe2PQ@i"))) # hashes of the middle github.com fingerprint gitlab_key = collect(reinterpret(Cchar, codeunits("\0\0\0\vssh-ed25519\0\0\0 \a\xee\br\x95N:\xae\xc6\xfbz\bέtn\x12.\x9dA\xb6\x7f\xe79\xe1\xc7\x13\x95\x0e\xcd\x17_"))) # various known hosts files no_file = tempname() empty_file = tempname(); touch(empty_file) known_hosts = joinpath(@__DIR__, "known_hosts") wrong_hosts = tempname() open(wrong_hosts, write=true) do io for line in eachline(known_hosts) words = split(line) words[1] = words[1] == "github.com" ? "gitlab.com" : words[1] == "gitlab.com" ? "github.com" : words[1] println(io, join(words, " ")) end end @testset "unknown host" begin host = "unknown.host" for key in [github_key, gitlab_key, random_key], files in [[no_file], [empty_file], [known_hosts]] check = LibGit2.ssh_knownhost_check(files, host, key) @test check == CHECK_NOTFOUND end end @testset "known hosts" begin for (host, key) in [ "github.com" => github_key, "gitlab.com" => gitlab_key, ] for files in [[no_file], [empty_file]] check = LibGit2.ssh_knownhost_check(files, host, key) @test check == CHECK_NOTFOUND end for files in [ [known_hosts], [empty_file, known_hosts], [known_hosts, empty_file], [known_hosts, wrong_hosts], ] check = LibGit2.ssh_knownhost_check(files, host, key) @test check == CHECK_MATCH end for files in [ [wrong_hosts], [empty_file, wrong_hosts], [wrong_hosts, empty_file], [wrong_hosts, known_hosts], ] check = LibGit2.ssh_knownhost_check(files, host, key) @test check == CHECK_MISMATCH end end end rm(empty_file) end @testset "HTTPS credential prompt" begin url = "https://github.com/test/package.jl" valid_username = "julia" valid_password = randstring(16) valid_cred = LibGit2.UserPasswordCredential(valid_username, valid_password) https_ex = quote include($LIBGIT2_HELPER_PATH) credential_loop($valid_cred, $url) end # User provides a valid username and password challenges = [ "Username for 'https://github.com': " => "$valid_username\n", "Password for 'https://$valid_username@github.com': " => "$valid_password\n", ] err, auth_attempts, p = challenge_prompt(https_ex, challenges) @test err == git_ok @test auth_attempts == 1 # User sends EOF in username prompt which aborts the credential request challenges = [ "Username for 'https://github.com': " => "\x04", ] err, auth_attempts, p = challenge_prompt(https_ex, challenges) @test err == abort_prompt @test auth_attempts == 1 # User sends EOF in password prompt which aborts the credential request challenges = [ "Username for 'https://github.com': " => "foo\n", "Password for 'https://foo@github.com': " => "\x04", ] err, auth_attempts, p = challenge_prompt(https_ex, challenges) @test err == abort_prompt @test auth_attempts == 1 # User provides an empty password which aborts the credential request since we # cannot tell it apart from an EOF. challenges = [ "Username for 'https://github.com': " => "foo\n", "Password for 'https://foo@github.com': " => "\n", ] err, auth_attempts, p = challenge_prompt(https_ex, challenges) @test err == abort_prompt @test auth_attempts == 1 # User repeatedly chooses invalid username/password until the prompt limit is # reached challenges = [ "Username for 'https://github.com': " => "foo\n", "Password for 'https://foo@github.com': " => "bar\n", "Username for 'https://github.com' [foo]: " => "foo\n", "Password for 'https://foo@github.com': " => "bar\n", "Username for 'https://github.com' [foo]: " => "foo\n", "Password for 'https://foo@github.com': " => "bar\n", ] err, auth_attempts, p = challenge_prompt(https_ex, challenges) @test err == prompt_limit @test auth_attempts == 3 Base.shred!(valid_cred) end @testset "SSH agent username" begin url = "github.com:test/package.jl" valid_key = joinpath(KEY_DIR, "valid") valid_cred = LibGit2.SSHCredential("git", "", valid_key, valid_key * ".pub") function gen_ex(; username="git") quote include($LIBGIT2_HELPER_PATH) payload = CredentialPayload(allow_prompt=false, allow_ssh_agent=true, allow_git_helpers=false) credential_loop($valid_cred, $url, $username, payload) end end # An empty string username_ptr ex = gen_ex(username="") err, auth_attempts, p = challenge_prompt(ex, []) @test err == exhausted_error @test auth_attempts == 3 # A null username_ptr passed into `git_cred_ssh_key_from_agent` can cause a # segfault. ex = gen_ex(username=nothing) err, auth_attempts, p = challenge_prompt(ex, []) @test err == exhausted_error @test auth_attempts == 2 Base.shred!(valid_cred) end @testset "SSH default" begin mktempdir() do home_dir url = "github.com:test/package.jl" default_key = joinpath(home_dir, ".ssh", "id_rsa") mkdir(dirname(default_key)) valid_key = joinpath(KEY_DIR, "valid") valid_cred = LibGit2.SSHCredential("git", "", valid_key, valid_key * ".pub") valid_p_key = joinpath(KEY_DIR, "valid-passphrase") passphrase = "secret" valid_p_cred = LibGit2.SSHCredential("git", passphrase, valid_p_key, valid_p_key * ".pub") function gen_ex(cred) quote valid_cred = $cred default_cred = deepcopy(valid_cred) default_cred.prvkey = $default_key default_cred.pubkey = $default_key * ".pub" cp(valid_cred.prvkey, default_cred.prvkey) cp(valid_cred.pubkey, default_cred.pubkey) try include($LIBGIT2_HELPER_PATH) credential_loop(default_cred, $url, "git", shred=false) finally rm(default_cred.prvkey) rm(default_cred.pubkey) end end end withenv("SSH_KEY_PATH" => nothing, "SSH_PUB_KEY_PATH" => nothing, "SSH_KEY_PASS" => nothing, HOME => home_dir) do # Automatically use the default key ex = gen_ex(valid_cred) err, auth_attempts, p = challenge_prompt(ex, []) @test err == git_ok @test auth_attempts == 1 @test p.credential.prvkey == default_key @test p.credential.pubkey == default_key * ".pub" # Confirm the private key if any other prompting is required ex = gen_ex(valid_p_cred) challenges = [ "Private key location for 'git@github.com' [$default_key]: " => "\n", "Passphrase for $default_key: " => "$passphrase\n", ] err, auth_attempts, p = challenge_prompt(ex, challenges) @test err == git_ok @test auth_attempts == 1 end Base.shred!(valid_cred) Base.shred!(valid_p_cred) end end @testset "SSH expand tilde" begin url = "git@github.com:test/package.jl" valid_key = joinpath(KEY_DIR, "valid") valid_cred = LibGit2.SSHCredential("git", "", valid_key, valid_key * ".pub") invalid_key = joinpath(KEY_DIR, "invalid") ssh_ex = quote include($LIBGIT2_HELPER_PATH) payload = CredentialPayload(allow_prompt=true, allow_ssh_agent=false, allow_git_helpers=false) credential_loop($valid_cred, $url, "git", payload, shred=false) end withenv("SSH_KEY_PATH" => nothing, "SSH_PUB_KEY_PATH" => nothing, "SSH_KEY_PASS" => nothing, HOME => KEY_DIR) do # Expand tilde during the private key prompt challenges = [ "Private key location for 'git@github.com': " => "~/valid\n", ] err, auth_attempts, p = challenge_prompt(ssh_ex, challenges) @test err == git_ok @test auth_attempts == 1 @test p.credential.prvkey == abspath(valid_key) end withenv("SSH_KEY_PATH" => valid_key, "SSH_PUB_KEY_PATH" => invalid_key * ".pub", "SSH_KEY_PASS" => nothing, HOME => KEY_DIR) do # Expand tilde during the public key prompt challenges = [ "Private key location for 'git@github.com' [$valid_key]: " => "\n", "Public key location for 'git@github.com' [$invalid_key.pub]: " => "~/valid.pub\n", ] err, auth_attempts, p = challenge_prompt(ssh_ex, challenges) @test err == git_ok @test auth_attempts == 2 @test p.credential.pubkey == abspath(valid_key * ".pub") end Base.shred!(valid_cred) end @testset "SSH explicit credentials" begin url = "git@github.com:test/package.jl" username = "git" valid_p_key = joinpath(KEY_DIR, "valid-passphrase") passphrase = "secret" valid_cred = LibGit2.SSHCredential(username, passphrase, valid_p_key, valid_p_key * ".pub") invalid_key = joinpath(KEY_DIR, "invalid") invalid_cred = LibGit2.SSHCredential(username, "", invalid_key, invalid_key * ".pub") function gen_ex(cred; allow_prompt=true, allow_ssh_agent=false) quote include($LIBGIT2_HELPER_PATH) payload = CredentialPayload($cred, allow_prompt=$allow_prompt, allow_ssh_agent=$allow_ssh_agent, allow_git_helpers=false) credential_loop($valid_cred, $url, $username, payload) end end # Explicitly provided credential is correct. Note: allowing prompting and # SSH agent to ensure they are skipped. ex = gen_ex(valid_cred, allow_prompt=true, allow_ssh_agent=true) err, auth_attempts, p = challenge_prompt(ex, []) @test err == git_ok @test auth_attempts == 1 @test p.explicit == valid_cred @test p.credential != valid_cred # Explicitly provided credential is incorrect ex = gen_ex(invalid_cred, allow_prompt=false, allow_ssh_agent=false) err, auth_attempts, p = challenge_prompt(ex, []) @test err == exhausted_error @test auth_attempts == 3 @test p.explicit == invalid_cred @test p.credential != invalid_cred Base.shred!(valid_cred) Base.shred!(invalid_cred) end @testset "HTTPS explicit credentials" begin url = "https://github.com/test/package.jl" valid_cred = LibGit2.UserPasswordCredential("julia", randstring(16)) invalid_cred = LibGit2.UserPasswordCredential("alice", randstring(15)) function gen_ex(cred; allow_prompt=true) quote include($LIBGIT2_HELPER_PATH) payload = CredentialPayload($cred, allow_prompt=$allow_prompt, allow_git_helpers=false) credential_loop($valid_cred, $url, "", payload) end end # Explicitly provided credential is correct ex = gen_ex(valid_cred, allow_prompt=true) err, auth_attempts, p = challenge_prompt(ex, []) @test err == git_ok @test auth_attempts == 1 @test p.explicit == valid_cred @test p.credential != valid_cred # Explicitly provided credential is incorrect ex = gen_ex(invalid_cred, allow_prompt=false) err, auth_attempts, p = challenge_prompt(ex, []) @test err == exhausted_error @test auth_attempts == 2 @test p.explicit == invalid_cred @test p.credential != invalid_cred Base.shred!(valid_cred) Base.shred!(invalid_cred) end @testset "Cached credentials" begin url = "https://github.com/test/package.jl" cred_id = "https://github.com" valid_username = "julia" valid_password = randstring(16) valid_cred = LibGit2.UserPasswordCredential(valid_username, valid_password) invalid_username = "alice" invalid_password = randstring(15) invalid_cred = LibGit2.UserPasswordCredential(invalid_username, invalid_password) function gen_ex(; cached_cred=nothing, allow_prompt=true) quote include($LIBGIT2_HELPER_PATH) cache = CachedCredentials() $(cached_cred !== nothing && :(LibGit2.approve(cache, $cached_cred, $url))) payload = CredentialPayload(cache, allow_prompt=$allow_prompt, allow_git_helpers=false) credential_loop($valid_cred, $url, "", payload) end end # Cache contains a correct credential err, auth_attempts, p = challenge_prompt(gen_ex(cached_cred=valid_cred), []) @test err == git_ok @test auth_attempts == 1 # Note: Approved cached credentials are not shredded # Add a credential into the cache ex = gen_ex() challenges = [ "Username for 'https://github.com': " => "$valid_username\n", "Password for 'https://$valid_username@github.com': " => "$valid_password\n", ] err, auth_attempts, p = challenge_prompt(ex, challenges) cache = p.cache @test err == git_ok @test auth_attempts == 1 @test typeof(cache) == LibGit2.CachedCredentials @test cache.cred == Dict(cred_id => valid_cred) @test p.credential == valid_cred # Replace a credential in the cache ex = gen_ex(cached_cred=invalid_cred) challenges = [ "Username for 'https://github.com' [alice]: " => "$valid_username\n", "Password for 'https://$valid_username@github.com': " => "$valid_password\n", ] err, auth_attempts, p = challenge_prompt(ex, challenges) cache = p.cache @test err == git_ok @test auth_attempts == 2 @test typeof(cache) == LibGit2.CachedCredentials @test cache.cred == Dict(cred_id => valid_cred) @test p.credential == valid_cred # Canceling a credential request should leave the cache unmodified ex = gen_ex(cached_cred=invalid_cred) challenges = [ "Username for 'https://github.com' [alice]: " => "foo\n", "Password for 'https://foo@github.com': " => "bar\n", "Username for 'https://github.com' [foo]: " => "\x04", ] err, auth_attempts, p = challenge_prompt(ex, challenges) cache = p.cache @test err == abort_prompt @test auth_attempts == 3 @test typeof(cache) == LibGit2.CachedCredentials @test cache.cred == Dict(cred_id => invalid_cred) @test p.credential != invalid_cred # An EAUTH error should remove credentials from the cache ex = gen_ex(cached_cred=invalid_cred, allow_prompt=false) err, auth_attempts, p = challenge_prompt(ex, []) cache = p.cache @test err == exhausted_error @test auth_attempts == 2 @test typeof(cache) == LibGit2.CachedCredentials @test cache.cred == Dict() @test p.credential != invalid_cred Base.shred!(valid_cred) Base.shred!(invalid_cred) end @testset "HTTPS git helper username" begin url = "https://github.com/test/package.jl" valid_username = "julia" valid_password = randstring(16) valid_cred = LibGit2.UserPasswordCredential(valid_username, valid_password) config_path = joinpath(dir, config_file) write(config_path, """ [credential] username = $valid_username """) https_ex = quote include($LIBGIT2_HELPER_PATH) LibGit2.with(LibGit2.GitConfig($config_path, LibGit2.Consts.CONFIG_LEVEL_APP)) do cfg payload = CredentialPayload(nothing, nothing, cfg, allow_git_helpers=true) credential_loop($valid_cred, $url, nothing, payload, shred=false) end end # Username is supplied from the git configuration file challenges = [ "Username for 'https://github.com' [$valid_username]: " => "\n", "Password for 'https://$valid_username@github.com': " => "$valid_password\n", ] err, auth_attempts, p = challenge_prompt(https_ex, challenges) @test err == git_ok @test auth_attempts == 1 # Verify credential wasn't accidentally zeroed (#24731) @test p.credential == valid_cred Base.shred!(valid_cred) end @testset "HTTPS git helper password" begin if GIT_INSTALLED url = "https://github.com/test/package.jl" valid_username = "julia" valid_password = randstring(16) valid_cred = LibGit2.UserPasswordCredential(valid_username, valid_password) cred_file = joinpath(dir, "test-credentials") config_path = joinpath(dir, config_file) write(config_path, """ [credential] helper = store --file $cred_file """) # Directly write to the cleartext credential store. Note: we are not using # the LibGit2.approve message to avoid any possibility of the tests # accidentally writing to a user's global store. write(cred_file, "https://$valid_username:$valid_password@github.com") https_ex = quote include($LIBGIT2_HELPER_PATH) LibGit2.with(LibGit2.GitConfig($config_path, LibGit2.Consts.CONFIG_LEVEL_APP)) do cfg payload = CredentialPayload(nothing, nothing, cfg, allow_git_helpers=true) credential_loop($valid_cred, $url, nothing, payload, shred=false) end end # Username will be provided by the credential helper challenges = [] err, auth_attempts, p = challenge_prompt(https_ex, challenges) @test err == git_ok @test auth_attempts == 1 # Verify credential wasn't accidentally zeroed (#24731) @test p.credential == valid_cred Base.shred!(valid_cred) end end @testset "Incompatible explicit credentials" begin # User provides a user/password credential where a SSH credential is required. valid_cred = LibGit2.UserPasswordCredential("foo", "bar") expect_ssh_ex = quote include($LIBGIT2_HELPER_PATH) payload = CredentialPayload($valid_cred, allow_ssh_agent=false, allow_git_helpers=false) credential_loop($valid_cred, "ssh://github.com/repo", "", Cuint(LibGit2.Consts.CREDTYPE_SSH_KEY), payload) end err, auth_attempts, p = challenge_prompt(expect_ssh_ex, []) @test err == incompatible_error @test auth_attempts == 1 @test p.explicit == valid_cred @test p.credential != valid_cred Base.shred!(valid_cred) # User provides a SSH credential where a user/password credential is required. valid_cred = LibGit2.SSHCredential("foo", "", "", "") expect_https_ex = quote include($LIBGIT2_HELPER_PATH) payload = CredentialPayload($valid_cred, allow_ssh_agent=false, allow_git_helpers=false) credential_loop($valid_cred, "https://github.com/repo", "", Cuint(LibGit2.Consts.CREDTYPE_USERPASS_PLAINTEXT), payload) end err, auth_attempts, p = challenge_prompt(expect_https_ex, []) @test err == incompatible_error @test auth_attempts == 1 @test p.explicit == valid_cred @test p.credential != valid_cred Base.shred!(valid_cred) end # A hypothetical scenario where the allowed authentication can either be # SSH or username/password. @testset "SSH & HTTPS authentication" begin allowed_types = Cuint(LibGit2.Consts.CREDTYPE_SSH_KEY) | Cuint(LibGit2.Consts.CREDTYPE_USERPASS_PLAINTEXT) # User provides a user/password credential where a SSH credential is required. valid_cred = LibGit2.UserPasswordCredential("foo", "bar") ex = quote include($LIBGIT2_HELPER_PATH) payload = CredentialPayload($valid_cred, allow_ssh_agent=false, allow_git_helpers=false) credential_loop($valid_cred, "foo://github.com/repo", "", $allowed_types, payload) end err, auth_attempts, p = challenge_prompt(ex, []) @test err == git_ok @test auth_attempts == 1 Base.shred!(valid_cred) end @testset "CredentialPayload reset" begin urls = [ "https://github.com/test/package.jl" "https://myhost.com/demo.jl" ] valid_username = "julia" valid_password = randstring(16) valid_cred = LibGit2.UserPasswordCredential(valid_username, valid_password) # Users should be able to re-use the same payload if the state is reset ex = quote include($LIBGIT2_HELPER_PATH) user = nothing payload = CredentialPayload(allow_git_helpers=false) first_result = credential_loop($valid_cred, $(urls[1]), user, payload) LibGit2.reset!(payload) second_result = credential_loop($valid_cred, $(urls[2]), user, payload) (first_result, second_result) end challenges = [ "Username for 'https://github.com': " => "$valid_username\n", "Password for 'https://$valid_username@github.com': " => "$valid_password\n", "Username for 'https://myhost.com': " => "$valid_username\n", "Password for 'https://$valid_username@myhost.com': " => "$valid_password\n", ] first_result, second_result = challenge_prompt(ex, challenges) err, auth_attempts, p = first_result @test err == git_ok @test auth_attempts == 1 err, auth_attempts, p = second_result @test err == git_ok @test auth_attempts == 1 Base.shred!(valid_cred) end end # Note: Tests only work on linux as SSL_CERT_FILE is only respected on linux systems. @testset "Hostname verification" begin openssl_installed = false common_name = "" if Sys.islinux() try # OpenSSL needs to be on the path openssl_installed = !isempty(read(`openssl version`, String)) catch ex @warn "Skipping hostname verification tests. Is `openssl` on the path?" exception=ex end # Find a hostname that maps to the loopback address hostnames = ["localhost"] # In minimal environments a hostname might not be available (issue #20758) try # In some environments, namely Macs, the hostname "macbook.local" is bound # to the external address while "macbook" is bound to the loopback address. pushfirst!(hostnames, replace(gethostname(), r"\..*$" => "")) catch end loopbacks = (ip"127.0.0.1", ip"::1") for hostname in hostnames local addr try addr = getaddrinfo(hostname) catch continue end if addr ∈ loopbacks common_name = hostname break end end if isempty(common_name) @warn "Skipping hostname verification tests. Unable to determine a hostname which maps to the loopback address" end end if openssl_installed && !isempty(common_name) mktempdir() do root key = joinpath(root, common_name * ".key") cert = joinpath(root, common_name * ".crt") pem = joinpath(root, common_name * ".pem") conf = joinpath(root, common_name * ".conf") # Make sure test doesn't depend on system OpenSSL config (which may be broken) open(conf, "w") do io write(io, """ [req] distinguished_name = req_distinguished_name [req_distinguished_name] CN = $common_name """) end # Generate a certificate which has the CN set correctly but no subjectAltName err = IOBuffer() p = run(pipeline(addenv( `openssl req -new -x509 -newkey rsa:2048 -sha256 -nodes -keyout $key -out $cert -days 1 -subj "/CN=$common_name"`, "OPENSSL_CONF" => conf), stderr=err); wait=false) wait(p) @testset let err = String(take!(err)) @test success(p) end run(`openssl x509 -in $cert -out $pem -outform PEM`) # Make a fake Julia package and minimal HTTPS server with our generated # certificate. The minimal server can't actually serve a Git repository. mkdir(joinpath(root, "Example.jl")) local pobj, port for attempt in 1:10 # Find an available port by listening, but there's a race condition where # another process could grab this port, so retry on failure port, server = listenany(49052 + rand(1:100) + attempt*10) close(server) pobj = cd(root) do open(`openssl s_server -key $key -cert $cert -WWW -accept $port`) end # Two options: Either we reached "ACCEPT" and the process is running and ready # or it failed to listen and exited, in which case we try again. if !contains(readuntil(pobj, "ACCEPT"; keep=true), "ACCEPT") close(pobj) else break end end @test process_running(pobj) if process_running(pobj) errfile = joinpath(root, "error") repo_url = "https://$common_name:$port/Example.jl" repo_dir = joinpath(root, "dest") code = """ using Serialization import LibGit2 dest_dir = "$repo_dir" open("$errfile", "w+") do f try repo = LibGit2.clone("$repo_url", dest_dir) catch err serialize(f, err) finally isdir(dest_dir) && rm(dest_dir, recursive=true) end end """ cmd = `$(Base.julia_cmd()) --startup-file=no -e $code` try # The generated certificate is normally invalid run(cmd) err = open(errfile, "r") do f deserialize(f) end @test err.code == LibGit2.Error.ERROR @test startswith(lowercase(err.msg), lowercase("user rejected certificate for localhost")) rm(errfile) # Specify that Julia use only the custom certificate. Note: we need to # spawn a new Julia process in order for this ENV variable to take effect. withenv("SSL_CERT_FILE" => pem) do run(cmd) err = open(errfile, "r") do f deserialize(f) end @test err.code == LibGit2.Error.ERROR @test occursin(r"invalid content-type: '?text/plain'?"i, err.msg) end # OpenSSL s_server should still be running @test process_running(pobj) finally kill(pobj) end end end end end end let cache = LibGit2.CachedCredentials() get!(cache, "foo", LibGit2.SSHCredential("", "bar")) Base.shred!(cache) @test all(cache["foo"].pass.data .== UInt(0)) end end # module