6

I'm using GNU Stow to manage my dotfiles as per this guide. This works well for cases where there is no pre-existing dotfile on the machine. E.g. if there is no file ~/.config/foo.cfg then the following works well:

~/.dotfiles$ mkdir -p foo/.config
~/.dotfiles$ echo My config > foo/.config/foo.cfg
~/.dotfiles$ stow foo
~/.dotfiles$ ls -l ~/.config
lrwxrwxrwx 1 user group 21 Dec  6 19:03 ~/.config -> .dotfiles/foo/.config

It becomes less straightforward if ~/.config/foo.cfg already exists:

~/.dotfiles$ stow foo
WARNING! stowing bar would cause conflicts:
  * existing target is neither a link nor a directory: foo.cfg
All operations aborted.

So far the only solution I can find is to manually delete ~/.config/foo.cfg and re-run stow foo. This is quite awkward when provisioning a stow repo to a new machine which might have dozens of pre-existing .dotfiles and essentially defeats the purpose of using stow to manage dotfiles.

Stow has the --adopt option. Running stow --adopt foo has the effect of replacing the stow'd foo files with the pre-existing foo files that are on the machine, and then creating the symlinks. What I'm looking for is the way to achieve the opposite; replace the machine's .dotfiles with a symlink to the stow'd version so that new machines can be provisioned using the stow'd dotfiles from a Git repo.

This seems like such an obvious requirement for using Stow to manage dotfiles that I feel I'm missing something and/or the problem has been solved.

Any ideas?

JBentley
  • 233
  • 2
  • 9
  • It seems like a somewhat unsafe operation. The `stow` manual explicitly states that "Stow will never delete anything that it doesn't own." – Kusalananda Dec 06 '21 at 19:41

2 Answers2

5

I ran into the same limitation and I've found a work-flow that works well with the --adopt option. As stated in the Stow documentation:

...it allows files in the target tree, with potentially different contents to the equivalent versions in the stow package’s installation image, to be adopted into the package, then compared by running something like ‘git diff ...’ inside the stow package, and finally either kept (e.g. via ‘git commit ...’) or discarded (‘git checkout HEAD ...’).

My work-flow:

  1. Run stow with the --adopt option.
  2. Compare the "adopted" files with the ones that were originally in my repo using git diff.
  3. Discard all changes introduced by the "adopted" files with git reset --hard, reverting the entire directory the last committed state.

Example:

# Running from inside .dotfiles repository
❯ tree . -a
.
├── bash
│   ├── .bashrc
│   └── .xprofile

# Stow bash folder as usual and note conflicts (.bashrc in this case)
❯ stow bash
WARNING! stowing bash would cause conflicts:
  * existing target is neither a link nor a directory: .bashrc
All operations aborted.
e conflicts

# Rerun Stow with --adopt flag to place conflicting files in .dotfiles repo (no warnings)
❯ stow bash --adopt

# Use git diff to compare adopted and committed file
❯ git diff
diff --git a/bash/.bashrc b/bash/.bashrc
index cbd6843..0ac2879 100644
--- a/bash/.bashrc
+++ b/bash/.bashrc
@@ -1,121 +1 @@
... Line changes are listed ...

# Discard adopted file and revert back to contents as per last commit
❯ git reset --hard

# Done

I'm new to dotfiles and Stow so there might be limitations I'm missing. So far I'm happy with the result.

mauedu
  • 51
  • 1
  • 3
  • +1, definitely an interesting solution. When I get the chance I'll try this and then accept the answer if it seems foolproof. – JBentley Apr 13 '22 at 19:00
1

I ran into the same problem. I personally use Ansible to automate all of my GNU Stow commands. While an Ansible script may not be exactly what you need, my approach might help you explore different options; you could write a similar script in bash, possibly by altering the shell commands from my Ansible playbook written below.

My dotfile folders are structured like this: "~/dotfiles/home/" containing subfolder names like "bash" and "zsh". In this snippet, dotfile_packages = "~/dotfiles/home/"

Keep in mind, this is an unsafe approach because you are deleting lots of files unattended.

  - name: Find all dotfiles subfolders
    find:
      paths: "{{ dotfile_packages }}"
      file_type: directory
      recurse: no
    register: dotfile_folders

  - name: Get list of dotfiles to check for GNU Stow conflicts
    shell: find ./home/{{ item }} \( -type f -o -type l \) | sed 's/.\/home\/{{ item }}\///g'
    args:
      chdir: "{{ repo_dir }}"
    register: conflict_files
    with_items: "{{ dotfile_folders.files | map(attribute='path') | list | map('basename') }}"

  - name: Show list of files to be checked for GNU Stow conflicts
    debug:
      msg: "{{ item }}" 
    with_items: "{{ conflict_files.results | map(attribute='stdout_lines') | list | flatten }}"

  # If we don't unstow first, the delete operation erases any previously stowed dotfile sources.
  # Ignoring errors because sometimes unstow will fail to finish because OS added some untracked files.
  - name: Unstow all packages deleting conflict files.
    shell: stow -D -d {{ dotfile_packages }} -t ~/ {{ item }}
    with_items: "{{ dotfile_folders.files | map(attribute='path') | list | map('basename') }}"
    ignore_errors: yes

  - name: Delete conflicting files
    shell: rm -rf ~/{{ item }}
    with_items: "{{ conflict_files.results | map(attribute='stdout_lines') | list | flatten }}"

  - name: Symlink dotfiles
    shell: stow -d {{ dotfile_packages }} -t ~/ {{ item }}
    with_items: "{{ dotfile_folders.files | map(attribute='path') | list | map('basename') }}"

You can view this snippet being used in my dotfiles here.