Repository: twpayne/chezmoi
Pull request #336 (2019-06-09 17:03:13)

Add --follow option to add command
This assists migration from symlink-based systems.


Patch:
diff --git a/cmd/add.go b/cmd/add.go
index 9a2169b24..91e6f5bc6 100644
--- a/cmd/add.go
+++ b/cmd/add.go
@@ -35,6 +35,7 @@ func init() {
 	persistentFlags.BoolVarP(&config.add.options.Empty, "empty", "e", false, "add empty files")
 	persistentFlags.BoolVar(&config.add.options.Encrypt, "encrypt", false, "encrypt files")
 	persistentFlags.BoolVarP(&config.add.options.Exact, "exact", "x", false, "add directories exactly")
+	persistentFlags.BoolVarP(&config.add.options.Follow, "follow", "f", false, "follow last symlink")
 	persistentFlags.BoolVarP(&config.add.prompt, "prompt", "p", false, "prompt before adding")
 	persistentFlags.BoolVarP(&config.add.recursive, "recursive", "r", false, "recurse in to subdirectories")
 	persistentFlags.BoolVarP(&config.add.options.Template, "template", "T", false, "add files as templates")
diff --git a/cmd/helps.gen.go b/cmd/helps.gen.go
index c7dd2c5ee..9beb7c6e2 100644
--- a/cmd/helps.gen.go
+++ b/cmd/helps.gen.go
@@ -22,6 +22,12 @@ var helps = map[string]help{
 			"\n" +
 			"Set the \"exact\" attribute on added directories.\n" +
 			"\n" +
+			"\"-f\", \"--follow\"\n" +
+			"\n" +
+			"If the target is a symlink, add what it points to, rather than the symlink\n" +
+			"itself. This is useful when migrating your dotfiles from a system that uses\n" +
+			"symlinks.\n" +
+			"\n" +
 			"\"-p\", \"--prompt\"\n" +
 			"\n" +
 			"Interactively prompt before adding each file.\n" +
diff --git a/cmd/upgrade.go b/cmd/upgrade.go
index 209e19e2c..1b4ee282d 100644
--- a/cmd/upgrade.go
+++ b/cmd/upgrade.go
@@ -163,21 +163,8 @@ func (c *Config) runUpgradeCmd(fs vfs.FS, args []string) error {
 		return fmt.Errorf("invalid --method value: %s", method)
 	}
 
-	// Find the executable. If we replaced the executable directly, then use
-	// that, otherwise look in $PATH.
-	path := executableFilename
-	if method != methodReplaceExecutable {
-		path, err = exec.LookPath(c.upgrade.repo)
-		if err != nil {
-			return err
-		}
-	}
-
 	// Execute the new version.
-	if c.Verbose {
-		fmt.Printf("exec %s --version\n", path)
-	}
-	return syscall.Exec(path, []string{path, "--version"}, os.Environ())
+	return syscall.Exec(executableFilename, []string{executableFilename, "--version"}, os.Environ())
 }
 
 func (c *Config) getChecksums(rr *github.RepositoryRelease) (map[string][]byte, error) {
diff --git a/docs/HOWTO.md b/docs/HOWTO.md
index 740872df7..440a0abc2 100644
--- a/docs/HOWTO.md
+++ b/docs/HOWTO.md
@@ -21,6 +21,7 @@
 * [Export archives](#export-archives)
 * [Use a non-git version control system](#use-a-non-git-version-control-system)
 * [Use a merge tool other than vimdiff](#use-a-merge-tool-other-than-vimdiff)
+* [Migrate from a dotfile manager that uses symlinks](#migrate-from-a-dotfile-manager-that-uses-symlinks)
 
 ## Use a hosted repo to manage your dotfiles across multiple machines
 
@@ -517,9 +518,23 @@ GitHub](https://github.com/twpayne/chezmoi/issues/new).
 ## Use a merge tool other than vimdiff
 
 By default, chezmoi uses vimdiff, but you can use any merge tool of your choice.
-In your config file, specify the command and args to use.  For example, to use
+In your config file, specify the command and args to use. For example, to use
 neovim's diff mode specify:
 
     [merge]
       command = "nvim"
       args = "-d"
+
+## Migrate from a dotfile manager that uses symlinks
+
+Many dotfile managers replace dotfiles with symbolic links to files in a common
+directory. If you `chezmoi add` such a symlink, chezmoi will add the symlink,
+not the file. To assist with migrating from symlink-based systems, use the
+`--follow` / `-f` option to `chezmoi add`, for example:
+
+    chezmoi add --follow ~/.bashrc
+
+This will tell `chezmoi add` that the target state of `~/.bashrc` is the target
+of the `~/.bashrc` symlink, rather than the symlink itself. When you run
+`chezmoi apply`, chezmoi will replace the `~/.bashrc` symlink with the file
+contents.
\ No newline at end of file
diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md
index 0a92742e4..f7f105b77 100644
--- a/docs/REFERENCE.md
+++ b/docs/REFERENCE.md
@@ -290,6 +290,12 @@ Set the `empty` attribute on added files.
 
 Set the `exact` attribute on added directories.
 
+#### `-f`, `--follow`
+
+If the target is a symlink, add what it points to, rather than the symlink
+itself. This is useful when migrating your dotfiles from a system that uses
+symlinks.
+
 #### `-p`, `--prompt`
 
 Interactively prompt before adding each file.
diff --git a/lib/chezmoi/targetstate.go b/lib/chezmoi/targetstate.go
index 8fe327416..20a564c3d 100644
--- a/lib/chezmoi/targetstate.go
+++ b/lib/chezmoi/targetstate.go
@@ -30,6 +30,7 @@ type AddOptions struct {
 	Empty    bool
 	Encrypt  bool
 	Exact    bool
+	Follow   bool
 	Template bool
 }
 
@@ -83,7 +84,16 @@ func (ts *TargetState) Add(fs vfs.FS, addOptions AddOptions, targetPath string,
 	}
 	if info == nil {
 		var err error
-		info, err = fs.Lstat(targetPath)
+		if addOptions.Follow {
+			info, err = fs.Stat(targetPath)
+		} else {
+			info, err = fs.Lstat(targetPath)
+		}
+		if err != nil {
+			return err
+		}
+	} else if addOptions.Follow && info.Mode()&os.ModeType == os.ModeSymlink {
+		info, err = fs.Stat(targetPath)
 		if err != nil {
 			return err
 		}


Test patch:
diff --git a/cmd/add_test.go b/cmd/add_test.go
index 5b38ba573..e934466e5 100644
--- a/cmd/add_test.go
+++ b/cmd/add_test.go
@@ -200,6 +200,49 @@ func TestAddCommand(t *testing.T) {
 				),
 			},
 		},
+		{
+			name: "add_symlink_follow",
+			args: []string{"/home/user/foo"},
+			add: addCmdConfig{
+				options: chezmoi.AddOptions{
+					Follow: true,
+				},
+			},
+			root: map[string]interface{}{
+				"/home/user":               &vfst.Dir{Perm: 0755},
+				"/home/user/.chezmoi":      &vfst.Dir{Perm: 0700},
+				"/home/user/.dotfiles/foo": "bar",
+				"/home/user/foo":           &vfst.Symlink{Target: ".dotfiles/foo"},
+			},
+			tests: []vfst.Test{
+				vfst.TestPath("/home/user/.chezmoi/foo",
+					vfst.TestModeIsRegular,
+					vfst.TestContentsString("bar"),
+				),
+			},
+		},
+		{
+			name: "add_symlink_follow_double",
+			args: []string{"/home/user/foo"},
+			add: addCmdConfig{
+				options: chezmoi.AddOptions{
+					Follow: true,
+				},
+			},
+			root: map[string]interface{}{
+				"/home/user":               &vfst.Dir{Perm: 0755},
+				"/home/user/.chezmoi":      &vfst.Dir{Perm: 0700},
+				"/home/user/.dotfiles/baz": "qux",
+				"/home/user/foo":           &vfst.Symlink{Target: "bar"},
+				"/home/user/bar":           &vfst.Symlink{Target: ".dotfiles/baz"},
+			},
+			tests: []vfst.Test{
+				vfst.TestPath("/home/user/.chezmoi/foo",
+					vfst.TestModeIsRegular,
+					vfst.TestContentsString("qux"),
+				),
+			},
+		},
 		{
 			name: "add_symlink_in_dir_recursive",
 			args: []string{"/home/user/foo"},