1
0
Fork 0
mirror of synced 2024-06-03 07:41:09 -04:00

Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Chad Homan 2018-01-29 16:28:41 -06:00
commit 130965e34d
22 changed files with 1067 additions and 257 deletions

10
CHANGES
View file

@ -1,3 +1,13 @@
1.12.0
* Add basic Zsh completion (#71, #79)
* Support directories in `.yadm/encrypt` (#81, #82)
* Support exclusions in `.yadm/encrypt` (#86)
* Improve portability with printf (#87)
* Eliminate usage of `eval` and `ls`
1.11.1
* Create private dirs prior to merge (#74)
1.11.0 1.11.0
* Option for Cygwin to copy files instead of symlink (#62) * Option for Cygwin to copy files instead of symlink (#62)
* Support `YADM_DISTRO` in Jinja templates (#68) * Support `YADM_DISTRO` in Jinja templates (#68)

View file

@ -3,13 +3,15 @@ CONTRIBUTORS
Tim Byrne Tim Byrne
Espen Henriksen Espen Henriksen
Cameron Eagans Cameron Eagans
Klas Mellbourn
Jan Schulz Jan Schulz
Patrick Hof
Satoshi Ohki
Siôn Le Roux Siôn Le Roux
Sébastien Gross Sébastien Gross
Thomas Luzat
Tomas Cernaj Tomas Cernaj
Uroš Golja Uroš Golja
japm48
Franciszek Madej Franciszek Madej
Klas Mellbourn
Paraplegic Racehorse Paraplegic Racehorse
Patrick Hof
Satoshi Ohki

View file

@ -1,12 +1,15 @@
.PHONY: all
all: yadm.md contrib all: yadm.md contrib
yadm.md: yadm.1 yadm.md: yadm.1
@groff -man -Tascii ./yadm.1 | col -bx | sed 's/^[A-Z]/## &/g' | sed '/yadm(1)/d' > yadm.md @groff -man -Tascii ./yadm.1 | col -bx | sed 's/^[A-Z]/## &/g' | sed '/yadm(1)/d' > yadm.md
.PHONY: contrib
contrib: contrib:
@echo "CONTRIBUTORS\n" > CONTRIBUTORS @echo "CONTRIBUTORS\n" > CONTRIBUTORS
@git shortlog -ns master gh-pages dev dev-pages | cut -f2 >> CONTRIBUTORS @git shortlog -ns master gh-pages dev dev-pages | cut -f2 >> CONTRIBUTORS
.PHONY: pdf
pdf: pdf:
@groff -man -Tps ./yadm.1 > yadm.ps @groff -man -Tps ./yadm.1 > yadm.ps
@open yadm.ps @open yadm.ps
@ -39,8 +42,19 @@ shellcheck:
[ "$$test_result" -ne 0 ] && exit 1; \ [ "$$test_result" -ne 0 ] && exit 1; \
done; true done; true
.PHONY: testhost
testhost:
@target=HEAD
@rm -rf /tmp/testhost
@git show $(target):yadm > /tmp/testhost
@chmod a+x /tmp/testhost
@echo Starting testhost target=\"$$target\"
@docker run -w /root --hostname testhost --rm -it -v "/tmp/testhost:/bin/yadm:ro" yadm/testbed:latest bash
.PHONY: man
man: man:
groff -man -Tascii ./yadm.1 | less groff -man -Tascii ./yadm.1 | less
.PHONY: wide
wide: wide:
man ./yadm.1 man ./yadm.1

View file

@ -1,19 +1,36 @@
# Prerequisites
**yadm** completion only works if Git completions are also enabled.
# Installation # Installation
## Homebrew ## Bash completions
### Prerequisites
**yadm** completion only works if Git completions are also enabled.
### Homebrew
If using `homebrew` to install **yadm**, completions should automatically be handled if you also install `brew install bash-completion`. This might require you to include the main completion script in your own bashrc file like this: If using `homebrew` to install **yadm**, completions should automatically be handled if you also install `brew install bash-completion`. This might require you to include the main completion script in your own bashrc file like this:
``` ```
[ -f /usr/local/etc/bash_completion ] && source /usr/local/etc/bash_completion [ -f /usr/local/etc/bash_completion ] && source /usr/local/etc/bash_completion
``` ```
## Manual installation ### Manual installation
Copy the completion script locally, and add this to you bashrc: Copy the completion script locally, and add this to you bashrc:
``` ```
[ -f /full/path/to/yadm.bash_completion ] && source /full/path/to/yadm.bash_completion [ -f /full/path/to/yadm.bash_completion ] && source /full/path/to/yadm.bash_completion
``` ```
## Zsh completions
### Homebrew
If using `homebrew` to install **yadm**, completions should handled automatically.
### Manual installation
Copy the completion script `yadm.zsh_completion` locally, rename it to `_yadm`, and add the containing folder to `$fpath` in `.zshrc`:
```
fpath=(/path/to/folder/containing_yadm $fpath)
autoload -U compinit
compinit
```
### Installation using [zplug](https://github.com/b4b4r07/zplug)
Load `_yadm` as a plugin in your `.zshrc`:
```
fpath=("$ZPLUG_HOME/bin" $fpath)
zplug "TheLocehiliosan/yadm", rename-to:_yadm, use:"completion/yadm.zsh_completion", as:command, defer:2
```

View file

@ -0,0 +1,46 @@
#compdef yadm
_yadm(){
local -a _1st_arguments
_1st_arguments=(
'help:Display yadm command help'
'init:Initialize an empty repository'
'config:Configure a setting'
'list:List tracked files'
'alt:Create links for alternates'
'bootstrap:Execute $HOME/.yadm/bootstrap'
'encrypt:Encrypt files'
'decrypt:Decrypt files'
'perms:Fix perms for private files'
'add:git add'
'push:git push'
'pull:git pull'
'diff:git diff'
'checkout:git checkout'
'co:git co'
'commit:git commit'
'ci:git ci'
'status:git status'
'st:git st'
'reset:git reset'
'log:git log'
)
local context state line expl
local -A opt_args
_arguments '*:: :->subcmds' && return 0
if (( CURRENT == 1 )); then
_describe -t commands "yadm commands" _1st_arguments -V1
return
fi
case "$words[1]" in
*)
_arguments ':filenames:_files'
;;
esac
}
_yadm "$@"

View file

@ -1,66 +0,0 @@
load common
load_fixtures
@test "Default /bin/ls" {
echo "
By default, the value of LS_PROGRAM should be /bin/ls
"
# shellcheck source=/dev/null
YADM_TEST=1 source "$T_YADM"
status=0
output=$( require_ls; echo "$LS_PROGRAM" ) || {
status=$?
true
}
echo "output=$output"
[ "$status" == 0 ]
[ "$output" = "/bin/ls" ]
}
@test "Fallback on 'ls'" {
echo "
When LS_PROGRAM doesn't exist, use 'ls'
"
# shellcheck source=/dev/null
YADM_TEST=1 source "$T_YADM"
status=0
LS_PROGRAM="/ls/missing"
output=$( require_ls; echo "$LS_PROGRAM" ) || {
status=$?
true
}
echo "output=$output"
[ "$status" == 0 ]
[ "$output" = "ls" ]
}
@test "Fail if ls isn't in PATH" {
echo "
When LS_PROGRAM doesn't exist, use 'ls'
"
# shellcheck source=/dev/null
YADM_TEST=1 source "$T_YADM"
status=0
LS_PROGRAM="/ls/missing"
savepath="$PATH"
# shellcheck disable=SC2123
PATH=
output=$( require_ls 2>&1; echo "$LS_PROGRAM" ) || {
status=$?
true
}
PATH="$savepath"
echo "output=$output"
[ "$status" != 0 ]
[[ "$output" =~ functionality\ requires\ .ls.\ to\ be\ installed ]]
}

View file

@ -0,0 +1,318 @@
load common
load_fixtures
setup() {
# SC2153 is intentional
# shellcheck disable=SC2153
make_parents "$T_YADM_ENCRYPT"
make_parents "$T_DIR_WORK"
make_parents "$T_DIR_REPO"
mkdir "$T_DIR_WORK"
git init --shared=0600 --bare "$T_DIR_REPO" >/dev/null 2>&1
GIT_DIR="$T_DIR_REPO" git config core.bare 'false'
GIT_DIR="$T_DIR_REPO" git config core.worktree "$T_DIR_WORK"
GIT_DIR="$T_DIR_REPO" git config yadm.managed 'true'
}
teardown() {
destroy_tmp
}
function run_parse() {
# shellcheck source=/dev/null
YADM_TEST=1 source "$T_YADM"
YADM_ENCRYPT="$T_YADM_ENCRYPT"
export YADM_ENCRYPT
GIT_DIR="$T_DIR_REPO"
export GIT_DIR
# shellcheck disable=SC2034
status=0
{ output=$( parse_encrypt) && parse_encrypt; } || {
status=$?
true
}
if [ "$1" == "twice" ]; then
GIT_DIR="$T_DIR_REPO" parse_encrypt
fi
echo -e "OUTPUT:$output\n"
echo "ENCRYPT_INCLUDE_FILES:"
echo " Size: ${#ENCRYPT_INCLUDE_FILES[@]}"
echo " Items: ${ENCRYPT_INCLUDE_FILES[*]}"
echo "EXPECT_INCLUDE:"
echo " Size: ${#EXPECT_INCLUDE[@]}"
echo " Items: ${EXPECT_INCLUDE[*]}"
}
@test "parse_encrypt (not called)" {
echo "
parse_encrypt() is not called
Array should be 'unparsed'
"
# shellcheck source=/dev/null
YADM_TEST=1 source "$T_YADM"
echo "ENCRYPT_INCLUDE_FILES=$ENCRYPT_INCLUDE_FILES"
[ "$ENCRYPT_INCLUDE_FILES" == "unparsed" ]
}
@test "parse_encrypt (short-circuit)" {
echo "
Parsing should not happen more than once
"
run_parse "twice"
echo "PARSE_ENCRYPT_SHORT: $PARSE_ENCRYPT_SHORT"
[ "$status" == 0 ]
[ "$output" == "" ]
[[ "$PARSE_ENCRYPT_SHORT" =~ not\ reprocessed ]]
}
@test "parse_encrypt (file missing)" {
echo "
.yadm/encrypt is empty
Array should be empty
"
EXPECT_INCLUDE=()
run_parse
[ "$status" == 0 ]
[ "$output" == "" ]
[ "${#ENCRYPT_INCLUDE_FILES[@]}" -eq "${#EXPECT_INCLUDE[@]}" ]
[ "${ENCRYPT_INCLUDE_FILES[*]}" == "${EXPECT_INCLUDE[*]}" ]
}
@test "parse_encrypt (empty file)" {
echo "
.yadm/encrypt is empty
Array should be empty
"
touch "$T_YADM_ENCRYPT"
EXPECT_INCLUDE=()
run_parse
[ "$status" == 0 ]
[ "$output" == "" ]
[ "${#ENCRYPT_INCLUDE_FILES[@]}" -eq "${#EXPECT_INCLUDE[@]}" ]
[ "${ENCRYPT_INCLUDE_FILES[*]}" == "${EXPECT_INCLUDE[*]}" ]
}
@test "parse_encrypt (files)" {
echo "
.yadm/encrypt is references present and missing files
Array should be as expected
"
echo "file1" > "$T_DIR_WORK/file1"
echo "file3" > "$T_DIR_WORK/file3"
echo "file5" > "$T_DIR_WORK/file5"
{ echo "file1"
echo "file2"
echo "file3"
echo "file4"
echo "file5"
} > "$T_YADM_ENCRYPT"
EXPECT_INCLUDE=("file1" "file3" "file5")
run_parse
[ "$status" == 0 ]
[ "$output" == "" ]
[ "${#ENCRYPT_INCLUDE_FILES[@]}" -eq "${#EXPECT_INCLUDE[@]}" ]
[ "${ENCRYPT_INCLUDE_FILES[*]}" == "${EXPECT_INCLUDE[*]}" ]
}
@test "parse_encrypt (files and dirs)" {
echo "
.yadm/encrypt is references present and missing files
.yadm/encrypt is references present and missing dirs
Array should be as expected
"
mkdir -p "$T_DIR_WORK/dir1"
mkdir -p "$T_DIR_WORK/dir2"
echo "file1" > "$T_DIR_WORK/file1"
echo "file2" > "$T_DIR_WORK/file2"
echo "a" > "$T_DIR_WORK/dir1/a"
echo "b" > "$T_DIR_WORK/dir1/b"
{ echo "file1"
echo "file2"
echo "file3"
echo "dir1"
echo "dir2"
echo "dir3"
} > "$T_YADM_ENCRYPT"
EXPECT_INCLUDE=("file1" "file2" "dir1" "dir2")
run_parse
[ "$status" == 0 ]
[ "$output" == "" ]
[ "${#ENCRYPT_INCLUDE_FILES[@]}" -eq "${#EXPECT_INCLUDE[@]}" ]
[ "${ENCRYPT_INCLUDE_FILES[*]}" == "${EXPECT_INCLUDE[*]}" ]
}
@test "parse_encrypt (comments/empty lines)" {
echo "
.yadm/encrypt is references present and missing files
.yadm/encrypt is references present and missing dirs
.yadm/encrypt contains comments / blank lines
Array should be as expected
"
mkdir -p "$T_DIR_WORK/dir1"
mkdir -p "$T_DIR_WORK/dir2"
echo "file1" > "$T_DIR_WORK/file1"
echo "file2" > "$T_DIR_WORK/file2"
echo "file3" > "$T_DIR_WORK/file3"
echo "a" > "$T_DIR_WORK/dir1/a"
echo "b" > "$T_DIR_WORK/dir1/b"
{ echo "file1"
echo "file2"
echo "#file3"
echo " #file3"
echo ""
echo "dir1"
echo "dir2"
echo "dir3"
} > "$T_YADM_ENCRYPT"
EXPECT_INCLUDE=("file1" "file2" "dir1" "dir2")
run_parse
[ "$status" == 0 ]
[ "$output" == "" ]
[ "${#ENCRYPT_INCLUDE_FILES[@]}" -eq "${#EXPECT_INCLUDE[@]}" ]
[ "${ENCRYPT_INCLUDE_FILES[*]}" == "${EXPECT_INCLUDE[*]}" ]
}
@test "parse_encrypt (w/spaces)" {
echo "
.yadm/encrypt is references present and missing files
.yadm/encrypt is references present and missing dirs
.yadm/encrypt references contain spaces
Array should be as expected
"
mkdir -p "$T_DIR_WORK/di r1"
mkdir -p "$T_DIR_WORK/dir2"
echo "file1" > "$T_DIR_WORK/file1"
echo "fi le2" > "$T_DIR_WORK/fi le2"
echo "file3" > "$T_DIR_WORK/file3"
echo "a" > "$T_DIR_WORK/di r1/a"
echo "b" > "$T_DIR_WORK/di r1/b"
{ echo "file1"
echo "fi le2"
echo "#file3"
echo " #file3"
echo ""
echo "di r1"
echo "dir2"
echo "dir3"
} > "$T_YADM_ENCRYPT"
EXPECT_INCLUDE=("file1" "fi le2" "di r1" "dir2")
run_parse
[ "$status" == 0 ]
[ "$output" == "" ]
[ "${#ENCRYPT_INCLUDE_FILES[@]}" -eq "${#EXPECT_INCLUDE[@]}" ]
[ "${ENCRYPT_INCLUDE_FILES[*]}" == "${EXPECT_INCLUDE[*]}" ]
}
@test "parse_encrypt (wildcards)" {
echo "
.yadm/encrypt contains wildcards
Array should be as expected
"
mkdir -p "$T_DIR_WORK/di r1"
mkdir -p "$T_DIR_WORK/dir2"
echo "file1" > "$T_DIR_WORK/file1"
echo "fi le2" > "$T_DIR_WORK/fi le2"
echo "file2" > "$T_DIR_WORK/file2"
echo "file3" > "$T_DIR_WORK/file3"
echo "a" > "$T_DIR_WORK/di r1/a"
echo "b" > "$T_DIR_WORK/di r1/b"
{ echo "fi*"
echo "#file3"
echo " #file3"
echo ""
echo "#dir2"
echo "di r1"
echo "dir2"
echo "dir3"
} > "$T_YADM_ENCRYPT"
EXPECT_INCLUDE=("fi le2" "file1" "file2" "file3" "di r1" "dir2")
run_parse
[ "$status" == 0 ]
[ "$output" == "" ]
[ "${#ENCRYPT_INCLUDE_FILES[@]}" -eq "${#EXPECT_INCLUDE[@]}" ]
[ "${ENCRYPT_INCLUDE_FILES[*]}" == "${EXPECT_INCLUDE[*]}" ]
}
@test "parse_encrypt (excludes)" {
echo "
.yadm/encrypt contains exclusions
Array should be as expected
"
mkdir -p "$T_DIR_WORK/di r1"
mkdir -p "$T_DIR_WORK/dir2"
mkdir -p "$T_DIR_WORK/dir3"
echo "file1" > "$T_DIR_WORK/file1"
echo "file1.ex" > "$T_DIR_WORK/file1.ex"
echo "fi le2" > "$T_DIR_WORK/fi le2"
echo "file3" > "$T_DIR_WORK/file3"
echo "test" > "$T_DIR_WORK/test"
echo "a.txt" > "$T_DIR_WORK/di r1/a.txt"
echo "b.txt" > "$T_DIR_WORK/di r1/b.txt"
echo "c.inc" > "$T_DIR_WORK/di r1/c.inc"
{ echo "fi*"
echo "#file3"
echo " #file3"
echo ""
echo " #test"
echo "#dir2"
echo "di r1/*"
echo "dir2"
echo "dir3"
echo "dir4"
echo "!*.ex"
echo "!di r1/*.txt"
} > "$T_YADM_ENCRYPT"
EXPECT_INCLUDE=("fi le2" "file1" "file3" "di r1/c.inc" "dir2" "dir3")
run_parse
[ "$status" == 0 ]
[ "$output" == "" ]
[ "${#ENCRYPT_INCLUDE_FILES[@]}" -eq "${#EXPECT_INCLUDE[@]}" ]
[ "${ENCRYPT_INCLUDE_FILES[*]}" == "${EXPECT_INCLUDE[*]}" ]
}

View file

@ -440,3 +440,140 @@ EOF
remote_output=$(GIT_DIR="$T_DIR_REPO" git remote show) remote_output=$(GIT_DIR="$T_DIR_REPO" git remote show)
[ "$remote_output" = "origin" ] [ "$remote_output" = "origin" ]
} }
@test "Command 'clone' (local insecure .ssh and .gnupg data, no related data in repo)" {
echo "
Local .ssh/.gnupg data exists and is insecure
but yadm repo contains no .ssh/.gnupg data
local insecure data should remain accessible
(yadm is hands-off)
"
#; setup scenario
rm -rf "$T_DIR_WORK" "$T_DIR_REPO"
mkdir -p "$T_DIR_WORK/.ssh"
mkdir -p "$T_DIR_WORK/.gnupg"
touch "$T_DIR_WORK/.ssh/testfile"
touch "$T_DIR_WORK/.gnupg/testfile"
find "$T_DIR_WORK" -exec chmod a+rw '{}' ';'
#; run clone (with debug on)
run "${T_YADM_Y[@]}" clone -d -w "$T_DIR_WORK" "$REMOTE_URL"
#; validate status and output
[ "$status" -eq 0 ]
[[ "$output" =~ Initialized ]]
[[ "$output" =~ initial\ private\ dir\ perms\ drwxrwxrwx.+\.ssh ]]
[[ "$output" =~ initial\ private\ dir\ perms\ drwxrwxrwx.+\.gnupg ]]
[[ "$output" =~ pre-merge\ private\ dir\ perms\ drwxrwxrwx.+\.ssh ]]
[[ "$output" =~ pre-merge\ private\ dir\ perms\ drwxrwxrwx.+\.gnupg ]]
[[ "$output" =~ post-merge\ private\ dir\ perms\ drwxrwxrwx.+\.ssh ]]
[[ "$output" =~ post-merge\ private\ dir\ perms\ drwxrwxrwx.+\.gnupg ]]
# standard perms still apply afterwards unless disabled with auto.perms
test_perms "$T_DIR_WORK/.gnupg" "drwx------"
test_perms "$T_DIR_WORK/.ssh" "drwx------"
}
@test "Command 'clone' (local insecure .gnupg data, related data in repo)" {
echo "
Local .gnupg data exists and is insecure
and yadm repo contains .gnupg data
.gnupg dir should be secured post merge
"
#; setup scenario
IN_REPO=(.bash_profile .vimrc .gnupg/gpg.conf)
setup
rm -rf "$T_DIR_WORK" "$T_DIR_REPO"
mkdir -p "$T_DIR_WORK/.gnupg"
touch "$T_DIR_WORK/.gnupg/testfile"
find "$T_DIR_WORK" -exec chmod a+rw '{}' ';'
#; run clone (with debug on)
run "${T_YADM_Y[@]}" clone -d -w "$T_DIR_WORK" "$REMOTE_URL"
#; validate status and output
[ "$status" -eq 0 ]
[[ "$output" =~ Initialized ]]
[[ "$output" =~ initial\ private\ dir\ perms\ drwxrwxrwx.+\.gnupg ]]
[[ "$output" =~ pre-merge\ private\ dir\ perms\ drwxrwxrwx.+\.gnupg ]]
[[ "$output" =~ post-merge\ private\ dir\ perms\ drwxrwxrwx.+\.gnupg ]]
test_perms "$T_DIR_WORK/.gnupg" "drwx------"
}
@test "Command 'clone' (local insecure .ssh data, related data in repo)" {
echo "
Local .ssh data exists and is insecure
and yadm repo contains .ssh data
.ssh dir should be secured post merge
"
#; setup scenario
IN_REPO=(.bash_profile .vimrc .ssh/config)
setup
rm -rf "$T_DIR_WORK" "$T_DIR_REPO"
mkdir -p "$T_DIR_WORK/.ssh"
touch "$T_DIR_WORK/.ssh/testfile"
find "$T_DIR_WORK" -exec chmod a+rw '{}' ';'
#; run clone (with debug on)
run "${T_YADM_Y[@]}" clone -d -w "$T_DIR_WORK" "$REMOTE_URL"
#; validate status and output
[ "$status" -eq 0 ]
[[ "$output" =~ Initialized ]]
[[ "$output" =~ initial\ private\ dir\ perms\ drwxrwxrwx.+\.ssh ]]
[[ "$output" =~ pre-merge\ private\ dir\ perms\ drwxrwxrwx.+\.ssh ]]
[[ "$output" =~ post-merge\ private\ dir\ perms\ drwxrwxrwx.+\.ssh ]]
test_perms "$T_DIR_WORK/.ssh" "drwx------"
}
@test "Command 'clone' (no existing .gnupg, .gnupg data tracked in repo)" {
echo "
Local .gnupg does not exist
and yadm repo contains .gnupg data
.gnupg dir should be created and secured prior to merge
tracked .gnupg data should be user accessible only
"
#; setup scenario
IN_REPO=(.bash_profile .vimrc .gnupg/gpg.conf)
setup
rm -rf "$T_DIR_WORK"
mkdir -p "$T_DIR_WORK"
rm -rf "$T_DIR_REPO"
#; run clone (with debug on)
run "${T_YADM_Y[@]}" clone -d -w "$T_DIR_WORK" "$REMOTE_URL"
#; validate status and output
[ "$status" -eq 0 ]
[[ "$output" =~ Initialized ]]
[[ ! "$output" =~ initial\ private\ dir\ perms ]]
[[ "$output" =~ pre-merge\ private\ dir\ perms\ drwx------.+\.gnupg ]]
[[ "$output" =~ post-merge\ private\ dir\ perms\ drwx------.+\.gnupg ]]
test_perms "$T_DIR_WORK/.gnupg" "drwx------"
}
@test "Command 'clone' (no existing .ssh, .ssh data tracked in repo)" {
echo "
Local .ssh does not exist
and yadm repo contains .ssh data
.ssh dir should be created and secured prior to merge
tracked .ssh data should be user accessible only
"
#; setup scenario
IN_REPO=(.bash_profile .vimrc .ssh/config)
setup
rm -rf "$T_DIR_WORK"
mkdir -p "$T_DIR_WORK"
rm -rf "$T_DIR_REPO"
#; run clone (with debug on)
run "${T_YADM_Y[@]}" clone -d -w "$T_DIR_WORK" "$REMOTE_URL"
#; validate status and output
[ "$status" -eq 0 ]
[[ "$output" =~ Initialized ]]
[[ ! "$output" =~ initial\ private\ dir\ perms ]]
[[ "$output" =~ pre-merge\ private\ dir\ perms\ drwx------.+\.ssh ]]
[[ "$output" =~ post-merge\ private\ dir\ perms\ drwx------.+\.ssh ]]
test_perms "$T_DIR_WORK/.ssh" "drwx------"
}

View file

@ -4,15 +4,20 @@ status=;output=; #; populated by bats run()
IN_REPO=(alt* "dir one") IN_REPO=(alt* "dir one")
export TEST_TREE_WITH_ALT=1 export TEST_TREE_WITH_ALT=1
EXCLUDED_NAME="excluded-base"
function create_encrypt() { function create_encrypt() {
for efile in "encrypted-base##" "encrypted-system##$T_SYS" "encrypted-host##$T_SYS.$T_HOST" "encrypted-user##$T_SYS.$T_HOST.$T_USER"; do for efile in "encrypted-base##" "encrypted-system##$T_SYS" "encrypted-host##$T_SYS.$T_HOST" "encrypted-user##$T_SYS.$T_HOST.$T_USER"; do
echo "$efile" >> "$T_YADM_ENCRYPT" echo "$efile" >> "$T_YADM_ENCRYPT"
echo "$efile" >> "$T_DIR_WORK/$efile" echo "$efile" >> "$T_DIR_WORK/$efile"
mkdir -p "$T_DIR_WORK/dir one/$efile" mkdir -p "$T_DIR_WORK/dir one/$efile"
echo "'dir one'/$efile/file1" >> "$T_YADM_ENCRYPT" echo "dir one/$efile/file1" >> "$T_YADM_ENCRYPT"
echo "dir one/$efile/file1" >> "$T_DIR_WORK/dir one/$efile/file1" echo "dir one/$efile/file1" >> "$T_DIR_WORK/dir one/$efile/file1"
done done
echo "$EXCLUDED_NAME##" >> "$T_YADM_ENCRYPT"
echo "!$EXCLUDED_NAME##" >> "$T_YADM_ENCRYPT"
echo "$EXCLUDED_NAME##" >> "$T_DIR_WORK/$EXCLUDED_NAME##"
} }
setup() { setup() {
@ -130,6 +135,12 @@ function test_alt() {
fi fi
fi fi
if [ -L "$T_DIR_WORK/$EXCLUDED_NAME" ] ; then
echo "ERROR: Found link: $T_DIR_WORK/$EXCLUDED_NAME"
echo "ERROR: Excluded files should not be linked"
return 1
fi
#; validate link content #; validate link content
if [[ "$alt_type" =~ none ]] || [ "$auto_alt" = "false" ]; then if [[ "$alt_type" =~ none ]] || [ "$auto_alt" = "false" ]; then
#; no link should be present #; no link should be present

View file

@ -88,6 +88,8 @@ EOF
"$T_GPG_PROGRAM" -q -d "$T_YADM_ARCHIVE" | tar t | sort > "$T_TMP/archive_list" "$T_GPG_PROGRAM" -q -d "$T_YADM_ARCHIVE" | tar t | sort > "$T_TMP/archive_list"
fi fi
excluded="$2"
#; inventory what is expected in the archive #; inventory what is expected in the archive
( (
if cd "$T_DIR_WORK"; then if cd "$T_DIR_WORK"; then
@ -95,10 +97,23 @@ EOF
# (globbing is desired) # (globbing is desired)
while IFS='' read -r glob || [ -n "$glob" ]; do while IFS='' read -r glob || [ -n "$glob" ]; do
if [[ ! $glob =~ ^# && ! $glob =~ ^[[:space:]]*$ ]] ; then if [[ ! $glob =~ ^# && ! $glob =~ ^[[:space:]]*$ ]] ; then
local IFS=$'\n' if [[ ! $glob =~ ^!(.+) ]] ; then
for matching_file in $(eval ls "$glob" 2>/dev/null); do local IFS=$'\n'
echo "$matching_file" for matching_file in $glob; do
done if [ -e "$matching_file" ]; then
if [ "$matching_file" != "$excluded" ]; then
if [ -d "$matching_file" ]; then
echo "$matching_file/"
for subfile in "$matching_file"/*; do
echo "$subfile"
done
else
echo "$matching_file"
fi
fi
fi
done
fi
fi fi
done < "$T_YADM_ENCRYPT" | sort > "$T_TMP/expected_list" done < "$T_YADM_ENCRYPT" | sort > "$T_TMP/expected_list"
fi fi
@ -290,7 +305,77 @@ EOF
#; add paths with spaces to YADM_ARCHIVE #; add paths with spaces to YADM_ARCHIVE
local original_encrypt local original_encrypt
original_encrypt=$(cat "$T_YADM_ENCRYPT") original_encrypt=$(cat "$T_YADM_ENCRYPT")
echo -e "'space test'/file*" >> "$T_YADM_ENCRYPT" echo -e "space test/file*" >> "$T_YADM_ENCRYPT"
#; run encrypt
run expect <<EOF
set timeout 2;
spawn ${T_YADM_Y[*]} encrypt;
expect "passphrase:" {send "$T_PASSWD\n"}
expect "passphrase:" {send "$T_PASSWD\n"}
expect "$"
foreach {pid spawnid os_error_flag value} [wait] break
exit \$value
EOF
#; validate status and output
[ "$status" -eq 0 ]
[[ "$output" =~ Wrote\ new\ file:.+$T_YADM_ARCHIVE ]]
#; validate the archive
validate_archive symmetric
}
@test "Command 'encrypt' (exclusions in YADM_ENCRYPT)" {
echo "
When 'encrypt' command is provided,
and YADM_ENCRYPT is present
Create YADM_ARCHIVE
Report the archive created
Archive should be valid
Exit with 0
"
#; add paths with spaces to YADM_ARCHIVE
local original_encrypt
original_encrypt=$(cat "$T_YADM_ENCRYPT")
echo -e ".ssh/*" >> "$T_YADM_ENCRYPT"
echo -e "!.ssh/sec*.pub" >> "$T_YADM_ENCRYPT"
#; run encrypt
run expect <<EOF
set timeout 2;
spawn ${T_YADM_Y[*]} encrypt;
expect "passphrase:" {send "$T_PASSWD\n"}
expect "passphrase:" {send "$T_PASSWD\n"}
expect "$"
foreach {pid spawnid os_error_flag value} [wait] break
exit \$value
EOF
#; validate status and output
[ "$status" -eq 0 ]
[[ "$output" =~ Wrote\ new\ file:.+$T_YADM_ARCHIVE ]]
[[ ! "$output" =~ \.ssh/secret.pub ]]
#; validate the archive
validate_archive symmetric ".ssh/secret.pub"
}
@test "Command 'encrypt' (directories in YADM_ENCRYPT)" {
echo "
When 'encrypt' command is provided,
and YADM_ENCRYPT is present
Create YADM_ARCHIVE
Report the archive created
Archive should be valid
Exit with 0
"
#; add directory paths to YADM_ARCHIVE
local original_encrypt
original_encrypt=$(cat "$T_YADM_ENCRYPT")
echo -e "space test" >> "$T_YADM_ENCRYPT"
#; run encrypt #; run encrypt
run expect <<EOF run expect <<EOF

View file

@ -27,13 +27,8 @@ function validate_perms() {
gpg) gpg)
restricted=("${restricted[@]}" $T_DIR_WORK/.gnupg $T_DIR_WORK/.gnupg/*) restricted=("${restricted[@]}" $T_DIR_WORK/.gnupg $T_DIR_WORK/.gnupg/*)
;; ;;
encrypt) *)
local glob restricted=("${restricted[@]}" $T_DIR_WORK/$p)
while IFS='' read -r glob || [ -n "$glob" ]; do
if [[ ! $glob =~ ^# ]] ; then
restricted=("${restricted[@]}" $T_DIR_WORK/$glob)
fi
done < "$T_YADM_ENCRYPT"
;; ;;
esac esac
done done
@ -80,7 +75,7 @@ function validate_perms() {
" "
#; this version has a comment in it #; this version has a comment in it
echo -e "#.vimrc\n.hammerspoon/*" > "$T_YADM_ENCRYPT" echo -e "#.vimrc\n.tmux.conf\n.hammerspoon/*\n!.tmux.conf" > "$T_YADM_ENCRYPT"
#; run perms #; run perms
run "${T_YADM_Y[@]}" perms run "${T_YADM_Y[@]}" perms
@ -89,11 +84,8 @@ function validate_perms() {
[ "$status" -eq 0 ] [ "$status" -eq 0 ]
[ "$output" = "" ] [ "$output" = "" ]
#; this version has no comments in it
echo -e ".hammerspoon/*" > "$T_YADM_ENCRYPT"
#; validate permissions #; validate permissions
validate_perms ssh gpg encrypt validate_perms ssh gpg ".hammerspoon/*"
} }
@test "Command 'perms' (ssh-perms=false)" { @test "Command 'perms' (ssh-perms=false)" {

View file

@ -9,6 +9,11 @@ export TEST_TREE_WITH_ALT=1
setup() { setup() {
destroy_tmp destroy_tmp
build_repo "${IN_REPO[@]}" build_repo "${IN_REPO[@]}"
echo "excluded-encrypt##yadm.j2" > "$T_YADM_ENCRYPT"
echo "included-encrypt##yadm.j2" >> "$T_YADM_ENCRYPT"
echo "!excluded-encrypt*" >> "$T_YADM_ENCRYPT"
echo "included-encrypt" > "$T_DIR_WORK/included-encrypt##yadm.j2"
echo "excluded-encrypt" > "$T_DIR_WORK/excluded-encrypt##yadm.j2"
} }
@ -27,6 +32,11 @@ function test_alt() {
real_name="alt-jinja" real_name="alt-jinja"
file_content_match="custom_class-custom_system-custom_host-custom_user-${T_DISTRO}" file_content_match="custom_class-custom_system-custom_host-custom_user-${T_DISTRO}"
;; ;;
encrypt)
real_name="included-encrypt"
file_content_match="included-encrypt"
missing_name="excluded-encrypt"
;;
esac esac
if [ "$test_overwrite" = "true" ] ; then if [ "$test_overwrite" = "true" ] ; then
@ -63,6 +73,11 @@ function test_alt() {
fi fi
fi fi
if [ -n "$missing_name" ] && [ -f "$T_DIR_WORK/$missing_name" ]; then
echo "ERROR: File should not have been created '$missing_name'"
return 1
fi
#; validate link content #; validate link content
if [[ "$alt_type" =~ none ]] || [ "$auto_alt" = "false" ]; then if [[ "$alt_type" =~ none ]] || [ "$auto_alt" = "false" ]; then
#; no real file should be present #; no real file should be present
@ -173,3 +188,16 @@ function test_alt() {
GIT_DIR="$T_DIR_REPO" git config local.class custom_class GIT_DIR="$T_DIR_REPO" git config local.class custom_class
test_alt 'override_all' 'false' '' test_alt 'override_all' 'false' ''
} }
@test "Command 'alt' (select jinja within .yadm/encrypt)" {
echo "
When the command 'alt' is provided
and file matches ##yadm.j2 within .yadm/encrypt
and file excluded within .yadm/encrypt
Report jinja template processing
Verify that the correct content is written
Exit with 0
"
test_alt 'encrypt' 'false' ''
}

View file

@ -73,7 +73,7 @@ function count_introspect() {
Exit with 0 Exit with 0
" "
count_introspect "configs" 0 12 'yadm\.auto-alt' count_introspect "configs" 0 13 'yadm\.auto-alt'
} }
@test "Command 'introspect' (repo)" { @test "Command 'introspect' (repo)" {

View file

@ -0,0 +1,102 @@
load common
load_fixtures
status=;output=; #; populated by bats run()
IN_REPO=(.bash_profile .vimrc)
setup() {
destroy_tmp
build_repo "${IN_REPO[@]}"
rm -rf "$T_DIR_WORK"
mkdir -p "$T_DIR_WORK"
}
@test "Private dirs (private dirs missing)" {
echo "
When a git command is run
And private directories are missing
Create private directories prior to command
"
#; confirm directories are missing at start
[ ! -e "$T_DIR_WORK/.gnupg" ]
[ ! -e "$T_DIR_WORK/.ssh" ]
#; run status
export DEBUG=yes
run "${T_YADM_Y[@]}" status
#; validate status and output
[ "$status" -eq 0 ]
[[ "$output" =~ On\ branch\ master ]]
#; confirm private directories are created
[ -d "$T_DIR_WORK/.gnupg" ]
test_perms "$T_DIR_WORK/.gnupg" "drwx------"
[ -d "$T_DIR_WORK/.ssh" ]
test_perms "$T_DIR_WORK/.ssh" "drwx------"
#; confirm directories are created before command is run
[[ "$output" =~ Creating.+/.gnupg/.+Creating.+/.ssh/.+Running\ git\ command\ git\ status ]]
}
@test "Private dirs (private dirs missing / yadm.auto-private-dirs=false)" {
echo "
When a git command is run
And private directories are missing
But auto-private-dirs is false
Do not create private dirs
"
#; confirm directories are missing at start
[ ! -e "$T_DIR_WORK/.gnupg" ]
[ ! -e "$T_DIR_WORK/.ssh" ]
#; set configuration
run "${T_YADM_Y[@]}" config --bool "yadm.auto-private-dirs" "false"
#; run status
run "${T_YADM_Y[@]}" status
#; validate status and output
[ "$status" -eq 0 ]
[[ "$output" =~ On\ branch\ master ]]
#; confirm private directories are not created
[ ! -e "$T_DIR_WORK/.gnupg" ]
[ ! -e "$T_DIR_WORK/.ssh" ]
}
@test "Private dirs (private dirs exist / yadm.auto-perms=false)" {
echo "
When a git command is run
And private directories exist
And yadm is configured not to auto update perms
Do not alter directories
"
#shellcheck disable=SC2174
mkdir -m 0777 -p "$T_DIR_WORK/.gnupg" "$T_DIR_WORK/.ssh"
#; confirm directories are preset and open
[ -d "$T_DIR_WORK/.gnupg" ]
test_perms "$T_DIR_WORK/.gnupg" "drwxrwxrwx"
[ -d "$T_DIR_WORK/.ssh" ]
test_perms "$T_DIR_WORK/.ssh" "drwxrwxrwx"
#; set configuration
run "${T_YADM_Y[@]}" config --bool "yadm.auto-perms" "false"
#; run status
run "${T_YADM_Y[@]}" status
#; validate status and output
[ "$status" -eq 0 ]
[[ "$output" =~ On\ branch\ master ]]
#; confirm directories are still preset and open
[ -d "$T_DIR_WORK/.gnupg" ]
test_perms "$T_DIR_WORK/.gnupg" "drwxrwxrwx"
[ -d "$T_DIR_WORK/.ssh" ]
test_perms "$T_DIR_WORK/.ssh" "drwxrwxrwx"
}

223
yadm
View file

@ -19,7 +19,7 @@ if [ -z "$BASH_VERSION" ]; then
[ "$YADM_TEST" != 1 ] && exec bash "$0" "$@" [ "$YADM_TEST" != 1 ] && exec bash "$0" "$@"
fi fi
VERSION=1.11.0 VERSION=1.12.0
YADM_WORK="$HOME" YADM_WORK="$HOME"
YADM_DIR="$HOME/.yadm" YADM_DIR="$HOME/.yadm"
@ -35,13 +35,14 @@ FULL_COMMAND=""
GPG_PROGRAM="gpg" GPG_PROGRAM="gpg"
GIT_PROGRAM="git" GIT_PROGRAM="git"
LS_PROGRAM="/bin/ls"
ENVTPL_PROGRAM="envtpl" ENVTPL_PROGRAM="envtpl"
LSB_RELEASE_PROGRAM="lsb_release" LSB_RELEASE_PROGRAM="lsb_release"
PROC_VERSION="/proc/version" PROC_VERSION="/proc/version"
OPERATING_SYSTEM="Unknown" OPERATING_SYSTEM="Unknown"
ENCRYPT_INCLUDE_FILES="unparsed"
#; flag causing path translations with cygpath #; flag causing path translations with cygpath
USE_CYGPATH=0 USE_CYGPATH=0
@ -128,6 +129,7 @@ function main() {
function alt() { function alt() {
require_repo require_repo
parse_encrypt
local_class="$(config local.class)" local_class="$(config local.class)"
if [ -z "$local_class" ] ; then if [ -z "$local_class" ] ; then
@ -160,32 +162,11 @@ function alt() {
match1="^(.+)##(()|$match_system|$match_system\.$match_host|$match_system\.$match_host\.$match_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)$" match2="^(.+)##($match_class|$match_class\.$match_system|$match_class\.$match_system\.$match_host|$match_class\.$match_system\.$match_host\.$match_user)$"
#; process relative to YADM_WORK cd_work "Alternates" || return
YADM_WORK=$(unix_path "$("$GIT_PROGRAM" config core.worktree)")
cd "$YADM_WORK" || {
debug "Alternates not processed, unable to cd to $YADM_WORK"
return
}
#; only be noisy if the "alt" command was run directly #; only be noisy if the "alt" command was run directly
[ "$YADM_COMMAND" = "alt" ] && loud="YES" [ "$YADM_COMMAND" = "alt" ] && loud="YES"
#; build a list of files from YADM_ENCRYPT
ENC_FILES=()
index=0
if [ -f "$YADM_ENCRYPT" ] ; then
while IFS='' read -r glob || [ -n "$glob" ]; do
if [[ ! $glob =~ ^# && ! $glob =~ ^[[:space:]]*$ ]] ; then
# echo "working on ->$glob<-"
local IFS=$'\n'
for matching_file in $(eval "$LS_PROGRAM" "$glob" 2>/dev/null); do
ENC_FILES[$index]="$matching_file"
((index++))
done
fi
done < "$YADM_ENCRYPT"
fi
#; decide if a copy should be done instead of a symbolic link #; decide if a copy should be done instead of a symbolic link
local do_copy=0 local do_copy=0
if [[ $OPERATING_SYSTEM == CYGWIN* ]] ; then if [[ $OPERATING_SYSTEM == CYGWIN* ]] ; then
@ -199,7 +180,7 @@ function alt() {
for match in $match1 $match2; do for match in $match1 $match2; do
last_linked='' last_linked=''
local IFS=$'\n' local IFS=$'\n'
for tracked_file in $("$GIT_PROGRAM" ls-files | sort) "${ENC_FILES[@]}"; do for tracked_file in $("$GIT_PROGRAM" ls-files | sort) "${ENCRYPT_INCLUDE_FILES[@]}"; do
tracked_file="$YADM_WORK/$tracked_file" tracked_file="$YADM_WORK/$tracked_file"
#; process both the path, and it's parent directory #; process both the path, and it's parent directory
for alt_path in "$tracked_file" "${tracked_file%/*}"; do for alt_path in "$tracked_file" "${tracked_file%/*}"; do
@ -229,7 +210,7 @@ function alt() {
#; for every file which is a *##yadm.j2 create a real file #; for every file which is a *##yadm.j2 create a real file
local IFS=$'\n' local IFS=$'\n'
local match="^(.+)##yadm\\.j2$" local match="^(.+)##yadm\\.j2$"
for tracked_file in $("$GIT_PROGRAM" ls-files | sort) $(cat "$YADM_ENCRYPT" 2>/dev/null); do for tracked_file in $("$GIT_PROGRAM" ls-files | sort) "${ENCRYPT_INCLUDE_FILES[@]}"; do
tracked_file="$YADM_WORK/$tracked_file" tracked_file="$YADM_WORK/$tracked_file"
if [ -e "$tracked_file" ] ; then if [ -e "$tracked_file" ] ; then
if [[ $tracked_file =~ $match ]] ; then if [[ $tracked_file =~ $match ]] ; then
@ -292,6 +273,8 @@ function clone() {
shift shift
done done
[ -n "$DEBUG" ] && display_private_perms "initial"
#; clone will begin with a bare repo #; clone will begin with a bare repo
local empty= local empty=
init $empty init $empty
@ -310,6 +293,15 @@ function clone() {
rm -rf "$YADM_REPO" rm -rf "$YADM_REPO"
error_out "Unable to fetch origin ${clone_args[0]}" 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" debug "Doing an initial merge of origin/master"
"$GIT_PROGRAM" merge origin/master || { "$GIT_PROGRAM" merge origin/master || {
debug "Merge failed, doing a reset and stashing conflicts." debug "Merge failed, doing a reset and stashing conflicts."
@ -351,6 +343,8 @@ EOF
fi fi
} }
[ -n "$DEBUG" ] && display_private_perms "post-merge"
CHANGES_POSSIBLE=1 CHANGES_POSSIBLE=1
} }
@ -422,14 +416,9 @@ function encrypt() {
require_gpg require_gpg
require_encrypt require_encrypt
require_ls parse_encrypt
#; process relative to YADM_WORK cd_work "Encryption" || return
YADM_WORK=$(unix_path "$("$GIT_PROGRAM" config core.worktree)")
cd "$YADM_WORK" || {
debug "Encryption not processed, unable to cd to $YADM_WORK"
return
}
#; Build gpg options for gpg #; Build gpg options for gpg
GPG_KEY="$(config yadm.gpg-recipient)" GPG_KEY="$(config yadm.gpg-recipient)"
@ -441,26 +430,13 @@ function encrypt() {
GPG_OPTS=("-c") GPG_OPTS=("-c")
fi fi
#; build a list of files from YADM_ENCRYPT
ENC_FILES=()
index=0
while IFS='' read -r glob || [ -n "$glob" ]; do
if [[ ! $glob =~ ^# && ! $glob =~ ^[[:space:]]*$ ]] ; then
local IFS=$'\n'
for matching_file in $(eval "$LS_PROGRAM" "$glob" 2>/dev/null); do
ENC_FILES[$index]="$matching_file"
((index++))
done
fi
done < "$YADM_ENCRYPT"
#; report which files will be encrypted #; report which files will be encrypted
echo "Encrypting the following files:" echo "Encrypting the following files:"
"$LS_PROGRAM" -1 "${ENC_FILES[@]}" printf '%s\n' "${ENCRYPT_INCLUDE_FILES[@]}"
echo echo
#; encrypt all files which match the globs #; encrypt all files which match the globs
if tar -f - -c "${ENC_FILES[@]}" | $GPG_PROGRAM --yes "${GPG_OPTS[@]}" --output "$YADM_ARCHIVE"; then if tar -f - -c "${ENCRYPT_INCLUDE_FILES[@]}" | $GPG_PROGRAM --yes "${GPG_OPTS[@]}" --output "$YADM_ARCHIVE"; then
echo "Wrote new file: $YADM_ARCHIVE" echo "Wrote new file: $YADM_ARCHIVE"
else else
error_out "Unable to write $YADM_ARCHIVE" error_out "Unable to write $YADM_ARCHIVE"
@ -513,9 +489,18 @@ function git_command() {
set -- "config" "${@:2}" set -- "config" "${@:2}"
fi 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 CHANGES_POSSIBLE=1
#; pass commands through to git #; pass commands through to git
debug "Running git command $GIT_PROGRAM $*"
"$GIT_PROGRAM" "$@" "$GIT_PROGRAM" "$@"
return "$?" return "$?"
} }
@ -613,6 +598,7 @@ local.os
local.user local.user
yadm.auto-alt yadm.auto-alt
yadm.auto-perms yadm.auto-perms
yadm.auto-private-dirs
yadm.cygwin-copy yadm.cygwin-copy
yadm.git-program yadm.git-program
yadm.gpg-perms yadm.gpg-perms
@ -644,11 +630,7 @@ function list() {
#; process relative to YADM_WORK when --all is specified #; process relative to YADM_WORK when --all is specified
if [ -n "$LIST_ALL" ] ; then if [ -n "$LIST_ALL" ] ; then
YADM_WORK=$(unix_path "$("$GIT_PROGRAM" config core.worktree)") cd_work "List" || return
cd "$YADM_WORK" || {
debug "List not processed, unable to cd to $YADM_WORK"
return
}
fi fi
#; list tracked files #; list tracked files
@ -658,40 +640,29 @@ function list() {
function perms() { function perms() {
require_ls parse_encrypt
#; TODO: prevent repeats in the files changed #; TODO: prevent repeats in the files changed
#; process relative to YADM_WORK cd_work "Perms" || return
YADM_WORK=$(unix_path "$("$GIT_PROGRAM" config core.worktree)")
cd "$YADM_WORK" || {
debug "Perms not processed, unable to cd to $YADM_WORK"
return
}
GLOBS=() GLOBS=()
#; include the archive created by "encrypt" #; include the archive created by "encrypt"
[ -f "$YADM_ARCHIVE" ] && GLOBS=("${GLOBS[@]}" "$YADM_ARCHIVE") [ -f "$YADM_ARCHIVE" ] && GLOBS+=("$YADM_ARCHIVE")
#; include all .ssh files (unless disabled) #; include all .ssh files (unless disabled)
if [[ $(config --bool yadm.ssh-perms) != "false" ]] ; then if [[ $(config --bool yadm.ssh-perms) != "false" ]] ; then
GLOBS=("${GLOBS[@]}" ".ssh" ".ssh/*") GLOBS+=(".ssh" ".ssh/*")
fi fi
#; include all gpg files (unless disabled) #; include all gpg files (unless disabled)
if [[ $(config --bool yadm.gpg-perms) != "false" ]] ; then if [[ $(config --bool yadm.gpg-perms) != "false" ]] ; then
GLOBS=("${GLOBS[@]}" ".gnupg" ".gnupg/*") GLOBS+=(".gnupg" ".gnupg/*")
fi fi
#; include globs found in YADM_ENCRYPT (if present) #; include any files we encrypt
if [ -f "$YADM_ENCRYPT" ] ; then GLOBS+=("${ENCRYPT_INCLUDE_FILES[@]}")
while IFS='' read -r glob || [ -n "$glob" ]; do
if [[ ! $glob =~ ^# ]] ; then
GLOBS=("${GLOBS[@]}" $(eval "$LS_PROGRAM" "$glob" 2>/dev/null))
fi
done < "$YADM_ENCRYPT"
fi
#; remove group/other permissions from collected globs #; remove group/other permissions from collected globs
#shellcheck disable=SC2068 #shellcheck disable=SC2068
@ -841,7 +812,7 @@ function set_operating_system() {
CYGWIN*) CYGWIN*)
git_version=$(git --version 2>/dev/null) git_version=$(git --version 2>/dev/null)
if [[ "$git_version" =~ windows ]] ; then if [[ "$git_version" =~ windows ]] ; then
USE_CYGPATH=1 USE_CYGPATH=1
fi fi
;; ;;
*) *)
@ -852,13 +823,13 @@ function set_operating_system() {
function debug() { function debug() {
[ -n "$DEBUG" ] && echo -e "DEBUG: $*" [ -n "$DEBUG" ] && echo_e "DEBUG: $*"
} }
function error_out() { function error_out() {
echo -e "ERROR: $*" echo_e "ERROR: $*"
exit_with_hook 1 exit_with_hook 1
} }
@ -906,6 +877,89 @@ function invoke_hook() {
} }
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
ENCRYPT_INCLUDE_FILES=("${FINAL_INCLUDE[@]}")
fi
}
#; ****** Auto Functions ****** #; ****** Auto Functions ******
function auto_alt() { function auto_alt() {
@ -990,13 +1044,6 @@ function require_gpg() {
function require_repo() { function require_repo() {
[ -d "$YADM_REPO" ] || error_out "Git repo does not exist. did you forget to run 'init' or 'clone'?" [ -d "$YADM_REPO" ] || error_out "Git repo does not exist. did you forget to run 'init' or 'clone'?"
} }
function require_ls() {
if [ ! -f "$LS_PROGRAM" ] ; then
command -v ls >/dev/null 2>&1 || \
error_out "This functionality requires 'ls' to be installed at '$LS_PROGRAM' or listed in your \$PATH"
LS_PROGRAM=ls
fi
}
function require_shell() { function require_shell() {
[ -x "$SHELL" ] || error_out "\$SHELL does not refer to an executable." [ -x "$SHELL" ] || error_out "\$SHELL does not refer to an executable."
} }
@ -1028,6 +1075,20 @@ function mixed_path() {
fi 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) ****** #; ****** Main processing (when not unit testing) ******
if [ "$YADM_TEST" != 1 ] ; then if [ "$YADM_TEST" != 1 ] ; then

45
yadm.1
View file

@ -1,5 +1,5 @@
." vim: set spell so=8: ." vim: set spell so=8:
.TH yadm 1 "10 July 2017" "1.11.0" .TH yadm 1 "25 October 2017" "1.12.0"
.SH NAME .SH NAME
yadm \- Yet Another Dotfiles Manager yadm \- Yet Another Dotfiles Manager
.SH SYNOPSIS .SH SYNOPSIS
@ -350,6 +350,9 @@ If disabled, you may still run
manually to update permissions. manually to update permissions.
This feature is enabled by default. This feature is enabled by default.
.TP .TP
.B yadm.auto-private-dirs
Disable the automatic creating of private directories described in the section PERMISSIONS.
.TP
.B yadm.ssh-perms .B yadm.ssh-perms
Disable the permission changes to Disable the permission changes to
.IR $HOME/.ssh/* . .IR $HOME/.ssh/* .
@ -583,6 +586,11 @@ For example:
.gnupg/*.gpg .gnupg/*.gpg
.RE .RE
Standard filename expansions (*, ?, [) are supported. Other shell expansions
like brace and tilde are not supported. Spaces in paths are supported, and
should not be quoted. If a directory is specified, its contents will be
included, but not recursively. Paths beginning with a "!" will be excluded.
The The
.B yadm encrypt .B yadm encrypt
command will find all files matching the patterns, and prompt for a password. Once a command will find all files matching the patterns, and prompt for a password. Once a
@ -608,12 +616,10 @@ It is recommended that you use a private repository when keeping confidential
files, even though they are encrypted. files, even though they are encrypted.
.SH PERMISSIONS .SH PERMISSIONS
When files are checked out of a Git repository, their initial permissions are When files are checked out of a Git repository, their initial permissions are
dependent upon the user's umask. This can result in confidential files with lax permissions. dependent upon the user's umask. Because of this,
To prevent this,
.B yadm .B yadm
will automatically update the permissions of confidential files. will automatically update the permissions of some file paths. The "group" and
The "group" and "others" permissions will be removed from the following files: "others" permissions will be removed from the following files:
.RI - " $HOME/.yadm/files.gpg .RI - " $HOME/.yadm/files.gpg
@ -629,11 +635,32 @@ The "group" and "others" permissions will be removed from the following files:
.B yadm .B yadm
will automatically update permissions by default. This can be disabled using the will automatically update permissions by default. This can be disabled using the
.I yadm.auto-perms .I yadm.auto-perms
configuration. configuration. Even if disabled, permissions can be manually updated by running
Even if disabled, permissions can be manually updated by running
.BR yadm\ perms . .BR yadm\ perms .
The SSH directory processing can be disabled using the The
.I .ssh
directory processing can be disabled using the
.I yadm.ssh-perms .I yadm.ssh-perms
configuration. The
.I .gnupg
directory processing can be disabled using the
.I yadm.gpg-perms
configuration.
When cloning a repo which includes data in a
.IR .ssh " or " .gnupg
directory, if those directories do not exist at the time of cloning,
.B yadm
will create the directories with mask 0700 prior to merging the fetched data
into the work-tree.
When running a Git command and
.IR .ssh " or " .gnupg
directories do not exist,
.B yadm
will create those directories with mask 0700 prior to running the Git command.
This can be disabled using the
.I yadm.auto-private-dirs
configuration. configuration.
.SH HOOKS .SH HOOKS
For every command For every command

157
yadm.md
View file

@ -214,50 +214,54 @@
manually to update permissions. This feature is enabled by manually to update permissions. This feature is enabled by
default. default.
yadm.auto-private-dirs
Disable the automatic creating of private directories described
in the section PERMISSIONS.
yadm.ssh-perms yadm.ssh-perms
Disable the permission changes to $HOME/.ssh/*. This feature is Disable the permission changes to $HOME/.ssh/*. This feature is
enabled by default. enabled by default.
yadm.gpg-perms yadm.gpg-perms
Disable the permission changes to $HOME/.gnupg/*. This feature Disable the permission changes to $HOME/.gnupg/*. This feature
is enabled by default. is enabled by default.
yadm.gpg-recipient yadm.gpg-recipient
Asymmetrically encrypt files with a gpg public/private key pair. Asymmetrically encrypt files with a gpg public/private key pair.
Provide a "key ID" to specify which public key to encrypt with. Provide a "key ID" to specify which public key to encrypt with.
The key must exist in your public keyrings. If left blank or The key must exist in your public keyrings. If left blank or
not provided, symmetric encryption is used instead. If set to not provided, symmetric encryption is used instead. If set to
"ASK", gpg will interactively ask for recipients. See the "ASK", gpg will interactively ask for recipients. See the
ENCRYPTION section for more details. This feature is disabled ENCRYPTION section for more details. This feature is disabled
by default. by default.
yadm.gpg-program yadm.gpg-program
Specify an alternate program to use instead of "gpg". By Specify an alternate program to use instead of "gpg". By
default, the first "gpg" found in $PATH is used. default, the first "gpg" found in $PATH is used.
yadm.git-program yadm.git-program
Specify an alternate program to use instead of "git". By Specify an alternate program to use instead of "git". By
default, the first "git" found in $PATH is used. default, the first "git" found in $PATH is used.
yadm.cygwin-copy yadm.cygwin-copy
If set to "true", for Cygwin hosts, alternate files will be If set to "true", for Cygwin hosts, alternate files will be
copies instead of symbolic links. This might be desirable, copies instead of symbolic links. This might be desirable,
because non-Cygwin software may not properly interpret Cygwin because non-Cygwin software may not properly interpret Cygwin
symlinks. symlinks.
These last four "local" configurations are not stored in the These last four "local" configurations are not stored in the
$HOME/.yadm/config, they are stored in the local repository. $HOME/.yadm/config, they are stored in the local repository.
local.class local.class
Specify a CLASS for the purpose of symlinking alternate files. Specify a CLASS for the purpose of symlinking alternate files.
By default, no CLASS will be matched. By default, no CLASS will be matched.
local.os local.os
Override the OS for the purpose of symlinking alternate files. Override the OS for the purpose of symlinking alternate files.
local.hostname local.hostname
Override the HOSTNAME for the purpose of symlinking alternate Override the HOSTNAME for the purpose of symlinking alternate
files. files.
local.user local.user
@ -268,7 +272,7 @@
to have an automated way of choosing an alternate version of a file for to have an automated way of choosing an alternate version of a file for
a different operating system, host, or user. yadm implements a feature a different operating system, host, or user. yadm implements a feature
which will automatically create a symbolic link to the appropriate ver- which will automatically create a symbolic link to the appropriate ver-
sion of a file, as long as you follow a specific naming convention. sion of a file, as long as you follow a specific naming convention.
yadm can detect files with names ending in any of the following: yadm can detect files with names ending in any of the following:
## ##
@ -280,10 +284,10 @@
##OS.HOSTNAME ##OS.HOSTNAME
##OS.HOSTNAME.USER ##OS.HOSTNAME.USER
If there are any files managed by yadm's repository, or listed in If there are any files managed by yadm's repository, or listed in
$HOME/.yadm/encrypt, which match this naming convention, symbolic links $HOME/.yadm/encrypt, which match this naming convention, symbolic links
will be created for the most appropriate version. This may best be will be created for the most appropriate version. This may best be
demonstrated by example. Assume the following files are managed by demonstrated by example. Assume the following files are managed by
yadm's repository: yadm's repository:
- $HOME/path/example.txt## - $HOME/path/example.txt##
@ -305,7 +309,7 @@
$HOME/path/example.txt -> $HOME/path/example.txt##Darwin $HOME/path/example.txt -> $HOME/path/example.txt##Darwin
Since the hostname doesn't match any of the managed files, the more Since the hostname doesn't match any of the managed files, the more
generic version is chosen. generic version is chosen.
If running on a Linux server named "host4", the link will be: If running on a Linux server named "host4", the link will be:
@ -323,42 +327,42 @@
If no "##" version exists and no files match the current CLASS/OS/HOST- If no "##" version exists and no files match the current CLASS/OS/HOST-
NAME/USER, then no link will be created. NAME/USER, then no link will be created.
Links are also created for directories named this way, as long as they Links are also created for directories named this way, as long as they
have at least one yadm managed file within them. have at least one yadm managed file within them.
CLASS must be manually set using yadm config local.class <class>. OS CLASS must be manually set using yadm config local.class <class>. OS
is determined by running uname -s, HOSTNAME by running hostname, and is determined by running uname -s, HOSTNAME by running hostname, and
USER by running id -u -n. yadm will automatically create these links USER by running id -u -n. yadm will automatically create these links
by default. This can be disabled using the yadm.auto-alt configuration. by default. This can be disabled using the yadm.auto-alt configuration.
Even if disabled, links can be manually created by running yadm alt. Even if disabled, links can be manually created by running yadm alt.
It is possible to use "%" as a "wildcard" in place of CLASS, OS, HOST- It is possible to use "%" as a "wildcard" in place of CLASS, OS, HOST-
NAME, or USER. For example, The following file could be linked for any NAME, or USER. For example, The following file could be linked for any
host when the user is "harvey". host when the user is "harvey".
$HOME/path/example.txt##%.%.harvey $HOME/path/example.txt##%.%.harvey
CLASS is a special value which is stored locally on each host (inside CLASS is a special value which is stored locally on each host (inside
the local repository). To use alternate symlinks using CLASS, you must the local repository). To use alternate symlinks using CLASS, you must
set the value of class using the configuration local.class. This is set the value of class using the configuration local.class. This is
set like any other yadm configuration with the yadm config command. The set like any other yadm configuration with the yadm config command. The
following sets the CLASS to be "Work". following sets the CLASS to be "Work".
yadm config local.class Work yadm config local.class Work
Similarly, the values of OS, HOSTNAME, and USER can be manually over- Similarly, the values of OS, HOSTNAME, and USER can be manually over-
ridden using the configuration options local.os, local.hostname, and ridden using the configuration options local.os, local.hostname, and
local.user. local.user.
## JINJA ## JINJA
If the envtpl command is available, Jinja templates will also be pro- If the envtpl command is available, Jinja templates will also be pro-
cessed to create or overwrite real files. yadm will treat files ending cessed to create or overwrite real files. yadm will treat files ending
in in
##yadm.j2 ##yadm.j2
as Jinja templates. During processing, the following variables are set as Jinja templates. During processing, the following variables are set
according to the rules explained in the ALTERNATES section: according to the rules explained in the ALTERNATES section:
YADM_CLASS YADM_CLASS
@ -366,7 +370,7 @@
YADM_HOSTNAME YADM_HOSTNAME
YADM_USER YADM_USER
In addition YADM_DISTRO is exposed as the value of lsb_release -si if In addition YADM_DISTRO is exposed as the value of lsb_release -si if
lsb_release is locally available. lsb_release is locally available.
For example, a file named whatever##yadm.j2 with the following content For example, a file named whatever##yadm.j2 with the following content
@ -377,7 +381,7 @@
config=dev-whatever config=dev-whatever
{% endif -%} {% endif -%}
would output a file named whatever with the following content if the would output a file named whatever with the following content if the
user is "harvey": user is "harvey":
config=work-Linux config=work-Linux
@ -390,45 +394,48 @@
## ENCRYPTION ## ENCRYPTION
It can be useful to manage confidential files, like SSH or GPG keys, It can be useful to manage confidential files, like SSH or GPG keys,
across multiple systems. However, doing so would put plain text data across multiple systems. However, doing so would put plain text data
into a Git repository, which often resides on a public system. yadm into a Git repository, which often resides on a public system. yadm
implements a feature which can make it easy to encrypt and decrypt a implements a feature which can make it easy to encrypt and decrypt a
set of files so the encrypted version can be maintained in the Git set of files so the encrypted version can be maintained in the Git
repository. This feature will only work if the gpg(1) command is repository. This feature will only work if the gpg(1) command is
available. available.
To use this feature, a list of patterns must be created and saved as To use this feature, a list of patterns must be created and saved as
$HOME/.yadm/encrypt. This list of patterns should be relative to the $HOME/.yadm/encrypt. This list of patterns should be relative to the
configured work-tree (usually $HOME). For example: configured work-tree (usually $HOME). For example:
.ssh/*.key .ssh/*.key
.gnupg/*.gpg .gnupg/*.gpg
Standard filename expansions (*, ?, [) are supported. Other shell
expansions like brace and tilde are not supported. Spaces in paths are
supported, and should not be quoted. If a directory is specified, its
contents will be included, but not recursively. Paths beginning with a
"!" will be excluded.
The yadm encrypt command will find all files matching the patterns, and The yadm encrypt command will find all files matching the patterns, and
prompt for a password. Once a password has confirmed, the matching prompt for a password. Once a password has confirmed, the matching
files will be encrypted and saved as $HOME/.yadm/files.gpg. The pat- files will be encrypted and saved as $HOME/.yadm/files.gpg. The pat-
terns and files.gpg should be added to the yadm repository so they are terns and files.gpg should be added to the yadm repository so they are
available across multiple systems. available across multiple systems.
To decrypt these files later, or on another system run yadm decrypt and To decrypt these files later, or on another system run yadm decrypt and
provide the correct password. After files are decrypted, permissions provide the correct password. After files are decrypted, permissions
are automatically updated as described in the PERMISSIONS section. are automatically updated as described in the PERMISSIONS section.
Symmetric encryption is used by default, but asymmetric encryption may Symmetric encryption is used by default, but asymmetric encryption may
be enabled using the yadm.gpg-recipient configuration. be enabled using the yadm.gpg-recipient configuration.
NOTE: It is recommended that you use a private repository when keeping NOTE: It is recommended that you use a private repository when keeping
confidential files, even though they are encrypted. confidential files, even though they are encrypted.
## PERMISSIONS ## PERMISSIONS
When files are checked out of a Git repository, their initial permis- When files are checked out of a Git repository, their initial permis-
sions are dependent upon the user's umask. This can result in confiden- sions are dependent upon the user's umask. Because of this, yadm will
tial files with lax permissions. automatically update the permissions of some file paths. The "group"
and "others" permissions will be removed from the following files:
To prevent this, yadm will automatically update the permissions of con-
fidential files. The "group" and "others" permissions will be removed
from the following files:
- $HOME/.yadm/files.gpg - $HOME/.yadm/files.gpg
@ -439,26 +446,38 @@
- The GPG directory and files, .gnupg/* - The GPG directory and files, .gnupg/*
yadm will automatically update permissions by default. This can be dis- yadm will automatically update permissions by default. This can be dis-
abled using the yadm.auto-perms configuration. Even if disabled, per- abled using the yadm.auto-perms configuration. Even if disabled, per-
missions can be manually updated by running yadm perms. The SSH direc- missions can be manually updated by running yadm perms. The .ssh
tory processing can be disabled using the yadm.ssh-perms configuration. directory processing can be disabled using the yadm.ssh-perms configu-
ration. The .gnupg directory processing can be disabled using the
yadm.gpg-perms configuration.
When cloning a repo which includes data in a .ssh or .gnupg directory,
if those directories do not exist at the time of cloning, yadm will
create the directories with mask 0700 prior to merging the fetched data
into the work-tree.
When running a Git command and .ssh or .gnupg directories do not exist,
yadm will create those directories with mask 0700 prior to running the
Git command. This can be disabled using the yadm.auto-private-dirs
configuration.
## HOOKS ## HOOKS
For every command yadm supports, a program can be provided to run For every command yadm supports, a program can be provided to run
before or after that command. These are referred to as "hooks". yadm before or after that command. These are referred to as "hooks". yadm
looks for hooks in the directory $HOME/.yadm/hooks. Each hook is named looks for hooks in the directory $HOME/.yadm/hooks. Each hook is named
using a prefix of pre_ or post_, followed by the command which should using a prefix of pre_ or post_, followed by the command which should
trigger the hook. For example, to create a hook which is run after trigger the hook. For example, to create a hook which is run after
every yadm pull command, create a hook named post_pull. Hooks must every yadm pull command, create a hook named post_pull. Hooks must
have the executable file permission set. have the executable file permission set.
If a pre_ hook is defined, and the hook terminates with a non-zero exit If a pre_ hook is defined, and the hook terminates with a non-zero exit
status, yadm will refuse to run the yadm command. For example, if a status, yadm will refuse to run the yadm command. For example, if a
pre_commit hook is defined, but that command ends with a non-zero exit pre_commit hook is defined, but that command ends with a non-zero exit
status, the yadm commit will never be run. This allows one to "short- status, the yadm commit will never be run. This allows one to "short-
circuit" any operation using a pre_ hook. circuit" any operation using a pre_ hook.
Hooks have the following environment variables available to them at Hooks have the following environment variables available to them at
runtime: runtime:
YADM_HOOK_COMMAND YADM_HOOK_COMMAND
@ -477,8 +496,8 @@
The path to the work-tree The path to the work-tree
## FILES ## FILES
The following are the default paths yadm uses for its own data. These The following are the default paths yadm uses for its own data. These
paths can be altered using universal options. See the OPTIONS section paths can be altered using universal options. See the OPTIONS section
for details. for details.
$HOME/.yadm $HOME/.yadm

View file

@ -1,6 +1,6 @@
Summary: Yet Another Dotfiles Manager Summary: Yet Another Dotfiles Manager
Name: yadm Name: yadm
Version: 1.11.0 Version: 1.12.0
Release: 1%{?dist} Release: 1%{?dist}
URL: https://github.com/TheLocehiliosan/yadm URL: https://github.com/TheLocehiliosan/yadm
License: GPLv3 License: GPLv3
@ -34,10 +34,17 @@ install -m 644 yadm.1 ${RPM_BUILD_ROOT}%{_mandir}/man1
%attr(755,root,root) %{_bindir}/yadm %attr(755,root,root) %{_bindir}/yadm
%attr(644,root,root) %{_mandir}/man1/* %attr(644,root,root) %{_mandir}/man1/*
%license LICENSE %license LICENSE
%doc CHANGES CONTRIBUTORS README.md completion/yadm.bash_completion %doc CHANGES CONTRIBUTORS README.md completion/*
%changelog %changelog
* Mon July 10 2017 Tim Byrne <sultan@locehilios.com> - 1.11.0-1 * Wed Oct 25 2017 Tim Byrne <sultan@locehilios.com> - 1.12.0-1
- Bump version to 1.12.0
- Include zsh completion
* Wed Aug 23 2017 Tim Byrne <sultan@locehilios.com> - 1.11.1-1
- Bump version to 1.11.1
* Mon Jul 10 2017 Tim Byrne <sultan@locehilios.com> - 1.11.0-1
- Bump version to 1.11.0 - Bump version to 1.11.0
* Wed May 10 2017 Tim Byrne <sultan@locehilios.com> - 1.10.0-1 * Wed May 10 2017 Tim Byrne <sultan@locehilios.com> - 1.10.0-1