-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathdotfiles-submodule-symlinks
More file actions
executable file
·433 lines (403 loc) · 17.5 KB
/
Copy pathdotfiles-submodule-symlinks
File metadata and controls
executable file
·433 lines (403 loc) · 17.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
#!/bin/bash
# This script is used to symlink dotfiles from a submodule of a repo checked out
# in $HOME, to place symlinks in $HOME that point to the dotfiles in submodules.
# The expected use is that this repo has been added as a submodule to another as
# is written in the `Use as "`$HOME` is _another_ repo"` section of the README.
# E.g. given the assumption this file is at ~/dotfiles/bin/submodule-symlink.sh,
# It would be called as `cd ~ && ./dotfiles/bin/dotfiles-submodule-symlinks`.
# _THIS_ script supports both modern MacOS and Ubuntu (WSL). It has inputs to
# request that it use the BSD util's options but is not tested on other BSDs.
################################################################################
# Before we start, we need to do some minor house-keeping. We support this for
# use in Ubuntu and MacOS. But it uses a _system bash_ shebang. It used features
# from bash v4 (readarray) and v4.3 (namerefs). We only plan to support Linux's
# with a bash version that supports both of these. MacOS (Darwin) on the other
# hand does not have a system bash that supports these. It's stuck on an ancient
# version of bash (v3.2.*). We support this with in-situ swapsies. So just check
# we're in an environment we expect to be compatible.
# source ./bin/system-bash-compat # << typically how this would be called, but
# # << we need to handle this being intended for existing in a submodule.
# # << See lower down for how we make sure we find `system-bash-compat`.
SYSTEM_BASH_COMPAT_BIN=bin/system-bash-compat
# If you need to configure either PROCTOR_OLD_BASH or PROCTOR_BSD_UTIL see the
# compat script's hook description.
################################################################################
# Now we've sorted out whether we can run or not... run...
VALIDATE_INPUT() {
eval "
case \$$1 in
$2)
echo \"Validated input: $1 is \$$1\";;
*)
echo \"Invalid input: $1's choices are $2, was given \$$1\" && exit;;
esac
"
}
# DSS_HOOKIN_SOURCE defaults to .include/hooks/dotfiles-submodule-symlinks
# This is the PATH to a hook-in script that will be sourced if it exists.
# It provides a way to optionally source a script that can be used to add
# customisation in any of the provided ways, or any other way, if you know
# what you're doing. """Intended""" options are listed here;
DSS_HOOKIN_SOURCE_DEFAULT=".include/hooks/dotfiles-submodule-symlinks"
: "${DSS_HOOKIN_SOURCE:=$DSS_HOOKIN_SOURCE_DEFAULT}"
# CLOBBER_CHECKEDIN_ROOT_IGNORELIST can be set as an array in the hook script,
# and will be added to the GLOBALLY_IGNORED_FILES to bypass these from the
# tracked collision test, so CLOBBER_CHECKEDIN_ROOT can be set to "ERROR",
# except for explicitely approved clobbering files in this list. This is
# where you would list for example gpg config files, as they are core to
# this basic dotfiles setup, but are not themselves extendible / includable.
# CLOBBER_CHECKEDIN_ROOT defaults to ERROR
# ERROR means that if a file is checked in in both the root module and this
# as a submodule, besides files this ignores below, then fail this process.
# WARN means that instead of erroring, a warning is printed, and that link
# is not written (the file in the root module won't be symlinked over, but
# the script will continue to overwrite other non-checked-in files).
# REPLACE will copy the file out of **this** and replace your existing file
# with the one from **these** files. You'll see it in `git diff`.
: "${CLOBBER_CHECKEDIN_ROOT:=ERROR}"
VALIDATE_INPUT "CLOBBER_CHECKEDIN_ROOT" "ERROR|WARN|REPLACE"
# CLOBBER_HOME defaults to ONLYLINKS
# ONLYLINKS means that if files exist and are already symlinks, they will be
# replaced with symlinks to **these** files. Also if file doesn't exist.
# NODIFF means that diff will evaluate the files. If they are the same, then
# this will replace them with links. Also actions the ONLYLINKS directive.
# DESTRUCTIVE means this wont do any checks, and just overwrite any existing
# file with a symlink to **these** files.
# GRACEFULLY will do DESTRUCTIVE but place files that were not previously
# symlinks, with non-empty diffs, into a temp folder, ~/.include/.clobbered/
: "${CLOBBER_HOME:=ONLYLINKS}"
VALIDATE_INPUT "CLOBBER_HOME" "ONLYLINKS|NODIFF|DESTRUCTIVE|GRACEFULLY"
# EXECUTION_CONTEXT defaults to HOME
# HOME means `pwd` must be `~`
# TEST means `pwd` can be anywhere. The purpose of this is to run within ~
# and it should alert you that you are not running it from $HOME, unless you
# set this to acknowledge you're intentionally running it elsewhere. Testing
# this script outside $HOME, or allowing this to be run by a script inside
# of a devcontainers build, would be the primary reasons for this. There is
# no easy way, or real reason either, to have a specific "DEVCONTAINERS"-ish
# context here, as the process inside the devcontainer build that runs the
# dotfiles setup does not provide the "dotfiles.targetPath" as an env var.
: "${EXECUTION_CONTEXT:=HOME}"
VALIDATE_INPUT "EXECUTION_CONTEXT" "HOME|TEST"
# SUBMOD_PATH defaults to dotfiles
# This is the value of the PATH that this is submoduled under.
: "${SUBMOD_PATH:=dotfiles}"
# If EXECUTION_CONTEXT=TEST then set the "PWD Test" and "DOTFILES" paths.
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
_PWD="$(pwd)" # ~= "~"
_SUBMOD="$_PWD/$SUBMOD_PATH" # ~= "~/dotfiles"
_HOOK_SOURCE="$_PWD/$DSS_HOOKIN_SOURCE"
echo "Looking for a $SYSTEM_BASH_COMPAT_BIN to run the guard..."
# We needed to know the _SUBMOD path before calling system-bash-compat
# Favour calling the copy in the submodule state
if [ -f "$_SUBMOD/$SYSTEM_BASH_COMPAT_BIN" ]; then
echo "Found and running $_SUBMOD/$SYSTEM_BASH_COMPAT_BIN"
source $_SUBMOD/$SYSTEM_BASH_COMPAT_BIN
else
# If it doesn't exist in the submodule state, try root repository.
echo "Missing submodule copy $_SUBMOD/$SYSTEM_BASH_COMPAT_BIN"
if [ -f "$REPO_ROOT/$SYSTEM_BASH_COMPAT_BIN" ]; then
echo "Found and running $REPO_ROOT/$SYSTEM_BASH_COMPAT_BIN"
source $REPO_ROOT/$SYSTEM_BASH_COMPAT_BIN
else
# If it was in neither the submod or root, something went wrong. As a final
# alternative, check if there is a system-bash-compat adjacent to this...
echo "Missing repository root copy $REPO_ROOT/$SYSTEM_BASH_COMPAT_BIN"
THIS_SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")" # but back out one..
if [ -f "$THIS_SCRIPT_DIR/../$SYSTEM_BASH_COMPAT_BIN" ]; then
echo "Found and running $THIS_SCRIPT_DIR/../$SYSTEM_BASH_COMPAT_BIN"
source $THIS_SCRIPT_DIR/../$SYSTEM_BASH_COMPAT_BIN
else
echo "Missing adjacent copy $THIS_SCRIPT_DIR/../$SYSTEM_BASH_COMPAT_BIN"
echo "Couldn't locate system-bash-compat, is your submodule up to date?"
exit 127
fi
fi
fi
echo # Newline
echo "SUBMODULE PATH IS SET TO $SUBMOD_PATH"
echo "HOOK-IN PATH IS SET TO $DSS_HOOKIN_SOURCE"
if [ "$EXECUTION_CONTEXT" == "HOME" ]; then
if [ "$(pwd)" == "$HOME" ]; then
echo "EXECUTION_CONTEXT is HOME running in $HOME"
else
echo "EXECUTION_CONTEXT is default HOME but you are running this from --"
echo " $(pwd)"
echo " Rerun this with"
# Check each input against its default, see if it should be added to rerun
RERUN_ARGS="EXECUTION_CONTEXT=TEST"
[ "$DSS_HOOKIN_SOURCE" != "$DSS_HOOKIN_SOURCE_DEFAULT" ] && \
RERUN_ARGS="$RERUN_ARGS DSS_HOOKIN_SOURCE=$DSS_HOOKIN_SOURCE"
[ "$SUBMOD_PATH" != "dotfiles" ] && \
RERUN_ARGS="$RERUN_ARGS SUBMOD_PATH=$SUBMOD_PATH"
[ "$CLOBBER_CHECKEDIN_ROOT" != "ERROR" ] && \
RERUN_ARGS="$RERUN_ARGS CLOBBER_CHECKEDIN_ROOT=$CLOBBER_CHECKEDIN_ROOT"
[ "$CLOBBER_HOME" != "ONLYLINKS" ] && \
RERUN_ARGS="$RERUN_ARGS CLOBBER_HOME=$CLOBBER_HOME"
echo " $RERUN_ARGS ./$SUBMOD_PATH/bin/dotfiles-submodule-symlinks"
exit 127
fi
else
# Even though we know EXECUTION_CONTEXT=TEST this should be run from repo root
if [ "$REPO_ROOT" != "$_PWD" ]; then
echo "EXECUTION_CONTEXT is TEST but this should only be from a repo root!"
echo " PWD -- $_PWD"
echo " REPO ROOT -- $REPO_ROOT"
exit 127
else
echo "EXECUTION_CONTEXT is TEST running in $_PWD"
fi
fi
# Here is where we source the optional hookin script set in DSS_HOOKIN_SOURCE.
_HOOK_RAN=0
if [[ -f "$_HOOK_SOURCE" ]]; then
_HOOK_RAN=1
source "$_HOOK_SOURCE"
else
echo "Optional hook-in script '$_HOOK_SOURCE' does not exist, skipping."
if [[ "$DSS_HOOKIN_SOURCE" != "$DSS_HOOKIN_SOURCE_DEFAULT" ]]; then
echo "Optional hook-in script was non-default yet didn't exist. Exiting."
exit 127
fi
fi
# Ignored files ~ files that can't be symlinks that this will ignore.
# Each is the full path internal to either the root or submodule, sans ~/ prefix
GLOBALLY_IGNORED_FILES=(
"$SUBMOD_PATH" # ~= "dotfiles"
# "dotfiles" itself must be ignored as git will track the submodule as a file.
# Likewise the submodule contains a ".git" file that stores the hash, but git
# doesn't consider this "tracked", and `git ls-tree` is how we're capturing what
# files to action on.
".gitignore"
".gitattributes"
".gitmodules"
)
IGNORED_FILES=(
"${GLOBALLY_IGNORED_FILES[@]}"
"${CLOBBER_CHECKEDIN_ROOT_IGNORELIST[@]}"
)
# Mac's ("Darwin") bash is _ancient_ (`bash --version` ~= 3.2.*) so if we are in
# a Darwin kernel, we need to proctor readarray (bash 4+) to make the command
# available in older bashes. Usage: _readarray_ ARRAYNAME < <(git ...)
_readarray_() {
local _arr_name="$1"
if [ "$PROCTOR_OLD_BASH" == "true" ]; then
local line
local arr=()
while IFS= read -r line; do
arr+=("$line")
done
# write back to the named array variable
eval "$_arr_name=(\"\${arr[@]}\")"
else
local __tmp
readarray -t __tmp
eval "$_arr_name=(\"\${__tmp[@]}\")"
unset __tmp
fi
}
# List files tracked in the repo root, and by this repo as a submodule.
# Use ~ $(git -C "<directory>" ls-tree --full-tree --name-only -r HEAD)
# But read it with readarray to handle entries with spaces.
# Usage: filter_array SOURCE_ARRAY_NAME IGNORE_ARRAY_NAME RESULT_ARRAY_NAME
filter_array() {
local array_name="$1"
local ignore_list_name="$2"
local ret_name="$3"
local -a filtered=()
local item
# copy the arrays into locals
eval "local arr=(\"\${${array_name}[@]}\")"
eval "local ignore_list=(\"\${${ignore_list_name}[@]}\")"
for item in "${arr[@]}"; do
skip=
local ignore
for ignore in "${ignore_list[@]}"; do
[[ "$item" == "$ignore" ]] && skip=1 && break
done
[[ -z "$skip" ]] && filtered+=("$item")
done
# set the result array by name
eval "$ret_name=(\"\${filtered[@]}\")"
}
_readarray_ _LS < <(git -C "$_PWD" ls-tree --full-tree --name-only -r HEAD)
filter_array _LS IGNORED_FILES ROOT_TRACKED
_readarray_ _LS < <(git -C "$_SUBMOD" ls-tree --full-tree --name-only -r HEAD)
filter_array _LS IGNORED_FILES SUBMODULE_TRACKED
# Iterate both lists to get a list of all collisions and non-collisions.
TRACKED_FILES_COLLIDING=()
TRACKED_FILES_NONCOLLIDING=()
for subpath in "${SUBMODULE_TRACKED[@]}"; do
_collided=0
for rootpath in "${ROOT_TRACKED[@]}"; do
if [ "$rootpath" == "$subpath" ] ; then
TRACKED_FILES_COLLIDING+=("$subpath")
_collided=1
fi
done
if [ "$_collided" == "0" ] ; then
TRACKED_FILES_NONCOLLIDING+=("$subpath")
fi
done
# Write out the discovered tracked files
echo
echo CLOBBER_CHECKEDIN_ROOT is set to $CLOBBER_CHECKEDIN_ROOT;
echo " GLOBALLY_IGNORED_FILES ARE -- (ignored by this)"
printf ' %s\n' "${GLOBALLY_IGNORED_FILES[@]}"
if [ "$_HOOK_RAN" == "1" ] ; then
echo " CLOBBER_CHECKEDIN_ROOT_IGNORELIST IS -- (ignored by this)"
printf ' %s\n' "${CLOBBER_CHECKEDIN_ROOT_IGNORELIST[@]}"
else
echo " CLOBBER_CHECKEDIN_ROOT_IGNORELIST IS -- (no hook ran)"
fi
echo " ROOT TRACKED FILES -- ignoring the ignored list(s)"
printf ' %s\n' "${ROOT_TRACKED[@]}"
echo " SUBMODULE TRACKED FILES -- ignoring the ignored list(s)"
printf ' %s\n' "${SUBMODULE_TRACKED[@]}"
# Write out the status of collisions and non-collisions.
if [ ${#TRACKED_FILES_COLLIDING[@]} -eq 0 ]; then
echo " NO COLLISIONS FOUND, NOTHING TO WARN OR ERROR ON, OR REPLACE"
else
echo " COLLISIONS BETWEEN THESE TWO?"
printf ' %s\n' "${TRACKED_FILES_COLLIDING[@]}"
case $CLOBBER_CHECKEDIN_ROOT in
'ERROR')
echo " COLLISIONS FOUND, ERRORING";
printf ' %s\n' "${TRACKED_FILES_COLLIDING[@]}";
exit 127;;
'WARN')
echo " COLLISIONS FOUND, WARNING -- CONTINUING BUT WON'T SYMLINK THESE";
printf ' %s\n' "${TRACKED_FILES_COLLIDING[@]}";;
'REPLACE')
echo " COLLISIONS FOUND, REPLACE -- REPLACING ROOT'S COPY WITH THIS'S";
for collider in "${TRACKED_FILES_COLLIDING[@]}"; do
echo " Copying $SUBMOD_PATH/$collider over $collider"
rm "$collider"
cp "$SUBMOD_PATH/$collider" "$collider"
done ;;
*)
exit 127;;
esac
fi
# Wrap `ln -s` for use on MacOS. BSD's ln doesn't have a relative -r option. We
# only really needed the relative links in WSL anyway...
_ln_s_() {
local bsd_opts="$1"
local gnu_opts="$2"
local actual_file="$3"
local symlink_name="$4"
if [[ "$PROCTOR_BSD_UTIL" == "true" ]]; then
eval "ln -s$bsd_opts \"$actual_file\" \"$symlink_name\""
else
eval "ln -s$gnu_opts \"$actual_file\" \"$symlink_name\""
fi
}
# Now we've dealt with the "checked in, tracked files" that collide, we can deal
# with the non-checked in files, which each need their own collision check.
action-onlylinks() {
if [ "$FILE_ALREADY_EXISTS" == "false" ] ; then
echo " File $tracked_file doesn't exist yet. Linking to it."
mkdir -p "$(dirname "$tracked_file")"
_ln_s_ "" "r" "$_SUBMOD/$tracked_file" "$tracked_file"
else
if [ "$IS_SYMLINK_ALREADY" == "true" ] ; then
if [ "$SYMLINK_ALREADY_LINKS_TO_TARGET" == "true" ] ; then
echo " $tracked_file already linked $SUBMOD_PATH/$tracked_file"
else
echo " Replacing exisitng link $tracked_file"
rm "$tracked_file"
_ln_s_ "" "r" "$_SUBMOD/$tracked_file" "$tracked_file"
fi
else
echo " File $tracked_file is not a symlink, ignore it -- ONLYLINKS"
fi
fi
}
action-nodiff() {
# action-onlylinks done before this, so we only need file exists not symlink
if [ "$FILE_ALREADY_EXISTS" == "true" ] ; then
if [ "$IS_SYMLINK_ALREADY" == "false" ] ; then
if [ "$(diff "$tracked_file" "$SUBMOD_PATH/$tracked_file")" == "" ] ; then
# Empty diff
echo " File $tracked_file has an empty diff, replace with symlink"
rm "$tracked_file"
_ln_s_ "" "r" "$_SUBMOD/$tracked_file" "$tracked_file"
else
# Non-empty diff
echo " File $tracked_file has a non-empty diff, ignore it -- NODIFF"
fi
fi
fi
}
action-gracefully() {
if [ "$FILE_ALREADY_EXISTS" == "true" ] ; then
if [ "$IS_SYMLINK_ALREADY" == "false" ] ; then
if [ "$(diff "$tracked_file" "$SUBMOD_PATH/$tracked_file")" != "" ] ; then
# Exists, not a symlink, Non-empty diff :: backup and symlink
mkdir -p "$_PWD/.include/.clobbered/$(dirname "$tracked_file")"
mv "$tracked_file" "$_PWD/.include/.clobbered/$tracked_file"
_ln_s_ "" "r" "$_SUBMOD/$tracked_file" "$tracked_file"
echo " File $tracked_file has a non-empty diff, back it up first!"
fi
fi
fi
}
echo
echo CLOBBER_HOME is set to $CLOBBER_HOME;
for tracked_file in "${TRACKED_FILES_NONCOLLIDING[@]}"; do
echo " Actioning $tracked_file"
# Get "is a symlink and points to ..." status
# -e/-f track links and will say false if the _link_ is broken, so use -L to
# make sure we say the "file already exists" if the _broken link_ exists!
if [ -e "$tracked_file" ] || [ -L "$tracked_file" ]; then
FILE_ALREADY_EXISTS=true
else
FILE_ALREADY_EXISTS=false
fi
if [ -L "$tracked_file" ] ; then
IS_SYMLINK_ALREADY=true
if [ "$(readlink -f "$tracked_file")" == "$_SUBMOD/$tracked_file" ] ; then
SYMLINK_ALREADY_LINKS_TO_TARGET=true
else
SYMLINK_ALREADY_LINKS_TO_TARGET=false
fi
else
IS_SYMLINK_ALREADY=false
fi
#
case $CLOBBER_HOME in
'ONLYLINKS')
# ONLYLINKS means that if files exist and are already symlinks, they will be
# replaced with symlinks to **these** files. Also if file doesn't exist.
action-onlylinks;;
'NODIFF')
# NODIFF means that diff will evaluate the files. If they are the same, then
# this will replace them with links. Also actions the ONLYLINKS directive.
action-onlylinks;
action-nodiff;;
'DESTRUCTIVE')
# DESTRUCTIVE means this wont do any checks, and just overwrite any existing
# file with a symlink to **these** files.
echo " Actioning destructively and forcefully..."
mkdir -p "$(dirname "$tracked_file")"
_ln_s_ "f" "rf" "$_SUBMOD/$tracked_file" "$tracked_file"
;;
'GRACEFULLY')
# GRACEFULLY will do DESTRUCTIVE but place files that were not previously
# symlinks, with non-empty diffs, into a temp folder, ~/.include/.clobbered/
action-onlylinks;
action-nodiff;
action-gracefully;;
*)
;;
esac
done
################################################################################
# HISTORY: this script replaces a "find all not-{.git, .gitignore, .gitmodules}"
# "Find them and exec over them to duplicate paths and symlink files."
# Recorded here as a fun side note.
# find ~/dotfiles -type f ! -name '.git' ! -name '.gitignore' ! -name \
# '.gitmodules' -exec bash -c 'dotpath=$(echo "${0:$(($(pwd | wc -c)+$(echo \
# "dotfiles" | wc -c)))}") && mkdir -p $(dirname $dotpath) && \
# ln -sf ~/dotfiles/$dotpath ~/$dotpath' '{}' \;
################################################################################