640 lines
23 KiB
Go
640 lines
23 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"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
const CommandMigrationManifestVersion = 1
|
|
|
|
const (
|
|
CommandMigrationPending = "pending"
|
|
CommandMigrationConsumed = "consumed"
|
|
|
|
CommandMigrationMove = "command_move"
|
|
CommandMigrationFlagExtraction = "flag_extraction"
|
|
)
|
|
|
|
// CommandMigrationManifest governs compatibility-preserving surface moves that
|
|
// cannot be represented as an in-command flag rename. The merge-base owns the
|
|
// authorization; the candidate copy is only a lifecycle receipt.
|
|
type CommandMigrationManifest struct {
|
|
Version int `json:"version"`
|
|
Migrations []CommandMigration `json:"migrations"`
|
|
}
|
|
|
|
type CommandMigration struct {
|
|
Kind string `json:"kind"`
|
|
Legacy CommandMigrationSide `json:"legacy"`
|
|
Replacement CommandMigrationSide `json:"replacement"`
|
|
LegacyFlag CommandMigrationFlag `json:"legacy_flag"`
|
|
Schema CommandMigrationSchema `json:"schema"`
|
|
State string `json:"state"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
|
|
type CommandMigrationSide struct {
|
|
Command string `json:"command"`
|
|
Before CommandMigrationState `json:"before"`
|
|
After CommandMigrationState `json:"after"`
|
|
}
|
|
|
|
type CommandMigrationState struct {
|
|
Present bool `json:"present"`
|
|
Runnable bool `json:"runnable,omitempty"`
|
|
Hidden bool `json:"hidden,omitempty"`
|
|
}
|
|
|
|
type CommandMigrationFlag struct {
|
|
Name string `json:"name,omitempty"`
|
|
Before FlagMigrationState `json:"before"`
|
|
After FlagMigrationState `json:"after"`
|
|
}
|
|
|
|
type CommandMigrationSchema struct {
|
|
ProductID string `json:"product_id"`
|
|
SourceToolID string `json:"source_tool_id"`
|
|
ReplacementToolID string `json:"replacement_tool_id"`
|
|
Parameters []CommandParameterMigration `json:"parameters"`
|
|
}
|
|
|
|
type CommandParameterMigration struct {
|
|
From string `json:"from"`
|
|
To string `json:"to,omitempty"`
|
|
ReplacementConstant *CommandReplacementConstant `json:"replacement_constant,omitempty"`
|
|
}
|
|
|
|
type CommandReplacementConstant struct {
|
|
Property string `json:"property"`
|
|
Value bool `json:"value"`
|
|
}
|
|
|
|
func ReadCommandMigrationManifest(r io.Reader) (CommandMigrationManifest, error) {
|
|
data, err := io.ReadAll(r)
|
|
if err != nil {
|
|
return CommandMigrationManifest{}, fmt.Errorf("read command migration manifest: %w", err)
|
|
}
|
|
var manifest CommandMigrationManifest
|
|
decoder := json.NewDecoder(bytes.NewReader(data))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(&manifest); err != nil {
|
|
return CommandMigrationManifest{}, err
|
|
}
|
|
if err := decoder.Decode(&struct{}{}); err != io.EOF {
|
|
if err == nil {
|
|
return CommandMigrationManifest{}, fmt.Errorf("command migration manifest contains trailing multiple JSON values")
|
|
}
|
|
return CommandMigrationManifest{}, fmt.Errorf("read trailing command migration manifest data: %w", err)
|
|
}
|
|
strict := json.NewDecoder(bytes.NewReader(data))
|
|
strict.UseNumber()
|
|
if err := validateLabeledMigrationJSONValue(strict, "$", reflect.TypeOf(CommandMigrationManifest{}), "command"); err != nil {
|
|
return CommandMigrationManifest{}, err
|
|
}
|
|
if err := manifest.Validate(); err != nil {
|
|
return CommandMigrationManifest{}, err
|
|
}
|
|
return manifest, nil
|
|
}
|
|
|
|
func (m CommandMigrationManifest) Validate() error {
|
|
if m.Version != CommandMigrationManifestVersion {
|
|
return fmt.Errorf("unsupported command migration manifest version %d (want %d)", m.Version, CommandMigrationManifestVersion)
|
|
}
|
|
if m.Migrations == nil {
|
|
return fmt.Errorf("command migration manifest migrations must be an array")
|
|
}
|
|
seen := make(map[string]bool, len(m.Migrations))
|
|
for index, migration := range m.Migrations {
|
|
if err := migration.validate(); err != nil {
|
|
return fmt.Errorf("command migration %d: %w", index, err)
|
|
}
|
|
if seen[migration.key()] {
|
|
return fmt.Errorf("command migration %d duplicates %s", index, migration.displayKey())
|
|
}
|
|
seen[migration.key()] = true
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m CommandMigration) validate() error {
|
|
if m.Kind != CommandMigrationMove && m.Kind != CommandMigrationFlagExtraction {
|
|
return fmt.Errorf("invalid kind %q", m.Kind)
|
|
}
|
|
if !isExactCommandPath(m.Legacy.Command) || !isExactCommandPath(m.Replacement.Command) {
|
|
return fmt.Errorf("legacy and replacement must be exact command paths rooted at dws")
|
|
}
|
|
if m.Legacy.Command == m.Replacement.Command {
|
|
return fmt.Errorf("legacy and replacement command paths must differ")
|
|
}
|
|
if strings.TrimSpace(m.Reason) == "" || m.Reason != strings.TrimSpace(m.Reason) {
|
|
return fmt.Errorf("migration must include a non-empty trimmed reason")
|
|
}
|
|
if m.State != CommandMigrationPending && m.State != CommandMigrationConsumed {
|
|
return fmt.Errorf("invalid state %q", m.State)
|
|
}
|
|
for label, state := range map[string]CommandMigrationState{
|
|
"legacy before": m.Legacy.Before,
|
|
"legacy after": m.Legacy.After,
|
|
"replacement before": m.Replacement.Before,
|
|
"replacement after": m.Replacement.After,
|
|
} {
|
|
if err := state.validate(label); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !m.Legacy.Before.Present || !m.Legacy.Before.Runnable || m.Legacy.Before.Hidden ||
|
|
!m.Legacy.After.Present || !m.Legacy.After.Runnable {
|
|
return fmt.Errorf("legacy command must remain runnable and start visible")
|
|
}
|
|
if m.Replacement.Before.Present || !m.Replacement.After.Present || !m.Replacement.After.Runnable || m.Replacement.After.Hidden {
|
|
return fmt.Errorf("replacement command must migrate exactly from absent to visible runnable")
|
|
}
|
|
if err := m.Schema.validate(m.Kind); err != nil {
|
|
return err
|
|
}
|
|
switch m.Kind {
|
|
case CommandMigrationMove:
|
|
if strings.HasPrefix(m.Replacement.Command, m.Legacy.Command+" ") ||
|
|
strings.HasPrefix(m.Legacy.Command, m.Replacement.Command+" ") {
|
|
return fmt.Errorf("command_move legacy and replacement command paths must not have an ancestor relationship")
|
|
}
|
|
if !m.Legacy.After.Hidden {
|
|
return fmt.Errorf("command_move legacy command must migrate exactly from visible to hidden")
|
|
}
|
|
if m.LegacyFlag != (CommandMigrationFlag{}) {
|
|
return fmt.Errorf("command_move must not declare legacy_flag")
|
|
}
|
|
if m.Schema.SourceToolID != m.Schema.ReplacementToolID {
|
|
return fmt.Errorf("command_move must retain one stable Schema tool identity")
|
|
}
|
|
case CommandMigrationFlagExtraction:
|
|
if m.Legacy.After.Hidden || m.Legacy.Before != m.Legacy.After {
|
|
return fmt.Errorf("flag_extraction legacy command must remain visible and unchanged")
|
|
}
|
|
if err := m.LegacyFlag.validate(); err != nil {
|
|
return err
|
|
}
|
|
if m.Schema.SourceToolID == m.Schema.ReplacementToolID {
|
|
return fmt.Errorf("flag_extraction requires a distinct replacement Schema tool")
|
|
}
|
|
if m.LegacyFlag.Before.Type != "bool" || m.LegacyFlag.After.Type != "bool" {
|
|
return fmt.Errorf("flag_extraction legacy_flag must be an optional bool")
|
|
}
|
|
var extracted *CommandParameterMigration
|
|
constantCount := 0
|
|
for index := range m.Schema.Parameters {
|
|
parameter := &m.Schema.Parameters[index]
|
|
if parameter.ReplacementConstant == nil {
|
|
continue
|
|
}
|
|
constantCount++
|
|
if parameter.From == m.LegacyFlag.Name {
|
|
extracted = parameter
|
|
}
|
|
}
|
|
if constantCount != 1 {
|
|
return fmt.Errorf("flag_extraction must declare exactly one replacement constant")
|
|
}
|
|
if extracted == nil {
|
|
return fmt.Errorf("flag_extraction must map the exact extracted flag to replacement_constant")
|
|
}
|
|
if !extracted.ReplacementConstant.Value {
|
|
return fmt.Errorf("flag_extraction v1 replacement_constant must be true")
|
|
}
|
|
wantNoOpt := strconv.FormatBool(extracted.ReplacementConstant.Value)
|
|
if m.LegacyFlag.Before.NoOpt != wantNoOpt || m.LegacyFlag.After.NoOpt != wantNoOpt {
|
|
return fmt.Errorf("flag_extraction legacy_flag no_opt must equal replacement constant %q", wantNoOpt)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s CommandMigrationState) validate(label string) error {
|
|
if !s.Present {
|
|
if s.Runnable || s.Hidden {
|
|
return fmt.Errorf("%s absent state must not declare command attributes", label)
|
|
}
|
|
return nil
|
|
}
|
|
if !s.Runnable {
|
|
return fmt.Errorf("%s present state must be runnable", label)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f CommandMigrationFlag) validate() error {
|
|
if !isExactFlagName(f.Name) {
|
|
return fmt.Errorf("legacy_flag name must be an exact flag")
|
|
}
|
|
if err := f.Before.validate("legacy_flag before"); err != nil {
|
|
return err
|
|
}
|
|
if err := f.After.validate("legacy_flag after"); err != nil {
|
|
return err
|
|
}
|
|
if !f.Before.Present || !f.After.Present || f.Before.Hidden || !f.After.Hidden {
|
|
return fmt.Errorf("legacy_flag must migrate exactly from visible to hidden while remaining present")
|
|
}
|
|
if f.Before.Required || f.After.Required {
|
|
return fmt.Errorf("legacy_flag must remain optional before and after extraction")
|
|
}
|
|
before := f.Before
|
|
after := f.After
|
|
before.Hidden = false
|
|
after.Hidden = false
|
|
if before != after {
|
|
return fmt.Errorf("legacy_flag may change only hidden visibility")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s CommandMigrationSchema) validate(kind string) error {
|
|
for label, value := range map[string]string{
|
|
"product_id": s.ProductID,
|
|
"source_tool_id": s.SourceToolID,
|
|
"replacement_tool_id": s.ReplacementToolID,
|
|
} {
|
|
if !isExactSchemaIdentifier(value) {
|
|
return fmt.Errorf("schema %s must be an exact identifier", label)
|
|
}
|
|
}
|
|
if s.Parameters == nil {
|
|
return fmt.Errorf("schema parameters must be an array")
|
|
}
|
|
seenFrom := map[string]bool{}
|
|
seenTo := map[string]bool{}
|
|
seenConstantProperty := map[string]bool{}
|
|
for index, parameter := range s.Parameters {
|
|
if !isExactFlagName(parameter.From) {
|
|
return fmt.Errorf("schema parameter %d from must be an exact parameter name", index)
|
|
}
|
|
if seenFrom[parameter.From] {
|
|
return fmt.Errorf("schema parameter %d duplicates from %q", index, parameter.From)
|
|
}
|
|
seenFrom[parameter.From] = true
|
|
if kind == CommandMigrationMove {
|
|
if parameter.ReplacementConstant != nil {
|
|
return fmt.Errorf("command_move schema parameter %d must not declare replacement_constant", index)
|
|
}
|
|
if !isExactFlagName(parameter.To) || parameter.To == parameter.From {
|
|
return fmt.Errorf("command_move schema parameter %d requires a distinct exact to name", index)
|
|
}
|
|
if seenTo[parameter.To] {
|
|
return fmt.Errorf("schema parameter %d duplicates to %q", index, parameter.To)
|
|
}
|
|
seenTo[parameter.To] = true
|
|
continue
|
|
}
|
|
if kind != CommandMigrationFlagExtraction {
|
|
return fmt.Errorf("invalid command migration kind %q", kind)
|
|
}
|
|
|
|
hasTo := parameter.To != ""
|
|
hasConstant := parameter.ReplacementConstant != nil
|
|
if !hasTo && !hasConstant {
|
|
return fmt.Errorf("flag_extraction schema parameter %d requires an exact to or replacement_constant", index)
|
|
}
|
|
if hasTo && hasConstant {
|
|
return fmt.Errorf("flag_extraction schema parameter %d must declare exactly one target", index)
|
|
}
|
|
if hasTo {
|
|
if !isExactFlagName(parameter.To) {
|
|
return fmt.Errorf("flag_extraction schema parameter %d to must be an exact parameter name", index)
|
|
}
|
|
if seenTo[parameter.To] {
|
|
return fmt.Errorf("schema parameter %d duplicates to %q", index, parameter.To)
|
|
}
|
|
if seenConstantProperty[parameter.To] {
|
|
return fmt.Errorf("schema parameter %d duplicates parameter target %q", index, parameter.To)
|
|
}
|
|
seenTo[parameter.To] = true
|
|
continue
|
|
}
|
|
property := parameter.ReplacementConstant.Property
|
|
if !isExactSchemaIdentifier(property) {
|
|
return fmt.Errorf("flag_extraction schema parameter %d replacement_constant requires an exact property", index)
|
|
}
|
|
if seenConstantProperty[property] {
|
|
return fmt.Errorf("schema parameter %d duplicates replacement_constant property %q", index, property)
|
|
}
|
|
if seenTo[property] {
|
|
return fmt.Errorf("schema parameter %d duplicates parameter target %q", index, property)
|
|
}
|
|
seenConstantProperty[property] = true
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isExactSchemaIdentifier(value string) bool {
|
|
return value != "" && value == strings.TrimSpace(value) &&
|
|
!strings.ContainsAny(value, "*?[]{} /\\\t\r\n")
|
|
}
|
|
|
|
func (m CommandMigration) key() string {
|
|
return strings.Join([]string{m.Kind, m.Legacy.Command, m.Replacement.Command, m.Schema.ProductID, m.Schema.SourceToolID, m.Schema.ReplacementToolID}, "\x00")
|
|
}
|
|
|
|
func (m CommandMigration) displayKey() string {
|
|
return fmt.Sprintf("%s %q -> %q", m.Kind, m.Legacy.Command, m.Replacement.Command)
|
|
}
|
|
|
|
type commandMigrationPhase string
|
|
|
|
const (
|
|
commandMigrationBefore commandMigrationPhase = "before"
|
|
commandMigrationAfter commandMigrationPhase = "after"
|
|
commandMigrationPartial commandMigrationPhase = "partial"
|
|
)
|
|
|
|
func AuthorizeCommandMigrations(
|
|
current Snapshot,
|
|
references map[string]Snapshot,
|
|
authority CommandMigrationManifest,
|
|
candidate CommandMigrationManifest,
|
|
) ([]CommandMigration, 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 command migrations: %w", err)
|
|
}
|
|
if err := candidate.Validate(); err != nil {
|
|
return nil, fmt.Errorf("validate candidate command migrations: %w", err)
|
|
}
|
|
type commandMoveTopologyCheck struct {
|
|
label string
|
|
snapshot Snapshot
|
|
manifest CommandMigrationManifest
|
|
}
|
|
topologyChecks := []commandMoveTopologyCheck{
|
|
{label: "approved", snapshot: current, manifest: authority},
|
|
{label: "candidate", snapshot: current, manifest: candidate},
|
|
}
|
|
if mergeBase, _, ok := flagMigrationAuthoritySnapshot(references); ok {
|
|
topologyChecks = append(topologyChecks,
|
|
commandMoveTopologyCheck{label: "approved base", snapshot: mergeBase, manifest: authority},
|
|
commandMoveTopologyCheck{label: "candidate base", snapshot: mergeBase, manifest: candidate},
|
|
)
|
|
}
|
|
for _, check := range topologyChecks {
|
|
if err := validateCommandMoveLegacyLeaves(check.snapshot, check.manifest); err != nil {
|
|
return nil, fmt.Errorf("validate %s command migration topology: %w", check.label, err)
|
|
}
|
|
}
|
|
return evaluateCommandMigrationLifecycle(current, references, authority, candidate)
|
|
}
|
|
|
|
func validateCommandMoveLegacyLeaves(snapshot Snapshot, manifest CommandMigrationManifest) error {
|
|
commands := commandIndex(snapshot)
|
|
for _, migration := range manifest.Migrations {
|
|
if migration.Kind != CommandMigrationMove {
|
|
continue
|
|
}
|
|
if _, present := commands[migration.Legacy.Command]; !present {
|
|
continue
|
|
}
|
|
for _, command := range snapshot.Commands {
|
|
if strings.HasPrefix(command.Path, migration.Legacy.Command+" ") {
|
|
return fmt.Errorf("command_move legacy command %q must be a leaf", migration.Legacy.Command)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func evaluateCommandMigrationLifecycle(
|
|
current Snapshot,
|
|
references map[string]Snapshot,
|
|
authority CommandMigrationManifest,
|
|
candidate CommandMigrationManifest,
|
|
) ([]CommandMigration, error) {
|
|
if len(authority.Migrations) == 0 && len(candidate.Migrations) == 0 {
|
|
return nil, nil
|
|
}
|
|
if _, ok := references["stable"]; !ok {
|
|
return nil, fmt.Errorf("command migration lifecycle requires a stable reference")
|
|
}
|
|
mergeBase, label, ok := flagMigrationAuthoritySnapshot(references)
|
|
if !ok {
|
|
return nil, fmt.Errorf("command migration lifecycle requires a main or merge-base reference")
|
|
}
|
|
authorityByKey := commandMigrationIndex(authority)
|
|
candidateByKey := commandMigrationIndex(candidate)
|
|
authorizations := make([]CommandMigration, 0, len(authority.Migrations))
|
|
for _, approved := range authority.Migrations {
|
|
basePhase := matchCommandMigrationPhase(mergeBase, approved)
|
|
wantBase := commandMigrationBefore
|
|
if approved.State == CommandMigrationConsumed {
|
|
wantBase = commandMigrationAfter
|
|
}
|
|
if basePhase != wantBase {
|
|
return nil, fmt.Errorf("approved command migration %s is %s in %s, want exact %s state for %s", approved.displayKey(), basePhase, label, wantBase, approved.State)
|
|
}
|
|
proposed, exists := candidateByKey[approved.key()]
|
|
if exists && !sameCommandMigrationApproval(approved, proposed) {
|
|
return nil, fmt.Errorf("candidate modified base-owned command migration %s", approved.displayKey())
|
|
}
|
|
currentPhase := matchCommandMigrationPhase(current, approved)
|
|
switch approved.State {
|
|
case CommandMigrationPending:
|
|
if !exists {
|
|
return nil, fmt.Errorf("candidate removed pending command migration %s", approved.displayKey())
|
|
}
|
|
switch currentPhase {
|
|
case commandMigrationBefore:
|
|
if proposed.State != CommandMigrationPending {
|
|
return nil, fmt.Errorf("candidate falsely consumed unchanged command migration %s", approved.displayKey())
|
|
}
|
|
case commandMigrationAfter:
|
|
if proposed.State != CommandMigrationConsumed {
|
|
return nil, fmt.Errorf("candidate completed command migration %s without marking it consumed", approved.displayKey())
|
|
}
|
|
authorizations = append(authorizations, approved)
|
|
default:
|
|
return nil, fmt.Errorf("candidate partially applied command migration %s", approved.displayKey())
|
|
}
|
|
case CommandMigrationConsumed:
|
|
if currentPhase != commandMigrationAfter {
|
|
return nil, fmt.Errorf("candidate drifted from consumed command migration %s", approved.displayKey())
|
|
}
|
|
allAfter := true
|
|
for _, reference := range references {
|
|
if matchCommandMigrationPhase(reference, approved) != commandMigrationAfter {
|
|
allAfter = false
|
|
break
|
|
}
|
|
}
|
|
if allAfter {
|
|
if exists {
|
|
return nil, fmt.Errorf("consumed command migration %s is stale after all references reached the after state", approved.displayKey())
|
|
}
|
|
continue
|
|
}
|
|
if !exists || proposed.State != CommandMigrationConsumed {
|
|
return nil, fmt.Errorf("candidate must retain consumed command migration %s until every reference reaches the after state", approved.displayKey())
|
|
}
|
|
authorizations = append(authorizations, approved)
|
|
}
|
|
}
|
|
for _, proposed := range candidate.Migrations {
|
|
if _, exists := authorityByKey[proposed.key()]; exists {
|
|
continue
|
|
}
|
|
if proposed.State != CommandMigrationPending {
|
|
return nil, fmt.Errorf("candidate-added command migration %s must start pending", proposed.displayKey())
|
|
}
|
|
if matchCommandMigrationPhase(mergeBase, proposed) != commandMigrationBefore {
|
|
return nil, fmt.Errorf("candidate-added command migration %s does not match the merge-base before state", proposed.displayKey())
|
|
}
|
|
if matchCommandMigrationPhase(current, proposed) != commandMigrationBefore {
|
|
return nil, fmt.Errorf("candidate-added command migration %s cannot authorize its own interface change", proposed.displayKey())
|
|
}
|
|
}
|
|
return authorizations, nil
|
|
}
|
|
|
|
func commandMigrationIndex(manifest CommandMigrationManifest) map[string]CommandMigration {
|
|
index := make(map[string]CommandMigration, len(manifest.Migrations))
|
|
for _, migration := range manifest.Migrations {
|
|
index[migration.key()] = migration
|
|
}
|
|
return index
|
|
}
|
|
|
|
func sameCommandMigrationApproval(left, right CommandMigration) bool {
|
|
left.State = ""
|
|
right.State = ""
|
|
return reflect.DeepEqual(left, right)
|
|
}
|
|
|
|
func matchCommandMigrationPhase(snapshot Snapshot, migration CommandMigration) commandMigrationPhase {
|
|
commands := commandIndex(snapshot)
|
|
legacy := commandMigrationStateForCommand(commands, migration.Legacy.Command)
|
|
replacement := commandMigrationStateForCommand(commands, migration.Replacement.Command)
|
|
before := legacy == migration.Legacy.Before && replacement == migration.Replacement.Before
|
|
after := legacy == migration.Legacy.After && replacement == migration.Replacement.After
|
|
if migration.Kind == CommandMigrationFlagExtraction {
|
|
command, exists := commands[migration.Legacy.Command]
|
|
flagState := FlagMigrationState{}
|
|
if exists {
|
|
flagState = flagMigrationStateForCommand(command, migration.LegacyFlag.Name)
|
|
}
|
|
before = before && flagState == migration.LegacyFlag.Before
|
|
after = after && flagState == migration.LegacyFlag.After
|
|
if replacement, replacementExists := commands[migration.Replacement.Command]; replacementExists {
|
|
after = after && commandMigrationReplacementConstantsMatch(replacement, migration)
|
|
} else {
|
|
after = false
|
|
}
|
|
}
|
|
if before {
|
|
return commandMigrationBefore
|
|
}
|
|
if after {
|
|
return commandMigrationAfter
|
|
}
|
|
return commandMigrationPartial
|
|
}
|
|
|
|
func commandMigrationReplacementConstantsMatch(command Command, migration CommandMigration) bool {
|
|
expected := make(map[string]bool)
|
|
for _, parameter := range migration.Schema.Parameters {
|
|
if parameter.ReplacementConstant == nil {
|
|
continue
|
|
}
|
|
expected[parameter.ReplacementConstant.Property] = parameter.ReplacementConstant.Value
|
|
}
|
|
return reflect.DeepEqual(command.BoolConstParams, expected)
|
|
}
|
|
|
|
func commandMigrationStateForCommand(commands map[string]Command, path string) CommandMigrationState {
|
|
command, exists := commands[path]
|
|
if !exists {
|
|
return CommandMigrationState{}
|
|
}
|
|
return CommandMigrationState{Present: true, Runnable: command.Runnable, Hidden: command.Hidden}
|
|
}
|
|
|
|
// CompareAllWithInterfaceMigrations applies both migration families to one
|
|
// ordinary report, so the two ledgers cannot mask each other's unrelated
|
|
// findings.
|
|
func CompareAllWithInterfaceMigrations(
|
|
current Snapshot,
|
|
references map[string]Snapshot,
|
|
flagAuthority FlagMigrationManifest,
|
|
flagCandidate FlagMigrationManifest,
|
|
commandAuthority CommandMigrationManifest,
|
|
commandCandidate CommandMigrationManifest,
|
|
) (Report, error) {
|
|
flagAuthorizations, err := AuthorizeFlagMigrations(current, references, flagAuthority, flagCandidate)
|
|
if err != nil {
|
|
return Report{}, err
|
|
}
|
|
commandAuthorizations, err := AuthorizeCommandMigrations(current, references, commandAuthority, commandCandidate)
|
|
if err != nil {
|
|
return Report{}, err
|
|
}
|
|
report := CompareAll(current, references)
|
|
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, flagAuthorizations) ||
|
|
commandMigrationAuthorizesChange(current, reference, change, commandAuthorizations) {
|
|
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
|
|
}
|
|
|
|
func commandMigrationAuthorizesChange(current, reference Snapshot, change Change, authorizations []CommandMigration) bool {
|
|
canonicalPath := acceptedPathIndex(reference)[change.Path]
|
|
if canonicalPath == "" {
|
|
canonicalPath = change.Path
|
|
}
|
|
for _, migration := range authorizations {
|
|
if canonicalPath != migration.Legacy.Command ||
|
|
matchCommandMigrationPhase(reference, migration) != commandMigrationBefore ||
|
|
matchCommandMigrationPhase(current, migration) != commandMigrationAfter {
|
|
continue
|
|
}
|
|
switch migration.Kind {
|
|
case CommandMigrationMove:
|
|
if change.Kind == "command_became_hidden" && change.Flag == "" {
|
|
return true
|
|
}
|
|
case CommandMigrationFlagExtraction:
|
|
if change.Kind == "flag_became_hidden" && change.Flag == migration.LegacyFlag.Name {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|