feat(vendor): --exclude plus content-type selection (--types/--exclude-types)
vendor add gains --exclude a,b,c (all mode minus those ids) as the mirror of --include, for taking the majority of a source while dropping a few items. Mutual exclusion with --include is a hard error, unknown ids fail naming the available ones (generalized checkIDEntries). vendor add and vendor select gain --types (allow-list) and --exclude-types (deny-list) over the four content types. The filter is stored as the optional, v2-compatible Selection.exclude_types field and survives vendor update: new upstream items of an excluded type never enter the work repository (unlike id expansion, which would silently re-admit them). --types/--exclude-types replace the filter on select, absent flags keep it, and --mode all clears it along with include/exclude. Also fixes materializeTree to create the staging directory for an empty selection — previously select-to-zero failed at the staging rename.
This commit is contained in:
1 parent
1ab97bff43
commit
0c779bc970
9 files changed
+533
-50
No files matched your search
Vendored
+117
-12
@@ -21,11 +21,14 @@ const externalDir = "external"
|
||||
|
||||
// AddOptions parameterize vendor add.
|
||||
type AddOptions struct {
|
||||
Name string
|
||||
URL string
|
||||
Ref string
|
||||
Include []string
|
||||
Renames map[string]string
|
||||
Name string
|
||||
URL string
|
||||
Ref string
|
||||
Include []string
|
||||
Exclude []string
|
||||
OnlyTypes []string
|
||||
ExcludeTypes []string
|
||||
Renames map[string]string
|
||||
}
|
||||
|
||||
// Add vendors a new source into the work repository at workDir: it clones,
|
||||
@@ -68,12 +71,29 @@ func Add(workDir string, opts AddOptions, stdout io.Writer) error {
|
||||
return err
|
||||
}
|
||||
|
||||
sel := lockfile.Selection{Mode: lockfile.ModeAll}
|
||||
if len(opts.Include) > 0 {
|
||||
sel = lockfile.Selection{Mode: lockfile.ModeInclude, Include: opts.Include}
|
||||
if err := checkIncludeEntries(items, opts.Include, opts.Name); err != nil {
|
||||
if len(opts.Include) > 0 && len(opts.Exclude) > 0 {
|
||||
return fmt.Errorf("--include and --exclude cannot be combined (a v2 selection is either include mode or all mode minus excludes)")
|
||||
}
|
||||
excludeTypes, err := TypeFilter(opts.OnlyTypes, opts.ExcludeTypes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sel := lockfile.Selection{Mode: lockfile.ModeAll, ExcludeTypes: excludeTypes}
|
||||
switch {
|
||||
case len(opts.Include) > 0:
|
||||
sel = lockfile.Selection{Mode: lockfile.ModeInclude, Include: opts.Include, ExcludeTypes: excludeTypes}
|
||||
if err := checkIDEntries(items, opts.Include, opts.Name, "include"); err != nil {
|
||||
return err
|
||||
}
|
||||
case len(opts.Exclude) > 0:
|
||||
sel.Exclude = opts.Exclude
|
||||
if err := checkIDEntries(items, opts.Exclude, opts.Name, "exclude"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := checkIncludeVsTypes(items, sel, opts.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
selected := applySelection(items, sel)
|
||||
@@ -147,21 +167,96 @@ func discoveryFromTreeRoot(root string) lockfile.Discovery {
|
||||
return d
|
||||
}
|
||||
|
||||
func checkIncludeEntries(items []discovery.Item, include []string, name string) error {
|
||||
// checkIDEntries fails when any id is not available upstream (not discovered
|
||||
// or skipped) — typo protection for include and exclude lists alike.
|
||||
func checkIDEntries(items []discovery.Item, ids []string, name, flag string) error {
|
||||
available := map[string]bool{}
|
||||
for _, it := range items {
|
||||
if !it.Skipped {
|
||||
available[it.UpstreamID] = true
|
||||
}
|
||||
}
|
||||
for _, want := range include {
|
||||
for _, want := range ids {
|
||||
if !available[want] {
|
||||
return fmt.Errorf("source %q: include entry %q not found upstream (available: %s)", name, want, strings.Join(sortedKeys(available), ", "))
|
||||
return fmt.Errorf("source %q: %s entry %q not found upstream (available: %s)", name, flag, want, strings.Join(sortedKeys(available), ", "))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkIncludeVsTypes fails when an include id belongs to a type excluded by
|
||||
// the type filter — silently dropping it would betray the explicit include.
|
||||
func checkIncludeVsTypes(items []discovery.Item, sel lockfile.Selection, name string) error {
|
||||
if sel.Mode != lockfile.ModeInclude || len(sel.ExcludeTypes) == 0 {
|
||||
return nil
|
||||
}
|
||||
excluded := map[string]bool{}
|
||||
for _, t := range sel.ExcludeTypes {
|
||||
excluded[t] = true
|
||||
}
|
||||
for _, it := range items {
|
||||
if it.Skipped || !excluded[it.Type] {
|
||||
continue
|
||||
}
|
||||
for _, id := range sel.Include {
|
||||
if id == it.UpstreamID {
|
||||
return fmt.Errorf("source %q: include entry %q belongs to type %s, which the type filter excludes", name, id, it.Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TypeFilter translates --types (allow-list) and --exclude-types (deny-list)
|
||||
// into the canonical exclude_types list: the complement of the allow-list,
|
||||
// or the deny-list verbatim. Both empty means no filter (nil). A filter that
|
||||
// would exclude nothing normalizes to nil; a filter excluding everything is
|
||||
// returned as-is and fails later at the zero-selection guard.
|
||||
func TypeFilter(only, exclude []string) ([]string, error) {
|
||||
if len(only) > 0 && len(exclude) > 0 {
|
||||
return nil, fmt.Errorf("--types and --exclude-types cannot be combined")
|
||||
}
|
||||
if len(only) == 0 && len(exclude) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
names := only
|
||||
if len(names) == 0 {
|
||||
names = exclude
|
||||
}
|
||||
valid := map[string]bool{}
|
||||
for _, t := range lockfile.Types {
|
||||
valid[t] = true
|
||||
}
|
||||
var unknown []string
|
||||
for _, t := range names {
|
||||
if !valid[t] {
|
||||
unknown = append(unknown, t)
|
||||
}
|
||||
}
|
||||
if len(unknown) > 0 {
|
||||
return nil, fmt.Errorf("unknown content type(s) %s (valid: %s)", strings.Join(unknown, ", "), strings.Join(lockfile.Types, ", "))
|
||||
}
|
||||
if len(exclude) > 0 {
|
||||
out := append([]string{}, exclude...)
|
||||
sort.Strings(out)
|
||||
return out, nil
|
||||
}
|
||||
allowed := map[string]bool{}
|
||||
for _, t := range only {
|
||||
allowed[t] = true
|
||||
}
|
||||
var out []string
|
||||
for _, t := range lockfile.Types {
|
||||
if !allowed[t] {
|
||||
out = append(out, t)
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func applySelection(items []discovery.Item, sel lockfile.Selection) []discovery.Item {
|
||||
exclude := map[string]bool{}
|
||||
for _, e := range sel.Exclude {
|
||||
@@ -171,11 +266,18 @@ func applySelection(items []discovery.Item, sel lockfile.Selection) []discovery.
|
||||
for _, i := range sel.Include {
|
||||
include[i] = true
|
||||
}
|
||||
excludedTypes := map[string]bool{}
|
||||
for _, t := range sel.ExcludeTypes {
|
||||
excludedTypes[t] = true
|
||||
}
|
||||
var out []discovery.Item
|
||||
for _, it := range items {
|
||||
if it.Skipped {
|
||||
continue
|
||||
}
|
||||
if excludedTypes[it.Type] {
|
||||
continue
|
||||
}
|
||||
switch sel.Mode {
|
||||
case lockfile.ModeInclude:
|
||||
if include[it.UpstreamID] {
|
||||
@@ -251,6 +353,9 @@ func reportAdd(w io.Writer, name string, src *lockfile.Source, discovered, selec
|
||||
fmt.Fprintf(w, " rev: %s\n", src.Rev)
|
||||
fmt.Fprintf(w, "discovered %d items (%s)\n", real, counts(discovered))
|
||||
fmt.Fprintf(w, "selected %d items (%s)\n", len(selected), counts(selected))
|
||||
if len(src.Selection.ExcludeTypes) > 0 {
|
||||
fmt.Fprintf(w, "type filter: excluding %s\n", strings.Join(src.Selection.ExcludeTypes, ", "))
|
||||
}
|
||||
fmt.Fprintf(w, "materialized under %s/\n", filepath.Join(externalDir, name))
|
||||
if len(src.Warnings) > 0 {
|
||||
fmt.Fprintf(w, "warnings:\n")
|
||||
|
||||
Reference in new issue
Block a user