[meta] Add homeshick
This commit is contained in:
parent
9886e23d88
commit
700ad3cca1
61 changed files with 4555 additions and 0 deletions
4
Makefile
4
Makefile
|
@ -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
|
||||
|
|
76
dot_homesick/repos/homeshick/CONTRIBUTING.md
Normal file
76
dot_homesick/repos/homeshick/CONTRIBUTING.md
Normal 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).
|
20
dot_homesick/repos/homeshick/LICENSE
Normal file
20
dot_homesick/repos/homeshick/LICENSE
Normal 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.
|
43
dot_homesick/repos/homeshick/README.md
Normal file
43
dot_homesick/repos/homeshick/README.md
Normal file
|
@ -0,0 +1,43 @@
|
|||
homeshick [](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).
|
212
dot_homesick/repos/homeshick/bin/executable_homeshick
Normal file
212
dot_homesick/repos/homeshick/bin/executable_homeshick
Normal 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
|
72
dot_homesick/repos/homeshick/completions/_homeshick
Normal file
72
dot_homesick/repos/homeshick/completions/_homeshick
Normal 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 "$@"
|
|
@ -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
|
83
dot_homesick/repos/homeshick/completions/homeshick.fish
Normal file
83
dot_homesick/repos/homeshick/completions/homeshick.fish
Normal 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'
|
13
dot_homesick/repos/homeshick/dot_editorconfig
Normal file
13
dot_homesick/repos/homeshick/dot_editorconfig
Normal 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
|
||||
|
1
dot_homesick/repos/homeshick/dot_gitignore
Normal file
1
dot_homesick/repos/homeshick/dot_gitignore
Normal file
|
@ -0,0 +1 @@
|
|||
*.sublime-*
|
29
dot_homesick/repos/homeshick/dot_travis.yml
Normal file
29
dot_homesick/repos/homeshick/dot_travis.yml
Normal 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
|
17
dot_homesick/repos/homeshick/homeshick.csh
Normal file
17
dot_homesick/repos/homeshick/homeshick.csh
Normal 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
|
14
dot_homesick/repos/homeshick/homeshick.fish
Normal file
14
dot_homesick/repos/homeshick/homeshick.fish
Normal 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
|
15
dot_homesick/repos/homeshick/homeshick.sh
Normal file
15
dot_homesick/repos/homeshick/homeshick.sh
Normal 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
|
||||
}
|
59
dot_homesick/repos/homeshick/lib/commands/check.sh
Normal file
59
dot_homesick/repos/homeshick/lib/commands/check.sh
Normal 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"
|
||||
}
|
82
dot_homesick/repos/homeshick/lib/commands/clone.sh
Normal file
82
dot_homesick/repos/homeshick/lib/commands/clone.sh
Normal 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
|
||||
}
|
21
dot_homesick/repos/homeshick/lib/commands/generate.sh
Normal file
21
dot_homesick/repos/homeshick/lib/commands/generate.sh
Normal 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"
|
||||
}
|
97
dot_homesick/repos/homeshick/lib/commands/help.sh
Normal file
97
dot_homesick/repos/homeshick/lib/commands/help.sh
Normal 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"
|
||||
}
|
128
dot_homesick/repos/homeshick/lib/commands/link.sh
Normal file
128
dot_homesick/repos/homeshick/lib/commands/link.sh
Normal 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)
|
||||
}
|
18
dot_homesick/repos/homeshick/lib/commands/list.sh
Normal file
18
dot_homesick/repos/homeshick/lib/commands/list.sh
Normal 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"
|
||||
}
|
53
dot_homesick/repos/homeshick/lib/commands/pull.sh
Normal file
53
dot_homesick/repos/homeshick/lib/commands/pull.sh
Normal 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"
|
||||
}
|
78
dot_homesick/repos/homeshick/lib/commands/refresh.sh
Normal file
78
dot_homesick/repos/homeshick/lib/commands/refresh.sh
Normal 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"
|
||||
}
|
109
dot_homesick/repos/homeshick/lib/commands/track.sh
Normal file
109
dot_homesick/repos/homeshick/lib/commands/track.sh
Normal 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"
|
||||
}
|
|
@ -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
|
52
dot_homesick/repos/homeshick/lib/exit_status.sh
Normal file
52
dot_homesick/repos/homeshick/lib/exit_status.sh
Normal 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
|