Repository: twpayne/chezmoi
Pull request #1022 (2021-02-04 17:17:32)

More chezmoi2 fixes


Patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 78f7c4b84..845bd196e 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -22,6 +22,7 @@ jobs:
       with:
         go-version: 1.15.x
     - name: Cache Go modules
+      if: runner.os != 'macos' # FIXME re-enable
       uses: actions/cache@v2
       with:
         path: ~/go/pkg/mod
@@ -151,7 +152,7 @@ jobs:
       - name: Lint
         uses: golangci/golangci-lint-action@v2
         with:
-          version: v1.35.2
+          version: v1.36.0
       - name: ShellCheck
         uses: ludeeus/action-shellcheck@1.0.0
         with:
diff --git a/.golangci.yml b/.golangci.yml
index 1db611fd5..05a55459b 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -12,7 +12,6 @@ linters:
   - exportloopref
   - forbidigo
   - gci
-  - gochecknoinits
   - gocritic
   - godot
   - goerr113
@@ -25,11 +24,11 @@ linters:
   - gosec
   - gosimple
   - govet
+  - ifshort
   - ineffassign
   - interfacer
   - makezero
   - misspell
-  - paralleltest
   - prealloc
   - predeclared
   - rowserrcheck
@@ -39,7 +38,6 @@ linters:
   - structcheck
   - stylecheck
   - thelper
-  - tparallel
   - typecheck
   - unconvert
   - unparam
@@ -50,6 +48,7 @@ linters:
   - exhaustivestruct
   - funlen
   - gochecknoglobals
+  - gochecknoinits
   - gocognit
   - goconst
   - gocyclo
@@ -62,8 +61,10 @@ linters:
   - nestif
   - nlreturn
   - noctx
-  - nolintlint # FIXME renable
+  - nolintlint # FIXME re-enable
+  - paralleltest
   - testpackage
+  - tparallel
   - wrapcheck
   - wsl
 
@@ -82,30 +83,15 @@ issues:
     text: "do not define dynamic errors, use wrapped static errors instead"
   - linters:
     - dupl
-    path: "secretgopass.go"
-  - linters:
-    - dupl
-    path: "secretpass.go"
-  - linters:
-    - nolintlint
-    - paralleltest
-    path: "^main_test.go$"
+    path: "^cmd/secret(go)?pass\\.go$"
   - linters:
     - forbidigo
-    - gochecknoinits
-    - paralleltest
-    path: cmd/
-  - linters:
-    - gochecknoinits
-    path: internal/chezmoi/chezmoi_unix.go
+    path: ^cmd/
   - linters:
     - forbidigo
     - gosec
-    path: internal/cmd/
-  - linters:
-    - paralleltest
-    path: internal/
+    path: ^internal/cmd/
   - linters:
     - gosec
     - scopelint
-    path: "_test\\.go"
+    path: "_test\\.go$"
diff --git a/Makefile b/Makefile
index 26f12d575..9ae800390 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-GOLANGCI_LINT_VERSION=1.35.2
+GOLANGCI_LINT_VERSION=1.36.0
 
 .PHONY: default
 default: generate build run test lint format
diff --git a/chezmoi2/cmd/applycmd.go b/chezmoi2/cmd/applycmd.go
index a5d95eabf..e60f1230e 100644
--- a/chezmoi2/cmd/applycmd.go
+++ b/chezmoi2/cmd/applycmd.go
@@ -41,7 +41,7 @@ func (c *Config) runApplyCmd(cmd *cobra.Command, args []string) error {
 		recursive:     c.apply.recursive,
 		skipEncrypted: c.apply.skipEncrypted,
 		sourcePath:    c.apply.sourcePath,
-		umask:         c.Umask.FileMode(),
+		umask:         c.Umask,
 		preApplyFunc:  c.defaultPreApplyFunc,
 	})
 }
diff --git a/chezmoi2/cmd/archivecmd.go b/chezmoi2/cmd/archivecmd.go
index f31e31685..897f72786 100644
--- a/chezmoi2/cmd/archivecmd.go
+++ b/chezmoi2/cmd/archivecmd.go
@@ -4,7 +4,6 @@ import (
 	"archive/tar"
 	"compress/gzip"
 	"fmt"
-	"os"
 	"os/user"
 	"strconv"
 	"strings"
@@ -60,7 +59,6 @@ func (c *Config) runArchiveCmd(cmd *cobra.Command, args []string) error {
 	if err := c.applyArgs(archiveSystem, "", args, applyArgsOptions{
 		include:   c.archive.include,
 		recursive: c.archive.recursive,
-		umask:     os.ModePerm,
 	}); err != nil {
 		return err
 	}
diff --git a/chezmoi2/cmd/config.go b/chezmoi2/cmd/config.go
index 17ebeb38b..68bb54e15 100644
--- a/chezmoi2/cmd/config.go
+++ b/chezmoi2/cmd/config.go
@@ -63,7 +63,7 @@ type Config struct {
 	// Global configuration, settable in the config file.
 	SourceDir     string                 `mapstructure:"sourceDir"`
 	DestDir       string                 `mapstructure:"destDir"`
-	Umask         fileMode               `mapstructure:"umask"`
+	Umask         os.FileMode            `mapstructure:"umask"`
 	Remove        bool                   `mapstructure:"remove"`
 	Color         string                 `mapstructure:"color"`
 	Data          map[string]interface{} `mapstructure:"data"`
@@ -174,7 +174,7 @@ func newConfig(options ...configOption) (*Config, error) {
 		fs:      vfs.OSFS,
 		homeDir: homeDir,
 		DestDir: homeDir,
-		Umask:   fileMode(chezmoi.GetUmask()),
+		Umask:   chezmoi.Umask,
 		Color:   "auto",
 		Diff: diffCmdConfig{
 			include: chezmoi.NewIncludeSet(chezmoi.IncludeAll &^ chezmoi.IncludeScripts),
@@ -403,7 +403,7 @@ func (c *Config) defaultPreApplyFunc(targetRelPath chezmoi.RelPath, targetEntryS
 		return nil
 	case lastWrittenEntryState == nil:
 		return nil
-	case lastWrittenEntryState.Equivalent(actualEntryState, c.Umask.FileMode()):
+	case lastWrittenEntryState.Equivalent(actualEntryState):
 		return nil
 	}
 
@@ -1319,6 +1319,10 @@ func (c *Config) withTerminal(prompt string, f func(terminal) error) error {
 
 	if stdinFile, ok := c.stdin.(*os.File); ok && term.IsTerminal(int(stdinFile.Fd())) {
 		fd := int(stdinFile.Fd())
+		width, height, err := term.GetSize(fd)
+		if err != nil {
+			return err
+		}
 		oldState, err := term.MakeRaw(fd)
 		if err != nil {
 			return err
@@ -1326,13 +1330,17 @@ func (c *Config) withTerminal(prompt string, f func(terminal) error) error {
 		defer func() {
 			_ = term.Restore(fd, oldState)
 		}()
-		return f(term.NewTerminal(struct {
+		t := term.NewTerminal(struct {
 			io.Reader
 			io.Writer
 		}{
 			Reader: c.stdin,
 			Writer: c.stdout,
-		}, prompt))
+		}, prompt)
+		if err := t.SetSize(width, height); err != nil {
+			return err
+		}
+		return f(t)
 	}
 
 	if runtime.GOOS == "windows" {
@@ -1345,6 +1353,10 @@ func (c *Config) withTerminal(prompt string, f func(terminal) error) error {
 	}
 	defer devTTY.Close()
 	fd := int(devTTY.Fd())
+	width, height, err := term.GetSize(fd)
+	if err != nil {
+		return err
+	}
 	oldState, err := term.MakeRaw(fd)
 	if err != nil {
 		return err
@@ -1352,7 +1364,11 @@ func (c *Config) withTerminal(prompt string, f func(terminal) error) error {
 	defer func() {
 		_ = term.Restore(fd, oldState)
 	}()
-	return f(term.NewTerminal(devTTY, prompt))
+	t := term.NewTerminal(devTTY, prompt)
+	if err := t.SetSize(width, height); err != nil {
+		return err
+	}
+	return f(t)
 }
 
 func (c *Config) writeOutput(data []byte) error {
diff --git a/chezmoi2/cmd/diffcmd.go b/chezmoi2/cmd/diffcmd.go
index a932ec6d7..60c30c583 100644
--- a/chezmoi2/cmd/diffcmd.go
+++ b/chezmoi2/cmd/diffcmd.go
@@ -40,7 +40,7 @@ func (c *Config) runDiffCmd(cmd *cobra.Command, args []string) error {
 	if err := c.applyArgs(gitDiffSystem, c.destDirAbsPath, args, applyArgsOptions{
 		include:   c.Diff.include,
 		recursive: c.Diff.recursive,
-		umask:     c.Umask.FileMode(),
+		umask:     c.Umask,
 	}); err != nil {
 		return err
 	}
diff --git a/chezmoi2/cmd/doctorcmd.go b/chezmoi2/cmd/doctorcmd.go
index 9f4886ede..2270ac604 100644
--- a/chezmoi2/cmd/doctorcmd.go
+++ b/chezmoi2/cmd/doctorcmd.go
@@ -216,6 +216,7 @@ func (c *Config) runDoctorCmd(cmd *cobra.Command, args []string) error {
 		},
 	}
 
+	//nolint:ifshort
 	worstResult := checkOK
 	resultWriter := tabwriter.NewWriter(c.stdout, 3, 0, 3, ' ', 0)
 	fmt.Fprint(resultWriter, "RESULT\tCHECK\tMESSAGE\n")
diff --git a/chezmoi2/cmd/dumpcmd.go b/chezmoi2/cmd/dumpcmd.go
index ccbdd746f..0ce588136 100644
--- a/chezmoi2/cmd/dumpcmd.go
+++ b/chezmoi2/cmd/dumpcmd.go
@@ -1,8 +1,6 @@
 package cmd
 
 import (
-	"os"
-
 	"github.com/spf13/cobra"
 
 	"github.com/twpayne/chezmoi/chezmoi2/internal/chezmoi"
@@ -39,7 +37,6 @@ func (c *Config) runDumpCmd(cmd *cobra.Command, args []string) error {
 	if err := c.applyArgs(dumpSystem, "", args, applyArgsOptions{
 		include:   c.dump.include,
 		recursive: c.dump.recursive,
-		umask:     os.ModePerm,
 	}); err != nil {
 		return err
 	}
diff --git a/chezmoi2/cmd/editcmd.go b/chezmoi2/cmd/editcmd.go
index cccf43990..023743b4f 100644
--- a/chezmoi2/cmd/editcmd.go
+++ b/chezmoi2/cmd/editcmd.go
@@ -48,7 +48,7 @@ func (c *Config) runEditCmd(cmd *cobra.Command, args []string, sourceState *chez
 			if err := c.applyArgs(c.destSystem, c.destDirAbsPath, noArgs, applyArgsOptions{
 				include:      c.Edit.include,
 				recursive:    true,
-				umask:        c.Umask.FileMode(),
+				umask:        c.Umask,
 				preApplyFunc: c.defaultPreApplyFunc,
 			}); err != nil {
 				return err
@@ -131,7 +131,7 @@ func (c *Config) runEditCmd(cmd *cobra.Command, args []string, sourceState *chez
 		if err := c.applyArgs(c.destSystem, c.destDirAbsPath, args, applyArgsOptions{
 			include:      c.Edit.include,
 			recursive:    false,
-			umask:        c.Umask.FileMode(),
+			umask:        c.Umask,
 			preApplyFunc: c.defaultPreApplyFunc,
 		}); err != nil {
 			return err
diff --git a/chezmoi2/cmd/filemode.go b/chezmoi2/cmd/filemode.go
deleted file mode 100644
index 9f4bc74f9..000000000
--- a/chezmoi2/cmd/filemode.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package cmd
-
-import (
-	"fmt"
-	"os"
-	"strconv"
-)
-
-// A fileMode represents a file mode. It implements the
-// github.com/spf13/pflag.Value interface for use as a command line flag.
-type fileMode os.FileMode
-
-func (m *fileMode) FileMode() os.FileMode {
-	return os.FileMode(*m)
-}
-
-func (m *fileMode) Set(s string) error {
-	mUint64, err := strconv.ParseUint(s, 8, 32)
-	if err != nil || os.FileMode(mUint64)&os.ModePerm != os.FileMode(mUint64) {
-		return fmt.Errorf("%s: invalid mode", s)
-	}
-	*m = fileMode(mUint64)
-	return nil
-}
-
-func (m *fileMode) String() string {
-	return fmt.Sprintf("%03o", *m)
-}
-
-func (m *fileMode) Type() string {
-	return "file mode"
-}
diff --git a/chezmoi2/cmd/initcmd.go b/chezmoi2/cmd/initcmd.go
index b0737b0ea..8da4f8e4f 100644
--- a/chezmoi2/cmd/initcmd.go
+++ b/chezmoi2/cmd/initcmd.go
@@ -188,7 +188,7 @@ func (c *Config) runInitCmd(cmd *cobra.Command, args []string) error {
 		if err := c.applyArgs(c.destSystem, c.destDirAbsPath, noArgs, applyArgsOptions{
 			include:       chezmoi.NewIncludeSet(chezmoi.IncludeAll),
 			recursive:     false,
-			umask:         c.Umask.FileMode(),
+			umask:         c.Umask,
 			preApplyFunc:  c.defaultPreApplyFunc,
 			skipEncrypted: c.init.skipEncrypted,
 		}); err != nil {
diff --git a/chezmoi2/cmd/keepassxctemplatefuncs.go b/chezmoi2/cmd/keepassxctemplatefuncs.go
index 809f3cf9b..7ef72b037 100644
--- a/chezmoi2/cmd/keepassxctemplatefuncs.go
+++ b/chezmoi2/cmd/keepassxctemplatefuncs.go
@@ -121,7 +121,6 @@ func (c *Config) keepassxcVersion() *semver.Version {
 func (c *Config) runKeepassxcCLICommand(name string, args []string) ([]byte, error) {
 	if c.Keepassxc.password == "" {
 		password, err := c.readPassword(fmt.Sprintf("Insert password to unlock %s: ", c.Keepassxc.Database))
-		fmt.Println()
 		if err != nil {
 			return nil, err
 		}
diff --git a/chezmoi2/cmd/statuscmd.go b/chezmoi2/cmd/statuscmd.go
index dc8310904..4d330ac25 100644
--- a/chezmoi2/cmd/statuscmd.go
+++ b/chezmoi2/cmd/statuscmd.go
@@ -2,7 +2,6 @@ package cmd
 
 import (
 	"fmt"
-	"os"
 	"strings"
 
 	"github.com/spf13/cobra"
@@ -46,9 +45,9 @@ func (c *Config) runStatusCmd(cmd *cobra.Command, args []string, sourceState *ch
 		switch {
 		case targetEntryState.Type == chezmoi.EntryStateTypeScript:
 			y = 'R'
-		case !targetEntryState.Equivalent(actualEntryState, c.Umask.FileMode()):
-			x = statusRune(lastWrittenEntryState, actualEntryState, c.Umask.FileMode())
-			y = statusRune(actualEntryState, targetEntryState, c.Umask.FileMode())
+		case !targetEntryState.Equivalent(actualEntryState):
+			x = statusRune(lastWrittenEntryState, actualEntryState)
+			y = statusRune(actualEntryState, targetEntryState)
 		}
 		if x != ' ' || y != ' ' {
 			fmt.Fprintf(&sb, "%c%c %s\n", x, y, targetRelPath)
@@ -58,7 +57,7 @@ func (c *Config) runStatusCmd(cmd *cobra.Command, args []string, sourceState *ch
 	if err := c.applyArgs(dryRunSystem, c.destDirAbsPath, args, applyArgsOptions{
 		include:      c.status.include,
 		recursive:    c.status.recursive,
-		umask:        c.Umask.FileMode(),
+		umask:        c.Umask,
 		preApplyFunc: preApplyFunc,
 	}); err != nil {
 		return err
@@ -66,8 +65,8 @@ func (c *Config) runStatusCmd(cmd *cobra.Command, args []string, sourceState *ch
 	return c.writeOutputString(sb.String())
 }
 
-func statusRune(fromState, toState *chezmoi.EntryState, umask os.FileMode) rune {
-	if fromState == nil || fromState.Equivalent(toState, umask) {
+func statusRune(fromState, toState *chezmoi.EntryState) rune {
+	if fromState == nil || fromState.Equivalent(toState) {
 		return ' '
 	}
 	switch toState.Type {
diff --git a/chezmoi2/cmd/updatecmd.go b/chezmoi2/cmd/updatecmd.go
index 774c8aa5e..8c85e5ccc 100644
--- a/chezmoi2/cmd/updatecmd.go
+++ b/chezmoi2/cmd/updatecmd.go
@@ -79,7 +79,7 @@ func (c *Config) runUpdateCmd(cmd *cobra.Command, args []string) error {
 	return c.applyArgs(c.destSystem, c.destDirAbsPath, args, applyArgsOptions{
 		include:      c.update.include,
 		recursive:    c.update.recursive,
-		umask:        c.Umask.FileMode(),
+		umask:        c.Umask,
 		preApplyFunc: c.defaultPreApplyFunc,
 	})
 }
diff --git a/chezmoi2/cmd/verifycmd.go b/chezmoi2/cmd/verifycmd.go
index 4aa2696ff..f08fcbac2 100644
--- a/chezmoi2/cmd/verifycmd.go
+++ b/chezmoi2/cmd/verifycmd.go
@@ -35,7 +35,7 @@ func (c *Config) runVerifyCmd(cmd *cobra.Command, args []string) error {
 	if err := c.applyArgs(dryRunSystem, c.destDirAbsPath, args, applyArgsOptions{
 		include:   c.verify.include,
 		recursive: c.verify.recursive,
-		umask:     c.Umask.FileMode(),
+		umask:     c.Umask,
 	}); err != nil {
 		return err
 	}
diff --git a/chezmoi2/internal/chezmoi/chezmoi.go b/chezmoi2/internal/chezmoi/chezmoi.go
index 5879c44f1..a0287189a 100644
--- a/chezmoi2/internal/chezmoi/chezmoi.go
+++ b/chezmoi2/internal/chezmoi/chezmoi.go
@@ -14,6 +14,9 @@ var (
 
 	// Skip indicates that entry should be skipped.
 	Skip = filepath.SkipDir
+
+	// Umask is the process's umask.
+	Umask = os.FileMode(0)
 )
 
 // Suffixes and prefixes.
diff --git a/chezmoi2/internal/chezmoi/chezmoi_unix.go b/chezmoi2/internal/chezmoi/chezmoi_unix.go
index bcd46a0fe..406dd4d7f 100644
--- a/chezmoi2/internal/chezmoi/chezmoi_unix.go
+++ b/chezmoi2/internal/chezmoi/chezmoi_unix.go
@@ -14,14 +14,11 @@ import (
 	vfs "github.com/twpayne/go-vfs"
 )
 
-var (
-	umask        os.FileMode
-	whitespaceRx = regexp.MustCompile(`\s+`)
-)
+var whitespaceRx = regexp.MustCompile(`\s+`)
 
 func init() {
-	umask = os.FileMode(syscall.Umask(0))
-	syscall.Umask(int(umask))
+	Umask = os.FileMode(syscall.Umask(0))
+	syscall.Umask(int(Umask))
 }
 
 // FQDNHostname returns the FQDN hostname.
@@ -32,17 +29,6 @@ func FQDNHostname(fs vfs.FS) (string, error) {
 	return lookupAddrFQDNHostname()
 }
 
-// GetUmask returns the umask.
-func GetUmask() os.FileMode {
-	return umask
-}
-
-// SetUmask sets the umask.
-func SetUmask(newUmask os.FileMode) {
-	umask = newUmask
-	syscall.Umask(int(umask))
-}
-
 // etcHostsFQDNHostname returns the FQDN hostname from parsing /etc/hosts.
 func etcHostsFQDNHostname(fs vfs.FS) (string, error) {
 	etcHostsContents, err := fs.ReadFile("/etc/hosts")
@@ -86,8 +72,3 @@ func lookupAddrFQDNHostname() (string, error) {
 	}
 	return strings.TrimSuffix(names[0], "."), nil
 }
-
-// umaskPermEqual returns if two permissions are equal after applying umask.
-func umaskPermEqual(perm1, perm2, umask os.FileMode) bool {
-	return perm1&^umask == perm2&^umask
-}
diff --git a/chezmoi2/internal/chezmoi/chezmoi_windows.go b/chezmoi2/internal/chezmoi/chezmoi_windows.go
index 2d1b9ca02..dd027fbe4 100644
--- a/chezmoi2/internal/chezmoi/chezmoi_windows.go
+++ b/chezmoi2/internal/chezmoi/chezmoi_windows.go
@@ -24,14 +24,6 @@ func FQDNHostname(fs vfs.FS) (string, error) {
 	return string(utf16.Decode(buf[0:n])), nil
 }
 
-// GetUmask returns the umask.
-func GetUmask() os.FileMode {
-	return os.ModePerm
-}
-
-// SetUmask sets the umask.
-func SetUmask(umask os.FileMode) {}
-
 // isExecutable returns false on Windows.
 func isExecutable(info os.FileInfo) bool {
 	return false
@@ -45,8 +37,3 @@ func isPrivate(info os.FileInfo) bool {
 func isSlash(c uint8) bool {
 	return c == '\\' || c == '/'
 }
-
-// umaskPermEqual returns true on Windows.
-func umaskPermEqual(perm1 os.FileMode, perm2 os.FileMode, umask os.FileMode) bool {
-	return true
-}
diff --git a/chezmoi2/internal/chezmoi/diff.go b/chezmoi2/internal/chezmoi/diff.go
index 95a07fd89..2bf304d76 100644
--- a/chezmoi2/internal/chezmoi/diff.go
+++ b/chezmoi2/internal/chezmoi/diff.go
@@ -61,7 +61,7 @@ func DiffPatch(path RelPath, fromData []byte, fromMode os.FileMode, toData []byt
 
 	var from diff.File
 	if fromData != nil || fromMode != 0 {
-		fromFileMode, err := filemode.NewFromOSFileMode(fromMode)
+		fromFileMode, err := diffFileMode(fromMode)
 		if err != nil {
 			return nil, err
 		}
@@ -74,7 +74,7 @@ func DiffPatch(path RelPath, fromData []byte, fromMode os.FileMode, toData []byt
 
 	var to diff.File
 	if toData != nil || toMode != 0 {
-		toFileMode, err := filemode.NewFromOSFileMode(toMode)
+		toFileMode, err := diffFileMode(toMode)
 		if err != nil {
 			return nil, err
 		}
@@ -118,6 +118,15 @@ func diffChunks(from, to string) []diff.Chunk {
 	return chunks
 }
 
+func diffFileMode(mode os.FileMode) (filemode.FileMode, error) {
+	fileMode, err := filemode.NewFromOSFileMode(mode)
+	if err != nil {
+		return 0, err
+	}
+	fileMode |= filemode.FileMode(mode.Perm())
+	return fileMode, nil
+}
+
 func isBinary(data []byte) bool {
 	return len(data) != 0 && !strings.HasPrefix(http.DetectContentType(data), "text/")
 }
diff --git a/chezmoi2/internal/chezmoi/entrystate.go b/chezmoi2/internal/chezmoi/entrystate.go
index 18626342e..c54bc65e0 100644
--- a/chezmoi2/internal/chezmoi/entrystate.go
+++ b/chezmoi2/internal/chezmoi/entrystate.go
@@ -3,6 +3,7 @@ package chezmoi
 import (
 	"bytes"
 	"os"
+	"runtime"
 )
 
 // An EntryStateType is an entry state type.
@@ -33,24 +34,28 @@ func (s *EntryState) Contents() []byte {
 }
 
 // Equal returns true if s is equal to other.
-func (s *EntryState) Equal(other *EntryState, umask os.FileMode) bool {
-	return s.Type == other.Type &&
-		s.Mode&^umask == other.Mode&^umask &&
-		bytes.Equal(s.ContentsSHA256, other.ContentsSHA256)
+func (s *EntryState) Equal(other *EntryState) bool {
+	if s.Type != other.Type {
+		return false
+	}
+	if runtime.GOOS != "windows" && s.Mode != other.Mode {
+		return false
+	}
+	return bytes.Equal(s.ContentsSHA256, other.ContentsSHA256)
 }
 
 // Equivalent returns true if s is equivalent to other.
-func (s *EntryState) Equivalent(other *EntryState, umask os.FileMode) bool {
+func (s *EntryState) Equivalent(other *EntryState) bool {
 	switch {
 	case s == nil:
 		return other == nil || other.Type == EntryStateTypeAbsent
 	case other == nil:
 		return s.Type == EntryStateTypeAbsent
 	case s.Type == EntryStateTypeFile:
-		return other.Type == EntryStateTypePresent || s.Equal(other, umask)
+		return other.Type == EntryStateTypePresent || s.Equal(other)
 	case s.Type == EntryStateTypePresent:
 		return other.Type == EntryStateTypeFile || other.Type == EntryStateTypePresent
 	default:
-		return s.Equal(other, umask)
+		return s.Equal(other)
 	}
 }
diff --git a/chezmoi2/internal/chezmoi/sourcestate.go b/chezmoi2/internal/chezmoi/sourcestate.go
index dbc5124b2..a131d44f4 100644
--- a/chezmoi2/internal/chezmoi/sourcestate.go
+++ b/chezmoi2/internal/chezmoi/sourcestate.go
@@ -93,18 +93,11 @@ func WithTemplateOptions(templateOptions []string) SourceStateOption {
 	}
 }
 
-// WithUmask sets the umask.
-func WithUmask(umask os.FileMode) SourceStateOption {
-	return func(s *SourceState) {
-		s.umask = umask
-	}
-}
-
 // NewSourceState creates a new source state with the given options.
 func NewSourceState(options ...SourceStateOption) *SourceState {
 	s := &SourceState{
 		entries:              make(map[RelPath]SourceStateEntry),
-		umask:                GetUmask(),
+		umask:                Umask,
 		encryption:           NoEncryption{},
 		ignore:               newPatternSet(),
 		priorityTemplateData: make(map[string]interface{}),
@@ -127,7 +120,6 @@ type AddOptions struct {
 	Include      *IncludeSet
 	RemoveDir    RelPath
 	Template     bool
-	umask        os.FileMode
 }
 
 // Add adds destAbsPathInfos to s.
@@ -253,7 +245,7 @@ func (s *SourceState) Add(sourceSystem System, persistentState PersistentState,
 		for _, sourceRelPath := range update.sourceRelPaths {
 			if err := targetSourceState.Apply(sourceSystem, NullPersistentState{}, s.sourceDirAbsPath, sourceRelPath.RelPath(), ApplyOptions{
 				Include: options.Include,
-				Umask:   options.umask,
+				Umask:   s.umask,
 			}); err != nil {
 				return err
 			}
@@ -333,7 +325,7 @@ func (s *SourceState) Apply(targetSystem System, persistentState PersistentState
 
 	targetAbsPath := targetDir.Join(targetRelPath)
 
-	targetEntryState, err := targetStateEntry.EntryState()
+	targetEntryState, err := targetStateEntry.EntryState(options.Umask)
 	if err != nil {
 		return err
 	}
diff --git a/chezmoi2/internal/chezmoi/targetstateentry.go b/chezmoi2/internal/chezmoi/targetstateentry.go
index 3b6c6d083..15ff50226 100644
--- a/chezmoi2/internal/chezmoi/targetstateentry.go
+++ b/chezmoi2/internal/chezmoi/targetstateentry.go
@@ -4,14 +4,14 @@ import (
 	"bytes"
 	"encoding/hex"
 	"os"
+	"runtime"
 	"time"
 )
 
 // A TargetStateEntry represents the state of an entry in the target state.
 type TargetStateEntry interface {
 	Apply(system System, persistentState PersistentState, actualStateEntry ActualStateEntry, umask os.FileMode) error
-	EntryState() (*EntryState, error)
-	Equal(actualStateEntry ActualStateEntry, umask os.FileMode) (bool, error)
+	EntryState(umask os.FileMode) (*EntryState, error)
 	Evaluate() error
 	SkipApply(persistentState PersistentState) (bool, error)
 }
@@ -70,21 +70,12 @@ func (t *TargetStateAbsent) Apply(system System, persistentState PersistentState
 }
 
 // EntryState returns t's entry state.
-func (t *TargetStateAbsent) EntryState() (*EntryState, error) {
+func (t *TargetStateAbsent) EntryState(umask os.FileMode) (*EntryState, error) {
 	return &EntryState{
 		Type: EntryStateTypeAbsent,
 	}, nil
 }
 
-// Equal returns true if actualStateEntry matches t.
-func (t *TargetStateAbsent) Equal(actualStateEntry ActualStateEntry, umask os.FileMode) (bool, error) {
-	_, ok := actualStateEntry.(*ActualStateAbsent)
-	if !ok {
-		return false, nil
-	}
-	return ok, nil
-}
-
 // Evaluate evaluates t.
 func (t *TargetStateAbsent) Evaluate() error {
 	return nil
@@ -98,37 +89,25 @@ func (t *TargetStateAbsent) SkipApply(persistentState PersistentState) (bool, er
 // Apply updates actualStateEntry to match t. It does not recurse.
 func (t *TargetStateDir) Apply(system System, persistentState PersistentState, actualStateEntry ActualStateEntry, umask os.FileMode) error {
 	if actualStateDir, ok := actualStateEntry.(*ActualStateDir); ok {
-		if umaskPermEqual(actualStateDir.perm, t.perm, umask) {
+		if runtime.GOOS == "windows" || actualStateDir.perm == t.perm&^umask {
 			return nil
 		}
-		return system.Chmod(actualStateDir.Path(), t.perm)
+		return system.Chmod(actualStateDir.Path(), t.perm&^umask)
 	}
 	if err := actualStateEntry.Remove(system); err != nil {
 		return err
 	}
-	return system.Mkdir(actualStateEntry.Path(), t.perm)
+	return system.Mkdir(actualStateEntry.Path(), t.perm&^umask)
 }
 
 // EntryState returns t's entry state.
-func (t *TargetStateDir) EntryState() (*EntryState, error) {
+func (t *TargetStateDir) EntryState(umask os.FileMode) (*EntryState, error) {
 	return &EntryState{
 		Type: EntryStateTypeDir,
-		Mode: os.ModeDir | t.perm,
+		Mode: os.ModeDir | t.perm&^umask,
 	}, nil
 }
 
-// Equal returns true if actualStateEntry matches t. It does not recurse.
-func (t *TargetStateDir) Equal(actualStateEntry ActualStateEntry, umask os.FileMode) (bool, error) {
-	actualStateDir, ok := actualStateEntry.(*ActualStateDir)
-	if !ok {
-		return false, nil
-	}
-	if !umaskPermEqual(actualStateDir.perm, t.perm, umask) {
-		return false, nil
-	}
-	return true, nil
-}
-
 // Evaluate evaluates t.
 func (t *TargetStateDir) Evaluate() error {
 	return nil
@@ -154,10 +133,10 @@ func (t *TargetStateFile) Apply(system System, persistentState PersistentState,
 			return err
 		}
 		if bytes.Equal(actualContentsSHA256, contentsSHA256) {
-			if umaskPermEqual(actualStateFile.perm, t.perm, umask) {
+			if runtime.GOOS == "windows" || actualStateFile.perm == t.perm&^umask {
 				return nil
 			}
-			return system.Chmod(actualStateFile.Path(), t.perm)
+			return system.Chmod(actualStateFile.Path(), t.perm&^umask)
 		}
 	} else if err := actualStateEntry.Remove(system); err != nil {
 		return err
@@ -166,11 +145,11 @@ func (t *TargetStateFile) Apply(system System, persistentState PersistentState,
 	if err != nil {
 		return err
 	}
-	return system.WriteFile(actualStateEntry.Path(), contents, t.perm)
+	return system.WriteFile(actualStateEntry.Path(), contents, t.perm&^umask)
 }
 
 // EntryState returns t's entry state.
-func (t *TargetStateFile) EntryState() (*EntryState, error) {
+func (t *TargetStateFile) EntryState(umask os.FileMode) (*EntryState, error) {
 	contents, err := t.Contents()
 	if err != nil {
 		return nil, err
@@ -181,35 +160,12 @@ func (t *TargetStateFile) EntryState() (*EntryState, error) {
 	}
 	return &EntryState{
 		Type:           EntryStateTypeFile,
-		Mode:           t.perm,
+		Mode:           t.perm &^ umask,
 		ContentsSHA256: hexBytes(contentsSHA256),
 		contents:       contents,
 	}, nil
 }
 
-// Equal returns true if actualStateEntry matches t.
-func (t *TargetStateFile) Equal(actualStateEntry ActualStateEntry, umask os.FileMode) (bool, error) {
-	actualStateFile, ok := actualStateEntry.(*ActualStateFile)
-	if !ok {
-		return false, nil
-	}
-	if !umaskPermEqual(actualStateFile.perm, t.perm, umask) {
-		return false, nil
-	}
-	actualContentsSHA256, err := actualStateFile.ContentsSHA256()
-	if err != nil {
-		return false, err
-	}
-	contentsSHA256, err := t.ContentsSHA256()
-	if err != nil {
-		return false, err
-	}
-	if !bytes.Equal(actualContentsSHA256, contentsSHA256) {
-		return false, nil
-	}
-	return true, nil
-}
-
 // Evaluate evaluates t.
 func (t *TargetStateFile) Evaluate() error {
 	_, err := t.ContentsSHA256()
@@ -224,10 +180,10 @@ func (t *TargetStateFile) SkipApply(persistentState PersistentState) (bool, erro
 // Apply updates actualStateEntry to match t.
 func (t *TargetStatePresent) Apply(system System, persistentState PersistentState, actualStateEntry ActualStateEntry, umask os.FileMode) error {
 	if actualStateFile, ok := actualStateEntry.(*ActualStateFile); ok {
-		if umaskPermEqual(actualStateFile.perm, t.perm, umask) {
+		if runtime.GOOS == "windows" || actualStateFile.perm == t.perm&^umask {
 			return nil
 		}
-		return system.Chmod(actualStateFile.Path(), t.perm)
+		return system.Chmod(actualStateFile.Path(), t.perm&^umask)
 	} else if err := actualStateEntry.Remove(system); err != nil {
 		return err
 	}
@@ -235,28 +191,17 @@ func (t *TargetStatePresent) Apply(system System, persistentState PersistentStat
 	if err != nil {
 		return err
 	}
-	return system.WriteFile(actualStateEntry.Path(), contents, t.perm)
+	return system.WriteFile(actualStateEntry.Path(), contents, t.perm&^umask)
 }
 
 // EntryState returns t's entry state.
-func (t *TargetStatePresent) EntryState() (*EntryState, error) {
+func (t *TargetStatePresent) EntryState(umask os.FileMode) (*EntryState, error) {
 	return &EntryState{
 		Type: EntryStateTypePresent,
+		Mode: t.perm &^ umask,
 	}, nil
 }
 
-// Equal returns true if actualStateEntry matches t.
-func (t *TargetStatePresent) Equal(actualStateEntry ActualStateEntry, umask os.FileMode) (bool, error) {
-	actualStateFile, ok := actualStateEntry.(*ActualStateFile)
-	if !ok {
-		return false, nil
-	}
-	if !umaskPermEqual(actualStateFile.perm, t.perm, umask) {
-		return false, nil
-	}
-	return true, nil
-}
-
 // Evaluate evaluates t.
 func (t *TargetStatePresent) Evaluate() error {
 	_, err := t.ContentsSHA256()
@@ -275,15 +220,10 @@ func (t *TargetStateRenameDir) Apply(system System, persistentState PersistentSt
 }
 
 // EntryState returns t's entry state.
-func (t *TargetStateRenameDir) EntryState() (*EntryState, error) {
+func (t *TargetStateRenameDir) EntryState(umask os.FileMode) (*EntryState, error) {
 	return nil, nil
 }
 
-// Equal returns false because actualStateEntry has not been renamed.
-func (t *TargetStateRenameDir) Equal(actualStateEntry ActualStateEntry, umask os.FileMode) (bool, error) {
-	return false, nil
-}
-
 // Evaluate does nothing.
 func (t *TargetStateRenameDir) Evaluate() error {
 	return nil
@@ -326,7 +266,7 @@ func (t *TargetStateScript) Apply(system System, persistentState PersistentState
 }
 
 // EntryState returns t's entry state.
-func (t *TargetStateScript) EntryState() (*EntryState, error) {
+func (t *TargetStateScript) EntryState(umask os.FileMode) (*EntryState, error) {
 	contentsSHA256, err := t.ContentsSHA256()
 	if err != nil {
 		return nil, err
@@ -337,12 +277,6 @@ func (t *TargetStateScript) EntryState() (*EntryState, error) {
 	}, nil
 }
 
-// Equal returns true if actualStateEntry matches t.
-func (t *TargetStateScript) Equal(actualStateEntry ActualStateEntry, umask os.FileMode) (bool, error) {
-	// Scripts are independent of the actual state.
-	return true, nil
-}
-
 // Evaluate evaluates t.
 func (t *TargetStateScript) Evaluate() error {
 	_, err := t.ContentsSHA256()
@@ -395,7 +329,7 @@ func (t *TargetStateSymlink) Apply(system System, persistentState PersistentStat
 }
 
 // EntryState returns t's entry state.
-func (t *TargetStateSymlink) EntryState() (*EntryState, error) {
+func (t *TargetStateSymlink) EntryState(umask os.FileMode) (*EntryState, error) {
 	linkname, err := t.Linkname()
 	if err != nil {
 		return nil, err
@@ -411,26 +345,6 @@ func (t *TargetStateSymlink) EntryState() (*EntryState, error) {
 	}, nil
 }
 
-// Equal returns true if actualStateEntry matches t.
-func (t *TargetStateSymlink) Equal(actualStateEntry ActualStateEntry, umask os.FileMode) (bool, error) {
-	actualStateSymlink, ok := actualStateEntry.(*ActualStateSymlink)
-	if !ok {
-		return false, nil
-	}
-	actualLinkname, err := actualStateSymlink.Linkname()
-	if err != nil {
-		return false, err
-	}
-	linkname, err := t.Linkname()
-	if err != nil {
-		return false, nil
-	}
-	if actualLinkname != linkname {
-		return false, nil
-	}
-	return true, nil
-}
-
 // Evaluate evaluates t.
 func (t *TargetStateSymlink) Evaluate() error {
 	_, err := t.Linkname()
diff --git a/cmd/secretkeepassxc.go b/cmd/secretkeepassxc.go
index d56ad985e..d71ea617b 100644
--- a/cmd/secretkeepassxc.go
+++ b/cmd/secretkeepassxc.go
@@ -129,8 +129,7 @@ func (c *Config) keePassXCAttributeFunc(entry, attribute string) string {
 }
 
 func readPassword(prompt string) (pw []byte, err error) {
-	fd := int(os.Stdin.Fd())
-	if term.IsTerminal(fd) {
+	if fd := int(os.Stdin.Fd()); term.IsTerminal(fd) {
 		fmt.Print(prompt)
 		pw, err = term.ReadPassword(fd)
 		fmt.Println()
diff --git a/go.mod b/go.mod
index e4b347ee9..6dc12561c 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/twpayne/chezmoi
 go 1.14
 
 require (
-	github.com/Masterminds/sprig/v3 v3.2.0
+	github.com/Masterminds/sprig/v3 v3.2.2
 	github.com/Microsoft/go-winio v0.4.16 // indirect
 	github.com/alecthomas/chroma v0.8.2 // indirect
 	github.com/alecthomas/colour v0.1.0 // indirect
@@ -18,16 +18,19 @@ require (
 	github.com/google/go-cmp v0.5.4 // indirect
 	github.com/google/go-github/v33 v33.0.0
 	github.com/google/renameio v1.0.0
-	github.com/google/uuid v1.1.2 // indirect
+	github.com/google/uuid v1.2.0 // indirect
 	github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect
 	github.com/huandu/xstrings v1.3.2 // indirect
 	github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
+	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
 	github.com/magiconair/properties v1.8.4 // indirect
-	github.com/mitchellh/mapstructure v1.4.0 // indirect
-	github.com/mitchellh/reflectwalk v1.0.1 // indirect
+	github.com/mattn/go-runewidth v0.0.10 // indirect
+	github.com/mitchellh/copystructure v1.1.1 // indirect
+	github.com/mitchellh/mapstructure v1.4.1 // indirect
 	github.com/muesli/combinator v0.3.0
 	github.com/pelletier/go-toml v1.8.1
 	github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7
+	github.com/rivo/uniseg v0.2.0 // indirect
 	github.com/rogpeppe/go-internal v1.7.0
 	github.com/rs/zerolog v1.20.0
 	github.com/sergi/go-diff v1.1.0
@@ -47,15 +50,15 @@ require (
 	go.etcd.io/bbolt v1.3.5
 	go.uber.org/multierr v1.6.0
 	golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect
-	golang.org/x/net v0.0.0-20201224014010-6772e930b67b // indirect
-	golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5
-	golang.org/x/sys v0.0.0-20201223074533-0d417f636930
+	golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect
+	golang.org/x/oauth2 v0.0.0-20210201163806-010130855d6c
+	golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c
 	golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf
-	golang.org/x/text v0.3.4 // indirect
+	golang.org/x/text v0.3.5 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
 	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
 	gopkg.in/ini.v1 v1.62.0 // indirect
 	gopkg.in/yaml.v2 v2.4.0
-	gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
+	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
 	howett.net/plist v0.0.0-20201203080718-1454fab16a06
 )
diff --git a/go.sum b/go.sum
index 61bd4d2ad..9409c4d80 100644
--- a/go.sum
+++ b/go.sum
@@ -35,12 +35,12 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7
 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
-github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg=
-github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
+github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
+github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
 github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
 github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
-github.com/Masterminds/sprig/v3 v3.2.0 h1:P1ekkbuU73Ui/wS0nK1HOM37hh4xdfZo485UPf8rc+Y=
-github.com/Masterminds/sprig/v3 v3.2.0/go.mod h1:tWhwTbUTndesPNeF0C900vKoq283u6zp4APT9vaF3SI=
+github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8=
+github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk=
 github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
 github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
 github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk=
@@ -210,8 +210,8 @@ github.com/google/renameio v1.0.0 h1:xhp2CnJmgQmpJU4RY8chagahUq5mbPPAbiSQstKpVMA
 github.com/google/renameio v1.0.0/go.mod h1:t/HQoYBZSsWSNK35C6CO/TpPLDVWvxOHboWUAweKUpk=
 github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
 github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
-github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
+github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
@@ -285,6 +285,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
 github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
+github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
 github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
 github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 github.com/magiconair/properties v1.8.4 h1:8KGKTcQQGm0Kv7vEbKFErAoAOFyyacLStRtQSeYtvkY=
@@ -298,6 +300,8 @@ github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+tw
 github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
 github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
+github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg=
+github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/microcosm-cc/bluemonday v1.0.4 h1:p0L+CTpo/PLFdkoPcJemLXG+fpMD7pYOoDEq1axMbGg=
 github.com/microcosm-cc/bluemonday v1.0.4/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w=
@@ -305,6 +309,8 @@ github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3N
 github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
 github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
 github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
+github.com/mitchellh/copystructure v1.1.1 h1:Bp6x9R1Wn16SIz3OfeDr0b7RnCG2OB66Y7PQyC/cvq4=
+github.com/mitchellh/copystructure v1.1.1/go.mod h1:EBArHfARyrSWO/+Wyr9zwEkc6XMFB9XyNgFNmRkZZU4=
 github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@@ -314,8 +320,8 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu
 github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
-github.com/mitchellh/mapstructure v1.4.0 h1:7ks8ZkOP5/ujthUsT07rNv+nkLXCQWKNHuwzOAesEks=
-github.com/mitchellh/mapstructure v1.4.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
+github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
 github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
 github.com/mitchellh/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE=
@@ -359,6 +365,10 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
+github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
+github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.7.0 h1:3qqXGV8nn7GJT65debw77Dzrx9sfWYgP0DDo7xcMFRk=
@@ -545,8 +555,8 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R
 golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
 golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw=
-golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
+golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -554,8 +564,8 @@ golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4Iltr
 golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5 h1:Lm4OryKCca1vehdsWogr9N4t7NfZxLbJoc/H0w4K4S4=
-golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210201163806-010130855d6c h1:HiAZXo96zOhVhtFHchj/ojzoxCFiPrp9/j0GtS38V3g=
+golang.org/x/oauth2 v0.0.0-20210201163806-010130855d6c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -604,6 +614,8 @@ golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201223074533-0d417f636930 h1:vRgIt+nup/B/BwIS0g2oC0haq0iqbV3ZA+u6+0TlNCo=
 golang.org/x/sys v0.0.0-20201223074533-0d417f636930/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
@@ -618,8 +630,8 @@ golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
-golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
+golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -785,8 +797,8 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
-gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=


Test patch:
diff --git a/chezmoi2/cmd/addcmd_test.go b/chezmoi2/cmd/addcmd_test.go
index ef7016524..33925312d 100644
--- a/chezmoi2/cmd/addcmd_test.go
+++ b/chezmoi2/cmd/addcmd_test.go
@@ -7,7 +7,6 @@ import (
 	"github.com/twpayne/go-vfs"
 	"github.com/twpayne/go-vfs/vfst"
 
-	"github.com/twpayne/chezmoi/chezmoi2/internal/chezmoi"
 	"github.com/twpayne/chezmoi/chezmoi2/internal/chezmoitest"
 )
 
@@ -29,7 +28,7 @@ func TestAddCmd(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.local/share/chezmoi/dot_dir",
 					vfst.TestIsDir,
-					vfst.TestModePerm(0o777&^chezmoi.GetUmask()),
+					vfst.TestModePerm(0o777&^chezmoitest.Umask),
 				),
 			},
 		},
@@ -49,11 +48,11 @@ func TestAddCmd(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.local/share/chezmoi/dot_dir",
 					vfst.TestIsDir,
-					vfst.TestModePerm(0o777&^chezmoi.GetUmask()),
+					vfst.TestModePerm(0o777&^chezmoitest.Umask),
 				),
 				vfst.TestPath("/home/user/.local/share/chezmoi/dot_dir/file",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^chezmoi.GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString("# contents of .dir/file\n"),
 				),
 			},
@@ -74,7 +73,7 @@ func TestAddCmd(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.local/share/chezmoi/dot_dir",
 					vfst.TestIsDir,
-					vfst.TestModePerm(0o777&^chezmoi.GetUmask()),
+					vfst.TestModePerm(0o777&^chezmoitest.Umask),
 				),
 				vfst.TestPath("/home/user/.local/share/chezmoi/dot_dir/file",
 					vfst.TestDoesNotExist,
@@ -97,11 +96,11 @@ func TestAddCmd(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.local/share/chezmoi/private_dot_dir",
 					vfst.TestIsDir,
-					vfst.TestModePerm(0o777&^chezmoi.GetUmask()),
+					vfst.TestModePerm(0o777&^chezmoitest.Umask),
 				),
 				vfst.TestPath("/home/user/.local/share/chezmoi/private_dot_dir/file",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^chezmoi.GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString("# contents of .dir/file\n"),
 				),
 			},
@@ -122,11 +121,11 @@ func TestAddCmd(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.local/share/chezmoi/private_dot_dir",
 					vfst.TestIsDir,
-					vfst.TestModePerm(0o777&^chezmoi.GetUmask()),
+					vfst.TestModePerm(0o777&^chezmoitest.Umask),
 				),
 				vfst.TestPath("/home/user/.local/share/chezmoi/private_dot_dir/file",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^chezmoi.GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString("# contents of .dir/file\n"),
 				),
 			},
@@ -156,7 +155,7 @@ func TestAddCmd(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.local/share/chezmoi/empty_dot_empty",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^chezmoi.GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContents(nil),
 				),
 			},
@@ -175,7 +174,7 @@ func TestAddCmd(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.local/share/chezmoi/executable_dot_executable",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^chezmoi.GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString("#!/bin/sh\n"),
 				),
 			},
@@ -191,7 +190,7 @@ func TestAddCmd(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.local/share/chezmoi/dot_file",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^chezmoi.GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString("# contents of .file\n"),
 				),
 			},
@@ -209,7 +208,7 @@ func TestAddCmd(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.local/share/chezmoi/symlink_dot_symlink",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^chezmoi.GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString(".dir/subdir/file\n"),
 				),
 			},
@@ -228,7 +227,7 @@ func TestAddCmd(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.local/share/chezmoi/dot_symlink",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^chezmoi.GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString("# contents of .file\n"),
 				),
 			},
diff --git a/chezmoi2/cmd/applycmd_test.go b/chezmoi2/cmd/applycmd_test.go
index 831265db6..215f9be7c 100644
--- a/chezmoi2/cmd/applycmd_test.go
+++ b/chezmoi2/cmd/applycmd_test.go
@@ -9,7 +9,6 @@ import (
 	"github.com/twpayne/go-vfs"
 	"github.com/twpayne/go-vfs/vfst"
 
-	"github.com/twpayne/chezmoi/chezmoi2/internal/chezmoi"
 	"github.com/twpayne/chezmoi/chezmoi2/internal/chezmoitest"
 )
 
@@ -28,45 +27,45 @@ func TestApplyCmd(t *testing.T) {
 				),
 				vfst.TestPath("/home/user/.dir",
 					vfst.TestIsDir,
-					vfst.TestModePerm(0o777&^chezmoi.GetUmask()),
+					vfst.TestModePerm(0o777&^chezmoitest.Umask),
 				),
 				vfst.TestPath("/home/user/.dir/file",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^chezmoi.GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString("# contents of .dir/file\n"),
 				),
 				vfst.TestPath("/home/user/.dir/subdir",
 					vfst.TestIsDir,
-					vfst.TestModePerm(0o777&^chezmoi.GetUmask()),
+					vfst.TestModePerm(0o777&^chezmoitest.Umask),
 				),
 				vfst.TestPath("/home/user/.dir/subdir/file",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^chezmoi.GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString("# contents of .dir/subdir/file\n"),
 				),
 				vfst.TestPath("/home/user/.empty",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^chezmoi.GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContents(nil),
 				),
 				vfst.TestPath("/home/user/.executable",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o777&^chezmoi.GetUmask()),
+					vfst.TestModePerm(0o777&^chezmoitest.Umask),
 					vfst.TestContentsString("# contents of .executable\n"),
 				),
 				vfst.TestPath("/home/user/.exists",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^chezmoi.GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString("# contents of .exists\n"),
 				),
 				vfst.TestPath("/home/user/.file",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^chezmoi.GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString("# contents of .file\n"),
 				),
 				vfst.TestPath("/home/user/.private",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o600&^chezmoi.GetUmask()),
+					vfst.TestModePerm(0o600&^chezmoitest.Umask),
 					vfst.TestContentsString("# contents of .private\n"),
 				),
 				vfst.TestPath("/home/user/.symlink",
@@ -75,7 +74,7 @@ func TestApplyCmd(t *testing.T) {
 				),
 				vfst.TestPath("/home/user/.template",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^chezmoi.GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString("key = value\n"),
 				),
 			},
@@ -119,20 +118,20 @@ func TestApplyCmd(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.dir",
 					vfst.TestIsDir,
-					vfst.TestModePerm(0o777&^chezmoi.GetUmask()),
+					vfst.TestModePerm(0o777&^chezmoitest.Umask),
 				),
 				vfst.TestPath("/home/user/.dir/file",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^chezmoi.GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString("# contents of .dir/file\n"),
 				),
 				vfst.TestPath("/home/user/.dir/subdir",
 					vfst.TestIsDir,
-					vfst.TestModePerm(0o777&^chezmoi.GetUmask()),
+					vfst.TestModePerm(0o777&^chezmoitest.Umask),
 				),
 				vfst.TestPath("/home/user/.dir/subdir/file",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^chezmoi.GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString("# contents of .dir/subdir/file\n"),
 				),
 			},
@@ -143,7 +142,7 @@ func TestApplyCmd(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.dir",
 					vfst.TestIsDir,
-					vfst.TestModePerm(0o777&^chezmoi.GetUmask()),
+					vfst.TestModePerm(0o777&^chezmoitest.Umask),
 				),
 				vfst.TestPath("/home/user/.dir/file",
 					vfst.TestDoesNotExist,
@@ -162,7 +161,7 @@ func TestApplyCmd(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.exists",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^chezmoi.GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString("# existing contents of .exists\n"),
 				),
 			},
diff --git a/chezmoi2/cmd/cmd_test.go b/chezmoi2/cmd/cmd_test.go
index 2c7dc98aa..c1f05a9f5 100644
--- a/chezmoi2/cmd/cmd_test.go
+++ b/chezmoi2/cmd/cmd_test.go
@@ -4,8 +4,18 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+
+	"github.com/twpayne/chezmoi/chezmoi2/internal/chezmoi"
+	"github.com/twpayne/chezmoi/chezmoi2/internal/chezmoitest"
 )
 
+func init() {
+	// github.com/twpayne/chezmoi/chezmoi2/internal/chezmoi reads the umask
+	// before github.com/twpayne/chezmoi/chezmoi2/internal/chezmoitest sets it,
+	// so update it.
+	chezmoi.Umask = chezmoitest.Umask
+}
+
 func TestMustGetLongHelpPanics(t *testing.T) {
 	assert.Panics(t, func() {
 		mustLongHelp("non-existent-command")
diff --git a/chezmoi2/cmd/config_test.go b/chezmoi2/cmd/config_test.go
index b23600382..2ceb7ac03 100644
--- a/chezmoi2/cmd/config_test.go
+++ b/chezmoi2/cmd/config_test.go
@@ -2,6 +2,7 @@ package cmd
 
 import (
 	"io"
+	"os"
 	"path/filepath"
 	"runtime"
 	"testing"
@@ -174,6 +175,7 @@ func newTestConfig(t *testing.T, fs vfs.FS, options ...configOption) *Config {
 			withSourceSystem(system),
 			withTestFS(fs),
 			withTestUser("user"),
+			withUmask(chezmoitest.Umask),
 		}, options...)...,
 	)
 	require.NoError(t, err)
@@ -246,3 +248,10 @@ func withTestUser(username string) configOption {
 		return nil
 	}
 }
+
+func withUmask(umask os.FileMode) configOption {
+	return func(c *Config) error {
+		c.Umask = umask
+		return nil
+	}
+}
diff --git a/chezmoi2/cmd/filemode_test.go b/chezmoi2/cmd/filemode_test.go
deleted file mode 100644
index cf84b94c5..000000000
--- a/chezmoi2/cmd/filemode_test.go
+++ /dev/null
@@ -1,70 +0,0 @@
-package cmd
-
-import (
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-)
-
-func TestFileMode(t *testing.T) {
-	for _, tc := range []struct {
-		s              string
-		expectedErr    bool
-		expected       fileMode
-		expectedString string
-	}{
-		{
-			s:              "0",
-			expected:       0,
-			expectedString: "000",
-		},
-		{
-			s:              "644",
-			expected:       0o644,
-			expectedString: "644",
-		},
-		{
-			s:              "755",
-			expected:       0o755,
-			expectedString: "755",
-		},
-		{
-			s:              "0",
-			expected:       0,
-			expectedString: "000",
-		},
-		{
-			s:           "-0",
-			expectedErr: true,
-		},
-		{
-			s:           "s",
-			expectedErr: true,
-		},
-		{
-			s:           "008",
-			expectedErr: true,
-		},
-		{
-			s:           "01000",
-			expectedErr: true,
-		},
-		{
-			s:           "-0",
-			expectedErr: true,
-		},
-	} {
-		t.Run(tc.s, func(t *testing.T) {
-			var p fileMode
-			err := p.Set(tc.s)
-			if tc.expectedErr {
-				require.Error(t, err)
-			} else {
-				require.NoError(t, err)
-				assert.Equal(t, tc.expectedString, p.String())
-				assert.Equal(t, "file mode", p.Type())
-			}
-		})
-	}
-}
diff --git a/chezmoi2/cmd/importcmd_test.go b/chezmoi2/cmd/importcmd_test.go
index 53448ac4e..f2639035c 100644
--- a/chezmoi2/cmd/importcmd_test.go
+++ b/chezmoi2/cmd/importcmd_test.go
@@ -56,16 +56,16 @@ func TestImportCmd(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.local/share/chezmoi/dot_dir",
 					vfst.TestIsDir,
-					vfst.TestModePerm(0o777),
+					vfst.TestModePerm(0o777&^chezmoitest.Umask),
 				),
 				vfst.TestPath("/home/user/.local/share/chezmoi/dot_dir/dot_file",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString("# contents of archive/.dir/.file\n"),
 				),
 				vfst.TestPath("/home/user/.local/share/chezmoi/dot_dir/symlink_dot_symlink",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString(".file\n"),
 				),
 			},
@@ -81,16 +81,16 @@ func TestImportCmd(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.local/share/chezmoi/dir/dot_dir",
 					vfst.TestIsDir,
-					vfst.TestModePerm(0o777),
+					vfst.TestModePerm(0o777&^chezmoitest.Umask),
 				),
 				vfst.TestPath("/home/user/.local/share/chezmoi/dir/dot_dir/dot_file",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString("# contents of archive/.dir/.file\n"),
 				),
 				vfst.TestPath("/home/user/.local/share/chezmoi/dir/dot_dir/symlink_dot_symlink",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString(".file\n"),
 				),
 			},
@@ -107,19 +107,19 @@ func TestImportCmd(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.local/share/chezmoi/dir/dot_dir",
 					vfst.TestIsDir,
-					vfst.TestModePerm(0o777),
+					vfst.TestModePerm(0o777&^chezmoitest.Umask),
 				),
 				vfst.TestPath("/home/user/.local/share/chezmoi/dir/file",
 					vfst.TestDoesNotExist,
 				),
 				vfst.TestPath("/home/user/.local/share/chezmoi/dir/dot_dir/dot_file",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString("# contents of archive/.dir/.file\n"),
 				),
 				vfst.TestPath("/home/user/.local/share/chezmoi/dir/dot_dir/symlink_dot_symlink",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString(".file\n"),
 				),
 			},
@@ -136,16 +136,16 @@ func TestImportCmd(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.local/share/chezmoi/dir/exact_dot_dir",
 					vfst.TestIsDir,
-					vfst.TestModePerm(0o777),
+					vfst.TestModePerm(0o777&^chezmoitest.Umask),
 				),
 				vfst.TestPath("/home/user/.local/share/chezmoi/dir/exact_dot_dir/dot_file",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString("# contents of archive/.dir/.file\n"),
 				),
 				vfst.TestPath("/home/user/.local/share/chezmoi/dir/exact_dot_dir/symlink_dot_symlink",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString(".file\n"),
 				),
 			},
diff --git a/chezmoi2/cmd/statuscmd_test.go b/chezmoi2/cmd/statuscmd_test.go
index eca249df7..bc34dddfb 100644
--- a/chezmoi2/cmd/statuscmd_test.go
+++ b/chezmoi2/cmd/statuscmd_test.go
@@ -9,7 +9,6 @@ import (
 	vfs "github.com/twpayne/go-vfs"
 	"github.com/twpayne/go-vfs/vfst"
 
-	"github.com/twpayne/chezmoi/chezmoi2/internal/chezmoi"
 	"github.com/twpayne/chezmoi/chezmoi2/internal/chezmoitest"
 )
 
@@ -33,7 +32,7 @@ func TestStatusCmd(t *testing.T) {
 			postApplyTests: []interface{}{
 				vfst.TestPath("/home/user/.local/share/chezmoi/dot_bashrc",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^chezmoi.GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString("# contents of .bashrc\n"),
 				),
 			},
diff --git a/chezmoi2/internal/chezmoi/chezmoi_test.go b/chezmoi2/internal/chezmoi/chezmoi_test.go
index 05270a1d6..858c016d2 100644
--- a/chezmoi2/internal/chezmoi/chezmoi_test.go
+++ b/chezmoi2/internal/chezmoi/chezmoi_test.go
@@ -8,7 +8,6 @@ import (
 	"github.com/rs/zerolog/pkgerrors"
 )
 
-//nolint:gochecknoinits
 func init() {
 	log.Logger = log.Output(zerolog.ConsoleWriter{
 		Out:     os.Stderr,
diff --git a/chezmoi2/internal/chezmoi/chezmoi_unix_test.go b/chezmoi2/internal/chezmoi/chezmoi_unix_test.go
index 07102c7e9..558e5221f 100644
--- a/chezmoi2/internal/chezmoi/chezmoi_unix_test.go
+++ b/chezmoi2/internal/chezmoi/chezmoi_unix_test.go
@@ -68,3 +68,7 @@ func TestEtcHostsFQDNHostname(t *testing.T) {
 		})
 	}
 }
+
+func TestUmask(t *testing.T) {
+	require.Equal(t, chezmoitest.Umask, Umask)
+}
diff --git a/chezmoi2/internal/chezmoi/entrystate_test.go b/chezmoi2/internal/chezmoi/entrystate_test.go
index 30249e0c3..ca45663aa 100644
--- a/chezmoi2/internal/chezmoi/entrystate_test.go
+++ b/chezmoi2/internal/chezmoi/entrystate_test.go
@@ -3,6 +3,7 @@ package chezmoi
 import (
 	"fmt"
 	"os"
+	"runtime"
 	"sort"
 	"testing"
 
@@ -65,22 +66,26 @@ func TestEntryStateEquivalent(t *testing.T) {
 	}
 
 	expectedEquivalents := map[string]bool{
-		"absent_nil1":          true,
-		"absent_nil2":          true,
-		"dir1_copy_dir1":       true,
-		"dir1_dir1_copy":       true,
-		"file1_copy_file1":     true,
-		"file1_copy_present":   true,
-		"file1_file1_copy":     true,
-		"file1_present":        true,
-		"file2_present":        true,
-		"nil1_absent":          true,
-		"nil2_absent":          true,
-		"present_file1_copy":   true,
-		"present_file1":        true,
-		"present_file2":        true,
-		"symlink_copy_symlink": true,
-		"symlink_symlink_copy": true,
+		"absent_nil1":           true,
+		"absent_nil2":           true,
+		"dir_private_dir1_copy": runtime.GOOS == "windows",
+		"dir_private_dir1":      runtime.GOOS == "windows",
+		"dir1_copy_dir_private": runtime.GOOS == "windows",
+		"dir1_copy_dir1":        true,
+		"dir1_dir_private":      runtime.GOOS == "windows",
+		"dir1_dir1_copy":        true,
+		"file1_copy_file1":      true,
+		"file1_copy_present":    true,
+		"file1_file1_copy":      true,
+		"file1_present":         true,
+		"file2_present":         true,
+		"nil1_absent":           true,
+		"nil2_absent":           true,
+		"present_file1_copy":    true,
+		"present_file1":         true,
+		"present_file2":         true,
+		"symlink_copy_symlink":  true,
+		"symlink_symlink_copy":  true,
 	}
 
 	entryStateKeys := make([]string, 0, len(entryStates))
@@ -108,7 +113,7 @@ func TestEntryStateEquivalent(t *testing.T) {
 			entryState1 := entryStates[tc.EntryState1Key]
 			entryState2 := entryStates[tc.EntryState2Key]
 			expectedEquivalent := entryState1 == entryState2 || expectedEquivalents[name]
-			assert.Equal(t, expectedEquivalent, entryState1.Equivalent(entryState2, 0o022))
+			assert.Equal(t, expectedEquivalent, entryState1.Equivalent(entryState2))
 		})
 	}
 }
diff --git a/chezmoi2/internal/chezmoi/sourcestate_test.go b/chezmoi2/internal/chezmoi/sourcestate_test.go
index b05875236..5eb79febc 100644
--- a/chezmoi2/internal/chezmoi/sourcestate_test.go
+++ b/chezmoi2/internal/chezmoi/sourcestate_test.go
@@ -34,7 +34,7 @@ func TestSourceStateAdd(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.local/share/chezmoi/dot_dir",
 					vfst.TestIsDir,
-					vfst.TestModePerm(0o777&^GetUmask()),
+					vfst.TestModePerm(0o777&^chezmoitest.Umask),
 				),
 				vfst.TestPath("/home/user/.local/share/chezmoi/dot_dir/file",
 					vfst.TestDoesNotExist,
@@ -63,11 +63,11 @@ func TestSourceStateAdd(t *testing.T) {
 				),
 				vfst.TestPath("/home/user/.local/share/chezmoi/dot_dir",
 					vfst.TestIsDir,
-					vfst.TestModePerm(0o777&^GetUmask()),
+					vfst.TestModePerm(0o777&^chezmoitest.Umask),
 				),
 				vfst.TestPath("/home/user/.local/share/chezmoi/dot_dir/file",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString("# contents of file\n"),
 				),
 			},
@@ -83,11 +83,11 @@ func TestSourceStateAdd(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.local/share/chezmoi/dot_dir",
 					vfst.TestIsDir,
-					vfst.TestModePerm(0o777&^GetUmask()),
+					vfst.TestModePerm(0o777&^chezmoitest.Umask),
 				),
 				vfst.TestPath("/home/user/.local/share/chezmoi/dot_dir/file",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString("# contents of .dir/file\n"),
 				),
 			},
@@ -121,11 +121,11 @@ func TestSourceStateAdd(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.local/share/chezmoi/dot_dir",
 					vfst.TestIsDir,
-					vfst.TestModePerm(0o777&^GetUmask()),
+					vfst.TestModePerm(0o777&^chezmoitest.Umask),
 				),
 				vfst.TestPath("/home/user/.local/share/chezmoi/dot_dir/subdir",
 					vfst.TestIsDir,
-					vfst.TestModePerm(0o777&^GetUmask()),
+					vfst.TestModePerm(0o777&^chezmoitest.Umask),
 				),
 				vfst.TestPath("/home/user/.local/share/chezmoi/dot_dir/subdir/file",
 					vfst.TestDoesNotExist,
@@ -143,18 +143,18 @@ func TestSourceStateAdd(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.local/share/chezmoi/dot_dir",
 					vfst.TestIsDir,
-					vfst.TestModePerm(0o777&^GetUmask()),
+					vfst.TestModePerm(0o777&^chezmoitest.Umask),
 				),
 				vfst.TestPath("/home/user/.local/share/chezmoi/dot_dir/file",
 					vfst.TestDoesNotExist,
 				),
 				vfst.TestPath("/home/user/.local/share/chezmoi/dot_dir/subdir",
 					vfst.TestIsDir,
-					vfst.TestModePerm(0o777&^GetUmask()),
+					vfst.TestModePerm(0o777&^chezmoitest.Umask),
 				),
 				vfst.TestPath("/home/user/.local/share/chezmoi/dot_dir/subdir/file",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString("# contents of .dir/subdir/file\n"),
 				),
 			},
@@ -203,7 +203,7 @@ func TestSourceStateAdd(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.local/share/chezmoi/empty_dot_empty",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContents(nil),
 				),
 			},
@@ -219,7 +219,7 @@ func TestSourceStateAdd(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.local/share/chezmoi/executable_dot_executable",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString("# contents of .executable\n"),
 				),
 			},
@@ -235,7 +235,7 @@ func TestSourceStateAdd(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.local/share/chezmoi/dot_executable",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString("# contents of .executable\n"),
 				),
 			},
@@ -252,7 +252,7 @@ func TestSourceStateAdd(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.local/share/chezmoi/exists_dot_exists",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString("# contents of .exists\n"),
 				),
 			},
@@ -268,7 +268,7 @@ func TestSourceStateAdd(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.local/share/chezmoi/dot_file",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString("# contents of .file\n"),
 				),
 			},
@@ -287,7 +287,7 @@ func TestSourceStateAdd(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.local/share/chezmoi/dot_file",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString("# contents of .file\n"),
 				),
 				vfst.TestPath("/home/user/.local/share/chezmoi/executable_dot_file",
@@ -309,7 +309,7 @@ func TestSourceStateAdd(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.local/share/chezmoi/dot_file",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString("# contents of .file\n"),
 				),
 			},
@@ -325,7 +325,7 @@ func TestSourceStateAdd(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.local/share/chezmoi/private_dot_private",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString("# contents of .private\n"),
 				),
 			},
@@ -341,7 +341,7 @@ func TestSourceStateAdd(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.local/share/chezmoi/dot_private",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString("# contents of .private\n"),
 				),
 			},
@@ -393,7 +393,7 @@ func TestSourceStateAdd(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.local/share/chezmoi/dot_template.tmpl",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString("key = {{ .variable }}\n"),
 				),
 			},
@@ -410,11 +410,11 @@ func TestSourceStateAdd(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.local/share/chezmoi/dot_dir",
 					vfst.TestIsDir,
-					vfst.TestModePerm(0o777&^GetUmask()),
+					vfst.TestModePerm(0o777&^chezmoitest.Umask),
 				),
 				vfst.TestPath("/home/user/.local/share/chezmoi/dot_dir/file",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString("# contents of .dir/file\n"),
 				),
 			},
@@ -528,7 +528,7 @@ func TestSourceStateApplyAll(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.dir",
 					vfst.TestIsDir,
-					vfst.TestModePerm(0o777&^GetUmask()),
+					vfst.TestModePerm(0o777&^chezmoitest.Umask),
 				),
 			},
 		},
@@ -547,7 +547,7 @@ func TestSourceStateApplyAll(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.dir",
 					vfst.TestIsDir,
-					vfst.TestModePerm(0o777&^GetUmask()),
+					vfst.TestModePerm(0o777&^chezmoitest.Umask),
 				),
 				vfst.TestPath("/home/user/.dir/file",
 					vfst.TestDoesNotExist,
@@ -566,7 +566,7 @@ func TestSourceStateApplyAll(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.file",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString("# contents of .file\n"),
 				),
 			},
@@ -599,7 +599,7 @@ func TestSourceStateApplyAll(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.empty",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContents(nil),
 				),
 			},
@@ -621,7 +621,7 @@ func TestSourceStateApplyAll(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.template",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString("key = value\n"),
 				),
 			},
@@ -638,7 +638,7 @@ func TestSourceStateApplyAll(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.exists",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString("# contents of .exists\n"),
 				),
 			},
@@ -656,7 +656,7 @@ func TestSourceStateApplyAll(t *testing.T) {
 			tests: []interface{}{
 				vfst.TestPath("/home/user/.exists",
 					vfst.TestModeIsRegular,
-					vfst.TestModePerm(0o666&^GetUmask()),
+					vfst.TestModePerm(0o666&^chezmoitest.Umask),
 					vfst.TestContentsString("# existing contents of .exists\n"),
 				),
 			},
@@ -708,7 +708,7 @@ func TestSourceStateApplyAll(t *testing.T) {
 				require.NoError(t, sourceState.Read())
 				require.NoError(t, sourceState.evaluateAll())
 				require.NoError(t, sourceState.applyAll(system, persistentState, "/home/user", ApplyOptions{
-					Umask: GetUmask(),
+					Umask: chezmoitest.Umask,
 				}))
 
 				vfst.RunTests(t, fs, "", tc.tests...)
diff --git a/chezmoi2/internal/chezmoi/targetstateentry_test.go b/chezmoi2/internal/chezmoi/targetstateentry_test.go
index 67dd75542..71a3829db 100644
--- a/chezmoi2/internal/chezmoi/targetstateentry_test.go
+++ b/chezmoi2/internal/chezmoi/targetstateentry_test.go
@@ -18,25 +18,25 @@ func TestTargetStateEntryApplyAndEqual(t *testing.T) {
 	targetStates := map[string]TargetStateEntry{
 		"absent": &TargetStateAbsent{},
 		"dir": &TargetStateDir{
-			perm: 0o777,
+			perm: 0o777 &^ chezmoitest.Umask,
 		},
 		"file": &TargetStateFile{
-			perm: 0o666,
+			perm: 0o666 &^ chezmoitest.Umask,
 			lazyContents: &lazyContents{
 				contents: []byte("# contents of file"),
 			},
 		},
 		"file_empty": &TargetStateFile{
-			perm: 0o666,
+			perm: 0o666 &^ chezmoitest.Umask,
 		},
 		"file_executable": &TargetStateFile{
-			perm: 0o777,
+			perm: 0o777 &^ chezmoitest.Umask,
 			lazyContents: &lazyContents{
 				contents: []byte("#!/bin/sh\n"),
 			},
 		},
 		"present": &TargetStatePresent{
-			perm: 0o666,
+			perm: 0o666 &^ chezmoitest.Umask,
 		},
 		"symlink": &TargetStateSymlink{
 			lazyLinkname: &lazyLinkname{
@@ -113,19 +113,11 @@ func TestTargetStateEntryApplyAndEqual(t *testing.T) {
 				require.NoError(t, err)
 
 				// Apply the target state entry.
-				require.NoError(t, targetState.Apply(s, nil, actualStateEntry, GetUmask()))
+				require.NoError(t, targetState.Apply(s, nil, actualStateEntry, chezmoitest.Umask))
 
 				// Verify that the actual state entry matches the desired
 				// state.
 				vfst.RunTests(t, fs, "", vfst.TestPath("/home/user/target", targetStateTest(t, targetState)...))
-
-				// Read the updated destination state entry from fs and
-				// verify that it is equal to the target state entry.
-				newActualStateEntry, err := NewActualStateEntry(s, "/home/user/target", nil, nil)
-				require.NoError(t, err)
-				equal, err := targetState.Equal(newActualStateEntry, GetUmask())
-				require.NoError(t, err)
-				require.True(t, equal)
 			})
 		})
 	}
@@ -141,7 +133,7 @@ func targetStateTest(t *testing.T, ts TargetStateEntry) []vfst.PathTest {
 	case *TargetStateDir:
 		return []vfst.PathTest{
 			vfst.TestIsDir,
-			vfst.TestModePerm(ts.perm &^ GetUmask()),
+			vfst.TestModePerm(ts.perm &^ chezmoitest.Umask),
 		}
 	case *TargetStateFile:
 		expectedContents, err := ts.Contents()
@@ -149,12 +141,12 @@ func targetStateTest(t *testing.T, ts TargetStateEntry) []vfst.PathTest {
 		return []vfst.PathTest{
 			vfst.TestModeIsRegular,
 			vfst.TestContents(expectedContents),
-			vfst.TestModePerm(ts.perm &^ GetUmask()),
+			vfst.TestModePerm(ts.perm &^ chezmoitest.Umask),
 		}
 	case *TargetStatePresent:
 		return []vfst.PathTest{
 			vfst.TestModeIsRegular,
-			vfst.TestModePerm(ts.perm &^ GetUmask()),
+			vfst.TestModePerm(ts.perm &^ chezmoitest.Umask),
 		}
 	case *TargetStateRenameDir:
 		// FIXME test for presence of newName
diff --git a/chezmoi2/internal/chezmoitest/chezmoitest_unix.go b/chezmoi2/internal/chezmoitest/chezmoitest_unix.go
new file mode 100644
index 000000000..df0c17d1b
--- /dev/null
+++ b/chezmoi2/internal/chezmoitest/chezmoitest_unix.go
@@ -0,0 +1,15 @@
+// +build !windows
+
+package chezmoitest
+
+import (
+	"os"
+	"syscall"
+)
+
+// Umask is the umask used in tests.
+const Umask = os.FileMode(0o022)
+
+func init() {
+	syscall.Umask(int(Umask))
+}
diff --git a/chezmoi2/internal/chezmoitest/chezmoitest_windows.go b/chezmoi2/internal/chezmoitest/chezmoitest_windows.go
new file mode 100644
index 000000000..b21388b47
--- /dev/null
+++ b/chezmoi2/internal/chezmoitest/chezmoitest_windows.go
@@ -0,0 +1,6 @@
+package chezmoitest
+
+import "os"
+
+// Umask is the umask used in tests.
+const Umask = os.FileMode(0)
diff --git a/chezmoi2/main_test.go b/chezmoi2/main_test.go
index 1db0c93c3..3e733212a 100644
--- a/chezmoi2/main_test.go
+++ b/chezmoi2/main_test.go
@@ -19,17 +19,11 @@ import (
 	"github.com/twpayne/go-vfs/vfst"
 
 	"github.com/twpayne/chezmoi/chezmoi2/cmd"
-	"github.com/twpayne/chezmoi/chezmoi2/internal/chezmoi"
 	"github.com/twpayne/chezmoi/chezmoi2/internal/chezmoitest"
 )
 
-// umask is the umask used in tests. The umask applies to the process and so
-// cannot be overridden in individual tests.
-const umask = 0o22
-
 //nolint:interfacer
 func TestMain(m *testing.M) {
-	chezmoi.SetUmask(umask)
 	os.Exit(testscript.RunMain(m, map[string]func() int{
 		"chezmoi": func() int {
 			return cmd.Main(cmd.VersionInfo{
@@ -42,7 +36,6 @@ func TestMain(m *testing.M) {
 	}))
 }
 
-//nolint:paralleltest
 func TestScript(t *testing.T) {
 	testscript.Run(t, testscript.Params{
 		Dir: filepath.Join("testdata", "scripts"),
@@ -118,13 +111,12 @@ func cmdCmpMod(ts *testscript.TestScript, neg bool, args []string) {
 	if err != nil {
 		ts.Fatalf("%s: %v", args[1], err)
 	}
-	umask := chezmoi.GetUmask()
-	equal := info.Mode().Perm()&^umask == os.FileMode(mode64)&^umask
+	equal := info.Mode().Perm() == os.FileMode(mode64)&^chezmoitest.Umask
 	if neg && equal {
 		ts.Fatalf("%s unexpectedly has mode %03o", args[1], info.Mode().Perm())
 	}
 	if !neg && !equal {
-		ts.Fatalf("%s has mode %03o, expected %03o", args[1], info.Mode().Perm(), os.FileMode(mode64))
+		ts.Fatalf("%s has mode %03o, expected %03o", args[1], info.Mode().Perm(), os.FileMode(mode64)&^chezmoitest.Umask)
 	}
 }
 
@@ -281,7 +273,7 @@ func cmdMkHomeDir(ts *testscript.TestScript, neg bool, args []string) {
 	workDir := ts.Getenv("WORK")
 	relPath, err := filepath.Rel(workDir, path)
 	ts.Check(err)
-	if err := newBuilder().Build(vfs.NewPathFS(vfs.OSFS, workDir), map[string]interface{}{
+	if err := vfst.NewBuilder().Build(vfs.NewPathFS(vfs.OSFS, workDir), map[string]interface{}{
 		relPath: map[string]interface{}{
 			".dir": map[string]interface{}{
 				"file": "# contents of .dir/file\n",
@@ -323,7 +315,7 @@ func cmdMkSourceDir(ts *testscript.TestScript, neg bool, args []string) {
 	workDir := ts.Getenv("WORK")
 	relPath, err := filepath.Rel(workDir, sourceDir)
 	ts.Check(err)
-	err = newBuilder().Build(vfs.NewPathFS(vfs.OSFS, workDir), map[string]interface{}{
+	err = vfst.NewBuilder().Build(vfs.NewPathFS(vfs.OSFS, workDir), map[string]interface{}{
 		relPath: map[string]interface{}{
 			"dot_absent": "",
 			"dot_dir": map[string]interface{}{
@@ -391,10 +383,6 @@ func cmdUNIX2DOS(ts *testscript.TestScript, neg bool, args []string) {
 	}
 }
 
-func newBuilder() *vfst.Builder {
-	return vfst.NewBuilder(vfst.BuilderUmask(umask))
-}
-
 func prependDirToPath(dir, path string) string {
 	return strings.Join(append([]string{dir}, filepath.SplitList(path)...), string(os.PathListSeparator))
 }
@@ -484,7 +472,7 @@ func setup(env *testscript.Env) error {
 		}
 	}
 
-	return newBuilder().Build(vfs.NewPathFS(vfs.OSFS, env.WorkDir), root)
+	return vfst.NewBuilder().Build(vfs.NewPathFS(vfs.OSFS, env.WorkDir), root)
 }
 
 // unix2DOS returns data with UNIX line endings converted to DOS line endings.
diff --git a/chezmoi2/testdata/scripts/diff.txt b/chezmoi2/testdata/scripts/diff.txt
index 9db158e02..8eb172a21 100644
--- a/chezmoi2/testdata/scripts/diff.txt
+++ b/chezmoi2/testdata/scripts/diff.txt
@@ -1,5 +1,3 @@
-# FIXME golden/remove-file-diff seems to be reversed
-
 mkhomedir golden
 mkhomedir
 mksourcedir
@@ -11,25 +9,29 @@ chezmoi diff
 # test that chezmoi diff generates a diff when a file is added to the source state
 cp golden/dot_newfile $CHEZMOISOURCEDIR/dot_newfile
 chezmoi diff
-cmp stdout golden/add-newfile-diff
+[!windows] cmp stdout golden/add-newfile-diff-unix
+[windows] cmp stdout golden/add-newfile-diff-windows
 rm $CHEZMOISOURCEDIR/dot_newfile
 
 # test that chezmoi diff generates a diff when a file is edited
 edit $HOME/.file
 chezmoi diff
-cmp stdout golden/modify-file-diff
+[!windows] cmp stdout golden/modify-file-diff-unix
+[windows] cmp stdout golden/modify-file-diff-windows
 chezmoi apply --force $HOME${/}.file
 
 # test that chezmoi diff generates a diff when a file is removed from the destination directory
 rm $HOME/.file
 chezmoi diff
-cmp stdout golden/remove-file-diff
+[!windows] cmp stdout golden/restore-file-diff-unix
+[windows] cmp stdout golden/restore-file-diff-windows
 chezmoi apply --force $HOME${/}.file
 
 # test that chezmoi diff generates a diff when a directory is removed from the destination directory
 rm $HOME/.dir
 chezmoi diff --recursive=false $HOME${/}.dir
-cmp stdout golden/remove-dir-diff
+[!windows] cmp stdout golden/restore-dir-diff-unix
+[windows] cmp stdout golden/restore-dir-diff-windows
 chezmoi apply --force $HOME${/}.dir
 
 [windows] stop 'remaining tests use file modes'
@@ -41,13 +43,12 @@ cmp stdout golden/chmod-file-diff
 chezmoi apply --force $HOME${/}.file
 
 # test that chezmoi diff generates a diff when a dir's permissions are changed
-# FIXME show changes to permissions in diff
 chmod 700 $HOME/.dir
 chezmoi diff
 cmp stdout golden/chmod-dir-diff
 chezmoi apply --force --recursive=false $HOME${/}.dir
 
--- golden/add-newfile-diff --
+-- golden/add-newfile-diff-unix --
 diff --git a/.newfile b/.newfile
 new file mode 100644
 index 0000000000000000000000000000000000000000..06e05235fdd12fd5c367b6d629fef94536c85525
@@ -55,7 +56,15 @@ index 0000000000000000000000000000000000000000..06e05235fdd12fd5c367b6d629fef945
 +++ b/.newfile
 @@ -0,0 +1 @@
 +# contents of .newfile
--- golden/modify-file-diff --
+-- golden/add-newfile-diff-windows --
+diff --git a/.newfile b/.newfile
+new file mode 100666
+index 0000000000000000000000000000000000000000..06e05235fdd12fd5c367b6d629fef94536c85525
+--- /dev/null
++++ b/.newfile
+@@ -0,0 +1 @@
++# contents of .newfile
+-- golden/modify-file-diff-unix --
 diff --git a/.file b/.file
 index 5d2730a8850a2db479af83de87cc8345437aef06..8a52cb9ce9551221716a53786ad74104c5902362 100644
 --- a/.file
@@ -63,7 +72,15 @@ index 5d2730a8850a2db479af83de87cc8345437aef06..8a52cb9ce9551221716a53786ad74104
 @@ -1,2 +1 @@
  # contents of .file
 -# edited
--- golden/remove-file-diff --
+-- golden/modify-file-diff-windows --
+diff --git a/.file b/.file
+index 5d2730a8850a2db479af83de87cc8345437aef06..8a52cb9ce9551221716a53786ad74104c5902362 100666
+--- a/.file
++++ b/.file
+@@ -1,2 +1 @@
+ # contents of .file
+-# edited
+-- golden/restore-file-diff-unix --
 diff --git a/.file b/.file
 new file mode 100644
 index 0000000000000000000000000000000000000000..8a52cb9ce9551221716a53786ad74104c5902362
@@ -71,17 +88,35 @@ index 0000000000000000000000000000000000000000..8a52cb9ce9551221716a53786ad74104
 +++ b/.file
 @@ -0,0 +1 @@
 +# contents of .file
--- golden/remove-dir-diff --
+-- golden/restore-file-diff-windows --
+diff --git a/.file b/.file
+new file mode 100666
+index 0000000000000000000000000000000000000000..8a52cb9ce9551221716a53786ad74104c5902362
+--- /dev/null
++++ b/.file
+@@ -0,0 +1 @@
++# contents of .file
+-- golden/restore-dir-diff-unix --
 diff --git a/.dir b/.dir
-new file mode 40000
+new file mode 40755
 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
 --- /dev/null
 +++ b/.dir
+-- golden/restore-dir-diff-windows --
+diff --git a/.dir b/.dir
+new file mode 40777
+index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
+--- /dev/null
++++ b/.dir
+-- golden/dot_newfile --
+# contents of .newfile
 -- golden/chmod-file-diff --
 diff --git a/.file b/.file
-old mode 100755
+old mode 100777
 new mode 100644
 -- golden/chmod-dir-diff --
 diff --git a/.dir b/.dir
+old mode 40700
+new mode 40755
 -- golden/dot_newfile --
 # contents of .newfile
diff --git a/main_test.go b/main_test.go
index 2e0b52c7c..3ea9b1be1 100644
--- a/main_test.go
+++ b/main_test.go
@@ -130,7 +130,6 @@ func cmdEdit(ts *testscript.TestScript, neg bool, args []string) {
 			ts.Fatalf("edit: %v", err)
 		}
 		data = append(data, []byte("# edited\n")...)
-		//nolint:gosec
 		if err := ioutil.WriteFile(filename, data, 0o666); err != nil {
 			ts.Fatalf("edit: %v", err)
 		}
@@ -266,7 +265,6 @@ func cmdRmFinalNewline(ts *testscript.TestScript, neg bool, args []string) {
 		if len(data) == 0 || data[len(data)-1] != '\n' {
 			continue
 		}
-		//nolint:gosec
 		if err := ioutil.WriteFile(filename, data[:len(data)-1], 0o666); err != nil {
 			ts.Fatalf("%s: %v", filename, err)
 		}
@@ -287,7 +285,6 @@ func cmdUNIX2DOS(ts *testscript.TestScript, neg bool, args []string) {
 		ts.Check(err)
 		dosData, err := unix2DOS(data)
 		ts.Check(err)
-		//nolint:gosec
 		if err := ioutil.WriteFile(filename, dosData, 0o666); err != nil {
 			ts.Fatalf("%s: %v", filename, err)
 		}