internal/packages/changelog/yaml.go (134 lines of code) (raw):
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.
package changelog
import (
"errors"
"fmt"
"github.com/Masterminds/semver/v3"
"gopkg.in/yaml.v3"
"github.com/elastic/elastic-package/internal/formatter"
)
// PatchYAML looks for the proper place to add the new revision in the changelog,
// trying to conserve original format and comments.
func PatchYAML(d []byte, patch Revision) ([]byte, error) {
var nodes []yaml.Node
err := yaml.Unmarshal(d, &nodes)
if err != nil {
return nil, err
}
patchVersion, err := semver.NewVersion(patch.Version)
if err != nil {
return nil, err
}
patched := false
var result []yaml.Node
for _, node := range nodes {
if patched {
result = append(result, node)
continue
}
var entry Revision
err := node.Decode(&entry)
if err != nil {
result = append(result, node)
continue
}
foundVersion, err := semver.NewVersion(entry.Version)
if err != nil {
return nil, err
}
if foundVersion.GreaterThan(patchVersion) {
return nil, errors.New("cannot add change to old version")
}
var newNode yaml.Node
if patchVersion.Equal(foundVersion) {
// Add the change to current entry.
entry.Changes = append(patch.Changes, entry.Changes...)
err := newNode.Encode(entry)
if err != nil {
return nil, err
}
// Keep comments of the original node.
newNode.HeadComment = node.HeadComment
newNode.LineComment = node.LineComment
newNode.FootComment = node.FootComment
// Quote version to keep common style.
setYamlMapValueStyle(&newNode, "version", yaml.DoubleQuotedStyle)
result = append(result, newNode)
patched = true
continue
}
// Add the change before first entry
err = newNode.Encode(patch)
if err != nil {
return nil, err
}
// If there is a comment on top, leave it there.
if node.HeadComment != "" {
newNode.HeadComment = node.HeadComment
node.HeadComment = ""
}
// Quote version to keep common style.
setYamlMapValueStyle(&newNode, "version", yaml.DoubleQuotedStyle)
result = append(result, newNode, node)
patched = true
}
if !patched {
return nil, errors.New("changelog entry was not added, this is probably a bug")
}
d, err = formatResult(result)
if err != nil {
return nil, fmt.Errorf("failed to format manifest: %w", err)
}
return d, nil
}
func SetManifestVersion(d []byte, version string) ([]byte, error) {
var node yaml.Node
err := yaml.Unmarshal(d, &node)
if err != nil {
return nil, fmt.Errorf("failed to decode manifest: %w", err)
}
// Manifest is a document, with a single element, that should be a map.
if len(node.Content) == 0 || node.Content[0].Kind != yaml.MappingNode {
return nil, errors.New("unexpected manifest content: not a map")
}
setYamlMapValue(node.Content[0], "version", version)
d, err = formatResult(&node)
if err != nil {
return nil, fmt.Errorf("failed to format manifest: %w", err)
}
return d, nil
}
func formatResult(result interface{}) ([]byte, error) {
d, err := yaml.Marshal(result)
if err != nil {
return nil, errors.New("failed to encode")
}
yamlFormatter := formatter.NewYAMLFormatter(formatter.KeysWithDotActionNone)
d, _, err = yamlFormatter.Format(d)
if err != nil {
return nil, errors.New("failed to format")
}
return d, nil
}
// setYamlMapValueStyle changes the style of one value in a YAML map. If the key
// is not found, it does nothing.
func setYamlMapValueStyle(node *yaml.Node, key string, style yaml.Style) {
// Check first if this is a map.
if node == nil || node.Kind != yaml.MappingNode {
return
}
// Look for the key, the value will be the next one.
var keyIdx int
for keyIdx = range node.Content {
child := node.Content[keyIdx]
if child.Kind == yaml.ScalarNode && child.Value == key {
break
}
}
valueIdx := keyIdx + 1
if valueIdx < len(node.Content) {
node.Content[valueIdx].Style = style
}
}
// setYamlMapValue sets a value in a map.
func setYamlMapValue(node *yaml.Node, key string, value string) {
// Check first if this is a map.
if node == nil || node.Kind != yaml.MappingNode {
return
}
// Look for the key, the value will be the next one.
var keyIdx int
for keyIdx = range node.Content {
child := node.Content[keyIdx]
if child.Kind == yaml.ScalarNode && child.Value == key {
break
}
}
valueIdx := keyIdx + 1
if valueIdx < len(node.Content) {
node.Content[valueIdx].Value = value
}
}