dt.iki.fi

Dotfiles

The term usually refers to a (shareable) collection of all sorts of user configuration files, often through a VCS such as git.

A collection of dotfiles might help to set up a fresh system, incorporate ideas & changes from different (but similar) systems, or to share these files with other Linux users.

Thoughts and Recommendations for Collecting Dotfiles in a Git Repository

  • You should backup things regularly, but this is not the same as a backup.
  • Do not try to automate the process; add files manually, one by one, sparingly. Consider if you really need to track that particular file. Once a file is added there's very little work involved.
  • Do not add whole directories, even if that means much more initial work.
  • Do not add constantly changing files - they will mess up your repo.
  • Do not add sensitive data (e.g. anything from ~/.ssh) - especially not if you're sharing these files publicly!
  • I have not found a way to track the files directly (unless one wants to turn their whole home folder into a git repository, which I don't), so it will be necessary to replace chosen config files with symlinks to files inside the repository

Create the Local Repository

Create a dedicated directory, e.g. ~/.config/.dotfiles and git init it.

Start copying files into the repo, replacing the sources with symlinks - I use this script:

bash
#!/bin/bash red(){ tput setaf 9 echo -e "$@" tput sgr0 } echo_exit_1(){ red "$@" exit 1 } usage(){ red -e "$@" cat <<EOF dotfile (verb, transitive) Takes a list of files on the command line, separates them into readables and writeables, and adds them to a git repo accordingly: - read only: file is copied to repo, no further action - read/write: file is copied to repo, original file is moved to a backup in the same directory, then replaced with a symlink to the repo file Only regular files are eligible, no directories, no symlinks, no funny stuff. EOF exit 1 } (( $# == 0 )) && usage Must supply at least one file sep="$(for ((i=0 ; i<$(tput cols); i++)); do printf ':'; done)" repo="$HOME/.config/.dotfiles" id="${0##*/}.$(date +%Y%m%d-%H%M%S)" # two arrays to be filled with files that are writeable # - those will be added to the repo and replaced by symlinks # and files that are read-only # - those will be added to the repo as copies writeables=() readables=() not=() backup=() commit=0 echo "Dotfile repo: $repo" [ -d "$repo/.git" ] && git -C "$repo" status || echo_exit_1 "Directory \"$repo\" does not appear to be a git repository's root, or something else is wrong." echo "$sep" if [ -d "$repo.bak" ]; then answer='' while [[ "$answer" != [yn] ]]; do read -rp "Do you want to replace the previous backup $repo.bak? [y/n] " answer; done [[ "$answer" == y ]] && rm -rf "$repo.bak" && cp -ra "$repo" "$repo.bak" else cp -ra "$repo" "$repo.bak" echo "First backup of whole repo created at $repo.bak" fi echo "$sep" echo -e "$# files to check/add:\n$@\n$sep" for file in "$@"; do # if file is a symlink, or NOT a regular file (not a directory or socket etc.), or NOT (at least) readable, then move on if [ -L "$file" ] || ! [ -f "$file" ] || ! [ -r "$file" ]; then not=( "${not[@]}" "$file" ) else # is it also writeable? if [ -w "$file" ]; then writeables=( "${writeables[@]}" "$file" ) else readables=( "${readables[@]}" "$file" ) fi fi done (( ${#not[@]} > 0 )) && red "${#not[@]} files were not eligible:\n${not[@]}" && echo "$sep" if (( ${#writeables[@]} > 0 )); then echo -e "${#writeables[@]} files are writeable and will be replaced by symlinks:\n${writeables[@]}\n$sep" for file in "${writeables[@]}"; do file="$(realpath "$file")" [ -d "${file%/*}" ] && mkdir -vp "$repo${file%/*}" echo -n "cp: " && cp -aiv "$file" "$repo$file" # only when files are identical, move original to backup and create symlink instead if cmp --quiet "$file" "$repo$file"; then echo -n "mv: " && mv -v "$file" "$file.$id" && \ echo -n "ln: " && ln -sv "$repo$file" "$file" && \ backup=( "${backup[@]}" "$file.$id" ) && commit=1 || red \ "Something went wrong moving and/or symlinking $file" else red "Files $file and $repo$file are not identical" fi echo done echo "$sep" if (( ${#backup[@]} > 0 )); then echo -e "${#backup[@]} backups created:\n${backup[@]}" answer='' while [[ "$answer" != [yn] ]]; do read -rp "Do you want to delete ALL these backups? [y/n] " answer; done [[ "$answer" == "y" ]] && rm -I "${backup[@]}" echo "$sep" fi fi if (( ${#readables[@]} > 0 )); then echo -e "${#readables[@]} files are readable only and will be copied to the repo:\n${readables[@]}\n$sep" commit=1 for file in "${readables[@]}"; do file="$(realpath "$file")" [ -d "${file%/*}" ] && mkdir -p "$repo${file%/*}" echo -n "cp: "; cp -aiv "$file" "$repo$file" done echo "$sep" fi if [[ "$commit" == 1 ]]; then git -C "$repo" status echo "$sep" read -rp "Do you want to add, commit & push now? " answer if [[ "$answer" = [yY]* ]]; then git -C "$repo" add . git -C "$repo" status echo "$sep" answer="" read -rp "Commit message: " answer [[ "$answer" == "" ]] && answer="$id" || answer="$answer - $id" git -C "$repo" commit -m "$answer" git -C "$repo" push origin master fi fi

Caveat

I noticed that some applications replace the symlinks with actual files when writing out their configuration - well, at least parcellite does that. I even tried using a hard link, but after a configuration change in parcellite the files don't match anymore.

Upload the Repo so that it can be Pushed & Pulled

There's variouss way to do it, but they all feel clunky to me. Here's how I did it this time:

  • scp the whole local dotfile repo to its central destination
  • Make the remote a bare repo by executing this in place: git config --bool core.bare true
  • Add the remote to your local repo: git remote add origin git:dotfiles.git (this works because "git" is a configured Host in ~/.ssh/config)
  • After making a few changes to the local repo, git push origin master should work. Subsequently, git push should be enough.

Clone the Repo onto another Machine

Just git clone + whatever remote you defined.
You should create a new branch immediately, and switch to it, e.g.:
git checkout -b laptop
Changes made to it can be pushed too:
git push origin laptop
If something changes in the master branch, and you want to incorporate it into your laptop branch:

git checkout master
git pull
git checkout laptop
git rebase master
# or maybe rather
git merge master

Working with branches

This sort of "collaboration" is still new to me. I find it beyond tricky to merge changes from one branch into another - selectively, without messing things up.

I think it's better to always push & pull to the desired branch explicitely, e.g.

git checkout master; git push origin master

It's also possible to fetch a branch without switching to it (don't use pull here - why I don't know, but it messed things up and I had to undo):

git checkout master; git fetch origin laptop:laptop

It might also be good to know how to undo almost anything with git.

Merging

A nice GUI: install meld and try git mergetool - or better don't try it: by default git will merge branches if it thinks they are mergeable. How exactly this logic works I do not know, but it failed for me - obviously different files were overwritten from another branch without asking, but some files I was asked about (that's where meld came in handy). I'm sure there's command line options to do exactly what I want but I haven't investigated yet.

Here's how to merge a single file from a different branch:

git checkout master; git checkout --patch laptop on/laptop/branch/some.file

The file does not have to exist yet on the current branch.

Also see

My list of git tricks.