func (api *API) CollectPushTags(cn *collection.Collection, push PushConfig) (*collection.Collection, error) {
	log.Debugf(
		"%s collection: %+v (%d repos / %d tags)",
		fn(), cn, cn.RepoCount(), cn.TagCount(),
	)
	log.Debugf("%s push config: %+v", fn(), push)
	pushPathTemplate, terr := makePushPathTemplate(push)
	if terr != nil {
		return nil, terr
	}
	refs := make([]string, len(cn.Refs()))
	done := make(chan error, len(cn.Refs()))
	tagc := make(chan rtags, len(refs))
	for i, repo := range cn.Repos() {
		go func(repo *repository.Repository, i int, done chan error) {
			refs[i] = repo.Ref()
			pushPrefix := getPushPrefix(push.Prefix, repo.PushPrefix())
			if err := validatePushPrefix(pushPrefix); err != nil {
				done <- err
				return
			}
			pushPath, perr := pushPathTemplate(pushPrefix, repo.PushPath(push.PathSeparator), repo.Name())
			if perr != nil {
				done <- perr
				return
			}
			pushRef := fmt.Sprintf(
				"%s%s~/.*/",
				push.Registry,
				pushPath,
			)
			log.Debugf("%s 'push' reference: %+v", fn(repo.Ref()), pushRef)
			pushRepo, _ := repository.ParseRef(pushRef)
			log.Infof("[PULL/PUSH] ANALYZE %s => %s", repo.Ref(), pushRef)
			username, password, _ := api.dockerClient.Config().GetCredentials(push.Registry)
			pushedTags, err := remote.FetchTags(pushRepo, username, password)
			if err != nil {
				if !strings.Contains(err.Error(), "404 Not Found") {
					done <- err
					return
				}
				log.Warnf("%s repo not found: %+s", fn(repo.Ref()), pushRef)
				pushedTags = make(map[string]*tag.Tag)
			}
			log.Debugf("%s pushed tags: %+v", fn(repo.Ref()), pushedTags)
			remoteTags := cn.TagMap(repo.Ref())
			log.Debugf("%s remote tags: %+v", fn(repo.Ref()), remoteTags)
			sortedKeys, tagNames, joinedTags := tag.Join(
				remoteTags,
				pushedTags,
				repo.Tags(),
			)
			log.Debugf("%s joined tags: %+v", fn(repo.Ref()), joinedTags)
			tagsToPush := make([]*tag.Tag, 0)
			for _, key := range sortedKeys {
				name := tagNames[key]
				tg := joinedTags[name]
				if tg.NeedsPush(push.UpdateChanged) {
					tagsToPush = append(tagsToPush, tg)
				}
			}
			log.Debugf("%s sending 'push' tags: %+v", fn(repo.Ref()), tagsToPush)
			tagc <- rtags{ref: repo.Ref(), tags: tagsToPush}
			done <- nil
			return
		}(repo, i, done)
		time.Sleep(api.config.WaitBetween)
	}
	if err := wait.Until(done); err != nil {
		return nil, err
	}
	tags := receiveTags(tagc)
	log.Debugf("%s 'push' tags: %+v", fn(), tags)
	return collection.New(refs, tags)
}
CollectPushTags blends passed collection with information fetched from [local] "push" registry,
makes required comparisons between them and spits organized info back as collection.Collection
package v1
import (
	"bufio"
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"html/template"
	"io"
	"regexp"
	"runtime"
	"strings"
	"time"
	"github.com/Masterminds/sprig/v3"
	log "github.com/sirupsen/logrus"
	"github.com/ivanilves/lstags/api/v1/collection"
	"github.com/ivanilves/lstags/api/v1/registry/client/cache"
	dockerclient "github.com/ivanilves/lstags/docker/client"
	dockerconfig "github.com/ivanilves/lstags/docker/config"
	"github.com/ivanilves/lstags/repository"
	"github.com/ivanilves/lstags/tag"
	"github.com/ivanilves/lstags/tag/local"
	"github.com/ivanilves/lstags/tag/remote"
	"github.com/ivanilves/lstags/util/wait"
)
type Config struct {
	DockerJSONConfigFile string
	ConcurrentRequests int
	WaitBetween time.Duration
	TraceRequests bool
	RetryRequests int
	RetryDelay time.Duration
	InsecureRegistryEx string
	VerboseLogging bool
	DryRun bool
}
type PushConfig struct {
	Prefix string
	Registry string
	UpdateChanged bool
	PathSeparator string
	PathTemplate string
	TagTemplate string
}
type API struct {
	config       Config
	dockerClient *dockerclient.DockerClient
}
type rtags struct {
	ref  string
	tags []*tag.Tag
}
func fn(labels ...string) string {
	function, _, _, _ := runtime.Caller(1)
	longname := runtime.FuncForPC(function).Name()
	nameparts := strings.Split(longname, ".")
	shortname := nameparts[len(nameparts)-1]
	if labels == nil {
		return fmt.Sprintf("[%s()]", shortname)
	}
	return fmt.Sprintf("[%s():%s]", shortname, strings.Join(labels, ":"))
}
func getBatchedSlices(batchSize int, unbatched ...string) [][]string {
	batchedSlices := make([][]string, 0)
	index := 0
	for range unbatched {
		batchedSlice := make([]string, 0)
		for c := 0; c < batchSize; c++ {
			batchedSlice = append(batchedSlice, unbatched[index])
			index++
			if index == len(unbatched) {
				break
			}
		}
		batchedSlices = append(batchedSlices, batchedSlice)
		if index == len(unbatched) {
			break
		}
	}
	return batchedSlices
}
func receiveTags(tagc chan rtags) map[string][]*tag.Tag {
	tags := make(map[string][]*tag.Tag)
	step := 1
	size := cap(tagc)
	for t := range tagc {
		log.Debugf("[%s] receiving tags: %+v", t.ref, t.tags)
		tags[t.ref] = t.tags
		if step >= size {
			close(tagc)
		}
		step++
	}
	return tags
}
func (api *API) CollectTags(refs ...string) (*collection.Collection, error) {
	if len(refs) == 0 {
		return nil, fmt.Errorf("no image references passed")
	}
	_, err := repository.ParseRefs(refs)
	if err != nil {
		return nil, err
	}
	tagc := make(chan rtags, len(refs))
	batchedSlicesOfRefs := getBatchedSlices(api.config.ConcurrentRequests, refs...)
	for bindex, brefs := range batchedSlicesOfRefs {
		log.Infof("BATCH %d of %d", bindex+1, len(batchedSlicesOfRefs))
		log.Debugf("%s references: %+v", fn(), brefs)
		repos, _ := repository.ParseRefs(brefs)
		for _, repo := range repos {
			log.Debugf("%s repository: %+v", fn(), repo)
		}
		done := make(chan error, len(repos))
		for _, repo := range repos {
			go func(repo *repository.Repository, done chan error) {
				log.Infof("ANALYZE %s", repo.Ref())
				username, password, _ := api.dockerClient.Config().GetCredentials(repo.Registry())
				remoteTags, err := remote.FetchTags(repo, username, password)
				if err != nil {
					done <- err
					return
				}
				log.Debugf("%s remote tags: %+v", fn(repo.Ref()), remoteTags)
				localTags, _ := local.FetchTags(repo, api.dockerClient)
				log.Debugf("%s local tags: %+v", fn(repo.Ref()), localTags)
				sortedKeys, tagNames, joinedTags := tag.Join(
					remoteTags,
					localTags,
					repo.Tags(),
				)
				log.Debugf("%s sending joined tags: %+v", fn(repo.Ref()), joinedTags)
				tagc <- rtags{ref: repo.Ref(), tags: tag.Collect(sortedKeys, tagNames, joinedTags)}
				done <- nil
				log.Infof("FETCHED %s", repo.Ref())
				return
			}(repo, done)
		}
		if err := wait.Until(done); err != nil {
			return nil, err
		}
		time.Sleep(api.config.WaitBetween)
	}
	tags := receiveTags(tagc)
	log.Debugf("%s tags: %+v", fn(), tags)
	return collection.New(refs, tags)
}
func getPushPrefix(prefix, defaultPrefix string) string {
	if prefix == "" {
		return defaultPrefix
	}
	if prefix[0:1] != "/" {
		prefix = "/" + prefix
	}
	if prefix[len(prefix)-1:] != "/" {
		prefix = prefix + "/"
	}
	return prefix
}
func validatePushPrefix(prefix string) error {
	const ex = `^/[a-z0-9_][a-z0-9_\-\.\/]+/$`
	if prefix == "/" {
		return nil
	}
	matched, err := regexp.MatchString(ex, prefix)
	if err != nil {
		return err
	}
	if !matched {
		return fmt.Errorf("Push prefix \"%s\" does not match valid pattern: %s", prefix, ex)
	}
	return nil
}