You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1246 lines
31 KiB
1246 lines
31 KiB
#!/bin/sh |
|
# yadm - Yet Another Dotfiles Manager |
|
# Copyright (C) 2015-2019 Tim Byrne |
|
|
|
# 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 <https://www.gnu.org/licenses/>. |
|
|
|
# execute script with bash (shebang line is /bin/sh for portability) |
|
if [ -z "$BASH_VERSION" ]; then |
|
[ "$YADM_TEST" != 1 ] && exec bash "$0" "$@" |
|
fi |
|
|
|
VERSION=1.12.0 |
|
|
|
YADM_WORK="$HOME" |
|
YADM_DIR= |
|
YADM_LEGACY_DIR="${HOME}/.yadm" |
|
|
|
# these are the default paths relative to YADM_DIR |
|
YADM_REPO="repo.git" |
|
YADM_CONFIG="config" |
|
YADM_ENCRYPT="encrypt" |
|
YADM_ARCHIVE="files.gpg" |
|
YADM_BOOTSTRAP="bootstrap" |
|
YADM_HOOKS="hooks" |
|
|
|
HOOK_COMMAND="" |
|
FULL_COMMAND="" |
|
|
|
GPG_PROGRAM="gpg" |
|
GIT_PROGRAM="git" |
|
ENVTPL_PROGRAM="envtpl" |
|
LSB_RELEASE_PROGRAM="lsb_release" |
|
|
|
PROC_VERSION="/proc/version" |
|
OPERATING_SYSTEM="Unknown" |
|
|
|
ENCRYPT_INCLUDE_FILES="unparsed" |
|
|
|
# flag causing path translations with cygpath |
|
USE_CYGPATH=0 |
|
|
|
# flag when something may have changes (which prompts auto actions to be performed) |
|
CHANGES_POSSIBLE=0 |
|
|
|
# flag when a bootstrap should be performed after cloning |
|
# 0: skip auto_bootstrap, 1: ask, 2: perform bootstrap, 3: prevent bootstrap |
|
DO_BOOTSTRAP=0 |
|
|
|
function main() { |
|
|
|
require_git |
|
|
|
# capture full command, for passing to hooks |
|
FULL_COMMAND="$*" |
|
|
|
# create the YADM_DIR if it doesn't exist yet |
|
[ -d "$YADM_DIR" ] || mkdir -p "$YADM_DIR" |
|
|
|
# parse command line arguments |
|
local retval=0 |
|
internal_commands="^(alt|bootstrap|clean|clone|config|decrypt|encrypt|enter|help|init|introspect|list|perms|version)$" |
|
if [ -z "$*" ] ; then |
|
# no argumnts will result in help() |
|
help |
|
elif [[ "$1" =~ $internal_commands ]] ; then |
|
# for internal commands, process all of the arguments |
|
YADM_COMMAND="$1" |
|
YADM_ARGS=() |
|
shift |
|
|
|
while [[ $# -gt 0 ]] ; do |
|
key="$1" |
|
case $key in |
|
-a) # used by list() |
|
LIST_ALL="YES" |
|
;; |
|
-d) # used by all commands |
|
DEBUG="YES" |
|
;; |
|
-f) # used by init() and clone() |
|
FORCE="YES" |
|
;; |
|
-l) # used by decrypt() |
|
DO_LIST="YES" |
|
;; |
|
-w) # used by init() and clone() |
|
if [[ ! "$2" =~ ^/ ]] ; then |
|
error_out "You must specify a fully qualified work tree" |
|
fi |
|
YADM_WORK="$2" |
|
shift |
|
;; |
|
*) # any unhandled arguments |
|
YADM_ARGS+=("$1") |
|
;; |
|
esac |
|
shift |
|
done |
|
[ ! -d "$YADM_WORK" ] && error_out "Work tree does not exist: [$YADM_WORK]" |
|
HOOK_COMMAND="$YADM_COMMAND" |
|
invoke_hook "pre" |
|
$YADM_COMMAND "${YADM_ARGS[@]}" |
|
else |
|
# any other commands are simply passed through to git |
|
HOOK_COMMAND="$1" |
|
invoke_hook "pre" |
|
git_command "$@" |
|
retval="$?" |
|
fi |
|
|
|
# process automatic events |
|
auto_alt |
|
auto_perms |
|
auto_bootstrap |
|
|
|
exit_with_hook $retval |
|
|
|
} |
|
|
|
# ****** yadm Commands ****** |
|
|
|
function alt() { |
|
|
|
require_repo |
|
parse_encrypt |
|
|
|
local local_class |
|
local_class="$(config local.class)" |
|
|
|
local local_system |
|
local_system="$(config local.os)" |
|
if [ -z "$local_system" ] ; then |
|
local_system="$OPERATING_SYSTEM" |
|
fi |
|
|
|
local local_host |
|
local_host="$(config local.hostname)" |
|
if [ -z "$local_host" ] ; then |
|
local_host=$(hostname) |
|
local_host=${local_host%%.*} # trim any domain from hostname |
|
fi |
|
|
|
local local_user |
|
local_user="$(config local.user)" |
|
if [ -z "$local_user" ] ; then |
|
local_user=$(id -u -n) |
|
fi |
|
|
|
if [ "$YADM_COMPATIBILITY" = "1" ]; then |
|
alt_past |
|
else |
|
alt_future |
|
fi |
|
|
|
} |
|
|
|
function alt_future() { |
|
# Future alternate processing, not implemented yet |
|
return |
|
} |
|
|
|
function alt_past() { |
|
|
|
if [ -z "$local_class" ] ; then |
|
match_class="%" |
|
else |
|
match_class="$local_class" |
|
fi |
|
match_class="(%|$match_class)" |
|
match_system="(%|$local_system)" |
|
match_host="(%|$local_host)" |
|
match_user="(%|$local_user)" |
|
|
|
# regex for matching "<file>##CLASS.SYSTEM.HOSTNAME.USER" |
|
match1="^(.+)##(()|$match_system|$match_system\.$match_host|$match_system\.$match_host\.$match_user)$" |
|
match2="^(.+)##($match_class|$match_class\.$match_system|$match_class\.$match_system\.$match_host|$match_class\.$match_system\.$match_host\.$match_user)$" |
|
|
|
cd_work "Alternates" || return |
|
|
|
# only be noisy if the "alt" command was run directly |
|
[ "$YADM_COMMAND" = "alt" ] && loud="YES" |
|
|
|
# decide if a copy should be done instead of a symbolic link |
|
local do_copy=0 |
|
if [[ $OPERATING_SYSTEM == CYGWIN* ]] ; then |
|
if [[ $(config --bool yadm.cygwin-copy) == "true" ]] ; then |
|
do_copy=1 |
|
fi |
|
fi |
|
|
|
# process the files tracked by yadm once, this info is used multiple times |
|
tracked_files=() |
|
local IFS=$'\n' |
|
for tracked_file in $("$GIT_PROGRAM" ls-files | LC_ALL=C sort); do |
|
tracked_files+=("$tracked_file") |
|
done |
|
|
|
# generate a list of possible alt files |
|
possible_alts=() |
|
local IFS=$'\n' |
|
for possible_alt in "${tracked_files[@]}" "${ENCRYPT_INCLUDE_FILES[@]}"; do |
|
if [[ $possible_alt =~ .\#\#. ]]; then |
|
possible_alts+=("$YADM_WORK/${possible_alt%##*}") |
|
fi |
|
done |
|
alt_linked=() |
|
|
|
# loop over all "tracked" files |
|
# for every file which matches the above regex, create a symlink |
|
for match in $match1 $match2; do |
|
last_linked='' |
|
local IFS=$'\n' |
|
# the alt_paths looped over here are a unique sorted list of both files and their immediate parent directory |
|
for alt_path in $(for tracked in "${tracked_files[@]}"; do printf "%s\n" "$tracked" "${tracked%/*}"; done | LC_ALL=C sort -u) "${ENCRYPT_INCLUDE_FILES[@]}"; do |
|
alt_path="$YADM_WORK/$alt_path" |
|
if [ -e "$alt_path" ] ; then |
|
if [[ $alt_path =~ $match ]] ; then |
|
if [ "$alt_path" != "$last_linked" ] ; then |
|
new_link="${BASH_REMATCH[1]}" |
|
debug "Linking $alt_path to $new_link" |
|
[ -n "$loud" ] && echo "Linking $alt_path to $new_link" |
|
if [ "$do_copy" -eq 1 ]; then |
|
if [ -L "$new_link" ]; then |
|
rm -f "$new_link" |
|
fi |
|
cp -f "$alt_path" "$new_link" |
|
else |
|
ln -nfs "$alt_path" "$new_link" |
|
alt_linked+=("$alt_path") |
|
fi |
|
last_linked="$alt_path" |
|
fi |
|
fi |
|
fi |
|
done |
|
done |
|
|
|
# review alternate candidates for stale links |
|
# if a possible alt IS linked, but it's target is not part of alt_linked, |
|
# remove it. |
|
if readlink_available; then |
|
for stale_candidate in "${possible_alts[@]}"; do |
|
if [ -L "$stale_candidate" ]; then |
|
link_target=$(readlink "$stale_candidate" 2>/dev/null) |
|
if [ -n "$link_target" ]; then |
|
removal=yes |
|
for review_link in "${alt_linked[@]}"; do |
|
[ "$link_target" = "$review_link" ] && removal=no |
|
done |
|
[ "$removal" = "yes" ] && rm -f "$stale_candidate" |
|
fi |
|
fi |
|
done |
|
fi |
|
|
|
# loop over all "tracked" files |
|
# for every file which is a *##yadm.j2 create a real file |
|
local match="^(.+)##yadm\\.j2$" |
|
for tracked_file in "${tracked_files[@]}" "${ENCRYPT_INCLUDE_FILES[@]}"; do |
|
tracked_file="$YADM_WORK/$tracked_file" |
|
if [ -e "$tracked_file" ] ; then |
|
if [[ $tracked_file =~ $match ]] ; then |
|
real_file="${BASH_REMATCH[1]}" |
|
if envtpl_available; then |
|
debug "Creating $real_file from template $tracked_file" |
|
[ -n "$loud" ] && echo "Creating $real_file from template $tracked_file" |
|
YADM_CLASS="$local_class" \ |
|
YADM_OS="$local_system" \ |
|
YADM_HOSTNAME="$local_host" \ |
|
YADM_USER="$local_user" \ |
|
YADM_DISTRO=$(query_distro) \ |
|
"$ENVTPL_PROGRAM" --keep-template "$tracked_file" -o "$real_file" |
|
else |
|
debug "envtpl not available, not creating $real_file from template $tracked_file" |
|
[ -n "$loud" ] && echo "envtpl not available, not creating $real_file from template $tracked_file" |
|
fi |
|
fi |
|
fi |
|
done |
|
|
|
} |
|
|
|
function bootstrap() { |
|
|
|
bootstrap_available || error_out "Cannot execute bootstrap\n'$YADM_BOOTSTRAP' is not an executable program." |
|
|
|
# GIT_DIR should not be set for user's bootstrap code |
|
unset GIT_DIR |
|
|
|
echo "Executing $YADM_BOOTSTRAP" |
|
exec "$YADM_BOOTSTRAP" |
|
|
|
} |
|
|
|
function clean() { |
|
|
|
error_out "\"git clean\" has been disabled for safety. You could end up removing all unmanaged files." |
|
|
|
} |
|
|
|
function clone() { |
|
|
|
DO_BOOTSTRAP=1 |
|
|
|
clone_args=() |
|
while [[ $# -gt 0 ]] ; do |
|
key="$1" |
|
case $key in |
|
--bootstrap) # force bootstrap, without prompt |
|
DO_BOOTSTRAP=2 |
|
;; |
|
--no-bootstrap) # prevent bootstrap, without prompt |
|
DO_BOOTSTRAP=3 |
|
;; |
|
*) # main arguments are kept intact |
|
clone_args+=("$1") |
|
;; |
|
esac |
|
shift |
|
done |
|
|
|
[ -n "$DEBUG" ] && display_private_perms "initial" |
|
|
|
# clone will begin with a bare repo |
|
local empty= |
|
init $empty |
|
|
|
# add the specified remote, and configure the repo to track origin/master |
|
debug "Adding remote to new repo" |
|
"$GIT_PROGRAM" remote add origin "${clone_args[@]}" |
|
debug "Configuring new repo to track origin/master" |
|
"$GIT_PROGRAM" config branch.master.remote origin |
|
"$GIT_PROGRAM" config branch.master.merge refs/heads/master |
|
|
|
# fetch / merge (and possibly fallback to reset) |
|
debug "Doing an initial fetch of the origin" |
|
"$GIT_PROGRAM" fetch origin || { |
|
debug "Removing repo after failed clone" |
|
rm -rf "$YADM_REPO" |
|
error_out "Unable to fetch origin ${clone_args[0]}" |
|
} |
|
debug "Determining if repo tracks private directories" |
|
for private_dir in .ssh/ .gnupg/; do |
|
found_log=$("$GIT_PROGRAM" log -n 1 origin/master -- "$private_dir" 2>/dev/null) |
|
if [ -n "$found_log" ]; then |
|
debug "Private directory $private_dir is tracked by repo" |
|
assert_private_dirs "$private_dir" |
|
fi |
|
done |
|
[ -n "$DEBUG" ] && display_private_perms "pre-merge" |
|
debug "Doing an initial merge of origin/master" |
|
"$GIT_PROGRAM" merge origin/master || { |
|
debug "Merge failed, doing a reset and stashing conflicts." |
|
"$GIT_PROGRAM" reset origin/master |
|
if cd "$YADM_WORK"; then # necessary because of a bug in Git |
|
"$GIT_PROGRAM" -c user.name='yadm clone' -c user.email='yadm' stash save Conflicts preserved from yadm clone command 2>&1 |
|
cat <<EOF |
|
**NOTE** |
|
Merging origin/master failed. |
|
|
|
As a result, yadm did 'reset origin/master', and then |
|
stashed the conflicting data. |
|
|
|
This likely happened because you had files in \$HOME |
|
which conflicted with files tracked by origin/master. |
|
|
|
You can review the stashed conflicts with the |
|
command 'yadm stash show -p' from within your |
|
\$HOME directory. If you want to restore the |
|
stashed data, you can run 'yadm stash apply' or |
|
'yadm stash pop' and then handle the conflicts |
|
in another way. |
|
EOF |
|
else |
|
# skip auto_bootstrap if conflicts could not be stashed |
|
DO_BOOTSTRAP=0 |
|
cat <<EOF |
|
**NOTE** |
|
Merging origin/master failed. |
|
yadm did 'reset origin/master' instead. |
|
|
|
yadm did not stash these conflicts beacuse it was unable |
|
to change to the $YADM_WORK directory. |
|
|
|
Please review and resolve any differences appropriately |
|
If you know what you're doing, and want to overwrite the |
|
tracked files, consider 'yadm reset --hard origin/master' |
|
EOF |
|
fi |
|
} |
|
|
|
[ -n "$DEBUG" ] && display_private_perms "post-merge" |
|
|
|
CHANGES_POSSIBLE=1 |
|
|
|
} |
|
|
|
function config() { |
|
|
|
use_repo_config=0 |
|
local_options="^local\.(class|os|hostname|user)$" |
|
for option in "$@"; do |
|
[[ "$option" =~ $local_options ]] && use_repo_config=1 |
|
done |
|
|
|
if [ -z "$*" ] ; then |
|
# with no parameters, provide some helpful documentation |
|
echo "yadm supports the following configurations:" |
|
echo |
|
for supported_config in $(introspect_configs); do |
|
echo " ${supported_config}" |
|
done |
|
echo |
|
cat << EOF |
|
Please read the CONFIGURATION section in the man |
|
page for more details about configurations, and |
|
how to adjust them. |
|
EOF |
|
elif [ "$use_repo_config" -eq 1 ]; then |
|
|
|
require_repo |
|
|
|
# operate on the yadm repo's configuration file |
|
# this is always local to the machine |
|
"$GIT_PROGRAM" config --local "$@" |
|
|
|
CHANGES_POSSIBLE=1 |
|
|
|
else |
|
# operate on the yadm configuration file |
|
"$GIT_PROGRAM" config --file="$(mixed_path "$YADM_CONFIG")" "$@" |
|
|
|
fi |
|
|
|
} |
|
|
|
function decrypt() { |
|
|
|
require_gpg |
|
require_archive |
|
|
|
YADM_WORK=$(unix_path "$("$GIT_PROGRAM" config core.worktree)") |
|
|
|
if [ "$DO_LIST" = "YES" ] ; then |
|
tar_option="t" |
|
else |
|
tar_option="x" |
|
fi |
|
|
|
# decrypt the archive |
|
if ($GPG_PROGRAM -d "$YADM_ARCHIVE" || echo 1) | tar v${tar_option}f - -C "$YADM_WORK"; then |
|
[ ! "$DO_LIST" = "YES" ] && echo "All files decrypted." |
|
else |
|
error_out "Unable to extract encrypted files." |
|
fi |
|
|
|
CHANGES_POSSIBLE=1 |
|
|
|
} |
|
|
|
function encrypt() { |
|
|
|
require_gpg |
|
require_encrypt |
|
parse_encrypt |
|
|
|
cd_work "Encryption" || return |
|
|
|
# Build gpg options for gpg |
|
GPG_KEY="$(config yadm.gpg-recipient)" |
|
if [ "$GPG_KEY" = "ASK" ]; then |
|
GPG_OPTS=("--no-default-recipient" "-e") |
|
elif [ "$GPG_KEY" != "" ]; then |
|
GPG_OPTS=("-e" "-r $GPG_KEY") |
|
else |
|
GPG_OPTS=("-c") |
|
fi |
|
|
|
# report which files will be encrypted |
|
echo "Encrypting the following files:" |
|
printf '%s\n' "${ENCRYPT_INCLUDE_FILES[@]}" |
|
echo |
|
|
|
# encrypt all files which match the globs |
|
if tar -f - -c "${ENCRYPT_INCLUDE_FILES[@]}" | $GPG_PROGRAM --yes "${GPG_OPTS[@]}" --output "$YADM_ARCHIVE"; then |
|
echo "Wrote new file: $YADM_ARCHIVE" |
|
else |
|
error_out "Unable to write $YADM_ARCHIVE" |
|
fi |
|
|
|
# offer to add YADM_ARCHIVE if untracked |
|
archive_status=$("$GIT_PROGRAM" status --porcelain -uall "$(mixed_path "$YADM_ARCHIVE")" 2>/dev/null) |
|
archive_regex="^\?\?" |
|
if [[ $archive_status =~ $archive_regex ]] ; then |
|
echo "It appears that $YADM_ARCHIVE is not tracked by yadm's repository." |
|
echo "Would you like to add it now? (y/n)" |
|
read -r answer < /dev/tty |
|
if [[ $answer =~ ^[yY]$ ]] ; then |
|
"$GIT_PROGRAM" add "$(mixed_path "$YADM_ARCHIVE")" |
|
fi |
|
fi |
|
|
|
CHANGES_POSSIBLE=1 |
|
|
|
} |
|
|
|
function enter() { |
|
require_shell |
|
require_repo |
|
|
|
shell_opts="" |
|
shell_path="" |
|
if [[ "$SHELL" =~ bash$ ]]; then |
|
shell_opts="--norc" |
|
shell_path="\w" |
|
elif [[ "$SHELL" =~ [cz]sh$ ]]; then |
|
shell_opts="-f" |
|
shell_path="%~" |
|
fi |
|
|
|
echo "Entering yadm repo" |
|
|
|
yadm_prompt="yadm shell ($YADM_REPO) $shell_path > " |
|
PROMPT="$yadm_prompt" PS1="$yadm_prompt" "$SHELL" $shell_opts |
|
|
|
echo "Leaving yadm repo" |
|
} |
|
|
|
function git_command() { |
|
|
|
require_repo |
|
|
|
# translate 'gitconfig' to 'config' -- 'config' is reserved for yadm |
|
if [ "$1" = "gitconfig" ] ; then |
|
set -- "config" "${@:2}" |
|
fi |
|
|
|
# ensure private .ssh and .gnupg directories exist first |
|
# TODO: consider restricting this to only commands which modify the work-tree |
|
|
|
auto_private_dirs=$(config --bool yadm.auto-private-dirs) |
|
if [ "$auto_private_dirs" != "false" ] ; then |
|
assert_private_dirs .gnupg/ .ssh/ |
|
fi |
|
|
|
CHANGES_POSSIBLE=1 |
|
|
|
# pass commands through to git |
|
debug "Running git command $GIT_PROGRAM $*" |
|
"$GIT_PROGRAM" "$@" |
|
return "$?" |
|
} |
|
|
|
function help() { |
|
|
|
cat << EOF |
|
Usage: yadm <command> [options...] |
|
|
|
Manage dotfiles maintained in a Git repository. Manage alternate files |
|
for specific systems or hosts. Encrypt/decrypt private files. |
|
|
|
Git Commands: |
|
Any Git command or alias can be used as a <command>. It will operate |
|
on yadm's repository and files in the work tree (usually \$HOME). |
|
|
|
Commands: |
|
yadm init [-f] - Initialize an empty repository |
|
yadm clone <url> [-f] - Clone an existing repository |
|
yadm config <name> <value> - Configure a setting |
|
yadm list [-a] - List tracked files |
|
yadm alt - Create links for alternates |
|
yadm bootstrap - Execute \$HOME/.config/yadm/bootstrap |
|
yadm encrypt - Encrypt files |
|
yadm decrypt [-l] - Decrypt files |
|
yadm perms - Fix perms for private files |
|
|
|
Files: |
|
\$HOME/.config/yadm/config - yadm's configuration file |
|
\$HOME/.config/yadm/repo.git - yadm's Git repository |
|
\$HOME/.config/yadm/encrypt - List of globs used for encrypt/decrypt |
|
\$HOME/.config/yadm/files.gpg - Encrypted data stored here |
|
|
|
Use "man yadm" for complete documentation. |
|
EOF |
|
|
|
exit_with_hook 1 |
|
|
|
} |
|
|
|
function init() { |
|
|
|
# safety check, don't attempt to init when the repo is already present |
|
[ -d "$YADM_REPO" ] && [ -z "$FORCE" ] && |
|
error_out "Git repo already exists. [$YADM_REPO]\nUse '-f' if you want to force it to be overwritten." |
|
|
|
# remove existing if forcing the init to happen anyway |
|
[ -d "$YADM_REPO" ] && { |
|
debug "Removing existing repo prior to init" |
|
rm -rf "$YADM_REPO" |
|
} |
|
|
|
# init a new bare repo |
|
debug "Init new repo" |
|
"$GIT_PROGRAM" init --shared=0600 --bare "$(mixed_path "$YADM_REPO")" "$@" |
|
configure_repo |
|
|
|
CHANGES_POSSIBLE=1 |
|
|
|
} |
|
|
|
function introspect() { |
|
case "$1" in |
|
commands|configs|repo|switches) |
|
"introspect_$1" |
|
;; |
|
esac |
|
} |
|
|
|
function introspect_commands() { |
|
cat <<-EOF |
|
alt |
|
bootstrap |
|
clean |
|
clone |
|
config |
|
decrypt |
|
encrypt |
|
enter |
|
gitconfig |
|
help |
|
init |
|
introspect |
|
list |
|
perms |
|
version |
|
EOF |
|
} |
|
|
|
function introspect_configs() { |
|
cat << EOF |
|
local.class |
|
local.hostname |
|
local.os |
|
local.user |
|
yadm.auto-alt |
|
yadm.auto-perms |
|
yadm.auto-private-dirs |
|
yadm.cygwin-copy |
|
yadm.git-program |
|
yadm.gpg-perms |
|
yadm.gpg-program |
|
yadm.gpg-recipient |
|
yadm.ssh-perms |
|
EOF |
|
} |
|
|
|
function introspect_repo() { |
|
echo "$YADM_REPO" |
|
} |
|
|
|
function introspect_switches() { |
|
cat <<-EOF |
|
--yadm-archive |
|
--yadm-bootstrap |
|
--yadm-config |
|
--yadm-dir |
|
--yadm-encrypt |
|
--yadm-repo |
|
-Y |
|
EOF |
|
} |
|
|
|
function list() { |
|
|
|
require_repo |
|
|
|
# process relative to YADM_WORK when --all is specified |
|
if [ -n "$LIST_ALL" ] ; then |
|
cd_work "List" || return |
|
fi |
|
|
|
# list tracked files |
|
"$GIT_PROGRAM" ls-files |
|
|
|
} |
|
|
|
function perms() { |
|
|
|
parse_encrypt |
|
|
|
# TODO: prevent repeats in the files changed |
|
|
|
cd_work "Perms" || return |
|
|
|
GLOBS=() |
|
|
|
# include the archive created by "encrypt" |
|
[ -f "$YADM_ARCHIVE" ] && GLOBS+=("$YADM_ARCHIVE") |
|
|
|
# include all .ssh files (unless disabled) |
|
if [[ $(config --bool yadm.ssh-perms) != "false" ]] ; then |
|
GLOBS+=(".ssh" ".ssh/*") |
|
fi |
|
|
|
# include all gpg files (unless disabled) |
|
if [[ $(config --bool yadm.gpg-perms) != "false" ]] ; then |
|
GLOBS+=(".gnupg" ".gnupg/*") |
|
fi |
|
|
|
# include any files we encrypt |
|
GLOBS+=("${ENCRYPT_INCLUDE_FILES[@]}") |
|
|
|
# remove group/other permissions from collected globs |
|
#shellcheck disable=SC2068 |
|
#(SC2068 is disabled because in this case, we desire globbing) |
|
chmod -f go-rwx ${GLOBS[@]} >/dev/null 2>&1 |
|
# TODO: detect and report changing permissions in a portable way |
|
|
|
} |
|
|
|
function version() { |
|
|
|
echo "yadm $VERSION" |
|
exit_with_hook 0 |
|
|
|
} |
|
|
|
# ****** Utility Functions ****** |
|
|
|
function query_distro() { |
|
distro="" |
|
if command -v "$LSB_RELEASE_PROGRAM" >/dev/null 2>&1; then |
|
distro=$($LSB_RELEASE_PROGRAM -si 2>/dev/null) |
|
fi |
|
echo "$distro" |
|
} |
|
|
|
function process_global_args() { |
|
|
|
# global arguments are removed before the main processing is done |
|
MAIN_ARGS=() |
|
while [[ $# -gt 0 ]] ; do |
|
key="$1" |
|
case $key in |
|
-Y|--yadm-dir) # override the standard YADM_DIR |
|
if [[ ! "$2" =~ ^/ ]] ; then |
|
error_out "You must specify a fully qualified yadm directory" |
|
fi |
|
YADM_DIR="$2" |
|
shift |
|
;; |
|
--yadm-repo) # override the standard YADM_REPO |
|
if [[ ! "$2" =~ ^/ ]] ; then |
|
error_out "You must specify a fully qualified repo path" |
|
fi |
|
YADM_OVERRIDE_REPO="$2" |
|
shift |
|
;; |
|
--yadm-config) # override the standard YADM_CONFIG |
|
if [[ ! "$2" =~ ^/ ]] ; then |
|
error_out "You must specify a fully qualified config path" |
|
fi |
|
YADM_OVERRIDE_CONFIG="$2" |
|
shift |
|
;; |
|
--yadm-encrypt) # override the standard YADM_ENCRYPT |
|
if [[ ! "$2" =~ ^/ ]] ; then |
|
error_out "You must specify a fully qualified encrypt path" |
|
fi |
|
YADM_OVERRIDE_ENCRYPT="$2" |
|
shift |
|
;; |
|
--yadm-archive) # override the standard YADM_ARCHIVE |
|
if [[ ! "$2" =~ ^/ ]] ; then |
|
error_out "You must specify a fully qualified archive path" |
|
fi |
|
YADM_OVERRIDE_ARCHIVE="$2" |
|
shift |
|
;; |
|
--yadm-bootstrap) # override the standard YADM_BOOTSTRAP |
|
if [[ ! "$2" =~ ^/ ]] ; then |
|
error_out "You must specify a fully qualified bootstrap path" |
|
fi |
|
YADM_OVERRIDE_BOOTSTRAP="$2" |
|
shift |
|
;; |
|
*) # main arguments are kept intact |
|
MAIN_ARGS+=("$1") |
|
;; |
|
esac |
|
shift |
|
done |
|
|
|
} |
|
|
|
function set_yadm_dir() { |
|
|
|
# only resolve YADM_DIR if it hasn't been provided already |
|
[ -n "$YADM_DIR" ] && return |
|
|
|
# compatibility with major version 1 ignores XDG_CONFIG_HOME |
|
if [ "$YADM_COMPATIBILITY" = "1" ]; then |
|
YADM_DIR="$YADM_LEGACY_DIR" |
|
return |
|
fi |
|
|
|
local base_yadm_dir |
|
base_yadm_dir="$XDG_CONFIG_HOME" |
|
if [[ ! "$base_yadm_dir" =~ ^/ ]] ; then |
|
base_yadm_dir="${HOME}/.config" |
|
fi |
|
YADM_DIR="${base_yadm_dir}/yadm" |
|
|
|
issue_legacy_path_warning |
|
|
|
} |
|
|
|
function issue_legacy_path_warning() { |
|
|
|
# no warnings if YADM_DIR is resolved as the leacy path |
|
[ "$YADM_DIR" = "$YADM_LEGACY_DIR" ] && return |
|
|
|
# no warnings if the legacy directory doesn't exist |
|
[ ! -d "$YADM_LEGACY_DIR" ] && return |
|
|
|
# test for legacy paths |
|
local legacy_found |
|
legacy_found=() |
|
# this is ordered by importance |
|
for legacy_path in \ |
|
"$YADM_LEGACY_DIR/$YADM_REPO" \ |
|
"$YADM_LEGACY_DIR/$YADM_CONFIG" \ |
|
"$YADM_LEGACY_DIR/$YADM_ENCRYPT" \ |
|
"$YADM_LEGACY_DIR/$YADM_ARCHIVE" \ |
|
"$YADM_LEGACY_DIR/$YADM_BOOTSTRAP" \ |
|
"$YADM_LEGACY_DIR/$YADM_HOOKS" \ |
|
; \ |
|
do |
|
[ -e "$legacy_path" ] && legacy_found+=("$legacy_path") |
|
done |
|
|
|
[ ${#legacy_found[@]} -eq 0 ] && return |
|
|
|
local path_list |
|
for legacy_path in "${legacy_found[@]}"; do |
|
path_list="$path_list * $legacy_path"$'\n' |
|
done |
|
|
|
cat <<EOF |
|
|
|
**WARNING** |
|
Legacy configuration paths have been detected. |
|
|
|
Beginning with version 2.0.0, yadm uses the XDG Base Directory Specification |
|
to find its configurations. Read more about this change here: |
|
|
|
https://yadm.io/docs/xdg_config_home |
|
|
|
In your environment, the configuration directory has been resolved to: |
|
|
|
$YADM_DIR |
|
|
|
To remove this warning do one of the following: |
|
* Move yadm configurations to the directory listed above. (RECOMMENDED) |
|
* Specify your preferred yadm directory with -Y each execution. |
|
* Define an environment variable "YADM_COMPATIBILITY=1" to run in version 1 |
|
compatibility mode. (DEPRECATED) |
|
|
|
Legacy paths detected: |
|
${path_list} |
|
*********** |
|
EOF |
|
|
|
} |
|
|
|
function configure_paths() { |
|
|
|
# change all paths to be relative to YADM_DIR |
|
YADM_REPO="$YADM_DIR/$YADM_REPO" |
|
YADM_CONFIG="$YADM_DIR/$YADM_CONFIG" |
|
YADM_ENCRYPT="$YADM_DIR/$YADM_ENCRYPT" |
|
YADM_ARCHIVE="$YADM_DIR/$YADM_ARCHIVE" |
|
YADM_BOOTSTRAP="$YADM_DIR/$YADM_BOOTSTRAP" |
|
YADM_HOOKS="$YADM_DIR/$YADM_HOOKS" |
|
|
|
# independent overrides for paths |
|
if [ -n "$YADM_OVERRIDE_REPO" ]; then |
|
YADM_REPO="$YADM_OVERRIDE_REPO" |
|
fi |
|
if [ -n "$YADM_OVERRIDE_CONFIG" ]; then |
|
YADM_CONFIG="$YADM_OVERRIDE_CONFIG" |
|
fi |
|
if [ -n "$YADM_OVERRIDE_ENCRYPT" ]; then |
|
YADM_ENCRYPT="$YADM_OVERRIDE_ENCRYPT" |
|
fi |
|
if [ -n "$YADM_OVERRIDE_ARCHIVE" ]; then |
|
YADM_ARCHIVE="$YADM_OVERRIDE_ARCHIVE" |
|
fi |
|
if [ -n "$YADM_OVERRIDE_BOOTSTRAP" ]; then |
|
YADM_BOOTSTRAP="$YADM_OVERRIDE_BOOTSTRAP" |
|
fi |
|
|
|
# use the yadm repo for all git operations |
|
GIT_DIR=$(mixed_path "$YADM_REPO") |
|
export GIT_DIR |
|
|
|
} |
|
|
|
function configure_repo() { |
|
|
|
debug "Configuring new repo" |
|
|
|
# change bare to false (there is a working directory) |
|
"$GIT_PROGRAM" config core.bare 'false' |
|
|
|
# set the worktree for the yadm repo |
|
"$GIT_PROGRAM" config core.worktree "$(mixed_path "$YADM_WORK")" |
|
|
|
# by default, do not show untracked files and directories |
|
"$GIT_PROGRAM" config status.showUntrackedFiles no |
|
|
|
# possibly used later to ensure we're working on the yadm repo |
|
"$GIT_PROGRAM" config yadm.managed 'true' |
|
|
|
} |
|
|
|
function set_operating_system() { |
|
|
|
# special detection of WSL (windows subsystem for linux) |
|
local proc_version |
|
proc_version=$(cat "$PROC_VERSION" 2>/dev/null) |
|
if [[ "$proc_version" =~ Microsoft ]]; then |
|
OPERATING_SYSTEM="WSL" |
|
else |
|
OPERATING_SYSTEM=$(uname -s) |
|
fi |
|
|
|
case "$OPERATING_SYSTEM" in |
|
CYGWIN*) |
|
git_version="$("$GIT_PROGRAM" --version 2>/dev/null)" |
|
if [[ "$git_version" =~ windows ]] ; then |
|
USE_CYGPATH=1 |
|
fi |
|
;; |
|
*) |
|
;; |
|
esac |
|
|
|
} |
|
|
|
function debug() { |
|
|
|
[ -n "$DEBUG" ] && echo_e "DEBUG: $*" |
|
|
|
} |
|
|
|
function error_out() { |
|
|
|
echo_e "ERROR: $*" |
|
exit_with_hook 1 |
|
|
|
} |
|
|
|
function exit_with_hook() { |
|
|
|
invoke_hook "post" "$1" |
|
exit "$1" |
|
|
|
} |
|
|
|
function invoke_hook() { |
|
|
|
mode="$1" |
|
exit_status="$2" |
|
hook_command="${YADM_HOOKS}/${mode}_$HOOK_COMMAND" |
|
|
|
if [ -x "$hook_command" ] ; then |
|
debug "Invoking hook: $hook_command" |
|
|
|
# expose some internal data to all hooks |
|
work=$(unix_path "$("$GIT_PROGRAM" config core.worktree)") |
|
YADM_HOOK_COMMAND=$HOOK_COMMAND |
|
YADM_HOOK_EXIT=$exit_status |
|
YADM_HOOK_FULL_COMMAND=$FULL_COMMAND |
|
YADM_HOOK_REPO=$YADM_REPO |
|
YADM_HOOK_WORK=$work |
|
export YADM_HOOK_COMMAND |
|
export YADM_HOOK_EXIT |
|
export YADM_HOOK_FULL_COMMAND |
|
export YADM_HOOK_REPO |
|
export YADM_HOOK_WORK |
|
|
|
"$hook_command" |
|
hook_status=$? |
|
|
|
# failing "pre" hooks will prevent commands from being run |
|
if [ "$mode" = "pre" ] && [ "$hook_status" -ne 0 ]; then |
|
echo "Hook $hook_command was not successful" |
|
echo "$HOOK_COMMAND will not be run" |
|
exit "$hook_status" |
|
fi |
|
|
|
fi |
|
|
|
} |
|
|
|
function assert_private_dirs() { |
|
work=$(unix_path "$("$GIT_PROGRAM" config core.worktree)") |
|
for private_dir in "$@"; do |
|
if [ ! -d "$work/$private_dir" ]; then |
|
debug "Creating $work/$private_dir" |
|
#shellcheck disable=SC2174 |
|
mkdir -m 0700 -p "$work/$private_dir" >/dev/null 2>&1 |
|
fi |
|
done |
|
} |
|
|
|
function display_private_perms() { |
|
when="$1" |
|
for private_dir in .ssh .gnupg; do |
|
if [ -d "$YADM_WORK/$private_dir" ]; then |
|
private_perms=$(ls -ld "$YADM_WORK/$private_dir") |
|
debug "$when" private dir perms "$private_perms" |
|
fi |
|
done |
|
} |
|
|
|
function cd_work() { |
|
YADM_WORK=$(unix_path "$("$GIT_PROGRAM" config core.worktree)") |
|
cd "$YADM_WORK" || { |
|
debug "$1 not processed, unable to cd to $YADM_WORK" |
|
return 1 |
|
} |
|
return 0 |
|
} |
|
|
|
function parse_encrypt() { |
|
if [ "$ENCRYPT_INCLUDE_FILES" != "unparsed" ]; then |
|
#shellcheck disable=SC2034 |
|
PARSE_ENCRYPT_SHORT="parse_encrypt() not reprocessed" |
|
return |
|
fi |
|
|
|
ENCRYPT_INCLUDE_FILES=() |
|
ENCRYPT_EXCLUDE_FILES=() |
|
|
|
cd_work "Parsing encrypt" || return |
|
|
|
exclude_pattern="^!(.+)" |
|
if [ -f "$YADM_ENCRYPT" ] ; then |
|
# parse both included/excluded |
|
while IFS='' read -r line || [ -n "$line" ]; do |
|
if [[ ! $line =~ ^# && ! $line =~ ^[[:space:]]*$ ]] ; then |
|
local IFS=$'\n' |
|
for pattern in $line; do |
|
if [[ "$pattern" =~ $exclude_pattern ]]; then |
|
for ex_file in ${BASH_REMATCH[1]}; do |
|
if [ -e "$ex_file" ]; then |
|
ENCRYPT_EXCLUDE_FILES+=("$ex_file") |
|
fi |
|
done |
|
else |
|
for in_file in $pattern; do |
|
if [ -e "$in_file" ]; then |
|
ENCRYPT_INCLUDE_FILES+=("$in_file") |
|
fi |
|
done |
|
fi |
|
done |
|
fi |
|
done < "$YADM_ENCRYPT" |
|
|
|
# remove excludes from the includes |
|
#(SC2068 is disabled because in this case, we desire globbing) |
|
FINAL_INCLUDE=() |
|
#shellcheck disable=SC2068 |
|
for included in "${ENCRYPT_INCLUDE_FILES[@]}"; do |
|
skip= |
|
#shellcheck disable=SC2068 |
|
for ex_file in ${ENCRYPT_EXCLUDE_FILES[@]}; do |
|
[ "$included" == "$ex_file" ] && { skip=1; break; } |
|
done |
|
[ -n "$skip" ] || FINAL_INCLUDE+=("$included") |
|
done |
|
|
|
# sort the encrypted files |
|
#shellcheck disable=SC2207 |
|
IFS=$'\n' ENCRYPT_INCLUDE_FILES=($(LC_ALL=C sort <<<"${FINAL_INCLUDE[*]}")) |
|
unset IFS |
|
fi |
|
|
|
} |
|
|
|
# ****** Auto Functions ****** |
|
|
|
function auto_alt() { |
|
|
|
# process alternates if there are possible changes |
|
if [ "$CHANGES_POSSIBLE" = "1" ] ; then |
|
auto_alt=$(config --bool yadm.auto-alt) |
|
if [ "$auto_alt" != "false" ] ; then |
|
[ -d "$YADM_REPO" ] && alt |
|
fi |
|
fi |
|
|
|
} |
|
|
|
function auto_perms() { |
|
|
|
# process permissions if there are possible changes |
|
if [ "$CHANGES_POSSIBLE" = "1" ] ; then |
|
auto_perms=$(config --bool yadm.auto-perms) |
|
if [ "$auto_perms" != "false" ] ; then |
|
[ -d "$YADM_REPO" ] && perms |
|
fi |
|
fi |
|
|
|
} |
|
|
|
function auto_bootstrap() { |
|
|
|
bootstrap_available || return |
|
|
|
[ "$DO_BOOTSTRAP" -eq 0 ] && return |
|
[ "$DO_BOOTSTRAP" -eq 3 ] && return |
|
[ "$DO_BOOTSTRAP" -eq 2 ] && bootstrap |
|
if [ "$DO_BOOTSTRAP" -eq 1 ] ; then |
|
echo "Found $YADM_BOOTSTRAP" |
|
echo "It appears that a bootstrap program exists." |
|
echo "Would you like to execute it now? (y/n)" |
|
read -r answer < /dev/tty |
|
if [[ $answer =~ ^[yY]$ ]] ; then |
|
bootstrap |
|
fi |
|
fi |
|
|
|
} |
|
|
|
# ****** Prerequisites Functions ****** |
|
|
|
function require_archive() { |
|
[ -f "$YADM_ARCHIVE" ] || error_out "$YADM_ARCHIVE does not exist. did you forget to create it?" |
|
} |
|
function require_encrypt() { |
|
[ -f "$YADM_ENCRYPT" ] || error_out "$YADM_ENCRYPT does not exist. did you forget to create it?" |
|
} |
|
function require_git() { |
|
local alt_git |
|
alt_git="$(config yadm.git-program)" |
|
|
|
local more_info |
|
more_info="" |
|
|
|
if [ "$alt_git" != "" ] ; then |
|
GIT_PROGRAM="$alt_git" |
|
more_info="\nThis command has been set via the yadm.git-program configuration." |
|
fi |
|
command -v "$GIT_PROGRAM" >/dev/null 2>&1 || |
|
error_out "This functionality requires Git to be installed, but the command '$GIT_PROGRAM' cannot be located.$more_info" |
|
} |
|
function require_gpg() { |
|
local alt_gpg |
|
alt_gpg="$(config yadm.gpg-program)" |
|
|
|
local more_info |
|
more_info="" |
|
|
|
if [ "$alt_gpg" != "" ] ; then |
|
GPG_PROGRAM="$alt_gpg" |
|
more_info="\nThis command has been set via the yadm.gpg-program configuration." |
|
fi |
|
command -v "$GPG_PROGRAM" >/dev/null 2>&1 || |
|
error_out "This functionality requires GPG to be installed, but the command '$GPG_PROGRAM' cannot be located.$more_info" |
|
} |
|
function require_repo() { |
|
[ -d "$YADM_REPO" ] || error_out "Git repo does not exist. did you forget to run 'init' or 'clone'?" |
|
} |
|
function require_shell() { |
|
[ -x "$SHELL" ] || error_out "\$SHELL does not refer to an executable." |
|
} |
|
function bootstrap_available() { |
|
[ -f "$YADM_BOOTSTRAP" ] && [ -x "$YADM_BOOTSTRAP" ] && return |
|
return 1 |
|
} |
|
function envtpl_available() { |
|
command -v "$ENVTPL_PROGRAM" >/dev/null 2>&1 && return |
|
return 1 |
|
} |
|
function readlink_available() { |
|
command -v "readlink" >/dev/null 2>&1 && return |
|
return 1 |
|
} |
|
|
|
# ****** Directory tranlations ****** |
|
|
|
function unix_path() { |
|
# for paths used by bash/yadm |
|
if [ "$USE_CYGPATH" = "1" ] ; then |
|
cygpath -u "$1" |
|
else |
|
echo "$1" |
|
fi |
|
} |
|
function mixed_path() { |
|
# for paths used by Git |
|
if [ "$USE_CYGPATH" = "1" ] ; then |
|
cygpath -m "$1" |
|
else |
|
echo "$1" |
|
fi |
|
} |
|
|
|
# ****** echo replacements ****** |
|
function echo() { |
|
IFS=' ' |
|
printf '%s\n' "$*" |
|
} |
|
function echo_n() { |
|
IFS=' ' |
|
printf '%s' "$*" |
|
} |
|
function echo_e() { |
|
IFS=' ' |
|
printf '%b\n' "$*" |
|
} |
|
|
|
# ****** Main processing (when not unit testing) ****** |
|
|
|
if [ "$YADM_TEST" != 1 ] ; then |
|
process_global_args "$@" |
|
set_operating_system |
|
set_yadm_dir |
|
configure_paths |
|
main "${MAIN_ARGS[@]}" |
|
fi
|
|
|