Fix live reload when editing inline partials

Fixes #13723
This commit is contained in:
Bjørn Erik Pedersen
2025-05-21 11:25:32 +02:00
parent 970b887ba1
commit 0c7b1a3f26
6 changed files with 227 additions and 90 deletions

View File

@@ -1766,6 +1766,60 @@ MyTemplate: {{ partial "MyTemplate.html" . }}|
b.AssertFileContent("public/index.html", "MyTemplate: MyTemplate Edited") b.AssertFileContent("public/index.html", "MyTemplate: MyTemplate Edited")
} }
func TestRebuildEditInlinePartial13723(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
baseURL = "https://example.com"
disableLiveReload = true
title = "Foo"
-- layouts/baseof.html --
{{ block "main" . }}Main.{{ end }}
{{ partial "myinlinepartialinbaseof.html" . }}|
{{- define "_partials/myinlinepartialinbaseof.html" }}
My inline partial in baseof.
{{ end }}
-- layouts/_partials/mypartial.html --
Mypartial.
{{ partial "myinlinepartial.html" . }}|
{{- define "_partials/myinlinepartial.html" }}
Mypartial Inline.|{{ .Title }}|
{{ end }}
-- layouts/_partials/myotherpartial.html --
Myotherpartial.
{{ partial "myotherinlinepartial.html" . }}|
{{- define "_partials/myotherinlinepartial.html" }}
Myotherpartial Inline.|{{ .Title }}|
{{ return "myotherinlinepartial" }}
{{ end }}
-- layouts/all.html --
{{ define "main" }}
{{ partial "mypartial.html" . }}|
{{ partial "myotherpartial.html" . }}|
{{ partial "myinlinepartialinall.html" . }}|
{{ end }}
{{- define "_partials/myinlinepartialinall.html" }}
My inline partial in all.
{{ end }}
`
b := TestRunning(t, files)
b.AssertFileContent("public/index.html", "Mypartial.", "Mypartial Inline.|Foo")
// Edit inline partial in partial.
b.EditFileReplaceAll("layouts/_partials/mypartial.html", "Mypartial Inline.", "Mypartial Inline Edited.").Build()
b.AssertFileContent("public/index.html", "Mypartial Inline Edited.|Foo")
// Edit inline partial in baseof.
b.EditFileReplaceAll("layouts/baseof.html", "My inline partial in baseof.", "My inline partial in baseof Edited.").Build()
b.AssertFileContent("public/index.html", "My inline partial in baseof Edited.")
// Edit inline partial in all.
b.EditFileReplaceAll("layouts/all.html", "My inline partial in all.", "My inline partial in all Edited.").Build()
b.AssertFileContent("public/index.html", "My inline partial in all Edited.")
}
func TestRebuildEditAsciidocContentFile(t *testing.T) { func TestRebuildEditAsciidocContentFile(t *testing.T) {
if !asciidocext.Supports() { if !asciidocext.Supports() {
t.Skip("skip asciidoc") t.Skip("skip asciidoc")

View File

@@ -15,6 +15,7 @@ package template
import ( import (
"fmt" "fmt"
"iter"
"github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/common/types"
template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate" template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
@@ -38,6 +39,19 @@ func (t *Template) Prepare() (*template.Template, error) {
return t.text, nil return t.text, nil
} }
func (t *Template) All() iter.Seq[*Template] {
return func(yield func(t *Template) bool) {
ns := t.nameSpace
ns.mu.Lock()
defer ns.mu.Unlock()
for _, v := range ns.set {
if !yield(v) {
return
}
}
}
}
// See https://github.com/golang/go/issues/5884 // See https://github.com/golang/go/issues/5884
func StripTags(html string) string { func StripTags(html string) string {
return stripTags(html) return stripTags(html)

View File

@@ -17,6 +17,7 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"iter"
"reflect" "reflect"
"github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/common/herrors"
@@ -433,3 +434,18 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
func isTrue(val reflect.Value) (truth, ok bool) { func isTrue(val reflect.Value) (truth, ok bool) {
return hreflect.IsTruthfulValue(val), true return hreflect.IsTruthfulValue(val), true
} }
func (t *Template) All() iter.Seq[*Template] {
return func(yield func(t *Template) bool) {
if t.common == nil {
return
}
t.muTmpl.RLock()
defer t.muTmpl.RUnlock()
for _, v := range t.tmpl {
if !yield(v) {
return
}
}
}
}

View File

@@ -533,7 +533,7 @@ func (t *Tree) parseControl(context string) (pos Pos, line int, pipe *PipeNode,
t.rangeDepth-- t.rangeDepth--
} }
switch next.Type() { switch next.Type() {
case nodeEnd: //done case nodeEnd: // done
case nodeElse: case nodeElse:
// Special case for "else if" and "else with". // Special case for "else if" and "else with".
// If the "else" is followed immediately by an "if" or "with", // If the "else" is followed immediately by an "if" or "with",

View File

@@ -2,6 +2,7 @@ package tplimpl
import ( import (
"io" "io"
"iter"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
@@ -44,16 +45,15 @@ var embeddedTemplatesAliases = map[string][]string{
"_shortcodes/twitter.html": {"_shortcodes/tweet.html"}, "_shortcodes/twitter.html": {"_shortcodes/tweet.html"},
} }
func (s *TemplateStore) parseTemplate(ti *TemplInfo) error { func (s *TemplateStore) parseTemplate(ti *TemplInfo, replace bool) error {
err := s.tns.doParseTemplate(ti) err := s.tns.doParseTemplate(ti, replace)
if err != nil { if err != nil {
return s.addFileContext(ti, "parse of template failed", err) return s.addFileContext(ti, "parse of template failed", err)
} }
return err return err
} }
func (t *templateNamespace) doParseTemplate(ti *TemplInfo) error { func (t *templateNamespace) doParseTemplate(ti *TemplInfo, replace bool) error {
if !ti.noBaseOf || ti.category == CategoryBaseof { if !ti.noBaseOf || ti.category == CategoryBaseof {
// Delay parsing until we have the base template. // Delay parsing until we have the base template.
return nil return nil
@@ -68,7 +68,7 @@ func (t *templateNamespace) doParseTemplate(ti *TemplInfo) error {
if ti.D.IsPlainText { if ti.D.IsPlainText {
prototype := t.parseText prototype := t.parseText
if prototype.Lookup(name) != nil { if !replace && prototype.Lookup(name) != nil {
name += "-" + strconv.FormatUint(t.nameCounter.Add(1), 10) name += "-" + strconv.FormatUint(t.nameCounter.Add(1), 10)
} }
templ, err = prototype.New(name).Parse(ti.content) templ, err = prototype.New(name).Parse(ti.content)
@@ -77,7 +77,7 @@ func (t *templateNamespace) doParseTemplate(ti *TemplInfo) error {
} }
} else { } else {
prototype := t.parseHTML prototype := t.parseHTML
if prototype.Lookup(name) != nil { if !replace && prototype.Lookup(name) != nil {
name += "-" + strconv.FormatUint(t.nameCounter.Add(1), 10) name += "-" + strconv.FormatUint(t.nameCounter.Add(1), 10)
} }
templ, err = prototype.New(name).Parse(ti.content) templ, err = prototype.New(name).Parse(ti.content)
@@ -181,19 +181,24 @@ func (t *templateNamespace) applyBaseTemplate(overlay *TemplInfo, base keyTempla
return nil return nil
} }
func (t *templateNamespace) templatesIn(in tpl.Template) []tpl.Template { func (t *templateNamespace) templatesIn(in tpl.Template) iter.Seq[tpl.Template] {
var templs []tpl.Template return func(yield func(t tpl.Template) bool) {
if textt, ok := in.(*texttemplate.Template); ok { switch in := in.(type) {
for _, t := range textt.Templates() { case *htmltemplate.Template:
templs = append(templs, t) for t := range in.All() {
if !yield(t) {
return
}
}
case *texttemplate.Template:
for t := range in.All() {
if !yield(t) {
return
}
}
} }
} }
if htmlt, ok := in.(*htmltemplate.Template); ok {
for _, t := range htmlt.Templates() {
templs = append(templs, t)
}
}
return templs
} }
/* /*
@@ -337,8 +342,6 @@ func (t *templateNamespace) createPrototypes(init bool) error {
t.prototypeHTML = htmltemplate.Must(t.parseHTML.Clone()) t.prototypeHTML = htmltemplate.Must(t.parseHTML.Clone())
t.prototypeText = texttemplate.Must(t.parseText.Clone()) t.prototypeText = texttemplate.Must(t.parseText.Clone())
} }
// t.execHTML = htmltemplate.Must(t.parseHTML.Clone())
// t.execText = texttemplate.Must(t.parseText.Clone())
return nil return nil
} }
@@ -350,3 +353,14 @@ func newTemplateNamespace(funcs map[string]any) *templateNamespace {
standaloneText: texttemplate.New("").Funcs(funcs), standaloneText: texttemplate.New("").Funcs(funcs),
} }
} }
func isText(t tpl.Template) bool {
switch t.(type) {
case *texttemplate.Template:
return true
case *htmltemplate.Template:
return false
default:
panic("unknown template type")
}
}

View File

@@ -114,17 +114,18 @@ func NewStore(opts StoreOptions, siteOpts SiteOptions) (*TemplateStore, error) {
panic("HTML output format not found") panic("HTML output format not found")
} }
s := &TemplateStore{ s := &TemplateStore{
opts: opts, opts: opts,
siteOpts: siteOpts, siteOpts: siteOpts,
optsOrig: opts, optsOrig: opts,
siteOptsOrig: siteOpts, siteOptsOrig: siteOpts,
htmlFormat: html, htmlFormat: html,
storeSite: configureSiteStorage(siteOpts, opts.Watching), storeSite: configureSiteStorage(siteOpts, opts.Watching),
treeMain: doctree.NewSimpleTree[map[nodeKey]*TemplInfo](), treeMain: doctree.NewSimpleTree[map[nodeKey]*TemplInfo](),
treeShortcodes: doctree.NewSimpleTree[map[string]map[TemplateDescriptor]*TemplInfo](), treeShortcodes: doctree.NewSimpleTree[map[string]map[TemplateDescriptor]*TemplInfo](),
templatesByPath: maps.NewCache[string, *TemplInfo](), templatesByPath: maps.NewCache[string, *TemplInfo](),
shortcodesByName: maps.NewCache[string, *TemplInfo](), shortcodesByName: maps.NewCache[string, *TemplInfo](),
cacheLookupPartials: maps.NewCache[string, *TemplInfo](), cacheLookupPartials: maps.NewCache[string, *TemplInfo](),
templatesSnapshotSet: maps.NewCache[*parse.Tree, struct{}](),
// Note that the funcs passed below is just for name validation. // Note that the funcs passed below is just for name validation.
tns: newTemplateNamespace(siteOpts.TemplateFuncs), tns: newTemplateNamespace(siteOpts.TemplateFuncs),
@@ -143,10 +144,10 @@ func NewStore(opts StoreOptions, siteOpts SiteOptions) (*TemplateStore, error) {
if err := s.insertEmbedded(); err != nil { if err := s.insertEmbedded(); err != nil {
return nil, err return nil, err
} }
if err := s.parseTemplates(); err != nil { if err := s.parseTemplates(false); err != nil {
return nil, err return nil, err
} }
if err := s.extractInlinePartials(); err != nil { if err := s.extractInlinePartials(false); err != nil {
return nil, err return nil, err
} }
if err := s.transformTemplates(); err != nil { if err := s.transformTemplates(); err != nil {
@@ -424,10 +425,11 @@ type TemplateStore struct {
siteOpts SiteOptions siteOpts SiteOptions
htmlFormat output.Format htmlFormat output.Format
treeMain *doctree.SimpleTree[map[nodeKey]*TemplInfo] treeMain *doctree.SimpleTree[map[nodeKey]*TemplInfo]
treeShortcodes *doctree.SimpleTree[map[string]map[TemplateDescriptor]*TemplInfo] treeShortcodes *doctree.SimpleTree[map[string]map[TemplateDescriptor]*TemplInfo]
templatesByPath *maps.Cache[string, *TemplInfo] templatesByPath *maps.Cache[string, *TemplInfo]
shortcodesByName *maps.Cache[string, *TemplInfo] shortcodesByName *maps.Cache[string, *TemplInfo]
templatesSnapshotSet *maps.Cache[*parse.Tree, struct{}]
dh descriptorHandler dh descriptorHandler
@@ -709,12 +711,16 @@ func (s *TemplateStore) RefreshFiles(include func(fi hugofs.FileMetaInfo) bool)
if err := s.insertTemplates(include, true); err != nil { if err := s.insertTemplates(include, true); err != nil {
return err return err
} }
if err := s.parseTemplates(); err != nil { if err := s.createTemplatesSnapshot(); err != nil {
return err return err
} }
if err := s.extractInlinePartials(); err != nil { if err := s.parseTemplates(true); err != nil {
return err return err
} }
if err := s.extractInlinePartials(true); err != nil {
return err
}
if err := s.transformTemplates(); err != nil { if err := s.transformTemplates(); err != nil {
return err return err
} }
@@ -940,59 +946,77 @@ func (s *TemplateStore) extractIdentifiers(line string) []string {
return identifiers return identifiers
} }
func (s *TemplateStore) extractInlinePartials() error { func (s *TemplateStore) extractInlinePartials(rebuild bool) error {
isPartialName := func(s string) bool { isPartialName := func(s string) bool {
return strings.HasPrefix(s, "partials/") || strings.HasPrefix(s, "_partials/") return strings.HasPrefix(s, "partials/") || strings.HasPrefix(s, "_partials/")
} }
p := s.tns
// We may find both inline and external partials in the current template namespaces, // We may find both inline and external partials in the current template namespaces,
// so only add the ones we have not seen before. // so only add the ones we have not seen before.
addIfNotSeen := func(isText bool, templs ...tpl.Template) error { for templ := range s.allRawTemplates() {
for _, templ := range templs { if templ.Name() == "" || !isPartialName(templ.Name()) {
if templ.Name() == "" || !isPartialName(templ.Name()) { continue
continue
}
name := templ.Name()
if !paths.HasExt(name) {
// Assume HTML. This in line with how the lookup works.
name = name + s.htmlFormat.MediaType.FirstSuffix.FullSuffix
}
if !strings.HasPrefix(name, "_") {
name = "_" + name
}
pi := s.opts.PathParser.Parse(files.ComponentFolderLayouts, name)
ti, err := s.insertTemplate(pi, nil, false, s.treeMain)
if err != nil {
return err
}
if ti != nil {
ti.Template = templ
ti.noBaseOf = true
ti.subCategory = SubCategoryInline
ti.D.IsPlainText = isText
}
} }
return nil if rebuild && s.templatesSnapshotSet.Contains(getParseTree(templ)) {
} // This partial was not created during this build.
addIfNotSeen(false, p.templatesIn(p.parseHTML)...) continue
addIfNotSeen(true, p.templatesIn(p.parseText)...) }
name := templ.Name()
for _, t := range p.baseofHtmlClones { if !paths.HasExt(name) {
if err := addIfNotSeen(false, p.templatesIn(t)...); err != nil { // Assume HTML. This in line with how the lookup works.
name = name + s.htmlFormat.MediaType.FirstSuffix.FullSuffix
}
if !strings.HasPrefix(name, "_") {
name = "_" + name
}
pi := s.opts.PathParser.Parse(files.ComponentFolderLayouts, name)
ti, err := s.insertTemplate(pi, nil, SubCategoryInline, false, s.treeMain)
if err != nil {
return err return err
} }
}
for _, t := range p.baseofTextClones { if ti != nil {
if err := addIfNotSeen(true, p.templatesIn(t)...); err != nil { ti.Template = templ
return err ti.noBaseOf = true
ti.subCategory = SubCategoryInline
ti.D.IsPlainText = isText(templ)
} }
} }
return nil return nil
} }
func (s *TemplateStore) allRawTemplates() iter.Seq[tpl.Template] {
p := s.tns
return func(yield func(tpl.Template) bool) {
for t := range p.templatesIn(p.parseHTML) {
if !yield(t) {
return
}
}
for t := range p.templatesIn(p.parseText) {
if !yield(t) {
return
}
}
for _, tt := range p.baseofHtmlClones {
for t := range p.templatesIn(tt) {
if !yield(t) {
return
}
}
}
for _, tt := range p.baseofTextClones {
for t := range p.templatesIn(tt) {
if !yield(t) {
return
}
}
}
}
}
func (s *TemplateStore) insertEmbedded() error { func (s *TemplateStore) insertEmbedded() error {
return fs.WalkDir(embeddedTemplatesFs, ".", func(path string, d fs.DirEntry, err error) error { return fs.WalkDir(embeddedTemplatesFs, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil { if err != nil {
@@ -1024,7 +1048,7 @@ func (s *TemplateStore) insertEmbedded() error {
return err return err
} }
} else { } else {
ti, err = s.insertTemplate(pi, nil, false, s.treeMain) ti, err = s.insertTemplate(pi, nil, SubCategoryEmbedded, false, s.treeMain)
if err != nil { if err != nil {
return err return err
} }
@@ -1105,7 +1129,7 @@ func (s *TemplateStore) insertShortcode(pi *paths.Path, fi hugofs.FileMetaInfo,
return ti, nil return ti, nil
} }
func (s *TemplateStore) insertTemplate(pi *paths.Path, fi hugofs.FileMetaInfo, replace bool, tree doctree.Tree[map[nodeKey]*TemplInfo]) (*TemplInfo, error) { func (s *TemplateStore) insertTemplate(pi *paths.Path, fi hugofs.FileMetaInfo, subCategory SubCategory, replace bool, tree doctree.Tree[map[nodeKey]*TemplInfo]) (*TemplInfo, error) {
key, _, category, d, err := s.toKeyCategoryAndDescriptor(pi) key, _, category, d, err := s.toKeyCategoryAndDescriptor(pi)
// See #13577. Warn for now. // See #13577. Warn for now.
if err != nil { if err != nil {
@@ -1119,7 +1143,7 @@ func (s *TemplateStore) insertTemplate(pi *paths.Path, fi hugofs.FileMetaInfo, r
return nil, nil return nil, nil
} }
return s.insertTemplate2(pi, fi, key, category, d, replace, false, tree) return s.insertTemplate2(pi, fi, key, category, subCategory, d, replace, false, tree)
} }
func (s *TemplateStore) insertTemplate2( func (s *TemplateStore) insertTemplate2(
@@ -1127,6 +1151,7 @@ func (s *TemplateStore) insertTemplate2(
fi hugofs.FileMetaInfo, fi hugofs.FileMetaInfo,
key string, key string,
category Category, category Category,
subCategory SubCategory,
d TemplateDescriptor, d TemplateDescriptor,
replace, isLegacyMapped bool, replace, isLegacyMapped bool,
tree doctree.Tree[map[nodeKey]*TemplInfo], tree doctree.Tree[map[nodeKey]*TemplInfo],
@@ -1160,6 +1185,11 @@ func (s *TemplateStore) insertTemplate2(
replace = fi.Meta().ModuleOrdinal < nkExisting.Fi.Meta().ModuleOrdinal replace = fi.Meta().ModuleOrdinal < nkExisting.Fi.Meta().ModuleOrdinal
} }
if !replace && existingFound {
// Always replace inline partials to allow for reloading.
replace = subCategory == SubCategoryInline && nkExisting.subCategory == SubCategoryInline
}
if !replace && existingFound { if !replace && existingFound {
if len(pi.Identifiers()) >= len(nkExisting.PathInfo.Identifiers()) { if len(pi.Identifiers()) >= len(nkExisting.PathInfo.Identifiers()) {
// e.g. /pages/home.foo.html and /pages/home.html where foo may be a valid language name in another site. // e.g. /pages/home.foo.html and /pages/home.html where foo may be a valid language name in another site.
@@ -1190,7 +1220,7 @@ func (s *TemplateStore) insertTemplate2(
return ti, nil return ti, nil
} }
func (s *TemplateStore) insertTemplates(include func(fi hugofs.FileMetaInfo) bool, replace bool) error { func (s *TemplateStore) insertTemplates(include func(fi hugofs.FileMetaInfo) bool, partialRebuild bool) error {
if include == nil { if include == nil {
include = func(fi hugofs.FileMetaInfo) bool { include = func(fi hugofs.FileMetaInfo) bool {
return true return true
@@ -1372,7 +1402,7 @@ func (s *TemplateStore) insertTemplates(include func(fi hugofs.FileMetaInfo) boo
} }
if replace && pi.NameNoIdentifier() == baseNameBaseof { if partialRebuild && pi.NameNoIdentifier() == baseNameBaseof {
// A baseof file has changed. // A baseof file has changed.
resetBaseVariants = true resetBaseVariants = true
} }
@@ -1380,12 +1410,12 @@ func (s *TemplateStore) insertTemplates(include func(fi hugofs.FileMetaInfo) boo
var ti *TemplInfo var ti *TemplInfo
var err error var err error
if pi.Type() == paths.TypeShortcode { if pi.Type() == paths.TypeShortcode {
ti, err = s.insertShortcode(pi, fi, replace, s.treeShortcodes) ti, err = s.insertShortcode(pi, fi, partialRebuild, s.treeShortcodes)
if err != nil || ti == nil { if err != nil || ti == nil {
return err return err
} }
} else { } else {
ti, err = s.insertTemplate(pi, fi, replace, s.treeMain) ti, err = s.insertTemplate(pi, fi, SubCategoryMain, partialRebuild, s.treeMain)
if err != nil || ti == nil { if err != nil || ti == nil {
return err return err
} }
@@ -1419,7 +1449,7 @@ func (s *TemplateStore) insertTemplates(include func(fi hugofs.FileMetaInfo) boo
desc.IsPlainText = outputFormat.IsPlainText desc.IsPlainText = outputFormat.IsPlainText
desc.MediaType = mediaType.Type desc.MediaType = mediaType.Type
ti, err := s.insertTemplate2(pi, fi, targetPath, category, desc, true, true, s.treeMain) ti, err := s.insertTemplate2(pi, fi, targetPath, category, SubCategoryMain, desc, true, true, s.treeMain)
if err != nil { if err != nil {
return err return err
} }
@@ -1430,6 +1460,7 @@ func (s *TemplateStore) insertTemplates(include func(fi hugofs.FileMetaInfo) boo
if err := s.tns.readTemplateInto(ti); err != nil { if err := s.tns.readTemplateInto(ti); err != nil {
return err return err
} }
} }
if resetBaseVariants { if resetBaseVariants {
@@ -1456,7 +1487,15 @@ func (s *TemplateStore) key(dir string) string {
return paths.TrimTrailing(dir) return paths.TrimTrailing(dir)
} }
func (s *TemplateStore) parseTemplates() error { func (s *TemplateStore) createTemplatesSnapshot() error {
s.templatesSnapshotSet.Reset()
for t := range s.allRawTemplates() {
s.templatesSnapshotSet.Set(getParseTree(t), struct{}{})
}
return nil
}
func (s *TemplateStore) parseTemplates(replace bool) error {
if err := func() error { if err := func() error {
// Read and parse all templates. // Read and parse all templates.
for _, v := range s.treeMain.All() { for _, v := range s.treeMain.All() {
@@ -1464,7 +1503,7 @@ func (s *TemplateStore) parseTemplates() error {
if vv.state == processingStateTransformed { if vv.state == processingStateTransformed {
continue continue
} }
if err := s.parseTemplate(vv); err != nil { if err := s.parseTemplate(vv, replace); err != nil {
return err return err
} }
} }
@@ -1484,7 +1523,7 @@ func (s *TemplateStore) parseTemplates() error {
// The regular expression used to detect if a template needs a base template has some // The regular expression used to detect if a template needs a base template has some
// rare false positives. Assume we don't need one. // rare false positives. Assume we don't need one.
vv.noBaseOf = true vv.noBaseOf = true
if err := s.parseTemplate(vv); err != nil { if err := s.parseTemplate(vv, replace); err != nil {
return err return err
} }
continue continue
@@ -1513,7 +1552,7 @@ func (s *TemplateStore) parseTemplates() error {
if vvv.state == processingStateTransformed { if vvv.state == processingStateTransformed {
continue continue
} }
if err := s.parseTemplate(vvv); err != nil { if err := s.parseTemplate(vvv, replace); err != nil {
return err return err
} }
} }