# This file is a part of Julia. License is MIT: https://julialang.org/license isdefined(Main, :TestHelpers) || @eval Main include(joinpath(dirname(@__FILE__), "TestHelpers.jl")) import TestHelpers: challenge_prompt const LIBGIT2_MIN_VER = v"0.23.0" const LIBGIT2_HELPER_PATH = joinpath(dirname(@__FILE__), "libgit2-helpers.jl") 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(f, LibGit2.Consts.FEATURE_SSH) > 0 @test findfirst(f, LibGit2.Consts.FEATURE_HTTPS) > 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)) 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 is_unix() && @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] == "" 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] == "" 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 cfg = LibGit2.GitConfig(joinpath(dir, config_file), LibGit2.Consts.CONFIG_LEVEL_APP) try @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 finally close(cfg) end end @testset "Initializing repository" begin @testset "with remote branch" begin repo = LibGit2.init(cache_repo) try @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 config = joinpath(cache_repo, ".git", "config") lines = split(open(readstring, config, "r"), "\n") @test any(map(x->x == "[remote \"upstream\"]", lines)) remote = LibGit2.get(LibGit2.GitRemote, repo, branch) @test LibGit2.url(remote) == repo_url @test LibGit2.name(remote) == "upstream" @test isa(remote, LibGit2.GitRemote) @test sprint(show, remote) == "GitRemote:\nRemote name: upstream url: $repo_url" @test LibGit2.isattached(repo) LibGit2.set_remote_url(repo, "", remote="upstream") remote = LibGit2.get(LibGit2.GitRemote, repo, branch) @test sprint(show, remote) == "GitRemote:\nRemote name: upstream url: " close(remote) LibGit2.set_remote_url(cache_repo, repo_url, remote="upstream") remote = LibGit2.get(LibGit2.GitRemote, repo, branch) @test sprint(show, remote) == "GitRemote:\nRemote name: upstream url: $repo_url" LibGit2.add_fetch!(repo, remote, "upstream") @test LibGit2.fetch_refspecs(remote) == String["+refs/heads/*:refs/remotes/upstream/*"] LibGit2.add_push!(repo, remote, "refs/heads/master") close(remote) remote = LibGit2.get(LibGit2.GitRemote, repo, branch) @test LibGit2.push_refspecs(remote) == String["refs/heads/master"] close(remote) # constructor with a refspec remote = LibGit2.GitRemote(repo, "upstream2", repo_url, "upstream") @test sprint(show, remote) == "GitRemote:\nRemote name: upstream2 url: $repo_url" @test LibGit2.fetch_refspecs(remote) == String["upstream"] close(remote) remote = LibGit2.GitRemoteAnon(repo, repo_url) @test LibGit2.url(remote) == repo_url @test LibGit2.name(remote) == "" @test isa(remote, LibGit2.GitRemote) close(remote) finally close(repo) end end @testset "bare" begin path = joinpath(dir, "Example.Bare") repo = LibGit2.init(path, true) try @test isdir(path) @test LibGit2.path(repo) == LibGit2.posixpath(realpath(path)) @test isfile(joinpath(path, LibGit2.Consts.HEAD_FILE)) @test LibGit2.isattached(repo) finally close(repo) end path = joinpath("garbagefakery", "Example.Bare") try LibGit2.GitRepo(path) error("unexpected") catch e @test typeof(e) == LibGit2.GitError @test startswith(sprint(show,e),"GitError(Code:ENOTFOUND, Class:OS, Failed to resolve path") end path = joinpath(dir, "Example.BareTwo") repo = LibGit2.init(path, true) try #just to see if this works LibGit2.cleanup(repo) finally close(repo) end end end @testset "Cloning repository" begin @testset "bare" begin repo_path = joinpath(dir, "Example.Bare1") repo = LibGit2.clone(cache_repo, repo_path, isbare = true) try @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"] finally close(repo) end end @testset "bare with remote callback" begin repo_path = joinpath(dir, "Example.Bare2") repo = LibGit2.clone(cache_repo, repo_path, isbare = true, remote_cb = LibGit2.mirror_cb()) try @test isdir(repo_path) @test LibGit2.path(repo) == LibGit2.posixpath(realpath(repo_path)) @test isfile(joinpath(repo_path, LibGit2.Consts.HEAD_FILE)) rmt = LibGit2.get(LibGit2.GitRemote, repo, "origin") try @test LibGit2.fetch_refspecs(rmt)[1] == "+refs/*:refs/*" @test LibGit2.isattached(repo) @test LibGit2.remotes(repo) == ["origin"] finally close(rmt) end finally close(repo) end end @testset "normal" begin repo = LibGit2.clone(cache_repo, test_repo) try @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))))" finally close(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) 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) 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 @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 cmt = LibGit2.GitCommit(repo, commit_oid1) try @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) short_str = sprint(show, short_oid1) @test short_str == "GitShortHash(\"$(string(short_oid1))\")" 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 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) finally close(cmt) end finally close(repo) close(repo_file) end end @testset "with branch" begin repo = LibGit2.GitRepo(cache_repo) try brnch = LibGit2.branch(repo) brref = LibGit2.head(repo) try @test LibGit2.isbranch(brref) @test !LibGit2.isremote(brref) @test LibGit2.name(brref) == "refs/heads/master" @test LibGit2.shortname(brref) == master_branch @test LibGit2.ishead(brref) @test isnull(LibGit2.upstream(brref)) 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 LibGit2.branch!(repo, test_branch, string(commit_oid1), set_head=false) @test isnull(LibGit2.lookup_branch(repo, test_branch, true)) tbref = Base.get(LibGit2.lookup_branch(repo, test_branch, false)) try @test LibGit2.shortname(tbref) == test_branch @test isnull(LibGit2.upstream(tbref)) finally close(tbref) end @test isnull(LibGit2.lookup_branch(repo, test_branch2, true)) LibGit2.branch!(repo, test_branch2; set_head=false) tbref = Base.get(LibGit2.lookup_branch(repo, test_branch2, false)) try @test LibGit2.shortname(tbref) == test_branch2 LibGit2.delete_branch(tbref) @test isnull(LibGit2.lookup_branch(repo, test_branch2, true)) finally close(tbref) end finally close(brref) end branches = map(b->LibGit2.shortname(b[1]), LibGit2.GitBranchIter(repo)) @test master_branch in branches @test test_branch in branches finally close(repo) end end @testset "with default configuration" begin repo = LibGit2.GitRepo(cache_repo) try 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 finally close(repo) end end @testset "with tags" begin repo = LibGit2.GitRepo(cache_repo) try tags = LibGit2.tag_list(repo) @test length(tags) == 0 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") @test isempty(LibGit2.fullname(tag1ref)) #because this is a reference to an OID 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" 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"] LibGit2.tag_delete(repo, tag1) tags = LibGit2.tag_list(repo) @test length(tags) == 1 @test tag2 ∈ tags @test tag1 ∉ tags finally close(repo) end end @testset "status" begin repo = LibGit2.GitRepo(cache_repo) try 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) finally close(repo) end end @testset "blobs" begin repo = LibGit2.GitRepo(cache_repo) try # 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) 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 finally close(repo) end end @testset "trees" begin repo = LibGit2.GitRepo(cache_repo) try @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 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) @test te_str == "GitTreeEntry:\nEntry name: testfile\nEntry type: Base.LibGit2.GitBlob\nEntry OID: $(LibGit2.entryid(tree_entry))\n" 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" finally close(repo) end end @testset "diff" begin repo = LibGit2.GitRepo(cache_repo) try @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}") 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 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:" 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) finally close(repo) end end end # TO DO: add more tests for various merge # preference options @testset "Fastforward merges" begin path = joinpath(dir, "Example.FF") repo = LibGit2.clone(cache_repo, path) # need to set this for merges to succeed cfg = LibGit2.GitConfig(repo) LibGit2.set!(cfg, "user.name", "AAAA") LibGit2.set!(cfg, "user.email", "BBBB@BBBB.COM") try oldhead = LibGit2.head_oid(repo) LibGit2.branch!(repo, "branch/ff_a") open(joinpath(LibGit2.path(repo),"ff_file1"),"w") do f write(f, "111\n") end LibGit2.add!(repo, "ff_file1") LibGit2.commit(repo, "add ff_file1") open(joinpath(LibGit2.path(repo),"ff_file2"),"w") do f write(f, "222\n") end LibGit2.add!(repo, "ff_file2") LibGit2.commit(repo, "add ff_file2") LibGit2.branch!(repo, "master") # switch back, now try to ff-merge the changes # from branch/a 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) oldhead = LibGit2.head_oid(repo) LibGit2.branch!(repo, "branch/ff_b") open(joinpath(LibGit2.path(repo),"ff_file3"),"w") do f write(f, "333\n") end LibGit2.add!(repo, "ff_file3") LibGit2.commit(repo, "add ff_file3") open(joinpath(LibGit2.path(repo),"ff_file4"),"w") do f write(f, "444\n") end LibGit2.add!(repo, "ff_file4") LibGit2.commit(repo, "add ff_file4") branchhead = LibGit2.head_oid(repo) 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) oldhead = LibGit2.head_oid(repo) LibGit2.branch!(repo, "branch/ff_c") open(joinpath(LibGit2.path(repo),"ff_file5"),"w") do f write(f, "555\n") end LibGit2.add!(repo, "ff_file5") LibGit2.commit(repo, "add ff_file5") open(joinpath(LibGit2.path(repo),"ff_file6"),"w") do f write(f, "666\n") end LibGit2.add!(repo, "ff_file6") LibGit2.commit(repo, "add ff_file6") branchhead = LibGit2.head_oid(repo) 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) finally close(repo) end end @testset "Merges" begin path = joinpath(dir, "Example.Merge") repo = LibGit2.clone(cache_repo, path) # need to set this for merges to succeed cfg = LibGit2.GitConfig(repo) LibGit2.set!(cfg, "user.name", "AAAA") LibGit2.set!(cfg, "user.email", "BBBB@BBBB.COM") try oldhead = LibGit2.head_oid(repo) LibGit2.branch!(repo, "branch/merge_a") open(joinpath(LibGit2.path(repo),"file1"),"w") do f write(f, "111\n") end LibGit2.add!(repo, "file1") LibGit2.commit(repo, "add file1") # 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." open(joinpath(LibGit2.path(repo), "file2"), "w") do f write(f, "222\n") end LibGit2.add!(repo, "file2") LibGit2.commit(repo, "add file2") upst_ann = LibGit2.GitAnnotated(repo, "branch/merge_a") head_ann = LibGit2.GitAnnotated(repo, "master") # (fail to) merge them because we can't fastforward @test_warn "WARNING: Cannot perform fast-forward merge." !LibGit2.merge!(repo, [upst_ann], true) # merge them now that we allow non-ff @test_warn "INFO: Review and commit merged changes." LibGit2.merge!(repo, [upst_ann], false) @test LibGit2.is_ancestor_of(string(oldhead), string(LibGit2.head_oid(repo)), repo) # go back to merge_a and rename a file LibGit2.branch!(repo, "branch/merge_b") mv(joinpath(LibGit2.path(repo),"file1"),joinpath(LibGit2.path(repo),"mvfile1")) LibGit2.add!(repo, "mvfile1") LibGit2.commit(repo, "move file1") LibGit2.branch!(repo, "master") upst_ann = LibGit2.GitAnnotated(repo, "branch/merge_b") rename_flag = 0 rename_flag = LibGit2.toggle(rename_flag, 0) # turns on the find renames opt mos = LibGit2.MergeOptions(flags=rename_flag) @test_warn "INFO: Review and commit merged changes." LibGit2.merge!(repo, [upst_ann], merge_opts=mos) finally close(repo) end end @testset "push" begin up_path = joinpath(dir, "Example.PushUp") up_repo = LibGit2.clone(cache_repo, up_path) our_path = joinpath(dir, "Example.Push") our_repo = LibGit2.clone(cache_repo, our_path) # need to set this for merges to succeed our_cfg = LibGit2.GitConfig(our_repo) LibGit2.set!(our_cfg, "user.name", "AAAA") LibGit2.set!(our_cfg, "user.email", "BBBB@BBBB.COM") up_cfg = LibGit2.GitConfig(up_repo) LibGit2.set!(up_cfg, "user.name", "AAAA") LibGit2.set!(up_cfg, "user.email", "BBBB@BBBB.COM") try open(joinpath(LibGit2.path(our_repo),"file1"),"w") do f write(f, "111\n") end LibGit2.add!(our_repo, "file1") LibGit2.commit(our_repo, "add file1") # we cannot yet locally push to non-bare repos @test_throws LibGit2.GitError LibGit2.push(our_repo, remoteurl=up_path) finally close(our_repo) close(up_repo) end end @testset "Fetch from cache repository" begin repo = LibGit2.GitRepo(test_repo) try # 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 finally close(repo) end end @testset "Examine test repository" begin @testset "files" begin @test readstring(joinpath(test_repo, test_file)) == readstring(joinpath(cache_repo, test_file)) end @testset "tags & branches" begin repo = LibGit2.GitRepo(test_repo) try # 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 tag2ref = LibGit2.GitReference(repo, "refs/tags/$tag2") try @test_throws LibGit2.Error.GitError LibGit2.upstream(tag2ref) finally close(tag2ref) end finally close(repo) end end @testset "commits with revwalk" begin repo = LibGit2.GitRepo(test_repo) cache = LibGit2.GitRepo(cache_repo) try 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_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 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 finally close(repo) close(cache) end end end @testset "Modify and reset repository" begin repo = LibGit2.GitRepo(test_repo) try # check index for file LibGit2.with(LibGit2.GitIndex(repo)) do idx i = find(test_file, idx) @test !isnull(i) idx_entry = idx[get(i)] @test idx_entry !== nothing idx_entry_str = sprint(show, idx_entry) @test idx_entry_str == "IndexEntry($(string(idx_entry.id)))" @test LibGit2.stage(idx_entry) == 0 i = find("zzz", idx) @test isnull(i) idx_str = sprint(show, idx) @test idx_str == "GitIndex:\nRepository: $(LibGit2.repository(idx))\nNumber of elements: 1\n" LibGit2.remove!(repo, test_file) LibGit2.read!(repo) @test count(idx) == 0 LibGit2.add!(repo, test_file) LibGit2.update!(repo, test_file) @test count(idx) == 1 end # check non-existent file status st = LibGit2.status(repo, "XYZ") @test isnull(st) # check file status st = LibGit2.status(repo, test_file) @test !isnull(st) @test LibGit2.isset(get(st), LibGit2.Consts.STATUS_CURRENT) # modify file open(joinpath(test_repo, test_file), "a") do io write(io, 0x41) end # file modified but not staged st_mod = LibGit2.status(repo, test_file) @test !LibGit2.isset(get(st_mod), LibGit2.Consts.STATUS_INDEX_MODIFIED) @test LibGit2.isset(get(st_mod), LibGit2.Consts.STATUS_WT_MODIFIED) # stage file LibGit2.add!(repo, test_file) # modified file staged st_stg = LibGit2.status(repo, test_file) @test LibGit2.isset(get(st_stg), LibGit2.Consts.STATUS_INDEX_MODIFIED) @test !LibGit2.isset(get(st_stg), LibGit2.Consts.STATUS_WT_MODIFIED) # try to unstage to unknown commit @test_throws LibGit2.Error.GitError LibGit2.reset!(repo, "XYZ", test_file) # status should not change st_new = LibGit2.status(repo, test_file) @test get(st_new) == get(st_stg) # try to unstage to HEAD new_head = LibGit2.reset!(repo, LibGit2.Consts.HEAD_FILE, test_file) st_uns = LibGit2.status(repo, test_file) @test get(st_uns) == get(st_mod) # reset repo @test_throws LibGit2.Error.GitError LibGit2.reset!(repo, LibGit2.GitHash(), LibGit2.Consts.RESET_HARD) new_head = LibGit2.reset!(repo, LibGit2.head_oid(repo), LibGit2.Consts.RESET_HARD) open(joinpath(test_repo, test_file), "r") do io @test read(io)[end] != 0x41 end finally close(repo) end end @testset "rebase" begin repo = LibGit2.GitRepo(test_repo) try LibGit2.branch!(repo, "branch/a") oldhead = LibGit2.head_oid(repo) open(joinpath(LibGit2.path(repo),"file1"),"w") do f write(f, "111\n") end LibGit2.add!(repo, "file1") LibGit2.commit(repo, "add file1") open(joinpath(LibGit2.path(repo),"file2"),"w") do f write(f, "222\n") end LibGit2.add!(repo, "file2") LibGit2.commit(repo, "add file2") 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 open(joinpath(LibGit2.path(repo),"file3"),"w") do f write(f, "333\n") end LibGit2.add!(repo, "file3") LibGit2.commit(repo, "add file3") newhead = LibGit2.head_oid(repo) files = LibGit2.diff_files(repo, "branch/a", "branch/b", filter=Set([LibGit2.Consts.DELTA_ADDED])) @test files == ["file3"] files = LibGit2.diff_files(repo, "branch/a", "branch/b", filter=Set([LibGit2.Consts.DELTA_MODIFIED])) @test files == [] # switch back and rebase LibGit2.branch!(repo, "branch/a") newnewhead = LibGit2.rebase!(repo, "branch/b") # issue #19624 @test newnewhead == newhead # add yet another file open(joinpath(LibGit2.path(repo),"file4"),"w") do f write(f, "444\n") end LibGit2.add!(repo, "file4") LibGit2.commit(repo, "add file4") # rebase with onto newhead = LibGit2.rebase!(repo, "branch/a", "master") newerhead = LibGit2.head_oid(repo) @test newerhead == newhead # add yet more files open(joinpath(LibGit2.path(repo),"file5"),"w") do f write(f, "555\n") end LibGit2.add!(repo, "file5") LibGit2.commit(repo, "add file5") open(joinpath(LibGit2.path(repo),"file6"),"w") do f write(f, "666\n") end LibGit2.add!(repo, "file6") LibGit2.commit(repo, "add file6") pre_abort_head = LibGit2.head_oid(repo) # 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 finally close(repo) end end @testset "merge" begin path = joinpath(dir, "Example.simple_merge") repo = LibGit2.clone(cache_repo, path) cfg = LibGit2.GitConfig(repo) LibGit2.set!(cfg, "user.name", "AAAA") LibGit2.set!(cfg, "user.email", "BBBB@BBBB.COM") try LibGit2.branch!(repo, "branch/merge_a") a_head = LibGit2.head_oid(repo) open(joinpath(LibGit2.path(repo),"merge_file1"),"w") do f write(f, "111\n") end LibGit2.add!(repo, "merge_file1") LibGit2.commit(repo, "add merge_file1") LibGit2.branch!(repo, "master") a_head_ann = LibGit2.GitAnnotated(repo, "branch/merge_a") @test_warn "INFO: Review and commit merged changes." LibGit2.merge!(repo, [a_head_ann]) #merge returns true if successful finally close(repo) end end @testset "Transact test repository" begin repo = LibGit2.GitRepo(test_repo) try 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)) finally close(repo) end end @testset "checkout/headname" begin repo = LibGit2.GitRepo(cache_repo) try LibGit2.checkout!(repo, string(commit_oid1)) @test !LibGit2.isattached(repo) @test LibGit2.headname(repo) == "(detached from $(string(commit_oid1)[1:7]))" finally close(repo) end end if is_unix() @testset "checkout/proptest" begin repo = LibGit2.GitRepo(test_repo) try 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) finally close(repo) end end end @testset "Credentials" begin creds_user = "USER" creds_pass = "PASS" creds = LibGit2.UserPasswordCredentials(creds_user, creds_pass) @test !LibGit2.checkused!(creds) @test !LibGit2.checkused!(creds) @test !LibGit2.checkused!(creds) @test LibGit2.checkused!(creds) @test creds.user == creds_user @test creds.pass == creds_pass sshcreds = LibGit2.SSHCredentials(creds_user, creds_pass) @test sshcreds.user == creds_user @test sshcreds.pass == creds_pass @test isempty(sshcreds.prvkey) @test isempty(sshcreds.pubkey) 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 is_unix() @testset "SSH credential prompt" begin url = "git@github.com/test/package.jl" key_dir = joinpath(dirname(@__FILE__), "libgit2") valid_key = joinpath(key_dir, "valid") invalid_key = joinpath(key_dir, "invalid") valid_p_key = joinpath(key_dir, "valid-passphrase") passphrase = "secret" ssh_cmd = """ include("$LIBGIT2_HELPER_PATH") valid_cred = LibGit2.SSHCredentials("git", "", "$valid_key", "$valid_key.pub") err, auth_attempts = credential_loop(valid_cred, "$url", "git") (err < 0 ? LibGit2.GitError(err) : err, auth_attempts) """ ssh_p_cmd = """ include("$LIBGIT2_HELPER_PATH") valid_cred = LibGit2.SSHCredentials("git", "$passphrase", "$valid_p_key", "$valid_p_key.pub") err, auth_attempts = credential_loop(valid_cred, "$url", "git") (err < 0 ? LibGit2.GitError(err) : err, auth_attempts) """ # 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. # Default credentials are valid withenv("SSH_KEY_PATH" => valid_key) do err, auth_attempts = challenge_prompt(ssh_cmd, []) @test err == 0 @test auth_attempts == 1 end # Default credentials are valid but requires a passphrase withenv("SSH_KEY_PATH" => valid_p_key) do challenges = [ "Passphrase for $valid_p_key:" => "$passphrase\n", ] err, auth_attempts = challenge_prompt(ssh_p_cmd, challenges) @test err == 0 @test auth_attempts == 1 # User mistypes passphrase. # Note: In reality LibGit2 will raise an error upon using the invalid SSH # credentials. Since we don't control the internals of LibGit2 though they # could also just re-call the credential callback like they do for HTTP. challenges = [ "Passphrase for $valid_p_key:" => "foo\n", # "Private key location for 'git@github.com' [$valid_p_key]:" => "\n", "Passphrase for $valid_p_key:" => "$passphrase\n", ] err, auth_attempts = challenge_prompt(ssh_p_cmd, challenges) @test err == 0 @test auth_attempts == 5 end withenv("SSH_KEY_PATH" => valid_p_key, "SSH_KEY_PASS" => passphrase) do err, auth_attempts = challenge_prompt(ssh_p_cmd, []) @test err == 0 @test auth_attempts == 1 end # TODO: Tests are currently broken. Credential callback prompts for: # "Passphrase for :" #= # Explicitly setting these env variables to be empty means the user will be # given a prompt with no defaults set. withenv("SSH_KEY_PATH" => "", "SSH_PUB_KEY_PATH" => "") do # User provides valid credentials challenges = [ "Private key location for 'git@github.com':" => "$valid_key\n", ] err, auth_attempts = challenge_prompt(ssh_cmd, challenges) @test err == 0 @test auth_attempts == 2 # User provides valid credentials that requires a passphrase challenges = [ "Private key location for 'git@github.com':" => "$valid_p_key\n", "Passphrase for $valid_p_key:" => "$passphrase\n", ] err, auth_attempts = challenge_prompt(ssh_p_cmd, challenges) @test err == 0 @test auth_attempts == 2 end =# # TODO: Tests are currently broken. Credential callback currently infinite loops # and never prompts user to change private keys. #= # Explicitly setting these env variables to an existing but invalid key pair # means the user will be given a prompt with that defaults to the given values. withenv("SSH_KEY_PATH" => invalid_key, "SSH_PUB_KEY_PATH" => invalid_key * ".pub") do challenges = [ "Private key location for 'git@github.com' [$invalid_key]:" => "$valid_key\n", ] err, auth_attempts = challenge_prompt(ssh_cmd, challenges) @test err == 0 @test auth_attempts == 2 end =# # TODO: Tests are currently broken. Credential callback currently infinite loops # and never prompts user to change private keys. #= withenv("SSH_KEY_PATH" => valid_key, "SSH_PUB_KEY_PATH" => valid_key * ".public") do @test !isfile(ENV["SSH_PUB_KEY_PATH"]) # User explicitly sets the SSH_PUB_KEY_PATH incorrectly. challenges = [ "Private key location for 'git@github.com' [$valid_key]:" => "\n" "Public key location for 'git@github.com':" => "$valid_key.pub\n" ] err, auth_attempts = challenge_prompt(ssh_cmd, challenges) @test err == 0 @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) https_cmd = """ include("$LIBGIT2_HELPER_PATH") valid_cred = LibGit2.UserPasswordCredentials("$valid_username", "$valid_password") err, auth_attempts = credential_loop(valid_cred, "$url") (err < 0 ? LibGit2.GitError(err) : err, auth_attempts) """ # User provides a valid username and password challenges = [ "Username for 'https://github.com':" => "$valid_username\n", "Password for 'https://$valid_username@github.com':" => "$valid_password\n", ] err, auth_attempts = challenge_prompt(https_cmd, challenges) @test err == 0 @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]:" => "$valid_username\n", "Password for 'https://$valid_username@github.com':" => "$valid_password\n", ] err, auth_attempts = challenge_prompt(https_cmd, challenges) @test err == 0 @test auth_attempts == 5 end end #= temporarily disabled until working on the buildbots, ref https://github.com/JuliaLang/julia/pull/17651#issuecomment-238211150 @testset "SSH" begin sshd_command = "" ssh_repo = joinpath(dir, "Example.SSH") if !is_windows() try # SSHD needs to be executed by its full absolute path sshd_command = strip(readstring(`which sshd`)) catch warn("Skipping SSH tests (Are `which` and `sshd` installed?)") end end if !isempty(sshd_command) mktempdir() do fakehomedir mkdir(joinpath(fakehomedir,".ssh")) # Unsetting the SSH agent serves two purposes. First, we make # sure that we don't accidentally pick up an existing agent, # and second we test that we fall back to using a key file # if the agent isn't present. withenv("HOME"=>fakehomedir,"SSH_AUTH_SOCK"=>nothing) do # Generate user file, first an unencrypted one wait(spawn(`ssh-keygen -N "" -C juliatest@localhost -f $fakehomedir/.ssh/id_rsa`)) # Generate host keys wait(spawn(`ssh-keygen -f $fakehomedir/ssh_host_rsa_key -N '' -t rsa`)) wait(spawn(`ssh-keygen -f $fakehomedir/ssh_host_dsa_key -N '' -t dsa`)) our_ssh_port = rand(13000:14000) # Chosen arbitrarily key_option = "AuthorizedKeysFile $fakehomedir/.ssh/id_rsa.pub" pidfile_option = "PidFile $fakehomedir/sshd.pid" sshp = agentp = nothing logfile = tempname() ssh_debug = false function spawn_sshd() debug_flags = ssh_debug ? `-d -d` : `` _p = open(logfile, "a") do logfilestream spawn(pipeline(pipeline(`$sshd_command -e -f /dev/null $debug_flags -h $fakehomedir/ssh_host_rsa_key -h $fakehomedir/ssh_host_dsa_key -p $our_ssh_port -o $pidfile_option -o 'Protocol 2' -o $key_option -o 'UsePrivilegeSeparation no' -o 'StrictModes no'`,STDOUT),stderr=logfilestream)) end # Give the SSH server 5 seconds to start up yield(); sleep(5) _p end sshp = spawn_sshd() TIOCSCTTY_str = "ccall(:ioctl, Void, (Cint, Cint, Int64), 0, (is_bsd() || is_apple()) ? 0x20007461 : is_linux() ? 0x540E : error(\"Fill in TIOCSCTTY for this OS here\"), 0)" # To fail rather than hang function killer_task(p, master) @async begin sleep(10) kill(p) if isopen(master) nb_available(master) > 0 && write(logfile, readavailable(master)) close(master) end end end try function try_clone(challenges = []) cmd = """ repo = nothing try $TIOCSCTTY_str reponame = "ssh://$(ENV["USER"])@localhost:$our_ssh_port$cache_repo" repo = LibGit2.clone(reponame, "$ssh_repo") catch err open("$logfile","a") do f println(f,"HOME: ",ENV["HOME"]) println(f, err) end finally close(repo) end """ # We try to be helpful by desperately looking for # a way to prompt the password interactively. Pretend # to be a TTY to suppress those shenanigans. Further, we # need to detach and change the controlling terminal with # TIOCSCTTY, since getpass opens the controlling terminal TestHelpers.with_fake_pty() do slave, master err = Base.Pipe() let p = spawn(detach( `$(Base.julia_cmd()) --startup-file=no -e $cmd`),slave,slave,STDERR) killer_task(p, master) for (challenge, response) in challenges readuntil(master, challenge) sleep(1) print(master, response) end sleep(2) wait(p) close(master) end end @test isfile(joinpath(ssh_repo,"testfile")) rm(ssh_repo, recursive = true) end # Should use the default files, no interaction required. try_clone() ssh_debug && (kill(sshp); sshp = spawn_sshd()) # Ok, now encrypt the file and test with that (this also # makes sure that we don't accidentally fall back to the # unencrypted version) wait(spawn(`ssh-keygen -p -N "xxxxx" -f $fakehomedir/.ssh/id_rsa`)) # Try with the encrypted file. Needs a password. try_clone(["Passphrase"=>"xxxxx\r\n"]) ssh_debug && (kill(sshp); sshp = spawn_sshd()) # Move the file. It should now ask for the location and # then the passphrase mv("$fakehomedir/.ssh/id_rsa","$fakehomedir/.ssh/id_rsa2") cp("$fakehomedir/.ssh/id_rsa.pub","$fakehomedir/.ssh/id_rsa2.pub") try_clone(["location"=>"$fakehomedir/.ssh/id_rsa2\n", "Passphrase"=>"xxxxx\n"]) mv("$fakehomedir/.ssh/id_rsa2","$fakehomedir/.ssh/id_rsa") rm("$fakehomedir/.ssh/id_rsa2.pub") # Ok, now start an agent agent_sock = tempname() agentp = spawn(`ssh-agent -a $agent_sock -d`) while stat(agent_sock).mode == 0 # Wait until the agent is started sleep(1) end # fake pty is required for the same reason as in try_clone # above withenv("SSH_AUTH_SOCK" => agent_sock) do TestHelpers.with_fake_pty() do slave, master cmd = """ $TIOCSCTTY_str run(pipeline(`ssh-add $fakehomedir/.ssh/id_rsa`, stderr = DevNull)) """ addp = spawn(detach(`$(Base.julia_cmd()) --startup-file=no -e $cmd`), slave, slave, STDERR) killer_task(addp, master) sleep(2) write(master, "xxxxx\n") wait(addp) end # Should now use the agent try_clone() end catch err println("SSHD logfile contents follows:") println(readstring(logfile)) rethrow(err) finally rm(logfile) sshp !== nothing && kill(sshp) agentp !== nothing && kill(agentp) end end end 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 is_linux() try # OpenSSL needs to be on the path openssl_installed = !isempty(readstring(`openssl version`)) catch warn("Skipping hostname verification tests. Is `openssl` on the path?") end # Find a hostname that maps to the loopback address hostnames = ["localhost"] # In minimal environments a hostname might not be available (issue #20758) try # In some environments, namely Macs, the hostname "macbook.local" is bound # to the external address while "macbook" is bound to the loopback address. unshift!(hostnames, replace(gethostname(), r"\..*$", "")) end loopback = ip"127.0.0.1" for hostname in hostnames 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 -nodes -keyout $key -out $cert -days 1 -subj "/CN=$common_name"`, stderr=DevNull)) run(`openssl x509 -in $cert -out $pem -outform PEM`) # Make a fake Julia package and minimal HTTPS server with our generated # certificate. The minimal server can't actually serve a Git repository. mkdir(joinpath(root, "Example.jl")) pobj = cd(root) do spawn(`openssl s_server -key $key -cert $cert -WWW`) end errfile = joinpath(root, "error") repo_url = "https://$common_name:4433/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 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 err.msg == "Invalid Content-Type: text/plain" end finally kill(pobj) end end end end end