[meta] Add homeshick

This commit is contained in:
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