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

JuliaLang / julia / #38182

15 Aug 2025 03:55AM UTC coverage: 77.87% (-0.4%) from 78.28%
#38182

push

local

web-flow
🤖 [master] Bump the SparseArrays stdlib from 30201ab to bb5ecc0 (#59263)

Stdlib: SparseArrays
URL: https://github.com/JuliaSparse/SparseArrays.jl.git
Stdlib branch: main
Julia branch: master
Old commit: 30201ab
New commit: bb5ecc0
Julia version: 1.13.0-DEV
SparseArrays version: 1.13.0
Bump invoked by: @ViralBShah
Powered by:
[BumpStdlibs.jl](https://github.com/JuliaLang/BumpStdlibs.jl)

Diff:
https://github.com/JuliaSparse/SparseArrays.jl/compare/30201abcb...bb5ecc091

```
$ git log --oneline 30201ab..bb5ecc0
bb5ecc0 fast quadratic form for dense matrix, sparse vectors (#640)
34ece87 Extend 3-arg `dot` to generic `HermOrSym` sparse matrices (#643)
095b685 Exclude unintended complex symmetric sparse matrices from 3-arg `dot` (#642)
8049287 Fix signature for 2-arg matrix-matrix `dot` (#641)
cff971d Make cond(::SparseMatrix, 1 / Inf) discoverable from 2-norm error (#629)
```

Co-authored-by: ViralBShah <744411+ViralBShah@users.noreply.github.com>

48274 of 61993 relevant lines covered (77.87%)

9571166.83 hits per line

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

85.62
/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
10
using NetworkOptions
11
using Printf: @printf
12
using SHA: sha1, sha256
13

14
export with, GitRepo, GitConfig
15

16
using LibGit2_jll
17

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

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

23
function ensure_initialized end
24

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

50
using .Error
51

52
struct State
53
    head::GitHash
1✔
54
    index::GitHash
55
    work::GitHash
56
end
57

58
"""
59
    head(pkg::AbstractString)::String
60

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

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

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

83
"""
84
    iscommit(id::AbstractString, repo::GitRepo)::Bool
85

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

226
"""
227
    is_ancestor_of(a::AbstractString, b::AbstractString, repo::GitRepo)::Bool
228

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

456
        if set_head
23✔
457
            # checkout selected branch
458
            with(peel(GitTree, branch_ref)) do btree
21✔
459
                checkout_tree(repo, btree)
21✔
460
            end
461

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

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

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

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

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

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

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

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

524
    return nothing
4✔
525
end
526

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

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

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

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

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

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

598
"""
599
    connect(rmt::GitRemote, direction::Consts.GIT_DIRECTION; kwargs...)
600

601
Open a connection to a remote. `direction` can be either `DIRECTION_FETCH`
602
or `DIRECTION_PUSH`.
603

604
The keyword arguments are:
605
  * `credentials::Creds=nothing`: provides credentials and/or settings when authenticating
606
    against a private repository.
607
  * `callbacks::Callbacks=Callbacks()`: user provided callbacks and payloads.
608
"""
609
function connect(rmt::GitRemote, direction::Consts.GIT_DIRECTION;
2✔
610
                 credentials::Creds=nothing,
611
                 callbacks::Callbacks=Callbacks())
612
    cred_payload = reset!(CredentialPayload(credentials))
1✔
613
    if !haskey(callbacks, :credentials)
1✔
614
        callbacks[:credentials] = (credentials_cb(), cred_payload)
1✔
615
    elseif haskey(callbacks, :credentials) && credentials !== nothing
×
616
        throw(ArgumentError(string(
×
617
            "Unable to both use the provided `credentials` as a payload when the ",
618
            "`callbacks` also contain a credentials payload.")))
619
    end
620

621
    remote_callbacks = RemoteCallbacks(callbacks)
1✔
622
    try
1✔
623
        connect(rmt, direction, remote_callbacks)
1✔
624
    catch err
625
        if isa(err, GitError) && err.code === Error.EAUTH
×
626
            reject(cred_payload)
×
627
        else
628
            Base.shred!(cred_payload)
×
629
        end
630
        rethrow()
×
631
    end
632
    approve(cred_payload)
1✔
633
    return rmt
1✔
634
end
635

636
""" git reset [<committish>] [--] <pathspecs>... """
637
function reset!(repo::GitRepo, committish::AbstractString, pathspecs::AbstractString...)
3✔
638
    obj = GitObject(repo, isempty(committish) ? Consts.HEAD_FILE : committish)
3✔
639
    # do not remove entries in the index matching the provided pathspecs with empty target commit tree
640
    reset!(repo, obj, pathspecs...)
2✔
641
end
642

643
"""
644
    reset!(repo::GitRepo, id::GitHash, mode::Cint=Consts.RESET_MIXED)
645

646
Reset the repository `repo` to its state at `id`, using one of three modes
647
set by `mode`:
648
  1. `Consts.RESET_SOFT` - move HEAD to `id`.
649
  2. `Consts.RESET_MIXED` - default, move HEAD to `id` and reset the index to `id`.
650
  3. `Consts.RESET_HARD` - move HEAD to `id`, reset the index to `id`, and discard all working changes.
651

652
# Examples
653
```julia
654
# fetch changes
655
LibGit2.fetch(repo)
656
isfile(joinpath(repo_path, our_file)) # will be false
657

658
# fastforward merge the changes
659
LibGit2.merge!(repo, fastforward=true)
660

661
# because there was not any file locally, but there is
662
# a file remotely, we need to reset the branch
663
head_oid = LibGit2.head_oid(repo)
664
new_head = LibGit2.reset!(repo, head_oid, LibGit2.Consts.RESET_HARD)
665
```
666
In this example, the remote which is being fetched from *does* have
667
a file called `our_file` in its index, which is why we must reset.
668

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

671
# Examples
672
```julia
673
repo = LibGit2.GitRepo(repo_path)
674
head_oid = LibGit2.head_oid(repo)
675
open(joinpath(repo_path, "file1"), "w") do f
676
    write(f, "111\n")
677
end
678
LibGit2.add!(repo, "file1")
679
mode = LibGit2.Consts.RESET_HARD
680
# will discard the changes to file1
681
# and unstage it
682
new_head = LibGit2.reset!(repo, head_oid, mode)
683
```
684
"""
685
reset!(repo::GitRepo, id::GitHash, mode::Cint = Consts.RESET_MIXED) =
5✔
686
    reset!(repo, GitObject(repo, id), mode)
687

688
"""
689
    LibGit2.revcount(repo::GitRepo, commit1::AbstractString, commit2::AbstractString)
690

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

697
Equivalent to `git rev-list --left-right --count <commit1> <commit2>`.
698

699
# Examples
700
```julia
701
repo = LibGit2.GitRepo(repo_path)
702
repo_file = open(joinpath(repo_path, test_file), "a")
703
println(repo_file, "hello world")
704
flush(repo_file)
705
LibGit2.add!(repo, test_file)
706
commit_oid1 = LibGit2.commit(repo, "commit 1")
707
println(repo_file, "hello world again")
708
flush(repo_file)
709
LibGit2.add!(repo, test_file)
710
commit_oid2 = LibGit2.commit(repo, "commit 2")
711
LibGit2.revcount(repo, string(commit_oid1), string(commit_oid2))
712
```
713

714
This will return `(-1, 0)`.
715
"""
716
function revcount(repo::GitRepo, commit1::AbstractString, commit2::AbstractString)
1✔
717
    commit1_id = revparseid(repo, commit1)
1✔
718
    commit2_id = revparseid(repo, commit2)
1✔
719
    base_id = merge_base(repo, string(commit1_id), string(commit2_id))
1✔
720
    fc = with(GitRevWalker(repo)) do walker
1✔
721
        count((i,r)->i!=base_id, walker, oid=commit1_id, by=Consts.SORT_TOPOLOGICAL)
2✔
722
    end
723
    sc = with(GitRevWalker(repo)) do walker
1✔
724
        count((i,r)->i!=base_id, walker, oid=commit2_id, by=Consts.SORT_TOPOLOGICAL)
3✔
725
    end
726
    return (fc-1, sc-1)
1✔
727
end
728

729
"""
730
    merge!(repo::GitRepo; kwargs...)::Bool
731

732
Perform a git merge on the repository `repo`, merging commits
733
with diverging history into the current branch. Return `true`
734
if the merge succeeded, `false` if not.
735

736
The keyword arguments are:
737
  * `committish::AbstractString=""`: Merge the named commit(s) in `committish`.
738
  * `branch::AbstractString=""`: Merge the branch `branch` and all its commits
739
    since it diverged from the current branch.
740
  * `fastforward::Bool=false`: If `fastforward` is `true`, only merge if the
741
    merge is a fast-forward (the current branch head is an ancestor of the
742
    commits to be merged), otherwise refuse to merge and return `false`.
743
    This is equivalent to the git CLI option `--ff-only`.
744
  * `merge_opts::MergeOptions=MergeOptions()`: `merge_opts` specifies options
745
    for the merge, such as merge strategy in case of conflicts.
746
  * `checkout_opts::CheckoutOptions=CheckoutOptions()`: `checkout_opts` specifies
747
    options for the checkout step.
748

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

751
!!! note
752
    If you specify a `branch`, this must be done in reference format, since
753
    the string will be turned into a `GitReference`. For example, if you
754
    wanted to merge branch `branch_a`, you would call
755
    `merge!(repo, branch="refs/heads/branch_a")`.
756
"""
757
function merge!(repo::GitRepo;
10✔
758
                committish::AbstractString = "",
759
                branch::AbstractString = "",
760
                fastforward::Bool = false,
761
                merge_opts::MergeOptions = MergeOptions(),
762
                checkout_opts::CheckoutOptions = CheckoutOptions())
763
    # merge into head branch
764
    upst_anns = if !isempty(committish) # merge committish into HEAD
5✔
765
        if committish == Consts.FETCH_HEAD # merge FETCH_HEAD
1✔
766
            fheads = fetchheads(repo)
×
767
            filter!(fh->fh.ismerge, fheads)
×
768
            if isempty(fheads)
×
769
                throw(GitError(Error.Merge, Error.ERROR,
×
770
                               "There is no fetch reference for this branch."))
771
            end
772
            Base.map(fh->GitAnnotated(repo,fh), fheads)
×
773
        else # merge committish
774
            [GitAnnotated(repo, committish)]
2✔
775
        end
776
    else
777
        if !isempty(branch) # merge provided branch into HEAD
4✔
778
            with(GitReference(repo, branch)) do brn_ref
1✔
779
                [GitAnnotated(repo, brn_ref)]
1✔
780
            end
781
        else # try to get tracking remote branch for the head
782
            if !isattached(repo)
3✔
783
                throw(GitError(Error.Merge, Error.ERROR,
1✔
784
                               "Repository HEAD is detached. Remote tracking branch cannot be used."))
785
            end
786
            if isorphan(repo)
2✔
787
                # this isn't really a merge, but really moving HEAD
788
                # https://github.com/libgit2/libgit2/issues/2135#issuecomment-35997764
789
                # try to figure out remote tracking of orphan head
790

791
                m = with(GitReference(repo, Consts.HEAD_FILE)) do head_sym_ref
1✔
792
                    match(r"refs/heads/(.*)", fullname(head_sym_ref))
1✔
793
                end
794
                if m === nothing
1✔
795
                    throw(GitError(Error.Merge, Error.ERROR,
×
796
                                   "Unable to determine name of orphan branch."))
797
                end
798
                branchname = m.captures[1]
1✔
799
                remotename = with(GitConfig, repo) do cfg
2✔
800
                    LibGit2.get(String, cfg, "branch.$branchname.remote")
1✔
801
                end
802
                oid = with(GitReference(repo, "refs/remotes/$remotename/$branchname")) do ref
2✔
803
                    LibGit2.GitHash(ref)
1✔
804
                end
805
                with(GitCommit(repo, oid)) do cmt
1✔
806
                    LibGit2.create_branch(repo, branchname, cmt)
1✔
807
                end
808
                return true
1✔
809
            else
810
                with(head(repo)) do head_ref
4✔
811
                    tr_brn_ref = upstream(head_ref)
1✔
812
                    if tr_brn_ref === nothing
1✔
813
                        throw(GitError(Error.Merge, Error.ERROR,
1✔
814
                                       "There is no tracking information for the current branch."))
815
                    end
816
                    try
×
817
                        [GitAnnotated(repo, tr_brn_ref)]
×
818
                    finally
819
                        close(tr_brn_ref)
×
820
                    end
821
                end
822
            end
823
        end
824
    end
825
    try
2✔
826
        merge!(repo, upst_anns, fastforward,
2✔
827
               merge_opts=merge_opts,
828
               checkout_opts=checkout_opts)
829
    finally
830
        Base.foreach(close, upst_anns)
2✔
831
    end
832
end
833

834
"""
835
    LibGit2.rebase!(repo::GitRepo, upstream::AbstractString="", newbase::AbstractString="")
836

837
Attempt an automatic merge rebase of the current branch, from `upstream` if provided, or
838
otherwise from the upstream tracking branch.
839
`newbase` is the branch to rebase onto. By default this is `upstream`.
840

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

845
    git rebase --merge [<upstream>]
846
    if [ -d ".git/rebase-merge" ]; then
847
        git rebase --abort
848
    fi
849

850
"""
851
function rebase!(repo::GitRepo, upstream::AbstractString="", newbase::AbstractString="")
852
    with(head(repo)) do head_ref
7✔
853
        head_ann = GitAnnotated(repo, head_ref)
4✔
854
        upst_ann = if isempty(upstream)
4✔
855
            brn_ref = LibGit2.upstream(head_ref)
1✔
856
            if brn_ref === nothing
1✔
857
                throw(GitError(Error.Rebase, Error.ERROR,
1✔
858
                               "There is no tracking information for the current branch."))
859
            end
860
            try
×
861
                GitAnnotated(repo, brn_ref)
×
862
            finally
863
                close(brn_ref)
×
864
            end
865
        else
866
            GitAnnotated(repo, upstream)
6✔
867
        end
868
        onto_ann = isempty(newbase) ? nothing : GitAnnotated(repo, newbase)
4✔
869
        try
3✔
870
            sig = default_signature(repo)
3✔
871
            try
3✔
872
                rbs = GitRebase(repo, head_ann, upst_ann, onto=onto_ann)
4✔
873
                try
3✔
874
                    for rbs_op in rbs
5✔
875
                        commit(rbs, sig)
2✔
876
                    end
3✔
877
                    finish(rbs, sig)
3✔
878
                catch
879
                    abort(rbs)
×
880
                    rethrow()
3✔
881
                finally
882
                    close(rbs)
3✔
883
                end
884
            finally
885
                #onto_ann !== nothing && close(onto_ann)
886
                close(sig)
3✔
887
            end
888
        finally
889
            if !isempty(newbase)
3✔
890
                close(onto_ann::GitAnnotated)
1✔
891
            end
892
            close(upst_ann)
3✔
893
            close(head_ann)
3✔
894
        end
895
    end
896
    return head_oid(repo)
3✔
897
end
898

899

900
"""
901
    authors(repo::GitRepo)::Vector{Signature}
902

903
Return all authors of commits to the `repo` repository.
904

905
# Examples
906
```julia
907
repo = LibGit2.GitRepo(repo_path)
908
repo_file = open(joinpath(repo_path, test_file), "a")
909

910
println(repo_file, commit_msg)
911
flush(repo_file)
912
LibGit2.add!(repo, test_file)
913
sig = LibGit2.Signature("TEST", "TEST@TEST.COM", round(time(), 0), 0)
914
commit_oid1 = LibGit2.commit(repo, "commit1"; author=sig, committer=sig)
915
println(repo_file, randstring(10))
916
flush(repo_file)
917
LibGit2.add!(repo, test_file)
918
commit_oid2 = LibGit2.commit(repo, "commit2"; author=sig, committer=sig)
919

920
# will be a Vector of [sig, sig]
921
auths = LibGit2.authors(repo)
922
```
923
"""
924
function authors(repo::GitRepo)
925
    return with(GitRevWalker(repo)) do walker
1✔
926
        map((oid,repo)->with(GitCommit(repo, oid)) do cmt
4✔
927
                            author(cmt)::Signature
3✔
928
                        end,
929
            walker) #, by = Consts.SORT_TIME)
930
    end
931
end
932

933
"""
934
    snapshot(repo::GitRepo)::State
935

936
Take a snapshot of the current state of the repository `repo`,
937
storing the current HEAD, index, and any uncommitted work.
938
The output `State` can be used later during a call to [`restore`](@ref)
939
to return the repository to the snapshotted state.
940
"""
941
function snapshot(repo::GitRepo)
1✔
942
    head = GitHash(repo, Consts.HEAD_FILE)
1✔
943
    index = with(GitIndex, repo) do idx; write_tree!(idx) end
2✔
944
    work = try
1✔
945
        with(GitIndex, repo) do idx
1✔
946
            if length(readdir(path(repo))) > 1
1✔
947
                add!(idx, ".")
1✔
948
                write!(idx)
1✔
949
            end
950
            write_tree!(idx)
1✔
951
        end
952
    finally
953
        # restore index
954
        with(GitIndex, repo) do idx
1✔
955
            read_tree!(idx, index)
1✔
956
            write!(idx)
1✔
957
        end
958
    end
959
    State(head, index, work)
1✔
960
end
961

962
"""
963
    restore(s::State, repo::GitRepo)
964

965
Return a repository `repo` to a previous `State` `s`, for
966
example the HEAD of a branch before a merge attempt. `s`
967
can be generated using the [`snapshot`](@ref) function.
968
"""
969
function restore(s::State, repo::GitRepo)
970
    head = reset!(repo, Consts.HEAD_FILE, "*")  # unstage everything
1✔
971
    with(GitIndex, repo) do idx
1✔
972
        read_tree!(idx, s.work)            # move work tree to index
1✔
973
        opts = CheckoutOptions(
1✔
974
                checkout_strategy = Consts.CHECKOUT_FORCE |     # check the index out to work
975
                                    Consts.CHECKOUT_REMOVE_UNTRACKED) # remove everything else
976
        checkout_index(repo, idx, options = opts)
1✔
977

978
        read_tree!(idx, s.index)  # restore index
1✔
979
    end
980
    reset!(repo, s.head, Consts.RESET_SOFT) # restore head
1✔
981
end
982

983
"""
984
    transact(f::Function, repo::GitRepo)
985

986
Apply function `f` to the git repository `repo`, taking a [`snapshot`](@ref) before
987
applying `f`. If an error occurs within `f`, `repo` will be returned to its snapshot
988
state using [`restore`](@ref). The error which occurred will be rethrown, but the
989
state of `repo` will not be corrupted.
990
"""
991
function transact(f::Function, repo::GitRepo)
1✔
992
    state = snapshot(repo)
1✔
993
    try f(repo) catch
1✔
994
        restore(state, repo)
1✔
995
        rethrow()
1✔
996
    finally
997
        close(repo)
1✔
998
    end
999
end
1000

1001
## lazy libgit2 initialization
1002

1003
const ENSURE_INITIALIZED_LOCK = ReentrantLock()
1004

1005
@noinline function throw_negative_refcount_error(x::Int)
×
1006
    error("Negative LibGit2 REFCOUNT $x\nThis shouldn't happen, please file a bug report!")
×
1007
end
1008

1009
function ensure_initialized()
22✔
1010
    lock(ENSURE_INITIALIZED_LOCK) do
2,676✔
1011
        x = Threads.atomic_cas!(REFCOUNT, 0, 1)
2,676✔
1012
        x > 0 && return
2,676✔
1013
        x < 0 && throw_negative_refcount_error(x)
4✔
1014
        try initialize()
4✔
1015
        catch
1016
            Threads.atomic_sub!(REFCOUNT, 1)
×
1017
            @assert REFCOUNT[] == 0
×
1018
            rethrow()
×
1019
        end
1020
    end
1021
    return nothing
2,676✔
1022
end
1023

1024
@noinline function initialize()
4✔
1025
    @check ccall((:git_libgit2_init, libgit2), Cint, ())
4✔
1026

1027
    cert_loc = NetworkOptions.ca_roots()
11✔
1028
    cert_loc !== nothing && set_ssl_cert_locations(cert_loc)
4✔
1029

1030
    atexit() do
4✔
1031
        # refcount zero, no objects to be finalized
1032
        if Threads.atomic_sub!(REFCOUNT, 1) == 1
4✔
1033
            ccall((:git_libgit2_shutdown, libgit2), Cint, ())
1✔
1034
        end
1035
    end
1036
end
1037

1038
function set_ssl_cert_locations(cert_loc)
1✔
1039
    cert_file = cert_dir = Cstring(C_NULL)
1✔
1040
    if isdir(cert_loc) # directories
1✔
1041
        cert_dir = cert_loc
×
1042
    else # files, /dev/null, non-existent paths, etc.
1043
        cert_file = cert_loc
1✔
1044
    end
1045
    ret = @ccall libgit2.git_libgit2_opts(
1✔
1046
        Consts.SET_SSL_CERT_LOCATIONS::Cint;
1047
        cert_file::Cstring,
1048
        cert_dir::Cstring)::Cint
1049
    ret >= 0 && return ret
1✔
1050
    # On macOS and Windows LibGit2_jll is built without a TLS backend that supports
1051
    # certificate locations; don't throw on this expected error so we allow certificate
1052
    # location environment variables to be set for other purposes.
1053
    # We still try doing so to support other LibGit2 builds.
1054
    err = Error.GitError(ret)
1✔
1055
    err.class == Error.SSL &&
1✔
1056
        err.msg == "TLS backend doesn't support certificate locations" ||
1057
        throw(err)
1058
    return ret
1✔
1059
end
1060

1061
"""
1062
    trace_set(level::Union{Integer,GIT_TRACE_LEVEL})
1063

1064
Sets the system tracing configuration to the specified level.
1065
"""
1066
function trace_set(level::Union{Integer,Consts.GIT_TRACE_LEVEL}, cb=trace_cb())
4✔
1067
    @check @ccall libgit2.git_trace_set(level::Cint, cb::Ptr{Cvoid})::Cint
4✔
1068
end
1069

1070
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

© 2026 Coveralls, Inc