1
0
Fork 0

[meta] Add homeshick

main
Sebastian Schulze 2020-01-27 16:13:15 +01:00
parent 9886e23d88
commit 700ad3cca1
Signed by: bascht
GPG Key ID: 5BCB1D3B4D38A35A
61 changed files with 4555 additions and 0 deletions

View File

@ -44,3 +44,7 @@ update-kubectx:
update-plug.vim:
curl -s -L https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim > $$(~/bin/chezmoi source-path ~/.vim/autoload/plug.vim)
update-homeschick:
curl -s -L -o homeshick-master.tar.gz https://github.com/andsens/homeshick/archive/master.tar.gz
chezmoi import --strip-components 1 --destination ${HOME}/.homesick/repos/homeshick homeshick-master.tar.gz

View File

@ -0,0 +1,76 @@
# Contribution guidelines #
## Reporting issues ##
Make sure that what you are experiencing is actually an error and that it lies
with homeshick (often it can be a git configuration error)
### Questions ###
If you have a question be sure to read
[the documentation](https://github.com/andsens/homeshick/wiki) first.
Often you will find the answer to it there.
### Description ###
As with bug reports everywhere else:
* state the action(s) you took
* explain what outcome you expected
* describe the actual result
You will also need to report which operating system you encountered the issue on
and which shell you used (type `echo $SHELL` in your terminal if you are unsure).
### Reproducing ###
Unless you ran in to a [heisenbug](http://en.wikipedia.org/wiki/Heisenbug),
it should be possible to reproduce the
bug in a testing environment. To that end run
`$HOME/.homesick/repos/homeshick/test/interactive` and reproduce the bug. This
script drops you into a new shell where `$HOME` is set to an (almost) empty
temporary folder. If you cannot reproduce the bug there, the error is likely
with your setup and not homeshick. Otherwise attach the commands you executed
in that environment to the issue.
## Pull requests ##
### Branching
**Work from and create pull requests on `development`, not `master`.**
`master` always represents the latest release since that is the way homeshick
updates itself. The `development` branch is where work is done for the next
release version of homeshick.
### Code style ###
* Indent with tabs and align with spaces.
* Always use double brackets for `if` blocks
* Run your changes through
[shellcheck](https://www.shellcheck.net/) with `test/shellcheck`
and the [bats](https://github.com/sstephenson/bats) testsuite with `test/run`
Use the supplied [editorconfig](http://editorconfig.org) file.
Most editors have [editorconfig plugins](http://editorconfig.org/#download)
to apply these settings.
### Content ###
**Every PR should only contain one feature change, bug fix or typo correction.**
Commits should be atomic units of work, if they are not you should rebase them
so that they are (typo corrections from a previous change for example do not
justify a commit).
### Description ###
The PR should clearly describe what problem the change fixes.
A feature addition with no justification and use-case will be rejected.
### Testing ###
Unless the code-change is a refactor, you should always add unit tests. When
fixing a bug there should be a new test case that fails with the old code and
succeeds with the new code. When introducing a new feature, it should be
tested extensively, a single test case will not suffice.
Note that bats does not fail a test case when using double brackets.
To assert variable values and file existance you *must* use single brackets!
Also consider negative test cases (e.g. what happens when a non-existing
castlename is passed as an argument?).
You can read about the details of the testing framework in the
[testing documentation](https://github.com/andsens/homeshick/wiki/Testing).

View File

@ -0,0 +1,20 @@
Copyright (c) 2012-2014 Anders Ingemann
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,43 @@
homeshick [![Build Status](https://travis-ci.org/andsens/homeshick.png?branch=development)](https://travis-ci.org/andsens/homeshick)
=========
In Unix, configuration files are king.
Tailoring tools to suit your needs through configuration can be empowering.
An immense number of hours is spent on getting these adjustments just right,
but once you leave the confines of your own computer, these local optimizations are left behind.
By the power of git, homeshick enables you to bring the symphony of settings
you have poured your heart into with you to remote computers.
With it you can begin to focus even more energy on bettering your work environment
since the benefits are reaped on whichever machine you are using.
However bare bones these machines are, provided that at least Bash 3 and Git 1.5 are available you can use homeshick.
homeshick can handle multiple dotfile repositories. This means that you can install
larger frameworks like [oh-my-zsh](https://github.com/robbyrussell/oh-my-zsh)
or a multitude of emacs or vim plugins alongside your own customizations without clutter.
For detailed [installation instructions](https://github.com/andsens/homeshick/wiki/Installation), [tutorials](https://github.com/andsens/homeshick/wiki/Tutorials) and [tips](https://github.com/andsens/homeshick/wiki/Automatic-deployment) & [tricks](https://github.com/andsens/homeshick/wiki/Symlinking) have a look at the [wiki](https://github.com/andsens/homeshick/wiki).
Quick install
-------------
homeshick is installed to your own home directory and does not require root privileges to be installed.
```sh
git clone https://github.com/andsens/homeshick.git $HOME/.homesick/repos/homeshick
```
*Note: If you'd like to help testing new features before they are released use `git clone --branch testing https://...`*
To invoke homeshick, source the `homeshick.sh` script from your rc-script:
```sh
# from sh and its derivates (bash, dash, ksh, zsh etc.)
printf '\nsource "$HOME/.homesick/repos/homeshick/homeshick.sh"' >> $HOME/.bashrc
# csh and derivatives (i.e. tcsh)
printf '\nalias homeshick source "$HOME/.homesick/repos/homeshick/homeshick.csh"\n' >> $HOME/.cshrc
# fish shell
echo \n'source "$HOME/.homesick/repos/homeshick/homeshick.fish"' >> "$HOME/.config/fish/config.fish"
```
Contributing
------------
Before submitting pull requests or reporting bugs, please make sure to read
the [contribution guidelines](CONTRIBUTING.md).

View File

@ -0,0 +1,212 @@
#!/usr/bin/env bash
repos="$HOME/.homesick/repos"
# Include all helper functions. We will include the required command function later on.
homeshick=${HOMESHICK_DIR:-$HOME/.homesick/repos/homeshick}
# On travis-ci exit_status for some reason errors out, ignore it
# shellcheck disable=SC1090
source "$homeshick/lib/exit_status.sh"
# shellcheck source=lib/fs.sh
source "$homeshick/lib/fs.sh"
# shellcheck source=lib/git.sh
source "$homeshick/lib/git.sh"
# shellcheck source=lib/log.sh
source "$homeshick/lib/log.sh"
# shellcheck source=lib/prompt.sh
source "$homeshick/lib/prompt.sh"
# lots of global variables in here, so just disable SC2034 for the entire file
# shellcheck disable=SC2034
true
exit_status=$EX_SUCCESS
type git &>/dev/null || err "$EX_SOFTWARE" "git not found in path"
if [[ ! -d $repos ]]; then
mkdir -p "$repos"
fi
# used in pull.sh
# shellcheck disable=SC2034
T_START=$(date +%s)
if [[ -z $GIT_VERSION ]]; then
read -r _ _ GIT_VERSION _ < <(command git --version)
if [[ ! $GIT_VERSION =~ ([0-9]+)(\.[0-9]+){0,3} ]]; then
err "$EX_SOFTWARE" "could not determine git version"
fi
fi
TALK=true
SKIP=false
FORCE=false
BATCH=false
VERBOSE=false
# Retrieve all the flags preceeding a subcommand
while [[ $# -gt 0 ]]; do
if [[ $1 =~ ^- ]]; then
# Convert combined short options into multiples short options (e.g. `-qb' to `-q -b')
if [[ $1 =~ ^-[a-z]{2,} ]]; then
param=$1
shift
set -- "${param:0:2}" "-${param:2}" "$@"
unset param
fi
case $1 in
-h | --help) cmd="help" ; shift; continue ;;
-q | --quiet) TALK=false ; shift; continue ;;
-s | --skip) SKIP=true ; shift; continue ;;
-f | --force) FORCE=true ; shift; continue ;;
-b | --batch) BATCH=true ; shift; continue ;;
-v | --verbose) VERBOSE=true ; shift; continue ;;
*) err "$EX_USAGE" "Unknown option '$1'" ;;
esac
else
break
fi
done
[[ $# -gt 0 ]] || cmd="help"
# Get the subcommand
valid_commands=(cd clone generate list check updates refresh pull symlink link track help)
if [[ ! $cmd ]]; then
# We actually want literal matching of the rhs here, $1 shouldn't be a regexp
# shellcheck disable=SC2076
if [[ " ${valid_commands[*]} " =~ " $1 " ]]; then
cmd=$1
shift
fi
if [[ ! $cmd ]]; then
err "$EX_USAGE" "Unknown command '$1'"
fi
fi
# Get the arguments for the subcommand, also parse flags if there are any left
while [[ $# -gt 0 ]]; do
if [[ $1 =~ ^- ]]; then
# Convert combined short options into multiples short options (e.g. `-qb' to `-q -b')
if [[ $1 =~ ^-[a-z]{2,} ]]; then
param=$1
shift
set -- "${param:0:2}" "-${param:2}" "$@"
unset param
fi
case $1 in
-h | --help)
cmd="help"
shift; continue ;;
-q | --quiet)
TALK=false
shift; continue ;;
-s | --skip)
SKIP=true
shift; continue ;;
-f | --force)
FORCE=true
shift; continue ;;
-b | --batch)
BATCH=true
shift; continue ;;
-v | --verbose)
VERBOSE=true
shift; continue ;;
*) err "$EX_USAGE" "Unknown option '$1'" ;;
esac
fi
case $cmd in
cd | clone | generate | check | updates | pull | symlink | link)
params+=("$1")
shift; continue ;;
refresh)
[[ ! $threshhold ]] && threshhold=$(($1*86400)) || params+=("$1")
shift; continue ;;
track)
[[ ! $castle ]] && castle=$1 || params+=("$1")
shift; continue ;;
list) err "$EX_USAGE" "The 'list' command does not take any arguments" ;;
help)
[[ ! $help_cmd ]] && help_cmd=$1
shift; continue;;
*) err "$EX_USAGE" "Unknown command '$1'" ;;
esac
done
# If no additional arguments are given, run the subcommand for every castle
if [[ ${#params[@]} -eq 0 ]]; then
case $cmd in
check | updates | refresh | pull | symlink | link)
while IFS= read -d $'\n' -r name ; do
params+=("$name")
done < <(list_castle_names) ;;
# These commands require parameters, show the help message instead
cd | clone | generate | track) help_cmd=$cmd; cmd="help"; exit_status=$EX_USAGE ;;
esac
fi
# Default param for refresh is 7
[[ ! $threshhold ]] && threshhold=$((7*86400))
# Include the file that implements the invoked command
case $cmd in
cd) ;;
symlink)
# shellcheck source=lib/commands/link.sh
source "$homeshick/lib/commands/link.sh" ;;
updates)
# shellcheck source=lib/commands/check.sh
source "$homeshick/lib/commands/check.sh" ;;
*)
# Don't know what will be included, so just disable the rule
# shellcheck disable=SC1090
source "$homeshick/lib/commands/$cmd.sh" ;;
esac
case $cmd in
list) list ;;
cd) help cd ;; # cd is implemented in the homeshick.{sh,csh} helper script.
help) help $help_cmd ;;
*)
for param in "${params[@]}"; do
case $cmd in
clone)
clone "$param" ;;
generate)
generate "$param" ;;
check|updates)
(check "$param") ;;
refresh)
(refresh $threshhold "$param") ;;
pull)
(pull "$param") ;;
symlink|link)
symlink "$param" ;;
track)
track "$castle" "$param" ;;
esac
result=$?
if [[ $result == "$EX_USAGE" ]]; then
exit "$EX_USAGE"
fi
if [[ $exit_status == 0 && $result != 0 ]]; then
exit_status=$result
fi
done
case $cmd in
clone)
symlink_cloned_files "${params[@]}" ;;
refresh)
pull_outdated $threshhold "${params[@]}" ;;
pull)
symlink_new_files "${params[@]}" ;;
esac
result=$?
if [[ $exit_status == 0 && $result != 0 ]]; then
exit_status=$result
fi
;;
esac
exit $exit_status

View File

@ -0,0 +1,72 @@
#compdef homeshick
_homeshick () {
local context state line curcontext="$curcontext" ret=1
_arguments -n : \
{-q,--quiet}'[Suppress status output]' \
'(-s --skip)'{-f,--force}'[Overwrite files that already exist]' \
'(-f --force)'{-s,--skip}'[Skip files that already exist]' \
{-b,--batch}'[Batch-mode: Skip interactive prompt]' \
{-v,--verbose}'[Verbose-mode: Detailed status output]' \
{-h,--help}'[Help message]' \
'1:commands:(cd clone generate list check refresh pull link track help)' \
'*::arguments:->arguments' \
&& ret=0
case $state in
(arguments)
curcontext="${curcontext%:*:*}:homeshick-arguments-$words[1]:"
case $words[1] in
(cd)
_arguments \
'1:castles:_homeshick_repo_folders' \
&& ret=0
;;
(check|refresh|pull)
_arguments \
'*: :_homeshick_castles' \
&& ret=0
;;
(link)
_arguments \
'*: :_homeshick_castles' \
&& ret=0
;;
(track)
_arguments \
'1: :_homeshick_castles' \
"*: :_path_files" \
&& ret=0
;;
(help)
_arguments \
':command:(cd clone generate list check refresh pull link track help)' \
&& ret=0
;;
esac
;;
esac
return ret
}
_homeshick_repo_folders() {
_path_files -/W $HOME/.homesick/repos
}
_homeshick_castles()
{
local castles repos="$HOME/.homesick/repos"
castles=()
for repo in $(find -L $repos -mindepth 2 -maxdepth 2 -type d -name .git); do
castles+=($(_homeshick_basename "${repo%/.git}"))
done
_describe -t castles 'castle' castles && ret=0
}
_homeshick_basename()
{
printf -- "${1##*/}"
}
_homeshick "$@"

View File

@ -0,0 +1,201 @@
#!bash
#
###############################################################################
#
# Bash completion for homeshick (https://github.com/andsens/homeshick).
#
# To use, add this line (or equivalent) to your .bashrc:
#
# source ~/.homesick/repos/homeshick/completions/homeshick-completion.bash
#
###############################################################################
#
# Copyright (C) 2013 Jeremy Lin
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
_homeshick_basename()
{
echo "${1##*/}"
}
_homeshick_castles()
{
local repos="$HOME/.homesick/repos"
# This should be a while loop like in link.sh, leave it for now though
# shellcheck disable=SC2044
for repo in $(find -L "$repos" -mindepth 2 -maxdepth 2 -type d -name .git); do
_homeshick_basename "${repo%/.git}"
done
}
_homeshick_complete_castles()
{
COMPREPLY=($(compgen -W "$(_homeshick_castles)" -- "$1"))
}
_homeshick_complete()
{
# The comments at the bottom of the file explain what's going on here.
if $_HOMESHICK_HAS_COMPOPT; then
compopt +o default +o nospace
COMPREPLY=()
else
COMPREPLY=('')
fi
local -r cmds='
cd
clone
generate
list
check
refresh
pull
link
track
help
symlink
updates
'
local -r short_opts='-q -s -f -b -v'
local -r long_opts='--quiet --skip --force --batch --verbose'
local -r protocols='file ftp ftps git http https rsync ssh'
# Scan through the command line and find the homeshick command
# (if present), as well as its expected position.
local cmd
local cmd_index=1 # Expected index of the command token.
local i
for (( i = 1; i < ${#COMP_WORDS[@]}; i++ )); do
local word="${COMP_WORDS[i]}"
case "$word" in
-*)
((cmd_index++))
;;
*)
cmd="$word"
break
;;
esac
done
local cur="${COMP_WORDS[COMP_CWORD]}"
if (( COMP_CWORD < cmd_index )); then
# Offer option completions.
case "$cur" in
--*)
COMPREPLY=($(compgen -W "$long_opts" -- "$cur"))
;;
-*)
COMPREPLY=($(compgen -W "$short_opts" -- "$cur"))
;;
*)
# Skip completion; we should never get here.
;;
esac
elif (( COMP_CWORD == cmd_index )); then
# Offer command name completions.
COMPREPLY=($(compgen -W "$cmds" -- "$cur"))
else
# Offer command argument completions.
case "$cmd" in
check | pull | link | symlink | updates)
# Offer one or more castle name completions.
_homeshick_complete_castles "$cur"
;;
cd)
# Offer exactly one castle name completion.
if (( COMP_CWORD == cmd_index + 1 )); then
_homeshick_complete_castles "$cur"
fi
;;
refresh)
# Offer a numerical completion for DAYS (mostly as a reminder
# that this argument should be a number), then castle name
# completions after that.
if (( COMP_CWORD == cmd_index + 1 )); then
COMPREPLY=({0..9})
else
_homeshick_complete_castles "$cur"
fi
;;
track)
# Offer one castle name completion, then filename completions
# after that.
if (( COMP_CWORD == cmd_index + 1 )); then
_homeshick_complete_castles "$cur"
else
$_HOMESHICK_HAS_COMPOPT && compopt -o default
# Let the default Readline filename completion take over.
COMPREPLY=()
fi
;;
clone)
# Offer an initial protocol completion.
if (( COMP_CWORD == cmd_index + 1 )); then
$_HOMESHICK_HAS_COMPOPT && compopt -o nospace
COMPREPLY=($(compgen -W "$protocols" -S '://' -- "$cur"))
fi
;;
help)
# Offer exactly one command name completion.
if (( COMP_CWORD == cmd_index + 1 )); then
COMPREPLY=($(compgen -W "$cmds" -- "$cur"))
fi
;;
*)
# Unknown command or unknowable argument.
;;
esac
fi
}
# The behavior of 'compgen -f' is pretty bizarre, in that if there's a
# directory 'foo', then compgen simply completes 'foo' and moves on, rather
# than offering to complete names of files underneath 'foo', which is what
# Bash (or Readline, actually) usually does.
#
# You can get the usual Readline filename completion behavior by doing
# 'complete -o default -F <func>', in which case the Readline filename
# completer is called if COMPREPLY=(). But this also means that you can no
# longer deny completion with COMPREPLY=() -- now Readline will always offer
# to complete a filename, even if this is inappropriate (e.g., the command
# takes no further arguments).
#
# In Bash 4.0 and above, there's a way to work around this problem. These
# versions have a 'compopt' builtin, which allows '-o default' to be enabled
# or disabled as needed. The workaround, then, is to disable '-o default'
# at the top of the completion function, and enable it as needed later on.
#
# For older Bash releases, there's a different workaround that doesn't work
# quite as well, but is still better than nothing. This workaround is to
# always enable '-o default' (which can't be disabled later), and then to use
# COMPREPLY=('') to semi-deny completion. That is, pressing Tab will just add
# space characters to the command line, rather than generating filenames.
#
if type compopt &>/dev/null; then
_HOMESHICK_HAS_COMPOPT=true
else
_HOMESHICK_HAS_COMPOPT=false
fi
complete -o default -F _homeshick_complete homeshick

View File

@ -0,0 +1,83 @@
function __fish_homeshick_strip_options
set cmd (commandline -opc)
if [ (count $cmd) -lt 2 ]
return 1
end
for item in $cmd[2..-1]
if [ (echo $item | sed 's/^\(.\).*/\1/') = "-" ]
continue
end
echo $item
end
end
function __fish_homeshick_needs_command
set cmd (__fish_homeshick_strip_options)
if [ (count $cmd) -eq 0 ]
return 0
end
return 1
end
function __fish_homeshick_using_command
set cmd (__fish_homeshick_strip_options)
if [ (count $cmd) -eq 0 ]
return 1
end
if [ $argv[1] = $cmd[1] ]
return 0
else
return 1
end
end
function __fish_homeshick_list_castles
set repos "$HOME/.homesick/repos"
for repo in (find -L $repos -mindepth 2 -maxdepth 2 -type d -name .git -exec dirname \{\} \;)
basename $repo
end
end
# general options
complete -f -c homeshick -s q -l quiet -d 'Suppress status output'
complete -f -c homeshick -s s -l skip -d 'Skip files that already exist'
complete -f -c homeshick -s f -l force -d 'Overwrite files that already exist'
complete -f -c homeshick -s b -l batch -d 'Batch-mode: Skip interactive prompts / Choose the default'
complete -f -c homeshick -s v -l verbose -d 'Verbose-mode: Detailed status output'
# cd
complete -f -c homeshick -n '__fish_homeshick_needs_command' -a cd -r -d 'Enter a castle'
complete -f -c homeshick -n '__fish_homeshick_using_command cd' -a '(__fish_homeshick_list_castles)' -d 'Castle'
# clone
complete -f -c homeshick -n '__fish_homeshick_needs_command' -a clone -r -d 'Clone URI as a castle for homeshick'
# generate
complete -f -c homeshick -n '__fish_homeshick_needs_command' -a generate -r -d 'Generate a castle repo'
# list
complete -f -c homeshick -n '__fish_homeshick_needs_command' -a list -d 'List cloned castles'
# check
complete -f -c homeshick -n '__fish_homeshick_needs_command' -a check -d 'Check a castle for updates'
complete -f -c homeshick -n '__fish_homeshick_using_command check' -a '(__fish_homeshick_list_castles)' -d 'Castle'
# refresh
complete -f -c homeshick -n '__fish_homeshick_needs_command' -a refresh -d 'Check if a castle needs refreshing'
complete -f -c homeshick -n '__fish_homeshick_using_command refresh' -a '(__fish_homeshick_list_castles)' -d 'Castle'
# pull
complete -f -c homeshick -n '__fish_homeshick_needs_command' -a pull -d 'Update a castle'
complete -f -c homeshick -n '__fish_homeshick_using_command pull' -a '(__fish_homeshick_list_castles)' -d 'Castle'
# link
complete -f -c homeshick -n '__fish_homeshick_needs_command' -a link -d 'Symlink all dotfiles from a castle'
complete -f -c homeshick -n '__fish_homeshick_using_command link' -a '(__fish_homeshick_list_castles)' -d 'Castle'
# track
complete -f -c homeshick -n '__fish_homeshick_needs_command' -a track -d 'Add a file to a castle'
complete -c homeshick -n '__fish_homeshick_using_command track' -a '(__fish_homeshick_list_castles)' -d 'Castle'
# help
complete -f -c homeshick -n '__fish_homeshick_needs_command' -a help -d 'Show usage of task'
complete -f -c homeshick -n '__fish_homeshick_using_command help' -a 'cd clone generate list check refresh pull link track' -d 'Task'

View File

@ -0,0 +1,13 @@
# EditorConfig is awesome: http://EditorConfig.org
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.sh]
indent_style = tab
[*.md]
max_line_length = 80

View File

@ -0,0 +1 @@
*.sublime-*

View File

@ -0,0 +1,29 @@
language: bash
dist: bionic
addons:
apt:
packages:
- shellcheck
- expect
- tcsh
before_script:
- git clone https://github.com/sstephenson/bats.git /tmp/bats
- mkdir -p /tmp/local
- bash /tmp/bats/install.sh /tmp/local
- export PATH=$PATH:/tmp/local/bin
- sudo apt-add-repository ppa:fish-shell/release-2 --yes
- sudo apt-get -qq update
- sudo apt-get -qq install fish
script:
- fish --version
- csh --version
- test/shellcheck
- bats --tap test/suites
notifications:
email:
on_success: never

View File

@ -0,0 +1,17 @@
# This helper script should be sourced via an alias, e.g.
#
# alias homeshick "source $HOME/.homesick/repos/homeshick/homeshick.csh"
#
if ( "$1" == "cd" && "x$2" != "x" ) then
if ( -d "$HOME/.homesick/repos/$2/home" ) then
cd "$HOME/.homesick/repos/$2/home"
else
cd "$HOME/.homesick/repos/$2"
endif
else
if ( $?HOMESHICK_DIR ) then
$HOMESHICK_DIR/bin/homeshick $*
else
$HOME/.homesick/repos/homeshick/bin/homeshick $*
endif
endif

View File

@ -0,0 +1,14 @@
# This script should be sourced in the context of your shell like so:
# source $HOME/.homesick/repos/homeshick/homeshick.fish
# Once the homeshick() function is defined, you can type
# "homeshick cd CASTLE" to enter a castle.
function homeshick
if test \( (count $argv) = 2 -a "$argv[1]" = "cd" \)
cd "$HOME/.homesick/repos/$argv[2]"
else if set -q HOMESHICK_DIR
eval $HOMESHICK_DIR/bin/homeshick (string escape -- $argv)
else
eval $HOME/.homesick/repos/homeshick/bin/homeshick (string escape -- $argv)
end
end

View File

@ -0,0 +1,15 @@
#!/usr/bin/env sh
# This script should be sourced in the context of your shell like so:
# source $HOME/.homeshick/repos/.homeshick/homeshick.sh
# Once the homeshick() function is defined, you can type
# "homeshick cd CASTLE" to enter a castle.
homeshick () {
if [ "$1" = "cd" ] && [ -n "$2" ]; then
# We want replicate cd behavior, so don't use cd ... ||
# shellcheck disable=SC2164
cd "$HOME/.homesick/repos/$2"
else
"${HOMESHICK_DIR:-$HOME/.homesick/repos/homeshick}/bin/homeshick" "$@"
fi
}

View File

@ -0,0 +1,59 @@
#!/bin/bash
function check {
local exit_status=$EX_SUCCESS
[[ ! $1 ]] && help_err check
local castle=$1
# repos is a global variable
# shellcheck disable=SC2154
local repo="$repos/$castle"
pending 'checking' "$castle"
castle_exists 'check' "$castle"
local ref
local branch
# Fetch the current branch name
ref=$(cd "$repo" && git symbolic-ref HEAD 2>/dev/null)
branch=${ref#refs/heads/}
# Get the upstream remote of that branch
local remote_name
local remote_url
remote_name=$(cd "$repo" && git config "branch.$branch.remote" 2>/dev/null)
remote_url=$(cd "$repo" && git config "remote.$remote_name.url" 2>/dev/null)
# Get the HEAD of the current branch on the upstream remote
local remote_head
remote_head=$(git ls-remote --heads "$remote_url" "$branch" 2>/dev/null | cut -f 1)
if [[ $remote_head ]]; then
local local_head
local_head=$(cd "$repo" && git rev-parse HEAD)
if [[ $remote_head == "$local_head" ]]; then
local git_status
git_status=$(cd "$repo" && git status --porcelain 2>/dev/null)
if [[ -z $git_status ]]; then
success 'up to date'
exit_status=$EX_SUCCESS
else
fail 'modified'
exit_status=$EX_MODIFIED
fi
else
local merge_base
local checked_ref
merge_base=$(cd "$repo" && git merge-base "$remote_head" "$local_head" 2>/dev/null)
checked_ref=$(cd "$repo" && git rev-parse --verify "$remote_head" 2>/dev/null)
# inlining checked_ref result makes the code unreadable
# shellcheck disable=SC2181
if [[ $? == 0 && $merge_base != "" && $merge_base == "$checked_ref" ]]; then
fail 'ahead'
exit_status=$EX_AHEAD
else
fail 'behind'
exit_status=$EX_BEHIND
fi
fi
else
ignore 'uncheckable'
exit_status=$EX_UNAVAILABLE
fi
return "$exit_status"
}

View File

@ -0,0 +1,82 @@
#!/bin/bash
function clone {
[[ ! $1 ]] && help_err clone
local git_repo=$1
is_github_shorthand "$git_repo"
if is_github_shorthand "$git_repo"; then
if [[ -e "$git_repo/.git" ]]; then
local msg="$git_repo also exists as a filesystem path,"
msg="${msg} use \`homeshick clone ./$git_repo' to circumvent the github shorthand"
warn 'clone' "$msg"
fi
git_repo="https://github.com/$git_repo.git"
fi
local repo_path
# repos is a global variable
# shellcheck disable=SC2154
repo_path=$repos"/"$(repo_basename "$git_repo")
pending 'clone' "$git_repo"
test -e "$repo_path" && err "$EX_ERR" "$repo_path already exists"
local git_out
version_compare "$GIT_VERSION" 1.6.5
if [[ $? != 2 ]]; then
git_out=$(git clone --recursive "$git_repo" "$repo_path" 2>&1) || \
err "$EX_SOFTWARE" "Unable to clone $git_repo. Git says:" "$git_out"
success
else
git_out=$(git clone "$git_repo" "$repo_path" 2>&1) || \
err "$EX_SOFTWARE" "Unable to clone $git_repo. Git says:" "$git_out"
success
pending 'submodules' "$git_repo"
git_out=$(cd "$repo_path"; git submodule update --init 2>&1) || \
err "$EX_SOFTWARE" "Unable to clone submodules for $git_repo. Git says:" "$git_out"
success
fi
return "$EX_SUCCESS"
}
function symlink_cloned_files {
local cloned_castles=()
while [[ $# -gt 0 ]]; do
local git_repo=$1
if is_github_shorthand "$git_repo"; then
git_repo="https://github.com/$git_repo.git"
fi
local castle
castle=$(repo_basename "$git_repo")
shift
local repo="$repos/$castle"
if [[ ! -d $repo/home ]]; then
continue;
fi
local num_files
num_files=$(find "$repo/home" -mindepth 1 -maxdepth 1 | wc -l | tr -dc "0123456789")
if [[ $num_files -gt 0 ]]; then
cloned_castles+=("$castle")
fi
done
ask_symlink "${cloned_castles[@]}"
return "$EX_SUCCESS"
}
# Convert username/repo into https://github.com/username/repo.git
function is_github_shorthand {
if [[ ! $1 =~ \.git$ && $1 =~ ^([0-9A-Za-z-]+/[0-9A-Za-z_\.-]+)$ ]]; then
return 0
fi
return 1
}
# Get the repo name from an URL
function repo_basename {
if [[ $1 =~ ^[^/:]+: ]]; then
# For scp-style syntax like '[user@]host.xz:path/to/repo.git/',
# remove the '[user@]host.xz:' part.
basename "${1#*:}" .git
else
basename "$1" .git
fi
}

View File

@ -0,0 +1,21 @@
#!/bin/bash
function generate {
[[ ! $1 ]] && help_err generate
local castle=$1
# repos is a global variable
# shellcheck disable=SC2154
local repo="$repos/$castle"
pending 'generate' "$castle"
if [[ -d $repo ]]; then
err "$EX_ERR" "The castle $castle already exists"
fi
mkdir "$repo"
local git_out
git_out=$(cd "$repo" && git init 2>&1) || \
err "$EX_SOFTWARE" "Unable to initialize repository $repo. Git says:" "$git_out"
mkdir "$repo/home"
success
return "$EX_SUCCESS"
}

View File

@ -0,0 +1,97 @@
#!/bin/bash
# help is used globally
# shellcheck disable=SC2120
function help {
if [[ $1 ]]; then
extended_help "$1"
exit "$EX_SUCCESS"
fi
printf "homes\e[1;34mh\e[0mick uses git in concert with symlinks to track your precious dotfiles.
Usage: homeshick [options] TASK
Tasks:
homeshick cd CASTLE # Enter a castle
homeshick clone URI.. # Clone URI as a castle for homeshick
homeshick generate CASTLE.. # Generate a castle repo
homeshick list # List cloned castles
homeshick check [CASTLE..] # Check a castle for updates
homeshick refresh [DAYS [CASTLE..]] # Check if a castle needs refreshing
homeshick pull [CASTLE..] # Update a castle
homeshick link [CASTLE..] # Symlinks all dotfiles from a castle
homeshick track CASTLE FILE.. # Add a file to a castle
homeshick help [TASK] # Show usage of a task
Aliases:
symlink # Alias to link
updates # Alias to check
Runtime options:
-q, [--quiet] # Suppress status output
-s, [--skip] # Skip files that already exist
-f, [--force] # Overwrite files that already exist
-b, [--batch] # Batch-mode: Skip interactive prompts / Choose the default
-v, [--verbose] # Verbose-mode: Detailed status output
Note:
To check, refresh, pull or symlink all your castles
simply omit the CASTLE argument
"
}
function extended_help {
case $1 in
cd)
printf "Enters a castle's home directory.\n"
printf "NOTE: For this to work, homeshick must be invoked via homeshick.{sh,csh,fish}.\n\n"
printf "Usage:\n homeshick cd CASTLE"
;;
clone)
printf "Clones URI as a castle for homeshick\n"
printf "Usage:\n homeshick clone URL.."
;;
generate)
printf "Generates a repo prepped for usage with homeshick\n"
printf "Usage:\n homeshick generate CASTLE.."
;;
list)
printf "Lists cloned castles\n"
printf "Usage:\n homeshick list"
;;
check|updates)
printf "Checks if a castle has been updated on the remote\n"
printf "Usage:\n homeshick %s [CASTLE..]" "$1"
;;
refresh)
printf "Checks if a castle has not been pulled in DAYS days.\n"
printf "The default is one week.\n"
printf "Usage:\n homeshick refresh [DAYS] [CASTLE..]"
;;
pull)
printf "Updates a castle. Also recurse into submodules.\n"
printf "Usage:\n homeshick pull [CASTLE..]"
;;
link|symlink)
printf "Symlinks all dotfiles from a castle\n"
printf "Usage:\n homeshick %s [CASTLE..]" "$1"
;;
track)
printf "Adds a file to a castle.\n"
printf "This moves the file into the castle and creates a symlink in its place.\n"
printf "Usage:\n homeshick track CASTLE FILE.."
;;
help)
printf "Shows usage of a task\n"
printf "Usage:\n homeshick help [TASK]"
;;
*)
# no args for help
# shellcheck disable=SC2119
help
;;
esac
printf "\n\n"
}

View File

@ -0,0 +1,128 @@
#!/bin/bash
function symlink {
[[ ! $1 ]] && help symlink
local castle=$1
castle_exists 'link' "$castle"
# repos is a global variable
# shellcheck disable=SC2154
local repo="$repos/$castle"
if [[ ! -d $repo/home ]]; then
ignore 'ignored' "$castle"
return "$EX_SUCCESS"
fi
# Run through the repo files using process substitution.
# The get_repo_files call is at the bottom of this loop.
# We set the IFS to nothing and the separator for `read' to NUL so that we
# don't separate files with newlines in their name into two iterations.
# `read's stdin comes from a third unused file descriptor because we are
# using the real stdin for prompting whether the user wants to
# overwrite or skip on conflicts.
while IFS= read -d $'\0' -r relpath <&3 ; do
local repopath="$repo/home/$relpath"
local homepath="$HOME/$relpath"
local rel_repopath
rel_repopath=$(create_rel_path "$(dirname "$homepath")/" "$repopath") || return $?
if [[ -e $homepath || -L $homepath ]]; then
# $homepath exists (but may be a dead symlink)
if [[ -L $homepath && $(readlink "$homepath") == "$rel_repopath" ]]; then
# $homepath symlinks to $repopath.
if $VERBOSE; then
ignore 'identical' "$relpath"
fi
continue
elif [[ $(readlink "$homepath") == "$repopath" ]]; then
# $homepath is an absolute symlink to $repopath
if [[ -d $repopath && ! -L $repopath ]]; then
# $repopath is a directory, but $homepath is a symlink -> legacy handling.
rm "$homepath"
else
# replace it with a relative symlink
rm "$homepath"
fi
else
# $homepath does not symlink to $repopath
# check if we should delete $homepath
if [[ -d $homepath && -d $repopath && ! -L $repopath ]]; then
# $repopath is a real directory while
# $homepath is a directory or a symlinked directory
# we do not take any action regardless of which it is.
if $VERBOSE; then
ignore 'identical' "$relpath"
fi
continue
elif $SKIP; then
ignore 'exists' "$relpath"
continue
elif ! $FORCE; then
prompt_no 'conflict' "$relpath exists" "overwrite?" || continue
fi
# Delete $homepath.
rm -rf "$homepath"
fi
fi
if [[ ! -d $repopath || -L $repopath ]]; then
# $repopath is not a real directory so we create a symlink to it
pending 'symlink' "$relpath"
ln -s "$rel_repopath" "$homepath"
else
pending 'directory' "$relpath"
mkdir "$homepath"
fi
success
# Fetch the repo files and redirect the output into file descriptor 3
done 3< <(get_repo_files "$repo")
return "$EX_SUCCESS"
}
# Fetches all files and folders in a repository that are tracked by git
# Works recursively on submodules as well
# Disable SC2154, we cannot do it inline where $homeshick is used.
# shellcheck disable=SC2154
function get_repo_files {
# Resolve symbolic links
# e.g. on osx $TMPDIR is in /var/folders...
# which is actually /private/var/folders...
# We do this so that the root part of $toplevel can be replaced
# git resolves symbolic links before it outputs $toplevel
local root
root=$(cd "$1" && pwd -P)
(
local path
while IFS= read -d $'\n' -r path; do
# Remove quotes from ls-files
# (used when there are newlines in the path)
path=${path/#\"/}
path=${path/%\"/}
# Check if home/ is a submodule
[[ $path == 'home' ]] && continue
# Remove the home/ part
path=${path/#home\//}
# Print the file path (NUL separated because \n can be used in filenames)
# Disable SC2059, using %s messes with the filename
# shellcheck disable=SC2059
printf "$path\0"
# Get the path of all the parent directories
# up to the repo root.
while true; do
path=$(dirname "$path")
# If path is '.' we're done
[[ $path == '.' ]] && break
# Print the path
# shellcheck disable=SC2059
printf "$path\0"
done
# Enter the repo, list the repo root files in home
# and do the same for any submodules
done < <(cd "$root" &&
git ls-files 'home/' &&
git submodule --quiet foreach --recursive \
"$homeshick/lib/submodule_files.sh \"$root\" \"\$toplevel\" \"\$path\"")
# Unfortunately we have to use an external script for `git submodule foreach'
# because versions prior to ~ 2.0 use `eval' to execute the argument.
# This somehow messes quite badly with string substitution.
) | sort -zu # sort the results and make the list unique (-u), NUL is the line separator (-z)
}

View File

@ -0,0 +1,18 @@
#!/bin/bash
function list {
while IFS= read -d $'\n' -r reponame ; do
local ref
local branch
# repos is a global variable
# shellcheck disable=SC2154
ref=$(cd "$repos/$reponame" && git symbolic-ref HEAD 2>/dev/null)
branch=${ref#refs/heads/}
local remote_name
local remote_url
remote_name=$(cd "$repos/$reponame" && git config "branch.$branch.remote" 2>/dev/null)
remote_url=$(cd "$repos/$reponame" && git config "remote.$remote_name.url" 2>/dev/null)
info "$reponame" "$remote_url"
done < <(list_castle_names)
return "$EX_SUCCESS"
}

View File

@ -0,0 +1,53 @@
#!/bin/bash
function pull {
[[ ! $1 ]] && help_err pull
local castle=$1
# repos is a global variable
# shellcheck disable=SC2154
local repo="$repos/$castle"
pending 'pull' "$castle"
castle_exists 'pull' "$castle"
if ! repo_has_upstream "$repo"; then
ignore 'no upstream' "Could not pull $castle, it has no upstream"
return "$EX_SUCCESS"
fi
local git_out
git_out=$(cd "$repo" && git pull 2>&1) || \
err "$EX_SOFTWARE" "Unable to pull $repo. Git says:" "$git_out"
version_compare "$GIT_VERSION" 1.6.5
if [[ $? != 2 ]]; then
git_out=$(cd "$repo" && git submodule update --recursive --init 2>&1) || \
err "$EX_SOFTWARE" "Unable update submodules for $repo. Git says:" "$git_out"
else
git_out=$(cd "$repo" && git submodule update --init 2>&1) || \
err "$EX_SOFTWARE" "Unable update submodules for $repo. Git says:" "$git_out"
fi
success
return "$EX_SUCCESS"
}
function symlink_new_files {
local updated_castles=()
while [[ $# -gt 0 ]]; do
local castle=$1
shift
local repo="$repos/$castle"
if [[ ! -d $repo/home ]]; then
continue;
fi
local git_out
local now
now=$(date +%s)
if ! git_out=$(cd "$repo" && git diff --name-only --diff-filter=A "HEAD@{(($now-$T_START+1)).seconds.ago}" HEAD -- home 2>/dev/null | wc -l 2>&1); then
continue # Ignore errors, this operation is not mission critical
fi
if [[ $git_out -gt 0 ]]; then
updated_castles+=("$castle")
fi
done
ask_symlink "${updated_castles[@]}"
return "$EX_SUCCESS"
}

View File

@ -0,0 +1,78 @@
#!/bin/bash
function refresh {
[[ ! $1 || ! $2 ]] && help_err refresh
local threshhold=$1
local castle=$2
# repos is a global variable
# shellcheck disable=SC2154
local fetch_head="$repos/$castle/.git/FETCH_HEAD"
pending 'checking' "$castle"
castle_exists 'refresh' "$castle"
if [[ -e $fetch_head ]]; then
local last_mod
last_mod=$(stat -c %Y "$fetch_head" 2> /dev/null || stat -f %m "$fetch_head")
if [[ $((T_START-last_mod)) -gt $threshhold ]]; then
fail "outdated"
return "$EX_TH_EXCEEDED"
else
success "fresh"
return "$EX_SUCCESS"
fi
else
fail "outdated"
return "$EX_TH_EXCEEDED"
fi
}
function pull_outdated {
local threshhold=$1; shift
local outdated_castles=()
while [[ $# -gt 0 ]]; do
local castle=$1; shift
local repo="$repos/$castle"
if [[ ! -d $repo ]]; then
# bogus argument, skip. User has already been warned by refresh()
continue
fi
local fetch_head="$repo/.git/FETCH_HEAD"
# When in interactive mode:
# No matter if we are going to pull the castles or not
# we reset the outdated ones by touching FETCH_HEAD
if [[ -e $fetch_head ]]; then
local last_mod
last_mod=$(stat -c %Y "$fetch_head" 2> /dev/null || stat -f %m "$fetch_head")
if [[ $((T_START-last_mod)) -gt $threshhold ]]; then
outdated_castles+=("$castle")
! $BATCH && touch "$fetch_head"
fi
else
outdated_castles+=("$castle")
! $BATCH && touch "$fetch_head"
fi
done
ask_pull "${outdated_castles[@]}"
return "$EX_SUCCESS"
}
function ask_pull {
if [[ $# -gt 0 ]]; then
if [[ $# == 1 ]]; then
msg="The castle $1 is outdated."
else
OIFS=$IFS
IFS=,
msg="The castles $* are outdated."
IFS=$OIFS
fi
if prompt_no 'refresh' "$msg" 'pull?'; then
# shellcheck source=lib/commands/pull.sh disable=SC2154
source "$homeshick/lib/commands/pull.sh"
for castle in "$@"; do
pull "$castle"
done
fi
fi
return "$EX_SUCCESS"
}

View File

@ -0,0 +1,109 @@
#!/bin/bash
function track {
[[ ! $1 || ! $2 ]] && help track
local castle=$1
local filename
filename=$(abs_path "$2")
if [[ $filename != $HOME/* ]]; then
err "$EX_ERR" "The file $filename must be in your home directory."
fi
# If the file is a dead symlink, we track it anyhow, hence the '! -L'
if [[ ! -e $filename && ! -L $filename ]]; then
err "$EX_ERR" "The file $filename does not exist."
fi
home_exists 'track' "$castle"
local files_to_track
files_to_track=$(find "$filename" -name .git -prune -o -not -type d -print)
if [[ -z $files_to_track ]]; then
ignore 'track' 'No files to track'
return "$EX_SUCCESS"
fi
# check-ignore was only added in 1.8.2
local check_ignore=false
version_compare "$GIT_VERSION" 1.8.2
[[ $? != 2 ]] && check_ignore=true
# repos is a global variable
# shellcheck disable=SC2154
local repo="$repos/$castle"
oldIFS=$IFS
IFS=$'\n'
for homepath in $files_to_track; do
IFS=$oldIFS
local relpath=${homepath#$HOME/}
pending 'track' "$relpath"
local relpath_in_repo="home/$relpath"
local repopath="$repo/$relpath_in_repo"
if [[ -e $repopath ]]; then
ignore 'exists' "The file $relpath is already being tracked."
continue
fi
if $check_ignore; then
if (cd "$repo" && git check-ignore --quiet "$relpath_in_repo") then
ignore 'ignored' "The file $relpath would be ignored by git."
continue
fi
fi
if [[ -e $repopath && $FORCE = false ]]; then
continue
prompt_no 'conflict' "$repopath exists" "overwrite?" || continue
fi
local remote_folder
remote_folder=$(dirname "$repopath")
mkdir -p "$remote_folder"
# Check if the file is a relative symlink, if so we don't move it but create
# an appropriate relative symlink instead that matches the new location
if [[ -L $homepath ]]; then
local target
target=$(readlink "$homepath")
if [[ $target =~ ^/ ]]; then
# It's an absolute symlink, just move it
mv -f "$homepath" "$repopath"
else
# Figure out the relative path from the symlink location in
# the castle to the path the symlink points at
# Convert the relative target into an absolute one
local abs_target
local target_dir
target_dir=$(abs_path "$(dirname "$homepath")")
abs_target=$(clean_path "$target_dir/$target")
# Get the relative path from the repopath dir to the target
local rel_targetpath
rel_targetpath=$(create_rel_path "$(dirname "$repopath")/" "$abs_target") || return $?
ln -s "$rel_targetpath" "$repopath"
# Remove $homepath so we can create the symlink further down
rm "$homepath"
fi
else
# Just a regular old file. Move it
mv -f "$homepath" "$repopath"
fi
# Create the symlink in place of the moved file (simulate what the link command does)
local rel_repopath
rel_repopath=$(create_rel_path "$(dirname "$homepath")/" "$repopath") || return $?
ln -s "$rel_repopath" "$homepath"
local git_out
git_out=$(cd "$repo" && git add "$relpath_in_repo" 2>&1)
status=$?
if [[ $status == 128 && $check_ignore == false ]]; then
# Currently our only option with git < 1.8.2, we can't be sure some other error hasn't occurred
ignore 'ignored' "The file $relpath would be ignored by git."
mv -f "$repopath" "$homepath"
continue
elif [[ $status != 0 ]]; then
fail 'track' "Unable to add file to git. Git says: $git_out"
exit
fi
success
done
return "$EX_SUCCESS"
}

View File

@ -0,0 +1,21 @@
#!/usr/bin/env bash
# This script is meant to be used in conjunction with
# `git submodule foreach'.
# It runs outputs all files tracked by a submodule.
# The paths are outputted relative to $root
root="$1"
toplevel="$2"
path="$3"
# toplevel/path relative to root
repo=${toplevel/#$root/}/$path
# If we are at root, remove the slash in front
repo=${repo/#\//}
# We are only interested in submodules under home/
if [[ $repo =~ ^home ]]; then
# just let cd fail if the path does not exist
# shellcheck disable=2164
cd "$toplevel/$path"
# List the files and prefix every line
# with the relative repo path
git ls-files | sed "s#^#${repo//#/\\#}/#"
fi

View File

@ -0,0 +1,52 @@
#!/bin/bash
# constants file, disable SC2034
# shellcheck disable=SC2034
true
# List of semi-standard exit status codes
# Sources:
# A: http://tldp.org/LDP/abs/html/exitcodes.html#EXITCODESREF
# B: http://www.gnu.org/software/libc/manual/html_node/Exit-Status.html
# C: sysexits.h
EX_OK=0 # successful termination ## source: A
EX_SUCCESS=0 # successful termination ## source: A
EX_ERR=1 # Catchall for general errors ## source: A
# Don't use. Reserved for bash
EX_SHELL=2 # Misuse of shell builtins ## source: A
EX_USAGE=64 # command line usage error ## source: C
EX_DATAERR=65 # data format error ## source: C
EX_NOINPUT=66 # cannot open input ## source: C
EX_NOUSER=67 # addressee unknown ## source: C
EX_NOHOST=68 # host name unknown ## source: C
EX_UNAVAILABLE=69 # service unavailable ## source: C
EX_SOFTWARE=70 # internal software error ## source: C
EX_OSERR=71 # system error (e.g., can't fork) ## source: C
EX_OSFILE=72 # critical OS file missing ## source: C
EX_CANTCREAT=73 # can't create (user) output file ## source: C
EX_IOERR=74 # input/output error ## source: C
EX_TEMPFAIL=75 # temp failure; user is invited to retry ## source: C
EX_PROTOCOL=76 # remote error in protocol ## source: C
EX_NOPERM=77 # permission denied ## source: C
EX_CONFIG=78 # configuration error ## source: C
# Don't use. Reserved for bash
EX_NOEXEC=126 # Command invoked cannot execute ## source: A
EX_NOTFOUND=127 # "command not found" ## source: A
# These two are in direct conflict, don't use
EX_EXIT_ERR=128 # Invalid argument to exit ## source: A
EX_EXEC_FAIL=128 # Failed to execute subprocess ## source: B
EX_SIGTERM=130 # Script terminated by Control-C ## source: A
# Custom homeshick status codes (range: 79-113)
EX_AHEAD=85 # local HEAD is ahead of its upstream branch
EX_BEHIND=86 # local HEAD is behind its upstream branch
EX_TH_EXCEEDED=87 # Time since last repository update is larger than the threshhold
EX_MODIFIED=88 # local working directory has modified files

View File

@ -0,0 +1,219 @@
#!/bin/bash
function castle_exists {
local action=$1
local castle=$2
# repos is a global variable, disable SC2154
# shellcheck disable=SC2154
local repo="$repos/$castle"
if [[ ! -d $repo ]]; then
err "$EX_ERR" "Could not $action $castle, expected $repo to exist"
fi
}
function home_exists {
local action=$1
local castle=$2
local repo="$repos/$castle"
if [[ ! -d $repo/home ]]; then
err "$EX_ERR" "Could not $action $castle, expected $repo to contain a home folder"
fi
}
function list_castle_names {
while IFS= read -d $'\0' -r repo ; do
# Avoid using basename for a small speed-up
# See link, for why it's OK to use bash string substitution in this case
# https://github.com/andsens/homeshick/pull/181/files#r196206593
local reponame
reponame="${repo%/.git}"
reponame="${reponame##*/}"
printf "%s\n" "$reponame"
done < <(find -L "$repos" -mindepth 2 -maxdepth 2 -name .git -type d -print0 | sort -z)
return "$EX_SUCCESS"
}
# Converts any path to an absolute path.
# All path parts except the last one must exist.
# A pwd option can be given as the first argument (like -P to resolve symlinks).
# In order to resolve the last part of the path append "/." to it.
function abs_path {
local path
local pwd_opt=""
if [[ $# -eq 1 ]]; then
path=$1
else
pwd_opt=$1
path=$2
fi
local dir
dir=$(dirname "$path")
if [[ ! -e $dir ]]; then
printf "The parent directory '%s' does not exist" "$dir" >&2
return 1
fi
local real_dir
local base
real_dir=$(cd "$dir" >/dev/null && printf "%s" "$(pwd "$pwd_opt")") || return $?
base=$(basename "$path")
if [[ $base = "." ]]; then
printf "%s\n" "$real_dir"
elif [[ $base = "/" ]]; then
printf "/\n"
elif [[ $real_dir = "/" ]]; then
printf "/%s\n" "$base"
else
printf "%s/%s\n" "$real_dir" "$base"
fi
}
# Removes unnecessary path parts, such as '/./' and 'somedir/../' and trailing slashes
function clean_path {
local path=$1
# Split path into parts
local parts=()
local rest=$path
while [[ $rest != '.' && $rest != '/' ]]; do
parts+=("$(basename "$rest")")
rest=$(dirname "$rest")
done
# reverse $parts, it's a lot easier to follow the code below then
local new_parts=()
for (( idx=${#parts[@]}-1 ; idx>=0 ; idx-- )); do
new_parts+=("${parts[$idx]}")
done
parts=("${new_parts[@]}")
local left
local right
local omit_left
local omit_right
# Run through the $parts until we cannot reduce it any longer
while true; do
omit_left=false
omit_right=false
# Step through pair-wise, with the directory separator ('/') being what we iterate over
# (the array is reversed)
# We only do one change, then bail, so we can work on the new path
for i in "${!parts[@]}"; do
left=${parts[$i]}
if [[ $i -ne ${#parts[@]}-1 ]]; then
# There is no $right for the last element
right=${parts[$i+1]}
else
right=''
fi
if [[ $left = '.' ]]; then
# Remove '/./'
omit_left=true
break
fi
if [[ $right = '.' ]]; then
# Remove '/./'
omit_right=true
break
fi
if [[ $left != '..' && $right == '..' ]]; then
# Remove 'somedir/../'
omit_left=true
omit_right=true
break
fi
if [[ $i -eq 0 && $left = '..' && $path = /* ]]; then
# On absolute paths, remove '/../somedir'
omit_left=true
break
fi
done
new_parts=()
# Create new_parts, omitting $left and/or $right
for j in "${!parts[@]}"; do
[[ $omit_left = true && $j -eq $i ]] && continue
[[ $omit_right = true && $j -eq $i+1 ]] && continue
new_parts+=("${parts[$j]}")
done
parts=("${new_parts[@]}")
if [[ $omit_left = false && $omit_right = false ]]; then
break
fi
done
# Construct $new_path from $parts
local new_path=''
for part in "${parts[@]}"; do
if [[ -z $new_path ]]; then
# Prevent leading slash
new_path="$part"
else
new_path="$new_path/$part"
fi
done
# Add leading slash for absolute paths
if [[ $path = /* ]]; then
new_path="/$new_path"
fi
printf "%s\n" "$new_path"
}
# Determines the relative path from source_dir to target
# As in: "What would the symlink look like if a file in $source_dir linked to $target?"
# $source_dir is the directory in which the link resides.
# $target is the relative path from $source_dir to it.
# NOTE: You will get different results for the same parameters depending on
# which path parts of $source_dir and $target are symlinks.
# Example:
# $source_dir: "/root/alpha/beta" ("beta" links to "../../gamma")
# $target: "/root/delta/file"
# result: "../root/delta/file"
# If "/root/delta" were a symlink to "../gamma", the result would be "file"
function create_rel_path {
local source_dir=$1
local target=$2
# Resolve symlinks in $source_dir and the parents of $target
source_dir=$(abs_path -P "${source_dir%/}/.")/ || return $?
target=$(abs_path -P "$target") || return $?
# Make sure $prefix has a trailing slash
local prefix
prefix=$target/
# Find the common prefix of $source_dir and $target
while [[ ! ${source_dir:0:${#prefix}} = "$prefix" ]]; do
# Remove directory parts from prefix until we find the common directory
prefix=$(dirname "$prefix")
# Append the trailing slash, except for "/" (hence the %/)
prefix=${prefix%/}/
done
# The path from the common directory to the target is
# $target without the prefix
local target_path
# Check if $target = $prefix (without the trailing slash)
if [[ $target = "${prefix%/}" ]]; then
target_path=''
else
target_path=${target##$prefix}
fi
# The path from the common directory to the source_dir is
# $source_dir without the prefix
local source_dir_path
source_dir_path=${source_dir##$prefix}
# Determine the path from the source_dir to the common directory (consists only of ../)
local common_dir_path=''
while [[ $source_dir_path != '' && $source_dir_path != '.' ]]; do
source_dir_path=$(dirname "$source_dir_path")
if [[ ${#common_dir_path} -eq 0 ]]; then
common_dir_path=".."
else
common_dir_path="$common_dir_path/.."
fi
done
# The relative path is just the path from $source_dir to $common dir to the target
if [[ -n $common_dir_path && -n $target_path ]]; then
# Add dir separator if both paths are non-empty
printf "%s/%s" "$common_dir_path" "$target_path"
else
printf "%s%s" "$common_dir_path" "$target_path"
fi
}

View File

@ -0,0 +1,46 @@
#!/bin/bash
# Snatched from http://stackoverflow.com/questions/4023830/bash-how-compare-two-strings-in-version-format
function version_compare {
if [[ $1 == "$2" ]]; then
return 0
fi
local IFS=.
# shellcheck disable=SC2206
local i ver1=($1) ver2=($2)
# fill empty fields in ver1 with zeros
for ((i=${#ver1[@]}; i<${#ver2[@]}; i++)); do
ver1[i]=0
done
for ((i=0; i<${#ver1[@]}; i++)); do
if [[ -z ${ver2[i]} ]]; then
# fill empty fields in ver2 with zeros
ver2[i]=0
fi
if ((10#${ver1[i]} > 10#${ver2[i]})); then
return 1
fi
if ((10#${ver1[i]} < 10#${ver2[i]})); then
return 2
fi
done
return 0
}
function repo_has_upstream {
local repo=$1
# Check if the castle has an upstream remote
# Fetch the current branch name
local ref
local branch
ref=$(cd "$repo" && git symbolic-ref HEAD 2>/dev/null)
branch=${ref#refs/heads/}
# Get the upstream remote of that branch
local remote_name
remote_name=$(cd "$repo" && git config "branch.$branch.remote" 2>/dev/null)
if [[ -z $remote_name ]]; then
return "$EX_ERR"
fi
return "$EX_SUCCESS"
}

View File

@ -0,0 +1,80 @@
#!/bin/bash
# Define some colors
txtdef="\e[0m" # Revert to default
bldred="\e[1;31m" # Red - error
bldgrn="\e[1;32m" # Green - success
bldylw="\e[1;33m" # Yellow - warning
bldblu="\e[1;34m" # Blue - no action/ignored
bldcyn="\e[1;36m" # Cyan - pending action
bldwht="\e[1;37m" # White - info
function err {
local exit_status=$1
local reason="$2"
shift 2
if [[ $pending_status ]]; then
# no args for fail
# shellcheck disable=SC2119
fail
fi
status "$bldred" "error" "$reason" >&2
if [[ $# -gt 0 ]]; then
printf "%s\n" "$@" >&2
fi
exit "$exit_status"
}
function help_err {
# shellcheck source=lib/commands/help.sh disable=SC2154
source "$homeshick/lib/commands/help.sh"
extended_help "$1"
exit "$EX_USAGE"
}
function status {
if $TALK; then
printf "$1%13s$txtdef %s\n" "$2" "$3"
fi
}
function warn {
status "$bldylw" "$1" "$2"
}
function info {
status "$bldwht" "$1" "$2"
}
pending_status=''
pending_message=''
function pending {
pending_status="$1"
pending_message="$2"
if $TALK; then
printf "$bldcyn%13s$txtdef %s" "$pending_status" "$pending_message"
fi
}
# fail is used globally
# shellcheck disable=SC2120
function fail {
[[ $1 ]] && pending_status=$1
[[ $2 ]] && pending_message=$2
status "\r$bldred" "$pending_status" "$pending_message"
unset pending_status pending_message
}
function ignore {
[[ $1 ]] && pending_status=$1
[[ $2 ]] && pending_message=$2
status "\r$bldblu" "$pending_status" "$pending_message"
unset pending_status pending_message
}
function success {
[[ $1 ]] && pending_status=$1
[[ $2 ]] && pending_message=$2
status "\r$bldgrn" "$pending_status" "$pending_message"
unset pending_status pending_message
}

View File

@ -0,0 +1,82 @@
#!/bin/bash
function ask_symlink {
if [[ $# -gt 0 ]]; then
if [[ $# == 1 ]]; then
msg="The castle $1 has new files."
else
OIFS=$IFS
IFS=,
msg="The castles $* have new files."
IFS=$OIFS
fi
if prompt_no 'updates' "$msg" 'symlink?'; then
# shellcheck source=lib/commands/link.sh disable=SC2154
source "$homeshick/lib/commands/link.sh"
for castle in "$@"; do
symlink "$castle"
done
fi
fi
return "$EX_SUCCESS"
}
# Singleline prompt that stays on the same line even if you press enter.
# Automatically colors the line according to the answer the user gives.
# Currently homeshick only has prompts with "no" as the default,
# so there's no reason to implement prompt_yes right now
function prompt_no {
local OTALK=$TALK
# Disable the quiet flag while prompting in interactive mode
if ! $BATCH; then
TALK=true
fi
local status=$1
local message=$2
local prompt=$3
local result=-1
# global vars
# shellcheck disable=SC2154
status "$bldwht" "$status" "$message"
if ! $BATCH; then
pending "$prompt" "[yN] "
while true; do
local answer=""
local char=""
while true; do
read -s -n 1 -r char
if [[ $char == "" ]]; then
break
fi
printf "%c" "$char"
answer="${answer}${char}"
done
case $answer in
Y|y) result=0 ;;
N|n) result=1 ;;
"") result=2 ;;
esac
[[ $result -ge 0 ]] && break
for (( i=0; i<${#answer}; i++ )) ; do
printf "\b"
done
printf "%${#answer}s\r"
# global vars
# shellcheck disable=SC2154
pending "$pending_status" "$pending_message"
done
else
pending "$prompt" "BATCH - No"
result=2
fi
if [[ $result == 0 ]]; then
success
else
fail
fi
TALK=$OTALK
return $result
}

View File

@ -0,0 +1,5 @@
repos
home
nothome
shunit2-*
setup.sh

View File

@ -0,0 +1,59 @@
#!/usr/bin/env bash
set -e
SCRIPTDIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
export SCRIPTDIR
if [[ -n $1 ]]; then
if [[ $1 == '--help' ]]; then
printf "Run homeshick working copy interactively\n"
printf " Usage: interactive [shell]\n"
exit 0
else
if [[ $# -gt 1 ]]; then
printf "only one SHELL argument allowed\n" >&2
exit 64 # 64=$EX_USAGE
fi
_shell=$1
fi
fi
if [[ -z $_shell ]]; then
_shell=$SHELL
fi
# Replacement function for the run fn in bats
# Disable SC2034 for global variables
# shellcheck disable=SC2034
run() {
local e E T
[[ ! "$-" =~ e ]] || e=1
[[ ! "$-" =~ E ]] || E=1
[[ ! "$-" =~ T ]] || T=1
set +e
set +E
set +T
output="$("$@" 2>&1)"
status="$?"
# shellcheck disable=SC2206
IFS=$'\n' lines=($output)
[ -z "$e" ] || set -e
[ -z "$E" ] || set -E
[ -z "$T" ] || set -T
}
# shellcheck source=test/helper.bash
source "$SCRIPTDIR/helper.bash"
export_env_vars
setup_env
ln_homeshick
setup_script="$SCRIPTDIR/setup.sh"
if [[ -f $setup_script ]]; then
$setup_script
else
"$SCRIPTDIR/setup-default.sh"
fi
cd "$HOME"
/usr/bin/env "$_shell"
rm_structure

View File

@ -0,0 +1,3 @@
#!/usr/bin/env bash
SCRIPTDIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
bats "${SCRIPTDIR}/suites"

View File

@ -0,0 +1,16 @@
#!/usr/bin/env bash
# shellcheck disable=SC2016
printf '\nsource "$HOME/.homesick/repos/homeshick/homeshick.sh"' >> "$HOME/.bashrc"
cat > "$HOME/.bashrc" <<EOF
source "$HOME/.homesick/repos/homeshick/homeshick.sh"
source "\$HOME/.homesick/repos/homeshick/completions/homeshick-completion.bash"
EOF
cat > "$HOME/.zshrc" <<EOF
source "\$HOME/.homesick/repos/homeshick/homeshick.sh"
fpath=(\$HOME/.homesick/repos/homeshick/completions \$fpath)
autoload -U compinit
compinit
EOF
printf '\nalias homeshick source "$HOME/.homesick/repos/homeshick/homeshick.csh"' >> "$HOME/.cshrc"
mkdir -p "$HOME/.config/fish"
printf '\nsource "$HOME/.homesick/repos/homeshick/homeshick.fish"' >> "$HOME/.config/fish/config.fish"

View File

@ -0,0 +1,15 @@
#!/usr/bin/env bash
shopt -s extglob
shopt -s nullglob
shellcheck --shell=bash \
bin/homeshick \
lib/!(exit_status).sh \
{,lib/commands/,test/,test/fixtures/}*.{sh,bash} \
test/{run,interactive,shellcheck}
shellcheck --shell=sh homeshick.sh
shellcheck --shell=bash homeshick.sh
shellcheck --shell=dash homeshick.sh
shellcheck --shell=ksh homeshick.sh

View File

@ -0,0 +1,17 @@
#!/bin/bash
# shellcheck disable=2164
function fixture_123.abc() {
local git_username="Homeshick user"
local git_useremail="homeshick@example.com"
local weirdname="$REPO_FIXTURES/135.abc"
git init "$weirdname"
cd "$weirdname"
git config user.name "$git_username"
git config user.email "$git_useremail"
touch .gitkeep
git add .gitkeep
git commit -m 'Add file to repo with weird name'
}
fixture_123.abc > /dev/null

View File

@ -0,0 +1,82 @@
#!/bin/bash
# shellcheck disable=2164
function fixture_dotfiles() {
local git_username="Homeshick user"
local git_useremail="homeshick@example.com"
local dotfiles="$REPO_FIXTURES/dotfiles"
git init "$dotfiles"
cd "$dotfiles"
git config user.name "$git_username"
git config user.email "$git_useremail"
mkdir home
cd home
mkdir -p .config/foo.conf
cat > .config/foo.conf/a.conf <<EOF
#I am just a regular config file
[A]
LikesIceCream=True
EOF
cat > .config/bar.dir <<EOF
#And I am just a regular config file with a weird name
[B]
LikesCucumber=False
EOF
# Do not put anything in .config/foo
# it is meant to be a directory
# only containing other directories
mkdir -p .config/foo/bar
touch .config/foo/bar/baz.conf
git add .config
git commit -m 'Files added for my new dotfiles repo'
mkdir .ssh
cat > .ssh/known_hosts <<EOF
github.com,207.97.227.239 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
EOF
git add .ssh
git commit -m 'Share known_hosts across machines'
local st3_settings="Library/Application Support/Sublime Text 3/Packages/User"
mkdir -p "$st3_settings"
cat > "$st3_settings/Preferences.sublime-settings" <<EOF
{
"caret_style": "wide",
"default_line_ending": "unix",
"ensure_newline_at_eof_on_save": true,
"font_face": "Inconsolata-dz",
"rulers": [110],
"shift_tab_unindent": true,
"tab_size": 2
}
EOF
git add "$st3_settings/Preferences.sublime-settings"
git commit -m 'Added my Sublime Text 3 settings'
local dotfiles_vim_submodule="$REPO_FIXTURES/dotfiles_vim_submodule"
(
git init "$dotfiles_vim_submodule"
cd "$dotfiles_vim_submodule"
git config user.name "$git_username"
git config user.email "$git_useremail"
mkdir -p autoload
cat > autoload/pathogen.vim <<EOF
dummy pathogen autloader file
EOF
mkdir -p bundles/vim-git
cat > bundles/vim-git/README.md <<EOF
Just a random README for the dummy vim-git bundle
EOF
git add autoload bundles
git commit -m 'vim-git bundle for my vim config'
)
cd "$dotfiles"
git submodule add "$dotfiles_vim_submodule" home/.vim
git commit -m 'New vim configuration submodule'
}
fixture_dotfiles > /dev/null

View File

@ -0,0 +1,37 @@
#!/bin/bash
# shellcheck disable=2164
function fixture_module_files() {
local git_username="Homeshick user"
local git_useremail="homeshick@example.com"
local module_files="$REPO_FIXTURES/module-files"
git init "$module_files"
cd "$module_files"
git config user.name "$git_username"
git config user.email "$git_useremail"
local my_module="$REPO_FIXTURES/my_module"
(
git init "$my_module"
cd "$my_module"
git config user.name "$git_username"
git config user.email "$git_useremail"
cat > module_foo.conf <<EOF
# my module file
EOF
git add module_foo.conf
git commit -m 'module_foo file for new my_module repo'
)
git submodule add "$my_module"
git commit -m 'my_module (git submodule) added for my new module-files repo'
mkdir home
cd home
ln -s ../my_module .my_module
git add .my_module
git commit -m 'Files added for my new module-files repo'
}
fixture_module_files > /dev/null

View File

@ -0,0 +1,99 @@
#!/bin/bash
# shellcheck disable=2164
function fixture_nested_submodules() {
local git_username="Homeshick user"
local git_useremail="homeshick@example.com"
local nested_submodules="$REPO_FIXTURES/nested-submodules"
git init "$nested_submodules"
cd "$nested_submodules"
git config user.name "$git_username"
git config user.email "$git_useremail"
cat > info <<EOF
This is level0 of the nested submodule repo
EOF
git add info
git commit -m 'Add info file for level0 repo'
local level1="$REPO_FIXTURES/level1"
(
git init "$level1"
cd "$level1"
git config user.name "$git_username"
git config user.email "$git_useremail"
cat > info <<EOF
This is level1 of the nested submodule repo
EOF
git add info
git commit -m 'Add info file for level1 repo'
local level2="$REPO_FIXTURES/level2"
(
git init "$level2"
cd "$level2"
git config user.name "$git_username"
git config user.email "$git_useremail"
cat > info <<EOF
This is level2 of the nested submodule repo
EOF
git add info
git commit -m 'Add info file for level2 repo'
)
git submodule add "$level2" level2
git commit -m 'level2 submodule added for level1'
)
git submodule add "$level1" level1
git commit -m 'level1 submodule added for level0'
local home="$REPO_FIXTURES/home-for-nested-submodule"
(
git init "$home"
cd "$home"
git config user.name "$git_username"
git config user.email "$git_useremail"
cat > .info <<EOF
This is level "home" of the nested submodule repo
EOF
git add .info
git commit -m 'Add info file for home repo'
local homesub="$REPO_FIXTURES/home-subdir-for-nested-submodule"
(
git init "$homesub"
cd "$homesub"
git config user.name "$git_username"
git config user.email "$git_useremail"
cat > .info1 <<EOF
This is level "homesub" of the nested submodule repo
EOF
git add .info1
git commit -m 'Add info file for homesub repo'
local subsub="$REPO_FIXTURES/sub-subdir-for-nested-submodule"
(
git init "$subsub"
cd "$subsub"
git config user.name "$git_username"
git config user.email "$git_useremail"
cat > .info2 <<EOF
This is level "subsub" of the nested submodule repo
EOF
git add .info2
git commit -m 'Add info file for subsub repo'
)
git submodule add "$subsub" .subdir2
git commit -m 'subsub submodule added for level2'
)
git submodule add "$homesub" .subdir1
git commit -m 'homesub submodule added for level1'
)
git submodule add "$home" home
git commit -m 'home submodule added for level0'
}
fixture_nested_submodules > /dev/null

View File

@ -0,0 +1,20 @@
#!/bin/bash
# shellcheck disable=2164
function fixture_nodirs() {
local git_username="Homeshick user"
local git_useremail="homeshick@example.com"
local nodirs="$REPO_FIXTURES/nodirs"
git init "$nodirs"
cd "$nodirs"
git config user.name "$git_username"
git config user.email "$git_useremail"
mkdir home
cd home
touch .file1
git add .file1
git commit -m 'Add .file1 to test listing files'
}
fixture_nodirs > /dev/null

View File

@ -0,0 +1,50 @@
#!/bin/bash
# shellcheck disable=2164
function fixture_rc_files() {
local git_username="Homeshick user"
local git_useremail="homeshick@example.com"
local rc_files="$REPO_FIXTURES/rc-files"
git init "$rc_files"
cd "$rc_files"
git config user.name "$git_username"
git config user.email "$git_useremail"
mkdir home
cd home
cat > .bashrc <<EOF
#!/bin/bash
PS1='\[33[01;32m\]\u@\h\[33[00m\]:\[33[01;34m\]\w\'
EOF
git add .bashrc
git commit -m '.bashrc file for my new rc-files repo'
cat > "$NOTHOME/some-file" <<EOF
File with some content.
EOF
ln -s "$NOTHOME/some-file" symlinked-file
git add symlinked-file
git commit -m 'Added a symlinked file'
mkdir "$NOTHOME/some-directory"
ln -s "$NOTHOME/some-directory" symlinked-directory
git add symlinked-directory
git commit -m 'Added a symlinked directory'
ln -s "$NOTHOME/nonexistent" dead-symlink
git add dead-symlink
git commit -m 'Added a dead symlink'
# Create a branch with a slash in it.
# Used for list suite unit test testSlashInBranch()
git branch branch/with/slash
cat > .gitignore <<EOF
.DS_Store
*.swp
EOF
git add .gitignore
git commit -m 'Added .gitignore file'
}
fixture_rc_files > /dev/null

View File

@ -0,0 +1,35 @@
#!/bin/bash
# shellcheck disable=2164
function fixture_repo_with_spaces_in_name() {
local git_username="Homeshick user"
local git_useremail="homeshick@example.com"
local namewithspaces="${REPO_FIXTURES}/repo with spaces in name"
git init "$namewithspaces"
cd "$namewithspaces"
git config user.name "$git_username"
git config user.email "$git_useremail"
mkdir home
cd home
touch .repowithspacesfile
git add .repowithspacesfile
git commit -m 'Add file to repo with spaces in name'
touch ".file with spaces in name"
git add ".file with spaces in name"
mkdir ".folder with spaces in name"
touch ".folder with spaces in name/another file with spaces in its name"
git add ".folder with spaces in name/another file with spaces in its name"
touch ".crazy
file␇☺"
git add ".crazy
file␇☺"
git commit -m 'Add file with newline and all kinds of crazy characters in the name'
}
fixture_repo_with_spaces_in_name > /dev/null

View File

@ -0,0 +1,37 @@
#!/bin/bash
# shellcheck disable=2164
function fixture_submodule_outside_home() {
local git_username="Homeshick user"
local git_useremail="homeshick@example.com"
local submodule_outside_home_files="$REPO_FIXTURES/submodule-outside-home"
git init "$submodule_outside_home_files"
cd "$submodule_outside_home_files"
git config user.name "$git_username"
git config user.email "$git_useremail"
local the_submodule="$REPO_FIXTURES/the-submodule"
(
git init "$the_submodule"
cd "$the_submodule"
git config user.name "$git_username"
git config user.email "$git_useremail"
cat > somefile.conf <<EOF
some kind of file
EOF
git add somefile.conf
git commit -m 'added some kind of file to the-submodule'
)
git submodule add "$the_submodule"
git commit -m 'the-submodule (git submodule) added for my new module-files repo'
mkdir home
cd home
ln -s .. root
git add root
git commit -m 'recursive symlink added'
}
fixture_submodule_outside_home > /dev/null

View File

@ -0,0 +1,20 @@
#!/bin/bash
# shellcheck disable=2164
function fixture_symlinks() {
local git_username="Homeshick user"
local git_useremail="homeshick@example.com"
local symlinks="$REPO_FIXTURES/symlinks"
git init "$symlinks"
cd "$symlinks"
git config user.name "$git_username"
git config user.email "$git_useremail"
mkdir home
cd home
ln -s ../../../../file_in_homedir link_to_homedir_file
git add link_to_homedir_file
git commit -m 'Add file to test symlinking'
}
fixture_symlinks > /dev/null

View File

@ -0,0 +1,161 @@
#!/bin/bash
function export_env_vars {
if [[ -n $BATS_TEST_DIRNAME ]]; then
TESTDIR=$(cd "$BATS_TEST_DIRNAME/.." && pwd)
else
TESTDIR=$(cd "$SCRIPTDIR" && pwd)
fi
export TESTDIR
_TMPDIR=$(mktemp -d 2>/dev/null || mktemp -d -t homeshick)
export _TMPDIR
export REPO_FIXTURES="$_TMPDIR/repos"
export HOME="$_TMPDIR/home"
export NOTHOME="$_TMPDIR/nothome"
export HOMESICK="$HOME/.homesick"
export HOMESHICK_FN="homeshick"
local repo_dir
repo_dir=$(cd "$TESTDIR/.." && pwd)
export HOMESHICK_DIR=${HOMESHICK_DIR:-$repo_dir}
export HOMESHICK_FN_SRC_SH="$HOMESHICK_DIR/homeshick.sh"
export HOMESHICK_FN_SRC_FISH="$HOMESHICK_DIR/homeshick.fish"
export HOMESHICK_FN_SRC_CSH="$HOMESHICK_DIR/homeshick.csh"
export HOMESHICK_BIN="$HOMESHICK_DIR/bin/homeshick"
# Check if expect is installed
if type expect &>/dev/null; then
export EXPECT_INSTALLED=true
else
export EXPECT_INSTALLED=false
fi
}
function remove_coreutils_from_path {
# Check if coreutils is in PATH
system=$(uname -a)
if [[ $system =~ "Darwin" && ! $system =~ "AppleTV" ]]; then
if type brew &>/dev/null; then
coreutils_path=$(brew --prefix coreutils 2>/dev/null)/libexec/gnubin
if [[ -d $coreutils_path && $PATH == *$coreutils_path* ]]; then
if [[ -z $HOMESHICK_KEEP_PATH || $HOMESHICK_KEEP_PATH == false ]]; then
export PATH=${PATH//$coreutils_path/''}
export PATH=${PATH//'::'/':'} # Remove any left over colons
fi
fi
fi
fi
}
function mk_structure {
mkdir "$REPO_FIXTURES" "$HOME" "$NOTHOME"
}
function ln_homeshick {
local hs_repo=$HOMESICK/repos/homeshick
mkdir -p "$hs_repo"
local repo_dir
repo_dir=$(cd "$TESTDIR/.." && pwd)
ln -s "$repo_dir/homeshick.sh" "$hs_repo/homeshick.sh"
ln -s "$repo_dir/homeshick.csh" "$hs_repo/homeshick.csh"
ln -s "$repo_dir/homeshick.fish" "$hs_repo/homeshick.fish"
ln -s "$repo_dir/bin" "$hs_repo/bin"
ln -s "$repo_dir/lib" "$hs_repo/lib"
ln -s "$repo_dir/completions" "$hs_repo/completions"
}
function rm_structure {
# Make sure _TMPDIR wasn't unset
[[ -n $_TMPDIR ]] && rm -rf "$_TMPDIR"
}
function setup_env {
remove_coreutils_from_path
export_env_vars
mk_structure
# shellcheck source=homeshick.sh
source "$HOMESHICK_FN_SRC_SH"
}
function setup {
setup_env
}
function teardown {
rm_structure
}
function fixture {
local name=$1
if [[ ! -e "$REPO_FIXTURES/$name" ]]; then
# shellcheck disable=SC1090
source "$TESTDIR/fixtures/$name.sh"
fi
}
function castle {
local fixture_name=$1
fixture "$fixture_name"
$HOMESHICK_FN --batch clone "$REPO_FIXTURES/$fixture_name" > /dev/null
}
function is_symlink {
expected=$1
path=$2
target=$(readlink "$path")
[ "$expected" = "$target" ]
}
function get_inode_no {
stat -c %i "$1" 2>/dev/null || stat -f %i "$1"
}
# Snatched from http://stackoverflow.com/questions/4023830/bash-how-compare-two-strings-in-version-format
function version_compare {
if [[ $1 == "$2" ]]; then
return 0
fi
local IFS=.
# shellcheck disable=SC2206
local i ver1=($1) ver2=($2)
# fill empty fields in ver1 with zeros
for ((i=${#ver1[@]}; i<${#ver2[@]}; i++)); do
ver1[i]=0
done
for ((i=0; i<${#ver1[@]}; i++)); do
if [[ -z ${ver2[i]} ]]; then
# fill empty fields in ver2 with zeros
ver2[i]=0
fi
if ((10#${ver1[i]} > 10#${ver2[i]})); then
return 1
fi
if ((10#${ver1[i]} < 10#${ver2[i]})); then
return 2
fi
done
return 0
}
function get_git_version {
if [[ -z $GIT_VERSION ]]; then
read -r _ _ GIT_VERSION _ < <(command git --version)
if [[ ! $GIT_VERSION =~ ([0-9]+)(\.[0-9]+){0,3} ]]; then
skip 'could not detect git version'
fi
fi
printf "%s" "$GIT_VERSION"
}
function commit_repo_state {
local repo=$1
(
# Let cd just fail
# shellcheck disable=SC2164
cd "$repo"
git config user.name "Homeshick user"
git config user.email "homeshick@example.com"
git add -A
git commit -m "Commiting Repo State from test helper.bash."
)
}

View File

@ -0,0 +1,95 @@
#!/usr/bin/env bats
load ../helper
function setup() {
setup_env
source "$HOMESHICK_DIR/lib/fs.sh"
}
@test 'test simple filepath' {
touch "$HOME/file"
local path
path=$(cd "$HOME" && abs_path file)
[ "$path" = "$HOME/file" ]
}
@test 'test filepath with spaces' {
mkdir "$HOME/folder with spaces"
touch "$HOME/folder with spaces/file"
local path
path=$(cd "$HOME" && abs_path folder\ with\ spaces/file)
[ "$path" = "$HOME/folder with spaces/file" ]
}
@test 'test filepath and filename with spaces' {
mkdir "$HOME/folder with spaces"
touch "$HOME/folder with spaces/file name with spaces"
local path
path=$(cd "$HOME" && abs_path folder\ with\ spaces/file\ name\ with\ spaces)
[ "$path" = "$HOME/folder with spaces/file name with spaces" ]
}
@test 'test folder' {
mkdir "$HOME/folder"
local path
path=$(cd "$HOME" && abs_path folder)
[ "$path" = "$HOME/folder" ]
}
@test 'test subfolder' {
mkdir -p "$HOME/folder/subfolder"
local path
path=$(cd "$HOME" && abs_path folder/subfolder)
[ "$path" = "$HOME/folder/subfolder" ]
}
@test 'test folders with spaces' {
mkdir -p "$HOME/folder with spaces/sub folder"
local path
path=$(cd "$HOME" && abs_path folder\ with\ spaces/sub\ folder)
[ "$path" = "$HOME/folder with spaces/sub folder" ]
}
@test 'test root' {
local path
path=$(abs_path /)
[ "$path" = "/" ]
}
@test 'test file in root' {
local path
path=$(abs_path /test)
[ "$path" = "/test" ]
}
@test 'test trailing slash' {
local path
path=$(abs_path /test/)
[ "$path" = "/test" ]
}
@test 'test trailing slashdot' {
mkdir "$HOME/test"
local path
path=$(cd "$HOME" && abs_path test/.)
[ "$path" = "$HOME/test" ]
}
@test 'test symlink' {
mkdir "$HOME/realdir"
ln -s realdir "$HOME/symlink"
local path
path=$(cd "$HOME" && abs_path symlink)
[ "$path" = "$HOME/symlink" ]
}
@test 'test symlink resolution' {
mkdir "$HOME/realdir"
ln -s realdir "$HOME/symlink"
local path
path=$(cd "$HOME" && abs_path -P symlink/.)
local abs_home
abs_home=$(cd "$HOME" >/dev/null && pwd -P)
[ "$path" = "$abs_home/realdir" ]
}

View File

@ -0,0 +1,100 @@
#!/usr/bin/env bats
load ../helper
@test 'cd to dotfiles castle' {
castle 'dotfiles'
local dotfiles_dir=$HOMESICK/repos/dotfiles
local result
result=$($HOMESHICK_FN cd dotfiles && pwd)
[ "$dotfiles_dir" = "$result" ]
}
@test 'cd to dotfiles castle subdirectory' {
castle 'dotfiles'
local dotfiles_dir=$HOMESICK/repos/dotfiles/home/.config/foo.conf
local result
result=$($HOMESHICK_FN cd dotfiles/home/.config/foo.conf && pwd)
[ "$dotfiles_dir" = "$result" ]
}
@test 'cd to my_module castle' {
castle 'module-files'
$HOMESHICK_FN --batch clone "$REPO_FIXTURES/my_module"
local my_module_dir=$HOMESICK/repos/my_module
local result
result=$($HOMESHICK_FN cd my_module && pwd)
[ "$my_module_dir" = "$result" ]
}
@test 'cd to nonexistent castle' {
local current_dir=$PWD
local result
result=$($HOMESHICK_FN cd non_existent 2>/dev/null; pwd)
[ "$current_dir" = "$result" ]
}
@test "cd'ing to nonexistent castle exits with errcode 1" {
run "$HOMESHICK_FN" cd non_existent
[ "$status" -eq 1 ]
}
@test 'cd to castle with spaces in its name' {
castle 'repo with spaces in name'
local spaces_repo_dir="$HOMESICK/repos/repo with spaces in name"
local result
result=$($HOMESHICK_FN cd repo\ with\ spaces\ in\ name && pwd)
[ "$spaces_repo_dir" = "$result" ]
}
@test 'cd to castle in sh' {
[ "$(type -t sh)" = "file" ] || skip "sh not installed"
castle 'dotfiles'
local dotfiles_dir=$HOMESICK/repos/dotfiles
cmd=". "$HOMESHICK_FN_SRC_SH" && $HOMESHICK_FN cd dotfiles && echo \$PWD"
local result
result=$( sh <<< "$cmd" 2>&1 )
[ "$dotfiles_dir" = "$result" ]
}
@test 'cd to castle in dash' {
[ "$(type -t dash)" = "file" ] || skip "dash not installed"
castle 'dotfiles'
local dotfiles_dir=$HOMESICK/repos/dotfiles
cmd=". "$HOMESHICK_FN_SRC_SH" && $HOMESHICK_FN cd dotfiles && echo \$PWD"
local result
result=$( dash <<< "$cmd" 2>&1 )
[ "$dotfiles_dir" = "$result" ]
}
@test 'cd to castle in csh' {
[ "$(type -t csh)" = "file" ] || skip "csh not installed"
$EXPECT_INSTALLED || skip 'expect not installed'
# in csh we can't alias a command and use that command on the same line
# % do_something; do_something_else
# is apparently different from
# % do_something
# % do_something_else
castle 'dotfiles'
local dotfiles_dir=$HOMESICK/repos/dotfiles
cat <<EOF | expect -f -
spawn csh
send "alias $HOMESHICK_FN source \"$HOMESHICK_FN_SRC_CSH\"\n"
send "$HOMESHICK_FN cd dotfiles\n"
send "pwd\n"
expect "*$dotfiles_dir*" {} default {exit 1}
send "exit\n"
expect EOF
EOF
}
@test 'cd to castle in fish' {
[ "$(type -t fish)" = "file" ] || skip "fish not installed"
castle 'dotfiles'
# fish $PWD has all symlinks resolved
local dotfiles_dir=$(cd "$HOMESICK/repos/dotfiles" && pwd -P)
cmd="source "$HOMESHICK_FN_SRC_FISH"; and $HOMESHICK_FN cd dotfiles; and pwd"
local result
result=$( fish <<< "$cmd" 2>&1 )
[ "$dotfiles_dir" = "$result" ]
}

View File

@ -0,0 +1,247 @@
#!/usr/bin/env bats
load ../helper
function add_new_file_to_castle {
cd "$HOMESICK/repos/rc-files" || return $?
touch homeshick_new_file_bats_test
}
function add_empty_folder_to_castle {
cd "$HOMESICK/repos/rc-files" || return $?
mkdir homeshick_new_folder_bats_test
}
function modify_file_in_castle {
cd "$HOMESICK/repos/rc-files" || return $?
echo modify >> "$(find home -type f | head -1)"
}
function delete_file_in_castle {
cd "$HOMESICK/repos/rc-files" || return $?
rm "$(find home -type f | head -1)"
}
function add_commit_to_castle {
cd "$HOMESICK/repos/rc-files" || return $?
git config user.name "Homeshick user"
git config user.email "homeshick@example.com"
cat >> home/.bashrc <<EOF
#!/bin/bash
PS1='\[33[01;32m\]\u@\h\[33[00m\]:\[33[01;34m\]\w\'
homeshick --batch refresh
EOF
git add home/.bashrc
git commit -m 'Added homeshick refresh check to .bashrc'
}
@test 'check an up to date castle' {
castle 'rc-files'
$HOMESHICK_FN check rc-files
$EXPECT_INSTALLED || skip 'expect not installed'
esc="\\u001b\\u005b"
cat <<EOF | expect -f -
spawn "$HOMESHICK_BIN" check rc-files
expect -ex "${esc}1;36m checking${esc}0m rc-files\r${esc}1;32m up to date${esc}0m rc-files\r\n" {} default {exit 1}
EOF
}
@test 'check an up to date castle with spaces in castle name' {
castle 'repo with spaces in name'
$HOMESHICK_FN check 'repo with spaces in name'
$EXPECT_INSTALLED || skip 'expect not installed'
esc="\\u001b\\u005b"
cat <<EOF | expect -f -
spawn "$HOMESHICK_BIN" check "repo with spaces in name"
expect -ex "${esc}1;36m checking${esc}0m repo with spaces in name\r${esc}1;32m up to date${esc}0m repo with spaces in name\r\n" {} default {exit 1}
EOF
}
@test 'check a castle that is behind upstream' {
castle 'rc-files'
(cd "$HOMESICK/repos/rc-files" && git reset --hard HEAD^1)
run "$HOMESHICK_FN" check rc-files
[ $status -eq 86 ] # EX_BEHIND
$EXPECT_INSTALLED || skip 'expect not installed'
esc="\\u001b\\u005b"
cat <<EOF | expect -f -
spawn "$HOMESHICK_BIN" check rc-files
expect -ex "${esc}1;36m checking${esc}0m rc-files\r${esc}1;31m behind${esc}0m rc-files\r\n" {} default {exit 1}
EOF
}
@test 'check a castle that is behind upstream with a new file' {
castle 'rc-files'
(cd "$HOMESICK/repos/rc-files" && git reset --hard HEAD^ && add_new_file_to_castle)
run "$HOMESHICK_FN" check rc-files
[ $status -eq 86 ] # EX_BEHIND
$EXPECT_INSTALLED || skip 'expect not installed'
esc="\\u001b\\u005b"
cat <<EOF | expect -f -
spawn "$HOMESHICK_BIN" check rc-files
expect -ex "${esc}1;36m checking${esc}0m rc-files\r${esc}1;31m behind${esc}0m rc-files\r\n" {} default {exit 1}
EOF
}
@test 'check a castle that is behind upstream with a modified file' {
castle 'rc-files'
(cd "$HOMESICK/repos/rc-files" && git reset --hard HEAD^1 && modify_file_in_castle)
run "$HOMESHICK_FN" check rc-files
[ $status -eq 86 ] # EX_BEHIND
$EXPECT_INSTALLED || skip 'expect not installed'
esc="\\u001b\\u005b"
cat <<EOF | expect -f -
spawn "$HOMESHICK_BIN" check rc-files
expect -ex "${esc}1;36m checking${esc}0m rc-files\r${esc}1;31m behind${esc}0m rc-files\r\n" {} default {exit 1}
EOF
}
@test 'check a castle that is behind upstream with a deleted file' {
castle 'rc-files'
(cd "$HOMESICK/repos/rc-files" && git reset --hard HEAD^1 && delete_file_in_castle)
run "$HOMESHICK_FN" check rc-files
[ $status -eq 86 ] # EX_BEHIND
$EXPECT_INSTALLED || skip 'expect not installed'
esc="\\u001b\\u005b"
cat <<EOF | expect -f -
spawn "$HOMESHICK_BIN" check rc-files
expect -ex "${esc}1;36m checking${esc}0m rc-files\r${esc}1;31m behind${esc}0m rc-files\r\n" {} default {exit 1}
EOF
}
@test 'check a castle that is ahead of upstream' {
castle 'rc-files'
(add_commit_to_castle)
run "$HOMESHICK_FN" check rc-files
[ $status -eq 85 ] # EX_AHEAD
$EXPECT_INSTALLED || skip 'expect not installed'
esc="\\u001b\\u005b"
cat <<EOF | expect -f -
spawn "$HOMESHICK_BIN" check rc-files
expect -ex "${esc}1;36m checking${esc}0m rc-files\r${esc}1;31m ahead${esc}0m rc-files\r\n" {} default {exit 1}
EOF
}
@test 'check a castle that is ahead of upstream with a new file' {
castle 'rc-files'
(add_commit_to_castle && add_new_file_to_castle)
run "$HOMESHICK_FN" check rc-files
[ $status -eq 85 ] # EX_AHEAD
$EXPECT_INSTALLED || skip 'expect not installed'
esc="\\u001b\\u005b"
cat <<EOF | expect -f -
spawn "$HOMESHICK_BIN" check rc-files
expect -ex "${esc}1;36m checking${esc}0m rc-files\r${esc}1;31m ahead${esc}0m rc-files\r\n" {} default {exit 1}
EOF
}
@test 'check a castle that is ahead of upstream with a modified file' {
castle 'rc-files'
(add_commit_to_castle; modify_file_in_castle)
run "$HOMESHICK_FN" check rc-files
[ $status -eq 85 ] # EX_AHEAD
$EXPECT_INSTALLED || skip 'expect not installed'
esc="\\u001b\\u005b"
cat <<EOF | expect -f -
spawn "$HOMESHICK_BIN" check rc-files
expect -ex "${esc}1;36m checking${esc}0m rc-files\r${esc}1;31m ahead${esc}0m rc-files\r\n" {} default {exit 1}
EOF
}
@test 'check a castle that is ahead of upstream with a deleted file' {
castle 'rc-files'
(add_commit_to_castle && delete_file_in_castle)
run "$HOMESHICK_FN" check rc-files
[ $status -eq 85 ] # EX_AHEAD
$EXPECT_INSTALLED || skip 'expect not installed'
esc="\\u001b\\u005b"
cat <<EOF | expect -f -
spawn "$HOMESHICK_BIN" check rc-files
expect -ex "${esc}1;36m checking${esc}0m rc-files\r${esc}1;31m ahead${esc}0m rc-files\r\n" {} default {exit 1}
EOF
}
@test 'check a castle with a new file' {
castle 'rc-files'
(add_new_file_to_castle)
run "$HOMESHICK_FN" check rc-files
[ $status -eq 88 ] # EX_MODIFIED
$EXPECT_INSTALLED || skip 'expect not installed'
esc="\\u001b\\u005b"
cat <<EOF | expect -f -
spawn "$HOMESHICK_BIN" check rc-files
expect -ex "${esc}1;36m checking${esc}0m rc-files\r${esc}1;31m modified${esc}0m rc-files\r\n" {} default {exit 1}
EOF
}
@test 'check a castle with a modified file' {
castle 'rc-files'
(modify_file_in_castle)
run "$HOMESHICK_FN" check rc-files
[ $status -eq 88 ] # EX_MODIFIED
$EXPECT_INSTALLED || skip 'expect not installed'
esc="\\u001b\\u005b"
cat <<EOF | expect -f -
spawn "$HOMESHICK_BIN" check rc-files
expect -ex "${esc}1;36m checking${esc}0m rc-files\r${esc}1;31m modified${esc}0m rc-files\r\n" {} default {exit 1}
EOF
}
@test 'check a castle with a deleted file' {
castle 'rc-files'
(delete_file_in_castle)
run "$HOMESHICK_FN" check rc-files
[ $status -eq 88 ] # EX_MODIFIED
$EXPECT_INSTALLED || skip 'expect not installed'
esc="\\u001b\\u005b"
cat <<EOF | expect -f -
spawn "$HOMESHICK_BIN" check rc-files
expect -ex "${esc}1;36m checking${esc}0m rc-files\r${esc}1;31m modified${esc}0m rc-files\r\n" {} default {exit 1}
EOF
}
@test 'check a castle with a new empty folder' {
castle 'rc-files'
(add_empty_folder_to_castle)
$HOMESHICK_FN check rc-files
$EXPECT_INSTALLED || skip 'expect not installed'
esc="\\u001b\\u005b"
cat <<EOF | expect -f -
spawn "$HOMESHICK_BIN" check rc-files
expect -ex "${esc}1;36m checking${esc}0m rc-files\r${esc}1;32m up to date${esc}0m rc-files\r\n" {} default {exit 1}
EOF
}
@test 'check a castle with a new folder and file in it' {
castle 'rc-files'
(
add_empty_folder_to_castle && \
cd "$HOMESICK/repos/rc-files" && \
touch homeshick_new_folder_bats_test/some_file
)
run "$HOMESHICK_FN" check rc-files
[ $status -eq 88 ] # EX_MODIFIED
$EXPECT_INSTALLED || skip 'expect not installed'
esc="\\u001b\\u005b"
cat <<EOF | expect -f -
spawn "$HOMESHICK_BIN" check rc-files
expect -ex "${esc}1;36m checking${esc}0m rc-files\r${esc}1;31m modified${esc}0m rc-files\r\n" {} default {exit 1}
EOF
}

View File

@ -0,0 +1,118 @@
#!/usr/bin/env bats
load ../helper
function setup() {
setup_env
source "$HOMESHICK_DIR/lib/fs.sh"
}
function test_clean_path() {
local path=$1
local expected=$2
local cleaned
cleaned=$(clean_path "$path")
if [[ $cleaned != "$expected" ]]; then
printf "got '%s'\n" "$cleaned"
[ "$expected" = "$cleaned" ]
fi
}
@test 'clean /home/user/somedir' {
test_clean_path '/home/user/somedir' '/home/user/somedir'
}
@test 'clean home/user/somedir' {
test_clean_path 'home/user/somedir' 'home/user/somedir'
}
@test 'clean /' {
test_clean_path '/' '/'
}
@test 'clean ""' {
test_clean_path '' ''
}
@test 'clean ../' {
test_clean_path '../' '..'
}
@test 'clean ../somedir' {
test_clean_path '../somedir' '../somedir'
}
@test 'clean /..' {
test_clean_path '/..' '/'
}
@test 'clean /../dir' {
test_clean_path '/../dir' '/dir'
}
@test 'clean somedir/../..' {
test_clean_path 'somedir/../..' '..'
}
@test 'clean /home/user/somedir/' {
test_clean_path '/home/user/somedir/' '/home/user/somedir'
}
@test 'clean /home/user/../user/somedir' {
test_clean_path '/home/user/../user/somedir' '/home/user/somedir'
}
@test 'clean /home/user/../user/somedir/' {
test_clean_path '/home/user/../user/somedir/' '/home/user/somedir'
}
@test 'clean /home/user/../../user/somedir/' {
test_clean_path '/home/user/../../user/somedir/' '/user/somedir'
}
@test 'clean /home/user/../anotherdir/../somedir/' {
test_clean_path '/home/user/../anotherdir/../somedir/' '/home/somedir'
}
@test 'clean /home/user/somedir/..' {
test_clean_path '/home/user/somedir/..' '/home/user'
}
@test 'clean /home/user/somedir/../' {
test_clean_path '/home/user/somedir/../' '/home/user'
}
@test 'clean /home/user/somedir/../../' {
test_clean_path '/home/user/somedir/../../' '/home'
}
@test 'clean /home/user/./somedir' {
test_clean_path '/home/user/./somedir' '/home/user/somedir'
}
@test 'clean /home/user/.' {
test_clean_path '/home/user/.' '/home/user'
}
@test 'clean /home/user/./' {
test_clean_path '/home/user/./' '/home/user'
}
@test 'clean /home/user/./somedir/../anotherdir/.' {
test_clean_path '/home/user/./somedir/../anotherdir/.' '/home/user/anotherdir'
}
@test 'clean /home/user../../somedir' {
clean_path '/home/user../../somedir'
test_clean_path '/home/user../../somedir' '/home/somedir'
}
@test 'clean /home/user../../some dir' {
clean_path '/home/user../../some dir'
test_clean_path '/home/user../../some dir' '/home/some dir'
}
@test 'clean /home/user name../../some dir' {
clean_path '/home/user name../../some dir'
test_clean_path '/home/user name../../some dir' '/home/some dir'
}

View File

@ -0,0 +1,75 @@
#!/usr/bin/env bats
load ../helper
@test 'clone with github shorthand, while matching path exists' {
$EXPECT_INSTALLED || skip 'expect not installed'
ping -c 1 -w 3 github.com || ping -c 1 -t 3 github.com || skip 'github not reachable'
(cd "$HOME" && git init andsens/rc-files)
open_bracket="\\u005b"
close_bracket="\\u005d"
esc="\\u001b$open_bracket"
(cd "$HOME" && cat <<EOF | expect -f -
spawn "$HOMESHICK_BIN" --batch clone andsens/rc-files
expect -ex "${esc}1;33m clone${esc}0m andsens/rc-files also exists as a filesystem path, use \`homeshick clone ./andsens/rc-files' to circumvent the github shorthand\r
${esc}1;36m clone${esc}0m https://github.com/andsens/rc-files.git\r${esc}1;32m clone${esc}0m https://github.com/andsens/rc-files.git\r
${esc}1;37m updates${esc}0m The castle rc-files has new files.\r
${esc}1;36m symlink?${esc}0m BATCH - No\r${esc}1;31m symlink?${esc}0m BATCH - No\r
" {} default {exit 1}
expect EOF
EOF
)
}
@test 'clone a repo' {
fixture 'rc-files'
$HOMESHICK_FN --batch clone "$REPO_FIXTURES/rc-files"
}
@test 'clone a repo with spaces in name' {
fixture 'repo with spaces in name'
$HOMESHICK_FN --batch clone "$REPO_FIXTURES/repo with spaces in name"
[ -d "$HOMESICK/repos/repo with spaces in name" ]
}
@test 'prompt for symlinking after clone' {
$EXPECT_INSTALLED || skip 'expect not installed'
fixture 'rc-files'
open_bracket="\\u005b"
close_bracket="\\u005d"
esc="\\u001b$open_bracket"
cat <<EOF | expect -f -
spawn "$HOMESHICK_BIN" clone "$REPO_FIXTURES/rc-files"
expect -ex "${esc}1;36m clone${esc}0m $REPO_FIXTURES/rc-files\r${esc}1;32m clone${esc}0m $REPO_FIXTURES/rc-files\r
${esc}1;37m updates${esc}0m The castle rc-files has new files.\r
${esc}1;36m symlink?${esc}0m ${open_bracket}yN${close_bracket} " {} default {exit 1}
send "y\n"
expect EOF
EOF
[ -f "$HOME/.bashrc" ]
}
@test 'clone repo with dot in its name' {
fixture '135.abc'
$HOMESHICK_FN --batch clone "$REPO_FIXTURES/135.abc"
[ -e "$HOMESICK/repos/135.abc" ]
}
@test 'recursive clone with git version >= 1.6.5' {
fixture 'nested-submodules'
GIT_VERSION=$(get_git_version)
run version_compare "$GIT_VERSION" 1.6.5
[[ $status == 2 ]] && skip 'git version too low'
$HOMESHICK_FN --batch clone "$REPO_FIXTURES/nested-submodules"
[ -e "$HOMESICK/repos/nested-submodules/level1/level2" ]
}
@test 'recursive clone with git version < 1.6.5' {
fixture 'nested-submodules'
GIT_VERSION=1.6.4 $HOMESHICK_FN --batch clone "$REPO_FIXTURES/nested-submodules"
[ -e "$HOMESICK/repos/nested-submodules/level1" ]
[ ! -e "$HOMESICK/repos/nested-submodules/level1/level2/info" ]
}

View File

@ -0,0 +1,85 @@
#!/usr/bin/env bats
load ../helper
function setup() {
setup_env
source "$HOMESHICK_DIR/lib/fs.sh"
}
function test_rel_path() {
local source_dir=$1
local target=$2
local expected=$3
local link
link=$(create_rel_path "$source_dir" "$target")
if [[ $link != "$expected" ]]; then
printf "got '%s'\n" "$link"
[ "$expected" = "$cleaned" ]
fi
}
@test 'relpath from . to file' {
touch "$HOME/file"
test_rel_path "$HOME/" "$HOME/file" "file"
}
@test 'relpath from folder/ to file' {
mkdir "$HOME/folder"
touch "$HOME/file"
test_rel_path "$HOME/folder/" "$HOME/file" "../file"
}
@test 'relpath from folder1/ to folder2/file' {
mkdir "$HOME/folder1" "$HOME/folder2"
touch "$HOME/folder2/file"
test_rel_path "$HOME/folder1/" "$HOME/folder2/file" "../folder2/file"
}
@test 'relpath from lvl1/lvl2/lvl3/ to lvl1-2/file' {
mkdir -p "$HOME/lvl1/lvl2/lvl3" "$HOME/lvl1-2"
touch "$HOME/lvl1-2/file"
test_rel_path "$HOME/lvl1/lvl2/lvl3/" "$HOME/lvl1-2/file" "../../../lvl1-2/file"
}
@test 'relpath from lvl1-2/ to lvl1/lvl2/lvl3/file' {
mkdir -p "$HOME/lvl1/lvl2/lvl3" "$HOME/lvl1-2"
touch "$HOME/lvl1/lvl2/lvl3/file"
test_rel_path "$HOME/lvl1-2/" "$HOME/lvl1/lvl2/lvl3/file" "../lvl1/lvl2/lvl3/file"
}
@test 'relpath from dir/ non-existent-file' {
run create_rel_path "$HOME/dir" "$HOME/non-existent-file" "non-existent-file"
}
@test 'relpath from dir/ dir2/non-existent-file' {
run create_rel_path "$HOME/dir" "$HOME/dir2/non-existent-file" "dir2/non-existent-file"
}
@test 'fail on non existent source_dir' {
run create_rel_path "$HOME/dir/dir2" "$HOME/file"
[ $status -eq 1 ]
}
@test 'relpath from inside symlinked dir on same level as real dir to file' {
mkdir -p "$HOME/realdir"
ln -s "realdir" "$HOME/symlinkdir"
touch "$HOME/file"
test_rel_path "$HOME/symlinkdir/" "$HOME/file" "../file"
}
@test 'relpath from inside symlinked dir on higher level than real dir to file' {
mkdir -p "$HOME/somedir/realdir"
ln -s "somedir/realdir" "$HOME/symlinkdir"
touch "$HOME/file"
test_rel_path "$HOME/symlinkdir/" "$HOME/file" "../../file"
}
@test 'relpath from inside symlinked dir on higher level than real dir to file where root dir is symlinked dir' {
mkdir "$HOME/root"
ln -s "root" "$HOME/symlinked-root"
mkdir -p "$HOME/symlinked-root/somedir/realdir"
ln -s "somedir/realdir" "$HOME/symlinked-root/symlinkdir"
touch "$HOME/symlinked-root/file"
test_rel_path "$HOME/symlinked-root/symlinkdir/" "$HOME/symlinked-root/file" "../../file"
}

View File

@ -0,0 +1,64 @@
#!/usr/bin/env bats
load ../helper
@test 'check non-existent and existent castle' {
castle 'rc-files'
run "$HOMESHICK_FN" check non-existent rc-files
[ $status -eq 1 ] # EX_ERR
}
@test 'clone non-existent and existent castle' {
fixture 'rc-files'
run "$HOMESHICK_FN" --batch clone "$REPO_FIXTURES/non-existent" "$REPO_FIXTURES/rc-files"
[ $status -eq 70 ] # EX_SOFTWARE
[ ! -d "$HOMESICK/repos/rc-files" ] # Should not exist, clone must fail early
}
@test 'generate castles with and without naming conflict' {
castle 'rc-files'
run "$HOMESHICK_FN" generate rc-files nonexistent
[ $status -eq 1 ] # EX_ERR
[ ! -d "$HOMESICK/repos/nonexistent" ] # Should not exist, generate must fail early
}
@test 'link non-existent and existent castle' {
castle 'rc-files'
run "$HOMESHICK_FN" link --batch rc-files nonexistent
[ $status -eq 1 ] # EX_ERR
}
@test 'pull non-existent and existent castle' {
castle 'rc-files'
local current_head
current_head=$(cd "$HOMESICK/repos/rc-files" && git rev-parse HEAD)
(cd "$HOMESICK/repos/rc-files" && git reset --hard HEAD^1)
run "$HOMESHICK_FN" pull --batch non-existent rc-files
[ $status -eq 1 ] # EX_ERR
local pulled_head
pulled_head=$(cd "$HOMESICK/repos/rc-files" && git rev-parse HEAD)
[ "$current_head" = "$pulled_head" ]
}
@test 'refresh non-existent and existent castle' {
castle 'rc-files'
$HOMESHICK_FN pull rc-files
run "$HOMESHICK_FN" refresh --batch 7 non-existent rc-files
[ $status -eq 1 ] # EX_ERR
}
@test 'track non-existent and existent file in castle' {
castle 'rc-files'
touch "$HOME/.newfiletotrack"
run "$HOMESHICK_FN" track rc-files "$HOME/.newfiletotrack" "$HOME/.nonexistent"
[ $status -eq 1 ] # EX_ERR
[ -f "$HOMESICK/repos/rc-files/home/.newfiletotrack" ]
}
@test 'track existent and non-existent file in castle' {
castle 'rc-files'
touch "$HOME/.newfiletotrack"
run "$HOMESHICK_FN" track rc-files non-existent .newfiletotrack
[ $status -eq 1 ] # EX_ERR
[ ! -f "$HOMESICK/repos/rc-files/home/.newfiletotrack" ]
}

View File

@ -0,0 +1,20 @@
#!/usr/bin/env bats
load ../helper
@test 'generate a castle' {
$HOMESHICK_FN --batch generate my_repo
[ -d "$HOMESICK/repos/my_repo" ]
}
@test 'generate a castle with spaces in name' {
$HOMESHICK_FN --batch generate my\ repo
[ -d "$HOMESICK/repos/my repo" ]
}
@test 'generate a castle with spaces in name with fish' {
[ "$(type -t fish)" = "file" ] || skip "fish not installed"
cmd="source "$HOMESHICK_FN_SRC_FISH"; and $HOMESHICK_FN --batch generate my\ repo"
fish <<< "$cmd" 2>&1
[ -d "$HOMESICK/repos/my repo" ]
}

View File

@ -0,0 +1,88 @@
#!/usr/bin/env bats
load ../helper
function setup() {
setup_env
source "$HOMESHICK_DIR/lib/commands/clone.sh"
}
@test 'git url basename: git@... .git' {
[ 'homeshick' = "$(repo_basename 'git@github.com:andsens/homeshick.git')" ]
}
@test 'git url basename: https://... .git' {
[ 'homeshick' = "$(repo_basename 'https://github.com/andsens/homeshick.git')" ]
}
@test 'git url basename: filepath' {
[ 'homeshick' = "$(repo_basename '/home/username/homeshick.git')" ]
}
@test 'git url basename: git@... .git (dot in reponame)' {
[ 'ebnf.vim' = "$(repo_basename 'git@github.com:vim-scripts/ebnf.vim.git')" ]
}
@test 'git url basename: ssh://... .git (dot in reponame)' {
[ 'ebnf.vim' = "$(repo_basename 'ssh://git@github.com/vim-scripts/ebnf.vim.git')" ]
}
@test 'git url basename: http://... .git (dot in reponame)' {
[ 'ebnf.vim' = "$(repo_basename 'https://github.com/vim-scripts/ebnf.vim.git')" ]
}
@test 'git url basename: numbers in reponame' {
[ 'spf13-vim' = "$(repo_basename 'https://github.com/vim-scripts/spf13/spf13-vim.git')" ]
}
@test 'git url basename: git@... (no .git extension)' {
[ 'homeshick' = "$(repo_basename 'git@github.com:andsens/homeshick')" ]
}
@test 'git url basename: git@... (dot in reponame, no .git extension)' {
[ 'ebnf.vim' = "$(repo_basename 'git@github.com:vim-scripts/ebnf.vim')" ]
}
@test 'git url basename: filepath (no .git extension)' {
[ 'homeshick' = "$(repo_basename '/home/username/homeshick')" ]
}
@test 'git url basename: filepath (colon in reponame)' {
[ 'dotfiles:emacs' = "$(repo_basename '/home/username/dotfiles:emacs.git')" ]
}
@test 'git url basename: filepath (no extension, colon in reponame)' {
[ 'dotfiles:emacs' = "$(repo_basename '/home/username/dotfiles:emacs.git')" ]
}
@test 'git url basename: filepath (no extension, colon in reponame & path)' {
[ 'dotfiles:emacs' = "$(repo_basename '/home/user:name/dotfiles:emacs')" ]
}
@test 'git url basename: git@... (no extension, no subfolder)' {
[ 'repo' = "$(repo_basename 'git@gitolite.example.com:repo')" ]
}
@test 'git url basename: git@... (no extension, no subfolder, colon in reponame)' {
[ 'dotfiles:emacs' = "$(repo_basename 'git@gitolite.example.com:dotfiles:emacs')" ]
}
@test 'git url basename: ssh:// (no extension, no subfolder)' {
[ 'repo' = "$(repo_basename 'ssh://git@gitolite.example.com/repo')" ]
}
@test 'git url basename: ssh:// (no extension, no subfolder, colon in reponame)' {
[ 'dotfiles:emacs' = "$(repo_basename 'ssh://git@gitolite.example.com/dotfiles:emacs')" ]
}
@test 'git url basename: http://... (with portnumber)' {
[ 'repo' = "$(repo_basename 'https://git.example.com:1234/repos/repo.git')" ]
}
@test 'git url basename: http://... (with portnumber, colon in subfoldername)' {
[ 'repo' = "$(repo_basename 'https://git.example.com:1234/dotfiles:emacs/repo.git')" ]
}
@test 'git url basename: http://... (with portnumber, colon in reponame)' {
[ 'repo:emacs' = "$(repo_basename 'https://git.example.com:1234/dotfiles/repo:emacs.git')" ]
}

View File

@ -0,0 +1,25 @@
#!/usr/bin/env bats
load ../helper
@test 'bash with homeshick_dir override' {
castle 'dotfiles'
local result
result=$( HOMESHICK_DIR=$_TMPDIR/nowhere "$HOMESHICK_FN" 2>&1 >/dev/null ) || true
[[ "$result" =~ "/nowhere/" ]]
}
@test 'fish with homeshick_dir override' {
[ "$(type -t fish)" = "file" ] || skip "fish not installed"
cmd="source "$HOMESHICK_FN_SRC_FISH"; set HOMESHICK_DIR \"$_TMPDIR/nowhere\"; $HOMESHICK_FN"
local result=$( fish <<< "$cmd" 2>&1 >/dev/null )
[[ "$result" =~ "/nowhere/" ]]
}
@test 'csh with homeshick_dir override' {
[ "$(type -t csh)" = "file" ] || skip "csh not installed"
cmd="set HOMESHICK_DIR=/nowhere; source \"${HOMESHICK_FN_SRC_CSH}\""
local result=$( csh <<< "$cmd" 2>&1 >/dev/null )
[[ "$result" =~ "/nowhere/" ]]
}

View File

@ -0,0 +1,230 @@
#!/usr/bin/env bats
load ../helper
@test 'link file with crazy name' {
castle 'repo with spaces in name'
$HOMESHICK_FN --batch link 'repo with spaces in name'
stat "$HOME/.crazy
file␇☺"
test -f "$HOME/.crazy
file␇☺"
}
@test 'do not fail when linking file with newline' {
castle 'rc-files'
test_filename="filename
newline"
touch "$HOMESICK/repos/rc-files/home/$test_filename"
commit_repo_state "$HOMESICK/repos/rc-files"
$HOMESHICK_FN --batch link rc-files
[ -L "$HOME/filename
newline" ]
is_symlink ".homesick/repos/rc-files/home/filename
newline" "$HOME/filename
newline"
}
@test 'link a file with spaces in its name' {
castle 'repo with spaces in name'
$HOMESHICK_FN --batch link "repo with spaces in name"
[ -f "$HOME/.file with spaces in name" ]
[ -f "$HOME/.folder with spaces in name/another file with spaces in its name" ]
}
@test 'only link submodule files inside home/' {
castle 'submodule-outside-home'
# '!' inverts the return value
! $HOMESHICK_FN --batch link submodule-outside-home 2>&1 | grep 'No such file or directory'
# This is the best I can do for testing.
# The failure does not cause any files to be created
# Ostensibly homeshick should exit with $? != 0 when linking fails, but it doesn't
}
@test 'link files of nested submodules' {
fixture 'nested-submodules'
GIT_VERSION=$(get_git_version)
run version_compare "$GIT_VERSION" 1.6.5
[[ $status == 2 ]] && skip 'git version too low'
$HOMESHICK_FN --batch clone "$REPO_FIXTURES/nested-submodules"
$HOMESHICK_FN --batch link nested-submodules
[ -f "$HOME/.subdir1/.subdir2/.info2" ]
}
@test "don't fail when linking uninitialized subrepos" {
fixture 'nested-submodules'
GIT_VERSION=$(get_git_version)
run version_compare "$GIT_VERSION" 1.6.5
[[ $status == 2 ]] && skip 'git version too low'
git clone "$REPO_FIXTURES/nested-submodules" "$HOMESICK/repos/nested-submodules"
[ -f "$HOMESICK/repos/nested-submodules/info" ]
$HOMESHICK_FN --batch link nested-submodules
[ ! -f "$HOMESICK/repos/nested-submodules/home/.info" ]
[ ! -f "$HOME/.info" ]
}
@test 'link submodule files' {
fixture 'nested-submodules'
GIT_VERSION=$(get_git_version)
run version_compare "$GIT_VERSION" 1.6.5
[[ $status == 2 ]] && skip 'git version too low'
$HOMESHICK_FN --batch clone "$REPO_FIXTURES/nested-submodules"
$HOMESHICK_FN --batch link nested-submodules
[ -f "$HOME/.info" ]
[ -f "$HOME/.subdir1/.info1" ]
}
@test 'link repo with no dirs in home/' {
castle 'nodirs'
$HOMESHICK_FN --batch link nodirs
[ -f "$HOME/.file1" ]
}
@test 'create file-less parent directories' {
castle 'dotfiles'
$HOMESHICK_FN --batch link dotfiles
[ -d "$HOME/.config/foo/bar" ]
}
@test 'symlink to a relative symlink' {
castle 'symlinks'
echo "test" > "$HOME/file_in_homedir"
$HOMESHICK_FN --batch link symlinks
[ "$(cat "$HOME/link_to_homedir_file")" = 'test' ]
}
@test 'overwrite prompt skipped when linking and --batch is on' {
castle 'rc-files'
touch "$HOME/.bashrc"
$HOMESHICK_FN --batch link rc-files
[ -f "$HOME/.bashrc" ]
[ ! -L "$HOME/.bashrc" ]
}
@test 'overwrite file with link when the prompt is answered with yes' {
$EXPECT_INSTALLED || skip 'expect not installed'
castle 'rc-files'
open_bracket="\\u005b"
close_bracket="\\u005d"
esc="\\u001b$open_bracket"
touch "$HOME/.bashrc"
cat <<EOF | expect -f -
spawn "$HOMESHICK_BIN" link rc-files
expect -ex "${esc}1;37m conflict${esc}0m .bashrc exists\r
${esc}1;36m overwrite?${esc}0m ${open_bracket}yN${close_bracket}" {} default {exit 1}
send "y\n"
expect EOF
EOF
[ -L "$HOME/.bashrc" ]
}
@test "don't overwrite file or prompt for it when linking and --skip is on" {
castle 'rc-files'
touch "$HOME/.bashrc"
$HOMESHICK_FN --skip link rc-files
[ -f "$HOME/.bashrc" -a ! -L "$HOME/.bashrc" ]
}
@test 'existing symlinks are not relinked when running link' {
castle 'module-files'
$HOMESHICK_FN --batch link module-files
local inode_before
inode_before=$(get_inode_no "$HOME/.my_module")
$HOMESHICK_FN --batch link module-files
local inode_after
inode_after=$(get_inode_no "$HOME/.my_module")
[ "$inode_before" -eq "$inode_after" ]
}
@test 'traverse into the folder structure when linking' {
castle 'dotfiles'
mkdir -p "$HOME/.config/bar.dir"
cat > "$HOME/.config/foo.conf" <<EOF
#I am just a regular foo.conf file
[foo]
A=True
EOF
cat > "$HOME/.config/bar.dir/bar.conf" <<EOF
#I am just a regular bar.conf file
[bar]
A=True
EOF
[ -f "$HOME/.config/foo.conf" ]
#.config/foo.conf should be overwritten by a directory of the same name
[ -d "$HOME/.config/bar.dir" ]
#.config/bar.dir should be overwritten by a file of the same name
$HOMESHICK_FN --batch --force link dotfiles
[ -d "$HOME/.config/foo.conf" ]
[ -f "$HOME/.config/bar.dir" ]
}
@test 'treat symlinked directories in the castle like files when linking' {
castle 'module-files'
$HOMESHICK_FN --batch link module-files
[ -L "$HOME/.my_module" ]
}
@test '.git directories are not symlinked' {
castle 'dotfiles'
$HOMESHICK_FN --batch link dotfiles
[ ! -e "$HOME/.vim/.git" ]
}
@test 'link a castle with spaces in its name' {
castle 'repo with spaces in name'
$HOMESHICK_FN --batch link repo\ with\ spaces\ in\ name
[ -f "$HOME/.repowithspacesfile" ]
}
@test 'pass multiple castlenames to link' {
castle 'rc-files'
castle 'dotfiles'
castle 'repo with spaces in name'
$HOMESHICK_FN --batch link rc-files dotfiles repo\ with\ spaces\ in\ name
is_symlink .homesick/repos/rc-files/home/.bashrc "$HOME/.bashrc"
is_symlink ../.homesick/repos/dotfiles/home/.ssh/known_hosts "$HOME/.ssh/known_hosts"
is_symlink ".homesick/repos/repo with spaces in name/home/.repowithspacesfile" "$HOME/.repowithspacesfile"
}
@test 'link all castles when no castle is specified' {
castle 'rc-files'
castle 'dotfiles'
castle 'repo with spaces in name'
$HOMESHICK_FN --batch link
is_symlink .homesick/repos/rc-files/home/.bashrc "$HOME/.bashrc"
is_symlink ../.homesick/repos/dotfiles/home/.ssh/known_hosts "$HOME/.ssh/known_hosts"
is_symlink ".homesick/repos/repo with spaces in name/home/.repowithspacesfile" "$HOME/.repowithspacesfile"
}
@test 'files ignored by git should not be linked' {
castle 'dotfiles'
touch "$HOMESICK/repos/dotfiles/home/shouldBeIgnored.txt"
cat > "$HOMESICK/repos/dotfiles/.gitignore" <<EOF
shouldBeIgnored.txt
EOF
commit_repo_state "$HOMESICK/repos/dotfiles"
$HOMESHICK_FN --batch link dotfiles
[ ! -L "$HOME/shouldBeIgnored.txt" ]
}
@test 'link file into directory that is a relative symlink' {
castle 'dotfiles'
mkdir -p "$HOME/two-levels/under-home"
ln -s "two-levels/under-home" "$HOME/.ssh"
$HOMESHICK_FN --batch link
is_symlink ../../.homesick/repos/dotfiles/home/.ssh/known_hosts "$HOME/two-levels/under-home/known_hosts"
[ -f "$HOME/.ssh/known_hosts" ]
}
@test 'link file into directory that is an absolute symlink' {
castle 'dotfiles'
mkdir -p "$HOME/two-levels/under-home"
ln -s "$HOME/two-levels/under-home" "$HOME/.config"
$HOMESHICK_FN --batch link
is_symlink ../../.homesick/repos/dotfiles/home/.config/bar.dir "$HOME/two-levels/under-home/bar.dir"
}

View File

@ -0,0 +1,259 @@
#!/usr/bin/env bats
load ../helper
## This is the linking table we are trying to verify:
## "not directory" can be a regular file or a symlink to either a file or a directory
## $HOME\repo | not directory | directory ##
## ---------------------|---------------|---------- ##
## nonexistent | link | mkdir ##
## symlink to repofile | identical | rm!&mkdir ##
## file | rm?&link | rm?&mkdir ##
## directory | rm?&link | identical ##
## directory (symlink) | rm?&link | identical ##
## First row: nonexistent
## First column: not directory
@test 'link file to nonexistent' {
castle 'rc-files'
$HOMESHICK_FN --batch link rc-files
is_symlink .homesick/repos/rc-files/home/.bashrc "$HOME/.bashrc"
}
@test 'link file symlink to nonexistent' {
castle 'rc-files'
$HOMESHICK_FN --batch link rc-files
is_symlink .homesick/repos/rc-files/home/symlinked-file "$HOME/symlinked-file"
}
@test 'link dir symlink to nonexistent' {
castle 'rc-files'
$HOMESHICK_FN --batch link rc-files
is_symlink .homesick/repos/rc-files/home/symlinked-directory "$HOME/symlinked-directory"
}
@test 'link dead symlink to nonexistent' {
castle 'rc-files'
$HOMESHICK_FN --batch link rc-files
is_symlink .homesick/repos/rc-files/home/dead-symlink "$HOME/dead-symlink"
}
## First row: nonexistent
## Second column: directory
@test 'link dir to nonexistent' {
castle 'dotfiles'
$HOMESHICK_FN --batch link dotfiles
[ ! -L "$HOME/.ssh" ]
[ -d "$HOME/.ssh" ]
}
## Second row: symlink to repofile
## First column: not directory
@test 'link file to reposymlink' {
castle 'rc-files'
$HOMESHICK_FN --batch link rc-files
local inode_before
inode_before=$(get_inode_no "$HOME/.bashrc")
$HOMESHICK_FN --batch --force link rc-files
local inode_after
inode_after=$(get_inode_no "$HOME/.bashrc")
is_symlink .homesick/repos/rc-files/home/.bashrc "$HOME/.bashrc"
[ "$inode_before" -eq "$inode_after" ]
}
@test 'link file symlink to reposymlink' {
castle 'rc-files'
$HOMESHICK_FN --batch link rc-files
local inode_before
inode_before=$(get_inode_no "$HOME/symlinked-file")
$HOMESHICK_FN --batch --force link rc-files
local inode_after
inode_after=$(get_inode_no "$HOME/symlinked-file")
is_symlink .homesick/repos/rc-files/home/symlinked-file "$HOME/symlinked-file"
[ "$inode_before" -eq "$inode_after" ]
}
@test 'link dir symlink to reposymlink' {
castle 'rc-files'
$HOMESHICK_FN --batch link rc-files
local inode_before
inode_before=$(get_inode_no "$HOME/symlinked-directory")
$HOMESHICK_FN --batch --force link rc-files
local inode_after
inode_after=$(get_inode_no "$HOME/symlinked-directory")
is_symlink .homesick/repos/rc-files/home/symlinked-directory "$HOME/symlinked-directory"
[ "$inode_before" -eq "$inode_after" ]
}
@test 'link dead symlink to reposymlink' {
castle 'rc-files'
$HOMESHICK_FN --batch link rc-files
local inode_before
inode_before=$(get_inode_no "$HOME/dead-symlink")
$HOMESHICK_FN --batch --force link rc-files
local inode_after
inode_after=$(get_inode_no "$HOME/dead-symlink")
is_symlink .homesick/repos/rc-files/home/dead-symlink "$HOME/dead-symlink"
[ "$inode_before" -eq "$inode_after" ]
}
## Second row: symlink to repofile
## Second column: directory
@test 'link legacy symlinks' {
castle 'dotfiles'
# Recreate the legacy scenario
ln -s "$HOMESICK/repos/dotfiles/home/.ssh" "$HOME/.ssh"
$HOMESHICK_FN --batch --force link dotfiles
# Without legacy handling if we were to run `file $HOME/.ssh/known_hosts` we would get
# .ssh/known_hosts: symbolic link in a loop
# The `test -e` is sufficient though
[ -e "$HOME/.ssh/known_hosts" ]
}
## Third row: file
## First column: not directory
@test 'link file to file' {
castle 'rc-files'
touch "$HOME/.bashrc"
$HOMESHICK_FN --batch --force link rc-files
is_symlink .homesick/repos/rc-files/home/.bashrc "$HOME/.bashrc"
$HOMESHICK_FN --batch --force link rc-files
is_symlink .homesick/repos/rc-files/home/.bashrc "$HOME/.bashrc"
}
@test 'link file symlink to file' {
castle 'rc-files'
touch "$HOME/symlinked-file"
$HOMESHICK_FN --batch --force link rc-files
is_symlink .homesick/repos/rc-files/home/symlinked-file "$HOME/symlinked-file"
}
@test 'link dir symlink to file' {
castle 'rc-files'
mkdir "$HOME/symlinked-directory"
$HOMESHICK_FN --batch --force link rc-files
is_symlink .homesick/repos/rc-files/home/symlinked-directory "$HOME/symlinked-directory"
}
@test 'link dead symlink to file' {
castle 'rc-files'
touch "$HOME/dead-symlink"
$HOMESHICK_FN --batch --force link rc-files
is_symlink .homesick/repos/rc-files/home/dead-symlink "$HOME/dead-symlink"
}
## Third row: file
## Second column: directory
@test 'link dir to file' {
castle 'dotfiles'
touch "$HOME/.ssh"
$HOMESHICK_FN --batch --force link dotfiles
[ -d "$HOME/.ssh" ]
[ ! -L "$HOME/.ssh" ]
}
## Fourth row: directory
## First column: not directory
@test 'link file to dir' {
castle 'rc-files'
mkdir "$HOME/.bashrc"
$HOMESHICK_FN --batch --force link rc-files
is_symlink .homesick/repos/rc-files/home/.bashrc "$HOME/.bashrc"
}
@test 'link file symlink to dir' {
castle 'rc-files'
mkdir "$HOME/symlinked-file"
$HOMESHICK_FN --batch --force link rc-files
is_symlink .homesick/repos/rc-files/home/symlinked-file "$HOME/symlinked-file"
}
@test 'link dir symlink to dir' {
castle 'rc-files'
mkdir "$HOME/symlinked-directory"
$HOMESHICK_FN --batch --force link rc-files
is_symlink .homesick/repos/rc-files/home/symlinked-directory "$HOME/symlinked-directory"
}
@test 'link dead symlink to dir' {
castle 'rc-files'
mkdir "$HOME/dead-symlink"
$HOMESHICK_FN --batch --force link rc-files
is_symlink .homesick/repos/rc-files/home/dead-symlink "$HOME/dead-symlink"
}
## Fourth row: directory
## Second column: directory
@test 'link dir to dir' {
castle 'dotfiles'
mkdir "$HOME/.ssh"
local inode_before
inode_before=$(get_inode_no "$HOME/.ssh")
$HOMESHICK_FN --batch --force link dotfiles
local inode_after
inode_after=$(get_inode_no "$HOME/.ssh")
[ "$inode_before" -eq "$inode_after" ]
[ -d "$HOME/.ssh" ]
[ ! -L "$HOME/.ssh" ]
}
## Fourth row: directory
## First column: not directory
@test 'link file to dir symlink' {
castle 'rc-files'
mkdir "$NOTHOME/symlink-target-dir"
ln -s "$NOTHOME/symlink-target-dir" "$HOME/.bashrc"
$HOMESHICK_FN --batch --force link rc-files
is_symlink .homesick/repos/rc-files/home/.bashrc "$HOME/.bashrc"
rm -rf "$NOTHOME/symlink-target-dir"
}
@test 'link file symlink to dir symlink' {
castle 'rc-files'
mkdir "$NOTHOME/symlink-target-dir"
ln -s "$NOTHOME/symlink-target-dir" "$HOME/symlinked-file"
$HOMESHICK_FN --batch --force link rc-files
is_symlink .homesick/repos/rc-files/home/symlinked-file "$HOME/symlinked-file"
rm -rf "$NOTHOME/symlink-target-dir"
}
@test 'link dir symlink to dir symlink' {
castle 'rc-files'
mkdir "$NOTHOME/symlink-target-dir"
ln -s "$NOTHOME/symlink-target-dir" "$HOME/symlinked-directory"
$HOMESHICK_FN --batch --force link rc-files
is_symlink .homesick/repos/rc-files/home/symlinked-directory "$HOME/symlinked-directory"
rm -rf "$NOTHOME/symlink-target-dir"
}
@test 'link dead symlink to dir symlink' {
castle 'rc-files'
mkdir "$NOTHOME/symlink-target-dir"
ln -s "$NOTHOME/symlink-target-dir" "$HOME/dead-symlink"
$HOMESHICK_FN --batch --force link rc-files
is_symlink .homesick/repos/rc-files/home/dead-symlink "$HOME/dead-symlink"
rm -rf "$NOTHOME/symlink-target-dir"
}
## Fourth row: directory
## Second column: directory
@test 'link dir to dir symlink' {
castle 'dotfiles'
mkdir "$NOTHOME/symlink-target-dir"
ln -s "$NOTHOME/symlink-target-dir" "$HOME/.ssh"
local inode_before
inode_before=$(get_inode_no "$HOME/.ssh")
$HOMESHICK_FN --batch --force link dotfiles
local inode_after
inode_after=$(get_inode_no "$HOME/.ssh")
is_symlink "$NOTHOME/symlink-target-dir" "$HOME/.ssh"
[ "$inode_before" -eq "$inode_after" ]
[ -d "$HOME/.ssh" ]
[ -L "$HOME/.ssh" ]
rm -rf "$NOTHOME/symlink-target-dir"
}

View File

@ -0,0 +1,70 @@
#!/usr/bin/env bats
load ../helper
@test 'list all castles' {
$EXPECT_INSTALLED || skip 'expect not installed'
castle 'rc-files'
castle 'dotfiles'
castle 'module-files'
castle 'repo with spaces in name'
esc="\\u001b\\u005b"
cat <<EOF | expect -f -
spawn "$HOMESHICK_BIN" list
expect -ex "${esc}1;37m dotfiles${esc}0m $REPO_FIXTURES/dotfiles\r
${esc}1;37m module-files${esc}0m $REPO_FIXTURES/module-files\r
${esc}1;37m rc-files${esc}0m $REPO_FIXTURES/rc-files\r
${esc}1;37mrepo with spaces in name${esc}0m $REPO_FIXTURES/repo with spaces in name\r\n" {} default {exit 1}
EOF
}
@test 'list castle with spaces in castlename' {
$EXPECT_INSTALLED || skip 'expect not installed'
castle 'repo with spaces in name'
esc="\\u001b\\u005b"
cat <<EOF | expect -f -
spawn "$HOMESHICK_BIN" list
expect -ex "${esc}1;37mrepo with spaces in name${esc}0m $REPO_FIXTURES/repo with spaces in name\r\n" {} default {exit 1}
EOF
}
@test 'list castle with altered upstream remote name' {
$EXPECT_INSTALLED || skip 'expect not installed'
castle 'rc-files'
(cd "$HOMESICK/repos/rc-files" && git remote rename origin nigiro)
esc="\\u001b\\u005b"
cat <<EOF | expect -f -
spawn "$HOMESHICK_BIN" list
expect -ex "${esc}1;37m rc-files${esc}0m $REPO_FIXTURES/rc-files\r\n" {} default {exit 1}
EOF
}
@test 'list castle with slash in branchname' {
$EXPECT_INSTALLED || skip 'expect not installed'
castle 'rc-files'
(cd "$HOMESICK/repos/rc-files" && git checkout branch/with/slash)
esc="\\u001b\\u005b"
cat <<EOF | expect -f -
spawn "$HOMESHICK_BIN" list
expect -ex "${esc}1;37m rc-files${esc}0m $REPO_FIXTURES/rc-files\r\n" {} default {exit 1}
EOF
}
@test 'list symlinks to castles' {
$EXPECT_INSTALLED || skip 'expect not installed'
castle 'rc-files'
(
cd "$HOMESICK/repos/" && \
mv rc-files "$_TMPDIR/rc-files" && \
ln -s "$_TMPDIR/rc-files" ./rc-files
)
esc="\\u001b\\u005b"
cat <<EOF | expect -f -
spawn "$HOMESHICK_BIN" list
expect -ex "${esc}1;37m rc-files${esc}0m $REPO_FIXTURES/rc-files\r\n" {} default {exit 1}
EOF
}

View File

@ -0,0 +1,40 @@
#!/usr/bin/env bats
load ../helper
@test 'invoke non existing command' {
run "$HOMESHICK_FN" commandthatdoesnexist
[ $status -eq 64 ] # EX_USAGE
}
@test 'verbose mode should print identical messages when linking' {
castle 'symlinks'
$HOMESHICK_FN link symlinks
$HOMESHICK_FN -v link symlinks | grep identical
}
@test 'normal verbosity should not print identical messages when linking' {
castle 'symlinks'
$HOMESHICK_FN link symlinks
[ ! $($HOMESHICK_FN link symlinks | grep identical) ]
}
@test 'link non-existent castle' {
run "$HOMESHICK_FN" link nonexistent
[ $status -eq 1 ] # EX_ERR
}
@test 'error should end with a single newline' {
$HOMESHICK_FN --batch generate existing-repo
output=$($HOMESHICK_FN --batch generate existing-repo 2>&1 | tr '\n' 'n')
run grep -q 'nn$' <<<"$output"
[ $status -eq 1 ]
}
@test 'fish function should not print errors when invoked without arguments' {
[ "$(type -t fish)" = "file" ] || skip "fish not installed"
cmd="source "$HOMESHICK_FN_SRC_FISH"; and $HOMESHICK_FN"
local stderr
stderr=$( fish <<< "$cmd" 2>&1 >/dev/null )
[ -z "$stderr" ]
}

View File

@ -0,0 +1,15 @@
#!/usr/bin/env bats
load ../helper
@test 'pull skips castles with no upstream remote' {
castle 'rc-files'
castle 'dotfiles'
# The dotfiles FETCH_HEAD should not exist after cloning
[ ! -e "$HOMESICK/repos/dotfiles/.git/FETCH_HEAD" ]
(cd "$HOMESICK/repos/rc-files" && git remote rm origin)
run "$HOMESHICK_FN" pull rc-files dotfiles
[ $status -eq 0 ] # EX_SUCCESS
# dotfiles FETCH_HEAD should exist if the castle was pulled
[ -e "$HOMESICK/repos/dotfiles/.git/FETCH_HEAD" ]
}

View File

@ -0,0 +1,104 @@
#!/usr/bin/env bats
load ../helper
@test 'refresh a freshly cloned castle' {
castle 'rc-files'
run "$HOMESHICK_FN" refresh -b rc-files
[ $status -eq 87 ] # EX_TH_EXCEEDED
$EXPECT_INSTALLED || skip 'expect not installed'
open_bracket="\\u005b"
close_bracket="\\u005d"
esc="\\u001b$open_bracket"
cat <<EOF | expect -f -
spawn "$HOMESHICK_BIN" refresh rc-files
expect -ex "${esc}1;36m checking${esc}0m rc-files\r${esc}1;31m outdated${esc}0m rc-files\r
${esc}1;37m refresh${esc}0m The castle rc-files is outdated.\r
${esc}1;36m pull?${esc}0m ${open_bracket}yN${close_bracket} " {} default {exit 1}
send "y\n"
expect EOF
EOF
}
@test 'refresh a castle that was just pulled' {
castle 'rc-files'
$HOMESHICK_FN pull rc-files
$HOMESHICK_FN -b refresh 7 rc-files
}
@test 'refresh a castle that was pulled 8 days ago' {
castle 'rc-files'
$HOMESHICK_FN pull rc-files # creates the FETCH_HEAD file
local fetch_head
local timestamp
fetch_head="$HOMESICK/repos/rc-files/.git/FETCH_HEAD"
system=`uname -a`
if [[ "$system" =~ "Linux" ]]; then
timestamp=$(date -d@$(($(date +%s)-6*86400)) '+%Y%m%d%H%M.%S')
else
# assume BSD system
timestamp=$(date -r $(($(date +%s)-6*86400)) '+%Y%m%d%H%M.%S')
fi
touch -t "$timestamp" "$fetch_head"
run "$HOMESHICK_FN" refresh -b rc-files
[ $status -eq 87 ] # EX_TH_EXCEEDED
$EXPECT_INSTALLED || skip 'expect not installed'
open_bracket="\\u005b"
close_bracket="\\u005d"
esc="\\u001b$open_bracket"
cat <<EOF | expect -f -
spawn "$HOMESHICK_BIN" refresh rc-files
expect -ex "${esc}1;36m checking${esc}0m rc-files\r${esc}1;31m outdated${esc}0m rc-files\r
${esc}1;37m refresh${esc}0m The castle rc-files is outdated.\r
${esc}1;36m pull?${esc}0m ${open_bracket}yN${close_bracket} " {} default {exit 1}
send "y\n"
expect EOF
EOF
}
@test 'refresh a castle and check that it is up to date' {
castle 'rc-files'
local current_head
current_head=$(cd "$HOMESICK/repos/rc-files" && git rev-parse HEAD)
(cd "$HOMESICK/repos/rc-files" && git reset --hard HEAD^1)
$EXPECT_INSTALLED || skip 'expect not installed'
open_bracket="\\u005b"
close_bracket="\\u005d"
esc="\\u001b$open_bracket"
cat <<EOF | expect -f -
spawn "$HOMESHICK_BIN" refresh rc-files
expect -ex "${esc}1;36m checking${esc}0m rc-files\r${esc}1;31m outdated${esc}0m rc-files\r
${esc}1;37m refresh${esc}0m The castle rc-files is outdated.\r
${esc}1;36m pull?${esc}0m ${open_bracket}yN${close_bracket} " {} default {exit 1}
send "y\n"
expect EOF
EOF
local pulled_head
pulled_head=$(cd "$HOMESICK/repos/rc-files" && git rev-parse HEAD)
[ "$current_head" = "$pulled_head" ]
}
@test 'refresh a castle with spaces in name' {
castle 'repo with spaces in name'
run "$HOMESHICK_FN" refresh -b 'repo with spaces in name'
[ $status -eq 87 ] # EX_TH_EXCEEDED
$EXPECT_INSTALLED || skip 'expect not installed'
open_bracket="\\u005b"
close_bracket="\\u005d"
esc="\\u001b$open_bracket"
cat <<EOF | expect -f -
spawn "$HOMESHICK_BIN" refresh "repo with spaces in name"
expect -ex "${esc}1;36m checking${esc}0m repo with spaces in name\r${esc}1;31m outdated${esc}0m repo with spaces in name\r
${esc}1;37m refresh${esc}0m The castle repo with spaces in name is outdated.\r
${esc}1;36m pull?${esc}0m ${open_bracket}yN${close_bracket} " {} default {exit 1}
send "y\n"
expect EOF
EOF
}

View File

@ -0,0 +1,319 @@
#!/usr/bin/env bats
load ../helper
@test 'track non-existent file' {
castle 'symlinks'
run "$HOMESHICK_FN" track symlinks "$HOME/non-existent-file"
[ $status -eq 1 ] # EX_ERR
}
@test 'track relative symlink in $HOME' {
castle 'symlinks'
echo "test" > "$HOME/some_file"
(cd "$HOME" && ln -s some_file link_to_some_file)
$HOMESHICK_FN track symlinks "$HOME/link_to_some_file"
[ "$(cat "$HOME/link_to_some_file")" = 'test' ]
is_symlink ../../../../some_file "$HOMESICK/repos/symlinks/home/link_to_some_file"
}
@test 'track absolute symlink' {
castle 'symlinks'
echo "test" > "$HOME/some_file"
(cd "$HOME" && ln -s "$HOME/some_file" link_to_some_file)
$HOMESHICK_FN track symlinks "$HOME/link_to_some_file"
[ "$(cat "$HOME/link_to_some_file")" = 'test' ]
is_symlink "$HOME/some_file" "$HOMESICK/repos/symlinks/home/link_to_some_file"
}
@test 'track relative symlink in deep folder structure to file outside $HOME' {
castle 'symlinks'
mkdir -p "$NOTHOME/some/folder/outside/home"
echo "test" > "$NOTHOME/some/folder/outside/home/some_file"
mkdir -p "$HOME/somedir/someotherdir/deep"
(cd "$HOME" && ln -s ../../../../nothome/some/folder/outside/home/some_file "$HOME/somedir/someotherdir/deep/link_to_some_file")
$HOMESHICK_FN track symlinks "$HOME/somedir/someotherdir/deep/link_to_some_file"
[ "$(cat "$HOME/somedir/someotherdir/deep/link_to_some_file")" = 'test' ]
local relpath='../../../../../../../../nothome/some/folder/outside/home/some_file'
is_symlink "$relpath" "$HOMESICK/repos/symlinks/home/somedir/someotherdir/deep/link_to_some_file"
}
@test 'track relative snakelike symlink' {
castle 'symlinks'
mkdir -p "$HOME/some/folder"
mkdir -p "$HOME/someother/folder"
echo "test" > "$HOME/someother/folder/some_file"
mkdir -p "$HOME/somethird/folder"
(cd "$HOME" && ln -s ../../some/folder/../../someother/folder/some_file "$HOME/somethird/folder/link_to_some_file")
$HOMESHICK_FN track symlinks "$HOME/somethird/folder/link_to_some_file"
[ "$(cat "$HOME/somethird/folder/link_to_some_file")" = 'test' ]
local relpath='../../../../../../someother/folder/some_file'
is_symlink "$relpath" "$HOMESICK/repos/symlinks/home/somethird/folder/link_to_some_file"
}
@test 'track dead symlink' {
castle 'symlinks'
(cd "$HOME" && ln -s some_file link_to_some_file)
$HOMESHICK_FN track symlinks "$HOME/link_to_some_file"
[ ! -e "$HOME/link_to_some_file" ]
is_symlink ../../../../some_file "$HOMESICK/repos/symlinks/home/link_to_some_file"
}
@test 'track absolute path' {
castle 'rc-files'
cat > "$HOME/.zshrc" <<EOF
homeshick --batch refresh
EOF
$HOMESHICK_FN track rc-files "$HOME/.zshrc"
[ -f "$HOMESICK/repos/rc-files/home/.zshrc" ]
[ -L "$HOME/.zshrc" ]
}
@test 'track path with spaces' {
castle 'rc-files'
cat > "$HOME/.path with spaces" <<EOF
homeshick --batch refresh
EOF
$HOMESHICK_FN track rc-files "$HOME/.path with spaces"
[ -f "$HOMESICK/repos/rc-files/home/.path with spaces" ]
[ -L "$HOME/.path with spaces" ]
}
@test 'track path with spaces (spaces in foldername and filename)' {
castle 'rc-files'
mkdir -p "$HOME/deep folder/structure/with spaces"
local file="$HOME/deep folder/structure/with spaces/.file with spaces"
cat > "$file" <<EOF
homeshick --batch refresh
EOF
$HOMESHICK_FN track rc-files "$HOME/deep folder/structure/with spaces/.file with spaces"
[ -f "$HOMESICK/repos/rc-files/home/deep folder/structure/with spaces/.file with spaces" ]
[ -L "$file" ]
}
@test 'track two paths with spaces' {
castle 'rc-files'
mkdir -p "$HOME/deep folder/structure/with spaces"
local file1="$HOME/deep folder/structure/with spaces/.file with spaces"
local file2="$HOME/.path with spaces"
cat > "$file1" <<EOF
homeshick --batch refresh
EOF
cat > "$file2" <<EOF
homeshick --batch refresh
EOF
$HOMESHICK_FN track rc-files "$HOME/.path with spaces" "$HOME/deep folder/structure/with spaces/.file with spaces"
[ -f "$HOMESICK/repos/rc-files/home/deep folder/structure/with spaces/.file with spaces" ]
[ -L "$file1" ]
[ -f "$HOMESICK/repos/rc-files/home/.path with spaces" ]
[ -L "$file2" ]
}
@test 'track relative path' {
castle 'rc-files'
cat > "$HOME/.zshrc" <<EOF
homeshick --batch refresh
EOF
(cd "$HOME" && $HOMESHICK_FN track rc-files .zshrc)
[ -f "$HOMESICK/repos/rc-files/home/.zshrc" ]
[ -L "$HOME/.zshrc" ]
}
@test 'track in castle with spaces' {
castle 'repo with spaces in name'
cat > "$HOME/.vimrc" <<EOF
My empty vim config
EOF
(cd "$HOME" && $HOMESHICK_FN track repo\ with\ spaces\ in\ name .vimrc)
local file="$HOMESICK/repos/repo with spaces in name/home/.vimrc"
[ -f "$file" ]
[ -L "$HOME/.vimrc" ]
}
@test 'disallow tracking outside homedir' {
castle 'rc-files'
cat > "$NOTHOME/some_other_file" <<EOF
homeshick should refuse to track this file
EOF
run "$HOMESHICK_FN" track rc-files "$NOTHOME/some_other_file"
[ $status -eq 1 ]
[ -e "$NOTHOME/some_other_file" ]
[ ! -L "$NOTHOME/some_other_file" ]
rm "$NOTHOME/some_other_file"
}
@test 'disallow overwrite when tracking' {
castle 'rc-files'
cat > "$HOME/.zshrc" <<EOF
homeshick --batch refresh
EOF
$HOMESHICK_FN track rc-files "$HOME/.zshrc"
[ -L "$HOME/.zshrc" ]
rm "$HOME/.zshrc"
cat > "$HOME/.zshrc" <<EOF
homeshick --batch refresh 7
EOF
$HOMESHICK_FN track rc-files "$HOME/.zshrc"
local tracked_file_size
tracked_file_size=$(stat -c %s "$HOMESICK/repos/rc-files/home/.zshrc" 2>/dev/null || \
stat -f %z "$HOMESICK/repos/rc-files/home/.zshrc")
[ 26 -eq "$tracked_file_size" ]
[ ! -L "$HOME/.zshrc" ]
}
@test 'disallow double tracking' {
castle 'rc-files'
cat > "$HOME/.zshrc" <<EOF
homeshick --batch refresh
EOF
$HOMESHICK_FN track rc-files "$HOME/.zshrc"
[ -L "$HOME/.zshrc" ]
$HOMESHICK_FN track rc-files "$HOME/.zshrc"
[ ! -L "$HOMESICK/repos/rc-files/home/.zshrc" ]
}
@test 'git add when tracked' {
castle 'rc-files'
cat > "$HOME/.zshrc" <<EOF
homeshick --batch refresh
EOF
$HOMESHICK_FN track rc-files "$HOME/.zshrc"
local git_status
git_status=$(cd "$HOMESICK/repos/rc-files" && git status --porcelain)
[ "A home/.zshrc" = "$git_status" ]
}
@test 'track folder' {
castle 'rc-files'
mkdir -p "$HOME/.somefolder/subfolder/stuff"
touch "$HOME/.somefolder/file1"
touch "$HOME/.somefolder/subfolder/file2"
touch "$HOME/.somefolder/subfolder/file3"
touch "$HOME/.somefolder/subfolder/stuff/file4"
$HOMESHICK_FN track rc-files "$HOME/.somefolder"
[ -e "$HOMESICK/repos/rc-files/home/.somefolder/file1" ]
[ -e "$HOMESICK/repos/rc-files/home/.somefolder/subfolder/file2" ]
[ -e "$HOMESICK/repos/rc-files/home/.somefolder/subfolder/file3" ]
[ -e "$HOMESICK/repos/rc-files/home/.somefolder/subfolder/stuff/file4" ]
}
@test "don't track ignored file" {
castle 'rc-files'
mkdir "$HOME/.folder"
touch "$HOME/.folder/somefile.swp"
$HOMESHICK_FN track rc-files "$HOME/.folder/somefile.swp"
[ ! -e "$HOMESICK/repos/rc-files/home/.folder/somefile.swp" ]
}
@test "don't track ignored files in folder" {
castle 'rc-files'
mkdir "$HOME/.folder"
touch "$HOME/.folder/somefile.swp"
touch "$HOME/.folder/trackthisthough"
$HOMESHICK_FN track rc-files "$HOME/.folder/"
[ ! -e "$HOMESICK/repos/rc-files/home/.folder/somefile.swp" ]
[ -e "$HOMESICK/repos/rc-files/home/.folder/trackthisthough" ]
}
@test 'track folder with spaces in name' {
castle 'rc-files'
mkdir -p "$HOME/.some folder/sub folder/stuff"
touch "$HOME/.some folder/file"
touch "$HOME/.some folder/sub folder/stuff/other file"
$HOMESHICK_FN track rc-files "$HOME/.some folder"
[ -e "$HOMESICK/repos/rc-files/home/.some folder/file" ]
[ -e "$HOMESICK/repos/rc-files/home/.some folder/sub folder/stuff/other file" ]
}
@test 'track with globbing' {
castle 'rc-files'
mkdir -p "$HOME/.folder/subfolder" "$HOME/.folder/subfolder2"
touch "$HOME/.folder/ignored.swp"
touch "$HOME/.folder/notglobbed.bash"
touch "$HOME/.folder/globbed2.exclude"
touch "$HOME/.folder/subfolder/this_as_well.bash"
touch "$HOME/.folder/subfolder2/and_this.bash"
$HOMESHICK_FN track rc-files "$HOME/.folder"/**/*.bash
[ ! -e "$HOMESICK/repos/rc-files/home/.folder/ignored.swp" ]
[ ! -e "$HOMESICK/repos/rc-files/home/.folder/notglobbed.bash" ]
[ ! -e "$HOMESICK/repos/rc-files/home/.folder/globbed2.exclude" ]
[ -e "$HOMESICK/repos/rc-files/home/.folder/subfolder/this_as_well.bash" ]
[ -e "$HOMESICK/repos/rc-files/home/.folder/subfolder2/and_this.bash" ]
}
@test 'track file in new folder with git version >= 1.8.2' {
GIT_VERSION=$(git --version | grep 'git version' | cut -d ' ' -f 3)
[[ ! $GIT_VERSION =~ ([0-9]+)(\.[0-9]+){0,3} ]] && skip 'could not detect git version'
run version_compare "$GIT_VERSION" 1.8.2
[[ $status == 2 ]] && skip 'git version too low'
castle 'rc-files'
mkdir "$HOME/.folder"
touch "$HOME/.folder/ignored.swp"
$HOMESHICK_FN track rc-files "$HOME/.folder/ignored.swp"
[ ! -e "$HOMESICK/repos/rc-files/home/.folder/ignored.swp" ]
[ ! -e "$HOMESICK/repos/rc-files/home/.folder" ]
}
@test 'track file in new folder with mocked git version < 1.8.2' {
# git > 2.3 exits with $?=1 when trying to track an ignored file.
# In lower versions this was 128, so the homeshick fallback check
# will not work.
# This is not a problem, it simply means that we can not emulate
# git < 1.8.2 behavior with git > 2.3.0 and must skip the test.
GIT_VERSION=$(git --version | grep 'git version' | cut -d ' ' -f 3)
[[ ! $GIT_VERSION =~ ([0-9]+)(\.[0-9]+){0,3} ]] && skip 'could not detect git version'
run version_compare "$GIT_VERSION" 2.3.0
[[ $status -lt 2 ]] && skip 'git version too high'
castle 'rc-files'
mkdir "$HOME/.folder"
touch "$HOME/.folder/ignored.swp"
GIT_VERSION=1.8.0 $HOMESHICK_FN track rc-files "$HOME/.folder/ignored.swp"
[ ! -e "$HOMESICK/repos/rc-files/home/.folder/ignored.swp" ]
[ -e "$HOMESICK/repos/rc-files/home/.folder" ]
}
@test 'track symlink in $HOME to $HOME' {
castle 'symlinks'
ln -s . .home
(cd "$HOME" && ln -s . .home)
$HOMESHICK_FN track symlinks "$HOME/.home"
is_symlink ../../../.. "$HOMESICK/repos/symlinks/home/.home"
}
@test 'track file pointing at hidden dir outside home' {
castle 'symlinks'
mkdir "$NOTHOME/..some"
ln -s ../nothome/..some/file "$HOME/.test"
$HOMESHICK_FN track symlinks "$HOME/.test"
is_symlink ../../../../../nothome/..some/file "$HOMESICK/repos/symlinks/home/.test"
}
@test 'track file pointing at hidden dir in snakelike fashion' {
castle 'symlinks'
ln -s .some/../.file "$HOME/.test"
$HOMESHICK_FN track symlinks "$HOME/.test"
is_symlink ../../../../.file "$HOMESICK/repos/symlinks/home/.test"
}
@test 'track regular file from a dir that is a symlink' {
castle 'symlinks'
mkdir -p "$HOME/two-levels/under-home"
ln -s "two-levels/under-home" "$HOME/symlinked-dir"
touch "$HOME/symlinked-dir/trackme"
$HOMESHICK_FN track symlinks "$HOME/symlinked-dir/trackme"
is_symlink ../../.homesick/repos/symlinks/home/symlinked-dir/trackme "$HOME/symlinked-dir/trackme"
}
@test 'track relative symlink from a dir that is a symlink' {
castle 'symlinks'
mkdir -p "$HOME/two-levels/under-home"
ln -s "two-levels/under-home" "$HOME/symlinked-dir"
touch "$HOME/linktome"
ln -s "../linktome" "$HOME/symlinked-dir/trackme"
$HOMESHICK_FN track symlinks "$HOME/symlinked-dir/trackme"
is_symlink ../../../../../linktome "$HOMESICK/repos/symlinks/home/symlinked-dir/trackme"
is_symlink ../../.homesick/repos/symlinks/home/symlinked-dir/trackme "$HOME/symlinked-dir/trackme"
}