#!/bin/bash
###
# Automatically commits data file changes to a git repository.
# Allows syncing tasks across machines; this behavior is disabled by default.
#
# Special care needs to be taken when combining this hook with Taskwarrior's
# "sync" command.  See [SYNC INTERACTION]
#
#
# Taskwarrior 2.4.3+ highly recommended for fancy commit messages and better
# error handling.
#
#
# There were some questionable coding choices made in this script.  Most of
# them were made in order to keep startup time and overhead low since we
# need to hook both on-launch and on-exit events.
#
# Please note that, with git push/pull activated, your startup time _will_
# suffer.  While the commit/push part is done in a background process, the same
# cannot be done for pull.  What this means for startup time is that it will
# increase according to the time it takes for a git pull to finish.
#
#
### SETUP
#
# [BASIC SETUP]
# Save this file as
#  ~/.task/hooks/on-exit_git.sh
# Make it executable:
#  $ chmod +x ~/.task/hooks/on-exit_git.sh
# NOTE: Due to the above-mentioned questionable coding choices, that name is
#       mandatory.  Do not use a different name.  The path, on the other hand,
#       is not hardcoded and can be adjusted.
#
# [GIT REPO SETUP]
# You can either only use a local git repo without syncing [A], or set up
# a remote tracking one for task synchronization [B].
#
# These are just examples, feel free to set this up any way you like.
# A private github repo would work just as well if you follow only the steps
# for setting up syncing on more Taskwarrior instances.
#
# [A] Local repo, no syncing
#   $ cd ~/.task
#   $ git init
#   $ echo '*' > .gitignore
#   $ git add -f .gitignore pending.data completed.data
#   $ git commit -m"Import current data"
#
# [B] Remote tracking repo with syncing
#  Set up a local bare repo to be accessed by other hosts through SSH.
#   $ mkdir ~/.taskdata.git
#   $ cd ~/.taskdata.git
#   $ git init --bare
#  Now do the steps outlined in [A], then
#   $ cd ~/.task
#   $ git remote add origin ~/.taskdata.git
#   $ git push origin master
#
#  To set up syncing with another Taskwarrior instance, log in to the other
#  machine, then:
#   [WITH PREEXISTING DATA]
#    Follow [BASIC SETUP].  Then:
#     $ cd ~/.task
#     $ git init
#     $ git remote add origin user@host:.taskdata.git
#    NOTE: We are going to overwrite pending.data and completed.data at this
#          point.  If your data on the two machines differs, be sure
#          to import the tasks we are about to delete into your other
#          Taskwarrior instance first or they will be lost.
#     $ rm pending.data completed.data undo.data backlog.data
#     $ git fetch origin
#     $ git checkout master
#
#   [WITHOUT PREEXISTING DATA]
#    $ cd ~
#    $ git clone user@host:.taskdata.git .task
#    $ task next
#    Now follow [BASIC SETUP] to activate the hook.
#
#  If you want to sync to a central repo, you will also have to create a symlink
#  to this hook so that it gets called both on launch and on exit. This is not
#  necessary if you go with [A], only if you go with (a variant of) [B].
#   $ cd ~/.task/hooks
#   $ ln -s on-exit_git.sh on-launch_git_pull.sh
#
#  Syncing is only enabled if the ~/.task/hooks/on-launch_git_pull.sh symlink
#  exists.  You can override this behavior by setting the environment
#  variable TWHOOK_GIT_SYNC to "FALSE".
#
#
# Feel free to add more than just pending.data and completed.data to the git
# repo - your hooks, undo.data, backlog.data, (...).
# Anything tracked in the git repo will be automatically synced when you run
# Taskwarrior.
#
#
# [SYNC INTERACTION]
#  You need to be very careful when combining Taskwarrior's inbuilt "sync"
#  with the git syncing this hook does.
#
#  You MUST NOT try to sync two Taskwarrior instances with both the inbuilt
#  "sync" and this hook script.  If you want to use the inbuilt "sync" for
#  allowing an Android app or a site like inthe.am access to your tasks,
#  please only run "sync" from _one_ of your Taskwarrior instances and let the
#  other ones get synchronized through your git repository.
#
#  To reiterate:
#  - Syncing with _only_ this hook, on all Taskwarrior instances, is fine.
#  - Syncing with "task sync" should only be done from one of the Taskwarrior
#    instances running this hook, or not at all.
#  - Disabling syncing in this hook (which it is by default) and only using
#    "task sync" is fine.
#
# That concludes the setup, and the documentation.
#
###
#
# To stop you from unintentionally running "task sync" while this hook's git
# syncing is active, calls to "task sync" will be blocked by default.
# Set the below variable to "FALSE" in order to allow the current Taskwarrior
# instance to sync through its inbuilt sync command.
# The environment variable TWHOOK_GIT_DISABLE_TW_SYNC (catchy, right?)
# overrides this setting for one-off usage.

DISABLE_TW_SYNC="TRUE"

if [ ! -z "$TWHOOK_GIT_DISABLE_TW_SYNC" ]; then
    DISABLE_TW_SYNC=$TWHOOK_GIT_DISABLE_TW_SYNC
fi


# Get info passed to hook script starting with Taskwarrior 2.4.3
for i in "$@"; do
    if [ ${i:0:5} = "args:" ]; then
        tw_args="${i:5}"
    elif [ ${i:0:8} = "command:" ]; then
        tw_command="${i:8}"
    elif [ ${i:0:5} = "data:" ]; then
        tw_data="${i:5}"
    fi
done

if [ -z "$tw_args" ]; then
    # Taskwarrior 2.4.3+ needed to show CLI arguments in the commit message
    tw_args="Taskwarrior 2.4.3+ needed for more info."
fi

if [ -z "$tw_data" ]; then
    # Taskwarrior didn't pass the "data" dir, try to infer it
    cd "$(dirname $0)"
    cd ..
    tw_data=$(pwd)
else
    cd "$tw_data"
fi

if [ -h "hooks/on-launch_git_pull.sh" ]; then
    SYNC="TRUE"
else
    SYNC="FALSE"
fi
if [ ! -z "$TWHOOK_GIT_SYNC" ]; then
    SYNC=$TWHOOK_GIT_SYNC
fi

if ! [ -d ".git" ]; then
    echo $(basename $0) "ERROR: No git repo found. Read the git hook instructions."
    exit 1
fi

# If this hook's git sync is to be used, block "task sync" from running.
if [ "$tw_command" = "synchronize" ]; then
    if [ "$SYNC" = "TRUE" ] && [ "$DISABLE_TW_SYNC" = "TRUE" ]; then
        echo $(basename $0) "ERROR: Trying to use \"task sync\" with active git syncing."
        echo "Run 'TWHOOK_GIT_DISABLE_TW_SYNC=\"FALSE\" task sync' if you know what you are doing."
        echo "NOTE: This can and will lead to problems if you are not careful!"
        exit 1
    fi
fi

# Make sure the previous commit/push went well.
i=0
while [ -f "on-exit_git.sh.log" ]; do
    # Wait for the "git push" background process to finish.
    if [ $i -gt 50 ]; then
        echo $(basename $0) "ERROR: Check ${tw_data}/on-exit_git.sh.log for details."
        echo "After you fix the problem, remove the log file and try again."
        exit 1
    fi
    sleep 0.1
    i=$((i+1))
done


on_exit() {
    exec 0<&-
    exec 1>&-
    exec 2>&-

    if git diff --quiet --exit-code; then
        # No changes found, exit early
        exit 0
    fi

    if ! git commit -a -m"$tw_args" &> on-exit_git.sh.log; then
        # Doesn't really matter which exit code we use, we just need to leave
        # the log behind so we know something went wrong.
        exit 1
    fi

    if [ "$SYNC" = "TRUE" ]; then
        if ! git push origin master &> on-exit_git.sh.log; then
            exit 1
        fi
    fi
    
    # Everything seems to have gone fine, remove log file.
    rm on-exit_git.sh.log
}


on_launch() {
    if [ "$SYNC" = "FALSE" ]; then
        if [ "$TWHOOK_GIT_SYNC" = "FALSE" ]; then
            echo "TWHOOK_GIT_SYNC override active; not syncing git repo."
            # TODO: Create "git_sync_now" file to let on_exit know it needs
            #       to sync if SYNC=1 even if no changes found.
            #       Previously committed changes are not pushed otherwise!
        fi
        return 0
    fi
    
    # TODO: Caching might be a good idea here.
    # Calling "git fetch" every time Taskwarrior is run is the easiest way to
    # make sure there won't be any inconsistencies, but it does lead to
    # considerable startup lag.
    if ! git fetch origin master &> on-launch_git_fetch.sh.log; then
        echo $(basename $0) "ERROR: \"git fetch origin master\" failed."
        echo "See ${tw_data}/on-launch_git_fetch.sh.log for details, or just try again."
        echo "Run 'TWHOOK_GIT_SYNC=\"FALSE\" task ${tw_args} to temporarily disable git syncing."
        exit 1
    fi
    rm on-launch_git_fetch.sh.log

    if ! git merge --ff-only origin/master &> on-launch_git_merge.sh.log; then
        echo $(basename $0) "ERROR: \"git merge --ff-only origin/master\" failed."
        echo "See ${tw_data}/on-launch_git_merge.sh.log for details, or just try again."
        echo "Run 'TWHOOK_GIT_SYNC=\"FALSE\" task ${tw_args} to temporarily disable git syncing."
        exit 1
    fi
    rm on-launch_git_merge.sh.log
}


if [ "${0:(-14)}" = "on-exit_git.sh" ]; then
    on_exit&
else
    on_launch
fi

