• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

JuliaLang / julia / #37456

pending completion
#37456

push

local

web-flow
Cover binomial cases for large K (BigInt) (#48073)

* Cover binomial cases for large K (BigInt)

Co-authored-by: Sebastian Stock <42280794+sostock@users.noreply.github.com>

9 of 9 new or added lines in 1 file covered. (100.0%)

70916 of 81824 relevant lines covered (86.67%)

35598279.1 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

12.5
/stdlib/LibGit2/src/LibGit2.jl
1
# This file is a part of Julia. License is MIT: https://julialang.org/license
2

3
"""
4
Interface to [libgit2](https://libgit2.org/).
5
"""
6
module LibGit2
7

8
import Base: ==
9
using Base: something, notnothing
10
using Base64: base64decode
11
using NetworkOptions
12
using Printf: @printf
13
using SHA: sha1, sha256
14

15
export with, GitRepo, GitConfig
16

17
const GITHUB_REGEX =
18
    r"^(?:(?:ssh://)?git@|git://|https://(?:[\w\.\+\-]+@)?)github.com[:/](([^/].+)/(.+?))(?:\.git)?$"i
19

20
const REFCOUNT = Threads.Atomic{Int}(0)
21

22
function ensure_initialized end
23

24
include("error.jl")
25
include("utils.jl")
26
include("consts.jl")
27
include("types.jl")
28
include("signature.jl")
29
include("oid.jl")
30
include("reference.jl")
31
include("commit.jl")
32
include("repository.jl")
33
include("config.jl")
34
include("walker.jl")
35
include("remote.jl")
36
include("strarray.jl")
37
include("index.jl")
38
include("merge.jl")
39
include("tag.jl")
40
include("blob.jl")
41
include("diff.jl")
42
include("rebase.jl")
43
include("blame.jl")
44
include("status.jl")
45
include("tree.jl")
46
include("gitcredential.jl")
47
include("callbacks.jl")
48

49
using .Error
50

51
struct State
52
    head::GitHash
53
    index::GitHash
54
    work::GitHash
55
end
56

57
"""
58
    head(pkg::AbstractString) -> String
59

60
Return current HEAD [`GitHash`](@ref) of
61
the `pkg` repo as a string.
62
"""
63
function head(pkg::AbstractString)
×
64
    with(GitRepo, pkg) do repo
×
65
        string(head_oid(repo))
×
66
    end
67
end
68

69
"""
70
    need_update(repo::GitRepo)
71

72
Equivalent to `git update-index`. Return `true`
73
if `repo` needs updating.
74
"""
75
function need_update(repo::GitRepo)
×
76
    if !isbare(repo)
×
77
        # read updates index from filesystem
78
        read!(repo, true)
×
79
    end
80
end
81

82
"""
83
    iscommit(id::AbstractString, repo::GitRepo) -> Bool
84

85
Check if commit `id` (which is a [`GitHash`](@ref) in string form)
86
is in the repository.
87

88
# Examples
89
```julia-repl
90
julia> repo = GitRepo(repo_path);
91

92
julia> LibGit2.add!(repo, test_file);
93

94
julia> commit_oid = LibGit2.commit(repo, "add test_file");
95

96
julia> LibGit2.iscommit(string(commit_oid), repo)
97
true
98
```
99
"""
100
function iscommit(id::AbstractString, repo::GitRepo)
×
101
    res = true
×
102
    try
×
103
        c = GitCommit(repo, id)
×
104
        if c === nothing
×
105
            res = false
×
106
        else
107
            close(c)
×
108
        end
109
    catch
110
        res = false
×
111
    end
112
    return res
×
113
end
114

115
"""
116
    LibGit2.isdirty(repo::GitRepo, pathspecs::AbstractString=""; cached::Bool=false) -> Bool
117

118
Check if there have been any changes to tracked files in the working tree (if
119
`cached=false`) or the index (if `cached=true`).
120
`pathspecs` are the specifications for options for the diff.
121

122
# Examples
123
```julia
124
repo = LibGit2.GitRepo(repo_path)
125
LibGit2.isdirty(repo) # should be false
126
open(joinpath(repo_path, new_file), "a") do f
127
    println(f, "here's my cool new file")
128
end
129
LibGit2.isdirty(repo) # now true
130
LibGit2.isdirty(repo, new_file) # now true
131
```
132

133
Equivalent to `git diff-index HEAD [-- <pathspecs>]`.
134
"""
135
isdirty(repo::GitRepo, paths::AbstractString=""; cached::Bool=false) =
×
136
    isdiff(repo, Consts.HEAD_FILE, paths, cached=cached)
137

138
"""
139
    LibGit2.isdiff(repo::GitRepo, treeish::AbstractString, pathspecs::AbstractString=""; cached::Bool=false)
140

141
Checks if there are any differences between the tree specified by `treeish` and the
142
tracked files in the working tree (if `cached=false`) or the index (if `cached=true`).
143
`pathspecs` are the specifications for options for the diff.
144

145
# Examples
146
```julia
147
repo = LibGit2.GitRepo(repo_path)
148
LibGit2.isdiff(repo, "HEAD") # should be false
149
open(joinpath(repo_path, new_file), "a") do f
150
    println(f, "here's my cool new file")
151
end
152
LibGit2.isdiff(repo, "HEAD") # now true
153
```
154

155
Equivalent to `git diff-index <treeish> [-- <pathspecs>]`.
156
"""
157
function isdiff(repo::GitRepo, treeish::AbstractString, paths::AbstractString=""; cached::Bool=false)
×
158
    tree = GitTree(repo, "$treeish^{tree}")
×
159
    try
×
160
        diff = diff_tree(repo, tree, paths, cached=cached)
×
161
        result = count(diff) > 0
×
162
        close(diff)
×
163
        return result
×
164
    finally
165
        close(tree)
×
166
    end
167
end
168

169
"""
170
    diff_files(repo::GitRepo, branch1::AbstractString, branch2::AbstractString; kwarg...) -> Vector{AbstractString}
171

172
Show which files have changed in the git repository `repo` between branches `branch1`
173
and `branch2`.
174

175
The keyword argument is:
176
  * `filter::Set{Consts.DELTA_STATUS}=Set([Consts.DELTA_ADDED, Consts.DELTA_MODIFIED, Consts.DELTA_DELETED]))`,
177
    and it sets options for the diff. The default is to show files added, modified, or deleted.
178

179
Return only the *names* of the files which have changed, *not* their contents.
180

181
# Examples
182
```julia
183
LibGit2.branch!(repo, "branch/a")
184
LibGit2.branch!(repo, "branch/b")
185
# add a file to repo
186
open(joinpath(LibGit2.path(repo),"file"),"w") do f
187
    write(f, "hello repo\n")
188
end
189
LibGit2.add!(repo, "file")
190
LibGit2.commit(repo, "add file")
191
# returns ["file"]
192
filt = Set([LibGit2.Consts.DELTA_ADDED])
193
files = LibGit2.diff_files(repo, "branch/a", "branch/b", filter=filt)
194
# returns [] because existing files weren't modified
195
filt = Set([LibGit2.Consts.DELTA_MODIFIED])
196
files = LibGit2.diff_files(repo, "branch/a", "branch/b", filter=filt)
197
```
198

199
Equivalent to `git diff --name-only --diff-filter=<filter> <branch1> <branch2>`.
200
"""
201
function diff_files(repo::GitRepo, branch1::AbstractString, branch2::AbstractString;
×
202
                    filter::Set{Consts.DELTA_STATUS}=Set([Consts.DELTA_ADDED, Consts.DELTA_MODIFIED, Consts.DELTA_DELETED]))
203
    b1_id = revparseid(repo, branch1*"^{tree}")
×
204
    b2_id = revparseid(repo, branch2*"^{tree}")
×
205
    tree1 = GitTree(repo, b1_id)
×
206
    tree2 = GitTree(repo, b2_id)
×
207
    files = AbstractString[]
×
208
    try
×
209
        diff = diff_tree(repo, tree1, tree2)
×
210
        for i in 1:count(diff)
×
211
            delta = diff[i]
×
212
            delta === nothing && break
×
213
            if Consts.DELTA_STATUS(delta.status) in filter
×
214
                Base.push!(files, unsafe_string(delta.new_file.path))
×
215
            end
216
        end
×
217
        close(diff)
×
218
    finally
219
        close(tree1)
×
220
        close(tree2)
×
221
    end
222
    return files
×
223
end
224

225
"""
226
    is_ancestor_of(a::AbstractString, b::AbstractString, repo::GitRepo) -> Bool
227

228
Return `true` if `a`, a [`GitHash`](@ref) in string form, is an ancestor of
229
`b`, a [`GitHash`](@ref) in string form.
230

231
# Examples
232
```julia-repl
233
julia> repo = GitRepo(repo_path);
234

235
julia> LibGit2.add!(repo, test_file1);
236

237
julia> commit_oid1 = LibGit2.commit(repo, "commit1");
238

239
julia> LibGit2.add!(repo, test_file2);
240

241
julia> commit_oid2 = LibGit2.commit(repo, "commit2");
242

243
julia> LibGit2.is_ancestor_of(string(commit_oid1), string(commit_oid2), repo)
244
true
245
```
246
"""
247
function is_ancestor_of(a::AbstractString, b::AbstractString, repo::GitRepo)
×
248
    A = revparseid(repo, a)
×
249
    merge_base(repo, a, b) == A
×
250
end
251

252
"""
253
    fetch(repo::GitRepo; kwargs...)
254

255
Fetches updates from an upstream of the repository `repo`.
256

257
The keyword arguments are:
258
  * `remote::AbstractString="origin"`: which remote, specified by name,
259
    of `repo` to fetch from. If this is empty, the URL will be used to
260
    construct an anonymous remote.
261
  * `remoteurl::AbstractString=""`: the URL of `remote`. If not specified,
262
    will be assumed based on the given name of `remote`.
263
  * `refspecs=AbstractString[]`: determines properties of the fetch.
264
  * `credentials=nothing`: provides credentials and/or settings when authenticating against
265
    a private `remote`.
266
  * `callbacks=Callbacks()`: user provided callbacks and payloads.
267

268
Equivalent to `git fetch [<remoteurl>|<repo>] [<refspecs>]`.
269
"""
270
function fetch(repo::GitRepo; remote::AbstractString="origin",
×
271
               remoteurl::AbstractString="",
272
               refspecs::Vector{<:AbstractString}=AbstractString[],
273
               credentials::Creds=nothing,
274
               callbacks::Callbacks=Callbacks())
275
    rmt = if isempty(remoteurl)
×
276
        get(GitRemote, repo, remote)
×
277
    else
278
        GitRemoteAnon(repo, remoteurl)
×
279
    end
280

281
    cred_payload = reset!(CredentialPayload(credentials), GitConfig(repo))
×
282
    if !haskey(callbacks, :credentials)
×
283
        callbacks[:credentials] = (credentials_cb(), cred_payload)
×
284
    elseif haskey(callbacks, :credentials) && credentials !== nothing
×
285
        throw(ArgumentError(string(
×
286
            "Unable to both use the provided `credentials` as a payload when the ",
287
            "`callbacks` also contain a credentials payload.")))
288
    end
289

290
    result = try
×
291
        remote_callbacks = RemoteCallbacks(callbacks)
×
292
        fo = FetchOptions(callbacks=remote_callbacks)
×
293
        fetch(rmt, refspecs, msg="from $(url(rmt))", options=fo)
×
294
    catch err
295
        if isa(err, GitError) && err.code === Error.EAUTH
×
296
            reject(cred_payload)
×
297
        else
298
            Base.shred!(cred_payload)
×
299
        end
300
        rethrow()
×
301
    finally
302
        close(rmt)
×
303
    end
304
    approve(cred_payload)
×
305
    return result
×
306
end
307

308
"""
309
    push(repo::GitRepo; kwargs...)
310

311
Pushes updates to an upstream of `repo`.
312

313
The keyword arguments are:
314
  * `remote::AbstractString="origin"`: the name of the upstream remote to push to.
315
  * `remoteurl::AbstractString=""`: the URL of `remote`.
316
  * `refspecs=AbstractString[]`: determines properties of the push.
317
  * `force::Bool=false`: determines if the push will be a force push,
318
     overwriting the remote branch.
319
  * `credentials=nothing`: provides credentials and/or settings when authenticating against
320
     a private `remote`.
321
  * `callbacks=Callbacks()`: user provided callbacks and payloads.
322

323
Equivalent to `git push [<remoteurl>|<repo>] [<refspecs>]`.
324
"""
325
function push(repo::GitRepo; remote::AbstractString="origin",
×
326
              remoteurl::AbstractString="",
327
              refspecs::Vector{<:AbstractString}=AbstractString[],
328
              force::Bool=false,
329
              credentials::Creds=nothing,
330
              callbacks::Callbacks=Callbacks())
331
    rmt = if isempty(remoteurl)
×
332
        get(GitRemote, repo, remote)
×
333
    else
334
        GitRemoteAnon(repo, remoteurl)
×
335
    end
336

337
    cred_payload = reset!(CredentialPayload(credentials), GitConfig(repo))
×
338
    if !haskey(callbacks, :credentials)
×
339
        callbacks[:credentials] = (credentials_cb(), cred_payload)
×
340
    elseif haskey(callbacks, :credentials) && credentials !== nothing
×
341
        throw(ArgumentError(string(
×
342
            "Unable to both use the provided `credentials` as a payload when the ",
343
            "`callbacks` also contain a credentials payload.")))
344
    end
345

346
    result = try
×
347
        remote_callbacks = RemoteCallbacks(callbacks)
×
348
        push_opts = PushOptions(callbacks=remote_callbacks)
×
349
        push(rmt, refspecs, force=force, options=push_opts)
×
350
    catch err
351
        if isa(err, GitError) && err.code === Error.EAUTH
×
352
            reject(cred_payload)
×
353
        else
354
            Base.shred!(cred_payload)
×
355
        end
356
        rethrow()
×
357
    finally
358
        close(rmt)
×
359
    end
360
    approve(cred_payload)
×
361
    return result
×
362
end
363

364
"""
365
    branch(repo::GitRepo)
366

367
Equivalent to `git branch`.
368
Create a new branch from the current HEAD.
369
"""
370
function branch(repo::GitRepo)
×
371
    head_ref = head(repo)
×
372
    try
×
373
        branch(head_ref)
×
374
    finally
375
        close(head_ref)
×
376
    end
377
end
378

379
"""
380
    branch!(repo::GitRepo, branch_name::AbstractString, commit::AbstractString=""; kwargs...)
381

382
Checkout a new git branch in the `repo` repository. `commit` is the [`GitHash`](@ref),
383
in string form, which will be the start of the new branch.
384
If `commit` is an empty string, the current HEAD will be used.
385

386
The keyword arguments are:
387
  * `track::AbstractString=""`: the name of the
388
    remote branch this new branch should track, if any.
389
    If empty (the default), no remote branch
390
    will be tracked.
391
  * `force::Bool=false`: if `true`, branch creation will
392
    be forced.
393
  * `set_head::Bool=true`: if `true`, after the branch creation
394
    finishes the branch head will be set as the HEAD of `repo`.
395

396
Equivalent to `git checkout [-b|-B] <branch_name> [<commit>] [--track <track>]`.
397

398
# Examples
399
```julia
400
repo = LibGit2.GitRepo(repo_path)
401
LibGit2.branch!(repo, "new_branch", set_head=false)
402
```
403
"""
404
function branch!(repo::GitRepo, branch_name::AbstractString,
×
405
                 commit::AbstractString = ""; # start point
406
                 track::AbstractString  = "", # track remote branch
407
                 force::Bool=false,           # force branch creation
408
                 set_head::Bool=true)         # set as head reference on exit
409
    # try to lookup branch first
410
    branch_ref = force ? nothing : lookup_branch(repo, branch_name)
×
411
    if branch_ref === nothing
×
412
        branch_rmt_ref = isempty(track) ? nothing : lookup_branch(repo, "$track/$branch_name", true)
×
413
        # if commit is empty get head commit oid
414
        commit_id = if isempty(commit)
×
415
            if branch_rmt_ref === nothing
×
416
                with(head(repo)) do head_ref
×
417
                    with(peel(GitCommit, head_ref)) do hrc
×
418
                        GitHash(hrc)
×
419
                    end
420
                end
421
            else
422
                tmpcmt = with(peel(GitCommit, branch_rmt_ref)) do hrc
×
423
                    GitHash(hrc)
×
424
                end
425
                close(branch_rmt_ref)
×
426
                tmpcmt
×
427
            end
428
        else
429
            GitHash(commit)
×
430
        end
431
        iszero(commit_id) && return
×
432
        cmt =  GitCommit(repo, commit_id)
×
433
        new_branch_ref = nothing
×
434
        try
×
435
            new_branch_ref = create_branch(repo, branch_name, cmt, force=force)
×
436
        finally
437
            close(cmt)
×
438
            new_branch_ref === nothing && throw(GitError(Error.Object, Error.ERROR, "cannot create branch `$branch_name` with `$commit_id`"))
×
439
            branch_ref = new_branch_ref
×
440
        end
441
    end
442
    try
×
443
        #TODO: what if branch tracks other then "origin" remote
444
        if !isempty(track) # setup tracking
×
445
            try
×
446
                with(GitConfig, repo) do cfg
×
447
                    set!(cfg, "branch.$branch_name.remote", Consts.REMOTE_ORIGIN)
×
448
                    set!(cfg, "branch.$branch_name.merge", name(branch_ref))
×
449
                end
450
            catch
451
                @warn "Please provide remote tracking for branch '$branch_name' in '$(path(repo))'"
×
452
            end
453
        end
454

455
        if set_head
×
456
            # checkout selected branch
457
            with(peel(GitTree, branch_ref)) do btree
×
458
                checkout_tree(repo, btree)
×
459
            end
460

461
            # switch head to the branch
462
            head!(repo, branch_ref)
×
463
        end
464
    finally
465
        close(branch_ref)
×
466
    end
467
    return
×
468
end
469

470
"""
471
    checkout!(repo::GitRepo, commit::AbstractString=""; force::Bool=true)
472

473
Equivalent to `git checkout [-f] --detach <commit>`.
474
Checkout the git commit `commit` (a [`GitHash`](@ref) in string form)
475
in `repo`. If `force` is `true`, force the checkout and discard any
476
current changes. Note that this detaches the current HEAD.
477

478
# Examples
479
```julia
480
repo = LibGit2.GitRepo(repo_path)
481
open(joinpath(LibGit2.path(repo), "file1"), "w") do f
482
    write(f, "111\n")
483
end
484
LibGit2.add!(repo, "file1")
485
commit_oid = LibGit2.commit(repo, "add file1")
486
open(joinpath(LibGit2.path(repo), "file1"), "w") do f
487
    write(f, "112\n")
488
end
489
# would fail without the force=true
490
# since there are modifications to the file
491
LibGit2.checkout!(repo, string(commit_oid), force=true)
492
```
493
"""
494
function checkout!(repo::GitRepo, commit::AbstractString = "";
×
495
                  force::Bool = true)
496
    # nothing to do
497
    isempty(commit) && return
×
498

499
    # grab head name
500
    head_name = Consts.HEAD_FILE
×
501
    try
×
502
        with(head(repo)) do head_ref
×
503
            head_name = shortname(head_ref)
×
504
            # if it is HEAD use short OID instead
505
            if head_name == Consts.HEAD_FILE
×
506
                head_name = string(GitHash(head_ref))
×
507
            end
508
        end
509
    catch
510
    end
511

512
    # search for commit to get a commit object
513
    obj = GitObject(repo, GitHash(commit))
×
514
    peeled = peel(GitCommit, obj)
×
515
    obj_oid = GitHash(peeled)
×
516

517
    # checkout commit
518
    checkout_tree(repo, peeled, options = force ? CheckoutOptions(checkout_strategy = Consts.CHECKOUT_FORCE) : CheckoutOptions())
×
519

520
    GitReference(repo, obj_oid, force=force,
×
521
                 msg="libgit2.checkout: moving from $head_name to $(obj_oid))")
522

523
    return nothing
×
524
end
525

526
"""
527
    clone(repo_url::AbstractString, repo_path::AbstractString; kwargs...)
528

529
Clone a remote repository located at `repo_url` to the local filesystem location `repo_path`.
530

531
The keyword arguments are:
532
  * `branch::AbstractString=""`: which branch of the remote to clone,
533
    if not the default repository branch (usually `master`).
534
  * `isbare::Bool=false`: if `true`, clone the remote as a bare repository,
535
    which will make `repo_path` itself the git directory instead of `repo_path/.git`.
536
    This means that a working tree cannot be checked out. Plays the role of the
537
    git CLI argument `--bare`.
538
  * `remote_cb::Ptr{Cvoid}=C_NULL`: a callback which will be used to create the remote
539
    before it is cloned. If `C_NULL` (the default), no attempt will be made to create
540
    the remote - it will be assumed to already exist.
541
  * `credentials::Creds=nothing`: provides credentials and/or settings when authenticating
542
    against a private repository.
543
  * `callbacks::Callbacks=Callbacks()`: user provided callbacks and payloads.
544

545
Equivalent to `git clone [-b <branch>] [--bare] <repo_url> <repo_path>`.
546

547
# Examples
548
```julia
549
repo_url = "https://github.com/JuliaLang/Example.jl"
550
repo1 = LibGit2.clone(repo_url, "test_path")
551
repo2 = LibGit2.clone(repo_url, "test_path", isbare=true)
552
julia_url = "https://github.com/JuliaLang/julia"
553
julia_repo = LibGit2.clone(julia_url, "julia_path", branch="release-0.6")
554
```
555
"""
556
function clone(repo_url::AbstractString, repo_path::AbstractString;
10✔
557
               branch::AbstractString="",
558
               isbare::Bool = false,
559
               remote_cb::Ptr{Cvoid} = C_NULL,
560
               credentials::Creds=nothing,
561
               callbacks::Callbacks=Callbacks())
562
    cred_payload = reset!(CredentialPayload(credentials))
5✔
563
    if !haskey(callbacks, :credentials)
6✔
564
        callbacks[:credentials] = (credentials_cb(), cred_payload)
5✔
565
    elseif haskey(callbacks, :credentials) && credentials !== nothing
×
566
        throw(ArgumentError(string(
×
567
            "Unable to both use the provided `credentials` as a payload when the ",
568
            "`callbacks` also contain a credentials payload.")))
569
    end
570

571
    # setup clone options
572
    lbranch = Base.cconvert(Cstring, branch)
5✔
573
    GC.@preserve lbranch begin
5✔
574
        remote_callbacks = RemoteCallbacks(callbacks)
5✔
575
        fetch_opts = FetchOptions(callbacks=remote_callbacks)
5✔
576
        clone_opts = CloneOptions(
5✔
577
                    bare = Cint(isbare),
578
                    checkout_branch = isempty(lbranch) ? Cstring(C_NULL) : Base.unsafe_convert(Cstring, lbranch),
579
                    fetch_opts = fetch_opts,
580
                    remote_cb = remote_cb
581
                )
582
        repo = try
5✔
583
            clone(repo_url, repo_path, clone_opts)
8✔
584
        catch err
585
            if isa(err, GitError) && err.code === Error.EAUTH
3✔
586
                reject(cred_payload)
2✔
587
            else
588
                Base.shred!(cred_payload)
1✔
589
            end
590
            rethrow()
5✔
591
        end
592
    end
593
    approve(cred_payload)
2✔
594
    return repo
2✔
595
end
596

597
""" git reset [<committish>] [--] <pathspecs>... """
598
function reset!(repo::GitRepo, committish::AbstractString, pathspecs::AbstractString...)
×
599
    obj = GitObject(repo, isempty(committish) ? Consts.HEAD_FILE : committish)
×
600
    # do not remove entries in the index matching the provided pathspecs with empty target commit tree
601
    reset!(repo, obj, pathspecs...)
×
602
end
603

604
"""
605
    reset!(repo::GitRepo, id::GitHash, mode::Cint=Consts.RESET_MIXED)
606

607
Reset the repository `repo` to its state at `id`, using one of three modes
608
set by `mode`:
609
  1. `Consts.RESET_SOFT` - move HEAD to `id`.
610
  2. `Consts.RESET_MIXED` - default, move HEAD to `id` and reset the index to `id`.
611
  3. `Consts.RESET_HARD` - move HEAD to `id`, reset the index to `id`, and discard all working changes.
612

613
# Examples
614
```julia
615
# fetch changes
616
LibGit2.fetch(repo)
617
isfile(joinpath(repo_path, our_file)) # will be false
618

619
# fastforward merge the changes
620
LibGit2.merge!(repo, fastforward=true)
621

622
# because there was not any file locally, but there is
623
# a file remotely, we need to reset the branch
624
head_oid = LibGit2.head_oid(repo)
625
new_head = LibGit2.reset!(repo, head_oid, LibGit2.Consts.RESET_HARD)
626
```
627
In this example, the remote which is being fetched from *does* have
628
a file called `our_file` in its index, which is why we must reset.
629

630
Equivalent to `git reset [--soft | --mixed | --hard] <id>`.
631

632
# Examples
633
```julia
634
repo = LibGit2.GitRepo(repo_path)
635
head_oid = LibGit2.head_oid(repo)
636
open(joinpath(repo_path, "file1"), "w") do f
637
    write(f, "111\n")
638
end
639
LibGit2.add!(repo, "file1")
640
mode = LibGit2.Consts.RESET_HARD
641
# will discard the changes to file1
642
# and unstage it
643
new_head = LibGit2.reset!(repo, head_oid, mode)
644
```
645
"""
646
reset!(repo::GitRepo, id::GitHash, mode::Cint = Consts.RESET_MIXED) =
×
647
    reset!(repo, GitObject(repo, id), mode)
648

649
"""
650
    LibGit2.revcount(repo::GitRepo, commit1::AbstractString, commit2::AbstractString)
651

652
List the number of revisions between `commit1` and `commit2` (committish OIDs in string form).
653
Since `commit1` and `commit2` may be on different branches, `revcount` performs a "left-right"
654
revision list (and count), returning a tuple of `Int`s - the number of left and right
655
commits, respectively. A left (or right) commit refers to which side of a symmetric
656
difference in a tree the commit is reachable from.
657

658
Equivalent to `git rev-list --left-right --count <commit1> <commit2>`.
659

660
# Examples
661
```julia
662
repo = LibGit2.GitRepo(repo_path)
663
repo_file = open(joinpath(repo_path, test_file), "a")
664
println(repo_file, "hello world")
665
flush(repo_file)
666
LibGit2.add!(repo, test_file)
667
commit_oid1 = LibGit2.commit(repo, "commit 1")
668
println(repo_file, "hello world again")
669
flush(repo_file)
670
LibGit2.add!(repo, test_file)
671
commit_oid2 = LibGit2.commit(repo, "commit 2")
672
LibGit2.revcount(repo, string(commit_oid1), string(commit_oid2))
673
```
674

675
This will return `(-1, 0)`.
676
"""
677
function revcount(repo::GitRepo, commit1::AbstractString, commit2::AbstractString)
×
678
    commit1_id = revparseid(repo, commit1)
×
679
    commit2_id = revparseid(repo, commit2)
×
680
    base_id = merge_base(repo, string(commit1_id), string(commit2_id))
×
681
    fc = with(GitRevWalker(repo)) do walker
×
682
        count((i,r)->i!=base_id, walker, oid=commit1_id, by=Consts.SORT_TOPOLOGICAL)
×
683
    end
684
    sc = with(GitRevWalker(repo)) do walker
×
685
        count((i,r)->i!=base_id, walker, oid=commit2_id, by=Consts.SORT_TOPOLOGICAL)
×
686
    end
687
    return (fc-1, sc-1)
×
688
end
689

690
"""
691
    merge!(repo::GitRepo; kwargs...) -> Bool
692

693
Perform a git merge on the repository `repo`, merging commits
694
with diverging history into the current branch. Return `true`
695
if the merge succeeded, `false` if not.
696

697
The keyword arguments are:
698
  * `committish::AbstractString=""`: Merge the named commit(s) in `committish`.
699
  * `branch::AbstractString=""`: Merge the branch `branch` and all its commits
700
    since it diverged from the current branch.
701
  * `fastforward::Bool=false`: If `fastforward` is `true`, only merge if the
702
    merge is a fast-forward (the current branch head is an ancestor of the
703
    commits to be merged), otherwise refuse to merge and return `false`.
704
    This is equivalent to the git CLI option `--ff-only`.
705
  * `merge_opts::MergeOptions=MergeOptions()`: `merge_opts` specifies options
706
    for the merge, such as merge strategy in case of conflicts.
707
  * `checkout_opts::CheckoutOptions=CheckoutOptions()`: `checkout_opts` specifies
708
    options for the checkout step.
709

710
Equivalent to `git merge [--ff-only] [<committish> | <branch>]`.
711

712
!!! note
713
    If you specify a `branch`, this must be done in reference format, since
714
    the string will be turned into a `GitReference`. For example, if you
715
    wanted to merge branch `branch_a`, you would call
716
    `merge!(repo, branch="refs/heads/branch_a")`.
717
"""
718
function merge!(repo::GitRepo;
×
719
                committish::AbstractString = "",
720
                branch::AbstractString = "",
721
                fastforward::Bool = false,
722
                merge_opts::MergeOptions = MergeOptions(),
723
                checkout_opts::CheckoutOptions = CheckoutOptions())
724
    # merge into head branch
725
    upst_anns = if !isempty(committish) # merge committish into HEAD
×
726
        if committish == Consts.FETCH_HEAD # merge FETCH_HEAD
×
727
            fheads = fetchheads(repo)
×
728
            filter!(fh->fh.ismerge, fheads)
×
729
            if isempty(fheads)
×
730
                throw(GitError(Error.Merge, Error.ERROR,
×
731
                               "There is no fetch reference for this branch."))
732
            end
733
            Base.map(fh->GitAnnotated(repo,fh), fheads)
×
734
        else # merge committish
735
            [GitAnnotated(repo, committish)]
×
736
        end
737
    else
738
        if !isempty(branch) # merge provided branch into HEAD
×
739
            with(GitReference(repo, branch)) do brn_ref
×
740
                [GitAnnotated(repo, brn_ref)]
×
741
            end
742
        else # try to get tracking remote branch for the head
743
            if !isattached(repo)
×
744
                throw(GitError(Error.Merge, Error.ERROR,
×
745
                               "Repository HEAD is detached. Remote tracking branch cannot be used."))
746
            end
747
            if isorphan(repo)
×
748
                # this isn't really a merge, but really moving HEAD
749
                # https://github.com/libgit2/libgit2/issues/2135#issuecomment-35997764
750
                # try to figure out remote tracking of orphan head
751

752
                m = with(GitReference(repo, Consts.HEAD_FILE)) do head_sym_ref
×
753
                    match(r"refs/heads/(.*)", fullname(head_sym_ref))
×
754
                end
755
                if m === nothing
×
756
                    throw(GitError(Error.Merge, Error.ERROR,
×
757
                                   "Unable to determine name of orphan branch."))
758
                end
759
                branchname = m.captures[1]
×
760
                remotename = with(GitConfig, repo) do cfg
×
761
                    LibGit2.get(String, cfg, "branch.$branchname.remote")
×
762
                end
763
                oid = with(GitReference(repo, "refs/remotes/$remotename/$branchname")) do ref
×
764
                    LibGit2.GitHash(ref)
×
765
                end
766
                with(GitCommit(repo, oid)) do cmt
×
767
                    LibGit2.create_branch(repo, branchname, cmt)
×
768
                end
769
                return true
×
770
            else
771
                with(head(repo)) do head_ref
×
772
                    tr_brn_ref = upstream(head_ref)
×
773
                    if tr_brn_ref === nothing
×
774
                        throw(GitError(Error.Merge, Error.ERROR,
×
775
                                       "There is no tracking information for the current branch."))
776
                    end
777
                    try
×
778
                        [GitAnnotated(repo, tr_brn_ref)]
×
779
                    finally
780
                        close(tr_brn_ref)
×
781
                    end
782
                end
783
            end
784
        end
785
    end
786
    try
×
787
        merge!(repo, upst_anns, fastforward,
×
788
               merge_opts=merge_opts,
789
               checkout_opts=checkout_opts)
790
    finally
791
        Base.foreach(close, upst_anns)
×
792
    end
793
end
794

795
"""
796
    LibGit2.rebase!(repo::GitRepo, upstream::AbstractString="", newbase::AbstractString="")
797

798
Attempt an automatic merge rebase of the current branch, from `upstream` if provided, or
799
otherwise from the upstream tracking branch.
800
`newbase` is the branch to rebase onto. By default this is `upstream`.
801

802
If any conflicts arise which cannot be automatically resolved, the rebase will abort,
803
leaving the repository and working tree in its original state, and the function will throw
804
a `GitError`. This is roughly equivalent to the following command line statement:
805

806
    git rebase --merge [<upstream>]
807
    if [ -d ".git/rebase-merge" ]; then
808
        git rebase --abort
809
    fi
810

811
"""
812
function rebase!(repo::GitRepo, upstream::AbstractString="", newbase::AbstractString="")
×
813
    with(head(repo)) do head_ref
×
814
        head_ann = GitAnnotated(repo, head_ref)
×
815
        upst_ann = if isempty(upstream)
×
816
            brn_ref = LibGit2.upstream(head_ref)
×
817
            if brn_ref === nothing
×
818
                throw(GitError(Error.Rebase, Error.ERROR,
×
819
                               "There is no tracking information for the current branch."))
820
            end
821
            try
×
822
                GitAnnotated(repo, brn_ref)
×
823
            finally
824
                close(brn_ref)
×
825
            end
826
        else
827
            GitAnnotated(repo, upstream)
×
828
        end
829
        onto_ann = isempty(newbase) ? nothing : GitAnnotated(repo, newbase)
×
830
        try
×
831
            sig = default_signature(repo)
×
832
            try
×
833
                rbs = GitRebase(repo, head_ann, upst_ann, onto=onto_ann)
×
834
                try
×
835
                    for rbs_op in rbs
×
836
                        commit(rbs, sig)
×
837
                    end
×
838
                    finish(rbs, sig)
×
839
                catch
840
                    abort(rbs)
×
841
                    rethrow()
×
842
                finally
843
                    close(rbs)
×
844
                end
845
            finally
846
                #onto_ann !== nothing && close(onto_ann)
847
                close(sig)
×
848
            end
849
        finally
850
            if !isempty(newbase)
×
851
                close(onto_ann::GitAnnotated)
×
852
            end
853
            close(upst_ann)
×
854
            close(head_ann)
×
855
        end
856
    end
857
    return head_oid(repo)
×
858
end
859

860

861
"""
862
    authors(repo::GitRepo) -> Vector{Signature}
863

864
Return all authors of commits to the `repo` repository.
865

866
# Examples
867
```julia
868
repo = LibGit2.GitRepo(repo_path)
869
repo_file = open(joinpath(repo_path, test_file), "a")
870

871
println(repo_file, commit_msg)
872
flush(repo_file)
873
LibGit2.add!(repo, test_file)
874
sig = LibGit2.Signature("TEST", "TEST@TEST.COM", round(time(), 0), 0)
875
commit_oid1 = LibGit2.commit(repo, "commit1"; author=sig, committer=sig)
876
println(repo_file, randstring(10))
877
flush(repo_file)
878
LibGit2.add!(repo, test_file)
879
commit_oid2 = LibGit2.commit(repo, "commit2"; author=sig, committer=sig)
880

881
# will be a Vector of [sig, sig]
882
auths = LibGit2.authors(repo)
883
```
884
"""
885
function authors(repo::GitRepo)
×
886
    return with(GitRevWalker(repo)) do walker
×
887
        map((oid,repo)->with(GitCommit(repo, oid)) do cmt
×
888
                            author(cmt)::Signature
×
889
                        end,
890
            walker) #, by = Consts.SORT_TIME)
891
    end
892
end
893

894
"""
895
    snapshot(repo::GitRepo) -> State
896

897
Take a snapshot of the current state of the repository `repo`,
898
storing the current HEAD, index, and any uncommitted work.
899
The output `State` can be used later during a call to [`restore`](@ref)
900
to return the repository to the snapshotted state.
901
"""
902
function snapshot(repo::GitRepo)
×
903
    head = GitHash(repo, Consts.HEAD_FILE)
×
904
    index = with(GitIndex, repo) do idx; write_tree!(idx) end
×
905
    work = try
×
906
        with(GitIndex, repo) do idx
×
907
            if length(readdir(path(repo))) > 1
×
908
                add!(idx, ".")
×
909
                write!(idx)
×
910
            end
911
            write_tree!(idx)
×
912
        end
913
    finally
914
        # restore index
915
        with(GitIndex, repo) do idx
×
916
            read_tree!(idx, index)
×
917
            write!(idx)
×
918
        end
919
    end
920
    State(head, index, work)
×
921
end
922

923
"""
924
    restore(s::State, repo::GitRepo)
925

926
Return a repository `repo` to a previous `State` `s`, for
927
example the HEAD of a branch before a merge attempt. `s`
928
can be generated using the [`snapshot`](@ref) function.
929
"""
930
function restore(s::State, repo::GitRepo)
×
931
    head = reset!(repo, Consts.HEAD_FILE, "*")  # unstage everything
×
932
    with(GitIndex, repo) do idx
×
933
        read_tree!(idx, s.work)            # move work tree to index
×
934
        opts = CheckoutOptions(
×
935
                checkout_strategy = Consts.CHECKOUT_FORCE |     # check the index out to work
936
                                    Consts.CHECKOUT_REMOVE_UNTRACKED) # remove everything else
937
        checkout_index(repo, idx, options = opts)
×
938

939
        read_tree!(idx, s.index)  # restore index
×
940
    end
941
    reset!(repo, s.head, Consts.RESET_SOFT) # restore head
×
942
end
943

944
"""
945
    transact(f::Function, repo::GitRepo)
946

947
Apply function `f` to the git repository `repo`, taking a [`snapshot`](@ref) before
948
applying `f`. If an error occurs within `f`, `repo` will be returned to its snapshot
949
state using [`restore`](@ref). The error which occurred will be rethrown, but the
950
state of `repo` will not be corrupted.
951
"""
952
function transact(f::Function, repo::GitRepo)
×
953
    state = snapshot(repo)
×
954
    try f(repo) catch
×
955
        restore(state, repo)
×
956
        rethrow()
×
957
    finally
958
        close(repo)
×
959
    end
960
end
961

962
## lazy libgit2 initialization
963

964
const ENSURE_INITIALIZED_LOCK = ReentrantLock()
965

966
@noinline function throw_negative_refcount_error(x::Int)
×
967
    error("Negative LibGit2 REFCOUNT $x\nThis shouldn't happen, please file a bug report!")
×
968
end
969

970
function ensure_initialized()
71✔
971
    lock(ENSURE_INITIALIZED_LOCK) do
320✔
972
        x = Threads.atomic_cas!(REFCOUNT, 0, 1)
320✔
973
        x > 0 && return
320✔
974
        x < 0 && throw_negative_refcount_error(x)
6✔
975
        try initialize()
12✔
976
        catch
977
            Threads.atomic_sub!(REFCOUNT, 1)
3✔
978
            @assert REFCOUNT[] == 0
3✔
979
            rethrow()
3✔
980
        end
981
    end
982
    return nothing
28✔
983
end
984

985
@noinline function initialize()
6✔
986
    @check ccall((:git_libgit2_init, :libgit2), Cint, ())
6✔
987

988
    cert_loc = NetworkOptions.ca_roots()
12✔
989
    cert_loc !== nothing && set_ssl_cert_locations(cert_loc)
6✔
990

991
    atexit() do
3✔
992
        # refcount zero, no objects to be finalized
993
        if Threads.atomic_sub!(REFCOUNT, 1) == 1
994
            ccall((:git_libgit2_shutdown, :libgit2), Cint, ())
995
        end
996
    end
997
end
998

999
function set_ssl_cert_locations(cert_loc)
6✔
1000
    cert_file = cert_dir = Cstring(C_NULL)
×
1001
    if isdir(cert_loc) # directories
6✔
1002
        cert_dir = cert_loc
×
1003
    else # files, /dev/null, non-existent paths, etc.
1004
        cert_file = cert_loc
×
1005
    end
1006
    ret = @ccall "libgit2".git_libgit2_opts(
6✔
1007
        Consts.SET_SSL_CERT_LOCATIONS::Cint;
1008
        cert_file::Cstring,
1009
        cert_dir::Cstring)::Cint
1010
    ret >= 0 && return ret
6✔
1011
    err = Error.GitError(ret)
3✔
1012
    err.class == Error.SSL &&
6✔
1013
        err.msg == "TLS backend doesn't support certificate locations" ||
1014
        throw(err)
1015
    var = nothing
×
1016
    for v in NetworkOptions.CA_ROOTS_VARS
×
1017
        haskey(ENV, v) && (var = v)
×
1018
    end
×
1019
    @assert var !== nothing # otherwise we shouldn't be here
×
1020
    msg = """
×
1021
    Your Julia is built with a SSL/TLS engine that libgit2 doesn't know how to configure to use a file or directory of certificate authority roots, but your environment specifies one via the $var variable. If you believe your system's root certificates are safe to use, you can `export JULIA_SSL_CA_ROOTS_PATH=""` in your environment to use those instead.
1022
    """
1023
    throw(Error.GitError(err.class, err.code, chomp(msg)))
×
1024
end
1025

1026
"""
1027
    trace_set(level::Union{Integer,GIT_TRACE_LEVEL})
1028

1029
Sets the system tracing configuration to the specified level.
1030
"""
1031
function trace_set(level::Union{Integer,Consts.GIT_TRACE_LEVEL}, cb=trace_cb())
×
1032
    @check @ccall "libgit2".git_trace_set(level::Cint, cb::Ptr{Cvoid})::Cint
×
1033
end
1034

1035
end # module
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc