950 lines
31 KiB
Julia
950 lines
31 KiB
Julia
# This file is a part of Julia. License is MIT: https://julialang.org/license
|
|
|
|
module LibGit2
|
|
|
|
import Base: merge!, ==
|
|
|
|
export with, GitRepo, GitConfig
|
|
|
|
const GITHUB_REGEX =
|
|
r"^(?:git@|git://|https://(?:[\w\.\+\-]+@)?)github.com[:/](([^/].+)/(.+?))(?:\.git)?$"i
|
|
|
|
const REFCOUNT = Threads.Atomic{UInt}()
|
|
|
|
include("utils.jl")
|
|
include("consts.jl")
|
|
include("types.jl")
|
|
include("error.jl")
|
|
include("signature.jl")
|
|
include("oid.jl")
|
|
include("reference.jl")
|
|
include("commit.jl")
|
|
include("repository.jl")
|
|
include("config.jl")
|
|
include("walker.jl")
|
|
include("remote.jl")
|
|
include("strarray.jl")
|
|
include("index.jl")
|
|
include("merge.jl")
|
|
include("tag.jl")
|
|
include("blob.jl")
|
|
include("diff.jl")
|
|
include("rebase.jl")
|
|
include("status.jl")
|
|
include("tree.jl")
|
|
include("callbacks.jl")
|
|
|
|
using .Error
|
|
|
|
struct State
|
|
head::GitHash
|
|
index::GitHash
|
|
work::GitHash
|
|
end
|
|
|
|
"""
|
|
head(pkg::AbstractString) -> String
|
|
|
|
Return current HEAD [`GitHash`](@ref) of
|
|
the `pkg` repo as a string.
|
|
"""
|
|
function head(pkg::AbstractString)
|
|
with(GitRepo, pkg) do repo
|
|
string(head_oid(repo))
|
|
end
|
|
end
|
|
|
|
"""
|
|
need_update(repo::GitRepo)
|
|
|
|
Equivalent to `git update-index`. Returns `true`
|
|
if `repo` needs updating.
|
|
"""
|
|
function need_update(repo::GitRepo)
|
|
if !isbare(repo)
|
|
# read updates index from filesystem
|
|
read!(repo, true)
|
|
end
|
|
end
|
|
|
|
"""
|
|
iscommit(id::AbstractString, repo::GitRepo) -> Bool
|
|
|
|
Checks if commit `id` (which is a [`GitHash`](@ref) in string form)
|
|
is in the repository.
|
|
|
|
# Example
|
|
|
|
```julia-repl
|
|
julia> repo = LibGit2.GitRepo(repo_path);
|
|
|
|
julia> LibGit2.add!(repo, test_file);
|
|
|
|
julia> commit_oid = LibGit2.commit(repo, "add test_file");
|
|
|
|
julia> LibGit2.iscommit(string(commit_oid), repo)
|
|
true
|
|
```
|
|
"""
|
|
function iscommit(id::AbstractString, repo::GitRepo)
|
|
res = true
|
|
try
|
|
c = GitCommit(repo, id)
|
|
if c === nothing
|
|
res = false
|
|
else
|
|
close(c)
|
|
end
|
|
catch
|
|
res = false
|
|
end
|
|
return res
|
|
end
|
|
|
|
"""
|
|
LibGit2.isdirty(repo::GitRepo, pathspecs::AbstractString=""; cached::Bool=false) -> Bool
|
|
|
|
Checks if there have been any changes to tracked files in the working tree (if
|
|
`cached=false`) or the index (if `cached=true`).
|
|
`pathspecs` are the specifications for options for the diff.
|
|
|
|
# Example
|
|
```julia
|
|
repo = LibGit2.GitRepo(repo_path)
|
|
LibGit2.isdirty(repo) # should be false
|
|
open(joinpath(repo_path, new_file), "a") do f
|
|
println(f, "here's my cool new file")
|
|
end
|
|
LibGit2.isdirty(repo) # now true
|
|
LibGit2.isdirty(repo, new_file) # now true
|
|
```
|
|
|
|
Equivalent to `git diff-index HEAD [-- <pathspecs>]`.
|
|
"""
|
|
isdirty(repo::GitRepo, paths::AbstractString=""; cached::Bool=false) =
|
|
isdiff(repo, Consts.HEAD_FILE, paths, cached=cached)
|
|
|
|
"""
|
|
LibGit2.isdiff(repo::GitRepo, treeish::AbstractString, pathspecs::AbstractString=""; cached::Bool=false)
|
|
|
|
Checks if there are any differences between the tree specified by `treeish` and the
|
|
tracked files in the working tree (if `cached=false`) or the index (if `cached=true`).
|
|
`pathspecs` are the specifications for options for the diff.
|
|
|
|
# Example
|
|
```julia
|
|
repo = LibGit2.GitRepo(repo_path)
|
|
LibGit2.isdiff(repo, "HEAD") # should be false
|
|
open(joinpath(repo_path, new_file), "a") do f
|
|
println(f, "here's my cool new file")
|
|
end
|
|
LibGit2.isdiff(repo, "HEAD") # now true
|
|
```
|
|
|
|
Equivalent to `git diff-index <treeish> [-- <pathspecs>]`.
|
|
"""
|
|
function isdiff(repo::GitRepo, treeish::AbstractString, paths::AbstractString=""; cached::Bool=false)
|
|
tree = GitTree(repo, "$treeish^{tree}")
|
|
try
|
|
diff = diff_tree(repo, tree, paths, cached=cached)
|
|
result = count(diff) > 0
|
|
close(diff)
|
|
return result
|
|
finally
|
|
close(tree)
|
|
end
|
|
end
|
|
|
|
"""
|
|
diff_files(repo::GitRepo, branch1::AbstractString, branch2::AbstractString; kwarg...) -> Vector{AbstractString}
|
|
|
|
Show which files have changed in the git repository `repo` between branches `branch1`
|
|
and `branch2`.
|
|
|
|
The keyword argument is:
|
|
* `filter::Set{Consts.DELTA_STATUS}=Set([Consts.DELTA_ADDED, Consts.DELTA_MODIFIED, Consts.DELTA_DELETED]))`,
|
|
and it sets options for the diff. The default is to show files added, modified, or deleted.
|
|
|
|
Returns only the *names* of the files which have changed, *not* their contents.
|
|
|
|
# Example
|
|
|
|
```julia
|
|
LibGit2.branch!(repo, "branch/a")
|
|
LibGit2.branch!(repo, "branch/b")
|
|
# add a file to repo
|
|
open(joinpath(LibGit2.path(repo),"file"),"w") do f
|
|
write(f, "hello repo\n")
|
|
end
|
|
LibGit2.add!(repo, "file")
|
|
LibGit2.commit(repo, "add file")
|
|
# returns ["file"]
|
|
filt = Set([LibGit2.Consts.DELTA_ADDED])
|
|
files = LibGit2.diff_files(repo, "branch/a", "branch/b", filter=filt)
|
|
# returns [] because existing files weren't modified
|
|
filt = Set([LibGit2.Consts.DELTA_MODIFIED])
|
|
files = LibGit2.diff_files(repo, "branch/a", "branch/b", filter=filt)
|
|
```
|
|
|
|
Equivalent to `git diff --name-only --diff-filter=<filter> <branch1> <branch2>`.
|
|
"""
|
|
function diff_files(repo::GitRepo, branch1::AbstractString, branch2::AbstractString;
|
|
filter::Set{Consts.DELTA_STATUS}=Set([Consts.DELTA_ADDED, Consts.DELTA_MODIFIED, Consts.DELTA_DELETED]))
|
|
b1_id = revparseid(repo, branch1*"^{tree}")
|
|
b2_id = revparseid(repo, branch2*"^{tree}")
|
|
tree1 = GitTree(repo, b1_id)
|
|
tree2 = GitTree(repo, b2_id)
|
|
files = AbstractString[]
|
|
try
|
|
diff = diff_tree(repo, tree1, tree2)
|
|
for i in 1:count(diff)
|
|
delta = diff[i]
|
|
delta === nothing && break
|
|
if Consts.DELTA_STATUS(delta.status) in filter
|
|
push!(files, unsafe_string(delta.new_file.path))
|
|
end
|
|
end
|
|
close(diff)
|
|
finally
|
|
close(tree1)
|
|
close(tree2)
|
|
end
|
|
return files
|
|
end
|
|
|
|
"""
|
|
is_ancestor_of(a::AbstractString, b::AbstractString, repo::GitRepo) -> Bool
|
|
|
|
Returns `true` if `a`, a [`GitHash`](@ref) in string form, is an ancestor of
|
|
`b`, a [`GitHash`](@ref) in string form.
|
|
|
|
# Example
|
|
|
|
```julia-repl
|
|
julia> repo = LibGit2.GitRepo(repo_path);
|
|
|
|
julia> LibGit2.add!(repo, test_file1);
|
|
|
|
julia> commit_oid1 = LibGit2.commit(repo, "commit1");
|
|
|
|
julia> LibGit2.add!(repo, test_file2);
|
|
|
|
julia> commit_oid2 = LibGit2.commit(repo, "commit2");
|
|
|
|
julia> LibGit2.is_ancestor_of(string(commit_oid1), string(commit_oid2), repo)
|
|
true
|
|
```
|
|
"""
|
|
function is_ancestor_of(a::AbstractString, b::AbstractString, repo::GitRepo)
|
|
A = revparseid(repo, a)
|
|
merge_base(repo, a, b) == A
|
|
end
|
|
|
|
"""
|
|
set_remote_url(repo::GitRepo, url::AbstractString; remote::AbstractString="origin")
|
|
|
|
Set the `url` for `remote` for the git repository `repo`.
|
|
The default name of the remote is `"origin"`.
|
|
|
|
# Examples
|
|
|
|
```julia
|
|
repo_path = joinpath("test_directory", "Example")
|
|
repo = LibGit2.init(repo_path)
|
|
url1 = "https://github.com/JuliaLang/Example.jl"
|
|
LibGit2.set_remote_url(repo, url1, remote="upstream")
|
|
url2 = "https://github.com/JuliaLang/Example2.jl"
|
|
LibGit2.set_remote_url(repo_path, url2, remote="upstream2")
|
|
```
|
|
"""
|
|
function set_remote_url(repo::GitRepo, url::AbstractString; remote::AbstractString="origin")
|
|
with(GitConfig, repo) do cfg
|
|
set!(cfg, "remote.$remote.url", url)
|
|
set!(cfg, "remote.$remote.pushurl", url)
|
|
end
|
|
end
|
|
|
|
"""
|
|
set_remote_url(path::AbstractString, url::AbstractString; remote::AbstractString="origin")
|
|
|
|
Set the `url` for `remote` for the git repository located at `path`.
|
|
The default name of the remote is `"origin"`.
|
|
"""
|
|
function set_remote_url(path::AbstractString, url::AbstractString; remote::AbstractString="origin")
|
|
with(GitRepo, path) do repo
|
|
set_remote_url(repo, url, remote=remote)
|
|
end
|
|
end
|
|
|
|
function make_payload(payload::Nullable{<:AbstractCredentials})
|
|
Ref{Nullable{AbstractCredentials}}(payload)
|
|
end
|
|
|
|
"""
|
|
fetch(repo::GitRepo; kwargs...)
|
|
|
|
Fetches updates from an upstream of the repository `repo`.
|
|
|
|
The keyword arguments are:
|
|
* `remote::AbstractString="origin"`: which remote, specified by name,
|
|
of `repo` to fetch from. If this is empty, the URL will be used to
|
|
construct an anonymous remote.
|
|
* `remoteurl::AbstractString=""`: the URL of `remote`. If not specified,
|
|
will be assumed based on the given name of `remote`.
|
|
* `refspecs=AbstractString[]`: determines properties of the fetch.
|
|
* `payload=Nullable{AbstractCredentials}()`: provides credentials, if necessary,
|
|
for instance if `remote` is a private repository.
|
|
|
|
Equivalent to `git fetch [<remoteurl>|<repo>] [<refspecs>]`.
|
|
"""
|
|
function fetch(repo::GitRepo; remote::AbstractString="origin",
|
|
remoteurl::AbstractString="",
|
|
refspecs::Vector{<:AbstractString}=AbstractString[],
|
|
payload::Nullable{<:AbstractCredentials}=Nullable{AbstractCredentials}())
|
|
rmt = if isempty(remoteurl)
|
|
get(GitRemote, repo, remote)
|
|
else
|
|
GitRemoteAnon(repo, remoteurl)
|
|
end
|
|
try
|
|
payload = make_payload(payload)
|
|
fo = FetchOptions(callbacks=RemoteCallbacks(credentials_cb(), payload))
|
|
fetch(rmt, refspecs, msg="from $(url(rmt))", options = fo)
|
|
finally
|
|
close(rmt)
|
|
end
|
|
end
|
|
|
|
"""
|
|
push(repo::GitRepo; kwargs...)
|
|
|
|
Pushes updates to an upstream of `repo`.
|
|
|
|
The keyword arguments are:
|
|
* `remote::AbstractString="origin"`: the name of the upstream remote to push to.
|
|
* `remoteurl::AbstractString=""`: the URL of `remote`.
|
|
* `refspecs=AbstractString[]`: determines properties of the push.
|
|
* `force::Bool=false`: determines if the push will be a force push,
|
|
overwriting the remote branch.
|
|
* `payload=Nullable{AbstractCredentials}()`: provides credentials, if necessary,
|
|
for instance if `remote` is a private repository.
|
|
|
|
Equivalent to `git push [<remoteurl>|<repo>] [<refspecs>]`.
|
|
"""
|
|
function push(repo::GitRepo; remote::AbstractString="origin",
|
|
remoteurl::AbstractString="",
|
|
refspecs::Vector{<:AbstractString}=AbstractString[],
|
|
force::Bool=false,
|
|
payload::Nullable{<:AbstractCredentials}=Nullable{AbstractCredentials}())
|
|
rmt = if isempty(remoteurl)
|
|
get(GitRemote, repo, remote)
|
|
else
|
|
GitRemoteAnon(repo, remoteurl)
|
|
end
|
|
try
|
|
payload = make_payload(payload)
|
|
push_opts=PushOptions(callbacks=RemoteCallbacks(credentials_cb(), payload))
|
|
push(rmt, refspecs, force=force, options=push_opts)
|
|
finally
|
|
close(rmt)
|
|
end
|
|
end
|
|
|
|
"""
|
|
branch(repo::GitRepo)
|
|
|
|
Equivalent to `git branch`.
|
|
Create a new branch from the current HEAD.
|
|
"""
|
|
function branch(repo::GitRepo)
|
|
head_ref = head(repo)
|
|
try
|
|
branch(head_ref)
|
|
finally
|
|
close(head_ref)
|
|
end
|
|
end
|
|
|
|
"""
|
|
branch!(repo::GitRepo, branch_name::AbstractString, commit::AbstractString=""; kwargs...)
|
|
|
|
Checkout a new git branch in the `repo` repository. `commit` is the [`GitHash`](@ref),
|
|
in string form, which will be the start of the new branch.
|
|
If `commit` is an empty string, the current HEAD will be used.
|
|
|
|
The keyword arguments are:
|
|
* `track::AbstractString=""`: the name of the
|
|
remote branch this new branch should track, if any.
|
|
If empty (the default), no remote branch
|
|
will be tracked.
|
|
* `force::Bool=false`: if `true`, branch creation will
|
|
be forced.
|
|
* `set_head::Bool=true`: if `true`, after the branch creation
|
|
finishes the branch head will be set as the HEAD of `repo`.
|
|
|
|
Equivalent to `git checkout [-b|-B] <branch_name> [<commit>] [--track <track>]`.
|
|
|
|
# Example
|
|
|
|
```julia
|
|
repo = LibGit2.GitRepo(repo_path)
|
|
LibGit2.branch!(repo, "new_branch", set_head=false)
|
|
```
|
|
"""
|
|
function branch!(repo::GitRepo, branch_name::AbstractString,
|
|
commit::AbstractString = ""; # start point
|
|
track::AbstractString = "", # track remote branch
|
|
force::Bool=false, # force branch creation
|
|
set_head::Bool=true) # set as head reference on exit
|
|
# try to lookup branch first
|
|
branch_ref = force ? Nullable{GitReference}() : lookup_branch(repo, branch_name)
|
|
if isnull(branch_ref)
|
|
branch_rmt_ref = isempty(track) ? Nullable{GitReference}() : lookup_branch(repo, "$track/$branch_name", true)
|
|
# if commit is empty get head commit oid
|
|
commit_id = if isempty(commit)
|
|
if isnull(branch_rmt_ref)
|
|
with(head(repo)) do head_ref
|
|
with(peel(GitCommit, head_ref)) do hrc
|
|
GitHash(hrc)
|
|
end
|
|
end
|
|
else
|
|
tmpcmt = with(peel(GitCommit, Base.get(branch_rmt_ref))) do hrc
|
|
GitHash(hrc)
|
|
end
|
|
close(Base.get(branch_rmt_ref))
|
|
tmpcmt
|
|
end
|
|
else
|
|
GitHash(commit)
|
|
end
|
|
iszero(commit_id) && return
|
|
cmt = GitCommit(repo, commit_id)
|
|
new_branch_ref = nothing
|
|
try
|
|
new_branch_ref = Nullable(create_branch(repo, branch_name, cmt, force=force))
|
|
finally
|
|
close(cmt)
|
|
isnull(new_branch_ref) && throw(GitError(Error.Object, Error.ERROR, "cannot create branch `$branch_name` with `$commit_id`"))
|
|
branch_ref = new_branch_ref
|
|
end
|
|
end
|
|
try
|
|
#TODO: what if branch tracks other then "origin" remote
|
|
if !isempty(track) # setup tracking
|
|
try
|
|
with(GitConfig, repo) do cfg
|
|
set!(cfg, "branch.$branch_name.remote", Consts.REMOTE_ORIGIN)
|
|
set!(cfg, "branch.$branch_name.merge", name(Base.get(branch_ref)))
|
|
end
|
|
catch
|
|
warn("Please provide remote tracking for branch '$branch_name' in '$(path(repo))'")
|
|
end
|
|
end
|
|
|
|
if set_head
|
|
# checkout selected branch
|
|
with(peel(GitTree, Base.get(branch_ref))) do btree
|
|
checkout_tree(repo, btree)
|
|
end
|
|
|
|
# switch head to the branch
|
|
head!(repo, Base.get(branch_ref))
|
|
end
|
|
finally
|
|
close(Base.get(branch_ref))
|
|
end
|
|
return
|
|
end
|
|
|
|
"""
|
|
checkout!(repo::GitRepo, commit::AbstractString=""; force::Bool=true)
|
|
|
|
Equivalent to `git checkout [-f] --detach <commit>`.
|
|
Checkout the git commit `commit` (a [`GitHash`](@ref) in string form)
|
|
in `repo`. If `force` is `true`, force the checkout and discard any
|
|
current changes. Note that this detaches the current HEAD.
|
|
|
|
# Example
|
|
|
|
```julia
|
|
repo = LibGit2.init(repo_path)
|
|
open(joinpath(LibGit2.path(repo), "file1"), "w") do f
|
|
write(f, "111\n")
|
|
end
|
|
LibGit2.add!(repo, "file1")
|
|
commit_oid = LibGit2.commit(repo, "add file1")
|
|
open(joinpath(LibGit2.path(repo), "file1"), "w") do f
|
|
write(f, "112\n")
|
|
end
|
|
# would fail without the force=true
|
|
# since there are modifications to the file
|
|
LibGit2.checkout!(repo, string(commit_oid), force=true)
|
|
```
|
|
"""
|
|
function checkout!(repo::GitRepo, commit::AbstractString = "";
|
|
force::Bool = true)
|
|
# nothing to do
|
|
isempty(commit) && return
|
|
|
|
# grab head name
|
|
head_name = Consts.HEAD_FILE
|
|
try
|
|
with(head(repo)) do head_ref
|
|
head_name = shortname(head_ref)
|
|
# if it is HEAD use short OID instead
|
|
if head_name == Consts.HEAD_FILE
|
|
head_name = string(GitHash(head_ref))
|
|
end
|
|
end
|
|
end
|
|
|
|
# search for commit to get a commit object
|
|
obj = GitObject(repo, GitHash(commit))
|
|
peeled = peel(GitCommit, obj)
|
|
obj_oid = GitHash(peeled)
|
|
|
|
# checkout commit
|
|
checkout_tree(repo, peeled, options = force ? CheckoutOptions(checkout_strategy = Consts.CHECKOUT_FORCE) : CheckoutOptions())
|
|
|
|
GitReference(repo, obj_oid, force=force,
|
|
msg="libgit2.checkout: moving from $head_name to $(obj_oid))")
|
|
|
|
return nothing
|
|
end
|
|
|
|
"""
|
|
clone(repo_url::AbstractString, repo_path::AbstractString; kwargs...)
|
|
|
|
Clone a remote repository located at `repo_url` to the local filesystem location `repo_path`.
|
|
|
|
The keyword arguments are:
|
|
* `branch::AbstractString=""`: which branch of the remote to clone,
|
|
if not the default repository branch (usually `master`).
|
|
* `isbare::Bool=false`: if `true`, clone the remote as a bare repository,
|
|
which will make `repo_path` itself the git directory instead of `repo_path/.git`.
|
|
This means that a working tree cannot be checked out. Plays the role of the
|
|
git CLI argument `--bare`.
|
|
* `remote_cb::Ptr{Void}=C_NULL`: a callback which will be used to create the remote
|
|
before it is cloned. If `C_NULL` (the default), no attempt will be made to create
|
|
the remote - it will be assumed to already exist.
|
|
* `payload::Nullable{P<:AbstractCredentials}=Nullable{AbstractCredentials}()`:
|
|
provides credentials if necessary, for instance if the remote is a private
|
|
repository.
|
|
|
|
Equivalent to `git clone [-b <branch>] [--bare] <repo_url> <repo_path>`.
|
|
|
|
# Examples
|
|
|
|
```julia
|
|
repo_url = "https://github.com/JuliaLang/Example.jl"
|
|
repo1 = LibGit2.clone(repo_url, "test_path")
|
|
repo2 = LibGit2.clone(repo_url, "test_path", isbare=true)
|
|
julia_url = "https://github.com/JuliaLang/julia"
|
|
julia_repo = LibGit2.clone(julia_url, "julia_path", branch="release-0.6")
|
|
```
|
|
"""
|
|
function clone(repo_url::AbstractString, repo_path::AbstractString;
|
|
branch::AbstractString="",
|
|
isbare::Bool = false,
|
|
remote_cb::Ptr{Void} = C_NULL,
|
|
payload::Nullable{<:AbstractCredentials}=Nullable{AbstractCredentials}())
|
|
# setup clone options
|
|
lbranch = Base.cconvert(Cstring, branch)
|
|
payload = make_payload(payload)
|
|
fetch_opts=FetchOptions(callbacks = RemoteCallbacks(credentials_cb(), payload))
|
|
clone_opts = CloneOptions(
|
|
bare = Cint(isbare),
|
|
checkout_branch = isempty(lbranch) ? Cstring(C_NULL) : Base.unsafe_convert(Cstring, lbranch),
|
|
fetch_opts=fetch_opts,
|
|
remote_cb = remote_cb
|
|
)
|
|
return clone(repo_url, repo_path, clone_opts)
|
|
end
|
|
|
|
""" git reset [<committish>] [--] <pathspecs>... """
|
|
function reset!(repo::GitRepo, committish::AbstractString, pathspecs::AbstractString...)
|
|
obj = GitObject(repo, isempty(committish) ? Consts.HEAD_FILE : committish)
|
|
# do not remove entries in the index matching the provided pathspecs with empty target commit tree
|
|
reset!(repo, Nullable(obj), pathspecs...)
|
|
end
|
|
|
|
"""
|
|
reset!(repo::GitRepo, id::GitHash, mode::Cint = Consts.RESET_MIXED)
|
|
|
|
Reset the repository `repo` to its state at `id`, using one of three modes
|
|
set by `mode`:
|
|
1. `Consts.RESET_SOFT` - move HEAD to `id`.
|
|
2. `Consts.RESET_MIXED` - default, move HEAD to `id` and reset the index to `id`.
|
|
3. `Consts.RESET_HARD` - move HEAD to `id`, reset the index to `id`, and discard all working changes.
|
|
|
|
Equivalent to `git reset [--soft | --mixed | --hard] <id>`.
|
|
|
|
# Example
|
|
|
|
```julia
|
|
repo = LibGit2.GitRepo(repo_path)
|
|
head_oid = LibGit2.head_oid(repo)
|
|
open(joinpath(repo_path, "file1"), "w") do f
|
|
write(f, "111\n")
|
|
end
|
|
LibGit2.add!(repo, "file1")
|
|
mode = LibGit2.Consts.RESET_HARD
|
|
# will discard the changes to file1
|
|
# and unstage it
|
|
new_head = LibGit2.reset!(repo, head_oid, mode)
|
|
```
|
|
"""
|
|
reset!(repo::GitRepo, id::GitHash, mode::Cint = Consts.RESET_MIXED) =
|
|
reset!(repo, GitObject(repo, id), mode)
|
|
|
|
"""
|
|
LibGit2.revcount(repo::GitRepo, commit1::AbstractString, commit2::AbstractString)
|
|
|
|
List the number of revisions between `commit1` and `commit2` (committish OIDs in string form).
|
|
Since `commit1` and `commit2` may be on different branches, `revcount` performs a "left-right"
|
|
revision list (and count), returning a tuple of `Int`s - the number of left and right
|
|
commits, respectively. A left (or right) commit refers to which side of a symmetric
|
|
difference in a tree the commit is reachable from.
|
|
|
|
Equivalent to `git rev-list --left-right --count <commit1> <commit2>`.
|
|
"""
|
|
function revcount(repo::GitRepo, commit1::AbstractString, commit2::AbstractString)
|
|
commit1_id = revparseid(repo, commit1)
|
|
commit2_id = revparseid(repo, commit2)
|
|
base_id = merge_base(repo, string(commit1_id), string(commit2_id))
|
|
fc = with(GitRevWalker(repo)) do walker
|
|
count((i,r)->i!=base_id, walker, oid=commit1_id, by=Consts.SORT_TOPOLOGICAL)
|
|
end
|
|
sc = with(GitRevWalker(repo)) do walker
|
|
count((i,r)->i!=base_id, walker, oid=commit2_id, by=Consts.SORT_TOPOLOGICAL)
|
|
end
|
|
return (fc-1, sc-1)
|
|
end
|
|
|
|
"""
|
|
merge!(repo::GitRepo; kwargs...) -> Bool
|
|
|
|
Perform a git merge on the repository `repo`, merging commits
|
|
with diverging history into the current branch. Returns `true`
|
|
if the merge succeeded, `false` if not.
|
|
|
|
The keyword arguments are:
|
|
* `committish::AbstractString=""`: Merge the named commit(s) in `committish`.
|
|
* `branch::AbstractString=""`: Merge the branch `branch` and all its commits
|
|
since it diverged from the current branch.
|
|
* `fastforward::Bool=false`: If `fastforward` is `true`, only merge if the
|
|
merge is a fast-forward (the current branch head is an ancestor of the
|
|
commits to be merged), otherwise refuse to merge and return `false`.
|
|
This is equivalent to the git CLI option `--ff-only`.
|
|
* `merge_opts::MergeOptions=MergeOptions()`: `merge_opts` specifies options
|
|
for the merge, such as merge strategy in case of conflicts.
|
|
* `checkout_opts::CheckoutOptions=CheckoutOptions()`: `checkout_opts` specifies
|
|
options for the checkout step.
|
|
|
|
Equivalent to `git merge [--ff-only] [<committish> | <branch>]`.
|
|
|
|
!!! note
|
|
If you specify a `branch`, this must be done in reference format, since
|
|
the string will be turned into a `GitReference`. For example, if you
|
|
wanted to merge branch `branch_a`, you would call
|
|
`merge!(repo, branch="refs/heads/branch_a")`.
|
|
"""
|
|
function merge!(repo::GitRepo;
|
|
committish::AbstractString = "",
|
|
branch::AbstractString = "",
|
|
fastforward::Bool = false,
|
|
merge_opts::MergeOptions = MergeOptions(),
|
|
checkout_opts::CheckoutOptions = CheckoutOptions())
|
|
# merge into head branch
|
|
upst_anns = if !isempty(committish) # merge committish into HEAD
|
|
if committish == Consts.FETCH_HEAD # merge FETCH_HEAD
|
|
fheads = fetchheads(repo)
|
|
filter!(fh->fh.ismerge, fheads)
|
|
if isempty(fheads)
|
|
throw(GitError(Error.Merge, Error.ERROR,
|
|
"There is no fetch reference for this branch."))
|
|
end
|
|
map(fh->GitAnnotated(repo,fh), fheads)
|
|
else # merge commitish
|
|
[GitAnnotated(repo, committish)]
|
|
end
|
|
else
|
|
if !isempty(branch) # merge provided branch into HEAD
|
|
with(GitReference(repo, branch)) do brn_ref
|
|
[GitAnnotated(repo, brn_ref)]
|
|
end
|
|
else # try to get tracking remote branch for the head
|
|
if !isattached(repo)
|
|
throw(GitError(Error.Merge, Error.ERROR,
|
|
"Repository HEAD is detached. Remote tracking branch cannot be used."))
|
|
end
|
|
if isorphan(repo)
|
|
# this isn't really a merge, but really moving HEAD
|
|
# https://github.com/libgit2/libgit2/issues/2135#issuecomment-35997764
|
|
# try to figure out remote tracking of orphan head
|
|
|
|
m = with(GitReference(repo, Consts.HEAD_FILE)) do head_sym_ref
|
|
match(r"refs/heads/(.*)", fullname(head_sym_ref))
|
|
end
|
|
if m === nothing
|
|
throw(GitError(Error.Merge, Error.ERROR,
|
|
"Unable to determine name of orphan branch."))
|
|
end
|
|
branchname = m.captures[1]
|
|
remotename = with(GitConfig, repo) do cfg
|
|
LibGit2.get(String, cfg, "branch.$branchname.remote")
|
|
end
|
|
oid = with(GitReference(repo, "refs/remotes/$remotename/$branchname")) do ref
|
|
LibGit2.GitHash(ref)
|
|
end
|
|
with(GitCommit(repo, oid)) do cmt
|
|
LibGit2.create_branch(repo, branchname, cmt)
|
|
end
|
|
return true
|
|
else
|
|
with(head(repo)) do head_ref
|
|
tr_brn_ref = upstream(head_ref)
|
|
if isnull(tr_brn_ref)
|
|
throw(GitError(Error.Merge, Error.ERROR,
|
|
"There is no tracking information for the current branch."))
|
|
end
|
|
try
|
|
[GitAnnotated(repo, Base.get(tr_brn_ref))]
|
|
finally
|
|
close(Base.get(tr_brn_ref))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
try
|
|
merge!(repo, upst_anns, fastforward,
|
|
merge_opts=merge_opts,
|
|
checkout_opts=checkout_opts)
|
|
finally
|
|
map(close, upst_anns)
|
|
end
|
|
end
|
|
|
|
"""
|
|
LibGit2.rebase!(repo::GitRepo, upstream::AbstractString="", newbase::AbstractString="")
|
|
|
|
Attempt an automatic merge rebase of the current branch, from `upstream` if provided, or
|
|
otherwise from the upstream tracking branch.
|
|
`newbase` is the branch to rebase onto. By default this is `upstream`.
|
|
|
|
If any conflicts arise which cannot be automatically resolved, the rebase will abort,
|
|
leaving the repository and working tree in its original state, and the function will throw
|
|
a `GitError`. This is roughly equivalent to the following command line statement:
|
|
|
|
git rebase --merge [<upstream>]
|
|
if [ -d ".git/rebase-merge" ]; then
|
|
git rebase --abort
|
|
fi
|
|
|
|
"""
|
|
function rebase!(repo::GitRepo, upstream::AbstractString="", newbase::AbstractString="")
|
|
with(head(repo)) do head_ref
|
|
head_ann = GitAnnotated(repo, head_ref)
|
|
upst_ann = if isempty(upstream)
|
|
brn_ref = LibGit2.upstream(head_ref)
|
|
if isnull(brn_ref)
|
|
throw(GitError(Error.Rebase, Error.ERROR,
|
|
"There is no tracking information for the current branch."))
|
|
end
|
|
try
|
|
GitAnnotated(repo, Base.get(brn_ref))
|
|
finally
|
|
close(brn_ref)
|
|
end
|
|
else
|
|
GitAnnotated(repo, upstream)
|
|
end
|
|
onto_ann = Nullable{GitAnnotated}(isempty(newbase) ? nothing : GitAnnotated(repo, newbase))
|
|
try
|
|
sig = default_signature(repo)
|
|
try
|
|
rbs = GitRebase(repo, head_ann, upst_ann, onto=onto_ann)
|
|
try
|
|
while (rbs_op = next(rbs)) !== nothing
|
|
commit(rbs, sig)
|
|
end
|
|
finish(rbs, sig)
|
|
catch err
|
|
abort(rbs)
|
|
rethrow(err)
|
|
finally
|
|
close(rbs)
|
|
end
|
|
finally
|
|
#!isnull(onto_ann) && close(get(onto_ann))
|
|
close(sig)
|
|
end
|
|
finally
|
|
if !isempty(newbase)
|
|
close(Base.get(onto_ann))
|
|
end
|
|
close(upst_ann)
|
|
close(head_ann)
|
|
end
|
|
end
|
|
return head_oid(repo)
|
|
end
|
|
|
|
|
|
"""
|
|
authors(repo::GitRepo) -> Vector{Signature}
|
|
|
|
Returns all authors of commits to the `repo` repository.
|
|
|
|
# Example
|
|
|
|
```julia
|
|
repo = LibGit2.GitRepo(repo_path)
|
|
repo_file = open(joinpath(repo_path, test_file), "a")
|
|
|
|
println(repo_file, commit_msg)
|
|
flush(repo_file)
|
|
LibGit2.add!(repo, test_file)
|
|
sig = LibGit2.Signature("TEST", "TEST@TEST.COM", round(time(), 0), 0)
|
|
commit_oid1 = LibGit2.commit(repo, "commit1"; author=sig, committer=sig)
|
|
println(repo_file, randstring(10))
|
|
flush(repo_file)
|
|
LibGit2.add!(repo, test_file)
|
|
commit_oid2 = LibGit2.commit(repo, "commit2"; author=sig, committer=sig)
|
|
|
|
# will be a Vector of [sig, sig]
|
|
auths = LibGit2.authors(repo)
|
|
```
|
|
"""
|
|
function authors(repo::GitRepo)
|
|
return with(GitRevWalker(repo)) do walker
|
|
map((oid,repo)->with(GitCommit(repo, oid)) do cmt
|
|
author(cmt)::Signature
|
|
end,
|
|
walker) #, by = Consts.SORT_TIME)
|
|
end
|
|
end
|
|
|
|
"""
|
|
snapshot(repo::GitRepo) -> State
|
|
|
|
Take a snapshot of the current state of the repository `repo`,
|
|
storing the current HEAD, index, and any uncommitted work.
|
|
The output `State` can be used later during a call to [`restore`](@ref)
|
|
to return the repository to the snapshotted state.
|
|
"""
|
|
function snapshot(repo::GitRepo)
|
|
head = GitHash(repo, Consts.HEAD_FILE)
|
|
index = with(GitIndex, repo) do idx; write_tree!(idx) end
|
|
work = try
|
|
with(GitIndex, repo) do idx
|
|
if length(readdir(path(repo))) > 1
|
|
add!(idx, ".")
|
|
write!(idx)
|
|
end
|
|
write_tree!(idx)
|
|
end
|
|
finally
|
|
# restore index
|
|
with(GitIndex, repo) do idx
|
|
read_tree!(idx, index)
|
|
write!(idx)
|
|
end
|
|
end
|
|
State(head, index, work)
|
|
end
|
|
|
|
"""
|
|
restore(s::State, repo::GitRepo)
|
|
|
|
Return a repository `repo` to a previous `State` `s`, for
|
|
example the HEAD of a branch before a merge attempt. `s`
|
|
can be generated using the [`snapshot`](@ref) function.
|
|
"""
|
|
function restore(s::State, repo::GitRepo)
|
|
head = reset!(repo, Consts.HEAD_FILE, "*") # unstage everything
|
|
with(GitIndex, repo) do idx
|
|
read_tree!(idx, s.work) # move work tree to index
|
|
opts = CheckoutOptions(
|
|
checkout_strategy = Consts.CHECKOUT_FORCE | # check the index out to work
|
|
Consts.CHECKOUT_REMOVE_UNTRACKED) # remove everything else
|
|
checkout_index(repo, Nullable(idx), options = opts)
|
|
|
|
read_tree!(idx, s.index) # restore index
|
|
end
|
|
reset!(repo, s.head, Consts.RESET_SOFT) # restore head
|
|
end
|
|
|
|
function transact(f::Function, repo::GitRepo)
|
|
state = snapshot(repo)
|
|
try f(repo) catch
|
|
restore(state, repo)
|
|
rethrow()
|
|
finally
|
|
close(repo)
|
|
end
|
|
end
|
|
|
|
function set_ssl_cert_locations(cert_loc)
|
|
cert_file = isfile(cert_loc) ? cert_loc : Cstring(C_NULL)
|
|
cert_dir = isdir(cert_loc) ? cert_loc : Cstring(C_NULL)
|
|
cert_file == C_NULL && cert_dir == C_NULL && return
|
|
# TODO FIX https://github.com/libgit2/libgit2/pull/3935#issuecomment-253910017
|
|
#ccall((:git_libgit2_opts, :libgit2), Cint,
|
|
# (Cint, Cstring, Cstring),
|
|
# Cint(Consts.SET_SSL_CERT_LOCATIONS), cert_file, cert_dir)
|
|
ENV["SSL_CERT_FILE"] = cert_file
|
|
ENV["SSL_CERT_DIR"] = cert_dir
|
|
end
|
|
|
|
function __init__()
|
|
# Look for OpenSSL env variable for CA bundle (linux only)
|
|
# windows and macOS use the OS native security backends
|
|
old_ssl_cert_dir = Base.get(ENV, "SSL_CERT_DIR", nothing)
|
|
old_ssl_cert_file = Base.get(ENV, "SSL_CERT_FILE", nothing)
|
|
@static if is_linux()
|
|
cert_loc = if "SSL_CERT_DIR" in keys(ENV)
|
|
ENV["SSL_CERT_DIR"]
|
|
elseif "SSL_CERT_FILE" in keys(ENV)
|
|
ENV["SSL_CERT_FILE"]
|
|
else
|
|
# If we have a bundled ca cert file, point libgit2 at that so SSL connections work.
|
|
abspath(ccall(:jl_get_julia_home, Any, ()),Base.DATAROOTDIR,"julia","cert.pem")
|
|
end
|
|
set_ssl_cert_locations(cert_loc)
|
|
end
|
|
|
|
err = ccall((:git_libgit2_init, :libgit2), Cint, ())
|
|
err > 0 || throw(ErrorException("error initializing LibGit2 module"))
|
|
REFCOUNT[] = 1
|
|
|
|
atexit() do
|
|
if Threads.atomic_sub!(REFCOUNT, UInt(1)) == 1
|
|
# refcount zero, no objects to be finalized
|
|
ccall((:git_libgit2_shutdown, :libgit2), Cint, ())
|
|
end
|
|
end
|
|
|
|
@static if is_linux()
|
|
if old_ssl_cert_dir != Base.get(ENV, "SSL_CERT_DIR", "")
|
|
if old_ssl_cert_dir === nothing
|
|
delete!(ENV, "SSL_CERT_DIR")
|
|
else
|
|
ENV["SSL_CERT_DIR"] = old_ssl_cert_dir
|
|
end
|
|
end
|
|
if old_ssl_cert_file != Base.get(ENV, "SSL_CERT_FILE", "")
|
|
if old_ssl_cert_file === nothing
|
|
delete!(ENV, "SSL_CERT_FILE")
|
|
else
|
|
ENV["SSL_CERT_FILE"] = old_ssl_cert_file
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
end # module
|