Revision b0ef6fc6fff067f6daa937d9b36a7879e6ea4b61 authored by Jameson Nash on 02 January 2018, 17:29:04 UTC, committed by Jameson Nash on 02 January 2018, 17:29:06 UTC
A GitHub link is not necessarily the most stable reference, and also appears to only list the top 100. The updated text is intended to make it clear that this lists may not be fully accurate (as JuliaLang does not have full control over it), and inform the user of an alternate method of listing the JuliaLang authorship history through git.
1 parent 2cc82d2
libgit2.jl
# This file is a part of Julia. License is MIT: https://julialang.org/license
isdefined(Main, :TestHelpers) || @eval Main include(joinpath(@__DIR__, "TestHelpers.jl"))
import Main.TestHelpers: challenge_prompt
using Base.Unicode: lowercase
const LIBGIT2_MIN_VER = v"0.23.0"
const LIBGIT2_HELPER_PATH = joinpath(@__DIR__, "libgit2-helpers.jl")
const KEY_DIR = joinpath(@__DIR__, "libgit2")
const HOME = Sys.iswindows() ? "USERPROFILE" : "HOME" # Environment variable name for home
const GIT_INSTALLED = try
success(`git --version`)
catch
false
end
function get_global_dir()
buf = Ref(LibGit2.Buffer())
LibGit2.@check ccall((:git_libgit2_opts, :libgit2), Cint,
(Cint, Cint, Ptr{LibGit2.Buffer}),
LibGit2.Consts.GET_SEARCH_PATH, LibGit2.Consts.CONFIG_LEVEL_GLOBAL, buf)
path = unsafe_string(buf[].ptr)
LibGit2.free(buf)
return path
end
function set_global_dir(dir)
LibGit2.@check ccall((:git_libgit2_opts, :libgit2), Cint,
(Cint, Cint, Cstring),
LibGit2.Consts.SET_SEARCH_PATH, LibGit2.Consts.CONFIG_LEVEL_GLOBAL, dir)
return
end
function with_libgit2_temp_home(f)
mktempdir() do tmphome
oldpath = get_global_dir()
set_global_dir(tmphome)
try
@test get_global_dir() == tmphome
f(tmphome)
finally
set_global_dir(oldpath)
end
return
end
end
#########
# TESTS #
#########
@testset "Check library version" begin
v = LibGit2.version()
@test v.major == LIBGIT2_MIN_VER.major && v.minor >= LIBGIT2_MIN_VER.minor
end
@testset "Check library features" begin
f = LibGit2.features()
@test findfirst(equalto(LibGit2.Consts.FEATURE_SSH), f) > 0
@test findfirst(equalto(LibGit2.Consts.FEATURE_HTTPS), f) > 0
end
@testset "OID" begin
z = LibGit2.GitHash()
@test LibGit2.iszero(z)
@test z == zero(LibGit2.GitHash)
@test z == LibGit2.GitHash(z)
rs = string(z)
rr = LibGit2.raw(z)
@test z == LibGit2.GitHash(rr)
@test z == LibGit2.GitHash(rs)
@test z == LibGit2.GitHash(pointer(rr))
@test LibGit2.GitShortHash(z, 20) == LibGit2.GitShortHash(rs[1:20])
@test_throws ArgumentError LibGit2.GitHash(Ptr{UInt8}(C_NULL))
@test_throws ArgumentError LibGit2.GitHash(rand(UInt8, 2*LibGit2.OID_RAWSZ))
@test_throws ArgumentError LibGit2.GitHash("a")
end
@testset "StrArrayStruct" begin
p = ["XXX","YYY"]
a = Base.cconvert(Ptr{LibGit2.StrArrayStruct}, p)
b = Base.unsafe_convert(Ptr{LibGit2.StrArrayStruct}, a)
@test p == convert(Vector{String}, unsafe_load(b))
@noinline gcuse(a) = a
gcuse(a)
end
@testset "Signature" begin
sig = LibGit2.Signature("AAA", "AAA@BBB.COM", round(time(), 0), 0)
git_sig = convert(LibGit2.GitSignature, sig)
sig2 = LibGit2.Signature(git_sig)
close(git_sig)
@test sig.name == sig2.name
@test sig.email == sig2.email
@test sig.time == sig2.time
sig3 = LibGit2.Signature("AAA","AAA@BBB.COM")
@test sig3.name == sig.name
@test sig3.email == sig.email
end
@testset "Default config" begin
with_libgit2_temp_home() do tmphome
cfg = LibGit2.GitConfig()
@test isa(cfg, LibGit2.GitConfig)
@test LibGit2.getconfig("fake.property", "") == ""
LibGit2.set!(cfg, "fake.property", "AAAA")
@test LibGit2.getconfig("fake.property", "") == "AAAA"
end
end
# See #21872 and #21636
LibGit2.version() >= v"0.26.0" && Sys.isunix() && @testset "Default config with symlink" begin
with_libgit2_temp_home() do tmphome
write(joinpath(tmphome, "real_gitconfig"), "[fake]\n\tproperty = BBB")
symlink(joinpath(tmphome, "real_gitconfig"),
joinpath(tmphome, ".gitconfig"))
cfg = LibGit2.GitConfig()
@test isa(cfg, LibGit2.GitConfig)
LibGit2.getconfig("fake.property", "") == "BBB"
LibGit2.set!(cfg, "fake.property", "AAAA")
LibGit2.getconfig("fake.property", "") == "AAAA"
end
end
@testset "Git URL parsing" begin
@testset "HTTPS URL" begin
m = match(LibGit2.URL_REGEX, "https://user:pass@server.com:80/org/project.git")
@test m[:scheme] == "https"
@test m[:user] == "user"
@test m[:password] == "pass"
@test m[:host] == "server.com"
@test m[:port] == "80"
@test m[:path] == "org/project.git"
end
@testset "SSH URL" begin
m = match(LibGit2.URL_REGEX, "ssh://user:pass@server:22/project.git")
@test m[:scheme] == "ssh"
@test m[:user] == "user"
@test m[:password] == "pass"
@test m[:host] == "server"
@test m[:port] == "22"
@test m[:path] == "project.git"
end
@testset "SSH URL, scp-like syntax" begin
m = match(LibGit2.URL_REGEX, "user@server:project.git")
@test m[:scheme] === nothing
@test m[:user] == "user"
@test m[:password] === nothing
@test m[:host] == "server"
@test m[:port] === nothing
@test m[:path] == "project.git"
end
# scp-like syntax corner case. The SCP syntax does not support port so everything after
# the colon is part of the path.
@testset "scp-like syntax, no port" begin
m = match(LibGit2.URL_REGEX, "server:1234/repo")
@test m[:scheme] === nothing
@test m[:user] === nothing
@test m[:password] === nothing
@test m[:host] == "server"
@test m[:port] === nothing
@test m[:path] == "1234/repo"
end
@testset "HTTPS URL, realistic" begin
m = match(LibGit2.URL_REGEX, "https://github.com/JuliaLang/Example.jl.git")
@test m[:scheme] == "https"
@test m[:user] === nothing
@test m[:password] === nothing
@test m[:host] == "github.com"
@test m[:port] === nothing
@test m[:path] == "JuliaLang/Example.jl.git"
end
@testset "SSH URL, realistic" begin
m = match(LibGit2.URL_REGEX, "git@github.com:JuliaLang/Example.jl.git")
@test m[:scheme] === nothing
@test m[:user] == "git"
@test m[:password] === nothing
@test m[:host] == "github.com"
@test m[:port] === nothing
@test m[:path] == "JuliaLang/Example.jl.git"
end
@testset "usernames with special characters" begin
m = match(LibGit2.URL_REGEX, "user-name@hostname.com")
@test m[:user] == "user-name"
end
@testset "HTTPS URL, no path" begin
m = match(LibGit2.URL_REGEX, "https://user:pass@server.com:80")
@test m[:path] === nothing
end
@testset "scp-like syntax, no path" begin
m = match(LibGit2.URL_REGEX, "user@server:")
@test m[:path] == ""
m = match(LibGit2.URL_REGEX, "user@server")
@test m[:path] === nothing
end
@testset "HTTPS URL, invalid path" begin
m = match(LibGit2.URL_REGEX, "https://git@server:repo")
@test m === nothing
end
# scp-like syntax should have a colon separating the hostname from the path
@testset "scp-like syntax, invalid path" begin
m = match(LibGit2.URL_REGEX, "git@server/repo")
@test m === nothing
end
end
@testset "Git URL formatting" begin
@testset "HTTPS URL" begin
url = LibGit2.git_url(
scheme="https",
username="user",
password="pass",
host="server.com",
port=80,
path="org/project.git")
@test url == "https://user:pass@server.com:80/org/project.git"
end
@testset "SSH URL" begin
url = LibGit2.git_url(
scheme="ssh",
username="user",
password="pass",
host="server",
port="22",
path="project.git")
@test url == "ssh://user:pass@server:22/project.git"
end
@testset "SSH URL, scp-like syntax" begin
url = LibGit2.git_url(
scheme="",
username="user",
host="server",
path="project.git")
@test url == "user@server:project.git"
end
@testset "HTTPS URL, realistic" begin
url = LibGit2.git_url(
scheme="https",
host="github.com",
path="JuliaLang/Example.jl.git")
@test url == "https://github.com/JuliaLang/Example.jl.git"
end
@testset "SSH URL, realistic" begin
url = LibGit2.git_url(
username="git",
host="github.com",
path="JuliaLang/Example.jl.git")
@test url == "git@github.com:JuliaLang/Example.jl.git"
end
@testset "HTTPS URL, no path" begin
url = LibGit2.git_url(
scheme="https",
username="user",
password="pass",
host="server.com",
port="80")
@test url == "https://user:pass@server.com:80"
end
@testset "scp-like syntax, no path" begin
url = LibGit2.git_url(
username="user",
host="server.com")
@test url == "user@server.com"
end
@testset "HTTP URL, path includes slash prefix" begin
url = LibGit2.git_url(
scheme="http",
host="server.com",
path="/path")
@test url == "http://server.com/path"
end
@testset "empty" begin
@test_throws ArgumentError LibGit2.git_url()
@test LibGit2.git_url(host="server.com") == "server.com"
url = LibGit2.git_url(
scheme="",
username="",
password="",
host="server.com",
port="",
path="")
@test url == "server.com"
end
end
@testset "Passphrase Required" begin
@testset "missing file" begin
@test !LibGit2.is_passphrase_required("")
file = joinpath(KEY_DIR, "foobar")
@test !isfile(file)
@test !LibGit2.is_passphrase_required(file)
end
@testset "not private key" begin
@test !LibGit2.is_passphrase_required(joinpath(KEY_DIR, "invalid.pub"))
end
@testset "private key, with passphrase" begin
@test LibGit2.is_passphrase_required(joinpath(KEY_DIR, "valid-passphrase"))
end
@testset "private key, no passphrase" begin
@test !LibGit2.is_passphrase_required(joinpath(KEY_DIR, "valid"))
end
end
@testset "GitCredential" begin
@testset "missing" begin
str = ""
cred = read!(IOBuffer(str), LibGit2.GitCredential())
@test cred == LibGit2.GitCredential()
@test sprint(write, cred) == str
end
@testset "empty" begin
str = """
protocol=
host=
path=
username=
password=
"""
cred = read!(IOBuffer(str), LibGit2.GitCredential())
@test cred == LibGit2.GitCredential("", "", "", "", "")
@test sprint(write, cred) == str
end
@testset "input/output" begin
str = """
protocol=https
host=example.com
username=alice
password=*****
"""
cred = read!(IOBuffer(str), LibGit2.GitCredential())
@test cred == LibGit2.GitCredential("https", "example.com", nothing, "alice", "*****")
@test sprint(write, cred) == str
end
@testset "use http path" begin
cred = LibGit2.GitCredential("https", "example.com", "dir/file", "alice", "*****")
expected = """
protocol=https
host=example.com
username=alice
password=*****
"""
@test cred.use_http_path
cred.use_http_path = false
@test cred.path == "dir/file"
@test sprint(write, cred) == expected
end
@testset "URL input/output" begin
str = """
host=example.com
password=bar
url=https://a@b/c
username=foo
"""
expected = """
protocol=https
host=b
path=c
username=foo
"""
cred = read!(IOBuffer(str), LibGit2.GitCredential())
@test cred == LibGit2.GitCredential("https", "b", "c", "foo", nothing)
@test sprint(write, cred) == expected
end
@testset "ismatch" begin
# Equal
cred = LibGit2.GitCredential("https", "github.com")
@test LibGit2.ismatch("https://github.com", cred)
# Credential hostname is different
cred = LibGit2.GitCredential("https", "github.com")
@test !LibGit2.ismatch("https://myhost", cred)
# Credential is less specific than URL
cred = LibGit2.GitCredential("https")
@test !LibGit2.ismatch("https://github.com", cred)
# Credential is more specific than URL
cred = LibGit2.GitCredential("https", "github.com", "path", "user", "pass")
@test LibGit2.ismatch("https://github.com", cred)
# Credential needs to have an "" username to match
cred = LibGit2.GitCredential("https", "github.com", nothing, "")
@test LibGit2.ismatch("https://@github.com", cred)
cred = LibGit2.GitCredential("https", "github.com", nothing, nothing)
@test !LibGit2.ismatch("https://@github.com", cred)
end
end
mktempdir() do dir
# test parameters
repo_url = "https://github.com/JuliaLang/Example.jl"
cache_repo = joinpath(dir, "Example")
test_repo = joinpath(dir, "Example.Test")
test_sig = LibGit2.Signature("TEST", "TEST@TEST.COM", round(time(), 0), 0)
test_file = "testfile"
config_file = "testconfig"
commit_msg1 = randstring(10)
commit_msg2 = randstring(10)
commit_oid1 = LibGit2.GitHash()
commit_oid2 = LibGit2.GitHash()
commit_oid3 = LibGit2.GitHash()
master_branch = "master"
test_branch = "test_branch"
test_branch2 = "test_branch_two"
tag1 = "tag1"
tag2 = "tag2"
@testset "Configuration" begin
LibGit2.with(LibGit2.GitConfig(joinpath(dir, config_file), LibGit2.Consts.CONFIG_LEVEL_APP)) do cfg
@test_throws LibGit2.Error.GitError LibGit2.get(AbstractString, cfg, "tmp.str")
@test isempty(LibGit2.get(cfg, "tmp.str", "")) == true
LibGit2.set!(cfg, "tmp.str", "AAAA")
LibGit2.set!(cfg, "tmp.int32", Int32(1))
LibGit2.set!(cfg, "tmp.int64", Int64(1))
LibGit2.set!(cfg, "tmp.bool", true)
@test LibGit2.get(cfg, "tmp.str", "") == "AAAA"
@test LibGit2.get(cfg, "tmp.int32", Int32(0)) == Int32(1)
@test LibGit2.get(cfg, "tmp.int64", Int64(0)) == Int64(1)
@test LibGit2.get(cfg, "tmp.bool", false) == true
# Ordering of entries appears random when using `LibGit2.set!`
count = 0
for entry in LibGit2.GitConfigIter(cfg, r"tmp.*")
count += 1
name, value = unsafe_string(entry.name), unsafe_string(entry.value)
if name == "tmp.str"
@test value == "AAAA"
elseif name == "tmp.int32"
@test value == "1"
elseif name == "tmp.int64"
@test value == "1"
elseif name == "tmp.bool"
@test value == "true"
else
error("Found unexpected entry: $name")
end
show_str = sprint(show, entry)
@test show_str == string("ConfigEntry(\"", name, "\", \"", value, "\")")
end
@test count == 4
end
end
@testset "Configuration Iteration" begin
config_path = joinpath(dir, config_file)
# Write config entries with duplicate names
open(config_path, "a") do fp
write(fp, """
[credential]
helper = store
username = julia
[credential]
helper = cache
""")
end
LibGit2.with(LibGit2.GitConfig(config_path, LibGit2.Consts.CONFIG_LEVEL_APP)) do cfg
# Will only see the last entry
@test LibGit2.get(cfg, "credential.helper", "") == "cache"
count = 0
for entry in LibGit2.GitConfigIter(cfg, "credential.helper")
count += 1
name, value = unsafe_string(entry.name), unsafe_string(entry.value)
@test name == "credential.helper"
@test value == (count == 1 ? "store" : "cache")
end
@test count == 2
end
end
@testset "Initializing repository" begin
@testset "with remote branch" begin
LibGit2.with(LibGit2.init(cache_repo)) do repo
@test isdir(cache_repo)
@test LibGit2.path(repo) == LibGit2.posixpath(realpath(cache_repo))
@test isdir(joinpath(cache_repo, ".git"))
# set a remote branch
branch = "upstream"
LibGit2.GitRemote(repo, branch, repo_url) |> close
# test remote's representation in the repo's config
config = joinpath(cache_repo, ".git", "config")
lines = split(open(x->read(x, String), config, "r"), "\n")
@test any(map(x->x == "[remote \"upstream\"]", lines))
LibGit2.with(LibGit2.get(LibGit2.GitRemote, repo, branch)) do remote
# test various remote properties
@test LibGit2.url(remote) == repo_url
@test LibGit2.push_url(remote) == ""
@test LibGit2.name(remote) == "upstream"
@test isa(remote, LibGit2.GitRemote)
# test showing a GitRemote object
@test sprint(show, remote) == "GitRemote:\nRemote name: upstream url: $repo_url"
end
# test setting and getting the remote's URL
@test LibGit2.isattached(repo)
LibGit2.set_remote_url(repo, "upstream", "unknown")
LibGit2.with(LibGit2.get(LibGit2.GitRemote, repo, branch)) do remote
@test LibGit2.url(remote) == "unknown"
@test LibGit2.push_url(remote) == "unknown"
@test sprint(show, remote) == "GitRemote:\nRemote name: upstream url: unknown"
end
LibGit2.set_remote_url(cache_repo, "upstream", repo_url)
LibGit2.with(LibGit2.get(LibGit2.GitRemote, repo, branch)) do remote
@test LibGit2.url(remote) == repo_url
@test LibGit2.push_url(remote) == repo_url
@test sprint(show, remote) == "GitRemote:\nRemote name: upstream url: $repo_url"
LibGit2.add_fetch!(repo, remote, "upstream")
# test setting fetch and push refspecs
@test LibGit2.fetch_refspecs(remote) == String["+refs/heads/*:refs/remotes/upstream/*"]
LibGit2.add_push!(repo, remote, "refs/heads/master")
end
LibGit2.with(LibGit2.get(LibGit2.GitRemote, repo, branch)) do remote
@test LibGit2.push_refspecs(remote) == String["refs/heads/master"]
end
# constructor with a refspec
LibGit2.with(LibGit2.GitRemote(repo, "upstream2", repo_url, "upstream")) do remote
@test sprint(show, remote) == "GitRemote:\nRemote name: upstream2 url: $repo_url"
@test LibGit2.fetch_refspecs(remote) == String["upstream"]
end
LibGit2.with(LibGit2.GitRemoteAnon(repo, repo_url)) do remote
@test LibGit2.url(remote) == repo_url
@test LibGit2.push_url(remote) == ""
@test LibGit2.name(remote) == ""
@test isa(remote, LibGit2.GitRemote)
end
end
end
@testset "bare" begin
path = joinpath(dir, "Example.Bare")
LibGit2.with(LibGit2.init(path, true)) do repo
@test isdir(path)
@test LibGit2.path(repo) == LibGit2.posixpath(realpath(path))
@test isfile(joinpath(path, LibGit2.Consts.HEAD_FILE))
@test LibGit2.isattached(repo)
end
path = joinpath("garbagefakery", "Example.Bare")
try
LibGit2.GitRepo(path)
error("unexpected")
catch e
@test typeof(e) == LibGit2.GitError
@test startswith(
lowercase(sprint(show, e)),
lowercase("GitError(Code:ENOTFOUND, Class:OS, failed to resolve path"))
end
path = joinpath(dir, "Example.BareTwo")
LibGit2.with(LibGit2.init(path, true)) do repo
#just to see if this works
LibGit2.cleanup(repo)
end
end
end
@testset "Cloning repository" begin
function bare_repo_tests(repo, repo_path)
@test isdir(repo_path)
@test LibGit2.path(repo) == LibGit2.posixpath(realpath(repo_path))
@test isfile(joinpath(repo_path, LibGit2.Consts.HEAD_FILE))
@test LibGit2.isattached(repo)
@test LibGit2.remotes(repo) == ["origin"]
end
@testset "bare" begin
repo_path = joinpath(dir, "Example.Bare1")
LibGit2.with(LibGit2.clone(cache_repo, repo_path, isbare = true)) do repo
bare_repo_tests(repo, repo_path)
end
end
@testset "bare with remote callback" begin
repo_path = joinpath(dir, "Example.Bare2")
LibGit2.with(LibGit2.clone(cache_repo, repo_path, isbare = true, remote_cb = LibGit2.mirror_cb())) do repo
bare_repo_tests(repo, repo_path)
LibGit2.with(LibGit2.get(LibGit2.GitRemote, repo, "origin")) do rmt
@test LibGit2.fetch_refspecs(rmt)[1] == "+refs/*:refs/*"
end
end
end
@testset "normal" begin
LibGit2.with(LibGit2.clone(cache_repo, test_repo)) do repo
@test isdir(test_repo)
@test LibGit2.path(repo) == LibGit2.posixpath(realpath(test_repo))
@test isdir(joinpath(test_repo, ".git"))
@test LibGit2.workdir(repo) == LibGit2.path(repo)*"/"
@test LibGit2.isattached(repo)
@test LibGit2.isorphan(repo)
repo_str = sprint(show, repo)
@test repo_str == "LibGit2.GitRepo($(sprint(show,LibGit2.path(repo))))"
end
end
end
@testset "Update cache repository" begin
@testset "with commits" begin
repo = LibGit2.GitRepo(cache_repo)
repo_file = open(joinpath(cache_repo,test_file), "a")
try
# create commits
println(repo_file, commit_msg1)
flush(repo_file)
LibGit2.add!(repo, test_file)
@test LibGit2.iszero(commit_oid1)
commit_oid1 = LibGit2.commit(repo, commit_msg1; author=test_sig, committer=test_sig)
@test !LibGit2.iszero(commit_oid1)
@test LibGit2.GitHash(LibGit2.head(cache_repo)) == commit_oid1
println(repo_file, randstring(10))
flush(repo_file)
LibGit2.add!(repo, test_file)
commit_oid3 = LibGit2.commit(repo, randstring(10); author=test_sig, committer=test_sig)
println(repo_file, commit_msg2)
flush(repo_file)
LibGit2.add!(repo, test_file)
@test LibGit2.iszero(commit_oid2)
commit_oid2 = LibGit2.commit(repo, commit_msg2; author=test_sig, committer=test_sig)
@test !LibGit2.iszero(commit_oid2)
# test getting list of commit authors
auths = LibGit2.authors(repo)
@test length(auths) == 3
for auth in auths
@test auth.name == test_sig.name
@test auth.time == test_sig.time
@test auth.email == test_sig.email
end
# check various commit properties - commit_oid1 happened before
# commit_oid2, so it *is* an ancestor of commit_oid2
@test LibGit2.is_ancestor_of(string(commit_oid1), string(commit_oid2), repo)
@test LibGit2.iscommit(string(commit_oid1), repo)
@test !LibGit2.iscommit(string(commit_oid1)*"fake", repo)
@test LibGit2.iscommit(string(commit_oid2), repo)
# lookup commits
LibGit2.with(LibGit2.GitCommit(repo, commit_oid1)) do cmt
@test LibGit2.Consts.OBJECT(typeof(cmt)) == LibGit2.Consts.OBJ_COMMIT
@test commit_oid1 == LibGit2.GitHash(cmt)
short_oid1 = LibGit2.GitShortHash(string(commit_oid1))
@test hex(commit_oid1) == hex(short_oid1)
@test cmp(commit_oid1, short_oid1) == 0
@test cmp(short_oid1, commit_oid1) == 0
@test !(short_oid1 < commit_oid1)
# test showing ShortHash
short_str = sprint(show, short_oid1)
@test short_str == "GitShortHash(\"$(string(short_oid1))\")"
short_oid2 = LibGit2.GitShortHash(cmt)
@test startswith(hex(commit_oid1), hex(short_oid2))
LibGit2.with(LibGit2.GitCommit(repo, short_oid2)) do cmt2
@test commit_oid1 == LibGit2.GitHash(cmt2)
end
# check that the author and committer signatures are correct
auth = LibGit2.author(cmt)
@test isa(auth, LibGit2.Signature)
@test auth.name == test_sig.name
@test auth.time == test_sig.time
@test auth.email == test_sig.email
short_auth = LibGit2.author(LibGit2.GitCommit(repo, short_oid1))
@test short_auth.name == test_sig.name
@test short_auth.time == test_sig.time
@test short_auth.email == test_sig.email
cmtr = LibGit2.committer(cmt)
@test isa(cmtr, LibGit2.Signature)
@test cmtr.name == test_sig.name
@test cmtr.time == test_sig.time
@test cmtr.email == test_sig.email
@test LibGit2.message(cmt) == commit_msg1
# test showing the commit
showstr = split(sprint(show, cmt), "\n")
# the time of the commit will vary so just test the first two parts
@test contains(showstr[1], "Git Commit:")
@test contains(showstr[2], "Commit Author: Name: TEST, Email: TEST@TEST.COM, Time:")
@test contains(showstr[3], "Committer: Name: TEST, Email: TEST@TEST.COM, Time:")
@test contains(showstr[4], "SHA:")
@test showstr[5] == "Message:"
@test showstr[6] == commit_msg1
@test LibGit2.revcount(repo, string(commit_oid1), string(commit_oid3)) == (-1,0)
blame = LibGit2.GitBlame(repo, test_file)
@test LibGit2.counthunks(blame) == 3
@test_throws BoundsError getindex(blame, LibGit2.counthunks(blame)+1)
@test_throws BoundsError getindex(blame, 0)
sig = LibGit2.Signature(blame[1].orig_signature)
@test sig.name == cmtr.name
@test sig.email == cmtr.email
show_strs = split(sprint(show, blame[1]), "\n")
@test show_strs[1] == "GitBlameHunk:"
@test show_strs[2] == "Original path: $test_file"
@test show_strs[3] == "Lines in hunk: 1"
@test show_strs[4] == "Final commit oid: $commit_oid1"
@test show_strs[6] == "Original commit oid: $commit_oid1"
@test length(show_strs) == 7
end
finally
close(repo)
close(repo_file)
end
end
@testset "with branch" begin
LibGit2.with(LibGit2.GitRepo(cache_repo)) do repo
brnch = LibGit2.branch(repo)
LibGit2.with(LibGit2.head(repo)) do brref
# various branch properties
@test LibGit2.isbranch(brref)
@test !LibGit2.isremote(brref)
@test LibGit2.name(brref) == "refs/heads/master"
@test LibGit2.shortname(brref) == master_branch
@test LibGit2.ishead(brref)
@test 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/master"
@test show_strs[3] == "Branch is HEAD."
@test repo.ptr == LibGit2.repository(brref).ptr
@test brnch == master_branch
@test LibGit2.headname(repo) == master_branch
# create a branch *without* setting its tip as HEAD
LibGit2.branch!(repo, test_branch, string(commit_oid1), set_head=false)
# null because we are looking for a REMOTE branch
@test 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 master_branch in branches
@test test_branch in branches
end
end
@testset "with default configuration" begin
LibGit2.with(LibGit2.GitRepo(cache_repo)) do repo
try
LibGit2.Signature(repo)
catch ex
# these test configure repo with new signature
# in case when global one does not exsist
@test isa(ex, LibGit2.Error.GitError) == true
cfg = LibGit2.GitConfig(repo)
LibGit2.set!(cfg, "user.name", "AAAA")
LibGit2.set!(cfg, "user.email", "BBBB@BBBB.COM")
sig = LibGit2.Signature(repo)
@test sig.name == "AAAA"
@test sig.email == "BBBB@BBBB.COM"
@test LibGit2.getconfig(repo, "user.name", "") == "AAAA"
@test LibGit2.getconfig(cache_repo, "user.name", "") == "AAAA"
end
end
end
@testset "with tags" begin
LibGit2.with(LibGit2.GitRepo(cache_repo)) do repo
tags = LibGit2.tag_list(repo)
@test length(tags) == 0
# create tag and extract it from a GitReference
tag_oid1 = LibGit2.tag_create(repo, tag1, commit_oid1, sig=test_sig)
@test !LibGit2.iszero(tag_oid1)
tags = LibGit2.tag_list(repo)
@test length(tags) == 1
@test tag1 in tags
tag1ref = LibGit2.GitReference(repo, "refs/tags/$tag1")
# because this is a reference to an OID
@test isempty(LibGit2.fullname(tag1ref))
# test showing a GitReference to a GitTag, and the GitTag itself
show_strs = split(sprint(show, tag1ref), "\n")
@test show_strs[1] == "GitReference:"
@test show_strs[2] == "Tag with name refs/tags/$tag1"
tag1tag = LibGit2.peel(LibGit2.GitTag, tag1ref)
@test LibGit2.name(tag1tag) == tag1
@test LibGit2.target(tag1tag) == commit_oid1
@test sprint(show, tag1tag) == "GitTag:\nTag name: $tag1 target: $commit_oid1"
# peels to the commit the tag points to
tag1cmt = LibGit2.peel(tag1ref)
@test LibGit2.GitHash(tag1cmt) == commit_oid1
tag_oid2 = LibGit2.tag_create(repo, tag2, commit_oid2)
@test !LibGit2.iszero(tag_oid2)
tags = LibGit2.tag_list(repo)
@test length(tags) == 2
@test tag2 in tags
refs = LibGit2.ref_list(repo)
@test refs == ["refs/heads/master", "refs/heads/test_branch", "refs/tags/tag1", "refs/tags/tag2"]
# test deleting a tag
LibGit2.tag_delete(repo, tag1)
tags = LibGit2.tag_list(repo)
@test length(tags) == 1
@test tag2 ∈ tags
@test tag1 ∉ tags
# test git describe functions applied to these GitTags
description = LibGit2.GitDescribeResult(repo)
fmtted_description = LibGit2.format(description)
@test sprint(show, description) == "GitDescribeResult:\n$fmtted_description\n"
@test fmtted_description == "tag2"
description = LibGit2.GitDescribeResult(LibGit2.GitObject(repo, "HEAD"))
fmtted_description = LibGit2.format(description)
@test sprint(show, description) == "GitDescribeResult:\n$fmtted_description\n"
@test fmtted_description == "tag2"
end
end
@testset "status" begin
LibGit2.with(LibGit2.GitRepo(cache_repo)) do repo
status = LibGit2.GitStatus(repo)
@test length(status) == 0
@test_throws BoundsError status[1]
repo_file = open(joinpath(cache_repo,"statusfile"), "a")
# create commits
println(repo_file, commit_msg1)
flush(repo_file)
LibGit2.add!(repo, test_file)
status = LibGit2.GitStatus(repo)
@test length(status) != 0
@test_throws BoundsError status[0]
@test_throws BoundsError status[length(status)+1]
# we've added a file - show that it is new
@test status[1].status == LibGit2.Consts.STATUS_WT_NEW
close(repo_file)
end
end
@testset "blobs" begin
LibGit2.with(LibGit2.GitRepo(cache_repo)) do repo
# this is slightly dubious, as it assumes the object has not been packed
# could be replaced by another binary format
hash_string = hex(commit_oid1)
blob_file = joinpath(cache_repo,".git/objects", hash_string[1:2], hash_string[3:end])
id = LibGit2.addblob!(repo, blob_file)
blob = LibGit2.GitBlob(repo, id)
@test LibGit2.isbinary(blob)
len1 = length(blob)
# test showing a GitBlob
blob_show_strs = split(sprint(show, blob), "\n")
@test blob_show_strs[1] == "GitBlob:"
@test contains(blob_show_strs[2], "Blob id:")
@test blob_show_strs[3] == "Contents are binary."
blob2 = LibGit2.GitBlob(repo, LibGit2.GitHash(blob))
@test LibGit2.isbinary(blob2)
@test length(blob2) == len1
end
end
@testset "trees" begin
LibGit2.with(LibGit2.GitRepo(cache_repo)) do repo
@test_throws LibGit2.Error.GitError LibGit2.GitTree(repo, "HEAD")
tree = LibGit2.GitTree(repo, "HEAD^{tree}")
@test isa(tree, LibGit2.GitTree)
@test isa(LibGit2.GitObject(repo, "HEAD^{tree}"), LibGit2.GitTree)
@test LibGit2.Consts.OBJECT(typeof(tree)) == LibGit2.Consts.OBJ_TREE
@test count(tree) == 1
# test showing the GitTree and its entries
tree_str = sprint(show, tree)
@test tree_str == "GitTree:\nOwner: $(LibGit2.repository(tree))\nNumber of entries: 1\n"
@test_throws BoundsError tree[0]
@test_throws BoundsError tree[2]
tree_entry = tree[1]
@test LibGit2.filemode(tree_entry) == 33188
te_str = sprint(show, tree_entry)
ref_te_str = "GitTreeEntry:\nEntry name: testfile\nEntry type: Base.LibGit2.GitBlob\nEntry OID: "
ref_te_str *= "$(LibGit2.entryid(tree_entry))\n"
@test te_str == ref_te_str
blob = LibGit2.GitBlob(tree_entry)
blob_str = sprint(show, blob)
@test blob_str == "GitBlob:\nBlob id: $(LibGit2.GitHash(blob))\nContents:\n$(LibGit2.content(blob))\n"
end
end
@testset "diff" begin
LibGit2.with(LibGit2.GitRepo(cache_repo)) do repo
@test !LibGit2.isdirty(repo)
@test !LibGit2.isdirty(repo, test_file)
@test !LibGit2.isdirty(repo, "nonexistent")
@test !LibGit2.isdiff(repo, "HEAD")
@test !LibGit2.isdirty(repo, cached=true)
@test !LibGit2.isdirty(repo, test_file, cached=true)
@test !LibGit2.isdirty(repo, "nonexistent", cached=true)
@test !LibGit2.isdiff(repo, "HEAD", cached=true)
open(joinpath(cache_repo,test_file), "a") do f
println(f, "zzzz")
end
@test LibGit2.isdirty(repo)
@test LibGit2.isdirty(repo, test_file)
@test !LibGit2.isdirty(repo, "nonexistent")
@test LibGit2.isdiff(repo, "HEAD")
@test !LibGit2.isdirty(repo, cached=true)
@test !LibGit2.isdiff(repo, "HEAD", cached=true)
LibGit2.add!(repo, test_file)
@test LibGit2.isdirty(repo)
@test LibGit2.isdiff(repo, "HEAD")
@test LibGit2.isdirty(repo, cached=true)
@test LibGit2.isdiff(repo, "HEAD", cached=true)
tree = LibGit2.GitTree(repo, "HEAD^{tree}")
# test properties of the diff_tree
diff = LibGit2.diff_tree(repo, tree, "", cached=true)
@test count(diff) == 1
@test_throws BoundsError diff[0]
@test_throws BoundsError diff[2]
@test LibGit2.Consts.DELTA_STATUS(diff[1].status) == LibGit2.Consts.DELTA_MODIFIED
@test diff[1].nfiles == 2
# test showing a DiffDelta
diff_strs = split(sprint(show, diff[1]), '\n')
@test diff_strs[1] == "DiffDelta:"
@test diff_strs[2] == "Status: DELTA_MODIFIED"
@test diff_strs[3] == "Number of files: 2"
@test diff_strs[4] == "Old file:"
@test diff_strs[5] == "DiffFile:"
@test contains(diff_strs[6], "Oid:")
@test contains(diff_strs[7], "Path:")
@test contains(diff_strs[8], "Size:")
@test isempty(diff_strs[9])
@test diff_strs[10] == "New file:"
# test showing a GitDiff
diff_strs = split(sprint(show, diff), '\n')
@test diff_strs[1] == "GitDiff:"
@test diff_strs[2] == "Number of deltas: 1"
@test diff_strs[3] == "GitDiffStats:"
@test diff_strs[4] == "Files changed: 1"
@test diff_strs[5] == "Insertions: 1"
@test diff_strs[6] == "Deletions: 0"
LibGit2.commit(repo, "zzz")
@test !LibGit2.isdirty(repo)
@test !LibGit2.isdiff(repo, "HEAD")
@test !LibGit2.isdirty(repo, cached=true)
@test !LibGit2.isdiff(repo, "HEAD", cached=true)
end
end
end
function setup_clone_repo(cache_repo::AbstractString, path::AbstractString; name="AAAA", email="BBBB@BBBB.COM")
repo = LibGit2.clone(cache_repo, path)
# need to set this for merges to succeed
cfg = LibGit2.GitConfig(repo)
LibGit2.set!(cfg, "user.name", name)
LibGit2.set!(cfg, "user.email", email)
return repo
end
# TO DO: add more tests for various merge
# preference options
function add_and_commit_file(repo, filenm, filecontent)
open(joinpath(LibGit2.path(repo), filenm),"w") do f
write(f, filecontent)
end
LibGit2.add!(repo, filenm)
return LibGit2.commit(repo, "add $filenm")
end
@testset "Fastforward merges" begin
LibGit2.with(setup_clone_repo(cache_repo, joinpath(dir, "Example.FF"))) do repo
# Sets up a branch "branch/ff_a" which will be two commits ahead
# of "master". It's possible to fast-forward merge "branch/ff_a"
# into "master", which is the default behavior.
oldhead = LibGit2.head_oid(repo)
LibGit2.branch!(repo, "branch/ff_a")
add_and_commit_file(repo, "ff_file1", "111\n")
add_and_commit_file(repo, "ff_file2", "222\n")
LibGit2.branch!(repo, "master")
# switch back, now try to ff-merge the changes
# from branch/a
# set up the merge using GitAnnotated objects
upst_ann = LibGit2.GitAnnotated(repo, "branch/ff_a")
head_ann = LibGit2.GitAnnotated(repo, "master")
# ff merge them
@test LibGit2.merge!(repo, [upst_ann], true)
@test LibGit2.is_ancestor_of(string(oldhead), string(LibGit2.head_oid(repo)), repo)
# Repeat the process, but specifying a commit to merge in as opposed
# to a branch name or GitAnnotated.
oldhead = LibGit2.head_oid(repo)
LibGit2.branch!(repo, "branch/ff_b")
add_and_commit_file(repo, "ff_file3", "333\n")
branchhead = add_and_commit_file(repo, "ff_file4", "444\n")
LibGit2.branch!(repo, "master")
# switch back, now try to ff-merge the changes
# from branch/a using committish
@test LibGit2.merge!(repo, committish=string(branchhead))
@test LibGit2.is_ancestor_of(string(oldhead), string(LibGit2.head_oid(repo)), repo)
# Repeat the process, but specifying a branch name to merge in as opposed
# to a commit or GitAnnotated.
oldhead = LibGit2.head_oid(repo)
LibGit2.branch!(repo, "branch/ff_c")
add_and_commit_file(repo, "ff_file5", "555\n")
branchhead = add_and_commit_file(repo, "ff_file6", "666\n")
LibGit2.branch!(repo, "master")
# switch back, now try to ff-merge the changes
# from branch/ff_c using branch name
@test LibGit2.merge!(repo, branch="refs/heads/branch/ff_c")
@test LibGit2.is_ancestor_of(string(oldhead), string(LibGit2.head_oid(repo)), repo)
LibGit2.branch!(repo, "branch/ff_d")
branchhead = add_and_commit_file(repo, "ff_file7", "777\n")
LibGit2.branch!(repo, "master")
# switch back, now try to ff-merge the changes
# from branch/a
# set up the merge using GitAnnotated objects
# from a fetchhead
fh = LibGit2.fetchheads(repo)
upst_ann = LibGit2.GitAnnotated(repo, fh[1])
@test LibGit2.merge!(repo, [upst_ann], true)
@test LibGit2.is_ancestor_of(string(oldhead), string(LibGit2.head_oid(repo)), repo)
end
end
@testset "Cherrypick" begin
LibGit2.with(setup_clone_repo(cache_repo, joinpath(dir, "Example.Cherrypick"))) do repo
# Create a commit on the new branch and cherry-pick it over to
# master. Since the cherry-pick does *not* make a new commit on
# master, we have to create our own commit of the dirty state.
oldhead = LibGit2.head_oid(repo)
LibGit2.branch!(repo, "branch/cherry_a")
cmt_oid = add_and_commit_file(repo, "file1", "111\n")
cmt = LibGit2.GitCommit(repo, cmt_oid)
# switch back, try to cherrypick
# from branch/cherry_a
LibGit2.branch!(repo, "master")
LibGit2.cherrypick(repo, cmt, options=Base.LibGit2.CherrypickOptions())
cmt_oid2 = LibGit2.commit(repo, "add file1")
@test isempty(LibGit2.diff_files(repo, "master", "branch/cherry_a"))
end
end
@testset "Merges" begin
LibGit2.with(setup_clone_repo(cache_repo, joinpath(dir, "Example.Merge"))) do repo
oldhead = LibGit2.head_oid(repo)
LibGit2.branch!(repo, "branch/merge_a")
add_and_commit_file(repo, "file1", "111\n")
# switch back, add a commit, try to merge
# from branch/merge_a
LibGit2.branch!(repo, "master")
# test for showing a Reference to a non-HEAD branch
brref = LibGit2.GitReference(repo, "refs/heads/branch/merge_a")
@test LibGit2.name(brref) == "refs/heads/branch/merge_a"
@test !LibGit2.ishead(brref)
show_strs = split(sprint(show, brref), "\n")
@test show_strs[1] == "GitReference:"
@test show_strs[2] == "Branch with name refs/heads/branch/merge_a"
@test show_strs[3] == "Branch is not HEAD."
add_and_commit_file(repo, "file2", "222\n")
upst_ann = LibGit2.GitAnnotated(repo, "branch/merge_a")
head_ann = LibGit2.GitAnnotated(repo, "master")
# (fail to) merge them because we can't fastforward
@test_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, "master")
upst_ann = LibGit2.GitAnnotated(repo, "branch/merge_b")
rename_flag = 0
rename_flag = LibGit2.toggle(rename_flag, 0) # turns on the find renames opt
mos = LibGit2.MergeOptions(flags=rename_flag)
@test_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
end
@testset "Fetch from cache repository" begin
LibGit2.with(LibGit2.GitRepo(test_repo)) do repo
# fetch changes
@test LibGit2.fetch(repo) == 0
@test !isfile(joinpath(test_repo, test_file))
# ff merge them
@test LibGit2.merge!(repo, fastforward=true)
# because there was not any file we need to reset branch
head_oid = LibGit2.head_oid(repo)
new_head = LibGit2.reset!(repo, head_oid, LibGit2.Consts.RESET_HARD)
@test isfile(joinpath(test_repo, test_file))
@test new_head == head_oid
# GitAnnotated for a fetchhead
fh_ann = LibGit2.GitAnnotated(repo, LibGit2.Consts.FETCH_HEAD)
@test LibGit2.GitHash(fh_ann) == head_oid
# Detach HEAD - no merge
LibGit2.checkout!(repo, string(commit_oid3))
@test_throws LibGit2.Error.GitError LibGit2.merge!(repo, fastforward=true)
# Switch to a branch without remote - no merge
LibGit2.branch!(repo, test_branch)
@test_throws LibGit2.Error.GitError LibGit2.merge!(repo, fastforward=true)
# Set the username and email for the test_repo (needed for rebase)
cfg = LibGit2.GitConfig(repo)
LibGit2.set!(cfg, "user.name", "AAAA")
LibGit2.set!(cfg, "user.email", "BBBB@BBBB.COM")
# If upstream argument is empty, libgit2 will look for tracking
# information. If the current branch isn't tracking any upstream
# the rebase should fail.
@test_throws LibGit2.GitError LibGit2.rebase!(repo)
# Try rebasing on master instead
newhead = LibGit2.rebase!(repo, master_branch)
@test newhead == head_oid
# Switch to the master branch
LibGit2.branch!(repo, master_branch)
fetch_heads = LibGit2.fetchheads(repo)
@test fetch_heads[1].name == "refs/heads/master"
@test fetch_heads[1].ismerge == true # we just merged master
@test fetch_heads[2].name == "refs/heads/test_branch"
@test fetch_heads[2].ismerge == false
@test fetch_heads[3].name == "refs/tags/tag2"
@test fetch_heads[3].ismerge == false
for fh in fetch_heads
@test fh.url == cache_repo
fh_strs = split(sprint(show, fh), '\n')
@test fh_strs[1] == "FetchHead:"
@test fh_strs[2] == "Name: $(fh.name)"
@test fh_strs[3] == "URL: $(fh.url)"
@test fh_strs[5] == "Merged: $(fh.ismerge)"
end
end
end
@testset "Examine test repository" begin
@testset "files" begin
@test read(joinpath(test_repo, test_file), String) == read(joinpath(cache_repo, test_file), String)
end
@testset "tags & branches" begin
LibGit2.with(LibGit2.GitRepo(test_repo)) do repo
# all tag in place
tags = LibGit2.tag_list(repo)
@test length(tags) == 1
@test tag2 in tags
# all tag in place
branches = map(b->LibGit2.shortname(b[1]), LibGit2.GitBranchIter(repo))
@test master_branch in branches
@test test_branch in branches
# issue #16337
LibGit2.with(LibGit2.GitReference(repo, "refs/tags/$tag2")) do tag2ref
@test_throws LibGit2.Error.GitError LibGit2.upstream(tag2ref)
end
end
end
@testset "commits with revwalk" begin
repo = LibGit2.GitRepo(test_repo)
cache = LibGit2.GitRepo(cache_repo)
try
# test map with oid
oids = LibGit2.with(LibGit2.GitRevWalker(repo)) do walker
LibGit2.map((oid,repo)->(oid,repo), walker, oid=commit_oid1, by=LibGit2.Consts.SORT_TIME)
end
@test length(oids) == 1
# test map with range
str_1 = string(commit_oid1)
str_3 = string(commit_oid3)
oids = LibGit2.with(LibGit2.GitRevWalker(repo)) do walker
LibGit2.map((oid,repo)->(oid,repo), walker, range="$str_1..$str_3", by=LibGit2.Consts.SORT_TIME)
end
@test length(oids) == 1
test_oids = LibGit2.with(LibGit2.GitRevWalker(repo)) do walker
LibGit2.map((oid,repo)->string(oid), walker, by = LibGit2.Consts.SORT_TIME)
end
cache_oids = LibGit2.with(LibGit2.GitRevWalker(cache)) do walker
LibGit2.map((oid,repo)->string(oid), walker, by = LibGit2.Consts.SORT_TIME)
end
for i in eachindex(oids)
@test cache_oids[i] == test_oids[i]
end
# test with specified oid
LibGit2.with(LibGit2.GitRevWalker(repo)) do walker
@test count((oid,repo)->(oid == commit_oid1), walker, oid=commit_oid1, by=LibGit2.Consts.SORT_TIME) == 1
end
# test without specified oid
LibGit2.with(LibGit2.GitRevWalker(repo)) do walker
@test count((oid,repo)->(oid == commit_oid1), walker, by=LibGit2.Consts.SORT_TIME) == 1
end
finally
close(repo)
close(cache)
end
end
end
@testset "Modify and reset repository" begin
LibGit2.with(LibGit2.GitRepo(test_repo)) do repo
# check index for file
LibGit2.with(LibGit2.GitIndex(repo)) do idx
i = find(test_file, idx)
@test 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 = find("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 count(idx) == 0
LibGit2.add!(repo, test_file)
LibGit2.update!(repo, test_file)
@test count(idx) == 1
end
# check non-existent file status
st = LibGit2.status(repo, "XYZ")
@test 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", "master")
newerhead = LibGit2.head_oid(repo)
@test newerhead == newhead
# add yet more files
add_and_commit_file(repo, "file5", "555\n")
pre_abort_head = add_and_commit_file(repo, "file6", "666\n")
# Rebase type
head_ann = LibGit2.GitAnnotated(repo, "branch/a")
upst_ann = LibGit2.GitAnnotated(repo, "master")
rb = LibGit2.GitRebase(repo, head_ann, upst_ann)
@test_throws BoundsError rb[3]
@test_throws BoundsError rb[0]
rbo = next(rb)
rbo_str = sprint(show, rbo)
@test rbo_str == "RebaseOperation($(string(rbo.id)))\nOperation type: REBASE_OPERATION_PICK\n"
rb_str = sprint(show, rb)
@test rb_str == "GitRebase:\nNumber: 2\nCurrently performing operation: 1\n"
rbo = rb[2]
rbo_str = sprint(show, rbo)
@test rbo_str == "RebaseOperation($(string(rbo.id)))\nOperation type: REBASE_OPERATION_PICK\n"
# test rebase abort
LibGit2.abort(rb)
@test LibGit2.head_oid(repo) == pre_abort_head
end
end
@testset "merge" begin
LibGit2.with(setup_clone_repo(cache_repo, joinpath(dir, "Example.simple_merge"))) do repo
LibGit2.branch!(repo, "branch/merge_a")
a_head = LibGit2.head_oid(repo)
add_and_commit_file(repo, "merge_file1", "111\n")
LibGit2.branch!(repo, "master")
a_head_ann = LibGit2.GitAnnotated(repo, "branch/merge_a")
# merge returns true if successful
@test_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/headname" begin
LibGit2.with(LibGit2.GitRepo(cache_repo)) do repo
LibGit2.checkout!(repo, string(commit_oid1))
@test !LibGit2.isattached(repo)
@test LibGit2.headname(repo) == "(detached from $(string(commit_oid1)[1:7]))"
end
end
if Sys.isunix()
@testset "checkout/proptest" begin
LibGit2.with(LibGit2.GitRepo(test_repo)) do repo
cp(joinpath(test_repo, test_file), joinpath(test_repo, "proptest"))
LibGit2.add!(repo, "proptest")
id1 = LibGit2.commit(repo, "test property change 1")
# change in file permissions (#17610)
chmod(joinpath(test_repo, "proptest"),0o744)
LibGit2.add!(repo, "proptest")
id2 = LibGit2.commit(repo, "test property change 2")
LibGit2.checkout!(repo, string(id1))
@test !LibGit2.isdirty(repo)
# change file to symlink (#18420)
mv(joinpath(test_repo, "proptest"), joinpath(test_repo, "proptest2"))
symlink(joinpath(test_repo, "proptest2"), joinpath(test_repo, "proptest"))
LibGit2.add!(repo, "proptest", "proptest2")
id3 = LibGit2.commit(repo, "test symlink change")
LibGit2.checkout!(repo, string(id1))
@test !LibGit2.isdirty(repo)
end
end
end
@testset "Credentials" begin
creds_user = "USER"
creds_pass = "PASS"
creds = LibGit2.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 isempty(sshcreds.prvkey)
@test isempty(sshcreds.pubkey)
sshcreds2 = LibGit2.SSHCredential(creds_user, creds_pass)
@test sshcreds == sshcreds2
end
@testset "CachedCredentials" begin
cache = LibGit2.CachedCredentials()
url = "https://github.com/JuliaLang/Example.jl"
cred_id = LibGit2.credential_identifier(url)
cred = LibGit2.UserPasswordCredential("julia", "password")
@test !haskey(cache, cred_id)
# Attempt to reject a credential which wasn't stored
LibGit2.reject(cache, cred, url)
@test !haskey(cache, cred_id)
@test cred.user == "julia"
@test cred.pass == "password"
# Approve a credential which causes it to be stored
LibGit2.approve(cache, cred, url)
@test haskey(cache, cred_id)
@test cache[cred_id] === cred
# Reject an approved should cause it to be removed
LibGit2.reject(cache, cred, url)
@test !haskey(cache, cred_id)
@test cred.user == "julia"
@test cred.pass == "password"
end
@testset "Git credential username" begin
@testset "fill username" begin
config_path = joinpath(dir, config_file)
isfile(config_path) && rm(config_path)
LibGit2.with(LibGit2.GitConfig(config_path, LibGit2.Consts.CONFIG_LEVEL_APP)) do cfg
# No credential settings should be set for these tests
@test isempty(collect(LibGit2.GitConfigIter(cfg, r"credential.*")))
# No credential settings in configuration.
cred = LibGit2.GitCredential("https", "github.com")
username = LibGit2.default_username(cfg, cred)
@test username === nothing
# Add a credential setting for a specific for a URL
LibGit2.set!(cfg, "credential.https://github.com.username", "foo")
cred = LibGit2.GitCredential("https", "github.com")
username = LibGit2.default_username(cfg, cred)
@test username == "foo"
cred = LibGit2.GitCredential("https", "mygithost")
username = LibGit2.default_username(cfg, 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")
cred = LibGit2.GitCredential("https", "github.com")
username = LibGit2.default_username(cfg, cred)
@test username == "foo"
cred = LibGit2.GitCredential("https", "mygithost")
username = LibGit2.default_username(cfg, cred)
@test username == "bar"
end
end
@testset "empty username" begin
config_path = joinpath(dir, config_file)
isfile(config_path) && rm(config_path)
LibGit2.with(LibGit2.GitConfig(config_path, LibGit2.Consts.CONFIG_LEVEL_APP)) do cfg
# No credential settings should be set for these tests
@test isempty(collect(LibGit2.GitConfigIter(cfg, r"credential.*")))
# An empty username should count as being set
LibGit2.set!(cfg, "credential.https://github.com.username", "")
LibGit2.set!(cfg, "credential.username", "name")
cred = LibGit2.GitCredential("https", "github.com")
username = LibGit2.default_username(cfg, cred)
@test username == ""
cred = LibGit2.GitCredential("https", "mygithost", "path")
username = LibGit2.default_username(cfg, cred)
@test username == "name"
end
end
end
@testset "Git helpers useHttpPath" begin
@testset "use_http_path" begin
config_path = joinpath(dir, config_file)
isfile(config_path) && rm(config_path)
LibGit2.with(LibGit2.GitConfig(config_path, LibGit2.Consts.CONFIG_LEVEL_APP)) do cfg
# No credential settings should be set for these tests
@test isempty(collect(LibGit2.GitConfigIter(cfg, r"credential.*")))
# No credential settings in configuration.
cred = LibGit2.GitCredential("https", "github.com")
@test !LibGit2.use_http_path(cfg, cred)
# Add a credential setting for a specific for a URL
LibGit2.set!(cfg, "credential.https://github.com.useHttpPath", "true")
cred = LibGit2.GitCredential("https", "github.com")
@test LibGit2.use_http_path(cfg, cred)
cred = LibGit2.GitCredential("https", "mygithost")
@test !LibGit2.use_http_path(cfg, cred)
# Invert the current settings.
LibGit2.set!(cfg, "credential.useHttpPath", "true")
LibGit2.set!(cfg, "credential.https://github.com.useHttpPath", "false")
cred = LibGit2.GitCredential("https", "github.com")
@test !LibGit2.use_http_path(cfg, cred)
cred = LibGit2.GitCredential("https", "mygithost")
@test LibGit2.use_http_path(cfg, cred)
end
end
end
@testset "GitCredentialHelper" begin
GitCredentialHelper = LibGit2.GitCredentialHelper
GitCredential = LibGit2.GitCredential
@testset "parse" begin
@test parse(GitCredentialHelper, "!echo hello") == GitCredentialHelper(`echo hello`)
@test parse(GitCredentialHelper, "/bin/bash") == GitCredentialHelper(`/bin/bash`)
@test parse(GitCredentialHelper, "store") == GitCredentialHelper(`git credential-store`)
end
@testset "empty helper" begin
config_path = joinpath(dir, config_file)
# Note: LibGit2.set! doesn't allow us to set duplicates or ordering
open(config_path, "w+") do fp
write(fp, """
[credential]
helper = !echo first
[credential "https://mygithost"]
helper = ""
[credential]
helper = !echo second
""")
end
LibGit2.with(LibGit2.GitConfig(config_path, LibGit2.Consts.CONFIG_LEVEL_APP)) do cfg
@test length(collect(LibGit2.GitConfigIter(cfg, r"credential.*"))) == 3
expected = [
GitCredentialHelper(`echo first`),
GitCredentialHelper(`echo second`),
]
@test LibGit2.credential_helpers(cfg, GitCredential("https", "github.com")) == expected
println(STDERR, "The following 'Resetting the helper list...' warning is expected:")
@test_broken LibGit2.credential_helpers(cfg, GitCredential("https", "mygithost")) == expected[2]
end
end
@testset "approve/reject" begin
# In order to use the "store" credential helper `git` needs to be installed and
# on the path.
if GIT_INSTALLED
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)
@test LibGit2.fill!(helper, deepcopy(query)) == query
LibGit2.approve(helper, filled)
@test isfile(credential_path)
@test LibGit2.fill!(helper, deepcopy(query)) == filled
LibGit2.reject(helper, filled)
@test LibGit2.fill!(helper, deepcopy(query)) == query
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
@test !isfile(credential_path)
@test LibGit2.fill!(helper, deepcopy(query)) == query
@test LibGit2.fill!(helper, deepcopy(query_a)) == query_a
@test LibGit2.fill!(helper, deepcopy(query_b)) == query_b
LibGit2.approve(helper, filled_a)
@test isfile(credential_path)
@test LibGit2.fill!(helper, deepcopy(query)) == without_path(filled_a)
@test LibGit2.fill!(helper, deepcopy(query_a)) == filled_a
@test LibGit2.fill!(helper, deepcopy(query_b)) == query_b
LibGit2.approve(helper, filled_b)
@test LibGit2.fill!(helper, deepcopy(query)) == without_path(filled_b)
@test LibGit2.fill!(helper, deepcopy(query_a)) == filled_a
@test LibGit2.fill!(helper, deepcopy(query_b)) == filled_b
LibGit2.reject(helper, filled_b)
@test LibGit2.fill!(helper, deepcopy(query)) == without_path(filled_a)
@test LibGit2.fill!(helper, deepcopy(query_a)) == filled_a
@test LibGit2.fill!(helper, deepcopy(query_b)) == query_b
end
end
end
end
# 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
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
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
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
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
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
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
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
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
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
# 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
end
# A hypothetical scenario where the the allowed authentication can either be
# SSH or username/password.
@testset "SSH & HTTPS authentication" begin
allowed_types = Cuint(LibGit2.Consts.CREDTYPE_SSH_KEY) |
Cuint(LibGit2.Consts.CREDTYPE_USERPASS_PLAINTEXT)
# User provides a user/password credential where a SSH credential is required.
ex = quote
include($LIBGIT2_HELPER_PATH)
valid_cred = LibGit2.UserPasswordCredential("foo", "bar")
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
end
@testset "CredentialPayload reset" begin
urls = [
"https://github.com/test/package.jl"
"https://myhost.com/demo.jl"
]
valid_username = "julia"
valid_password = randstring(16)
# Users should be able to re-use the same payload if the state is reset
ex = quote
include($LIBGIT2_HELPER_PATH)
valid_cred = LibGit2.UserPasswordCredential($valid_username, $valid_password)
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
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"\..*$" => ""))
end
loopback = ip"127.0.0.1"
for hostname in hostnames
local addr
try
addr = getaddrinfo(hostname)
catch
continue
end
if addr == loopback
common_name = hostname
break
end
end
if isempty(common_name)
@warn "Skipping hostname verification tests. Unable to determine a hostname which maps to the loopback address"
end
end
if openssl_installed && !isempty(common_name)
mktempdir() do root
key = joinpath(root, common_name * ".key")
cert = joinpath(root, common_name * ".crt")
pem = joinpath(root, common_name * ".pem")
# Generated a certificate which has the CN set correctly but no subjectAltName
run(pipeline(`openssl req -new -x509 -newkey rsa:2048 -sha256 -nodes -keyout $key -out $cert -days 1 -subj "/CN=$common_name"`, stderr=DevNull))
run(`openssl x509 -in $cert -out $pem -outform PEM`)
# Find an available port by listening
port, server = listenany(49152)
close(server)
# Make a fake Julia package and minimal HTTPS server with our generated
# certificate. The minimal server can't actually serve a Git repository.
mkdir(joinpath(root, "Example.jl"))
pobj = cd(root) do
spawn(`openssl s_server -key $key -cert $cert -WWW -accept $port`)
end
errfile = joinpath(root, "error")
repo_url = "https://$common_name:$port/Example.jl"
repo_dir = joinpath(root, "dest")
code = """
dest_dir = "$repo_dir"
open("$errfile", "w+") do f
try
repo = LibGit2.clone("$repo_url", dest_dir)
catch err
serialize(f, err)
finally
isdir(dest_dir) && rm(dest_dir, recursive=true)
end
end
"""
cmd = `$(Base.julia_cmd()) --startup-file=no -e $code`
try
# The generated certificate is normally invalid
run(cmd)
err = open(errfile, "r") do f
deserialize(f)
end
@test err.code == LibGit2.Error.ECERTIFICATE
@test startswith(lowercase(err.msg),
lowercase("The SSL certificate is invalid"))
rm(errfile)
# Specify that Julia use only the custom certificate. Note: we need to
# spawn a new Julia process in order for this ENV variable to take effect.
withenv("SSL_CERT_FILE" => pem) do
run(cmd)
err = open(errfile, "r") do f
deserialize(f)
end
@test err.code == LibGit2.Error.ERROR
@test lowercase(err.msg) == lowercase("invalid Content-Type: text/plain")
end
# OpenSSL s_server should still be running
@test process_running(pobj)
finally
kill(pobj)
end
end
end
end
end
Computing file changes ...