git-sync

Tool for periodic syncing of git repositories
git clone https://git.sinitax.com/sinitax/git-sync
Log | Files | Refs | sfeed.txt

git-sync (10673B)


      1#!/usr/bin/env bash
      2#
      3# git-sync
      4#
      5# synchronize tracking repositories
      6#
      7# 2012-20 by Simon Thum and contributors
      8# Licensed as: CC0
      9#
     10# This script intends to sync via git near-automatically
     11# in "tracking" repositories where a nice history is not
     12# crucial, but having one at all is.
     13#
     14# Unlike the myriad of scripts to do just that already available,
     15# it follows the KISS principle: It is small, requires nothing but
     16# git and bash, but does not even try to shield you from git.
     17#
     18# Mode sync (default)
     19#
     20# Sync will likely get from you from a dull normal git repo with trivial
     21# changes to an updated dull normal git repo equal to origin. No more,
     22# no less. The intent is to do everything that's needed to sync
     23# automatically, and resort to manual intervention as soon
     24# as something non-trivial occurs. It is designed to be safe
     25# in that it will likely refuse to do anything not known to
     26# be safe.
     27#
     28# Mode check
     29#
     30# Check only performs the basic checks to make sure the repository
     31# is in an orderly state to continue syncing, i.e. committing
     32# changes, pull etc. without losing any data. When check returns
     33# 0, sync can start immediately. This does not, however, indicate
     34# that syncing is at all likely to succeed.
     35
     36# command used to auto-commit file modifications
     37DEFAULT_AUTOCOMMIT_CMD="git add -u ; git commit -m \"%message\";"
     38
     39# command used to auto-commit all changes
     40ALL_AUTOCOMMIT_CMD="git add -A ; git commit -m \"%message\";"
     41
     42# default commit message substituted into autocommit commands
     43DEFAULT_AUTOCOMMIT_MSG="changes from $(uname -n) on $(date)"
     44
     45
     46# AUTOCOMMIT_CMD="echo \"Please commit or stash pending changes\"; exit 1;"
     47# TODO mode for stash push & pop
     48
     49print_usage() {
     50    cat << EOF
     51usage: $0 [-h] [-n] [-s] [MODE]
     52
     53Synchronize the current branch to a remote backup
     54MODE may be either "sync" (the default) or "check", to verify that the branch is ready to sync
     55
     56OPTIONS:
     57   -h      Show this message
     58   -n      Commit new files even if branch.\$branch_name.syncNewFiles isn't set
     59   -s      Sync the branch even if branch.\$branch_name.sync isn't set
     60EOF
     61}
     62sync_new_files_anyway="false"
     63sync_anyway="false"
     64
     65while getopts "hns" opt ; do
     66    case $opt in
     67        h )
     68            print_usage
     69            exit 0
     70            ;;
     71        n )
     72            sync_new_files_anyway="true"
     73            ;;
     74        s )
     75            sync_anyway="true"
     76            ;;
     77    esac
     78done
     79shift $((OPTIND-1))
     80
     81#
     82#    utility functions, some adapted from git bash completion
     83#
     84
     85__log_msg()
     86{
     87    echo git-sync: $1
     88}
     89
     90# echo the git dir
     91__gitdir()
     92{
     93	if [ "true" = "$(git rev-parse --is-inside-work-tree "$PWD" | head -1)" ]; then
     94		git rev-parse --git-dir "$PWD" 2>/dev/null
     95	fi
     96}
     97
     98# echos repo state
     99git_repo_state ()
    100{
    101	local g="$(__gitdir)"
    102	if [ -n "$g" ]; then
    103		if [ -f "$g/rebase-merge/interactive" ]; then
    104			echo "REBASE-i"
    105		elif [ -d "$g/rebase-merge" ]; then
    106			echo "REBASE-m"
    107		else
    108			if [ -d "$g/rebase-apply" ]; then
    109				echo "AM/REBASE"
    110			elif [ -f "$g/MERGE_HEAD" ]; then
    111				echo "MERGING"
    112			elif [ -f "$g/CHERRY_PICK_HEAD" ]; then
    113				echo "CHERRY-PICKING"
    114			elif [ -f "$g/BISECT_LOG" ]; then
    115				echo "BISECTING"
    116			fi
    117		fi
    118		if [ "true" = "$(git rev-parse --is-inside-git-dir 2>/dev/null)" ]; then
    119			if [ "true" = "$(git rev-parse --is-bare-repository 2>/dev/null)" ]; then
    120				echo "|BARE"
    121			else
    122				echo "|GIT_DIR"
    123			fi
    124		elif [ "true" = "$(git rev-parse --is-inside-work-tree 2>/dev/null)" ]; then
    125			git diff --no-ext-diff --quiet --exit-code || echo "|DIRTY"
    126#			if [ -n "${GIT_PS1_SHOWSTASHSTATE-}" ]; then
    127#			        git rev-parse --verify refs/stash >/dev/null 2>&1 && s="$"
    128#			fi
    129#
    130#			if [ -n "${GIT_PS1_SHOWUNTRACKEDFILES-}" ]; then
    131#			   if [ -n "$(git ls-files --others --exclude-standard)" ]; then
    132#			      u="%"
    133#			   fi
    134#			fi
    135#
    136#			if [ -n "${GIT_PS1_SHOWUPSTREAM-}" ]; then
    137#				__git_ps1_show_upstream
    138#			fi
    139		fi
    140	else
    141	    echo "NOGIT"
    142	fi
    143}
    144
    145# check if we only have untouched, modified or (if configured) new files
    146check_initial_file_state()
    147{
    148    local syncNew="$(git config --get --bool branch.$branch_name.syncNewFiles)"
    149    if [[ "true" == "$syncNew" || "true" == "$sync_new_files_anyway" ]]; then
    150	# allow for new files
    151	if [ ! -z "$(git status --porcelain | grep -E '^[^ \?][^M\?] *')" ]; then
    152	    echo "NonNewOrModified"
    153	fi
    154    else
    155	# also bail on new files
    156	if [ ! -z "$(git status --porcelain | grep -E '^[^ ][^M] *')" ]; then
    157	    echo "NotOnlyModified"
    158	fi
    159    fi
    160}
    161
    162# look for local changes
    163# used to decide if autocommit should be invoked
    164local_changes()
    165{
    166    if [ ! -z "$(git status --porcelain | grep -E '^(\?\?|[MARC] |[ MARC][MD])*')" ]; then
    167	echo "LocalChanges"
    168    fi
    169}
    170
    171# determine sync state of repository, i.e. how the remote relates to our HEAD
    172sync_state()
    173{
    174    local count="$(git rev-list --count --left-right $remote_name/$branch_name...HEAD)"
    175
    176    case "$count" in
    177	"") # no upstream
    178	    echo "noUpstream"
    179	    false
    180	    ;;
    181	"0	0")
    182	    echo "equal"
    183	    true
    184	    ;;
    185	"0	"*)
    186	    echo "ahead"
    187	    true
    188	    ;;
    189	*"	0")
    190	    echo "behind"
    191	    true
    192	    ;;
    193	*)
    194	    echo "diverged"
    195	    true
    196	    ;;
    197    esac
    198}
    199
    200# exit, issue warning if not in sync
    201exit_assuming_sync() {
    202    if [ "equal" == "$(sync_state)" ] ; then
    203	__log_msg "In sync, all fine."
    204	exit 0;
    205    else
    206	__log_msg "Synchronization FAILED! You should definitely check your repository carefully!"
    207	__log_msg "(Possibly a transient network problem? Please try again in that case.)"
    208	exit 3
    209    fi
    210}
    211
    212#
    213#        Here git-sync actually starts
    214#
    215
    216# first some sanity checks
    217rstate="$(git_repo_state)"
    218if [[ -z "$rstate" || "|DIRTY" = "$rstate" ]]; then
    219    __log_msg "Preparing. Repo in $(__gitdir)"
    220elif [[ "NOGIT" = "$rstate" ]] ; then
    221    __log_msg "No git repository detected. Exiting."
    222    exit 128 # matches git's error code
    223else
    224    __log_msg "Git repo state considered unsafe for sync: $(git_repo_state)"
    225    exit 2
    226fi
    227
    228# determine the current branch (thanks to stackoverflow)
    229branch_name=$(git symbolic-ref -q HEAD)
    230branch_name=${branch_name##refs/heads/}
    231
    232if [ -z "$branch_name" ] ; then
    233    __log_msg "Syncing is only possible on a branch."
    234    git status
    235    exit 2
    236fi
    237
    238# while at it, determine the remote to operate on
    239remote_name=$(git config --get branch.$branch_name.pushRemote)
    240if [ -z "$remote_name" ] ; then
    241    remote_name=$(git config --get remote.pushDefault)
    242fi
    243if [ -z "$remote_name" ] ; then
    244    remote_name=$(git config --get branch.$branch_name.remote)
    245fi
    246
    247if [ -z "$remote_name" ] ; then
    248    __log_msg "the current branch does not have a configured remote."
    249    echo
    250    __log_msg "Please use"
    251    echo
    252    __log_msg "  git branch --set-upstream-to=[remote_name]/$branch_name"
    253    echo
    254    __log_msg "replacing [remote_name] with the name of your remote, i.e. - origin"
    255    __log_msg "to set the remote tracking branch for git-sync to work"
    256    exit 2
    257fi
    258
    259# check if current branch is configured for sync
    260if [[ "true" != "$(git config --get --bool branch.$branch_name.sync)" && "true" != "$sync_anyway" ]] ; then
    261    echo
    262    __log_msg "Please use"
    263    echo
    264    __log_msg "  git config --bool branch.$branch_name.sync true"
    265    echo
    266    __log_msg "to enlist branch $branch_name for synchronization."
    267    __log_msg "Branch $branch_name has to have a same-named remote branch"
    268    __log_msg "for git-sync to work."
    269    echo
    270    __log_msg "(If you don't know what this means, you should change that"
    271    __log_msg "before relying on this script. You have been warned.)"
    272    echo
    273    exit 1
    274fi
    275
    276# determine mode
    277if [[ -z "$1" || "$1" == "sync" ]]; then
    278    mode="sync"
    279elif [[ "check" == "$1" ]]; then
    280    mode="check"
    281else
    282    __log_msg "Mode $1 not recognized"
    283    exit 100
    284fi
    285
    286__log_msg "Mode $mode"
    287
    288__log_msg "Using $remote_name/$branch_name"
    289
    290# check for intentionally unhandled file states
    291if [ ! -z "$(check_initial_file_state)" ] ; then
    292    __log_msg "There are changed files you should probably handle manually."
    293    git status
    294    exit 1
    295fi
    296
    297# if in check mode, this is all we need to know
    298if [ $mode == "check" ] ; then
    299    __log_msg "check OK; sync may start."
    300    exit 0
    301fi
    302
    303# check if we have to commit local changes, if yes, do so
    304if [ ! -z "$(local_changes)" ]; then
    305    autocommit_cmd=""
    306    config_autocommit_cmd="$(git config --get branch.$branch_name.autocommitscript)"
    307
    308    # discern the three ways to auto-commit
    309    if [ ! -z "$config_autocommit_cmd" ]; then
    310	autocommit_cmd="$config_autocommit_cmd"
    311    elif [[ "true" == "$(git config --get --bool branch.$branch_name.syncNewFiles)" || "true" == "$sync_new_files_anyway" ]]; then
    312	autocommit_cmd=${ALL_AUTOCOMMIT_CMD}
    313    else
    314        autocommit_cmd=${DEFAULT_AUTOCOMMIT_CMD}
    315    fi
    316
    317    commit_msg="$(git config --get branch.$branch_name.syncCommitMsg)"
    318    if [ "" == "$commit_msg" ]; then
    319      commit_msg=${DEFAULT_AUTOCOMMIT_MSG}
    320    fi
    321    autocommit_cmd=$(echo "$autocommit_cmd" | sed "s/%message/$commit_msg/")
    322
    323    __log_msg "Committing local changes using ${autocommit_cmd}"
    324    eval $autocommit_cmd
    325
    326    # after autocommit, we should be clean
    327    rstate="$(git_repo_state)"
    328    if [[ ! -z "$rstate" ]]; then
    329	__log_msg "Auto-commit left uncommitted changes. Please add or remove them as desired and retry."
    330	exit 1
    331    fi
    332fi
    333
    334# fetch remote to get to the current sync state
    335# TODO make fetching/pushing optional
    336__log_msg "Fetching from $remote_name/$branch_name"
    337git fetch $remote_name $branch_name
    338if [ $? != 0 ] ; then
    339    __log_msg "git fetch $remote_name returned non-zero. Likely a network problem; exiting."
    340    exit 3
    341fi
    342
    343case "$(sync_state)" in
    344"noUpstream")
    345	__log_msg "Strange state, you're on your own. Good luck."
    346	exit 2
    347	;;
    348"equal")
    349	exit_assuming_sync
    350	;;
    351"ahead")
    352	__log_msg "Pushing changes..."
    353	git push $remote_name $branch_name:$branch_name
    354	if [ $? == 0 ]; then
    355	    exit_assuming_sync
    356	else
    357	    __log_msg "git push returned non-zero. Likely a connection failure."
    358	    exit 3
    359	fi
    360	;;
    361"behind")
    362	__log_msg "We are behind, fast-forwarding..."
    363	git merge --ff --ff-only $remote_name/$branch_name
    364	if [ $? == 0 ]; then
    365	    exit_assuming_sync
    366	else
    367	    __log_msg "git merge --ff --ff-only returned non-zero ($?). Exiting."
    368	    exit 2
    369	fi
    370	;;
    371"diverged")
    372	__log_msg "We have diverged. Trying to rebase..."
    373	git rebase $remote_name/$branch_name
    374	if [[ $? == 0 && -z "$(git_repo_state)" && "ahead" == "$(sync_state)" ]] ; then
    375	    __log_msg "Rebasing went fine, pushing..."
    376	    git push $remote_name $branch_name:$branch_name
    377	    exit_assuming_sync
    378	else
    379	    __log_msg "Rebasing failed, likely there are conflicting changes. Resolve them and finish the rebase before repeating git-sync."
    380	    exit 1
    381	fi
    382	# TODO: save master, if rebasing fails, make a branch of old master
    383	;;
    384esac