git-sync is in tree now \o/
This commit is contained in:
parent
af6f22088d
commit
8d189fd3a2
385
bin/git-sync
385
bin/git-sync
@ -1,385 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
#
|
|
||||||
# git-sync
|
|
||||||
#
|
|
||||||
# synchronize tracking repositories
|
|
||||||
#
|
|
||||||
# 2012-20 by Simon Thum and contributors
|
|
||||||
# Licensed as: CC0
|
|
||||||
#
|
|
||||||
# This scrips intends to sync via git near-automatically
|
|
||||||
# in "tracking" repositories where a nice history is not
|
|
||||||
# crucial, but having one at all is.
|
|
||||||
#
|
|
||||||
# Unlike the myriad of scripts to do just that already available,
|
|
||||||
# it follows the KISS principle: It is small, requires nothing but
|
|
||||||
# git and bash, but does not even try to shield you from git.
|
|
||||||
#
|
|
||||||
# Mode sync (default)
|
|
||||||
#
|
|
||||||
# Sync will likely get from you from a dull normal git repo with trivial
|
|
||||||
# changes to an updated dull normal git repo equal to origin. No more,
|
|
||||||
# no less. The intent is to do everything that's needed to sync
|
|
||||||
# automatically, and resort to manual intervention as soon
|
|
||||||
# as something non-trivial occurs. It is designed to be safe
|
|
||||||
# in that it will likely refuse to do anything not known to
|
|
||||||
# be safe.
|
|
||||||
#
|
|
||||||
# Mode check
|
|
||||||
#
|
|
||||||
# Check only performs the basic checks to make sure the repository
|
|
||||||
# is in an orderly state to continue syncing, i.e. committing
|
|
||||||
# changes, pull etc. without losing any data. When check returns
|
|
||||||
# 0, sync can start immediately. This does not, however, indicate
|
|
||||||
# that syncing is at all likely to succeed.
|
|
||||||
|
|
||||||
# command used to auto-commit file modifications
|
|
||||||
DEFAULT_AUTOCOMMIT_CMD="git add -u ; git commit -m \"%message\";"
|
|
||||||
|
|
||||||
# command used to auto-commit all changes
|
|
||||||
ALL_AUTOCOMMIT_CMD="git add -A ; git commit -m \"%message\";"
|
|
||||||
|
|
||||||
# default commit message substituted into autocommit commands
|
|
||||||
DEFAULT_AUTOCOMMIT_MSG="changes from $(uname -n) on $(date)"
|
|
||||||
|
|
||||||
|
|
||||||
# AUTOCOMMIT_CMD="echo \"Please commit or stash pending changes\"; exit 1;"
|
|
||||||
# TODO mode for stash push & pop
|
|
||||||
|
|
||||||
print_usage() {
|
|
||||||
cat << EOF
|
|
||||||
usage: $0 [-h] [-n] [-s] [MODE]
|
|
||||||
|
|
||||||
Synchronize the current branch to a remote backup
|
|
||||||
MODE may be either "sync" (the default) or "check", to verify that the branch is ready to sync
|
|
||||||
|
|
||||||
OPTIONS:
|
|
||||||
-h Show this message
|
|
||||||
-n Commit new files even if branch.\$branch_name.syncNewFiles isn't set
|
|
||||||
-s Sync the branch even if branch.\$branch_name.sync isn't set
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
sync_new_files_anyway="false"
|
|
||||||
sync_anyway="false"
|
|
||||||
|
|
||||||
while getopts "hns" opt ; do
|
|
||||||
case $opt in
|
|
||||||
h )
|
|
||||||
print_usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
n )
|
|
||||||
sync_new_files_anyway="true"
|
|
||||||
;;
|
|
||||||
s )
|
|
||||||
sync_anyway="true"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
shift $((OPTIND-1))
|
|
||||||
|
|
||||||
#
|
|
||||||
# utility functions, some adapted from git bash completion
|
|
||||||
#
|
|
||||||
|
|
||||||
__log_msg()
|
|
||||||
{
|
|
||||||
echo git-sync: $1
|
|
||||||
}
|
|
||||||
|
|
||||||
# echo the git dir
|
|
||||||
__gitdir()
|
|
||||||
{
|
|
||||||
if [ "true" = "$(git rev-parse --is-inside-work-tree "$PWD" | head -1)" ]; then
|
|
||||||
git rev-parse --git-dir "$PWD" 2>/dev/null
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# echos repo state
|
|
||||||
git_repo_state ()
|
|
||||||
{
|
|
||||||
local g="$(__gitdir)"
|
|
||||||
if [ -n "$g" ]; then
|
|
||||||
if [ -f "$g/rebase-merge/interactive" ]; then
|
|
||||||
echo "REBASE-i"
|
|
||||||
elif [ -d "$g/rebase-merge" ]; then
|
|
||||||
echo "REBASE-m"
|
|
||||||
else
|
|
||||||
if [ -d "$g/rebase-apply" ]; then
|
|
||||||
echo "AM/REBASE"
|
|
||||||
elif [ -f "$g/MERGE_HEAD" ]; then
|
|
||||||
echo "MERGING"
|
|
||||||
elif [ -f "$g/CHERRY_PICK_HEAD" ]; then
|
|
||||||
echo "CHERRY-PICKING"
|
|
||||||
elif [ -f "$g/BISECT_LOG" ]; then
|
|
||||||
echo "BISECTING"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
if [ "true" = "$(git rev-parse --is-inside-git-dir 2>/dev/null)" ]; then
|
|
||||||
if [ "true" = "$(git rev-parse --is-bare-repository 2>/dev/null)" ]; then
|
|
||||||
echo "|BARE"
|
|
||||||
else
|
|
||||||
echo "|GIT_DIR"
|
|
||||||
fi
|
|
||||||
elif [ "true" = "$(git rev-parse --is-inside-work-tree 2>/dev/null)" ]; then
|
|
||||||
git diff --no-ext-diff --quiet --exit-code || echo "|DIRTY"
|
|
||||||
# if [ -n "${GIT_PS1_SHOWSTASHSTATE-}" ]; then
|
|
||||||
# git rev-parse --verify refs/stash >/dev/null 2>&1 && s="$"
|
|
||||||
# fi
|
|
||||||
#
|
|
||||||
# if [ -n "${GIT_PS1_SHOWUNTRACKEDFILES-}" ]; then
|
|
||||||
# if [ -n "$(git ls-files --others --exclude-standard)" ]; then
|
|
||||||
# u="%"
|
|
||||||
# fi
|
|
||||||
# fi
|
|
||||||
#
|
|
||||||
# if [ -n "${GIT_PS1_SHOWUPSTREAM-}" ]; then
|
|
||||||
# __git_ps1_show_upstream
|
|
||||||
# fi
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "NOGIT"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# check if we only have untouched, modified or (if configured) new files
|
|
||||||
check_initial_file_state()
|
|
||||||
{
|
|
||||||
local syncNew="$(git config --get --bool branch.$branch_name.syncNewFiles)"
|
|
||||||
if [[ "true" == "$syncNew" || "true" == "$sync_new_files_anyway" ]]; then
|
|
||||||
# allow for new files
|
|
||||||
if [ ! -z "$(git status --porcelain | grep -E '^[^ \?][^M\?] *')" ]; then
|
|
||||||
echo "NonNewOrModified"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
# also bail on new files
|
|
||||||
if [ ! -z "$(git status --porcelain | grep -E '^[^ ][^M] *')" ]; then
|
|
||||||
echo "NotOnlyModified"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# look for local changes
|
|
||||||
# used to decide if autocommit should be invoked
|
|
||||||
local_changes()
|
|
||||||
{
|
|
||||||
if [ ! -z "$(git status --porcelain | grep -E '^(\?\?|[MARC] |[ MARC][MD])*')" ]; then
|
|
||||||
echo "LocalChanges"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# determine sync state of repository, i.e. how the remote relates to our HEAD
|
|
||||||
sync_state()
|
|
||||||
{
|
|
||||||
local count="$(git rev-list --count --left-right $remote_name/$branch_name...HEAD)"
|
|
||||||
|
|
||||||
case "$count" in
|
|
||||||
"") # no upstream
|
|
||||||
echo "noUpstream"
|
|
||||||
false
|
|
||||||
;;
|
|
||||||
"0 0")
|
|
||||||
echo "equal"
|
|
||||||
true
|
|
||||||
;;
|
|
||||||
"0 "*)
|
|
||||||
echo "ahead"
|
|
||||||
true
|
|
||||||
;;
|
|
||||||
*" 0")
|
|
||||||
echo "behind"
|
|
||||||
true
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "diverged"
|
|
||||||
true
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# exit, issue warning if not in sync
|
|
||||||
exit_assuming_sync() {
|
|
||||||
if [ "equal" == "$(sync_state)" ] ; then
|
|
||||||
__log_msg "In sync, all fine."
|
|
||||||
exit 0;
|
|
||||||
else
|
|
||||||
__log_msg "Synchronization FAILED! You should definitely check your repository carefully!"
|
|
||||||
__log_msg "(Possibly a transient network problem? Please try again in that case.)"
|
|
||||||
exit 3
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Here git-sync actually starts
|
|
||||||
#
|
|
||||||
|
|
||||||
# first some sanity checks
|
|
||||||
rstate="$(git_repo_state)"
|
|
||||||
if [[ -z "$rstate" || "|DIRTY" = "$rstate" ]]; then
|
|
||||||
__log_msg "Preparing. Repo in $(__gitdir)"
|
|
||||||
elif [[ "NOGIT" = "$rstate" ]] ; then
|
|
||||||
__log_msg "No git repository detected. Exiting."
|
|
||||||
exit 128 # matches git's error code
|
|
||||||
else
|
|
||||||
__log_msg "Git repo state considered unsafe for sync: $(git_repo_state)"
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
# determine the current branch (thanks to stackoverflow)
|
|
||||||
branch_name=$(git symbolic-ref -q HEAD)
|
|
||||||
branch_name=${branch_name##refs/heads/}
|
|
||||||
|
|
||||||
if [ -z "$branch_name" ] ; then
|
|
||||||
__log_msg "Syncing is only possible on a branch."
|
|
||||||
git status
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
# while at it, determine the remote to operate on
|
|
||||||
remote_name=$(git config --get branch.$branch_name.pushRemote)
|
|
||||||
if [ -z "$remote_name" ] ; then
|
|
||||||
remote_name=$(git config --get remote.pushDefault)
|
|
||||||
fi
|
|
||||||
if [ -z "$remote_name" ] ; then
|
|
||||||
remote_name=$(git config --get branch.$branch_name.remote)
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$remote_name" ] ; then
|
|
||||||
__log_msg "the current branch does not have a configured remote."
|
|
||||||
echo
|
|
||||||
__log_msg "Please use"
|
|
||||||
echo
|
|
||||||
__log_msg " git branch --set-upstream-to=[remote_name]/$branch_name"
|
|
||||||
echo
|
|
||||||
__log_msg "replacing [remote_name] with the name of your remote, i.e. - origin"
|
|
||||||
__log_msg "to set the remote tracking branch for git-sync to work"
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
# check if current branch is configured for sync
|
|
||||||
if [[ "true" != "$(git config --get --bool branch.$branch_name.sync)" && "true" != "$sync_anyway" ]] ; then
|
|
||||||
echo
|
|
||||||
__log_msg "Please use"
|
|
||||||
echo
|
|
||||||
__log_msg " git config --bool branch.$branch_name.sync true"
|
|
||||||
echo
|
|
||||||
__log_msg "to enlist branch $branch_name for synchronization."
|
|
||||||
__log_msg "Branch $branch_name has to have a same-named remote branch"
|
|
||||||
__log_msg "for git-sync to work."
|
|
||||||
echo
|
|
||||||
__log_msg "(If you don't know what this means, you should change that"
|
|
||||||
__log_msg "before relying on this script. You have been warned.)"
|
|
||||||
echo
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# determine mode
|
|
||||||
if [[ -z "$1" || "$1" == "sync" ]]; then
|
|
||||||
mode="sync"
|
|
||||||
elif [[ "check" == "$1" ]]; then
|
|
||||||
mode="check"
|
|
||||||
else
|
|
||||||
__log_msg "Mode $1 not recognized"
|
|
||||||
exit 100
|
|
||||||
fi
|
|
||||||
|
|
||||||
__log_msg "Mode $mode"
|
|
||||||
|
|
||||||
__log_msg "Using $remote_name/$branch_name"
|
|
||||||
|
|
||||||
# check for intentionally unhandled file states
|
|
||||||
if [ ! -z "$(check_initial_file_state)" ] ; then
|
|
||||||
__log_msg "There are changed files you should probably handle manually."
|
|
||||||
git status
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# if in check mode, this is all we need to know
|
|
||||||
if [ $mode == "check" ] ; then
|
|
||||||
__log_msg "check OK; sync may start."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# check if we have to commit local changes, if yes, do so
|
|
||||||
if [ ! -z "$(local_changes)" ]; then
|
|
||||||
autocommit_cmd=""
|
|
||||||
config_autocommit_cmd="$(git config --get branch.$branch_name.autocommitscript)"
|
|
||||||
|
|
||||||
# discern the three ways to auto-commit
|
|
||||||
if [ ! -z "$config_autocommit_cmd" ]; then
|
|
||||||
autocommit_cmd="$config_autocommit_cmd"
|
|
||||||
elif [[ "true" == "$(git config --get --bool branch.$branch_name.syncNewFiles)" || "true" == "$sync_new_files_anyway" ]]; then
|
|
||||||
autocommit_cmd=${ALL_AUTOCOMMIT_CMD}
|
|
||||||
else
|
|
||||||
autocommit_cmd=${DEFAULT_AUTOCOMMIT_CMD}
|
|
||||||
fi
|
|
||||||
|
|
||||||
commit_msg="$(git config --get branch.$branch_name.syncCommitMsg)"
|
|
||||||
if [ "" == "$commit_msg" ]; then
|
|
||||||
commit_msg=${DEFAULT_AUTOCOMMIT_MSG}
|
|
||||||
fi
|
|
||||||
autocommit_cmd=$(echo "$autocommit_cmd" | sed "s/%message/$commit_msg/")
|
|
||||||
|
|
||||||
__log_msg "Committing local changes using ${autocommit_cmd}"
|
|
||||||
eval $autocommit_cmd
|
|
||||||
|
|
||||||
# after autocommit, we should be clean
|
|
||||||
rstate="$(git_repo_state)"
|
|
||||||
if [[ ! -z "$rstate" ]]; then
|
|
||||||
__log_msg "Auto-commit left uncommitted changes. Please add or remove them as desired and retry."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# fetch remote to get to the current sync state
|
|
||||||
# TODO make fetching/pushing optional
|
|
||||||
__log_msg "Fetching from $remote_name/$branch_name"
|
|
||||||
git fetch $remote_name $branch_name
|
|
||||||
if [ $? != 0 ] ; then
|
|
||||||
__log_msg "git fetch $remote_name returned non-zero. Likely a network problem; exiting."
|
|
||||||
exit 3
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "$(sync_state)" in
|
|
||||||
"noUpstream")
|
|
||||||
__log_msg "Strange state, you're on your own. Good luck."
|
|
||||||
exit 2
|
|
||||||
;;
|
|
||||||
"equal")
|
|
||||||
exit_assuming_sync
|
|
||||||
;;
|
|
||||||
"ahead")
|
|
||||||
__log_msg "Pushing changes..."
|
|
||||||
git push $remote_name $branch_name:$branch_name
|
|
||||||
if [ $? == 0 ]; then
|
|
||||||
exit_assuming_sync
|
|
||||||
else
|
|
||||||
__log_msg "git push returned non-zero. Likely a connection failure."
|
|
||||||
exit 3
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
"behind")
|
|
||||||
__log_msg "We are behind, fast-forwarding..."
|
|
||||||
git merge --ff --ff-only $remote_name/$branch_name
|
|
||||||
if [ $? == 0 ]; then
|
|
||||||
exit_assuming_sync
|
|
||||||
else
|
|
||||||
__log_msg "git merge --ff --ff-only returned non-zero ($?). Exiting."
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
"diverged")
|
|
||||||
__log_msg "We have diverged. Trying to rebase..."
|
|
||||||
git rebase $remote_name/$branch_name
|
|
||||||
if [[ $? == 0 && -z "$(git_repo_state)" && "ahead" == "$(sync_state)" ]] ; then
|
|
||||||
__log_msg "Rebasing went fine, pushing..."
|
|
||||||
git push $remote_name $branch_name:$branch_name
|
|
||||||
exit_assuming_sync
|
|
||||||
else
|
|
||||||
__log_msg "Rebasing failed, likely there are conflicting changes. Resolve them and finish the rebase before repeating git-sync."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
# TODO: save master, if rebasing fails, make a branch of old master
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user