mirror of
https://github.com/TheLocehiliosan/yadm
synced 2025-06-05 17:13:58 +00:00
The file will only be created from the template the first run. On subsequent runs it will not be updated, even if the template has changed.
2303 lines
58 KiB
Bash
Executable File
2303 lines
58 KiB
Bash
Executable File
#!/bin/sh
|
|
# yadm - Yet Another Dotfiles Manager
|
|
# Copyright (C) 2015-2024 Tim Byrne
|
|
# Copyright (C) 2025 Erik Flodin
|
|
|
|
# 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/>.
|
|
|
|
# shellcheck shell=bash
|
|
# execute script with bash (shebang line is /bin/sh for portability)
|
|
if [ -z "$BASH_VERSION" ]; then
|
|
[ "$YADM_TEST" != 1 ] && exec bash "$0" "$@"
|
|
fi
|
|
|
|
VERSION=3.5.0
|
|
|
|
YADM_WORK="$HOME"
|
|
YADM_DIR=
|
|
YADM_DATA=
|
|
|
|
YADM_LEGACY_DIR="${HOME}/.yadm"
|
|
YADM_LEGACY_ARCHIVE="files.gpg"
|
|
|
|
# these are the default paths relative to YADM_DIR
|
|
YADM_CONFIG="config"
|
|
YADM_ENCRYPT="encrypt"
|
|
YADM_BOOTSTRAP="bootstrap"
|
|
YADM_HOOKS="hooks"
|
|
YADM_ALT="alt"
|
|
|
|
# these are the default paths relative to YADM_DATA
|
|
YADM_REPO="repo.git"
|
|
YADM_ARCHIVE="archive"
|
|
|
|
HOOK_COMMAND=""
|
|
FULL_COMMAND=""
|
|
|
|
GPG_PROGRAM="gpg"
|
|
OPENSSL_PROGRAM="openssl"
|
|
GIT_PROGRAM="git"
|
|
AWK_PROGRAM=("gawk" "awk")
|
|
GIT_CRYPT_PROGRAM="git-crypt"
|
|
TRANSCRYPT_PROGRAM="transcrypt"
|
|
J2CLI_PROGRAM="j2"
|
|
ENVTPL_PROGRAM="envtpl"
|
|
ESH_PROGRAM="esh"
|
|
LSB_RELEASE_PROGRAM="lsb_release"
|
|
|
|
OS_RELEASE="/etc/os-release"
|
|
PROC_VERSION="/proc/version"
|
|
OPERATING_SYSTEM="Unknown"
|
|
|
|
ENCRYPT_INCLUDE_FILES="unparsed"
|
|
NO_ENCRYPT_TRACKED_FILES=()
|
|
|
|
LEGACY_WARNING_ISSUED=0
|
|
INVALID_ALT=()
|
|
|
|
GPG_OPTS=()
|
|
OPENSSL_OPTS=()
|
|
|
|
# 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
|
|
# the parameters will be space delimited and
|
|
# spaces, tabs, and backslashes will be escaped
|
|
_tab=$'\t'
|
|
for param in "$@"; do
|
|
param="${param//\\/\\\\}"
|
|
param="${param//$_tab/\\$_tab}"
|
|
param="${param// /\\ }"
|
|
_fc+=("$param")
|
|
done
|
|
FULL_COMMAND="${_fc[*]}"
|
|
|
|
# create the YADM_DIR & YADM_DATA if they doesn't exist yet
|
|
[ -d "$YADM_DIR" ] || mkdir -p "$YADM_DIR"
|
|
[ -d "$YADM_DATA" ] || mkdir -p "$YADM_DATA"
|
|
|
|
# parse command line arguments
|
|
local retval=0
|
|
internal_commands="^(alt|bootstrap|clean|clone|config|decrypt|encrypt|enter|git-crypt|help|--help|init|introspect|list|perms|transcrypt|upgrade|version|--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_COMMAND="${YADM_COMMAND/__/}"
|
|
YADM_ARGS=()
|
|
shift
|
|
|
|
# commands listed below do not process any of the parameters
|
|
if [[ "$YADM_COMMAND" =~ ^(enter|git_crypt)$ ]]; then
|
|
YADM_ARGS=("$@")
|
|
else
|
|
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(), clone() and upgrade()
|
|
FORCE="YES"
|
|
;;
|
|
-l) # used by decrypt()
|
|
DO_LIST="YES"
|
|
[[ "$YADM_COMMAND" =~ ^(clone|config)$ ]] && YADM_ARGS+=("$1")
|
|
;;
|
|
-w) # used by init() and clone()
|
|
YADM_WORK="$(qualify_path "$2" "work tree")"
|
|
shift
|
|
;;
|
|
*) # any unhandled arguments
|
|
YADM_ARGS+=("$1")
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
fi
|
|
[ ! -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
|
|
|
|
}
|
|
|
|
# ****** Alternate Processing ******
|
|
|
|
function score_file() {
|
|
local source="$1"
|
|
local target="$2"
|
|
local conditions="$3"
|
|
|
|
score=0
|
|
local template_processor=""
|
|
|
|
IFS=',' read -ra fields <<<"$conditions"
|
|
for field in "${fields[@]}"; do
|
|
local label=${field%%.*}
|
|
local value=${field#*.}
|
|
[ "$field" = "$label" ] && value="" # when .value is omitted
|
|
|
|
# Check for negative condition prefix (e.g., "~<label>")
|
|
local negate=0
|
|
if [ "${label:0:1}" = "~" ]; then
|
|
negate=1
|
|
label="${label:1}"
|
|
fi
|
|
|
|
shopt -s nocasematch
|
|
local -i delta=$((negate ? 1 : -1))
|
|
case "$label" in
|
|
default)
|
|
if ((negate)); then
|
|
INVALID_ALT+=("$source")
|
|
else
|
|
delta=0
|
|
fi
|
|
;;
|
|
a | arch)
|
|
[[ "$value" = "$local_arch" ]] && delta=1 || delta=-1
|
|
;;
|
|
o | os)
|
|
[[ "$value" = "$local_system" ]] && delta=2 || delta=-2
|
|
;;
|
|
d | distro)
|
|
[[ "${value// /_}" = "${local_distro// /_}" ]] && delta=4 || delta=-4
|
|
;;
|
|
f | distro_family)
|
|
[[ "${value// /_}" = "${local_distro_family// /_}" ]] && delta=8 || delta=-8
|
|
;;
|
|
c | class)
|
|
in_list "$value" "${local_classes[@]}" && delta=16 || delta=-16
|
|
;;
|
|
h | hostname)
|
|
[[ "$value" = "$local_host" ]] && delta=32 || delta=-32
|
|
;;
|
|
u | user)
|
|
[[ "$value" = "$local_user" ]] && delta=64 || delta=-64
|
|
;;
|
|
e | extension)
|
|
# extension isn't a condition and doesn't affect the score
|
|
continue
|
|
;;
|
|
s | seed | t | template | yadm)
|
|
if ((negate)); then
|
|
INVALID_ALT+=("$source")
|
|
elif [ "${label:0:1}" != "s" ] || [ ! -e "$target" ]; then
|
|
template_processor=$(choose_template_processor "$value")
|
|
if [ -n "$template_processor" ]; then
|
|
delta=0
|
|
elif [ -n "$loud" ]; then
|
|
echo "No supported template processor for $source"
|
|
else
|
|
debug "No supported template processor for $source"
|
|
fi
|
|
fi
|
|
;;
|
|
*)
|
|
INVALID_ALT+=("$source")
|
|
;;
|
|
esac
|
|
shopt -u nocasematch
|
|
|
|
((negate)) && delta=$((-delta))
|
|
if ((delta < 0)); then
|
|
score=0
|
|
return
|
|
fi
|
|
score=$((score + delta + (negate ? 0 : 1000)))
|
|
done
|
|
|
|
record_score "$score" "$target" "$source" "$template_processor"
|
|
}
|
|
|
|
function record_score() {
|
|
local score="$1"
|
|
local target="$2"
|
|
local source="$3"
|
|
local template_processor="$4"
|
|
|
|
# record nothing if the score is zero
|
|
[ "$score" -eq 0 ] && [ -z "$template_processor" ] && return
|
|
|
|
# search for the index of this target, to see if we already are tracking it
|
|
local -i index=$((${#alt_targets[@]} - 1))
|
|
for (( ; index >= 0; --index)); do
|
|
if [ "${alt_targets[$index]}" = "$target" ]; then
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [ $index -lt 0 ]; then
|
|
# $YADM_CONFIG must be processed first, in case other templates lookup yadm configurations
|
|
if [ "$target" = "$YADM_CONFIG" ]; then
|
|
alt_targets=("$target" "${alt_targets[@]}")
|
|
|
|
alt_sources=("$source" "${alt_sources[@]}")
|
|
alt_scores=("$score" "${alt_scores[@]}")
|
|
alt_template_processors=("$template_processor" "${alt_template_processors[@]}")
|
|
else
|
|
alt_targets+=("$target")
|
|
|
|
alt_sources+=("$source")
|
|
alt_scores+=("$score")
|
|
alt_template_processors+=("$template_processor")
|
|
fi
|
|
return
|
|
fi
|
|
|
|
if [[ -n "${alt_template_processors[$index]}" ]]; then
|
|
if [[ -z "$template_processor" || "$score" -lt "${alt_scores[$index]}" ]]; then
|
|
# Not template, or template but lower score
|
|
return
|
|
fi
|
|
elif [[ -z "$template_processor" && "$score" -le "${alt_scores[$index]}" ]]; then
|
|
# Not template and too low score
|
|
return
|
|
fi
|
|
|
|
# Record new alt
|
|
alt_sources[index]="$source"
|
|
alt_scores[index]="$score"
|
|
alt_template_processors[index]="$template_processor"
|
|
}
|
|
|
|
function choose_template_processor() {
|
|
local kind="$1"
|
|
|
|
if [[ "${kind:-default}" = "default" ]]; then
|
|
awk_available && echo "default"
|
|
elif [[ "$kind" = "esh" ]]; then
|
|
esh_available && echo "esh"
|
|
elif [[ "$kind" = "j2cli" || "$kind" = "j2" ]] && j2cli_available; then
|
|
echo "j2cli"
|
|
elif [[ "$kind" = "envtpl" || "$kind" = "j2" ]] && envtpl_available; then
|
|
echo "envtpl"
|
|
fi
|
|
|
|
}
|
|
|
|
# ****** Template Processors ******
|
|
|
|
function template() {
|
|
local processor="$1"
|
|
local input="$2"
|
|
local output="$3"
|
|
|
|
local content
|
|
if ! content=$("template_$processor" "$input"); then
|
|
echo "Error: failed to process template '$input'" >&2
|
|
return
|
|
fi
|
|
|
|
if [ -r "$output" ] && [ "$content" = "$(<"$output")" ]; then
|
|
debug "Template output '$output' is unchanged"
|
|
return
|
|
fi
|
|
|
|
# If the output file already exists as read-only, change it to be writable.
|
|
# There are some environments in which a read-only file will prevent the move
|
|
# from being successful.
|
|
if [ ! -w "$output" ] && [ -e "$output" ]; then
|
|
chmod u+w "$output"
|
|
fi
|
|
|
|
if [ -n "$loud" ]; then
|
|
echo "Creating $output from template $input"
|
|
else
|
|
debug "Creating $output from template $input"
|
|
fi
|
|
|
|
local temp_file="${output}.$$.$RANDOM"
|
|
if cat >"$temp_file" <<<"$content" && mv -f "$temp_file" "$output"; then
|
|
copy_perms "$input" "$output"
|
|
else
|
|
echo "Error: failed to create template output '$output'"
|
|
rm -f "$temp_file"
|
|
fi
|
|
}
|
|
|
|
function template_default() {
|
|
local input="$1"
|
|
|
|
# the explicit "space + tab" character class used below is used because not
|
|
# all versions of awk seem to support the POSIX character classes [[:blank:]]
|
|
local awk_pgm
|
|
read -r -d '' awk_pgm <<"EOF"
|
|
BEGIN {
|
|
classes = ARGV[2]
|
|
for (i = 3; i < ARGC; ++i) {
|
|
classes = classes "\n" ARGV[i]
|
|
}
|
|
yadm["class"] = class
|
|
yadm["classes"] = classes
|
|
yadm["arch"] = arch
|
|
yadm["os"] = os
|
|
yadm["hostname"] = host
|
|
yadm["user"] = user
|
|
yadm["distro"] = distro
|
|
yadm["distro_family"] = distro_family
|
|
yadm["source"] = ARGV[1]
|
|
|
|
VARIABLE = "(env|yadm)\\.[a-zA-Z0-9_]+"
|
|
|
|
current = 0
|
|
filename[current] = ARGV[1]
|
|
line[current] = 0
|
|
|
|
level = 0
|
|
skip[level] = 0
|
|
|
|
for (; current >= 0; --current) {
|
|
while ((res = getline <filename[current]) > 0) {
|
|
++line[current]
|
|
if ($0 ~ "^[ \t]*\\{%[ \t]*if[ \t]+" VARIABLE "[ \t]*[!=]=[ \t]*\".*\"[ \t]*%\\}$") {
|
|
if (skip[level]) { skip[++level] = 1; continue }
|
|
|
|
match($0, VARIABLE)
|
|
lhs = substr($0, RSTART, RLENGTH)
|
|
match($0, /[!=]=/)
|
|
op = substr($0, RSTART, RLENGTH)
|
|
match($0, /".*"/)
|
|
rhs = tolower(replace_vars(substr($0, RSTART + 1, RLENGTH - 2)))
|
|
|
|
if (lhs == "yadm.class") {
|
|
lhs = "not" rhs
|
|
split(classes, cls_array, "\n")
|
|
for (idx in cls_array) {
|
|
if (rhs == tolower(cls_array[idx])) { lhs = rhs; break }
|
|
}
|
|
}
|
|
else {
|
|
lhs = tolower(replace_vars("{{" lhs "}}"))
|
|
}
|
|
|
|
if (op == "==") { skip[++level] = lhs != rhs }
|
|
else { skip[++level] = lhs == rhs }
|
|
}
|
|
else if (/^[ \t]*\{%[ \t]*else[ \t]*%\}$/) {
|
|
if (level == 0 || skip[level] < 0) { error("else without matching if") }
|
|
skip[level] = skip[level] ? skip[level - 1] : -1
|
|
}
|
|
else if (/^[ \t]*\{%[ \t]*endif[ \t]*%\}$/) {
|
|
if (--level < 0) { error("endif without matching if") }
|
|
}
|
|
else if (!skip[level]) {
|
|
$0 = replace_vars($0)
|
|
if (match($0, /^[ \t]*\{%[ \t]*include[ \t]+("[^"]+"|[^"]+)[ \t]*%\}$/)) {
|
|
include = $0
|
|
sub(/^[ \t]*\{%[ \t]*include[ \t]+"?/, "", include)
|
|
sub(/"?[ \t]*%\}$/, "", include)
|
|
if (index(include, "/") != 1) {
|
|
include = source_dir "/" include
|
|
}
|
|
filename[++current] = include
|
|
line[current] = 0
|
|
}
|
|
else { print }
|
|
}
|
|
}
|
|
if (res >= 0) { close(filename[current]) }
|
|
else if (current == 0) { error("could not read input file") }
|
|
else { --current; error("could not read include file '" filename[current + 1] "'") }
|
|
}
|
|
if (level > 0) {
|
|
current = 0
|
|
error("unterminated if")
|
|
}
|
|
exit 0
|
|
}
|
|
function error(text) {
|
|
printf "%s:%d: error: %s\n",
|
|
filename[current], line[current], text > "/dev/stderr"
|
|
exit 1
|
|
}
|
|
function replace_vars(input) {
|
|
output = ""
|
|
while (match(input, "\\{\\{[ \t]*" VARIABLE "[ \t]*\\}\\}")) {
|
|
if (RSTART > 1) {
|
|
output = output substr(input, 0, RSTART - 1)
|
|
}
|
|
data = substr(input, RSTART + 2, RLENGTH - 4)
|
|
input = substr(input, RSTART + RLENGTH)
|
|
|
|
gsub(/[ \t]+/, "", data)
|
|
split(data, fields, /\./)
|
|
|
|
if (fields[1] == "env") {
|
|
output = output ENVIRON[fields[2]]
|
|
}
|
|
else if (fields[2] == "filename") {
|
|
output = output filename[current]
|
|
}
|
|
else {
|
|
output = output yadm[fields[2]]
|
|
}
|
|
}
|
|
return output input
|
|
}
|
|
EOF
|
|
|
|
"${AWK_PROGRAM[0]}" \
|
|
-v class="$local_class" \
|
|
-v arch="$local_arch" \
|
|
-v os="$local_system" \
|
|
-v host="$local_host" \
|
|
-v user="$local_user" \
|
|
-v distro="$local_distro" \
|
|
-v distro_family="$local_distro_family" \
|
|
-v source_dir="$(builtin_dirname "$input")" \
|
|
"$awk_pgm" \
|
|
"$input" "${local_classes[@]}"
|
|
}
|
|
|
|
function template_j2cli() {
|
|
local input="$1"
|
|
|
|
YADM_CLASS="$local_class" \
|
|
YADM_ARCH="$local_arch" \
|
|
YADM_OS="$local_system" \
|
|
YADM_HOSTNAME="$local_host" \
|
|
YADM_USER="$local_user" \
|
|
YADM_DISTRO="$local_distro" \
|
|
YADM_DISTRO_FAMILY="$local_distro_family" \
|
|
YADM_SOURCE="$input" \
|
|
YADM_CLASSES="$(join_string $'\n' "${local_classes[@]}")" \
|
|
"$J2CLI_PROGRAM" "$input"
|
|
}
|
|
|
|
function template_envtpl() {
|
|
local input="$1"
|
|
|
|
YADM_CLASS="$local_class" \
|
|
YADM_ARCH="$local_arch" \
|
|
YADM_OS="$local_system" \
|
|
YADM_HOSTNAME="$local_host" \
|
|
YADM_USER="$local_user" \
|
|
YADM_DISTRO="$local_distro" \
|
|
YADM_DISTRO_FAMILY="$local_distro_family" \
|
|
YADM_SOURCE="$input" \
|
|
YADM_CLASSES="$(join_string $'\n' "${local_classes[@]}")" \
|
|
"$ENVTPL_PROGRAM" -o - --keep-template "$input"
|
|
}
|
|
|
|
function template_esh() {
|
|
local input="$1"
|
|
|
|
YADM_CLASSES="$(join_string $'\n' "${local_classes[@]}")" \
|
|
"$ESH_PROGRAM" "$input" \
|
|
YADM_CLASS="$local_class" \
|
|
YADM_ARCH="$local_arch" \
|
|
YADM_OS="$local_system" \
|
|
YADM_HOSTNAME="$local_host" \
|
|
YADM_USER="$local_user" \
|
|
YADM_DISTRO="$local_distro" \
|
|
YADM_DISTRO_FAMILY="$local_distro_family" \
|
|
YADM_SOURCE="$input"
|
|
}
|
|
|
|
# ****** yadm Commands ******
|
|
|
|
function alt() {
|
|
|
|
require_repo
|
|
parse_encrypt
|
|
|
|
# gather values for processing alternates
|
|
local local_class
|
|
local -a local_classes
|
|
local local_arch
|
|
local local_system
|
|
local local_host
|
|
local local_user
|
|
local local_distro
|
|
local local_distro_family
|
|
set_local_alt_values
|
|
|
|
# only be noisy if the "alt" command was run directly
|
|
local loud=
|
|
[ "$YADM_COMMAND" = "alt" ] && loud="YES"
|
|
|
|
# decide if a copy should be done instead of a symbolic link
|
|
local do_copy=0
|
|
[ "$(config --bool yadm.alt-copy)" == "true" ] && do_copy=1
|
|
|
|
cd_work "Alternates" || return
|
|
|
|
# determine all tracked files
|
|
local tracked_files=()
|
|
local IFS=$'\n'
|
|
for tracked_file in $("$GIT_PROGRAM" ls-files -- '*##*'); do
|
|
tracked_files+=("$tracked_file")
|
|
done
|
|
|
|
local alt_targets=()
|
|
local alt_sources=()
|
|
local alt_scores=()
|
|
local alt_template_processors=()
|
|
|
|
local filename
|
|
for filename in "${tracked_files[@]}" "${ENCRYPT_INCLUDE_FILES[@]}"; do
|
|
local suffix="${filename#*##}"
|
|
if [ "$filename" = "$suffix" ]; then
|
|
continue
|
|
fi
|
|
local conditions="${suffix%%/*}"
|
|
suffix="${suffix:${#conditions}}"
|
|
|
|
local target="${YADM_BASE}/${filename%%##*}"
|
|
if [ "${target#"$YADM_ALT/"}" != "$target" ]; then
|
|
target="${YADM_BASE}/${target#"$YADM_ALT/"}"
|
|
fi
|
|
local source="${YADM_BASE}/${filename}"
|
|
|
|
# If conditions are given on a directory we check if this alt, without the
|
|
# filename part, has a target that's a symlink pointing at this source
|
|
# (which was the legacy behavior for yadm) and if so remove this target.
|
|
if [ -n "$suffix" ]; then
|
|
if [ -L "$target" ] && [ "$target" -ef "${YADM_BASE}/${filename%"$suffix"}" ]; then
|
|
rm -f "$target"
|
|
fi
|
|
target="$target$suffix"
|
|
fi
|
|
|
|
# Remove target if it's a symlink pointing at source
|
|
if [ -L "$target" ] && [ "$target" -ef "$source" ]; then
|
|
rm -f "$target"
|
|
fi
|
|
|
|
score_file "$source" "$target" "$conditions"
|
|
done
|
|
|
|
local alt_linked=()
|
|
|
|
alt_linking
|
|
report_invalid_alts
|
|
}
|
|
|
|
function report_invalid_alts() {
|
|
[ "$LEGACY_WARNING_ISSUED" = "1" ] && return
|
|
[ "${#INVALID_ALT[@]}" = "0" ] && return
|
|
local path_list=""
|
|
local invalid
|
|
for invalid in "${INVALID_ALT[@]}"; do
|
|
path_list="$path_list * $invalid"$'\n'
|
|
done
|
|
local msg
|
|
IFS='' read -r -d '' msg <<EOF
|
|
|
|
**WARNING**
|
|
Invalid alternates have been detected.
|
|
|
|
Beginning with version 2.0.0, yadm uses a new naming convention for alternate
|
|
files. Read more about this change here:
|
|
|
|
https://yadm.io/docs/upgrade_from_1
|
|
|
|
Or to learn more about alternates in general, read:
|
|
|
|
https://yadm.io/docs/alternates
|
|
|
|
To rename the invalid alternates run:
|
|
|
|
yadm mv <old name> <new name>
|
|
|
|
Invalid alternates detected:
|
|
${path_list}
|
|
***********
|
|
EOF
|
|
printf '%s\n' "$msg" >&2
|
|
}
|
|
|
|
function set_local_alt_values() {
|
|
|
|
local -a all_classes
|
|
all_classes=$(config --get-all local.class)
|
|
while IFS='' read -r class; do
|
|
local_classes+=("$class")
|
|
local_class="$class"
|
|
done <<<"$all_classes"
|
|
|
|
local_arch="$(config local.arch)"
|
|
if [[ -z "$local_arch" ]]; then
|
|
local_arch=$(uname -m)
|
|
fi
|
|
|
|
local_system="$(config local.os)"
|
|
if [[ -z "$local_system" ]]; then
|
|
local_system="$OPERATING_SYSTEM"
|
|
fi
|
|
|
|
local_host="$(config local.hostname)"
|
|
if [[ -z "$local_host" ]]; then
|
|
local_host=$(uname -n)
|
|
local_host=${local_host%%.*} # trim any domain from hostname
|
|
fi
|
|
|
|
local_user="$(config local.user)"
|
|
if [[ -z "$local_user" ]]; then
|
|
local_user=$(id -u -n)
|
|
fi
|
|
|
|
local_distro="$(config local.distro)"
|
|
if [[ -z "$local_distro" ]]; then
|
|
local_distro="$(query_distro)"
|
|
fi
|
|
|
|
local_distro_family="$(config local.distro-family)"
|
|
if [[ -z "$local_distro_family" ]]; then
|
|
local_distro_family="$(query_distro_family)"
|
|
fi
|
|
|
|
}
|
|
|
|
function alt_linking() {
|
|
local -a exclude=()
|
|
|
|
local log="debug"
|
|
[ -n "$loud" ] && log="echo"
|
|
|
|
local -i index
|
|
for ((index = 0; index < ${#alt_targets[@]}; ++index)); do
|
|
local target="${alt_targets[$index]}"
|
|
local source="${alt_sources[$index]}"
|
|
local template_processor="${alt_template_processors[$index]}"
|
|
|
|
if [ -L "$target" ]; then
|
|
rm -f "$target"
|
|
elif [ -d "$target" ]; then
|
|
echo "Skipping alt $source as $target is a directory"
|
|
continue
|
|
else
|
|
assert_parent "$target"
|
|
fi
|
|
|
|
if [ -n "$template_processor" ]; then
|
|
template "$template_processor" "$source" "$target"
|
|
elif [ "$do_copy" -eq 1 ]; then
|
|
$log "Copying $source to $target"
|
|
cp -f "$source" "$target"
|
|
elif [ -e "$target" ]; then
|
|
echo "Skipping alt $source as $target exists"
|
|
continue
|
|
else
|
|
$log "Linking $source to $target"
|
|
ln_relative "$source" "$target"
|
|
fi
|
|
|
|
exclude+=("${target#"$YADM_WORK"}")
|
|
done
|
|
|
|
update_exclude alt "${exclude[@]}"
|
|
}
|
|
|
|
function ln_relative() {
|
|
local source="$1"
|
|
local target="$2"
|
|
|
|
local rel_source
|
|
rel_source=$(relative_path "$(builtin_dirname "$target")" "$source")
|
|
|
|
ln -s "$rel_source" "$target"
|
|
alt_linked+=("$rel_source")
|
|
}
|
|
|
|
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
|
|
local -a args
|
|
local -i do_checkout=1
|
|
local -a submodules
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--bootstrap) # force bootstrap, without prompt
|
|
DO_BOOTSTRAP=2
|
|
;;
|
|
--no-bootstrap) # prevent bootstrap, without prompt
|
|
DO_BOOTSTRAP=3
|
|
;;
|
|
--checkout)
|
|
do_checkout=1
|
|
;;
|
|
-n | --no-checkout)
|
|
do_checkout=0
|
|
;;
|
|
--recursive | --recurse-submodules)
|
|
submodules+=(":/")
|
|
;;
|
|
--recurse-submodules=*)
|
|
submodules+=(":/${1#*=}")
|
|
;;
|
|
--bare | --mirror | --separate-git-dir=*)
|
|
# ignore arguments without separate parameter
|
|
;;
|
|
--separate-git-dir)
|
|
# ignore arguments with separate parameter
|
|
shift
|
|
;;
|
|
*)
|
|
args+=("$1")
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
[ -n "$DEBUG" ] && display_private_perms "initial"
|
|
|
|
# safety check, don't attempt to clone 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 clone to happen anyway
|
|
[ -d "$YADM_REPO" ] && {
|
|
debug "Removing existing repo prior to clone"
|
|
"$GIT_PROGRAM" -C "$YADM_WORK" submodule deinit -f --all
|
|
rm -rf "$YADM_REPO"
|
|
}
|
|
|
|
local wc
|
|
wc="$(mk_tmp_dir)"
|
|
[ -d "$wc" ] || error_out "Unable to create temporary directory"
|
|
|
|
# first clone without checkout
|
|
debug "Doing an initial clone of the repository"
|
|
(cd "$wc" &&
|
|
"$GIT_PROGRAM" -c core.sharedrepository=0600 clone --no-checkout \
|
|
--separate-git-dir="$YADM_REPO" "${args[@]}" repo.git) || {
|
|
debug "Removing repo after failed clone"
|
|
rm -rf "$YADM_REPO" "$wc"
|
|
error_out "Unable to clone the repository"
|
|
}
|
|
configure_repo
|
|
rm -rf "$wc"
|
|
|
|
# then reset the index as the --no-checkout flag makes the index empty
|
|
"$GIT_PROGRAM" reset --quiet -- ":/"
|
|
|
|
if [ "$YADM_WORK" = "$HOME" ]; then
|
|
debug "Determining if repo tracks private directories"
|
|
for private_dir in $(private_dirs all); do
|
|
found_log=$("$GIT_PROGRAM" log -n 1 -- "$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
|
|
fi
|
|
|
|
# finally check out (unless instructed not to) all files that don't exist in $YADM_WORK
|
|
if [[ $do_checkout -ne 0 ]]; then
|
|
[ -n "$DEBUG" ] && display_private_perms "pre-checkout"
|
|
|
|
cd_work "Clone" || return
|
|
|
|
"$GIT_PROGRAM" ls-files --deleted | while IFS= read -r file; do
|
|
"$GIT_PROGRAM" checkout -- ":/$file"
|
|
done
|
|
|
|
if [ ${#submodules[@]} -gt 0 ]; then
|
|
"$GIT_PROGRAM" submodule update --init --recursive -- "${submodules[@]}"
|
|
fi
|
|
|
|
if [ -n "$("$GIT_PROGRAM" ls-files --modified)" ]; then
|
|
local msg
|
|
IFS='' read -r -d '' msg <<EOF
|
|
**NOTE**
|
|
Local files with content that differs from the ones just
|
|
cloned were found in $YADM_WORK. They have been left
|
|
unmodified.
|
|
|
|
Please review and resolve any differences appropriately.
|
|
If you know what you're doing, and want to overwrite the
|
|
tracked files, consider 'yadm checkout "$YADM_WORK"'.
|
|
EOF
|
|
printf '%s\n' "$msg"
|
|
fi
|
|
|
|
[ -n "$DEBUG" ] && display_private_perms "post-checkout"
|
|
|
|
CHANGES_POSSIBLE=1
|
|
fi
|
|
|
|
}
|
|
|
|
function config() {
|
|
|
|
use_repo_config=0
|
|
local_options="^local\.(class|arch|os|hostname|user|distro|distro-family)$"
|
|
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
|
|
local IFS=$'\n'
|
|
for supported_config in $(introspect_configs); do
|
|
echo " ${supported_config}"
|
|
done
|
|
echo
|
|
local msg
|
|
read -r -d '' msg <<EOF
|
|
Please read the CONFIGURATION section in the man
|
|
page for more details about configurations, and
|
|
how to adjust them.
|
|
EOF
|
|
printf '%s\n' "$msg"
|
|
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 "$@"
|
|
|
|
CHANGES_POSSIBLE=1
|
|
|
|
else
|
|
# make sure parent folder of config file exists
|
|
assert_parent "$YADM_CONFIG"
|
|
# operate on the yadm configuration file
|
|
"$GIT_PROGRAM" config --file="$(mixed_path "$YADM_CONFIG")" "$@"
|
|
|
|
fi
|
|
|
|
}
|
|
|
|
function _set_gpg_options() {
|
|
gpg_key="$(config yadm.gpg-recipient)"
|
|
if [ "$gpg_key" = "ASK" ]; then
|
|
GPG_OPTS=("--no-default-recipient" "-e")
|
|
elif [ "$gpg_key" != "" ]; then
|
|
GPG_OPTS=("-e")
|
|
for key in $gpg_key; do
|
|
GPG_OPTS+=("-r $key")
|
|
done
|
|
else
|
|
GPG_OPTS=("-c")
|
|
fi
|
|
}
|
|
|
|
function _get_openssl_ciphername() {
|
|
OPENSSL_CIPHERNAME="$(config yadm.openssl-ciphername)"
|
|
if [ -z "$OPENSSL_CIPHERNAME" ]; then
|
|
OPENSSL_CIPHERNAME="aes-256-cbc"
|
|
fi
|
|
echo "$OPENSSL_CIPHERNAME"
|
|
}
|
|
|
|
function _set_openssl_options() {
|
|
cipher_name="$(_get_openssl_ciphername)"
|
|
OPENSSL_OPTS=("-${cipher_name}" -salt)
|
|
if [ "$(config --bool yadm.openssl-old)" == "true" ]; then
|
|
OPENSSL_OPTS+=(-md md5)
|
|
else
|
|
OPENSSL_OPTS+=(-pbkdf2 -iter 100000 -md sha512)
|
|
fi
|
|
}
|
|
|
|
function _get_cipher() {
|
|
output_archive="$1"
|
|
yadm_cipher="$(config yadm.cipher)"
|
|
if [ -z "$yadm_cipher" ]; then
|
|
yadm_cipher="gpg"
|
|
fi
|
|
}
|
|
|
|
function _decrypt_from() {
|
|
|
|
local output_archive
|
|
local yadm_cipher
|
|
_get_cipher "$1"
|
|
|
|
case "$yadm_cipher" in
|
|
gpg)
|
|
require_gpg
|
|
$GPG_PROGRAM -d "$output_archive"
|
|
;;
|
|
|
|
openssl)
|
|
require_openssl
|
|
_set_openssl_options
|
|
$OPENSSL_PROGRAM enc -d "${OPENSSL_OPTS[@]}" -in "$output_archive"
|
|
;;
|
|
|
|
*)
|
|
error_out "Unknown cipher '$yadm_cipher'"
|
|
;;
|
|
|
|
esac
|
|
|
|
}
|
|
|
|
function _encrypt_to() {
|
|
|
|
local output_archive
|
|
local yadm_cipher
|
|
_get_cipher "$1"
|
|
|
|
case "$yadm_cipher" in
|
|
gpg)
|
|
require_gpg
|
|
_set_gpg_options
|
|
$GPG_PROGRAM --yes "${GPG_OPTS[@]}" --output "$output_archive"
|
|
;;
|
|
|
|
openssl)
|
|
require_openssl
|
|
_set_openssl_options
|
|
$OPENSSL_PROGRAM enc -e "${OPENSSL_OPTS[@]}" -out "$output_archive"
|
|
;;
|
|
|
|
*)
|
|
error_out "Unknown cipher '$yadm_cipher'"
|
|
;;
|
|
|
|
esac
|
|
|
|
}
|
|
|
|
function decrypt() {
|
|
|
|
require_archive
|
|
|
|
[ -f "$YADM_ENCRYPT" ] && exclude_encrypted
|
|
|
|
if [ "$DO_LIST" = "YES" ]; then
|
|
tar_option="t"
|
|
else
|
|
tar_option="x"
|
|
fi
|
|
|
|
# decrypt the archive
|
|
if (_decrypt_from "$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_encrypt
|
|
exclude_encrypted
|
|
parse_encrypt
|
|
|
|
cd_work "Encryption" || return
|
|
|
|
# report which files will be encrypted
|
|
echo "Encrypting the following files:"
|
|
printf '%s\n' "${ENCRYPT_INCLUDE_FILES[@]}"
|
|
echo
|
|
|
|
if [ ${#NO_ENCRYPT_TRACKED_FILES[@]} -gt 0 ]; then
|
|
echo "Warning: The following files are tracked and will NOT be encrypted:"
|
|
printf '%s\n' "${NO_ENCRYPT_TRACKED_FILES[@]}"
|
|
echo
|
|
fi
|
|
|
|
# encrypt all files which match the globs
|
|
if tar -f - -c "${ENCRYPT_INCLUDE_FILES[@]}" | _encrypt_to "$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 git_crypt() {
|
|
require_git_crypt
|
|
enter "${GIT_CRYPT_PROGRAM} $*"
|
|
}
|
|
|
|
function transcrypt() {
|
|
require_transcrypt
|
|
enter "${TRANSCRYPT_PROGRAM} $*"
|
|
}
|
|
|
|
function enter() {
|
|
command="$*"
|
|
require_shell
|
|
require_repo
|
|
|
|
local -a shell_opts
|
|
local shell_path=""
|
|
if [[ "$SHELL" =~ bash$ ]]; then
|
|
shell_opts=("--norc")
|
|
shell_path="\w"
|
|
elif [[ "$SHELL" =~ [cz]sh$ ]]; then
|
|
shell_opts=("-f")
|
|
if [[ "$SHELL" =~ zsh$ && "$TERM" = "dumb" ]]; then
|
|
# Disable ZLE for tramp
|
|
shell_opts+=("--no-zle")
|
|
fi
|
|
shell_path="%~"
|
|
fi
|
|
|
|
shell_cmd=()
|
|
if [ -n "$command" ]; then
|
|
shell_cmd=('-c' "$*")
|
|
fi
|
|
|
|
GIT_WORK_TREE="$YADM_WORK"
|
|
export GIT_WORK_TREE
|
|
|
|
[ "${#shell_cmd[@]}" -eq 0 ] && echo "Entering yadm repo"
|
|
|
|
yadm_prompt="yadm shell ($YADM_REPO) $shell_path > "
|
|
PROMPT="$yadm_prompt" PS1="$yadm_prompt" "$SHELL" "${shell_opts[@]}" "${shell_cmd[@]}"
|
|
return_code="$?"
|
|
|
|
if [ "${#shell_cmd[@]}" -eq 0 ]; then
|
|
echo "Leaving yadm repo"
|
|
else
|
|
exit_with_hook "$return_code"
|
|
fi
|
|
}
|
|
|
|
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
|
|
|
|
if [ "$YADM_WORK" = "$HOME" ]; then
|
|
auto_private_dirs=$(config --bool yadm.auto-private-dirs)
|
|
if [ "$auto_private_dirs" != "false" ]; then
|
|
for pdir in $(private_dirs all); do
|
|
assert_private_dirs "$pdir"
|
|
done
|
|
fi
|
|
fi
|
|
|
|
CHANGES_POSSIBLE=1
|
|
|
|
# pass commands through to git
|
|
debug "Running git command $GIT_PROGRAM $*"
|
|
"$GIT_PROGRAM" "$@"
|
|
return "$?"
|
|
}
|
|
|
|
function help() {
|
|
readonly config="${YADM_CONFIG/$HOME/\$HOME}"
|
|
readonly encrypt="${YADM_ENCRYPT/$HOME/\$HOME}"
|
|
readonly bootstrap="${YADM_BOOTSTRAP/$HOME/\$HOME}"
|
|
readonly repo="${YADM_REPO/$HOME/\$HOME}"
|
|
readonly archive="${YADM_ARCHIVE/$HOME/\$HOME}"
|
|
|
|
readonly padding=" "
|
|
|
|
local msg
|
|
IFS='' read -r -d '' msg <<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
|
|
yadm enter [COMMAND] - Run sub-shell with GIT variables set
|
|
yadm git-crypt [OPTIONS] - Run git-crypt commands for the yadm repo
|
|
yadm transcrypt [OPTIONS] - Run transcrypt commands for the yadm repo
|
|
|
|
Files:
|
|
$config${padding:${#config}} - yadm's configuration file
|
|
$encrypt${padding:${#encrypt}} - List of globs to encrypt/decrypt
|
|
$bootstrap${padding:${#bootstrap}} - Script run via: yadm bootstrap
|
|
$repo${padding:${#repo}} - yadm's Git repository
|
|
$archive${padding:${#archive}} - Encrypted data stored here
|
|
|
|
Use "man yadm" for complete documentation.
|
|
EOF
|
|
printf '%s\n' "$msg"
|
|
exit_with_hook 1
|
|
|
|
}
|
|
|
|
# shellcheck disable=SC2120
|
|
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"
|
|
"$GIT_PROGRAM" -C "$YADM_WORK" submodule deinit -f --all
|
|
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() {
|
|
local msg
|
|
read -r -d '' msg <<-EOF
|
|
alt
|
|
bootstrap
|
|
clean
|
|
clone
|
|
config
|
|
decrypt
|
|
encrypt
|
|
enter
|
|
git-crypt
|
|
gitconfig
|
|
help
|
|
init
|
|
introspect
|
|
list
|
|
perms
|
|
transcrypt
|
|
upgrade
|
|
version
|
|
EOF
|
|
printf '%s' "$msg"
|
|
}
|
|
|
|
function introspect_configs() {
|
|
local msg
|
|
read -r -d '' msg <<-EOF
|
|
local.arch
|
|
local.class
|
|
local.distro
|
|
local.distro-family
|
|
local.hostname
|
|
local.os
|
|
local.user
|
|
yadm.alt-copy
|
|
yadm.auto-alt
|
|
yadm.auto-exclude
|
|
yadm.auto-perms
|
|
yadm.auto-private-dirs
|
|
yadm.cipher
|
|
yadm.git-program
|
|
yadm.gpg-perms
|
|
yadm.gpg-program
|
|
yadm.gpg-recipient
|
|
yadm.openssl-ciphername
|
|
yadm.openssl-old
|
|
yadm.openssl-program
|
|
yadm.ssh-perms
|
|
EOF
|
|
printf '%s' "$msg"
|
|
}
|
|
|
|
function introspect_repo() {
|
|
echo "$YADM_REPO"
|
|
}
|
|
|
|
function introspect_switches() {
|
|
local msg
|
|
read -r -d '' msg <<-EOF
|
|
--yadm-archive
|
|
--yadm-bootstrap
|
|
--yadm-config
|
|
--yadm-data
|
|
--yadm-dir
|
|
--yadm-encrypt
|
|
--yadm-repo
|
|
-Y
|
|
EOF
|
|
printf '%s' "$msg"
|
|
}
|
|
|
|
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")
|
|
|
|
# only include private globs if using HOME as worktree
|
|
if [ "$YADM_WORK" = "$HOME" ]; then
|
|
# include all .ssh files (unless disabled)
|
|
if [[ $(config --bool yadm.ssh-perms) != "false" ]]; then
|
|
GLOBS+=(".ssh" ".ssh/*" ".ssh/.[!.]*")
|
|
fi
|
|
|
|
# include all gpg files (unless disabled)
|
|
gnupghome="$(private_dirs gnupg)"
|
|
if [[ $(config --bool yadm.gpg-perms) != "false" ]]; then
|
|
GLOBS+=("${gnupghome}" "${gnupghome}/*" "${gnupghome}/.[!.]*")
|
|
fi
|
|
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
|
|
# TODO: detect and report changing permissions in a portable way
|
|
|
|
}
|
|
|
|
function upgrade() {
|
|
|
|
local actions_performed=0
|
|
local -a submodules
|
|
local repo_updates=0
|
|
|
|
[[ -n "${YADM_OVERRIDE_REPO}${YADM_OVERRIDE_ARCHIVE}" || "$YADM_DATA" = "$YADM_DIR" ]] &&
|
|
error_out "Unable to upgrade. Paths have been overridden with command line options"
|
|
|
|
# choose a legacy repo, the version 2 location will be favored
|
|
local LEGACY_REPO=
|
|
[ -d "$YADM_LEGACY_DIR/repo.git" ] && LEGACY_REPO="$YADM_LEGACY_DIR/repo.git"
|
|
[ -d "$YADM_DIR/repo.git" ] && LEGACY_REPO="$YADM_DIR/repo.git"
|
|
|
|
# handle legacy repo
|
|
if [ -d "$LEGACY_REPO" ]; then
|
|
# choose
|
|
# legacy repo detected, it must be moved to YADM_REPO
|
|
if [ -e "$YADM_REPO" ]; then
|
|
error_out "Unable to upgrade. '$YADM_REPO' already exists. Refusing to overwrite it."
|
|
else
|
|
actions_performed=1
|
|
echo "Moving $LEGACY_REPO to $YADM_REPO"
|
|
|
|
export GIT_DIR="$LEGACY_REPO"
|
|
|
|
# Must absorb git dirs, otherwise deinit below will fail for modules that have
|
|
# been cloned first and then added as a submodule.
|
|
"$GIT_PROGRAM" submodule absorbgitdirs
|
|
|
|
local submodule_status
|
|
submodule_status=$("$GIT_PROGRAM" -C "$YADM_WORK" submodule status)
|
|
while read -r sha submodule rest; do
|
|
[ "$submodule" == "" ] && continue
|
|
if [[ "$sha" = -* ]]; then
|
|
continue
|
|
fi
|
|
"$GIT_PROGRAM" -C "$YADM_WORK" submodule deinit ${FORCE:+-f} -- "$submodule" || {
|
|
for other in "${submodules[@]}"; do
|
|
"$GIT_PROGRAM" -C "$YADM_WORK" submodule update --init --recursive -- "$other"
|
|
done
|
|
error_out "Unable to upgrade. Could not deinit submodule $submodule"
|
|
}
|
|
submodules+=("$submodule")
|
|
done <<<"$submodule_status"
|
|
|
|
assert_parent "$YADM_REPO"
|
|
mv "$LEGACY_REPO" "$YADM_REPO"
|
|
fi
|
|
fi
|
|
GIT_DIR="$YADM_REPO"
|
|
export GIT_DIR
|
|
|
|
# choose a legacy archive, the version 2 location will be favored
|
|
local LEGACY_ARCHIVE=
|
|
[ -e "$YADM_LEGACY_DIR/$YADM_LEGACY_ARCHIVE" ] && LEGACY_ARCHIVE="$YADM_LEGACY_DIR/$YADM_LEGACY_ARCHIVE"
|
|
[ -e "$YADM_DIR/$YADM_LEGACY_ARCHIVE" ] && LEGACY_ARCHIVE="$YADM_DIR/$YADM_LEGACY_ARCHIVE"
|
|
|
|
# handle legacy archive
|
|
if [ -e "$LEGACY_ARCHIVE" ]; then
|
|
actions_performed=1
|
|
echo "Moving $LEGACY_ARCHIVE to $YADM_ARCHIVE"
|
|
assert_parent "$YADM_ARCHIVE"
|
|
# test to see if path is "tracked" in repo, if so 'git mv' must be used
|
|
if "$GIT_PROGRAM" ls-files --error-unmatch "$LEGACY_ARCHIVE" &>/dev/null; then
|
|
"$GIT_PROGRAM" mv "$LEGACY_ARCHIVE" "$YADM_ARCHIVE" && repo_updates=1
|
|
else
|
|
mv -i "$LEGACY_ARCHIVE" "$YADM_ARCHIVE"
|
|
fi
|
|
fi
|
|
|
|
# handle any remaining version 1 paths
|
|
for legacy_path in \
|
|
"$YADM_LEGACY_DIR/config" \
|
|
"$YADM_LEGACY_DIR/encrypt" \
|
|
"$YADM_LEGACY_DIR/bootstrap" \
|
|
"$YADM_LEGACY_DIR"/hooks/{pre,post}_*; do
|
|
if [ -e "$legacy_path" ]; then
|
|
new_filename="${legacy_path#"$YADM_LEGACY_DIR/"}"
|
|
new_filename="$YADM_DIR/$new_filename"
|
|
actions_performed=1
|
|
echo "Moving $legacy_path to $new_filename"
|
|
assert_parent "$new_filename"
|
|
# test to see if path is "tracked" in repo, if so 'git mv' must be used
|
|
if "$GIT_PROGRAM" ls-files --error-unmatch "$legacy_path" &>/dev/null; then
|
|
"$GIT_PROGRAM" mv "$legacy_path" "$new_filename" && repo_updates=1
|
|
else
|
|
mv -i "$legacy_path" "$new_filename"
|
|
fi
|
|
fi
|
|
done
|
|
|
|
# handle submodules, which need to be reinitialized
|
|
for submodule in "${submodules[@]}"; do
|
|
"$GIT_PROGRAM" -C "$YADM_WORK" submodule update --init --recursive -- "$submodule"
|
|
done
|
|
|
|
[ "$actions_performed" -eq 0 ] &&
|
|
echo "No legacy paths found. Upgrade is not necessary"
|
|
|
|
[ "$repo_updates" -eq 1 ] &&
|
|
echo "Some files tracked by yadm have been renamed. These changes should probably be commited now."
|
|
|
|
exit 0
|
|
|
|
}
|
|
|
|
function version() {
|
|
|
|
echo "bash version $BASH_VERSION"
|
|
printf " "
|
|
"$GIT_PROGRAM" --version
|
|
echo "yadm version $VERSION"
|
|
exit_with_hook 0
|
|
|
|
}
|
|
|
|
# ****** Utility Functions ******
|
|
|
|
function update_exclude() {
|
|
|
|
local auto_exclude
|
|
auto_exclude=$(config --bool yadm.auto-exclude)
|
|
[ "$auto_exclude" == "false" ] && return 0
|
|
|
|
local exclude_path="${YADM_REPO}/info/exclude"
|
|
local newline=$'\n'
|
|
|
|
local part_path="$exclude_path.yadm-$1"
|
|
local part_str
|
|
part_str=$(join_string "$newline" "${@:2}")
|
|
|
|
if [ -e "$part_path" ]; then
|
|
if [ "$part_str" = "$(<"$part_path")" ]; then
|
|
return
|
|
fi
|
|
|
|
rm -f "$part_path"
|
|
elif [ -z "$part_str" ]; then
|
|
return
|
|
fi
|
|
|
|
if [ -n "$part_str" ]; then
|
|
assert_parent "$part_path"
|
|
cat >"$part_path" <<<"$part_str"
|
|
fi
|
|
|
|
local exclude_flag="# yadm-auto-excludes"
|
|
|
|
local exclude_header="${exclude_flag}${newline}"
|
|
exclude_header="${exclude_header}# This section is managed by yadm."
|
|
exclude_header="${exclude_header}${newline}"
|
|
exclude_header="${exclude_header}# Any edits below will be lost."
|
|
exclude_header="${exclude_header}${newline}"
|
|
|
|
# read info/exclude
|
|
local unmanaged=""
|
|
local managed=""
|
|
if [ -e "$exclude_path" ]; then
|
|
local -i flag_seen=0
|
|
local line
|
|
while IFS='' read -r line || [ -n "$line" ]; do
|
|
[ "$line" = "$exclude_flag" ] && flag_seen=1
|
|
if ((flag_seen)); then
|
|
managed="${managed}${line}${newline}"
|
|
else
|
|
unmanaged="${unmanaged}${line}${newline}"
|
|
fi
|
|
done <"$exclude_path"
|
|
fi
|
|
|
|
local exclude_str=""
|
|
for suffix in alt encrypt; do
|
|
if [ -e "${exclude_path}.yadm-$suffix" ]; then
|
|
local header="# yadm $suffix$newline"
|
|
exclude_str="$exclude_str$header$(<"$exclude_path".yadm-"$suffix")"
|
|
fi
|
|
done
|
|
|
|
if [ "${exclude_header}${exclude_str}${newline}" != "$managed" ]; then
|
|
debug "Updating ${exclude_path}"
|
|
cat >"$exclude_path" <<<"${unmanaged}${exclude_header}${exclude_str}"
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
function exclude_encrypted() {
|
|
local -a exclude=()
|
|
|
|
if [ -r "$YADM_ENCRYPT" ]; then
|
|
local pattern
|
|
while IFS='' read -r pattern || [ -n "$pattern" ]; do
|
|
# Prepend / to the pattern so that it matches the same files as in
|
|
# parse_encrypt (i.e. only from the root)
|
|
if [ "${pattern:0:1}" = "!" ]; then
|
|
exclude+=("!/${pattern:1}")
|
|
elif ! [[ $pattern =~ ^[[:blank:]]*(#|$) ]]; then
|
|
exclude+=("/$pattern")
|
|
fi
|
|
done <"$YADM_ENCRYPT"
|
|
fi
|
|
|
|
update_exclude encrypt "${exclude[@]}"
|
|
}
|
|
|
|
function query_distro() {
|
|
local distro=""
|
|
if command -v "$LSB_RELEASE_PROGRAM" &>/dev/null; then
|
|
distro=$($LSB_RELEASE_PROGRAM -si 2>/dev/null)
|
|
elif [ -f "$OS_RELEASE" ]; then
|
|
while IFS='' read -r line || [ -n "$line" ]; do
|
|
if [[ "$line" = ID=* ]]; then
|
|
distro="${line#ID=}"
|
|
distro="${distro//\"/}"
|
|
break
|
|
fi
|
|
done <"$OS_RELEASE"
|
|
fi
|
|
echo "$distro"
|
|
}
|
|
|
|
function query_distro_family() {
|
|
local family=""
|
|
if [ -f "$OS_RELEASE" ]; then
|
|
while IFS='' read -r line || [ -n "$line" ]; do
|
|
if [[ "$line" = ID_LIKE=* ]]; then
|
|
family="${line#ID_LIKE=}"
|
|
break
|
|
elif [[ "$line" = ID=* ]]; then
|
|
family="${line#ID=}"
|
|
# No break, only used as fallback in case ID_LIKE isn't found
|
|
fi
|
|
done <"$OS_RELEASE"
|
|
fi
|
|
echo "${family//\"/}"
|
|
}
|
|
|
|
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
|
|
YADM_DIR="$(qualify_path "$2" "yadm")"
|
|
shift
|
|
;;
|
|
--yadm-data) # override the standard YADM_DATA
|
|
YADM_DATA="$(qualify_path "$2" "data")"
|
|
shift
|
|
;;
|
|
--yadm-repo) # override the standard YADM_REPO
|
|
YADM_OVERRIDE_REPO="$(qualify_path "$2" "repo")"
|
|
shift
|
|
;;
|
|
--yadm-config) # override the standard YADM_CONFIG
|
|
YADM_OVERRIDE_CONFIG="$(qualify_path "$2" "config")"
|
|
shift
|
|
;;
|
|
--yadm-encrypt) # override the standard YADM_ENCRYPT
|
|
YADM_OVERRIDE_ENCRYPT="$(qualify_path "$2" "encrypt")"
|
|
shift
|
|
;;
|
|
--yadm-archive) # override the standard YADM_ARCHIVE
|
|
YADM_OVERRIDE_ARCHIVE="$(qualify_path "$2" "archive")"
|
|
shift
|
|
;;
|
|
--yadm-bootstrap) # override the standard YADM_BOOTSTRAP
|
|
YADM_OVERRIDE_BOOTSTRAP="$(qualify_path "$2" "bootstrap")"
|
|
shift
|
|
;;
|
|
*) # main arguments are kept intact
|
|
MAIN_ARGS+=("$1")
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
}
|
|
|
|
function qualify_path() {
|
|
local path="$1"
|
|
if [ -z "$path" ]; then
|
|
error_out "You can't specify an empty $2 path"
|
|
fi
|
|
|
|
if [ "$path" = "." ]; then
|
|
path="$PWD"
|
|
elif [[ "$path" != /* ]]; then
|
|
path="$PWD/${path#./}"
|
|
fi
|
|
echo "$path"
|
|
}
|
|
|
|
function set_yadm_dirs() {
|
|
|
|
# only resolve YADM_DATA if it hasn't been provided already
|
|
if [ -z "$YADM_DATA" ]; then
|
|
local base_yadm_data="$XDG_DATA_HOME"
|
|
if [[ ! "$base_yadm_data" =~ ^/ ]]; then
|
|
base_yadm_data="${HOME}/.local/share"
|
|
fi
|
|
YADM_DATA="${base_yadm_data}/yadm"
|
|
fi
|
|
|
|
# only resolve YADM_DIR if it hasn't been provided already
|
|
if [ -z "$YADM_DIR" ]; then
|
|
local base_yadm_dir="$XDG_CONFIG_HOME"
|
|
if [[ ! "$base_yadm_dir" =~ ^/ ]]; then
|
|
base_yadm_dir="${HOME}/.config"
|
|
fi
|
|
YADM_DIR="${base_yadm_dir}/yadm"
|
|
fi
|
|
|
|
issue_legacy_path_warning
|
|
|
|
}
|
|
|
|
function issue_legacy_path_warning() {
|
|
|
|
# no warnings during upgrade
|
|
[[ "${MAIN_ARGS[*]}" =~ upgrade ]] && return
|
|
|
|
# no warnings if YADM_DIR is resolved as the leacy path
|
|
[ "$YADM_DIR" = "$YADM_LEGACY_DIR" ] && return
|
|
|
|
# no warnings if overrides have been provided
|
|
[[ -n "${YADM_OVERRIDE_REPO}${YADM_OVERRIDE_ARCHIVE}" || "$YADM_DATA" = "$YADM_DIR" ]] && return
|
|
|
|
# test for legacy paths
|
|
local legacy_found=()
|
|
# this is ordered by importance
|
|
for legacy_path in \
|
|
"$YADM_DIR/$YADM_REPO" \
|
|
"$YADM_DIR/$YADM_LEGACY_ARCHIVE" \
|
|
"$YADM_LEGACY_DIR/$YADM_REPO" \
|
|
"$YADM_LEGACY_DIR/$YADM_BOOTSTRAP" \
|
|
"$YADM_LEGACY_DIR/$YADM_CONFIG" \
|
|
"$YADM_LEGACY_DIR/$YADM_ENCRYPT" \
|
|
"$YADM_LEGACY_DIR/$YADM_HOOKS"/{pre,post}_* \
|
|
"$YADM_LEGACY_DIR/$YADM_LEGACY_ARCHIVE"; 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
|
|
|
|
local msg
|
|
IFS='' read -r -d '' msg <<EOF
|
|
|
|
**WARNING**
|
|
Legacy paths have been detected.
|
|
|
|
With version 3.0.0, yadm uses the XDG Base Directory Specification
|
|
to find its configurations and data. Read more about these changes here:
|
|
|
|
https://yadm.io/docs/upgrade_from_2
|
|
https://yadm.io/docs/upgrade_from_1
|
|
|
|
In your environment, the data directory has been resolved to:
|
|
|
|
$YADM_DATA
|
|
|
|
To remove this warning do one of the following:
|
|
* Run "yadm upgrade" to move the yadm data to the new paths. (RECOMMENDED)
|
|
* Manually move yadm data to new default paths and reinit any submodules.
|
|
* Specify your preferred paths with --yadm-data and --yadm-archive each execution.
|
|
|
|
Legacy paths detected:
|
|
${path_list}
|
|
***********
|
|
EOF
|
|
printf '%s\n' "$msg" >&2
|
|
LEGACY_WARNING_ISSUED=1
|
|
|
|
}
|
|
|
|
function configure_paths() {
|
|
|
|
# change paths to be relative to YADM_DIR
|
|
YADM_CONFIG="$YADM_DIR/$YADM_CONFIG"
|
|
YADM_ENCRYPT="$YADM_DIR/$YADM_ENCRYPT"
|
|
YADM_BOOTSTRAP="$YADM_DIR/$YADM_BOOTSTRAP"
|
|
YADM_HOOKS="$YADM_DIR/$YADM_HOOKS"
|
|
YADM_ALT="$YADM_DIR/$YADM_ALT"
|
|
|
|
# change paths to be relative to YADM_DATA
|
|
YADM_REPO="$YADM_DATA/$YADM_REPO"
|
|
YADM_ARCHIVE="$YADM_DATA/$YADM_ARCHIVE"
|
|
|
|
# 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
|
|
|
|
# obtain YADM_WORK from repo if it exists
|
|
if [ -d "$GIT_DIR" ]; then
|
|
local work
|
|
work=$(unix_path "$("$GIT_PROGRAM" config core.worktree)")
|
|
[ -n "$work" ] && YADM_WORK="$work"
|
|
fi
|
|
|
|
# YADM_BASE is used for manipulating the base worktree path for much of the
|
|
# alternate file processing
|
|
if [ "$YADM_WORK" == "/" ]; then
|
|
YADM_BASE=""
|
|
else
|
|
YADM_BASE="$YADM_WORK"
|
|
fi
|
|
|
|
}
|
|
|
|
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() {
|
|
|
|
if [[ "$(<$PROC_VERSION)" =~ [Mm]icrosoft ]]; then
|
|
OPERATING_SYSTEM="WSL"
|
|
else
|
|
OPERATING_SYSTEM=$(uname -s)
|
|
fi 2>/dev/null
|
|
|
|
case "$OPERATING_SYSTEM" in
|
|
CYGWIN* | MINGW* | MSYS*)
|
|
git_version="$("$GIT_PROGRAM" --version 2>/dev/null)"
|
|
if [[ "$git_version" =~ windows ]]; then
|
|
USE_CYGPATH=1
|
|
fi
|
|
OPERATING_SYSTEM=$(uname -o)
|
|
;;
|
|
*) ;;
|
|
esac
|
|
|
|
}
|
|
|
|
function set_awk() {
|
|
local pgm
|
|
for pgm in "${AWK_PROGRAM[@]}"; do
|
|
command -v "$pgm" &>/dev/null && AWK_PROGRAM=("$pgm") && return
|
|
done
|
|
}
|
|
|
|
function debug() {
|
|
|
|
[ -n "$DEBUG" ] && echo_e "DEBUG: $*"
|
|
|
|
}
|
|
|
|
function error_out() {
|
|
|
|
echo_e "ERROR: $*" >&2
|
|
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" ] ||
|
|
{ [[ $OPERATING_SYSTEM == MINGW* ]] && [ -f "$hook_command" ]; }; then
|
|
debug "Invoking hook: $hook_command"
|
|
|
|
# expose some internal data to all hooks
|
|
YADM_HOOK_COMMAND=$HOOK_COMMAND
|
|
YADM_HOOK_DIR=$YADM_DIR
|
|
YADM_HOOK_DATA=$YADM_DATA
|
|
YADM_HOOK_EXIT=$exit_status
|
|
YADM_HOOK_FULL_COMMAND=$FULL_COMMAND
|
|
YADM_HOOK_REPO=$YADM_REPO
|
|
YADM_HOOK_WORK=$YADM_WORK
|
|
|
|
# pack array to export it; filenames including a newline character (\n)
|
|
# are NOT supported
|
|
YADM_ENCRYPT_INCLUDE_FILES=$(join_string $'\n' "${ENCRYPT_INCLUDE_FILES[@]}")
|
|
|
|
export YADM_HOOK_COMMAND
|
|
export YADM_HOOK_DIR
|
|
export YADM_HOOK_DATA
|
|
export YADM_HOOK_EXIT
|
|
export YADM_HOOK_FULL_COMMAND
|
|
export YADM_HOOK_REPO
|
|
export YADM_HOOK_WORK
|
|
export YADM_ENCRYPT_INCLUDE_FILES
|
|
|
|
# export helper functions
|
|
export -f builtin_dirname
|
|
export -f relative_path
|
|
export -f unix_path
|
|
export -f mixed_path
|
|
|
|
"$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 private_dirs() {
|
|
fetch="$1"
|
|
pdirs=(.ssh)
|
|
if [ -z "${GNUPGHOME:-}" ]; then
|
|
pdirs+=(.gnupg)
|
|
else
|
|
pdirs+=("$(relative_path "$YADM_WORK" "$GNUPGHOME")")
|
|
fi
|
|
if [ "$fetch" = "all" ]; then
|
|
echo "${pdirs[@]}"
|
|
else
|
|
echo "${pdirs[1]}"
|
|
fi
|
|
}
|
|
|
|
function assert_private_dirs() {
|
|
for private_dir in "$@"; do
|
|
if [ ! -d "$YADM_WORK/$private_dir" ]; then
|
|
debug "Creating $YADM_WORK/$private_dir"
|
|
#shellcheck disable=SC2174
|
|
mkdir -m 0700 -p "$YADM_WORK/$private_dir" &>/dev/null
|
|
fi
|
|
done
|
|
}
|
|
|
|
function assert_parent() {
|
|
basedir=${1%/*}
|
|
if [ -n "$basedir" ]; then
|
|
[ -e "$basedir" ] || mkdir -p "$basedir"
|
|
fi
|
|
}
|
|
|
|
function display_private_perms() {
|
|
when="$1"
|
|
for private_dir in $(private_dirs all); 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() {
|
|
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=()
|
|
|
|
[ -f "$YADM_ENCRYPT" ] || return
|
|
|
|
cd_work "Parsing encrypt" || return
|
|
|
|
local -a exclude
|
|
local -a include
|
|
|
|
local pattern
|
|
while IFS='' read -r pattern || [ -n "$pattern" ]; do
|
|
if [ "${pattern:0:1}" = "!" ]; then
|
|
exclude+=("--exclude=/${pattern:1}")
|
|
elif ! [[ $pattern =~ ^[[:blank:]]*(#|$) ]]; then
|
|
include+=("$pattern")
|
|
fi
|
|
done <"$YADM_ENCRYPT"
|
|
|
|
if [ ${#include[@]} -gt 0 ]; then
|
|
while IFS='' read -r filename; do
|
|
if [ -n "$filename" ]; then
|
|
ENCRYPT_INCLUDE_FILES+=("${filename%/}")
|
|
fi
|
|
done <<<"$(
|
|
"$GIT_PROGRAM" --glob-pathspecs ls-files --others \
|
|
"${exclude[@]}" -- "${include[@]}" 2>/dev/null
|
|
)"
|
|
|
|
[ "$YADM_COMMAND" = "encrypt" ] || return
|
|
|
|
# List files that matches encryption pattern but is tracked
|
|
while IFS='' read -r filename; do
|
|
if [ -n "$filename" ]; then
|
|
NO_ENCRYPT_TRACKED_FILES+=("${filename%/}")
|
|
fi
|
|
done <<<"$(
|
|
"$GIT_PROGRAM" --glob-pathspecs ls-files \
|
|
"${exclude[@]}" -- "${include[@]}"
|
|
)"
|
|
fi
|
|
}
|
|
|
|
function builtin_dirname() {
|
|
# dirname is not builtin, and universally available, this is a built-in
|
|
# replacement using parameter expansion
|
|
local path="$1"
|
|
while [ "${path: -1}" = "/" ]; do
|
|
path="${path%/}"
|
|
done
|
|
|
|
local dir_name="${path%/*}"
|
|
while [ "${dir_name: -1}" = "/" ]; do
|
|
dir_name="${dir_name%/}"
|
|
done
|
|
|
|
if [ "$path" = "$dir_name" ]; then
|
|
dir_name="."
|
|
elif [ -z "$dir_name" ]; then
|
|
dir_name="/"
|
|
fi
|
|
echo "$dir_name"
|
|
}
|
|
|
|
function relative_path() {
|
|
# Output a path to $2/full, relative to $1/base
|
|
#
|
|
# This function created with ideas from
|
|
# https://stackoverflow.com/questions/2564634
|
|
local base="$1"
|
|
if [ "${base:0:1}" != "/" ]; then
|
|
base="$PWD/$base"
|
|
fi
|
|
|
|
local full="$2"
|
|
if [ "${full:0:1}" != "/" ]; then
|
|
full="$PWD/$full"
|
|
fi
|
|
|
|
local common_part="$base"
|
|
local result=""
|
|
|
|
while [ "$common_part" != "$full" ]; do
|
|
if [ "$common_part" = "/" ]; then
|
|
# No common part found. Append / if result is set to make the final
|
|
# result correct.
|
|
result="${result:+$result/}"
|
|
break
|
|
elif [ "${full#"$common_part"/}" != "$full" ]; then
|
|
common_part="$common_part/"
|
|
result="${result:+$result/}"
|
|
break
|
|
fi
|
|
# Move to parent directory and update result
|
|
common_part=$(builtin_dirname "$common_part")
|
|
result="..${result:+/$result}"
|
|
done
|
|
|
|
echo "$result${full#"$common_part"}"
|
|
}
|
|
|
|
# ****** 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
|
|
|
|
}
|
|
|
|
# ****** Helper Functions ******
|
|
|
|
function join_string {
|
|
local IFS="$1"
|
|
printf "%s" "${*:2}"
|
|
}
|
|
|
|
function in_list {
|
|
local element="$1"
|
|
shift
|
|
|
|
for e in "$@"; do
|
|
[[ "$e" = "$element" ]] && return 0
|
|
done
|
|
return 1
|
|
}
|
|
|
|
function get_mode {
|
|
local filename="$1"
|
|
local mode
|
|
|
|
# most *nixes
|
|
mode=$(stat -c '%a' "$filename" 2>/dev/null)
|
|
if [ -z "$mode" ]; then
|
|
# BSD-style
|
|
mode=$(stat -f '%p' "$filename" 2>/dev/null)
|
|
mode=${mode: -4}
|
|
fi
|
|
|
|
# only accept results if they are octal
|
|
if [[ ! $mode =~ ^[0-7]+$ ]]; then
|
|
return 1
|
|
fi
|
|
|
|
echo "$mode"
|
|
}
|
|
|
|
function copy_perms {
|
|
local source="$1"
|
|
local target="$2"
|
|
|
|
local mode
|
|
if ! mode=$(get_mode "$source") || ! chmod "$mode" "$target"; then
|
|
debug "Unable to copy perms '$mode' from '$source' to '$target'"
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
function mk_tmp_dir {
|
|
local tempdir="$YADM_DATA/tmp.$$.$RANDOM"
|
|
assert_parent "$tempdir/"
|
|
echo "$tempdir"
|
|
}
|
|
|
|
# ****** 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=""
|
|
|
|
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 ||
|
|
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=""
|
|
|
|
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 ||
|
|
error_out "This functionality requires GPG to be installed, but the command '$GPG_PROGRAM' cannot be located.$more_info"
|
|
}
|
|
function require_openssl() {
|
|
local alt_openssl
|
|
alt_openssl="$(config yadm.openssl-program)"
|
|
|
|
local more_info=""
|
|
|
|
if [ "$alt_openssl" != "" ]; then
|
|
OPENSSL_PROGRAM="$alt_openssl"
|
|
more_info="\nThis command has been set via the yadm.openssl-program configuration."
|
|
fi
|
|
command -v "$OPENSSL_PROGRAM" &>/dev/null ||
|
|
error_out "This functionality requires OpenSSL to be installed, but the command '$OPENSSL_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 require_git_crypt() {
|
|
command -v "$GIT_CRYPT_PROGRAM" &>/dev/null ||
|
|
error_out "This functionality requires git-crypt to be installed, but the command '$GIT_CRYPT_PROGRAM' cannot be located."
|
|
}
|
|
function require_transcrypt() {
|
|
command -v "$TRANSCRYPT_PROGRAM" &>/dev/null ||
|
|
error_out "This functionality requires transcrypt to be installed, but the command '$TRANSCRYPT_PROGRAM' cannot be located."
|
|
}
|
|
function bootstrap_available() {
|
|
[ -f "$YADM_BOOTSTRAP" ] && [ -x "$YADM_BOOTSTRAP" ] && return
|
|
return 1
|
|
}
|
|
function awk_available() {
|
|
command -v "${AWK_PROGRAM[0]}" &>/dev/null && return
|
|
return 1
|
|
}
|
|
function j2cli_available() {
|
|
command -v "$J2CLI_PROGRAM" &>/dev/null && return
|
|
return 1
|
|
}
|
|
function envtpl_available() {
|
|
command -v "$ENVTPL_PROGRAM" &>/dev/null && return
|
|
return 1
|
|
}
|
|
function esh_available() {
|
|
command -v "$ESH_PROGRAM" &>/dev/null && return
|
|
return 1
|
|
}
|
|
|
|
# ****** Directory translations ******
|
|
|
|
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_awk
|
|
set_yadm_dirs
|
|
configure_paths
|
|
main "${MAIN_ARGS[@]}"
|
|
fi
|