github.com/docker-library/go-dockerlibrary@v0.0.0-20200821205225-669fbe5c1d52/manifest/rfc2822.go (about)

     1  package manifest
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"io"
     7  	"path"
     8  	"regexp"
     9  	"sort"
    10  	"strings"
    11  
    12  	"github.com/docker-library/go-dockerlibrary/architecture"
    13  	"github.com/docker-library/go-dockerlibrary/pkg/stripper"
    14  
    15  	"pault.ag/go/debian/control"
    16  )
    17  
    18  var (
    19  	GitCommitRegex = regexp.MustCompile(`^[0-9a-f]{1,64}$`)
    20  	GitFetchRegex  = regexp.MustCompile(`^refs/(heads|tags)/[^*?:]+$`)
    21  
    22  	// https://github.com/docker/distribution/blob/v2.7.1/reference/regexp.go#L37
    23  	ValidTagRegex = regexp.MustCompile(`^\w[\w.-]{0,127}$`)
    24  )
    25  
    26  type Manifest2822 struct {
    27  	Global  Manifest2822Entry
    28  	Entries []Manifest2822Entry
    29  }
    30  
    31  type Manifest2822Entry struct {
    32  	control.Paragraph
    33  
    34  	Maintainers []string `delim:"," strip:"\n\r\t "`
    35  
    36  	Tags       []string `delim:"," strip:"\n\r\t "`
    37  	SharedTags []string `delim:"," strip:"\n\r\t "`
    38  
    39  	Architectures []string `delim:"," strip:"\n\r\t "`
    40  
    41  	GitRepo   string
    42  	GitFetch  string
    43  	GitCommit string
    44  	Directory string
    45  	File      string
    46  
    47  	// architecture-specific versions of the above fields
    48  	ArchValues map[string]string
    49  	// "ARCH-FIELD: VALUE"
    50  	// ala, "s390x-GitCommit: deadbeef"
    51  	// (sourced from Paragraph.Values via .SeedArchValues())
    52  
    53  	Constraints []string `delim:"," strip:"\n\r\t "`
    54  }
    55  
    56  var (
    57  	DefaultArchitecture = "amd64"
    58  
    59  	DefaultManifestEntry = Manifest2822Entry{
    60  		Architectures: []string{DefaultArchitecture},
    61  
    62  		GitFetch:  "refs/heads/master",
    63  		Directory: ".",
    64  		File:      "Dockerfile",
    65  	}
    66  )
    67  
    68  func deepCopyStringsMap(a map[string]string) map[string]string {
    69  	b := map[string]string{}
    70  	for k, v := range a {
    71  		b[k] = v
    72  	}
    73  	return b
    74  }
    75  
    76  func (entry Manifest2822Entry) Clone() Manifest2822Entry {
    77  	// SLICES! grr
    78  	entry.Maintainers = append([]string{}, entry.Maintainers...)
    79  	entry.Tags = append([]string{}, entry.Tags...)
    80  	entry.SharedTags = append([]string{}, entry.SharedTags...)
    81  	entry.Architectures = append([]string{}, entry.Architectures...)
    82  	entry.Constraints = append([]string{}, entry.Constraints...)
    83  	// and MAPS, oh my
    84  	entry.ArchValues = deepCopyStringsMap(entry.ArchValues)
    85  	return entry
    86  }
    87  
    88  func (entry *Manifest2822Entry) SeedArchValues() {
    89  	for field, val := range entry.Paragraph.Values {
    90  		if strings.HasSuffix(field, "-GitRepo") || strings.HasSuffix(field, "-GitFetch") || strings.HasSuffix(field, "-GitCommit") || strings.HasSuffix(field, "-Directory") || strings.HasSuffix(field, "-File") {
    91  			entry.ArchValues[field] = val
    92  		}
    93  	}
    94  }
    95  func (entry *Manifest2822Entry) CleanDirectoryValues() {
    96  	entry.Directory = path.Clean(entry.Directory)
    97  	for field, val := range entry.ArchValues {
    98  		if strings.HasSuffix(field, "-Directory") && val != "" {
    99  			entry.ArchValues[field] = path.Clean(val)
   100  		}
   101  	}
   102  }
   103  
   104  const StringSeparator2822 = ", "
   105  
   106  func (entry Manifest2822Entry) MaintainersString() string {
   107  	return strings.Join(entry.Maintainers, StringSeparator2822)
   108  }
   109  
   110  func (entry Manifest2822Entry) TagsString() string {
   111  	return strings.Join(entry.Tags, StringSeparator2822)
   112  }
   113  
   114  func (entry Manifest2822Entry) SharedTagsString() string {
   115  	return strings.Join(entry.SharedTags, StringSeparator2822)
   116  }
   117  
   118  func (entry Manifest2822Entry) ArchitecturesString() string {
   119  	return strings.Join(entry.Architectures, StringSeparator2822)
   120  }
   121  
   122  func (entry Manifest2822Entry) ConstraintsString() string {
   123  	return strings.Join(entry.Constraints, StringSeparator2822)
   124  }
   125  
   126  // if this method returns "true", then a.Tags and b.Tags can safely be combined (for the purposes of building)
   127  func (a Manifest2822Entry) SameBuildArtifacts(b Manifest2822Entry) bool {
   128  	// check xxxarch-GitRepo, etc. fields for sameness first
   129  	for _, key := range append(a.archFields(), b.archFields()...) {
   130  		if a.ArchValues[key] != b.ArchValues[key] {
   131  			return false
   132  		}
   133  	}
   134  
   135  	return a.ArchitecturesString() == b.ArchitecturesString() && a.GitRepo == b.GitRepo && a.GitFetch == b.GitFetch && a.GitCommit == b.GitCommit && a.Directory == b.Directory && a.File == b.File && a.ConstraintsString() == b.ConstraintsString()
   136  }
   137  
   138  // returns a list of architecture-specific fields in an Entry
   139  func (entry Manifest2822Entry) archFields() []string {
   140  	ret := []string{}
   141  	for key, val := range entry.ArchValues {
   142  		if val != "" {
   143  			ret = append(ret, key)
   144  		}
   145  	}
   146  	sort.Strings(ret)
   147  	return ret
   148  }
   149  
   150  // returns a new Entry with any of the values that are equal to the values in "defaults" cleared
   151  func (entry Manifest2822Entry) ClearDefaults(defaults Manifest2822Entry) Manifest2822Entry {
   152  	entry = entry.Clone() // make absolutely certain we have a deep clone
   153  	if entry.MaintainersString() == defaults.MaintainersString() {
   154  		entry.Maintainers = nil
   155  	}
   156  	if entry.TagsString() == defaults.TagsString() {
   157  		entry.Tags = nil
   158  	}
   159  	if entry.SharedTagsString() == defaults.SharedTagsString() {
   160  		entry.SharedTags = nil
   161  	}
   162  	if entry.ArchitecturesString() == defaults.ArchitecturesString() {
   163  		entry.Architectures = nil
   164  	}
   165  	if entry.GitRepo == defaults.GitRepo {
   166  		entry.GitRepo = ""
   167  	}
   168  	if entry.GitFetch == defaults.GitFetch {
   169  		entry.GitFetch = ""
   170  	}
   171  	if entry.GitCommit == defaults.GitCommit {
   172  		entry.GitCommit = ""
   173  	}
   174  	if entry.Directory == defaults.Directory {
   175  		entry.Directory = ""
   176  	}
   177  	if entry.File == defaults.File {
   178  		entry.File = ""
   179  	}
   180  	for _, key := range defaults.archFields() {
   181  		if defaults.ArchValues[key] == entry.ArchValues[key] {
   182  			delete(entry.ArchValues, key)
   183  		}
   184  	}
   185  	if entry.ConstraintsString() == defaults.ConstraintsString() {
   186  		entry.Constraints = nil
   187  	}
   188  	return entry
   189  }
   190  
   191  func (entry Manifest2822Entry) String() string {
   192  	ret := []string{}
   193  	if str := entry.MaintainersString(); str != "" {
   194  		ret = append(ret, "Maintainers: "+str)
   195  	}
   196  	if str := entry.TagsString(); str != "" {
   197  		ret = append(ret, "Tags: "+str)
   198  	}
   199  	if str := entry.SharedTagsString(); str != "" {
   200  		ret = append(ret, "SharedTags: "+str)
   201  	}
   202  	if str := entry.ArchitecturesString(); str != "" {
   203  		ret = append(ret, "Architectures: "+str)
   204  	}
   205  	if str := entry.GitRepo; str != "" {
   206  		ret = append(ret, "GitRepo: "+str)
   207  	}
   208  	if str := entry.GitFetch; str != "" {
   209  		ret = append(ret, "GitFetch: "+str)
   210  	}
   211  	if str := entry.GitCommit; str != "" {
   212  		ret = append(ret, "GitCommit: "+str)
   213  	}
   214  	if str := entry.Directory; str != "" {
   215  		ret = append(ret, "Directory: "+str)
   216  	}
   217  	if str := entry.File; str != "" {
   218  		ret = append(ret, "File: "+str)
   219  	}
   220  	for _, key := range entry.archFields() {
   221  		ret = append(ret, key+": "+entry.ArchValues[key])
   222  	}
   223  	if str := entry.ConstraintsString(); str != "" {
   224  		ret = append(ret, "Constraints: "+str)
   225  	}
   226  	return strings.Join(ret, "\n")
   227  }
   228  
   229  func (manifest Manifest2822) String() string {
   230  	entries := []Manifest2822Entry{manifest.Global.ClearDefaults(DefaultManifestEntry)}
   231  	entries = append(entries, manifest.Entries...)
   232  
   233  	ret := []string{}
   234  	for i, entry := range entries {
   235  		if i > 0 {
   236  			entry = entry.ClearDefaults(manifest.Global)
   237  		}
   238  		ret = append(ret, entry.String())
   239  	}
   240  
   241  	return strings.Join(ret, "\n\n")
   242  }
   243  
   244  func (entry *Manifest2822Entry) SetGitRepo(arch string, repo string) {
   245  	if entry.ArchValues == nil {
   246  		entry.ArchValues = map[string]string{}
   247  	}
   248  	entry.ArchValues[arch+"-GitRepo"] = repo
   249  }
   250  
   251  func (entry Manifest2822Entry) ArchGitRepo(arch string) string {
   252  	if val, ok := entry.ArchValues[arch+"-GitRepo"]; ok && val != "" {
   253  		return val
   254  	}
   255  	return entry.GitRepo
   256  }
   257  
   258  func (entry Manifest2822Entry) ArchGitFetch(arch string) string {
   259  	if val, ok := entry.ArchValues[arch+"-GitFetch"]; ok && val != "" {
   260  		return val
   261  	}
   262  	return entry.GitFetch
   263  }
   264  
   265  func (entry *Manifest2822Entry) SetGitCommit(arch string, commit string) {
   266  	if entry.ArchValues == nil {
   267  		entry.ArchValues = map[string]string{}
   268  	}
   269  	entry.ArchValues[arch+"-GitCommit"] = commit
   270  }
   271  
   272  func (entry Manifest2822Entry) ArchGitCommit(arch string) string {
   273  	if val, ok := entry.ArchValues[arch+"-GitCommit"]; ok && val != "" {
   274  		return val
   275  	}
   276  	return entry.GitCommit
   277  }
   278  
   279  func (entry Manifest2822Entry) ArchDirectory(arch string) string {
   280  	if val, ok := entry.ArchValues[arch+"-Directory"]; ok && val != "" {
   281  		return val
   282  	}
   283  	return entry.Directory
   284  }
   285  
   286  func (entry Manifest2822Entry) ArchFile(arch string) string {
   287  	if val, ok := entry.ArchValues[arch+"-File"]; ok && val != "" {
   288  		return val
   289  	}
   290  	return entry.File
   291  }
   292  
   293  func (entry Manifest2822Entry) HasTag(tag string) bool {
   294  	for _, existingTag := range entry.Tags {
   295  		if tag == existingTag {
   296  			return true
   297  		}
   298  	}
   299  	return false
   300  }
   301  
   302  // HasSharedTag returns true if the given tag exists in entry.SharedTags.
   303  func (entry Manifest2822Entry) HasSharedTag(tag string) bool {
   304  	for _, existingTag := range entry.SharedTags {
   305  		if tag == existingTag {
   306  			return true
   307  		}
   308  	}
   309  	return false
   310  }
   311  
   312  // HasArchitecture returns true if the given architecture exists in entry.Architectures
   313  func (entry Manifest2822Entry) HasArchitecture(arch string) bool {
   314  	for _, existingArch := range entry.Architectures {
   315  		if arch == existingArch {
   316  			return true
   317  		}
   318  	}
   319  	return false
   320  }
   321  
   322  func (manifest Manifest2822) GetTag(tag string) *Manifest2822Entry {
   323  	for i, entry := range manifest.Entries {
   324  		if entry.HasTag(tag) {
   325  			return &manifest.Entries[i]
   326  		}
   327  	}
   328  	return nil
   329  }
   330  
   331  // GetSharedTag returns a list of entries with the given tag in entry.SharedTags (or the empty list if there are no entries with the given tag).
   332  func (manifest Manifest2822) GetSharedTag(tag string) []*Manifest2822Entry {
   333  	ret := []*Manifest2822Entry{}
   334  	for i, entry := range manifest.Entries {
   335  		if entry.HasSharedTag(tag) {
   336  			ret = append(ret, &manifest.Entries[i])
   337  		}
   338  	}
   339  	return ret
   340  }
   341  
   342  // GetAllSharedTags returns a list of the sum of all SharedTags in all entries of this image manifest (in the order they appear in the file).
   343  func (manifest Manifest2822) GetAllSharedTags() []string {
   344  	fakeEntry := Manifest2822Entry{}
   345  	for _, entry := range manifest.Entries {
   346  		fakeEntry.SharedTags = append(fakeEntry.SharedTags, entry.SharedTags...)
   347  	}
   348  	fakeEntry.DeduplicateSharedTags()
   349  	return fakeEntry.SharedTags
   350  }
   351  
   352  type SharedTagGroup struct {
   353  	SharedTags []string
   354  	Entries    []*Manifest2822Entry
   355  }
   356  
   357  // GetSharedTagGroups returns a map of shared tag groups to the list of entries they share (as described in https://github.com/docker-library/go-dockerlibrary/pull/2#issuecomment-277853597).
   358  func (manifest Manifest2822) GetSharedTagGroups() []SharedTagGroup {
   359  	inter := map[string][]string{}
   360  	interOrder := []string{} // order matters, and maps randomize order
   361  	interKeySep := ","
   362  	for _, sharedTag := range manifest.GetAllSharedTags() {
   363  		interKeyParts := []string{}
   364  		for _, entry := range manifest.GetSharedTag(sharedTag) {
   365  			interKeyParts = append(interKeyParts, entry.Tags[0])
   366  		}
   367  		interKey := strings.Join(interKeyParts, interKeySep)
   368  		if _, ok := inter[interKey]; !ok {
   369  			interOrder = append(interOrder, interKey)
   370  		}
   371  		inter[interKey] = append(inter[interKey], sharedTag)
   372  	}
   373  	ret := []SharedTagGroup{}
   374  	for _, tags := range interOrder {
   375  		group := SharedTagGroup{
   376  			SharedTags: inter[tags],
   377  			Entries:    []*Manifest2822Entry{},
   378  		}
   379  		for _, tag := range strings.Split(tags, interKeySep) {
   380  			group.Entries = append(group.Entries, manifest.GetTag(tag))
   381  		}
   382  		ret = append(ret, group)
   383  	}
   384  	return ret
   385  }
   386  
   387  func (manifest *Manifest2822) AddEntry(entry Manifest2822Entry) error {
   388  	if len(entry.Tags) < 1 {
   389  		return fmt.Errorf("missing Tags")
   390  	}
   391  	if entry.GitRepo == "" || entry.GitFetch == "" || entry.GitCommit == "" {
   392  		return fmt.Errorf("Tags %q missing one of GitRepo, GitFetch, or GitCommit", entry.TagsString())
   393  	}
   394  	if invalidMaintainers := entry.InvalidMaintainers(); len(invalidMaintainers) > 0 {
   395  		return fmt.Errorf("Tags %q has invalid Maintainers: %q (expected format %q)", entry.TagsString(), strings.Join(invalidMaintainers, ", "), MaintainersFormat)
   396  	}
   397  
   398  	entry.DeduplicateSharedTags()
   399  	entry.CleanDirectoryValues()
   400  
   401  	if invalidTags := entry.InvalidTags(); len(invalidTags) > 0 {
   402  		return fmt.Errorf("Tags %q has invalid (Shared)Tags: %q", entry.TagsString(), strings.Join(invalidTags, ", "))
   403  	}
   404  	if invalidArchitectures := entry.InvalidArchitectures(); len(invalidArchitectures) > 0 {
   405  		return fmt.Errorf("Tags %q has invalid Architectures: %q", entry.TagsString(), strings.Join(invalidArchitectures, ", "))
   406  	}
   407  
   408  	seenTag := map[string]bool{}
   409  	for _, tag := range entry.Tags {
   410  		if otherEntry := manifest.GetTag(tag); otherEntry != nil {
   411  			return fmt.Errorf("Tags %q includes duplicate tag: %q (duplicated in %q)", entry.TagsString(), tag, otherEntry.TagsString())
   412  		}
   413  		if otherEntries := manifest.GetSharedTag(tag); len(otherEntries) > 0 {
   414  			return fmt.Errorf("Tags %q includes tag conflicting with a shared tag: %q (shared tag in %q)", entry.TagsString(), tag, otherEntries[0].TagsString())
   415  		}
   416  		if seenTag[tag] {
   417  			return fmt.Errorf("Tags %q includes duplicate tag: %q", entry.TagsString(), tag)
   418  		}
   419  		seenTag[tag] = true
   420  	}
   421  	for _, tag := range entry.SharedTags {
   422  		if otherEntry := manifest.GetTag(tag); otherEntry != nil {
   423  			return fmt.Errorf("Tags %q includes conflicting shared tag: %q (duplicated in %q)", entry.TagsString(), tag, otherEntry.TagsString())
   424  		}
   425  		if seenTag[tag] {
   426  			return fmt.Errorf("Tags %q includes duplicate tag: %q (in SharedTags)", entry.TagsString(), tag)
   427  		}
   428  		seenTag[tag] = true
   429  	}
   430  
   431  	for i, existingEntry := range manifest.Entries {
   432  		if existingEntry.SameBuildArtifacts(entry) {
   433  			manifest.Entries[i].Tags = append(existingEntry.Tags, entry.Tags...)
   434  			manifest.Entries[i].SharedTags = append(existingEntry.SharedTags, entry.SharedTags...)
   435  			manifest.Entries[i].DeduplicateSharedTags()
   436  			return nil
   437  		}
   438  	}
   439  
   440  	manifest.Entries = append(manifest.Entries, entry)
   441  
   442  	return nil
   443  }
   444  
   445  const (
   446  	MaintainersNameRegex   = `[^\s<>()][^<>()]*`
   447  	MaintainersEmailRegex  = `[^\s<>()]+`
   448  	MaintainersGitHubRegex = `[^\s<>()]+`
   449  
   450  	MaintainersFormat = `Full Name <contact-email-or-url> (@github-handle) OR Full Name (@github-handle)`
   451  )
   452  
   453  var (
   454  	MaintainersRegex = regexp.MustCompile(`^(` + MaintainersNameRegex + `)(?:\s+<(` + MaintainersEmailRegex + `)>)?\s+[(]@(` + MaintainersGitHubRegex + `)[)]$`)
   455  )
   456  
   457  func (entry Manifest2822Entry) InvalidMaintainers() []string {
   458  	invalid := []string{}
   459  	for _, maintainer := range entry.Maintainers {
   460  		if !MaintainersRegex.MatchString(maintainer) {
   461  			invalid = append(invalid, maintainer)
   462  		}
   463  	}
   464  	return invalid
   465  }
   466  
   467  func (entry Manifest2822Entry) InvalidTags() []string {
   468  	invalid := []string{}
   469  	for _, tag := range append(append([]string{}, entry.Tags...), entry.SharedTags...) {
   470  		if !ValidTagRegex.MatchString(tag) {
   471  			invalid = append(invalid, tag)
   472  		}
   473  	}
   474  	return invalid
   475  }
   476  
   477  func (entry Manifest2822Entry) InvalidArchitectures() []string {
   478  	invalid := []string{}
   479  	for _, arch := range entry.Architectures {
   480  		if _, ok := architecture.SupportedArches[arch]; !ok {
   481  			invalid = append(invalid, arch)
   482  		}
   483  	}
   484  	return invalid
   485  }
   486  
   487  // DeduplicateSharedTags will remove duplicate values from entry.SharedTags, preserving order.
   488  func (entry *Manifest2822Entry) DeduplicateSharedTags() {
   489  	aggregate := []string{}
   490  	seen := map[string]bool{}
   491  	for _, tag := range entry.SharedTags {
   492  		if seen[tag] {
   493  			continue
   494  		}
   495  		seen[tag] = true
   496  		aggregate = append(aggregate, tag)
   497  	}
   498  	entry.SharedTags = aggregate
   499  }
   500  
   501  // DeduplicateArchitectures will remove duplicate values from entry.Architectures and sort the result.
   502  func (entry *Manifest2822Entry) DeduplicateArchitectures() {
   503  	aggregate := []string{}
   504  	seen := map[string]bool{}
   505  	for _, arch := range entry.Architectures {
   506  		if seen[arch] {
   507  			continue
   508  		}
   509  		seen[arch] = true
   510  		aggregate = append(aggregate, arch)
   511  	}
   512  	sort.Strings(aggregate)
   513  	entry.Architectures = aggregate
   514  }
   515  
   516  type decoderWrapper struct {
   517  	*control.Decoder
   518  }
   519  
   520  func (decoder *decoderWrapper) Decode(entry *Manifest2822Entry) error {
   521  	// reset Architectures and SharedTags so that they can be either inherited or replaced, not additive
   522  	sharedTags := entry.SharedTags
   523  	entry.SharedTags = nil
   524  	arches := entry.Architectures
   525  	entry.Architectures = nil
   526  
   527  	for {
   528  		err := decoder.Decoder.Decode(entry)
   529  		if err != nil {
   530  			return err
   531  		}
   532  
   533  		// ignore empty paragraphs (blank lines at the start, excess blank lines between paragraphs, excess blank lines at EOF)
   534  		if len(entry.Paragraph.Order) == 0 {
   535  			continue
   536  		}
   537  
   538  		// if we had no SharedTags or Architectures, restore our "default" (original) values
   539  		if len(entry.SharedTags) == 0 {
   540  			entry.SharedTags = sharedTags
   541  		}
   542  		if len(entry.Architectures) == 0 {
   543  			entry.Architectures = arches
   544  		}
   545  		entry.DeduplicateArchitectures()
   546  
   547  		// pull out any new architecture-specific values from Paragraph.Values
   548  		entry.SeedArchValues()
   549  
   550  		return nil
   551  	}
   552  }
   553  
   554  func Parse2822(readerIn io.Reader) (*Manifest2822, error) {
   555  	reader := stripper.NewCommentStripper(readerIn)
   556  
   557  	realDecoder, err := control.NewDecoder(bufio.NewReader(reader), nil)
   558  	if err != nil {
   559  		return nil, err
   560  	}
   561  	decoder := decoderWrapper{realDecoder}
   562  
   563  	manifest := Manifest2822{
   564  		Global: DefaultManifestEntry.Clone(),
   565  	}
   566  
   567  	if err := decoder.Decode(&manifest.Global); err != nil {
   568  		return nil, err
   569  	}
   570  	if len(manifest.Global.Maintainers) < 1 {
   571  		return nil, fmt.Errorf("missing Maintainers")
   572  	}
   573  	if invalidMaintainers := manifest.Global.InvalidMaintainers(); len(invalidMaintainers) > 0 {
   574  		return nil, fmt.Errorf("invalid Maintainers: %q (expected format %q)", strings.Join(invalidMaintainers, ", "), MaintainersFormat)
   575  	}
   576  	if len(manifest.Global.Tags) > 0 {
   577  		return nil, fmt.Errorf("global Tags not permitted")
   578  	}
   579  	if invalidArchitectures := manifest.Global.InvalidArchitectures(); len(invalidArchitectures) > 0 {
   580  		return nil, fmt.Errorf("invalid global Architectures: %q", strings.Join(invalidArchitectures, ", "))
   581  	}
   582  
   583  	for {
   584  		entry := manifest.Global.Clone()
   585  
   586  		err := decoder.Decode(&entry)
   587  		if err == io.EOF {
   588  			break
   589  		}
   590  		if err != nil {
   591  			return nil, err
   592  		}
   593  
   594  		if !GitFetchRegex.MatchString(entry.GitFetch) {
   595  			return nil, fmt.Errorf(`Tags %q has invalid GitFetch (must be "refs/heads/..." or "refs/tags/..."): %q`, entry.TagsString(), entry.GitFetch)
   596  		}
   597  		if !GitCommitRegex.MatchString(entry.GitCommit) {
   598  			return nil, fmt.Errorf(`Tags %q has invalid GitCommit (must be a commit, not a tag or ref): %q`, entry.TagsString(), entry.GitCommit)
   599  		}
   600  
   601  		err = manifest.AddEntry(entry)
   602  		if err != nil {
   603  			return nil, err
   604  		}
   605  	}
   606  
   607  	return &manifest, nil
   608  }