Dotfiles
Table of Contents
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-up-
- 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-up-
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-up-
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-up-
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-up-
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-up-
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-up-
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-up-
My list of git tricks.