From ff8d802d8b2a95a1b42105cd41318bc52df06df3 Mon Sep 17 00:00:00 2001 From: Jameson Nash Date: Fri, 10 Jun 2022 13:26:19 -0400 Subject: [PATCH] [LibGit2] isolate tests from ~/.gitconfig Settings such as insteadOf can break the tests. --- stdlib/LibGit2/test/libgit2-tests.jl | 3225 ++++++++++++++++++++++++++ stdlib/LibGit2/test/libgit2.jl | 3224 +------------------------ stdlib/LibGit2/test/online-tests.jl | 101 + stdlib/LibGit2/test/online.jl | 102 +- stdlib/LibGit2/test/runtests.jl | 6 +- 5 files changed, 3341 insertions(+), 3317 deletions(-) create mode 100644 stdlib/LibGit2/test/libgit2-tests.jl create mode 100644 stdlib/LibGit2/test/online-tests.jl diff --git a/stdlib/LibGit2/test/libgit2-tests.jl b/stdlib/LibGit2/test/libgit2-tests.jl new file mode 100644 index 00000000000000..4dbd1837045e9b --- /dev/null +++ b/stdlib/LibGit2/test/libgit2-tests.jl @@ -0,0 +1,3225 @@ +# This file is a part of Julia. License is MIT: https://julialang.org/license + +module LibGit2Tests + +import LibGit2 +using Test +using Random, Serialization, Sockets + +const BASE_TEST_PATH = joinpath(Sys.BINDIR, "..", "share", "julia", "test") +isdefined(Main, :FakePTYs) || @eval Main include(joinpath($(BASE_TEST_PATH), "testhelpers", "FakePTYs.jl")) +import .Main.FakePTYs: with_fake_pty + +const timeout = 60 + +function challenge_prompt(code::Expr, challenges) + input_code = tempname() + open(input_code, "w") do fp + serialize(fp, code) + end + output_file = tempname() + torun = """ + import LibGit2 + using Serialization + result = open($(repr(input_code))) do fp + eval(deserialize(fp)) + end + open($(repr(output_file)), "w") do fp + serialize(fp, result) + end""" + cmd = `$(Base.julia_cmd()) --startup-file=no -e $torun` + try + challenge_prompt(cmd, challenges) + return open(output_file, "r") do fp + deserialize(fp) + end + finally + isfile(output_file) && rm(output_file) + isfile(input_code) && rm(input_code) + end + return nothing +end + +function challenge_prompt(cmd::Cmd, challenges) + function format_output(output) + str = read(seekstart(output), String) + isempty(str) && return "" + return "Process output found:\n\"\"\"\n$str\n\"\"\"" + end + out = IOBuffer() + with_fake_pty() do pts, ptm + p = run(detach(cmd), pts, pts, pts, wait=false) # getpass uses stderr by default + Base.close_stdio(pts) + + # Kill the process if it takes too long. Typically occurs when process is waiting + # for input. + timer = Channel{Symbol}(1) + watcher = @async begin + waited = 0 + while waited < timeout && process_running(p) + sleep(1) + waited += 1 + end + + if process_running(p) + kill(p) + put!(timer, :timeout) + elseif success(p) + put!(timer, :success) + else + put!(timer, :failure) + end + + # SIGKILL stubborn processes + if process_running(p) + sleep(3) + process_running(p) && kill(p, Base.SIGKILL) + end + wait(p) + end + + wroteall = false + try + for (challenge, response) in challenges + write(out, readuntil(ptm, challenge, keep=true)) + if !isopen(ptm) + error("Could not locate challenge: \"$challenge\". ", + format_output(out)) + end + write(ptm, response) + end + wroteall = true + + # Capture output from process until `pts` is closed + write(out, ptm) + catch ex + if !(wroteall && ex isa Base.IOError && ex.code == Base.UV_EIO) + # ignore EIO from `ptm` after `pts` dies + error("Process failed possibly waiting for a response. ", + format_output(out)) + end + end + + status = fetch(timer) + close(ptm) + if status !== :success + if status === :timeout + error("Process timed out possibly waiting for a response. ", + format_output(out)) + else + error("Failed process. ", format_output(out), "\n", p) + end + end + wait(watcher) + end + nothing +end + +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 == 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(); 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 + 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 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 + 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 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/$(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 + 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) + @test blob_str == "GitBlob:\nBlob id: $(LibGit2.GitHash(blob))\nContents:\n$(LibGit2.content(blob))\n" + + # 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((:giterr_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 + 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 + 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=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()" + 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]))" + 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 for a URL + LibGit2.set!(cfg, "credential.https://github.aaakk.us.kg.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.aaakk.us.kg.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 for a URL + LibGit2.set!(cfg, "credential.https://github.aaakk.us.kg.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.aaakk.us.kg.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 "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 + iter = LibGit2.GitConfigIter(cfg, r"credential.*\.helper") + @test LibGit2.split_cfg_entry.(iter) == [ + ("credential", "", "helper", "!echo first"), + ("credential", "https://mygithost", "helper", ""), + ("credential", "", "helper", "!echo second"), + ] + + 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 "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 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, 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 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") + + # 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 + run(pipeline(`openssl s_server -key $key -cert $cert -WWW -accept $port`, stderr=RawFD(2)), wait=false) + end + + 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 + +let cache = LibGit2.CachedCredentials() + get!(cache, "foo", LibGit2.SSHCredential("", "bar")) + Base.shred!(cache) + @test all(cache["foo"].pass.data .== UInt(0)) +end + +end # module diff --git a/stdlib/LibGit2/test/libgit2.jl b/stdlib/LibGit2/test/libgit2.jl index 4dbd1837045e9b..c78ecc8fa8bfc0 100644 --- a/stdlib/LibGit2/test/libgit2.jl +++ b/stdlib/LibGit2/test/libgit2.jl @@ -1,3225 +1,11 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license -module LibGit2Tests - -import LibGit2 -using Test -using Random, Serialization, Sockets - -const BASE_TEST_PATH = joinpath(Sys.BINDIR, "..", "share", "julia", "test") -isdefined(Main, :FakePTYs) || @eval Main include(joinpath($(BASE_TEST_PATH), "testhelpers", "FakePTYs.jl")) -import .Main.FakePTYs: with_fake_pty - -const timeout = 60 - -function challenge_prompt(code::Expr, challenges) - input_code = tempname() - open(input_code, "w") do fp - serialize(fp, code) - end - output_file = tempname() - torun = """ - import LibGit2 - using Serialization - result = open($(repr(input_code))) do fp - eval(deserialize(fp)) - end - open($(repr(output_file)), "w") do fp - serialize(fp, result) - end""" - cmd = `$(Base.julia_cmd()) --startup-file=no -e $torun` - try - challenge_prompt(cmd, challenges) - return open(output_file, "r") do fp - deserialize(fp) - end - finally - isfile(output_file) && rm(output_file) - isfile(input_code) && rm(input_code) - end - return nothing -end - -function challenge_prompt(cmd::Cmd, challenges) - function format_output(output) - str = read(seekstart(output), String) - isempty(str) && return "" - return "Process output found:\n\"\"\"\n$str\n\"\"\"" - end - out = IOBuffer() - with_fake_pty() do pts, ptm - p = run(detach(cmd), pts, pts, pts, wait=false) # getpass uses stderr by default - Base.close_stdio(pts) - - # Kill the process if it takes too long. Typically occurs when process is waiting - # for input. - timer = Channel{Symbol}(1) - watcher = @async begin - waited = 0 - while waited < timeout && process_running(p) - sleep(1) - waited += 1 - end - - if process_running(p) - kill(p) - put!(timer, :timeout) - elseif success(p) - put!(timer, :success) - else - put!(timer, :failure) - end - - # SIGKILL stubborn processes - if process_running(p) - sleep(3) - process_running(p) && kill(p, Base.SIGKILL) - end - wait(p) - end - - wroteall = false - try - for (challenge, response) in challenges - write(out, readuntil(ptm, challenge, keep=true)) - if !isopen(ptm) - error("Could not locate challenge: \"$challenge\". ", - format_output(out)) - end - write(ptm, response) - end - wroteall = true - - # Capture output from process until `pts` is closed - write(out, ptm) - catch ex - if !(wroteall && ex isa Base.IOError && ex.code == Base.UV_EIO) - # ignore EIO from `ptm` after `pts` dies - error("Process failed possibly waiting for a response. ", - format_output(out)) - end - end - - status = fetch(timer) - close(ptm) - if status !== :success - if status === :timeout - error("Process timed out possibly waiting for a response. ", - format_output(out)) - else - error("Failed process. ", format_output(out), "\n", p) - end - end - wait(watcher) - end - nothing -end - -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 == 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(); 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 - +# Set HOME to control where the .gitconfig file may be found. +# Note: In Cygwin environments `git` will use HOME instead of USERPROFILE. +# Setting both environment variables ensures home was overridden. 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 - 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 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 - 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 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/$(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 - 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) - @test blob_str == "GitBlob:\nBlob id: $(LibGit2.GitHash(blob))\nContents:\n$(LibGit2.content(blob))\n" - - # 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((:giterr_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 - 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 - 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=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()" - 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 + withenv("HOME" => dir, "USERPROFILE" => dir) do + include("libgit2-tests.jl") 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]))" - 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 for a URL - LibGit2.set!(cfg, "credential.https://github.aaakk.us.kg.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.aaakk.us.kg.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 for a URL - LibGit2.set!(cfg, "credential.https://github.aaakk.us.kg.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.aaakk.us.kg.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 "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 - iter = LibGit2.GitConfigIter(cfg, r"credential.*\.helper") - @test LibGit2.split_cfg_entry.(iter) == [ - ("credential", "", "helper", "!echo first"), - ("credential", "https://mygithost", "helper", ""), - ("credential", "", "helper", "!echo second"), - ] - - 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 "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 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, 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 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") - - # 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 - run(pipeline(`openssl s_server -key $key -cert $cert -WWW -accept $port`, stderr=RawFD(2)), wait=false) - end - - 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 - -let cache = LibGit2.CachedCredentials() - get!(cache, "foo", LibGit2.SSHCredential("", "bar")) - Base.shred!(cache) - @test all(cache["foo"].pass.data .== UInt(0)) end - -end # module diff --git a/stdlib/LibGit2/test/online-tests.jl b/stdlib/LibGit2/test/online-tests.jl new file mode 100644 index 00000000000000..96b6bf5b22371d --- /dev/null +++ b/stdlib/LibGit2/test/online-tests.jl @@ -0,0 +1,101 @@ +# This file is a part of Julia. License is MIT: https://julialang.org/license + +module LibGit2OnlineTests + +using Test +import LibGit2 +using Random + +function transfer_progress(progress::Ptr{LibGit2.TransferProgress}, payload::Dict) + status = payload[:transfer_progress] + progress = unsafe_load(progress) + + status[] = (current=progress.received_objects, total=progress.total_objects) + + return Cint(0) +end + +######### +# TESTS # +######### +# init & clone +mktempdir() do dir + repo_url = "https://github.com/JuliaLang/Example.jl" + + @testset "Cloning repository" begin + @testset "HTTPS protocol" begin + repo_path = joinpath(dir, "Example.HTTPS") + c = LibGit2.CredentialPayload(allow_prompt=false, allow_git_helpers=false) + repo = LibGit2.clone(repo_url, repo_path, credentials=c) + try + @test isdir(repo_path) + @test isdir(joinpath(repo_path, ".git")) + finally + close(repo) + end + end + + @testset "Transfer progress callbacks" begin + status = Ref((current=0, total=-1)) + callbacks = LibGit2.Callbacks( + :transfer_progress => ( + @cfunction(transfer_progress, Cint, (Ptr{LibGit2.TransferProgress}, Any)), + status, + ) + ) + + repo_path = joinpath(dir, "Example.TransferProgress") + c = LibGit2.CredentialPayload(allow_prompt=false, allow_git_helpers=false) + repo = LibGit2.clone(repo_url, repo_path, credentials=c, callbacks=callbacks) + try + @test isdir(repo_path) + @test isdir(joinpath(repo_path, ".git")) + + @test status[].total >= 0 + @test status[].current == status[].total + finally + close(repo) + end + end + + @testset "Incorrect URL" begin + repo_path = joinpath(dir, "Example.IncorrectURL") + # credentials are required because github tries to authenticate on unknown repo + cred = LibGit2.UserPasswordCredential("JeffBezanson", "hunter2") # make sure Jeff is using a good password :) + c = LibGit2.CredentialPayload(cred, allow_prompt=false, allow_git_helpers=false) + try + LibGit2.clone(repo_url*randstring(10), repo_path, credentials=c) + error("unexpected") + catch ex + @test isa(ex, LibGit2.Error.GitError) + # Return code seems to vary, see #32186, #32219 + @test ex.code ∈ (LibGit2.Error.EAUTH, LibGit2.Error.ERROR) + end + Base.shred!(cred) + end + + @testset "Empty Credentials" begin + repo_path = joinpath(dir, "Example.EmptyCredentials") + # credentials are required because github tries to authenticate on unknown repo + cred = LibGit2.UserPasswordCredential("","") # empty credentials cause authentication error + c = LibGit2.CredentialPayload(cred, allow_prompt=false, allow_git_helpers=false) + try + LibGit2.clone(repo_url*randstring(10), repo_path, credentials=c) + error("unexpected") + catch ex + @test isa(ex, LibGit2.Error.GitError) + @test ex.code == LibGit2.Error.EAUTH + end + end + end +end + +# needs to be run in separate process so it can re-initialize libgit2 +# with a useless self-signed certificate authority root certificate +file = joinpath(@__DIR__, "bad_ca_roots.jl") +cmd = `$(Base.julia_cmd()) --depwarn=no --startup-file=no $file` +if !success(pipeline(cmd; stdout=stdout, stderr=stderr)) + error("bad CA roots tests failed, cmd : $cmd") +end + +end # module diff --git a/stdlib/LibGit2/test/online.jl b/stdlib/LibGit2/test/online.jl index 96b6bf5b22371d..b2bcab83d9f4ea 100644 --- a/stdlib/LibGit2/test/online.jl +++ b/stdlib/LibGit2/test/online.jl @@ -1,101 +1,11 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license -module LibGit2OnlineTests - -using Test -import LibGit2 -using Random - -function transfer_progress(progress::Ptr{LibGit2.TransferProgress}, payload::Dict) - status = payload[:transfer_progress] - progress = unsafe_load(progress) - - status[] = (current=progress.received_objects, total=progress.total_objects) - - return Cint(0) -end - -######### -# TESTS # -######### -# init & clone +# Set HOME to control where the .gitconfig file may be found. +# Note: In Cygwin environments `git` will use HOME instead of USERPROFILE. +# Setting both environment variables ensures home was overridden. mktempdir() do dir - repo_url = "https://github.com/JuliaLang/Example.jl" - - @testset "Cloning repository" begin - @testset "HTTPS protocol" begin - repo_path = joinpath(dir, "Example.HTTPS") - c = LibGit2.CredentialPayload(allow_prompt=false, allow_git_helpers=false) - repo = LibGit2.clone(repo_url, repo_path, credentials=c) - try - @test isdir(repo_path) - @test isdir(joinpath(repo_path, ".git")) - finally - close(repo) - end - end - - @testset "Transfer progress callbacks" begin - status = Ref((current=0, total=-1)) - callbacks = LibGit2.Callbacks( - :transfer_progress => ( - @cfunction(transfer_progress, Cint, (Ptr{LibGit2.TransferProgress}, Any)), - status, - ) - ) - - repo_path = joinpath(dir, "Example.TransferProgress") - c = LibGit2.CredentialPayload(allow_prompt=false, allow_git_helpers=false) - repo = LibGit2.clone(repo_url, repo_path, credentials=c, callbacks=callbacks) - try - @test isdir(repo_path) - @test isdir(joinpath(repo_path, ".git")) - - @test status[].total >= 0 - @test status[].current == status[].total - finally - close(repo) - end - end - - @testset "Incorrect URL" begin - repo_path = joinpath(dir, "Example.IncorrectURL") - # credentials are required because github tries to authenticate on unknown repo - cred = LibGit2.UserPasswordCredential("JeffBezanson", "hunter2") # make sure Jeff is using a good password :) - c = LibGit2.CredentialPayload(cred, allow_prompt=false, allow_git_helpers=false) - try - LibGit2.clone(repo_url*randstring(10), repo_path, credentials=c) - error("unexpected") - catch ex - @test isa(ex, LibGit2.Error.GitError) - # Return code seems to vary, see #32186, #32219 - @test ex.code ∈ (LibGit2.Error.EAUTH, LibGit2.Error.ERROR) - end - Base.shred!(cred) - end - - @testset "Empty Credentials" begin - repo_path = joinpath(dir, "Example.EmptyCredentials") - # credentials are required because github tries to authenticate on unknown repo - cred = LibGit2.UserPasswordCredential("","") # empty credentials cause authentication error - c = LibGit2.CredentialPayload(cred, allow_prompt=false, allow_git_helpers=false) - try - LibGit2.clone(repo_url*randstring(10), repo_path, credentials=c) - error("unexpected") - catch ex - @test isa(ex, LibGit2.Error.GitError) - @test ex.code == LibGit2.Error.EAUTH - end - end + dir = realpath(dir) + withenv("HOME" => dir, "USERPROFILE" => dir) do + include("online-tests.jl") end end - -# needs to be run in separate process so it can re-initialize libgit2 -# with a useless self-signed certificate authority root certificate -file = joinpath(@__DIR__, "bad_ca_roots.jl") -cmd = `$(Base.julia_cmd()) --depwarn=no --startup-file=no $file` -if !success(pipeline(cmd; stdout=stdout, stderr=stderr)) - error("bad CA roots tests failed, cmd : $cmd") -end - -end # module diff --git a/stdlib/LibGit2/test/runtests.jl b/stdlib/LibGit2/test/runtests.jl index 69b20014d11e1d..88aea77f256711 100644 --- a/stdlib/LibGit2/test/runtests.jl +++ b/stdlib/LibGit2/test/runtests.jl @@ -1,4 +1,6 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license -include("libgit2.jl") -include("online.jl") +using Test +@testset verbose=true "LibGit2 $test" for test in eachline(joinpath(@__DIR__, "testgroups")) + include("$test.jl") +end