Skip to content

Commit

Permalink
git: add RefIterator interface
Browse files Browse the repository at this point in the history
  • Loading branch information
zombiezen committed Jul 28, 2023
1 parent a938bf2 commit cbce223
Show file tree
Hide file tree
Showing 3 changed files with 282 additions and 55 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased][]

### Added

- New methods `Git.IterateRefs` and `Git.IterateRemoteRefs`
provide a streaming API for listing refs.

### Deprecated

- `Git.ListRefs` and `Git.ListRefsVerbatim` are deprecated
in favor of `Git.IterateRefs`.
- `Git.ListRemoteRefs` is deprecated in favor of `Git.IterateRemoteRefs`.

### Fixed

- `Log` no longer retains data after closing.
Expand Down
70 changes: 62 additions & 8 deletions net.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
package git

import (
"bufio"
"bytes"
"context"
"fmt"
"io"
Expand Down Expand Up @@ -107,27 +109,79 @@ func (g *Git) clone(ctx context.Context, mode string, u *url.URL, opts CloneOpti
})
}

// IterateRemoteRefsOptions specifies filters for [Git.IterateRemoteRefs].
type IterateRemoteRefsOptions struct {
// If IncludeHead is true, the HEAD ref is included.
IncludeHead bool
// LimitToBranches limits the refs to those starting with "refs/heads/".
// This is additive with IncludeHead and LimitToTags.
LimitToBranches bool
// LimitToTags limits the refs to those starting with "refs/tags/".
// This is additive with IncludeHead and LimitToBranches.
LimitToTags bool
// If DereferenceTags is true,
// then the iterator will also produce refs for which [RefIterator.IsDereference] reports true
// that have the object hash of the tag object refers to.
DereferenceTags bool
}

// ListRemoteRefs lists all of the refs in a remote repository.
// remote may be a URL or the name of a remote.
//
// This function may block on user input if the remote requires
// credentials.
//
// Deprecated: This method will return an error on repositories with many branches (>100K).
// Use [Git.IterateRemoteRefs] instead.
func (g *Git) ListRemoteRefs(ctx context.Context, remote string) (map[Ref]Hash, error) {
return parseRefs(g.IterateRemoteRefs(ctx, remote, IterateRemoteRefsOptions{
IncludeHead: true,
}))
}

// IterateRemoteRefs starts listing all of the refs in a remote repository.
// remote may be a URL or the name of a remote.
//
// The iterator may block on user input if the remote requires credentials.
func (g *Git) IterateRemoteRefs(ctx context.Context, remote string, opts IterateRemoteRefsOptions) *RefIterator {
// TODO(someday): Add tests.

errPrefix := fmt.Sprintf("git ls-remote %q", remote)
out, err := g.output(ctx, errPrefix, []string{"ls-remote", "--quiet", "--", remote})
if err != nil {
return nil, err
args := []string{"ls-remote", "--quiet"}
if !opts.IncludeHead && !opts.DereferenceTags {
args = append(args, "--refs")
}
if len(out) == 0 {
return nil, nil
if opts.LimitToBranches {
args = append(args, "--heads")
}
refs, err := parseRefs(out, true)
if opts.LimitToTags {
args = append(args, "--tags")
}
args = append(args, "--", remote)

ctx, cancel := context.WithCancel(ctx)
stderr := new(bytes.Buffer)
pipe, err := StartPipe(ctx, g.runner, &Invocation{
Args: args,
Dir: g.dir,
Stderr: &limitWriter{w: stderr, n: errorOutputLimit},
})
if err != nil {
return refs, fmt.Errorf("%s: %w", errPrefix, err)
cancel()
return &RefIterator{
scanErr: fmt.Errorf("%s: %w", errPrefix, err),
scanDone: true,
}
}
return &RefIterator{
scanner: bufio.NewScanner(pipe),
stderr: stderr,
cancelFunc: cancel,
closer: pipe,
errPrefix: errPrefix,
ignoreDerefs: !opts.DereferenceTags,
ignoreHead: !opts.IncludeHead,
}
return refs, nil
}

// A FetchRefspec specifies a mapping from remote refs to local refs.
Expand Down
Loading

0 comments on commit cbce223

Please sign in to comment.