Repository: twpayne/chezmoi
Pull request #1033 (2021-02-14 19:38:49)

chezmoi2: Make user data available to config file template
Refs #865.


Patch:
diff --git a/chezmoi2/cmd/executetemplatecmd.go b/chezmoi2/cmd/executetemplatecmd.go
index 896e89419..9bf3fb274 100644
--- a/chezmoi2/cmd/executetemplatecmd.go
+++ b/chezmoi2/cmd/executetemplatecmd.go
@@ -45,7 +45,7 @@ func (c *Config) runExecuteTemplateCmd(cmd *cobra.Command, args []string, source
 		promptBool[key] = value
 	}
 	if c.executeTemplate.init {
-		for name, f := range map[string]interface{}{
+		chezmoi.RecursiveMerge(c.templateFuncs, map[string]interface{}{
 			"promptBool": func(prompt string) bool {
 				return promptBool[prompt]
 			},
@@ -58,9 +58,7 @@ func (c *Config) runExecuteTemplateCmd(cmd *cobra.Command, args []string, source
 				}
 				return prompt
 			},
-		} {
-			c.templateFuncs[name] = f
-		}
+		})
 	}
 
 	if len(args) == 0 {
diff --git a/chezmoi2/cmd/initcmd.go b/chezmoi2/cmd/initcmd.go
index eae4e554f..2ca4ed36b 100644
--- a/chezmoi2/cmd/initcmd.go
+++ b/chezmoi2/cmd/initcmd.go
@@ -157,18 +157,18 @@ func (c *Config) runInitCmd(cmd *cobra.Command, args []string) error {
 	}
 
 	// Find config template, execute it, and create config file.
-	filename, ext, data, err := c.findConfigTemplate()
+	filename, ext, configTemplateContents, err := c.findConfigTemplate()
 	if err != nil {
 		return err
 	}
 	var configFileContents []byte
 	if filename != "" {
-		configFileContents, err = c.createConfigFile(filename, data)
+		configFileContents, err = c.createConfigFile(filename, configTemplateContents)
 		if err != nil {
 			return err
 		}
 		configStateValue, err := json.Marshal(configState{
-			ConfigTemplateContentsSHA256: chezmoi.HexBytes(chezmoi.SHA256Sum(configFileContents)),
+			ConfigTemplateContentsSHA256: chezmoi.HexBytes(chezmoi.SHA256Sum(configTemplateContents)),
 		})
 		if err != nil {
 			return err
@@ -218,16 +218,12 @@ func (c *Config) runInitCmd(cmd *cobra.Command, args []string) error {
 // contents.
 func (c *Config) createConfigFile(filename chezmoi.RelPath, data []byte) ([]byte, error) {
 	funcMap := make(template.FuncMap)
-	for key, value := range c.templateFuncs {
-		funcMap[key] = value
-	}
-	for name, f := range map[string]interface{}{
+	chezmoi.RecursiveMerge(funcMap, c.templateFuncs)
+	chezmoi.RecursiveMerge(funcMap, map[string]interface{}{
 		"promptBool":   c.promptBool,
 		"promptInt":    c.promptInt,
 		"promptString": c.promptString,
-	} {
-		funcMap[name] = f
-	}
+	})
 
 	t, err := template.New(string(filename)).Funcs(funcMap).Parse(string(data))
 	if err != nil {
@@ -235,7 +231,9 @@ func (c *Config) createConfigFile(filename chezmoi.RelPath, data []byte) ([]byte
 	}
 
 	sb := strings.Builder{}
-	if err = t.Execute(&sb, c.defaultTemplateData()); err != nil {
+	templateData := c.defaultTemplateData()
+	chezmoi.RecursiveMerge(templateData, c.Data)
+	if err = t.Execute(&sb, templateData); err != nil {
 		return nil, err
 	}
 	contents := []byte(sb.String())
diff --git a/chezmoi2/internal/chezmoi/recursivemerge.go b/chezmoi2/internal/chezmoi/recursivemerge.go
index 86f2fd94a..e7cad2b3c 100644
--- a/chezmoi2/internal/chezmoi/recursivemerge.go
+++ b/chezmoi2/internal/chezmoi/recursivemerge.go
@@ -17,8 +17,8 @@ func recursiveCopy(v interface{}) interface{} {
 	return c
 }
 
-// recursiveMerge recursively merges maps in source into dest.
-func recursiveMerge(dest, source map[string]interface{}) {
+// RecursiveMerge recursively merges maps in source into dest.
+func RecursiveMerge(dest, source map[string]interface{}) {
 	for key, sourceValue := range source {
 		destValue, ok := dest[key]
 		if !ok {
@@ -35,6 +35,6 @@ func recursiveMerge(dest, source map[string]interface{}) {
 			dest[key] = recursiveCopy(sourceValue)
 			continue
 		}
-		recursiveMerge(destMap, sourceMap)
+		RecursiveMerge(destMap, sourceMap)
 	}
 }
diff --git a/chezmoi2/internal/chezmoi/sourcestate.go b/chezmoi2/internal/chezmoi/sourcestate.go
index 6ce337034..466b18cab 100644
--- a/chezmoi2/internal/chezmoi/sourcestate.go
+++ b/chezmoi2/internal/chezmoi/sourcestate.go
@@ -57,7 +57,7 @@ func WithEncryption(encryption Encryption) SourceStateOption {
 // WithPriorityTemplateData adds priority template data.
 func WithPriorityTemplateData(priorityTemplateData map[string]interface{}) SourceStateOption {
 	return func(s *SourceState) {
-		recursiveMerge(s.priorityTemplateData, priorityTemplateData)
+		RecursiveMerge(s.priorityTemplateData, priorityTemplateData)
 	}
 }
 
@@ -647,11 +647,11 @@ func (s *SourceState) TemplateData() map[string]interface{} {
 	if s.templateData == nil {
 		s.templateData = make(map[string]interface{})
 		if s.defaultTemplateDataFunc != nil {
-			recursiveMerge(s.templateData, s.defaultTemplateDataFunc())
+			RecursiveMerge(s.templateData, s.defaultTemplateDataFunc())
 			s.defaultTemplateDataFunc = nil
 		}
-		recursiveMerge(s.templateData, s.userTemplateData)
-		recursiveMerge(s.templateData, s.priorityTemplateData)
+		RecursiveMerge(s.templateData, s.userTemplateData)
+		RecursiveMerge(s.templateData, s.priorityTemplateData)
 	}
 	return s.templateData
 }
@@ -708,7 +708,7 @@ func (s *SourceState) addTemplateData(sourceAbsPath AbsPath) error {
 	if err := format.Unmarshal(data, &templateData); err != nil {
 		return fmt.Errorf("%s: %w", sourceAbsPath, err)
 	}
-	recursiveMerge(s.userTemplateData, templateData)
+	RecursiveMerge(s.userTemplateData, templateData)
 	return nil
 }
 


Test patch:
diff --git a/chezmoi2/internal/chezmoi/recursivemerge_test.go b/chezmoi2/internal/chezmoi/recursivemerge_test.go
index 4c07fc48e..e1b49020c 100644
--- a/chezmoi2/internal/chezmoi/recursivemerge_test.go
+++ b/chezmoi2/internal/chezmoi/recursivemerge_test.go
@@ -49,7 +49,7 @@ func TestRecursiveMerge(t *testing.T) {
 			},
 		},
 	} {
-		recursiveMerge(tc.dest, tc.source)
+		RecursiveMerge(tc.dest, tc.source)
 		assert.Equal(t, tc.expectedDest, tc.dest)
 	}
 }
@@ -59,8 +59,8 @@ func TestRecursiveMergeCopies(t *testing.T) {
 		"key": "initialValue",
 	}
 	dest := make(map[string]interface{})
-	recursiveMerge(dest, original)
-	recursiveMerge(dest, map[string]interface{}{
+	RecursiveMerge(dest, original)
+	RecursiveMerge(dest, map[string]interface{}{
 		"key": "mergedValue",
 	})
 	assert.Equal(t, "mergedValue", dest["key"])
diff --git a/chezmoi2/internal/chezmoi/sourcestate_test.go b/chezmoi2/internal/chezmoi/sourcestate_test.go
index 2c8dbb179..74cba924b 100644
--- a/chezmoi2/internal/chezmoi/sourcestate_test.go
+++ b/chezmoi2/internal/chezmoi/sourcestate_test.go
@@ -1269,7 +1269,7 @@ func withMinVersion(minVersion semver.Version) SourceStateOption {
 // withUserTemplateData adds template data.
 func withUserTemplateData(templateData map[string]interface{}) SourceStateOption {
 	return func(s *SourceState) {
-		recursiveMerge(s.userTemplateData, templateData)
+		RecursiveMerge(s.userTemplateData, templateData)
 	}
 }
 
diff --git a/chezmoi2/testdata/scripts/configstate.txt b/chezmoi2/testdata/scripts/configstate.txt
index e00f4f092..47a5f582b 100644
--- a/chezmoi2/testdata/scripts/configstate.txt
+++ b/chezmoi2/testdata/scripts/configstate.txt
@@ -8,13 +8,13 @@ cmp stdout golden/state-dump.yaml
 chezmoi apply
 
 # test that chezmoi apply prints a warning if the config file template has been changed
-edit $CHEZMOISOURCEDIR/.chezmoi.toml.tmpl
+cp golden/.chezmoi.toml.tmpl $CHEZMOISOURCEDIR/.chezmoi.toml.tmpl
 chezmoi apply
 stderr 'warning: config file template has changed'
 
 # test that chezmoi init re-generates the config file
 chezmoi init
-grep '# edited' $CHEZMOICONFIGDIR/chezmoi.toml
+cmp $CHEZMOICONFIGDIR/chezmoi.toml golden/chezmoi.toml
 
 # test that chezmoi apply no longer prints a warning after the config file is regenerated
 chezmoi apply
@@ -22,11 +22,18 @@ chezmoi apply
 
 # test that chezmoi apply --force ignores config file changes and updates the state
 edit $CHEZMOISOURCEDIR/.chezmoi.toml.tmpl
-grep -count=2 '# edited' $CHEZMOISOURCEDIR/.chezmoi.toml.tmpl
+grep '# edited' $CHEZMOISOURCEDIR/.chezmoi.toml.tmpl
 chezmoi apply --force
 ! stderr .
-grep -count=1 '# edited' $CHEZMOICONFIGDIR/chezmoi.toml
+! grep '# edited' $CHEZMOICONFIGDIR/chezmoi.toml
 
+-- golden/.chezmoi.toml.tmpl --
+{{ $email := get . "email" -}}
+{{ if not $email -}}
+{{   $email = promptString "email" -}}
+{{ end -}}
+[data]
+  email = {{ $email | quote }}
 -- golden/chezmoi.toml --
 [data]
   email = "user@home.org"