package git
import (
"errors"
"fmt"
"io"
"gopkg.in/src-d/go-git.v4/config"
"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/format/packfile"
"gopkg.in/src-d/go-git.v4/plumbing/protocol/packp"
"gopkg.in/src-d/go-git.v4/plumbing/protocol/packp/capability"
"gopkg.in/src-d/go-git.v4/plumbing/protocol/packp/sideband"
"gopkg.in/src-d/go-git.v4/plumbing/storer"
"gopkg.in/src-d/go-git.v4/plumbing/transport"
"gopkg.in/src-d/go-git.v4/plumbing/transport/client"
"gopkg.in/src-d/go-git.v4/storage/memory"
"gopkg.in/src-d/go-git.v4/utils/ioutil"
)
var NoErrAlreadyUpToDate = errors.New("already up-to-date")
// Remote represents a connection to a remote repository
type Remote struct {
c *config.RemoteConfig
s Storer
p sideband.Progress
}
func newRemote(s Storer, p sideband.Progress, c *config.RemoteConfig) *Remote {
return &Remote{s: s, p: p, c: c}
}
// Config return the config
func (r *Remote) Config() *config.RemoteConfig {
return r.c
}
func (r *Remote) String() string {
fetch := r.c.URL
push := r.c.URL
return fmt.Sprintf("%s\t%s (fetch)\n%[1]s\t%s (push)", r.c.Name, fetch, push)
}
// Fetch fetches references from the remote to the local repository.
func (r *Remote) Fetch(o *FetchOptions) error {
_, err := r.fetch(o)
return err
}
func (r *Remote) fetch(o *FetchOptions) (refs storer.ReferenceStorer, err error) {
if o.RemoteName == "" {
o.RemoteName = r.c.Name
}
if err := o.Validate(); err != nil {
return nil, err
}
if len(o.RefSpecs) == 0 {
o.RefSpecs = r.c.Fetch
}
s, err := r.newFetchPackSession()
if err != nil {
return nil, err
}
defer ioutil.CheckClose(s, &err)
ar, err := s.AdvertisedReferences()
if err != nil {
return nil, err
}
req, err := r.newUploadPackRequest(o, ar)
if err != nil {
return nil, err
}
remoteRefs, err := ar.AllReferences()
if err != nil {
return nil, err
}
req.Wants, err = getWants(o.RefSpecs, r.s, remoteRefs)
if len(req.Wants) == 0 {
return remoteRefs, NoErrAlreadyUpToDate
}
req.Haves, err = getHaves(r.s)
if err != nil {
return nil, err
}
if err := r.fetchPack(o, s, req); err != nil {
return nil, err
}
if err := r.updateLocalReferenceStorage(o.RefSpecs, remoteRefs); err != nil {
return nil, err
}
return remoteRefs, err
}
func (r *Remote) newFetchPackSession() (transport.FetchPackSession, error) {
ep, err := transport.NewEndpoint(r.c.URL)
if err != nil {
return nil, err
}
c, err := client.NewClient(ep)
if err != nil {
return nil, err
}
return c.NewFetchPackSession(ep)
}
func (r *Remote) fetchPack(o *FetchOptions, s transport.FetchPackSession,
req *packp.UploadPackRequest) (err error) {
reader, err := s.FetchPack(req)
if err != nil {
return err
}
defer ioutil.CheckClose(reader, &err)
if err := r.updateShallow(o, reader); err != nil {
return err
}
if err = r.updateObjectStorage(
buildSidebandIfSupported(req.Capabilities, reader, r.p),
); err != nil {
return err
}
return err
}
func getHaves(localRefs storer.ReferenceStorer) ([]plumbing.Hash, error) {
iter, err := localRefs.IterReferences()
if err != nil {
return nil, err
}
var haves []plumbing.Hash
err = iter.ForEach(func(ref *plumbing.Reference) error {
if ref.Type() != plumbing.HashReference {
return nil
}
haves = append(haves, ref.Hash())
return nil
})
if err != nil {
return nil, err
}
return haves, nil
}
func getWants(spec []config.RefSpec, localStorer Storer, remoteRefs storer.ReferenceStorer) ([]plumbing.Hash, error) {
wantTags := true
for _, s := range spec {
if !s.IsWildcard() {
wantTags = false
break
}
}
iter, err := remoteRefs.IterReferences()
if err != nil {
return nil, err
}
wants := map[plumbing.Hash]bool{}
err = iter.ForEach(func(ref *plumbing.Reference) error {
if !config.MatchAny(spec, ref.Name()) {
if !ref.IsTag() || !wantTags {
return nil
}
}
if ref.Type() == plumbing.SymbolicReference {
ref, err = storer.ResolveReference(remoteRefs, ref.Name())
if err != nil {
return err
}
}
if ref.Type() != plumbing.HashReference {
return nil
}
hash := ref.Hash()
exists, err := commitExists(localStorer, hash)
if err != nil {
return err
}
if !exists {
wants[hash] = true
}
return nil
})
if err != nil {
return nil, err
}
var result []plumbing.Hash
for h := range wants {
result = append(result, h)
}
return result, nil
}
func commitExists(s storer.EncodedObjectStorer, h plumbing.Hash) (bool, error) {
_, err := s.EncodedObject(plumbing.CommitObject, h)
if err == plumbing.ErrObjectNotFound {
return false, nil
}
return true, err
}
func (r *Remote) newUploadPackRequest(o *FetchOptions,
ar *packp.AdvRefs) (*packp.UploadPackRequest, error) {
req := packp.NewUploadPackRequestFromCapabilities(ar.Capabilities)
if o.Depth != 0 {
req.Depth = packp.DepthCommits(o.Depth)
if err := req.Capabilities.Set(capability.Shallow); err != nil {
return nil, err
}
}
if r.p == nil && ar.Capabilities.Supports(capability.NoProgress) {
if err := req.Capabilities.Set(capability.NoProgress); err != nil {
return nil, err
}
}
return req, nil
}
func (r *Remote) updateObjectStorage(reader io.Reader) error {
if sw, ok := r.s.(storer.PackfileWriter); ok {
w, err := sw.PackfileWriter()
if err != nil {
return err
}
defer w.Close()
_, err = io.Copy(w, reader)
return err
}
stream := packfile.NewScanner(reader)
d, err := packfile.NewDecoder(stream, r.s)
if err != nil {
return err
}
_, err = d.Decode()
return err
}
func buildSidebandIfSupported(l *capability.List, reader io.Reader, p sideband.Progress) io.Reader {
var t sideband.Type
switch {
case l.Supports(capability.Sideband):
t = sideband.Sideband
case l.Supports(capability.Sideband64k):
t = sideband.Sideband64k
default:
return reader
}
d := sideband.NewDemuxer(t, reader)
d.Progress = p
return d
}
func (r *Remote) updateLocalReferenceStorage(specs []config.RefSpec, refs memory.ReferenceStorage) error {
for _, spec := range specs {
for _, ref := range refs {
if !spec.Match(ref.Name()) {
continue
}
if ref.Type() != plumbing.HashReference {
continue
}
name := spec.Dst(ref.Name())
n := plumbing.NewHashReference(name, ref.Hash())
if err := r.s.SetReference(n); err != nil {
return err
}
}
}
return r.buildFetchedTags(refs)
}
func (r *Remote) buildFetchedTags(refs storer.ReferenceStorer) error {
iter, err := refs.IterReferences()
if err != nil {
return err
}
return iter.ForEach(func(ref *plumbing.Reference) error {
if !ref.IsTag() {
return nil
}
_, err := r.s.EncodedObject(plumbing.AnyObject, ref.Hash())
if err == plumbing.ErrObjectNotFound {
return nil
}
if err != nil {
return err
}
return r.s.SetReference(ref)
})
}
func (r *Remote) updateShallow(o *FetchOptions, resp *packp.UploadPackResponse) error {
if o.Depth == 0 {
return nil
}
return r.s.SetShallow(resp.Shallows)
}