# This file is a part of Julia. License is MIT: https://julialang.org/license isdefined(Main, :TestHelpers) || @eval Main include(joinpath(@__DIR__, "TestHelpers.jl")) import Main.TestHelpers: challenge_prompt const LIBGIT2_MIN_VER = v"0.23.0" const LIBGIT2_HELPER_PATH = joinpath(@__DIR__, "libgit2-helpers.jl") const KEY_DIR = joinpath(@__DIR__, "libgit2") 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((:git_libgit2_opts, :libgit2), Cint, (Cint, Cint, Ptr{LibGit2.Buffer}), LibGit2.Consts.GET_SEARCH_PATH, LibGit2.Consts.CONFIG_LEVEL_GLOBAL, buf) path = unsafe_string(buf[].ptr) LibGit2.free(buf) return path end function set_global_dir(dir) LibGit2.@check ccall((:git_libgit2_opts, :libgit2), Cint, (Cint, Cint, Cstring), LibGit2.Consts.SET_SEARCH_PATH, LibGit2.Consts.CONFIG_LEVEL_GLOBAL, dir) 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(equalto(LibGit2.Consts.FEATURE_SSH), f) > 0 @test findfirst(equalto(LibGit2.Consts.FEATURE_HTTPS), f) > 0 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 == convert(Vector{String}, unsafe_load(b)) @noinline gcuse(a) = a gcuse(a) end @testset "Signature" begin sig = LibGit2.Signature("AAA", "AAA@BBB.COM", round(time(), 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 # 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", password="pass", host="server.com", port=80, path="org/project.git") @test url == "https://user:pass@server.com:80/org/project.git" end @testset "SSH URL" begin url = LibGit2.git_url( scheme="ssh", username="user", password="pass", host="server", port="22", path="project.git") @test url == "ssh://user:pass@server:22/project.git" end @testset "SSH URL, scp-like syntax" begin url = LibGit2.git_url( scheme="", 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", password="pass", host="server.com", port="80") @test url == "https://user:pass@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="", password="", 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 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 end @testset "input/output" begin str = """ protocol=https host=example.com username=alice password=***** """ cred = read!(IOBuffer(str), LibGit2.GitCredential()) @test cred == LibGit2.GitCredential("https", "example.com", nothing, "alice", "*****") @test sprint(write, cred) == str 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 get(cred.path, "") == "dir/file" @test sprint(write, cred) == expected end @testset "URL input/output" begin str = """ host=example.com password=bar url=https://a@b/c username=foo """ expected = """ protocol=https host=b path=c username=foo """ cred = read!(IOBuffer(str), LibGit2.GitCredential()) @test cred == LibGit2.GitCredential("https", "b", "c", "foo", nothing) @test sprint(write, cred) == expected end @testset "ismatch" begin # Equal cred = LibGit2.GitCredential("https", "github.com") @test LibGit2.ismatch("https://github.com", cred) # Credential hostname is different cred = LibGit2.GitCredential("https", "github.com") @test !LibGit2.ismatch("https://myhost", cred) # Credential is less specific than URL cred = LibGit2.GitCredential("https") @test !LibGit2.ismatch("https://github.com", cred) # Credential is more specific than URL cred = LibGit2.GitCredential("https", "github.com", "path", "user", "pass") @test LibGit2.ismatch("https://github.com", cred) # Credential needs to have an "" username to match cred = LibGit2.GitCredential("https", "github.com", nothing, "") @test LibGit2.ismatch("https://@github.com", cred) cred = LibGit2.GitCredential("https", "github.com", nothing, nothing) @test !LibGit2.ismatch("https://@github.com", cred) end end mktempdir() do 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(), 0), 0) test_file = "testfile" config_file = "testconfig" commit_msg1 = randstring(10) commit_msg2 = randstring(10) commit_oid1 = LibGit2.GitHash() commit_oid2 = LibGit2.GitHash() commit_oid3 = LibGit2.GitHash() master_branch = "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 end @testset "Update cache repository" begin @testset "with commits" begin repo = LibGit2.GitRepo(cache_repo) 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 hex(commit_oid1) == hex(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(hex(commit_oid1), hex(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 showing the commit showstr = split(sprint(show, cmt), "\n") # the time of the commit will vary so just test the first two parts @test contains(showstr[1], "Git Commit:") @test contains(showstr[2], "Commit Author: Name: TEST, Email: TEST@TEST.COM, Time:") @test contains(showstr[3], "Committer: Name: TEST, Email: TEST@TEST.COM, Time:") @test contains(showstr[4], "SHA:") @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/master" @test LibGit2.shortname(brref) == master_branch @test LibGit2.ishead(brref) @test isnull(LibGit2.upstream(brref)) # 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/master" @test show_strs[3] == "Branch is HEAD." @test repo.ptr == LibGit2.repository(brref).ptr @test brnch == master_branch @test LibGit2.headname(repo) == master_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 isnull(LibGit2.lookup_branch(repo, test_branch, true)) # not null because we are now looking for a LOCAL branch LibGit2.with(Base.get(LibGit2.lookup_branch(repo, test_branch, false))) do tbref @test LibGit2.shortname(tbref) == test_branch @test isnull(LibGit2.upstream(tbref)) end @test isnull(LibGit2.lookup_branch(repo, test_branch2, true)) # test deleting the branch LibGit2.branch!(repo, test_branch2; set_head=false) LibGit2.with(Base.get(LibGit2.lookup_branch(repo, test_branch2, false))) do tbref @test LibGit2.shortname(tbref) == test_branch2 LibGit2.delete_branch(tbref) @test isnull(LibGit2.lookup_branch(repo, test_branch2, true)) end end branches = map(b->LibGit2.shortname(b[1]), LibGit2.GitBranchIter(repo)) @test master_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 exsist @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/master", "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 = hex(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 contains(blob_show_strs[2], "Blob id:") @test blob_show_strs[3] == "Contents are binary." blob2 = LibGit2.GitBlob(repo, LibGit2.GitHash(blob)) @test LibGit2.isbinary(blob2) @test length(blob2) == len1 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 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] @test LibGit2.filemode(tree_entry) == 33188 te_str = sprint(show, tree_entry) ref_te_str = "GitTreeEntry:\nEntry name: testfile\nEntry type: Base.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) @test blob_str == "GitBlob:\nBlob id: $(LibGit2.GitHash(blob))\nContents:\n$(LibGit2.content(blob))\n" 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 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 contains(diff_strs[6], "Oid:") @test contains(diff_strs[7], "Path:") @test contains(diff_strs[8], "Size:") @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 end end function setup_clone_repo(cache_repo::AbstractString, path::AbstractString; name="AAAA", email="BBBB@BBBB.COM") repo = LibGit2.clone(cache_repo, path) # 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=Base.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, "master") # 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, "master") # (fail to) merge them because we can't fastforward @test_warn "WARNING: Cannot perform fast-forward merge." !LibGit2.merge!(repo, [upst_ann], true) # merge them now that we allow non-ff @test_warn "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, "master") upst_ann = LibGit2.GitAnnotated(repo, "branch/merge_b") rename_flag = 0 rename_flag = LibGit2.toggle(rename_flag, 0) # turns on the find renames opt mos = LibGit2.MergeOptions(flags=rename_flag) @test_warn "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 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, master_branch) @test newhead == head_oid # Switch to the master branch LibGit2.branch!(repo, master_branch) fetch_heads = LibGit2.fetchheads(repo) @test fetch_heads[1].name == "refs/heads/master" @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 end @testset "Examine test repository" begin @testset "files" begin @test read(joinpath(test_repo, test_file), String) == read(joinpath(cache_repo, test_file), String) 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 master_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 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 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 = find(test_file, idx) @test !isnull(i) idx_entry = idx[get(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 = find("zzz", idx) @test isnull(i) 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 count(idx) == 0 LibGit2.add!(repo, test_file) LibGit2.update!(repo, test_file) @test count(idx) == 1 end # check non-existent file status st = LibGit2.status(repo, "XYZ") @test isnull(st) # check file status st = LibGit2.status(repo, test_file) @test !isnull(st) @test LibGit2.isset(get(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(get(st_mod), LibGit2.Consts.STATUS_INDEX_MODIFIED) @test LibGit2.isset(get(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(get(st_stg), LibGit2.Consts.STATUS_INDEX_MODIFIED) @test !LibGit2.isset(get(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 get(st_new) == get(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 get(st_uns) == get(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 isnull(LibGit2.lookup_remote(repo, remote_name)) for r in (repo, path) # Set just the fetch URL LibGit2.set_remote_fetch_url(r, remote_name, url) remote = get(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 isnull(LibGit2.lookup_remote(repo, remote_name)) # Set just the push URL LibGit2.set_remote_push_url(r, remote_name, url) remote = get(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 isnull(LibGit2.lookup_remote(repo, remote_name)) # Set the fetch and push URL LibGit2.set_remote_url(r, remote_name, url) remote = get(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 isnull(LibGit2.lookup_remote(repo, remote_name)) 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", "master") 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, "master") rb = LibGit2.GitRebase(repo, head_ann, upst_ann) @test_throws BoundsError rb[3] @test_throws BoundsError rb[0] rbo = next(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, "master") a_head_ann = LibGit2.GitAnnotated(repo, "branch/merge_a") # merge returns true if successful @test_warn "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/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]))" 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 = "PASS" creds = LibGit2.UserPasswordCredentials(creds_user, creds_pass) @test creds.user == creds_user @test creds.pass == creds_pass creds2 = LibGit2.UserPasswordCredentials(creds_user, creds_pass) @test creds == creds2 sshcreds = LibGit2.SSHCredentials(creds_user, creds_pass) @test sshcreds.user == creds_user @test sshcreds.pass == creds_pass @test isempty(sshcreds.prvkey) @test isempty(sshcreds.pubkey) sshcreds2 = LibGit2.SSHCredentials(creds_user, creds_pass) @test sshcreds == sshcreds2 end @testset "CachedCredentials" begin cache = LibGit2.CachedCredentials() url = "https://github.com/JuliaLang/Example.jl" cred_id = LibGit2.credential_identifier(url) cred = LibGit2.UserPasswordCredentials(deepcopy("julia"), deepcopy("password")) @test !haskey(cache, cred_id) # 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 # Reject an approved should cause it to be removed and erased LibGit2.reject(cache, cred, url) @test !haskey(cache, cred_id) @test cred.user != "julia" @test cred.pass != "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.*"))) # No credential settings in configuration. cred = LibGit2.GitCredential("https", "github.com") username = LibGit2.default_username(cfg, cred) @test isnull(username) # Add a credential setting for a specific for a URL LibGit2.set!(cfg, "credential.https://github.com.username", "foo") cred = LibGit2.GitCredential("https", "github.com") username = LibGit2.default_username(cfg, cred) @test !isnull(username) @test get(username) == "foo" cred = LibGit2.GitCredential("https", "mygithost") username = LibGit2.default_username(cfg, cred) @test isnull(username) # 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") cred = LibGit2.GitCredential("https", "github.com") username = LibGit2.default_username(cfg, cred) @test !isnull(username) @test get(username) == "foo" cred = LibGit2.GitCredential("https", "mygithost") username = LibGit2.default_username(cfg, cred) @test !isnull(username) @test get(username) == "bar" 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") cred = LibGit2.GitCredential("https", "github.com") username = LibGit2.default_username(cfg, cred) @test !isnull(username) @test get(username) == "" cred = LibGit2.GitCredential("https", "mygithost", "path") username = LibGit2.default_username(cfg, cred) @test !isnull(username) @test get(username) == "name" 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.*"))) # No credential settings in configuration. cred = LibGit2.GitCredential("https", "github.com") @test !LibGit2.use_http_path(cfg, cred) # Add a credential setting for a specific for a URL LibGit2.set!(cfg, "credential.https://github.com.useHttpPath", "true") cred = LibGit2.GitCredential("https", "github.com") @test LibGit2.use_http_path(cfg, cred) cred = LibGit2.GitCredential("https", "mygithost") @test !LibGit2.use_http_path(cfg, cred) # Invert the current settings. LibGit2.set!(cfg, "credential.useHttpPath", "true") LibGit2.set!(cfg, "credential.https://github.com.useHttpPath", "false") cred = LibGit2.GitCredential("https", "github.com") @test !LibGit2.use_http_path(cfg, cred) cred = LibGit2.GitCredential("https", "mygithost") @test LibGit2.use_http_path(cfg, 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 "empty helper" 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 """) end LibGit2.with(LibGit2.GitConfig(config_path, LibGit2.Consts.CONFIG_LEVEL_APP)) do cfg @test length(collect(LibGit2.GitConfigIter(cfg, r"credential.*"))) == 3 expected = [ GitCredentialHelper(`echo first`), GitCredentialHelper(`echo second`), ] @test LibGit2.credential_helpers(cfg, GitCredential("https", "github.com")) == expected @test_broken LibGit2.credential_helpers(cfg, GitCredential("https", "mygithost")) == expected[2] 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 config_path = joinpath(dir, config_file) credential_path = joinpath(dir, ".git-credentials") isfile(config_path) && rm(config_path) isfile(credential_path) && rm(credential_path) LibGit2.with(LibGit2.GitConfig(config_path, LibGit2.Consts.CONFIG_LEVEL_APP)) do cfg @test isempty(LibGit2.get(cfg, "credential.helper", "")) helper = parse(LibGit2.GitCredentialHelper, "store") # Requires `git` LibGit2.set!(cfg, "credential.helper", "store --file $credential_path") # Set HOME to control where .git-credentials file is written. withenv(HOME => dir) do query = LibGit2.GitCredential("https", "mygithost") filled = LibGit2.GitCredential("https", "mygithost", nothing, "bob", "s3cre7") @test LibGit2.fill!(helper, deepcopy(query)) == query LibGit2.approve(helper, filled) @test LibGit2.fill!(helper, deepcopy(query)) == filled LibGit2.reject(helper, filled) @test LibGit2.fill!(helper, deepcopy(query)) == query end 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 config_path = joinpath(dir, config_file) credential_path = joinpath(dir, ".git-credentials") isfile(config_path) && rm(config_path) isfile(credential_path) && rm(credential_path) LibGit2.with(LibGit2.GitConfig(config_path, LibGit2.Consts.CONFIG_LEVEL_APP)) do cfg @test isempty(LibGit2.get(cfg, "credential.helper", "")) helper = parse(LibGit2.GitCredentialHelper, "store") # Requires `git` LibGit2.set!(cfg, "credential.helper", "store --file $credential_path") LibGit2.set!(cfg, "credential.useHttpPath", "true") # Set HOME to control where .git-credentials file is written. withenv(HOME => 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 = Nullable() c end @test LibGit2.fill!(helper, deepcopy(query)) == query @test LibGit2.fill!(helper, deepcopy(query_a)) == query_a @test LibGit2.fill!(helper, deepcopy(query_b)) == query_b LibGit2.approve(helper, filled_a) @test LibGit2.fill!(helper, deepcopy(query)) == without_path(filled_a) @test LibGit2.fill!(helper, deepcopy(query_a)) == filled_a @test LibGit2.fill!(helper, deepcopy(query_b)) == query_b LibGit2.approve(helper, filled_b) @test LibGit2.fill!(helper, deepcopy(query)) == without_path(filled_b) @test LibGit2.fill!(helper, deepcopy(query_a)) == filled_a @test LibGit2.fill!(helper, deepcopy(query_b)) == filled_b LibGit2.reject(helper, filled_b) @test LibGit2.fill!(helper, deepcopy(query)) == without_path(filled_a) @test LibGit2.fill!(helper, deepcopy(query_a)) == filled_a @test LibGit2.fill!(helper, deepcopy(query_b)) == query_b end 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.SSHCredentials(username, "", valid_key, valid_key * ".pub") valid_p_key = joinpath(KEY_DIR, "valid-passphrase") passphrase = "secret" valid_p_cred = LibGit2.SSHCredentials(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, Nullable{String}($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 users 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = challenge_prompt(ssh_ex, challenges) @test err == git_ok @test auth_attempts == 2 end end @testset "HTTPS credential prompt" begin url = "https://github.com/test/package.jl" valid_username = "julia" valid_password = randstring(16) valid_cred = LibGit2.UserPasswordCredentials(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 = 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 = 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 = 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 = 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 = challenge_prompt(https_ex, challenges) @test err == prompt_limit @test auth_attempts == 3 end @testset "SSH agent username" begin url = "github.com:test/package.jl" valid_key = joinpath(KEY_DIR, "valid") valid_cred = LibGit2.SSHCredentials("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, Nullable{String}($username), payload) end end # An empty string username_ptr ex = gen_ex(username="") err, auth_attempts = 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 = challenge_prompt(ex, []) @test err == exhausted_error @test auth_attempts == 2 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.SSHCredentials("git", "", valid_key, valid_key * ".pub") valid_p_key = joinpath(KEY_DIR, "valid-passphrase") passphrase = "secret" valid_p_cred = LibGit2.SSHCredentials("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") 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 = challenge_prompt(ex, []) @test err == git_ok @test auth_attempts == 1 # 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 = challenge_prompt(ex, challenges) @test err == git_ok @test auth_attempts == 1 end end end @testset "SSH expand tilde" begin url = "git@github.com:test/package.jl" valid_key = joinpath(KEY_DIR, "valid") valid_cred = LibGit2.SSHCredentials("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) err, auth_attempts = credential_loop($valid_cred, $url, "git", payload) (err, auth_attempts, payload.credential) 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, credential = challenge_prompt(ssh_ex, challenges) @test err == git_ok @test auth_attempts == 1 @test get(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, credential = challenge_prompt(ssh_ex, challenges) @test err == git_ok @test auth_attempts == 2 @test get(credential).pubkey == abspath(valid_key * ".pub") end 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.SSHCredentials(username, passphrase, valid_p_key, valid_p_key * ".pub") invalid_key = joinpath(KEY_DIR, "invalid") invalid_cred = LibGit2.SSHCredentials(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 = challenge_prompt(ex, []) @test err == git_ok @test auth_attempts == 1 # Explicitly provided credential is incorrect ex = gen_ex(invalid_cred, allow_prompt=false, allow_ssh_agent=false) err, auth_attempts = challenge_prompt(ex, []) @test err == exhausted_error @test auth_attempts == 3 end @testset "HTTPS explicit credentials" begin url = "https://github.com/test/package.jl" valid_cred = LibGit2.UserPasswordCredentials("julia", randstring(16)) invalid_cred = LibGit2.UserPasswordCredentials("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 = challenge_prompt(ex, []) @test err == git_ok @test auth_attempts == 1 # Explicitly provided credential is incorrect ex = gen_ex(invalid_cred, allow_prompt=false) err, auth_attempts = challenge_prompt(ex, []) @test err == exhausted_error @test auth_attempts == 2 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.UserPasswordCredentials(valid_username, valid_password) invalid_username = "alice" invalid_password = randstring(15) invalid_cred = LibGit2.UserPasswordCredentials(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) err, auth_attempts = credential_loop($valid_cred, $url, "", payload) (err, auth_attempts, cache) end end # Cache contains a correct credential err, auth_attempts = challenge_prompt(gen_ex(cached_cred=valid_cred), []) @test err == git_ok @test auth_attempts == 1 # 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, cache = challenge_prompt(ex, challenges) @test err == git_ok @test auth_attempts == 1 @test typeof(cache) == LibGit2.CachedCredentials @test cache.cred == Dict(cred_id => 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, cache = challenge_prompt(ex, challenges) @test err == git_ok @test auth_attempts == 2 @test typeof(cache) == LibGit2.CachedCredentials @test cache.cred == Dict(cred_id => 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, cache = challenge_prompt(ex, challenges) @test err == abort_prompt @test auth_attempts == 3 @test typeof(cache) == LibGit2.CachedCredentials @test cache.cred == Dict(cred_id => invalid_cred) # An EAUTH error should remove credentials from the cache ex = gen_ex(cached_cred=invalid_cred, allow_prompt=false) err, auth_attempts, cache = challenge_prompt(ex, []) @test err == exhausted_error @test auth_attempts == 2 @test typeof(cache) == LibGit2.CachedCredentials @test cache.cred == Dict() end @testset "HTTPS git helper username" begin url = "https://github.com/test/package.jl" valid_username = "julia" valid_password = randstring(16) valid_cred = LibGit2.UserPasswordCredentials(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(Nullable{AbstractCredentials}(), Nullable{CachedCredentials}(), cfg, allow_git_helpers=true) credential_loop($valid_cred, $url, Nullable{String}(), payload) end end challenges = [ "Username for 'https://github.com' [$valid_username]:" => "\n", "Password for 'https://$valid_username@github.com':" => "$valid_password\n", ] err, auth_attempts = challenge_prompt(https_ex, challenges) @test err == git_ok @test auth_attempts == 1 end @testset "Incompatible explicit credentials" begin # User provides a user/password credential where a SSH credential is required. expect_ssh_ex = quote include($LIBGIT2_HELPER_PATH) valid_cred = LibGit2.UserPasswordCredentials("foo", "bar") payload = CredentialPayload(valid_cred, allow_ssh_agent=false, allow_git_helpers=false) credential_loop(valid_cred, "ssh://github.com/repo", Nullable(""), Cuint(LibGit2.Consts.CREDTYPE_SSH_KEY), payload) end err, auth_attempts = challenge_prompt(expect_ssh_ex, []) @test err == incompatible_error @test auth_attempts == 1 # User provides a SSH credential where a user/password credential is required. expect_https_ex = quote include($LIBGIT2_HELPER_PATH) valid_cred = LibGit2.SSHCredentials("foo", "", "", "") payload = CredentialPayload(valid_cred, allow_ssh_agent=false, allow_git_helpers=false) credential_loop(valid_cred, "https://github.com/repo", Nullable(""), Cuint(LibGit2.Consts.CREDTYPE_USERPASS_PLAINTEXT), payload) end err, auth_attempts = challenge_prompt(expect_https_ex, []) @test err == incompatible_error @test auth_attempts == 1 end # A hypothetical scenario where the 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. ex = quote include($LIBGIT2_HELPER_PATH) valid_cred = LibGit2.UserPasswordCredentials("foo", "bar") payload = CredentialPayload(valid_cred, allow_ssh_agent=false, allow_git_helpers=false) credential_loop(valid_cred, "foo://github.com/repo", Nullable(""), $allowed_types, payload) end err, auth_attempts = challenge_prompt(ex, []) @test err == git_ok @test auth_attempts == 1 end @testset "CredentialPayload reset" begin urls = [ "https://github.com/test/package.jl" "https://myhost.com/demo.jl" ] valid_username = "julia" valid_password = randstring(16) # Users should be able to re-use the same payload if the state is reset ex = quote include($LIBGIT2_HELPER_PATH) valid_cred = LibGit2.UserPasswordCredentials($valid_username, $valid_password) user = Nullable{String}() 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 = first_result @test err == git_ok @test auth_attempts == 1 err, auth_attempts = second_result @test err == git_ok @test auth_attempts == 1 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 warn("Skipping hostname verification tests. Is `openssl` on the path?") 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. unshift!(hostnames, replace(gethostname(), r"\..*$", "")) end loopback = ip"127.0.0.1" for hostname in hostnames local addr try addr = getaddrinfo(hostname) catch continue end if addr == loopback 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") # Generated a certificate which has the CN set correctly but no subjectAltName run(pipeline(`openssl req -new -x509 -newkey rsa:2048 -sha256 -nodes -keyout $key -out $cert -days 1 -subj "/CN=$common_name"`, stderr=DevNull)) run(`openssl x509 -in $cert -out $pem -outform PEM`) # Find an available port by listening port, server = listenany(49152) close(server) # 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")) pobj = cd(root) do spawn(`openssl s_server -key $key -cert $cert -WWW -accept $port`) end errfile = joinpath(root, "error") repo_url = "https://$common_name:$port/Example.jl" repo_dir = joinpath(root, "dest") code = """ 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.ECERTIFICATE @test startswith(lowercase(err.msg), lowercase("The SSL certificate is invalid")) 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 lowercase(err.msg) == lowercase("invalid Content-Type: text/plain") end # OpenSSL s_server should still be running @test process_running(pobj) finally kill(pobj) end end end end end