Branch: master
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
executable file 164 lines (144 sloc) 4.44 KB
set -e -f -u -o pipefail
warn() {
echo -e "$@" 1>&2
die() {
warn "$@"
exit 1
find_sources() {
find "$1" -maxdepth 2 -mindepth 2 -type d -path "$2/.git" \
| sed -e 's,/.git$,,' \
| sort
dotfiles_sources() {
find_sources "${HOME}/src" '*dotfiles'
bin_sources() {
find_sources "${HOME}" '*bin'
find_sources "${HOME}/src" '*bin'
check_git_works() {
if ! git help status > /dev/null; then
printf "Failed to run git.\\n" >&2
printf "On MacOS install it with 'xcode-select --install'\\n" >&2
exit 1
origin_is_ssh() {
local repo_url
repo_url="$(git config --get remote.origin.url)"
if [[ "${repo_url:0:8}" == "https://" ]]; then
return 1
return 0
need_ssh_agent() {
local dir
for dir in $(bin_sources) $(dotfiles_sources); do
if (cd "${dir}" && origin_is_ssh); then
return 0
return 1
check_ssh_agent() {
if ! ssh-add -l > /dev/null; then
ssh-add -l || true
die "Problem with ssh agent"
# Can't check for ${HOME} because that's not consistent across machines; the
# forwarded key from my laptop will have ${HOME} from my laptop rather than
# ${HOME} on the destination.
if [[ -z "$(ssh-add -l | grep /.ssh/ || true)" ]]; then
die "No ssh keys found"
update_git_checkout_and_push() {
if [[ "$#" -ne 1 ]]; then
die "update_git_checkout_and_push needs one argument, got $#"
local dir="$1"
echo "Processing ${dir}"
# Refuse to do anything if there are uncommitted changes.
# If there are deleted submodules hanging around clean them up with:
# $ git clean -f -f -d
git check-local-copy-is-clean --ignore-unpushed-commits
# Always pull and update submodules. Note that if we fail before submodules
# are completely updated there will be uncomitted changes that must be dealt
# with manually.
git pull
git submodule init
git submodule update
# Removed submodules won't be deleted by git pull, so we need to clean up.
# -d removes directories, -f makes removal happen for normal files (but I
# don't think that's necessary for tracked deletions), and the second -f makes
# submodules be deleted.
# Note that if 1) a submodue was deleted and 2) this program crashes after the
# pull but before the clean happens, this program can't recover on a future
# run: manual cleanup is necessary.
git clean -f -f -d
# We can't push to https:// URLs.
if origin_is_ssh; then
git push
echo "Cannot push to https:// URLs."
main() {
if [[ "$#" -ne 0 ]]; then
warn "Unexpected arguments: $*"
die "Usage: $0"
# Ensure we have a connection to the ssh agent and keys loaded, if we have ssh
# origins.
if need_ssh_agent; then
echo "Running dotfiles to check for pre-existing diffs."
# First make sure we're up to date so that current diffs aren't mixed in with
# future diffs.
echo "Running dotfiles to check for unexpected files."
dotfiles -r
# Update bin dirs.
local orig_code
orig_code="$(cat "${BASH_SOURCE[0]}")"
local dir
for dir in $(bin_sources); do
(cd "${dir}" && update_git_checkout_and_push "${dir}")
# Check for unfinished changes; this happens when submodules are removed
# because git won't delete the submodule directory.
(cd "${dir}" && git check-local-copy-is-clean)
local new_code
new_code="$(cat "${BASH_SOURCE[0]}")"
if [[ "${orig_code}" != "${new_code}" ]]; then
echo "Restarting with updated tools"
exec "${BASH_SOURCE[0]}"
# Check for diffs again with possibly new binaries.
# Update all dotfiles.
for dir in $(dotfiles_sources); do
(cd "${dir}" && update_git_checkout_and_push "${dir}")
# Forcibly update. There is a risk that we clobber a file in our home
# directory with a file we've just checked out, but the alternative is for
# the user to forcibly update manually.
dotfiles -f
# Check for unfinished changes; this happens when submodules are removed
# because git won't delete the submodule directory. Do this after running
# dotfiles, otherwise there is a high chance that the next run of this
# program will find diffs and will exit.
(cd "${dir}" && git check-local-copy-is-clean)
# Delete unexpected files and directories.
dotfiles -X -f
# Make sure $HOME/bin is in $PATH to support running
# "ssh foo@bar update-dotfiles-and-bin"
export PATH
main "$@"