From f223f38fcab3c94402603d1fadb2d6fa0ac3d05a Mon Sep 17 00:00:00 2001 From: Sean Whitton Date: Fri, 11 Nov 2022 23:32:12 -0700 Subject: GNU Stow -> hstow, and follow-up tidying & simplifications --- bin/bstraph | 78 +++++++++++++++++++ bin/bstraph.sh | 27 ------- bin/git-dotfiles-update-master | 6 -- bin/hstow | 172 +++++++++++++++++++++++++++++++++++++++++ bin/insinuate-dotfiles | 2 +- bin/unskel | 25 ------ 6 files changed, 251 insertions(+), 59 deletions(-) create mode 100755 bin/bstraph delete mode 100755 bin/bstraph.sh create mode 100755 bin/hstow delete mode 100755 bin/unskel (limited to 'bin') diff --git a/bin/bstraph b/bin/bstraph new file mode 100755 index 00000000..7d729ff3 --- /dev/null +++ b/bin/bstraph @@ -0,0 +1,78 @@ +#!/bin/sh + +# Bootstrap home directory after dotfiles repository successfully cloned (see +# INSINUATE-DOTFILES Consfigurator property and 'insinuate-dotfiles' script). +# This script should be POSIX sh and idempotent. This is the 'mr fixups' +# action for src/dotfiles, here rather than in src/dotfiles/lib-src/mr/config +# so that we can run it even if we don't have a Perl interpreter. + +set -e + +cd "$HOME/src/dotfiles" + +if [ -d /etc/skel ]; then + cd /etc/skel + for file in $(find . -type f); do + [ -e "$HOME/$file" -a ! -h "$HOME/$file" ] \ + && cmp "$file" "$HOME/$file" >/dev/null && rm "$HOME/$file" + done + cd "$HOME/src/dotfiles" +fi + +# On Debian systems root gets a special .bashrc and .profile. +for f in bashrc profile; do + [ -e /usr/share/base-files/dot.$f \ + -a -e "$HOME/.$f" -a ! -h "$HOME/.$f" ] \ + && cmp /usr/share/base-files/dot.$f "$HOME/.$f" >/dev/null \ + && rm "$HOME/.$f" +done + +# These will often end up created by, e.g., insinuate-dotfiles. +# Remove them so that the initial stow will not involve any conflicts. +for f in gpg.conf gpg-agent.conf dirmngr.conf .gpg-v21-migrated; do + [ -h "$HOME/.gnupg/$f" ] || rm -f "$HOME/.gnupg/$f" +done + +bin/hstow stow . + +if command -v git >/dev/null; then + # Use a rebase workflow as I'm the only committer. + git config pull.rebase true + + git config user.signingkey 8DC2487E51ABDD90B5C4753F0F56D0553B6D411B + + # Pushing and pulling are always done explicitly. + for branch in $(git for-each-ref \ + --format='%(refname:short)' refs/heads/); do + git rev-parse "$branch"@{upstream} >/dev/null 2>&1 \ + && git branch --unset-upstream "$branch" + done + git config push.default nothing + + # This is just for `magit-status'. + git config remote.pushDefault origin + + # Don't set up any tracking branches, or fetch it. + [ -z "$(git remote)" ] \ + && git remote add origin https://git.spwhitton.name/dotfiles + + # Non-POSIX cleanup: eventually drop. + rm -f .git/hooks/post-checkout{,_01gpgsign} + find bin lib/aid lib/backup lib/perl5 lib/hooks lib/athena lib/bins \ + lib/img lib/mr lib/src local/anacron/spool -type d -empty -delete \ + 2>/dev/null ||: + + # Eventually move to 'if ! [ "$MR_ACTION" = fixups ]; then' part, above. + bin/install-git-hooks dotfiles +fi + +cd "$HOME" +[ -e .mrconfig ] || cat >.mrconfig <"$HOME/.mrconfig" </dev/null || true -# done - # Could generalise to a script that reads a git config value for the # fingerprint to look for, updates branches specified by user and is # able to handle updating by both merge and rebase diff --git a/bin/hstow b/bin/hstow new file mode 100755 index 00000000..d151504b --- /dev/null +++ b/bin/hstow @@ -0,0 +1,172 @@ +#!/bin/sh + +# hstow -- POSIX sh minimal reimplementation of GNU Stow for dotfiles +# +# Copyright (C) 2022 Sean Whitton +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or (at +# your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# The point of this script is to obtain minimally functional dotfiles +# handling in one's home directory on even machines that lack a Perl +# interpreter. As such, many of GNU Stow's more advanced features are +# not reimplemented. Git depends on Perl, for now, but my two methods +# for deploying my dotfiles to remote machines -- my INSINUATE-DOTFILES +# Consfigurator property and 'insinuate-dotfiles' shell script -- do not +# depend on having Git on the remote side. See also 'bstraph' script. +# +# We completely skip filenames containing control characters, including +# newline and tab, as POSIX find(1) lacks -print0, and it's unlikely +# you'd need to stow any such files. +# Technique from . + +set -efu +IFS="$(printf '\n\t')" +export LC_ALL=C +tab="$(printf '\t')" +cchars="$(printf '*[\001-\037\177]*')" + +if ! command -v readlink >/dev/null; then + readlink () { + # Safe parse of ls(1) output given its POSIX specification. + ls -ld "$1" | tr -s ' ' \ + | cut -d' ' -f9- | cut -c$((4 + $(echo "$1" | wc -c)))- + } +fi + +read_globs_file () { + if [ -e "$DIR/$1" ]; then + while read -r line; do + printf "|./%s" "$line" + done <"$DIR/$1" | cut -c2- + fi +} + +dir_contents () { + ( cd "$1"; find . ! -name . ! -name "$cchars" ) +} + +fail () { + echo >&2 "hstow: $*" + exit 127 +} + +usage () { + fail "usage: hstow stow|unstow|restow|adopt DIRECTORY" +} + +stow () { + cd "$DIR" + [ -d "$HOME/.STOW" ] || mkdir "$HOME/.STOW" + [ -f "$HOME/.STOW/.stow" ] || touch "$HOME/.STOW/.stow" + [ -h "$HOME/.STOW/$NAME" ] \ + || ( cd "$HOME/.STOW"; ln -s "$DIR" "$NAME" ) + conflicts= + ignores="$(read_globs_file .hstow-local-ignore)" + + # Files that (i) always/often have their symlinks replaced with + # regular files when applications access them; and (ii) we don't + # ever want to edit the copy under $DIR directly, but only via the + # link/copy under $HOME. + $always_adopt || adoptions="$(read_globs_file .hstow-always-adopt)" + + for file in $(find . ! -name . ! -type d ! -name "$cchars" \ + ! -name .gitignore \ + ! -name .hstow-local-ignore \ + ! -name .hstow-always-adopt \ + | grep -v '^\./\.git/'); do + file_dir="$(dirname $file)" + if [ -n "$ignores" ]; then + eval case "'$file'" in "${ignores})" continue ";;" esac + eval case "'$file_dir'" in "${ignores})" continue ";;" esac + fi + + rel="$(echo $file | sed -E 's#/dot[-.]([^/]+)#/.\1#g; s#^\./##')" + dotdotslashes="$(echo $rel | sed -E 's#[^/]*$##; s#[^/]+#..#g')" + target="${dotdotslashes}.STOW/$NAME/$rel" + link="$HOME/$rel" + link_target= + [ -h "$link" ] && link_target="$(readlink $link)" + + [ "$target" = "$link_target" ] && continue + + if [ ! -h "$link" -a ! -h "$file" -a -f "$link" ]; then + if $always_adopt \ + || ( [ -n "$adoptions" ] \ + && eval case "'$file'" in \ + "${adoptions})" exit 0 ";;" \ + "*)" exit 1 ";;" \ + esac ); then + mv -f "$link" "$file" + ln -s "$target" "$link" + else + conflicts="$conflicts${tab}$file" + fi + elif [ -h "$link" ]; then + # With at least GNU ln(1), passing -f, but not also -T, does not + # replace an existing link in some cases. + # -T is not POSIX, so we just remove any existing link first. + rm "$link" + ln -s "$target" "$link" + else + mkdir -p "$HOME/$file_dir" + ln -s "$target" "$link" + fi + done + [ -z "$conflicts" ] && return + echo >&2 "hstow: encountered conflicts:" + for conflict in $conflicts; do echo >&2 " $conflict"; done + exit 127 +} + +unstow () { + cd "$HOME" + dir_pat="^.$(echo $DIR | cut -c$(echo $HOME | wc -c)-)/" + for file in $(find . -type l ! -name . ! -name "$cchars" \ + | grep -v "$dir_pat"); do + if readlink "$file" | grep -Eq '^(\.\./)*\.STOW/'"$NAME/"; then + rm "$file" + while true; do + file="$(dirname $file)" + [ "$file" = . ] && break + if [ -z "$(dir_contents $file)" ]; then + rmdir "$file" + else + break + fi + done + fi + done + [ -e "$HOME/.STOW/$NAME" ] && rm "$HOME/.STOW/$NAME" + if [ -d "$HOME/.STOW" ] \ + && [ "$(dir_contents $HOME/.STOW)" = "./.stow" ]; then + rm "$HOME/.STOW/.stow" + rmdir "$HOME/.STOW" + fi +} + +[ $# = 2 ] || usage +[ -d "$2" ] || fail "$2 is not an existing directory" +DIR="$(cd $2; pwd)" +[ "$(echo $DIR | cut -c-$(($(echo $HOME | wc -c) - 1)))" = "$HOME" ] \ + || fail "$DIR is not below $HOME" + +NAME="$(echo $DIR | tr / _)" +always_adopt=false +case "$1" in + 'stow') stow ;; + 'unstow') unstow ;; + 'restow') unstow; stow ;; + 'adopt') always_adopt=true; stow ;; + *) usage ;; +esac diff --git a/bin/insinuate-dotfiles b/bin/insinuate-dotfiles index 6cc5e5a4..d7ec9178 100755 --- a/bin/insinuate-dotfiles +++ b/bin/insinuate-dotfiles @@ -58,4 +58,4 @@ if ssh "$1" which gpg >/dev/null; then | ssh "$1" gpg --import fi # stow dotfiles into $HOME -ssh "$1" 'sh src/dotfiles/bin/bstraph.sh' +ssh "$1" 'sh src/dotfiles/bin/bstraph' diff --git a/bin/unskel b/bin/unskel deleted file mode 100755 index 80f77d53..00000000 --- a/bin/unskel +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/sh - -# Removes contents of /etc/skel in home directory. Checks for -# modifications, so should always be safe to run. - -SKEL="/etc/skel" -torm="" - -for skelfile in $(find $SKEL -maxdepth 1 -type f | sed -e "s|${SKEL}/||"); do - # The following conditional passes if the file in $HOME is the - # *same* as the file in $SKEL, so it ought to be deleted. - if diff -q "$SKEL/$skelfile" "$HOME/$skelfile" >/dev/null 2>&1; then - torm="$torm $HOME/$skelfile" - fi -done - -[ "$torm" = "" ] || rm -rf $torm - -# on Debian systems root gets a special .bashrc and .profile -if diff -q /usr/share/base-files/dot.bashrc "$HOME/.bashrc" >/dev/null 2>&1; then - rm -f "$HOME/.bashrc" -fi -if diff -q /usr/share/base-files/dot.profile "$HOME/.profile" >/dev/null 2>&1; then - rm -f "$HOME/.profile" -fi -- cgit v1.2.3