/
githubmirror
/
julia
Обзор
Документация
Войти
/
githubmirror
/
julia
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
stdlib/LibGit2/src/LibGit2.jl
1 102 строки
37 KB
Jameson Nash
docs: fix grammar and clarity in comments and docstrings (#62083)
17 июн 2026, 05:57
Не верифицирован
17 июн 2026, 05:57
27858bb
Код
Авторство
О чём код?
# This file is a part of Julia. License is MIT: https://julialang.org/license """ Interface to [libgit2](https://libgit2.org/). """ module LibGit2 import Base: == using Base: something using NetworkOptions using Printf: @printf using SHA: sha1, sha256 export with, GitRepo, GitConfig using LibGit2_jll const GITHUB_REGEX = r"^(?:(?:ssh://)?git@|git://|https://(?:[\w\.\+\-]+@)?)github.com[:/](([^/].+)/(.+?))(?:\.git)?$"i const REFCOUNT = Threads.Atomic{Int}(0) function ensure_initialized end include("error.jl") include("utils.jl") include("consts.jl") include("types.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("blame.jl") include("status.jl") include("tree.jl") include("gitcredential.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`. Return `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 Check if commit `id` (which is a [`GitHash`](@ref) in string form) is in the repository. # Examples ```julia-repl julia> repo = 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 Check 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. # Examples ```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. # Examples ```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. Return only the *names* of the files which have changed, *not* their contents. # Examples ```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[] with(tree1) do tree1 with(tree2) do tree2 with(diff_tree(repo, tree1, tree2)) do diff for i in 1:count(diff) delta = diff[i] delta === nothing && break if Consts.DELTA_STATUS(delta.status) in filter Base.push!(files, unsafe_string(delta.new_file.path)) end end end end end return files end """ is_ancestor_of(a::AbstractString, b::AbstractString, repo::GitRepo)::Bool Return `true` if `a`, a [`GitHash`](@ref) in string form, is an ancestor of `b`, a [`GitHash`](@ref) in string form. # Examples ```julia-repl julia> repo = 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 """ 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. * `depth::Integer=0`: limit fetching to the specified number of commits from the tip of each remote branch. `0` indicates a full fetch (the default). Use `Consts.FETCH_DEPTH_UNSHALLOW` to fetch all missing data from a shallow clone. Note: depth is, at the time of writing, only supported for network protocols (http, https, git, ssh), not for local filesystem paths. (https://github.com/libgit2/libgit2/issues/6634) * `credentials=nothing`: provides credentials and/or settings when authenticating against a private `remote`. * `callbacks=Callbacks()`: user provided callbacks and payloads. Equivalent to `git fetch [--depth <depth>] [<remoteurl>|<repo>] [<refspecs>]`. """ function fetch(repo::GitRepo; remote::AbstractString="origin", remoteurl::AbstractString="", refspecs::Vector{<:AbstractString}=AbstractString[], depth::Integer=0, credentials::Creds=nothing, callbacks::Callbacks=Callbacks()) rmt = if isempty(remoteurl) get(GitRemote, repo, remote) else GitRemoteAnon(repo, remoteurl) end cred_payload = reset!(CredentialPayload(credentials), GitConfig(repo)) if !haskey(callbacks, :credentials) callbacks[:credentials] = (credentials_cb(), cred_payload) elseif haskey(callbacks, :credentials) && credentials !== nothing throw(ArgumentError(string( "Unable to both use the provided `credentials` as a payload when the ", "`callbacks` also contain a credentials payload."))) end result = try remote_callbacks = RemoteCallbacks(callbacks) @static if LibGit2.VERSION >= v"1.7.0" fo = FetchOptions(callbacks=remote_callbacks, depth=Cuint(depth)) else depth != 0 && throw(ArgumentError("Depth parameter for fetch requires libgit2 >= 1.7.0")) fo = FetchOptions(callbacks=remote_callbacks) end fetch(rmt, refspecs, msg="from $(url(rmt))", options=fo) catch err if isa(err, GitError) && err.code === Error.EAUTH reject(cred_payload) else Base.shred!(cred_payload) end rethrow() finally close(rmt) end approve(cred_payload) return result 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. * `credentials=nothing`: provides credentials and/or settings when authenticating against a private `remote`. * `callbacks=Callbacks()`: user provided callbacks and payloads. Equivalent to `git push [<remoteurl>|<repo>] [<refspecs>]`. """ function push(repo::GitRepo; remote::AbstractString="origin", remoteurl::AbstractString="", refspecs::Vector{<:AbstractString}=AbstractString[], force::Bool=false, credentials::Creds=nothing, callbacks::Callbacks=Callbacks()) rmt = if isempty(remoteurl) get(GitRemote, repo, remote) else GitRemoteAnon(repo, remoteurl) end cred_payload = reset!(CredentialPayload(credentials), GitConfig(repo)) if !haskey(callbacks, :credentials) callbacks[:credentials] = (credentials_cb(), cred_payload) elseif haskey(callbacks, :credentials) && credentials !== nothing throw(ArgumentError(string( "Unable to both use the provided `credentials` as a payload when the ", "`callbacks` also contain a credentials payload."))) end result = try remote_callbacks = RemoteCallbacks(callbacks) push_opts = PushOptions(callbacks=remote_callbacks) push(rmt, refspecs, force=force, options=push_opts) catch err if isa(err, GitError) && err.code === Error.EAUTH reject(cred_payload) else Base.shred!(cred_payload) end rethrow() finally close(rmt) end approve(cred_payload) return result end """ branch(repo::GitRepo) Equivalent to `git branch --show-current`. Return the name of the current branch (HEAD reference). Throw if no branch is checked out. """ 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>]`. # Examples ```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 ? nothing : lookup_branch(repo, branch_name) if branch_ref === nothing branch_rmt_ref = isempty(track) ? nothing : lookup_branch(repo, "$track/$branch_name", true) # if commit is empty get head commit oid commit_id = if isempty(commit) if branch_rmt_ref === nothing with(head(repo)) do head_ref with(peel(GitCommit, head_ref)) do hrc GitHash(hrc) end end else tmpcmt = with(peel(GitCommit, branch_rmt_ref)) do hrc GitHash(hrc) end close(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 = create_branch(repo, branch_name, cmt, force=force) finally close(cmt) end if new_branch_ref === nothing throw(GitError(Error.Object, Error.ERROR, "cannot create branch `$branch_name` with `$commit_id`")) end branch_ref = new_branch_ref end branch_ref′ = branch_ref # Avoids boxing `branch_ref` try #TODO: what if branch tracks other than "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(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, branch_ref′)) do btree checkout_tree(repo, btree) end # switch head to the branch head!(repo, branch_ref′) end finally close(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. # Examples ```julia repo = LibGit2.GitRepo(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 = Ref(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 catch end # search for commit to get a commit object with(GitObject(repo, GitHash(commit))) do obj with(peel(GitCommit, obj)) do peeled obj_oid = GitHash(peeled) # checkout commit checkout_tree(repo, peeled, options = force ? CheckoutOptions(checkout_strategy = Consts.CHECKOUT_FORCE) : CheckoutOptions()) with(GitReference(repo, obj_oid, force=force, msg="libgit2.checkout: moving from $(head_name[]) to $(obj_oid)")) do _ end end end 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{Cvoid}=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. * `depth::Integer=0`: create a shallow clone with a history truncated to the specified number of commits. `0` indicates a full clone (the default). Use `Consts.FETCH_DEPTH_UNSHALLOW` to fetch all missing data from a shallow clone. Note: shallow clones are, at the time of writing, only supported for network protocols (http, https, git, ssh), not for local filesystem paths. (https://github.com/libgit2/libgit2/issues/6634) * `credentials::Creds=nothing`: provides credentials and/or settings when authenticating against a private repository. * `callbacks::Callbacks=Callbacks()`: user provided callbacks and payloads. Equivalent to `git clone [-b <branch>] [--bare] [--depth <depth>] <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") # Shallow clone with only the most recent commit shallow_repo = LibGit2.clone(repo_url, "shallow_path", depth=1) ``` """ function clone(repo_url::AbstractString, repo_path::AbstractString; branch::AbstractString="", isbare::Bool = false, remote_cb::Ptr{Cvoid} = C_NULL, depth::Integer = 0, credentials::Creds=nothing, callbacks::Callbacks=Callbacks()) cred_payload = reset!(CredentialPayload(credentials)) if !haskey(callbacks, :credentials) callbacks[:credentials] = (credentials_cb(), cred_payload) elseif haskey(callbacks, :credentials) && credentials !== nothing throw(ArgumentError(string( "Unable to both use the provided `credentials` as a payload when the ", "`callbacks` also contain a credentials payload."))) end # setup clone options lbranch = Base.cconvert(Cstring, branch) GC.@preserve lbranch begin remote_callbacks = RemoteCallbacks(callbacks) @static if LibGit2.VERSION >= v"1.7.0" fetch_opts = FetchOptions(callbacks=remote_callbacks, depth=Cuint(depth)) else depth != 0 && throw(ArgumentError("Shallow clone (depth parameter) requires libgit2 >= 1.7.0")) fetch_opts = FetchOptions(callbacks=remote_callbacks) end 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 ) repo = try clone(repo_url, repo_path, clone_opts) catch err if isa(err, GitError) && err.code === Error.EAUTH reject(cred_payload) else Base.shred!(cred_payload) end rethrow() end end approve(cred_payload) return repo end """ connect(rmt::GitRemote, direction::Consts.GIT_DIRECTION; kwargs...) Open a connection to a remote. `direction` can be either `DIRECTION_FETCH` or `DIRECTION_PUSH`. The keyword arguments are: * `credentials::Creds=nothing`: provides credentials and/or settings when authenticating against a private repository. * `callbacks::Callbacks=Callbacks()`: user provided callbacks and payloads. """ function connect(rmt::GitRemote, direction::Consts.GIT_DIRECTION; credentials::Creds=nothing, callbacks::Callbacks=Callbacks()) cred_payload = reset!(CredentialPayload(credentials)) if !haskey(callbacks, :credentials) callbacks[:credentials] = (credentials_cb(), cred_payload) elseif haskey(callbacks, :credentials) && credentials !== nothing throw(ArgumentError(string( "Unable to both use the provided `credentials` as a payload when the ", "`callbacks` also contain a credentials payload."))) end remote_callbacks = RemoteCallbacks(callbacks) try connect(rmt, direction, remote_callbacks) catch err if isa(err, GitError) && err.code === Error.EAUTH reject(cred_payload) else Base.shred!(cred_payload) end rethrow() end approve(cred_payload) return rmt end """ Equivalent to `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, 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. # Examples ```julia # fetch changes LibGit2.fetch(repo) isfile(joinpath(repo_path, our_file)) # will be false # fastforward merge the changes LibGit2.merge!(repo, fastforward=true) # because there was not any file locally, but there is # a file remotely, we need to reset the branch head_oid = LibGit2.head_oid(repo) new_head = LibGit2.reset!(repo, head_oid, LibGit2.Consts.RESET_HARD) ``` In this example, the remote which is being fetched from *does* have a file called `our_file` in its index, which is why we must reset. Equivalent to `git reset [--soft | --mixed | --hard] <id>`. # Examples ```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>`. # Examples ```julia repo = LibGit2.GitRepo(repo_path) repo_file = open(joinpath(repo_path, test_file), "a") println(repo_file, "hello world") flush(repo_file) LibGit2.add!(repo, test_file) commit_oid1 = LibGit2.commit(repo, "commit 1") println(repo_file, "hello world again") flush(repo_file) LibGit2.add!(repo, test_file) commit_oid2 = LibGit2.commit(repo, "commit 2") LibGit2.revcount(repo, string(commit_oid1), string(commit_oid2)) ``` This will return `(-1, 0)`. """ 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. Return `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 Base.map(fh->GitAnnotated(repo,fh), fheads) else # merge committish [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 tr_brn_ref === nothing throw(GitError(Error.Merge, Error.ERROR, "There is no tracking information for the current branch.")) end try [GitAnnotated(repo, tr_brn_ref)] finally close(tr_brn_ref) end end end end end try merge!(repo, upst_anns, fastforward, merge_opts=merge_opts, checkout_opts=checkout_opts) finally Base.foreach(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 brn_ref === nothing throw(GitError(Error.Rebase, Error.ERROR, "There is no tracking information for the current branch.")) end try GitAnnotated(repo, brn_ref) finally close(brn_ref) end else GitAnnotated(repo, upstream) end onto_ann = isempty(newbase) ? nothing : GitAnnotated(repo, newbase) try sig = default_signature(repo) try rbs = GitRebase(repo, head_ann, upst_ann, onto=onto_ann) try for rbs_op in rbs commit(rbs, sig) end finish(rbs, sig) catch abort(rbs) rethrow() finally close(rbs) end finally #onto_ann !== nothing && close(onto_ann) close(sig) end finally if !isempty(newbase) close(onto_ann::GitAnnotated) end close(upst_ann) close(head_ann) end end return head_oid(repo) end """ authors(repo::GitRepo)::Vector{Signature} Return all authors of commits to the `repo` repository. # Examples ```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, idx, options = opts) read_tree!(idx, s.index) # restore index end reset!(repo, s.head, Consts.RESET_SOFT) # restore head end """ transact(f::Function, repo::GitRepo) Apply function `f` to the git repository `repo`, taking a [`snapshot`](@ref) before applying `f`. If an error occurs within `f`, `repo` will be returned to its snapshot state using [`restore`](@ref). The error which occurred will be rethrown, but the state of `repo` will not be corrupted. """ function transact(f::Function, repo::GitRepo) state = snapshot(repo) try f(repo) catch restore(state, repo) rethrow() finally close(repo) end end ## lazy libgit2 initialization const ENSURE_INITIALIZED_LOCK = ReentrantLock() @noinline function throw_negative_refcount_error(x::Int) error("Negative LibGit2 REFCOUNT $x\nThis shouldn't happen, please file a bug report!") end function ensure_initialized() lock(ENSURE_INITIALIZED_LOCK) do x = Threads.atomic_cas!(REFCOUNT, 0, 1) x > 0 && return x < 0 && throw_negative_refcount_error(x) try initialize() catch Threads.atomic_sub!(REFCOUNT, 1) @assert REFCOUNT[] == 0 rethrow() end end return nothing end @noinline function initialize() @check ccall((:git_libgit2_init, libgit2), Cint, ()) cert_loc = NetworkOptions.ca_roots() cert_loc !== nothing && set_ssl_cert_locations(cert_loc) atexit() do # refcount zero, no objects to be finalized if Threads.atomic_sub!(REFCOUNT, 1) == 1 ccall((:git_libgit2_shutdown, libgit2), Cint, ()) end end end function set_ssl_cert_locations(cert_loc) cert_file = cert_dir = Cstring(C_NULL) if isdir(cert_loc) # directories cert_dir = cert_loc else # files, /dev/null, non-existent paths, etc. cert_file = cert_loc end ret = @ccall libgit2.git_libgit2_opts( Consts.SET_SSL_CERT_LOCATIONS::Cint; cert_file::Cstring, cert_dir::Cstring)::Cint ret >= 0 && return ret # On macOS and Windows LibGit2_jll is built without a TLS backend that supports # certificate locations; don't throw on this expected error so we allow certificate # location environment variables to be set for other purposes. # We still try doing so to support other LibGit2 builds. err = Error.GitError(ret) err.class == Error.SSL && err.msg == "TLS backend doesn't support certificate locations" || throw(err) return ret end """ trace_set(level::Union{Integer,GIT_TRACE_LEVEL}) Sets the system tracing configuration to the specified level. """ function trace_set(level::Union{Integer,Consts.GIT_TRACE_LEVEL}, cb=trace_cb()) @check @ccall libgit2.git_trace_set(level::Cint, cb::Ptr{Cvoid})::Cint end end # module