Skip to content

Commit

Permalink
Only create templates with chezmoi add --autotemplate if a replacemen…
Browse files Browse the repository at this point in the history
…t occurred
  • Loading branch information
twpayne committed May 17, 2021
1 parent 8a86a3d commit 3ca09db
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 23 deletions.
8 changes: 6 additions & 2 deletions internal/chezmoi/autotemplate.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,16 @@ func (b byValueLength) Less(i, j int) bool {
}
func (b byValueLength) Swap(i, j int) { b[i], b[j] = b[j], b[i] }

func autoTemplate(contents []byte, data map[string]interface{}) []byte {
// autoTemplate converts contents into a template by replacing values in data
// with their keys. It returns the template and if any replacements were made.
func autoTemplate(contents []byte, data map[string]interface{}) ([]byte, bool) {
// This naive approach will generate incorrect templates if the variable
// names match variable values. The algorithm here is probably O(N^2), we
// can do better.
variables := extractVariables(data)
sort.Sort(sort.Reverse(byValueLength(variables)))
contentsStr := string(contents)
replacements := false
for _, variable := range variables {
if variable.value == "" {
continue
Expand All @@ -48,6 +51,7 @@ func autoTemplate(contents []byte, data map[string]interface{}) []byte {
replacement := "{{ ." + variable.name + " }}"
contentsStr = contentsStr[:index] + replacement + contentsStr[index+len(variable.value):]
index += len(replacement)
replacements = true
} else {
// Otherwise, keep looking. Consume at least one byte so we make
// progress.
Expand All @@ -64,7 +68,7 @@ func autoTemplate(contents []byte, data map[string]interface{}) []byte {
}
}
}
return []byte(contentsStr)
return []byte(contentsStr), replacements
}

// extractVariables extracts all template variables from data.
Expand Down
44 changes: 29 additions & 15 deletions internal/chezmoi/autotemplate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,20 @@ import (

func TestAutoTemplate(t *testing.T) {
for _, tc := range []struct {
name string
contentsStr string
data map[string]interface{}
expected string
name string
contentsStr string
data map[string]interface{}
expected string
expectedReplacements bool
}{
{
name: "simple",
contentsStr: "email = you@example.com\n",
data: map[string]interface{}{
"email": "you@example.com",
},
expected: "email = {{ .email }}\n",
expected: "email = {{ .email }}\n",
expectedReplacements: true,
},
{
name: "longest_first",
Expand All @@ -31,6 +33,7 @@ func TestAutoTemplate(t *testing.T) {
expected: "" +
"name = {{ .name }}\n" +
"firstName = {{ .firstName }}\n",
expectedReplacements: true,
},
{
name: "alphabetical_first",
Expand All @@ -40,7 +43,8 @@ func TestAutoTemplate(t *testing.T) {
"beta": "John Smith",
"gamma": "John Smith",
},
expected: "name = {{ .alpha }}\n",
expected: "name = {{ .alpha }}\n",
expectedReplacements: true,
},
{
name: "nested_values",
Expand All @@ -50,7 +54,8 @@ func TestAutoTemplate(t *testing.T) {
"email": "you@example.com",
},
},
expected: "email = {{ .personal.email }}\n",
expected: "email = {{ .personal.email }}\n",
expectedReplacements: true,
},
{
name: "only_replace_words",
Expand All @@ -66,55 +71,62 @@ func TestAutoTemplate(t *testing.T) {
data: map[string]interface{}{
"homeDir": "/home/user",
},
expected: "{{ .homeDir }}",
expected: "{{ .homeDir }}",
expectedReplacements: true,
},
{
name: "longest_match_first_prefix",
contentsStr: "HOME=/home/user",
data: map[string]interface{}{
"homeDir": "/home/user",
},
expected: "HOME={{ .homeDir }}",
expected: "HOME={{ .homeDir }}",
expectedReplacements: true,
},
{
name: "longest_match_first_suffix",
contentsStr: "/home/user/something",
data: map[string]interface{}{
"homeDir": "/home/user",
},
expected: "{{ .homeDir }}/something",
expected: "{{ .homeDir }}/something",
expectedReplacements: true,
},
{
name: "longest_match_first_prefix_and_suffix",
contentsStr: "HOME=/home/user/something",
data: map[string]interface{}{
"homeDir": "/home/user",
},
expected: "HOME={{ .homeDir }}/something",
expected: "HOME={{ .homeDir }}/something",
expectedReplacements: true,
},
{
name: "words_only",
contentsStr: "aaa aa a aa aaa aa a aa aaa",
data: map[string]interface{}{
"alpha": "a",
},
expected: "aaa aa {{ .alpha }} aa aaa aa {{ .alpha }} aa aaa",
expected: "aaa aa {{ .alpha }} aa aaa aa {{ .alpha }} aa aaa",
expectedReplacements: true,
},
{
name: "words_only_2",
contentsStr: "aaa aa a aa aaa aa a aa aaa",
data: map[string]interface{}{
"alpha": "aa",
},
expected: "aaa {{ .alpha }} a {{ .alpha }} aaa {{ .alpha }} a {{ .alpha }} aaa",
expected: "aaa {{ .alpha }} a {{ .alpha }} aaa {{ .alpha }} a {{ .alpha }} aaa",
expectedReplacements: true,
},
{
name: "words_only_3",
contentsStr: "aaa aa a aa aaa aa a aa aaa",
data: map[string]interface{}{
"alpha": "aaa",
},
expected: "{{ .alpha }} aa a aa {{ .alpha }} aa a aa {{ .alpha }}",
expected: "{{ .alpha }} aa a aa {{ .alpha }} aa a aa {{ .alpha }}",
expectedReplacements: true,
},
{
name: "skip_empty",
Expand All @@ -126,7 +138,9 @@ func TestAutoTemplate(t *testing.T) {
},
} {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, string(autoTemplate([]byte(tc.contentsStr), tc.data)))
actualTemplate, actualReplacements := autoTemplate([]byte(tc.contentsStr), tc.data)
assert.Equal(t, tc.expected, string(actualTemplate))
assert.Equal(t, tc.expectedReplacements, actualReplacements)
})
}
}
Expand Down
11 changes: 7 additions & 4 deletions internal/chezmoi/sourcestate.go
Original file line number Diff line number Diff line change
Expand Up @@ -1123,7 +1123,7 @@ func (s *SourceState) newSourceStateFileEntryFromFile(actualStateFile *ActualSta
Encrypted: options.Encrypt,
Executable: isExecutable(info),
Private: isPrivate(info),
Template: options.Template || options.AutoTemplate,
Template: options.Template,
}
if options.Create {
fileAttr.Type = SourceFileTypeCreate
Expand All @@ -1135,7 +1135,11 @@ func (s *SourceState) newSourceStateFileEntryFromFile(actualStateFile *ActualSta
return nil, err
}
if options.AutoTemplate {
contents = autoTemplate(contents, s.TemplateData())
var replacements bool
contents, replacements = autoTemplate(contents, s.TemplateData())
if replacements {
fileAttr.Template = true
}
}
if len(contents) == 0 && !options.Empty {
return nil, nil
Expand Down Expand Up @@ -1169,8 +1173,7 @@ func (s *SourceState) newSourceStateFileEntryFromSymlink(actualStateSymlink *Act
template := false
switch {
case options.AutoTemplate:
contents = autoTemplate(contents, s.TemplateData())
fallthrough
contents, template = autoTemplate(contents, s.TemplateData())
case options.Template:
template = true
case !options.Template && options.TemplateSymlinks:
Expand Down
12 changes: 10 additions & 2 deletions testdata/scripts/addautotemplate.txt
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
# test adding a file with --autotemplate
# test that chezmoi add --autotemplate on a file with a replacement creates a template in the source directory
chezmoi add --autotemplate $HOME${/}.template
cmp $CHEZMOISOURCEDIR/dot_template.tmpl golden/dot_template.tmpl

# test adding a symlink with --autotemplate
# test that chezmoi add --autotemplate on a symlink with a replacement creates a template in the source directory
symlink $HOME/.symlink -> .target-value
chezmoi add --autotemplate $HOME${/}.symlink
cmp $CHEZMOISOURCEDIR/symlink_dot_symlink.tmpl golden/symlink_dot_symlink.tmpl

# test that chezmoi add --autotemplate does not create a template if no replacements occurred
chezmoi add --autotemplate $HOME${/}.file
cmp $CHEZMOISOURCEDIR/dot_file golden/dot_file

-- golden/dot_file --
# contents of .file
-- golden/dot_template.tmpl --
key = {{ .variable }}
-- golden/symlink_dot_symlink.tmpl --
.target-{{ .variable }}
-- home/user/.config/chezmoi/chezmoi.toml --
[data]
variable = "value"
-- home/user/.file --
# contents of .file
-- home/user/.template --
key = value

0 comments on commit 3ca09db

Please sign in to comment.