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.
436 lines
10 KiB
436 lines
10 KiB
#!/bin/bash |
|
|
|
# yadm - Yet Another Dotfiles Manager |
|
# Copyright (C) 2015 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, version 3 of the License. |
|
|
|
# This program is distributed in the hope that it will be useful, |
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
# GNU General Public License for more details. |
|
|
|
# You should have received a copy of the GNU General Public License |
|
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
|
|
VERSION=1.00 |
|
|
|
YADM_WORK="$HOME" |
|
YADM_DIR="$HOME/.yadm" |
|
|
|
YADM_REPO="$YADM_DIR/repo.git" |
|
YADM_CONFIG="$YADM_DIR/config" |
|
YADM_ENCRYPT="$YADM_DIR/encrypt" |
|
YADM_ARCHIVE="$YADM_DIR/files.gpg" |
|
|
|
#; flag when something may have changes (which prompts auto actions to be performed) |
|
CHANGES_POSSIBLE=0 |
|
|
|
#; use the YADM repo for all git operations |
|
export GIT_DIR="$YADM_REPO" |
|
|
|
function main() { |
|
|
|
require_git |
|
|
|
#; create the YADM_DIR if it doesn't exist yet |
|
[ -d "$YADM_DIR" ] || mkdir -p $YADM_DIR |
|
|
|
#; parse command line arguments |
|
internal_commands="^(alt|clean|clone|config|decrypt|encrypt|help|init|list|perms|version)$" |
|
if [ -z "$*" ] ; then |
|
#; no argumnts will result in help() |
|
help |
|
elif [ "$1" == "gitconfig" ] ; then |
|
#; 'config' is used for yadm, need to use 'gitcofnig' to pass through to git |
|
shift |
|
git_command config "$@" |
|
elif [[ "$1" =~ $internal_commands ]] ; then |
|
#; for internal commands, process all of the arguments |
|
YADM_COMMAND="$1" |
|
YADM_ARGS="" |
|
shift |
|
|
|
while [[ $# > 0 ]] ; do |
|
key="$1" |
|
case $key in |
|
-a|--all) #; used by list() |
|
LIST_ALL="YES" |
|
;; |
|
-d|--debug) #; used by all commands |
|
DEBUG="YES" |
|
;; |
|
-f|--force) #; used by init() and clone() |
|
FORCE="YES" |
|
;; |
|
-w|--work-tree) #; 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 |
|
if [ -z "$YADM_ARGS" ] ; then |
|
YADM_ARGS="$1" |
|
else |
|
YADM_ARGS+=" $1" |
|
fi |
|
;; |
|
esac |
|
shift |
|
done |
|
[ ! -d $YADM_WORK ] && error_out "Work tree does not exist: [$YADM_WORK]" |
|
$YADM_COMMAND "$YADM_ARGS" |
|
else |
|
#; any other commands are simply passed through to git |
|
git_command "$@" |
|
fi |
|
|
|
#; process automatic events |
|
auto_alt |
|
auto_perms |
|
|
|
} |
|
|
|
#; ****** YADM Commands ****** |
|
|
|
function alt() { |
|
|
|
require_repo |
|
|
|
#; regex for matching "<file>##SYSTEM.HOSTNAME" |
|
match_system=$(uname -s) |
|
match_host=$(hostname) |
|
match="^(.+)##($match_system|$match_system.$match_host)$" |
|
|
|
#; process relative to YADM_WORK |
|
YADM_WORK=$(git config core.worktree) |
|
cd $YADM_WORK |
|
|
|
#; only be noisy if the "alt" command was run directly |
|
[ "$YADM_COMMAND" == "alt" ] && LOUD="YES" |
|
|
|
#; loop over all "tracked" files |
|
#; for every file which matches the above regex, create a symlink |
|
for tracked_file in $(git ls-files | sort); do |
|
tracked_file="$YADM_WORK/$tracked_file" |
|
if [[ $tracked_file =~ $match ]] ; then |
|
new_link="${BASH_REMATCH[1]}" |
|
debug "Linking $tracked_file to $new_link" |
|
[ -n "$LOUD" ] && echo "Linking $tracked_file to $new_link" |
|
ln -fs "$tracked_file" "$new_link" |
|
fi |
|
done |
|
|
|
} |
|
|
|
function clean() { |
|
|
|
error_out "\"git clean\" has been disabled for safety. You could end up removing all unmanaged files." |
|
|
|
} |
|
|
|
function clone() { |
|
|
|
#; clone will begin with a bare repo |
|
init |
|
|
|
#; add the specified remote, and configure the repo to track origin/master |
|
debug "Adding remote to new repo" |
|
git remote add origin "$1" |
|
debug "Configuring new repo to track origin/master" |
|
git config branch.master.remote origin |
|
git config branch.master.merge refs/heads/master |
|
|
|
#; fetch / merge (and possibly fallback to reset) |
|
debug "Doing an initial fetch of the origin" |
|
git fetch origin |
|
git merge origin/master || { |
|
git reset origin/master |
|
echo <<EOF |
|
**NOTE** |
|
Merging origin/master failed. |
|
YADM did 'reset origin/master' instead. |
|
|
|
This likely happened because you had files in your |
|
work-tree, which conflict files tracked by origin/master |
|
|
|
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 |
|
} |
|
|
|
CHANGES_POSSIBLE=1 |
|
|
|
} |
|
|
|
function config() { |
|
|
|
#; ensure we have a file, even if empty |
|
[ -f "$YADM_CONFIG" ] || touch "$YADM_CONFIG" |
|
|
|
if [ -z "$@" ] ; then |
|
#; with no parameters, provide some helpful documentation |
|
echo TODO: Print help about available YADM configurations |
|
else |
|
#; operate on the YADM configuration file |
|
git config --file="$YADM_CONFIG" $@ |
|
fi |
|
|
|
} |
|
|
|
function decrypt() { |
|
|
|
require_gpg |
|
require_archive |
|
|
|
YADM_WORK=$(git config core.worktree) |
|
|
|
#; decrypt the archive |
|
(gpg -d "$YADM_ARCHIVE" || echo 1) | tar xv -C "$YADM_WORK" |
|
if [ $? = 0 ] ; then |
|
echo "All files decrypted." |
|
else |
|
error_out "Unable to extract encrypted files." |
|
fi |
|
|
|
CHANGES_POSSIBLE=1 |
|
|
|
} |
|
|
|
function encrypt() { |
|
|
|
require_gpg |
|
require_encrypt |
|
|
|
#; process relative to YADM_WORK |
|
YADM_WORK=$(git config core.worktree) |
|
cd $YADM_WORK |
|
|
|
#; build a list of globs from YADM_ENCRYPT |
|
GLOBS=() |
|
while IFS='' read -r glob || [ -n "$glob" ]; do |
|
if [[ ! $glob =~ ^# ]] ; then |
|
GLOBS=("${GLOBS[@]}" $(eval /bin/ls "$glob" 2>/dev/null)) |
|
fi |
|
done < "$YADM_ENCRYPT" |
|
|
|
#; encrypt all files which match the globs |
|
tar -cv ${GLOBS[@]} | gpg --yes -c --output "$YADM_ARCHIVE" |
|
if [ $? = 0 ]; then |
|
echo "Wrote new file: $YADM_ARCHIVE" |
|
else |
|
error_out "Unable to write $YADM_ARCHIVE" |
|
fi |
|
|
|
CHANGES_POSSIBLE=1 |
|
|
|
} |
|
|
|
function git_command() { |
|
|
|
require_repo |
|
|
|
#; pass commands through to git |
|
git "$@" |
|
|
|
CHANGES_POSSIBLE=1 |
|
|
|
} |
|
|
|
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: |
|
init [-f] - Initialize an empty repository |
|
clone [-f] GIT_URL - Clone an existing repository |
|
config [name] [value] - Configure a setting |
|
list [-a] - List tracked files |
|
alt - Create links for alternates |
|
encrypt - Encrypt files |
|
decrypt - Decrypt files |
|
perms - Fix perms for private files |
|
|
|
Paths: |
|
\$HOME/.yadm/config - YADM's configuration file |
|
\$HOME/.yadm/repo.git - YADM's Git repository |
|
\$HOME/.yadm/encrypt - List of globs used for encrypt/decrypt |
|
|
|
Use "man yadm" for complete documentation. |
|
EOF |
|
|
|
exit 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 exist. [$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 init --shared=0600 --bare "$YADM_REPO" |
|
configure_repo |
|
|
|
CHANGES_POSSIBLE=1 |
|
|
|
} |
|
|
|
function list() { |
|
|
|
require_repo |
|
|
|
#; process relative to YADM_WORK when --all is specified |
|
if [ -n "$LIST_ALL" ] ; then |
|
YADM_WORK=$(git config core.worktree) |
|
cd $YADM_WORK |
|
fi |
|
|
|
#; list tracked files |
|
git ls-files |
|
|
|
} |
|
|
|
function perms() { |
|
|
|
#; TODO: prevent repeats in the files changed |
|
|
|
#; process relative to YADM_WORK |
|
YADM_WORK=$(git config core.worktree) |
|
cd $YADM_WORK |
|
|
|
GLOBS=() |
|
|
|
#; include the archive created by "encrypt" |
|
[ -f "$YADM_ARCHIVE" ] && GLOBS=("${GLOBS[@]}" "$YADM_ARCHIVE") |
|
|
|
#; include all .ssh files (unless disabled) |
|
if [[ $(config yadm.ssh-perms) != "false" ]] ; then |
|
GLOBS=("${GLOBS[@]}" $(eval /bin/ls ".ssh/*" 2>/dev/null)) |
|
fi |
|
|
|
#; include globs found in YADM_ENCRYPT (if present) |
|
if [ -f "$YADM_ENCRYPT" ] ; then |
|
while IFS='' read -r glob || [ -n "$glob" ]; do |
|
if [[ ! $glob =~ ^# ]] ; then |
|
GLOBS=("${GLOBS[@]}" $(eval /bin/ls "$glob" 2>/dev/null)) |
|
fi |
|
done < "$YADM_ENCRYPT" |
|
fi |
|
|
|
#; remove group/other permissions from collected globs |
|
perms_modified=$(chmod -v go-rwx ${GLOBS[@]}) |
|
|
|
#; report any changed permissions |
|
if [ -n "$perms_modified" ] ; then |
|
echo "Updated permissions:" |
|
ls -l $perms_modified | sort | uniq |
|
fi |
|
|
|
} |
|
|
|
function version() { |
|
|
|
echo "yadm $VERSION" |
|
exit 0 |
|
|
|
} |
|
|
|
#; ****** Utility Functions ****** |
|
|
|
function configure_repo() { |
|
|
|
debug "Configuring new repo" |
|
|
|
#; change bare to false (there is a working directory) |
|
git config core.bare 'false' |
|
|
|
#; set the worktree for the YADM repo |
|
git config core.worktree "$YADM_WORK" |
|
|
|
#; possibly used later to ensure we're working on the YADM repo |
|
git config yadm.managed 'true' |
|
|
|
} |
|
|
|
function debug() { |
|
|
|
[ -n "$DEBUG" ] && echo -e "DEBUG: $@" |
|
|
|
} |
|
|
|
function error_out() { |
|
|
|
echo -e "ERROR: $@" |
|
exit 1 |
|
|
|
} |
|
|
|
#; ****** Auto Functions ****** |
|
|
|
function auto_alt() { |
|
|
|
#; process alternates if there are possible changes |
|
if [ "$CHANGES_POSSIBLE" == "1" ] ; then |
|
auto_alt=$(config yadm.auto-alt) |
|
if [ "$auto_alt" != "false" ] ; then |
|
alt |
|
fi |
|
fi |
|
|
|
} |
|
|
|
function auto_perms() { |
|
|
|
#; process permissions if there are possible changes |
|
if [ "$CHANGES_POSSIBLE" == "1" ] ; then |
|
auto_perms=$(config yadm.auto-perms) |
|
if [ "$auto_perms" != "false" ] ; then |
|
perms |
|
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() { |
|
command -v git >/dev/null 2>&1 || \ |
|
error_out "This functionality requires Git to be installed, but the command git cannot be located." |
|
} |
|
function require_gpg() { |
|
command -v gpg >/dev/null 2>&1 || \ |
|
error_out "This functionality requires GPG to be installed, but the command gpg cannot be located." |
|
} |
|
function require_repo() { |
|
[ -d "$YADM_REPO" ] || error_out "Git repo does not exist. did you forget to run 'init' or 'clone'?" |
|
} |
|
|
|
main "$@"
|
|
|