710 lines
22 KiB
Go
710 lines
22 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
package interfacesnapshot
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"reflect"
|
|
"strings"
|
|
)
|
|
|
|
const FlagMigrationManifestVersion = 1
|
|
|
|
const (
|
|
FlagMigrationPending = "pending"
|
|
FlagMigrationConsumed = "consumed"
|
|
)
|
|
|
|
type FlagMigrationManifest struct {
|
|
Version int `json:"version"`
|
|
Migrations []FlagMigration `json:"migrations"`
|
|
}
|
|
|
|
type FlagMigration struct {
|
|
Command string `json:"command"`
|
|
Legacy FlagMigrationSide `json:"legacy"`
|
|
Canonical FlagMigrationSide `json:"canonical"`
|
|
State string `json:"state"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
|
|
type FlagMigrationSide struct {
|
|
Name string `json:"name"`
|
|
Before FlagMigrationState `json:"before"`
|
|
After FlagMigrationState `json:"after"`
|
|
}
|
|
|
|
type FlagMigrationState struct {
|
|
Present bool `json:"present"`
|
|
Type string `json:"type,omitempty"`
|
|
Required bool `json:"required,omitempty"`
|
|
Hidden bool `json:"hidden,omitempty"`
|
|
Shorthand string `json:"shorthand,omitempty"`
|
|
NoOpt string `json:"no_opt,omitempty"`
|
|
Scope string `json:"scope,omitempty"`
|
|
AliasOf string `json:"alias_of,omitempty"`
|
|
}
|
|
|
|
func ReadFlagMigrationManifest(r io.Reader) (FlagMigrationManifest, error) {
|
|
data, err := io.ReadAll(r)
|
|
if err != nil {
|
|
return FlagMigrationManifest{}, fmt.Errorf("read flag migration manifest: %w", err)
|
|
}
|
|
var manifest FlagMigrationManifest
|
|
decoder := json.NewDecoder(bytes.NewReader(data))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(&manifest); err != nil {
|
|
return FlagMigrationManifest{}, err
|
|
}
|
|
if err := decoder.Decode(&struct{}{}); err != io.EOF {
|
|
if err == nil {
|
|
return FlagMigrationManifest{}, fmt.Errorf("flag migration manifest contains trailing multiple JSON values")
|
|
}
|
|
return FlagMigrationManifest{}, fmt.Errorf("read trailing flag migration manifest data: %w", err)
|
|
}
|
|
if err := validateFlagMigrationJSONSchema(data); err != nil {
|
|
return FlagMigrationManifest{}, err
|
|
}
|
|
if err := manifest.Validate(); err != nil {
|
|
return FlagMigrationManifest{}, err
|
|
}
|
|
return manifest, nil
|
|
}
|
|
|
|
func validateFlagMigrationJSONSchema(data []byte) error {
|
|
decoder := json.NewDecoder(bytes.NewReader(data))
|
|
decoder.UseNumber()
|
|
if err := validateMigrationJSONValue(decoder, "$", reflect.TypeOf(FlagMigrationManifest{})); err != nil {
|
|
return err
|
|
}
|
|
if token, err := decoder.Token(); err != io.EOF {
|
|
if err == nil {
|
|
return fmt.Errorf("flag migration manifest contains trailing JSON value %v", token)
|
|
}
|
|
return fmt.Errorf("read trailing flag migration manifest data: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateMigrationJSONValue(decoder *json.Decoder, path string, schema reflect.Type) error {
|
|
token, err := decoder.Token()
|
|
if err != nil {
|
|
return fmt.Errorf("read flag migration manifest value at %s: %w", path, err)
|
|
}
|
|
|
|
switch schema.Kind() {
|
|
case reflect.Struct:
|
|
if delimiter, ok := token.(json.Delim); !ok || delimiter != '{' {
|
|
return migrationJSONTypeError(path, schema, token)
|
|
}
|
|
fields := migrationJSONFields(schema)
|
|
seen := make(map[string]bool, len(fields))
|
|
for decoder.More() {
|
|
keyToken, keyErr := decoder.Token()
|
|
if keyErr != nil {
|
|
return fmt.Errorf("read flag migration manifest field at %s: %w", path, keyErr)
|
|
}
|
|
// encoding/json guarantees object member names are string tokens.
|
|
key := keyToken.(string)
|
|
fieldSchema, exists := fields[key]
|
|
if !exists {
|
|
for canonical := range fields {
|
|
if strings.EqualFold(key, canonical) {
|
|
return fmt.Errorf(
|
|
"flag migration manifest contains non-canonical field %q at %s (want %q)",
|
|
key,
|
|
path,
|
|
canonical,
|
|
)
|
|
}
|
|
}
|
|
return fmt.Errorf("flag migration manifest contains unknown field %q at %s", key, path)
|
|
}
|
|
if seen[key] {
|
|
return fmt.Errorf("flag migration manifest contains duplicate field %q at %s", key, path)
|
|
}
|
|
seen[key] = true
|
|
if err := validateMigrationJSONValue(decoder, path+"."+key, fieldSchema); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if _, closeErr := decoder.Token(); closeErr != nil {
|
|
return fmt.Errorf("close flag migration manifest object at %s: %w", path, closeErr)
|
|
}
|
|
return nil
|
|
case reflect.Slice:
|
|
if delimiter, ok := token.(json.Delim); !ok || delimiter != '[' {
|
|
return migrationJSONTypeError(path, schema, token)
|
|
}
|
|
for index := 0; decoder.More(); index++ {
|
|
if err := validateMigrationJSONValue(decoder, fmt.Sprintf("%s[%d]", path, index), schema.Elem()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if _, closeErr := decoder.Token(); closeErr != nil {
|
|
return fmt.Errorf("close flag migration manifest array at %s: %w", path, closeErr)
|
|
}
|
|
return nil
|
|
case reflect.String:
|
|
if _, ok := token.(string); !ok {
|
|
return migrationJSONTypeError(path, schema, token)
|
|
}
|
|
return nil
|
|
case reflect.Int:
|
|
if _, ok := token.(json.Number); !ok {
|
|
return migrationJSONTypeError(path, schema, token)
|
|
}
|
|
return nil
|
|
case reflect.Bool:
|
|
if _, ok := token.(bool); !ok {
|
|
return migrationJSONTypeError(path, schema, token)
|
|
}
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("flag migration manifest value at %s has unsupported Go schema type %s", path, schema)
|
|
}
|
|
}
|
|
|
|
func migrationJSONFields(schema reflect.Type) map[string]reflect.Type {
|
|
fields := make(map[string]reflect.Type, schema.NumField())
|
|
for index := 0; index < schema.NumField(); index++ {
|
|
field := schema.Field(index)
|
|
name := field.Tag.Get("json")
|
|
if comma := strings.IndexByte(name, ','); comma >= 0 {
|
|
name = name[:comma]
|
|
}
|
|
if name == "-" {
|
|
continue
|
|
}
|
|
if name == "" {
|
|
name = field.Name
|
|
}
|
|
fields[name] = field.Type
|
|
}
|
|
return fields
|
|
}
|
|
|
|
func migrationJSONTypeError(path string, want reflect.Type, token json.Token) error {
|
|
return fmt.Errorf(
|
|
"flag migration manifest value at %s must be %s, got %s",
|
|
path,
|
|
migrationJSONKindDescription(want),
|
|
migrationJSONTokenDescription(token),
|
|
)
|
|
}
|
|
|
|
func migrationJSONKindDescription(schema reflect.Type) string {
|
|
switch schema.Kind() {
|
|
case reflect.Struct:
|
|
return "an object"
|
|
case reflect.Slice:
|
|
return "an array"
|
|
case reflect.String:
|
|
return "a string"
|
|
case reflect.Int:
|
|
return "a number"
|
|
case reflect.Bool:
|
|
return "a boolean"
|
|
default:
|
|
return "the declared JSON type"
|
|
}
|
|
}
|
|
|
|
func migrationJSONTokenDescription(token json.Token) string {
|
|
if token == nil {
|
|
return "null"
|
|
}
|
|
if delimiter, ok := token.(json.Delim); ok {
|
|
switch delimiter {
|
|
case '{':
|
|
return "object"
|
|
case '[':
|
|
return "array"
|
|
default:
|
|
return fmt.Sprintf("delimiter %q", delimiter)
|
|
}
|
|
}
|
|
switch token.(type) {
|
|
case string:
|
|
return "string"
|
|
case json.Number, float64:
|
|
return "number"
|
|
case bool:
|
|
return "boolean"
|
|
default:
|
|
return fmt.Sprintf("%T", token)
|
|
}
|
|
}
|
|
|
|
func (m FlagMigrationManifest) Validate() error {
|
|
if m.Version != FlagMigrationManifestVersion {
|
|
return fmt.Errorf(
|
|
"unsupported flag migration manifest version %d (want %d)",
|
|
m.Version,
|
|
FlagMigrationManifestVersion,
|
|
)
|
|
}
|
|
if m.Migrations == nil {
|
|
return fmt.Errorf("flag migration manifest migrations must be an array")
|
|
}
|
|
seen := make(map[string]bool, len(m.Migrations))
|
|
legacyTargets := make(map[string]string, len(m.Migrations))
|
|
for index, migration := range m.Migrations {
|
|
if err := migration.validate(); err != nil {
|
|
return fmt.Errorf("flag migration %d: %w", index, err)
|
|
}
|
|
key := migration.key()
|
|
if seen[key] {
|
|
return fmt.Errorf("flag migration %d duplicates %s", index, key)
|
|
}
|
|
seen[key] = true
|
|
legacyKey := migration.Command + "\x00" + migration.Legacy.Name
|
|
if canonical, exists := legacyTargets[legacyKey]; exists && canonical != migration.Canonical.Name {
|
|
return fmt.Errorf(
|
|
"flag migration %d maps %s --%s to both --%s and --%s",
|
|
index,
|
|
migration.Command,
|
|
migration.Legacy.Name,
|
|
canonical,
|
|
migration.Canonical.Name,
|
|
)
|
|
}
|
|
legacyTargets[legacyKey] = migration.Canonical.Name
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m FlagMigration) validate() error {
|
|
if !isExactCommandPath(m.Command) {
|
|
return fmt.Errorf("command must be an exact command path rooted at dws: %q", m.Command)
|
|
}
|
|
if !isExactFlagName(m.Legacy.Name) {
|
|
return fmt.Errorf("legacy name must be an exact legacy flag: %q", m.Legacy.Name)
|
|
}
|
|
if !isExactFlagName(m.Canonical.Name) {
|
|
return fmt.Errorf("canonical name must be an exact canonical flag: %q", m.Canonical.Name)
|
|
}
|
|
if m.Legacy.Name == m.Canonical.Name {
|
|
return fmt.Errorf("legacy and canonical flags must differ: --%s", m.Legacy.Name)
|
|
}
|
|
if strings.TrimSpace(m.Reason) == "" {
|
|
return fmt.Errorf("migration must include a non-empty reason")
|
|
}
|
|
if m.Reason != strings.TrimSpace(m.Reason) {
|
|
return fmt.Errorf("migration reason must be trimmed")
|
|
}
|
|
if m.State != FlagMigrationPending && m.State != FlagMigrationConsumed {
|
|
return fmt.Errorf("invalid state %q", m.State)
|
|
}
|
|
if err := m.Legacy.Before.validate("legacy before"); err != nil {
|
|
return err
|
|
}
|
|
if err := m.Legacy.After.validate("legacy after"); err != nil {
|
|
return err
|
|
}
|
|
if err := m.Canonical.Before.validate("canonical before"); err != nil {
|
|
return err
|
|
}
|
|
if err := m.Canonical.After.validate("canonical after"); err != nil {
|
|
return err
|
|
}
|
|
if !m.Legacy.Before.Present || !m.Legacy.After.Present {
|
|
return fmt.Errorf("legacy flag must remain present before and after migration")
|
|
}
|
|
if m.Legacy.Before.Hidden || !m.Legacy.After.Hidden {
|
|
return fmt.Errorf("legacy flag must migrate exactly from visible to hidden")
|
|
}
|
|
if !m.Canonical.After.Present {
|
|
return fmt.Errorf("canonical flag must be present after migration")
|
|
}
|
|
if m.Canonical.After.Hidden {
|
|
return fmt.Errorf("canonical flag must remain visible")
|
|
}
|
|
// Requiredness belongs to the one logical parameter. The hidden legacy
|
|
// spelling must not remain independently required, while the canonical
|
|
// spelling inherits the exact before-state contract. An already-visible
|
|
// canonical flag cannot change requiredness; an existing hidden canonical
|
|
// placeholder may inherit it when promoted to the public spelling.
|
|
if m.Legacy.After.Required {
|
|
return fmt.Errorf("legacy compatibility alias must not remain independently required after migration")
|
|
}
|
|
if m.Legacy.Before.Required != m.Canonical.After.Required {
|
|
return fmt.Errorf(
|
|
"flag requiredness must be preserved from legacy before to canonical after",
|
|
)
|
|
}
|
|
if m.Canonical.Before.Present &&
|
|
!m.Canonical.Before.Hidden &&
|
|
m.Canonical.Before.Required != m.Canonical.After.Required {
|
|
return fmt.Errorf("canonical flag requiredness must remain unchanged when already present")
|
|
}
|
|
if m.Legacy.After.AliasOf != m.Canonical.Name {
|
|
return fmt.Errorf(
|
|
"legacy flag after state must declare alias_of %q",
|
|
m.Canonical.Name,
|
|
)
|
|
}
|
|
if m.Legacy.Before.AliasOf != "" || m.Canonical.Before.AliasOf != "" || m.Canonical.After.AliasOf != "" {
|
|
return fmt.Errorf("alias_of is only valid on the legacy after state")
|
|
}
|
|
if m.Canonical.Before.Present {
|
|
if m.Canonical.Before.Type != m.Canonical.After.Type {
|
|
return fmt.Errorf("canonical flag type must remain unchanged")
|
|
}
|
|
if m.Canonical.Before.Shorthand != m.Canonical.After.Shorthand {
|
|
return fmt.Errorf("canonical flag shorthand must remain unchanged")
|
|
}
|
|
if m.Canonical.Before.NoOpt != m.Canonical.After.NoOpt {
|
|
return fmt.Errorf("canonical flag no_opt must remain unchanged")
|
|
}
|
|
if m.Canonical.Before.Scope != m.Canonical.After.Scope {
|
|
return fmt.Errorf("canonical flag scope must remain unchanged")
|
|
}
|
|
}
|
|
if m.Legacy.Before.Type != m.Legacy.After.Type || m.Legacy.After.Type != m.Canonical.After.Type {
|
|
return fmt.Errorf("legacy and canonical flag types must match exactly")
|
|
}
|
|
if m.Legacy.Before.Shorthand != m.Legacy.After.Shorthand {
|
|
return fmt.Errorf("legacy flag shorthand must remain unchanged")
|
|
}
|
|
if m.Legacy.Before.NoOpt != m.Legacy.After.NoOpt {
|
|
return fmt.Errorf("legacy flag no_opt must remain unchanged")
|
|
}
|
|
if m.Legacy.Before.Scope != m.Legacy.After.Scope {
|
|
return fmt.Errorf("legacy flag scope must remain unchanged")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s FlagMigrationState) validate(label string) error {
|
|
if !s.Present {
|
|
if s.Type != "" || s.Required || s.Hidden || s.Shorthand != "" || s.NoOpt != "" || s.Scope != "" || s.AliasOf != "" {
|
|
return fmt.Errorf("%s absent state must not declare flag attributes", label)
|
|
}
|
|
return nil
|
|
}
|
|
if strings.TrimSpace(s.Type) == "" {
|
|
return fmt.Errorf("%s present state requires type", label)
|
|
}
|
|
if s.Scope != "local" && s.Scope != "inherited" {
|
|
return fmt.Errorf("%s present state has invalid scope %q", label, s.Scope)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m FlagMigration) key() string {
|
|
return m.Command + "\x00" + m.Legacy.Name + "\x00" + m.Canonical.Name
|
|
}
|
|
|
|
func isExactCommandPath(path string) bool {
|
|
return path != "" &&
|
|
path == strings.TrimSpace(path) &&
|
|
strings.Join(strings.Fields(path), " ") == path &&
|
|
(path == "dws" || strings.HasPrefix(path, "dws ")) &&
|
|
!strings.ContainsAny(path, "*?[]{}")
|
|
}
|
|
|
|
func isExactFlagName(name string) bool {
|
|
return name != "" &&
|
|
name == strings.TrimSpace(name) &&
|
|
!strings.HasPrefix(name, "-") &&
|
|
!strings.ContainsAny(name, "*?[]{} /\t\r\n")
|
|
}
|
|
|
|
// CompareAllWithFlagMigrations applies the ordinary compatibility policy and
|
|
// then consumes only exact, merge-base-owned flag migrations. Candidate-owned
|
|
// records participate in the lifecycle check, but never authorize their own
|
|
// interface change.
|
|
func CompareAllWithFlagMigrations(
|
|
current Snapshot,
|
|
references map[string]Snapshot,
|
|
authority FlagMigrationManifest,
|
|
candidate FlagMigrationManifest,
|
|
) (Report, error) {
|
|
authorizations, err := AuthorizeFlagMigrations(current, references, authority, candidate)
|
|
if err != nil {
|
|
return Report{}, err
|
|
}
|
|
report := CompareAll(current, references)
|
|
if len(authorizations) == 0 {
|
|
return report, nil
|
|
}
|
|
|
|
for index := range report.Comparisons {
|
|
comparison := &report.Comparisons[index]
|
|
reference := references[comparison.Reference]
|
|
filtered := comparison.Blocking[:0]
|
|
for _, change := range comparison.Blocking {
|
|
if flagMigrationAuthorizesChange(current, reference, change, authorizations) {
|
|
continue
|
|
}
|
|
filtered = append(filtered, change)
|
|
}
|
|
comparison.Blocking = filtered
|
|
comparison.Compatible = len(filtered) == 0
|
|
}
|
|
report.Compatible = true
|
|
for _, comparison := range report.Comparisons {
|
|
if !comparison.Compatible {
|
|
report.Compatible = false
|
|
break
|
|
}
|
|
}
|
|
return report, nil
|
|
}
|
|
|
|
// AuthorizeFlagMigrations validates both snapshots and manifests, enforces the
|
|
// base-owned migration lifecycle, and returns only approvals that may authorize
|
|
// the current exact interface transition. A non-empty lifecycle requires both a
|
|
// main/merge-base authority reference and a stable reference. Candidate-added
|
|
// records never appear in the returned authorization set.
|
|
func AuthorizeFlagMigrations(
|
|
current Snapshot,
|
|
references map[string]Snapshot,
|
|
authority FlagMigrationManifest,
|
|
candidate FlagMigrationManifest,
|
|
) ([]FlagMigration, error) {
|
|
if err := current.Validate(); err != nil {
|
|
return nil, fmt.Errorf("validate current interface snapshot: %w", err)
|
|
}
|
|
for label, snapshot := range references {
|
|
if err := snapshot.Validate(); err != nil {
|
|
return nil, fmt.Errorf("validate %s interface snapshot: %w", label, err)
|
|
}
|
|
}
|
|
if err := authority.Validate(); err != nil {
|
|
return nil, fmt.Errorf("validate approved flag migrations: %w", err)
|
|
}
|
|
if err := candidate.Validate(); err != nil {
|
|
return nil, fmt.Errorf("validate candidate flag migrations: %w", err)
|
|
}
|
|
return evaluateFlagMigrationLifecycle(current, references, authority, candidate)
|
|
}
|
|
|
|
type flagMigrationPhase string
|
|
|
|
const (
|
|
flagMigrationBefore flagMigrationPhase = "before"
|
|
flagMigrationAfter flagMigrationPhase = "after"
|
|
flagMigrationPartial flagMigrationPhase = "partial"
|
|
)
|
|
|
|
func evaluateFlagMigrationLifecycle(
|
|
current Snapshot,
|
|
references map[string]Snapshot,
|
|
authority FlagMigrationManifest,
|
|
candidate FlagMigrationManifest,
|
|
) ([]FlagMigration, error) {
|
|
if len(authority.Migrations) == 0 && len(candidate.Migrations) == 0 {
|
|
return nil, nil
|
|
}
|
|
if _, ok := references["stable"]; !ok {
|
|
return nil, fmt.Errorf("flag migration lifecycle requires a stable reference")
|
|
}
|
|
mergeBase, label, ok := flagMigrationAuthoritySnapshot(references)
|
|
if !ok {
|
|
return nil, fmt.Errorf("flag migration lifecycle requires a main or merge-base reference")
|
|
}
|
|
|
|
authorityByKey := flagMigrationIndex(authority)
|
|
candidateByKey := flagMigrationIndex(candidate)
|
|
authorizations := make([]FlagMigration, 0, len(authority.Migrations))
|
|
|
|
for _, approved := range authority.Migrations {
|
|
basePhase := matchFlagMigrationPhase(mergeBase, approved)
|
|
wantBasePhase := flagMigrationBefore
|
|
if approved.State == FlagMigrationConsumed {
|
|
wantBasePhase = flagMigrationAfter
|
|
}
|
|
if basePhase != wantBasePhase {
|
|
return nil, fmt.Errorf(
|
|
"approved flag migration %s is %s in %s, want exact %s state for %s",
|
|
approved.displayKey(),
|
|
basePhase,
|
|
label,
|
|
wantBasePhase,
|
|
approved.State,
|
|
)
|
|
}
|
|
|
|
proposed, exists := candidateByKey[approved.key()]
|
|
if exists && !sameFlagMigrationApproval(approved, proposed) {
|
|
return nil, fmt.Errorf("candidate modified base-owned flag migration %s", approved.displayKey())
|
|
}
|
|
|
|
currentPhase := matchFlagMigrationPhase(current, approved)
|
|
switch approved.State {
|
|
case FlagMigrationPending:
|
|
if !exists {
|
|
return nil, fmt.Errorf("candidate removed pending flag migration %s", approved.displayKey())
|
|
}
|
|
switch currentPhase {
|
|
case flagMigrationBefore:
|
|
if proposed.State != FlagMigrationPending {
|
|
return nil, fmt.Errorf("candidate falsely consumed unchanged flag migration %s", approved.displayKey())
|
|
}
|
|
case flagMigrationAfter:
|
|
if proposed.State != FlagMigrationConsumed {
|
|
return nil, fmt.Errorf("candidate completed flag migration %s without marking it consumed", approved.displayKey())
|
|
}
|
|
authorizations = append(authorizations, approved)
|
|
default:
|
|
return nil, fmt.Errorf("candidate partially applied flag migration %s", approved.displayKey())
|
|
}
|
|
case FlagMigrationConsumed:
|
|
if currentPhase != flagMigrationAfter {
|
|
return nil, fmt.Errorf("candidate drifted from consumed flag migration %s", approved.displayKey())
|
|
}
|
|
allReferencesAfter := true
|
|
for _, reference := range references {
|
|
if matchFlagMigrationPhase(reference, approved) != flagMigrationAfter {
|
|
allReferencesAfter = false
|
|
break
|
|
}
|
|
}
|
|
if allReferencesAfter {
|
|
if exists {
|
|
return nil, fmt.Errorf("consumed flag migration %s is stale after all references reached the after state", approved.displayKey())
|
|
}
|
|
continue
|
|
}
|
|
if !exists {
|
|
return nil, fmt.Errorf("candidate removed consumed flag migration %s before every reference reached the after state", approved.displayKey())
|
|
}
|
|
if proposed.State != FlagMigrationConsumed {
|
|
return nil, fmt.Errorf("candidate changed consumed flag migration %s back to pending", approved.displayKey())
|
|
}
|
|
authorizations = append(authorizations, approved)
|
|
}
|
|
}
|
|
|
|
for _, proposed := range candidate.Migrations {
|
|
if _, exists := authorityByKey[proposed.key()]; exists {
|
|
continue
|
|
}
|
|
if proposed.State != FlagMigrationPending {
|
|
return nil, fmt.Errorf("candidate-added flag migration %s must start pending", proposed.displayKey())
|
|
}
|
|
if matchFlagMigrationPhase(mergeBase, proposed) != flagMigrationBefore {
|
|
return nil, fmt.Errorf("candidate-added flag migration %s does not match the merge-base before state", proposed.displayKey())
|
|
}
|
|
if matchFlagMigrationPhase(current, proposed) != flagMigrationBefore {
|
|
return nil, fmt.Errorf("candidate-added flag migration %s cannot authorize its own interface change", proposed.displayKey())
|
|
}
|
|
}
|
|
|
|
return authorizations, nil
|
|
}
|
|
|
|
func flagMigrationAuthoritySnapshot(references map[string]Snapshot) (Snapshot, string, bool) {
|
|
for _, label := range []string{"merge-base", "main"} {
|
|
if snapshot, ok := references[label]; ok {
|
|
return snapshot, label, true
|
|
}
|
|
}
|
|
return Snapshot{}, "", false
|
|
}
|
|
|
|
func flagMigrationIndex(manifest FlagMigrationManifest) map[string]FlagMigration {
|
|
index := make(map[string]FlagMigration, len(manifest.Migrations))
|
|
for _, migration := range manifest.Migrations {
|
|
index[migration.key()] = migration
|
|
}
|
|
return index
|
|
}
|
|
|
|
func sameFlagMigrationApproval(left, right FlagMigration) bool {
|
|
left.State = ""
|
|
right.State = ""
|
|
return reflect.DeepEqual(left, right)
|
|
}
|
|
|
|
func matchFlagMigrationPhase(snapshot Snapshot, migration FlagMigration) flagMigrationPhase {
|
|
command, exists := commandIndex(snapshot)[migration.Command]
|
|
if !exists {
|
|
return flagMigrationPartial
|
|
}
|
|
legacy := flagMigrationStateForCommand(command, migration.Legacy.Name)
|
|
canonical := flagMigrationStateForCommand(command, migration.Canonical.Name)
|
|
if legacy == migration.Legacy.Before && canonical == migration.Canonical.Before {
|
|
return flagMigrationBefore
|
|
}
|
|
if legacy == migration.Legacy.After && canonical == migration.Canonical.After {
|
|
return flagMigrationAfter
|
|
}
|
|
return flagMigrationPartial
|
|
}
|
|
|
|
func flagMigrationStateForCommand(command Command, name string) FlagMigrationState {
|
|
for _, flag := range command.LocalFlags {
|
|
if flag.Name == name {
|
|
return flagMigrationState(flag, "local")
|
|
}
|
|
}
|
|
for _, flag := range command.InheritedFlags {
|
|
if flag.Name == name {
|
|
return flagMigrationState(flag, "inherited")
|
|
}
|
|
}
|
|
return FlagMigrationState{}
|
|
}
|
|
|
|
func flagMigrationState(flag Flag, scope string) FlagMigrationState {
|
|
return FlagMigrationState{
|
|
Present: true,
|
|
Type: flag.Type,
|
|
Required: flag.Required,
|
|
Hidden: flag.Hidden,
|
|
Shorthand: flag.Shorthand,
|
|
NoOpt: flag.NoOpt,
|
|
Scope: scope,
|
|
AliasOf: flag.AliasOf,
|
|
}
|
|
}
|
|
|
|
func flagMigrationAuthorizesChange(
|
|
current Snapshot,
|
|
reference Snapshot,
|
|
change Change,
|
|
authorizations []FlagMigration,
|
|
) bool {
|
|
canonicalPath := acceptedPathIndex(reference)[change.Path]
|
|
if canonicalPath == "" {
|
|
canonicalPath = change.Path
|
|
}
|
|
for _, migration := range authorizations {
|
|
if canonicalPath != migration.Command ||
|
|
matchFlagMigrationPhase(reference, migration) != flagMigrationBefore ||
|
|
matchFlagMigrationPhase(current, migration) != flagMigrationAfter {
|
|
continue
|
|
}
|
|
if change.Flag == migration.Legacy.Name && change.Kind == "flag_became_hidden" {
|
|
return true
|
|
}
|
|
if change.Flag != migration.Canonical.Name {
|
|
continue
|
|
}
|
|
if !migration.Canonical.Before.Present &&
|
|
migration.Canonical.After.Required &&
|
|
change.Kind == "required_flag_added" {
|
|
return true
|
|
}
|
|
if migration.Canonical.Before.Present &&
|
|
migration.Canonical.Before.Hidden &&
|
|
!migration.Canonical.Before.Required &&
|
|
migration.Canonical.After.Required &&
|
|
change.Kind == "flag_became_required" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (m FlagMigration) displayKey() string {
|
|
return fmt.Sprintf("%q --%s -> --%s", m.Command, m.Legacy.Name, m.Canonical.Name)
|
|
}
|