summaryrefslogtreecommitdiff
path: root/bin
diff options
context:
space:
mode:
authorSean Whitton <spwhitton@spwhitton.name>2022-11-11 23:32:12 -0700
committerSean Whitton <spwhitton@spwhitton.name>2022-11-12 11:23:53 -0700
commitf223f38fcab3c94402603d1fadb2d6fa0ac3d05a (patch)
tree8036d67f8e96d335368d8b1ff19d3c98770fd754 /bin
parent74585ec4711667c76ecdad7eb53590cb912501ba (diff)
downloaddotfiles-f223f38fcab3c94402603d1fadb2d6fa0ac3d05a.tar.gz
GNU Stow -> hstow, and follow-up tidying & simplifications
Diffstat (limited to 'bin')
-rwxr-xr-xbin/bstraph78
-rwxr-xr-xbin/bstraph.sh27
-rwxr-xr-xbin/git-dotfiles-update-master6
-rwxr-xr-xbin/hstow172
-rwxr-xr-xbin/insinuate-dotfiles2
-rwxr-xr-xbin/unskel25
6 files changed, 251 insertions, 59 deletions
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 <<EOF
+# -*- mode: conf -*-
+
+include = cat ~/src/dotfiles/lib-src/mr/config
+EOF
+mkdir -p .ssh tmp src lib mnt \
+ local/mutt local/big local/pub local/auth \
+ local/src local/bin local/lib local/log local/tmp local/info
+chmod -R u+rwX,go= local/auth
diff --git a/bin/bstraph.sh b/bin/bstraph.sh
deleted file mode 100755
index 46d6ba84..00000000
--- a/bin/bstraph.sh
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/bin/sh
-
-. $HOME/src/dotfiles/.shenv
-
-# Bootstrap home directory after dotfiles repository successfully
-# cloned (see ~/bin/insinuate-dotfiles). This script should
-# definitely be POSIX sh
-
-set -e
-
-if ! [ -e "$HOME/.mrconfig" ]; then
- cat >"$HOME/.mrconfig" <<EOF
-# -*- mode: conf -*-
-
-include = cat ~/src/dotfiles/lib-src/mr/config
-EOF
-fi
-(
- cd $HOME/src/dotfiles
- mr fixups
- mr stow
-
- if [ -z $(git remote) ]; then
- # don't set up any tracking branches or fetch it
- git remote add origin athena:dotfiles
- fi
-)
diff --git a/bin/git-dotfiles-update-master b/bin/git-dotfiles-update-master
index 0a3ac34d..d48290b4 100755
--- a/bin/git-dotfiles-update-master
+++ b/bin/git-dotfiles-update-master
@@ -1,11 +1,5 @@
#!/bin/sh
-# Before using this script, will want to unset all upstreams:
-# for head in $(git for-each-ref --format='%(refname)' refs/heads/); do
-# branch=$(echo "$head" | cut -d/ -f3)
-# git branch --unset-upstream "$branch" 2>/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 <spwhitton@spwhitton.name>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+# 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 <https://dwheeler.com/essays/filenames-in-shell.html>.
+
+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