Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
Signed-off-by: Simon Thum <simon.thum@gmx.de>
  • Loading branch information
simonthum committed May 20, 2012
0 parents commit 673fd51
Show file tree
Hide file tree
Showing 2 changed files with 397 additions and 0 deletions.
101 changes: 101 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# git-sync

sychronize tracking repositories

2012 by Simon Thum

This scrips intends to sync near-automatically via git
in "tracking" repositories where a nice history is not
as crucial as having one.

Licensed under CC0

## One more git sync script? Seriously?

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.

It is ultimately intended for git-savy people. Tested on msysgit and a
real bash. In case you know bash scripting, it will probably make your
eyes bleed, but for some reason it works.

### What does it do?

It 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 `git-sync` will likely refuse to do anything not known to
be safe.

### How am I supposed to use it?

Just call `git-sync` inside your average joe's repository (not in the
middle of a rebase, git-am, merge or whatever, not detached, no
untracked files) and everything will likely just work. If you don't
push in an intertwined manner, `git-sync` is virtually guaranteed to work.

Care has been taken that any kind of problem, pre-existing or not,
results in clear error messages and non-zero return code, but of
course no guarantee can be given.

## How does it work?

The flow is roughly:
1. sanity checks
You don't want to do this in the middle of a rebase
2. Check for new files
exit if there are, unless allowed in config
2. check for auto-commitable changes
3. perform auto-commit
one more check for leftover changes / general tidyness
4. fetch upstream
5. Relate upstream to ours
ahead -> push
behind -> pull
diverged -> rebase, then push
6. At exit, check sync state once more

On the first invocation, `git-sync` will ask you to whitelist the
current branch for sync using git config. This has to be done once for
every copy.

## Options

There are two `git config`-based options for tailoring your sync:

branch.$branch_name.syncNewFiles (bool)

Tells git-sync to invoke auto-commit even if new (untracked)
files are present. Normally you have to commit those yourself.

branch.$branch_name.autocommitscript (string)

A string which is being eval'ed by this script to perform an
auto-commit. Here you can run a commit script which should not
leave any uncommited state. The default will commit modified or
all files with a more or less useful message.

By default commit is done using:

git add -u ; git commit -m "changes from $(uname -n) on $(date)"

Or if you enable `syncNewFiles`:

git add -A ; git commit -m \"changes from $(uname -n) on $(date)\";"

# License

I declare this work to be useable under the provisions of the CC0 license.

http://creativecommons.org/publicdomain/zero/1.0/

Attribution is appreciated, but not required.

# Thanks

Thanks go to all the people behind git.
296 changes: 296 additions & 0 deletions git-sync
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
#!/bin/bash
#
# git-sync
#
# sychronize tracking repositories
#
# 2012 by Simon Thum
# Licensed as: CC0
#
# This scrips intends to sync via git near-automatically
# in "tracking" repositories where a nice history is not
# crucial as having one.
#
# 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.
#
# It 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.


# command used to auto-commit file modifications
DEFAULT_AUTOCOMMIT_CMD="git add -u ; git commit -m \"changes from $(uname -n) on $(date)\";"

# command used to auto-commit all changes
ALL_AUTOCOMMIT_CMD="git add -A ; git commit -m \"changes from $(uname -n) on $(date)\";"


# AUTOCOMMIT_CMD="echo \"Please commit or stash pending changes\"; exit 1;"
# TODO mode for stash push & pop



#
# utility functions, some adapted from git bash completion
#

# echo the git dir
__gitdir()
{
if [ "true" = "$(git rev-parse --is-inside-work-tree)" ]; then
git rev-parse --git-dir 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" ]; 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 --short | grep -E '^[^U\?][^U\?]*')" ]; 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
echo "git-sync: In sync, all fine."
exit 0;
else
echo "git-sync: Synchronization FAILED! You should definitely check your repository carefully!"
echo "(Possibly, a concurrent push to $remote_name screwed it?)"
exit 3
fi
}

#
# Here git-sync actually starts
#

# first some sanity checks
rstate="$(git_repo_state)"
if [[ -z "$rstate" || "|DIRTY" = "$rstate" ]]; then
echo "git-sync: Preparing for sync. Repo in $(__gitdir)"
elif [[ "NOGIT" = "$rstate" ]] ; then
echo "git-sync: No git repository detected. Exiting."
exit 128 # matches git's error code
else
echo "git-sync: 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
echo "git-sync: Syncing is only possible on a branch."
git status
exit 2
fi

# check if current branch is configured
if [ "true" != "$(git config --get --bool branch.$branch_name.sync)" ] ; then
echo
echo "git-sync: Please use"
echo
echo " git config --bool branch.$branch_name.sync true"
echo
echo "to whitelist branch $branch_name for synchronization."
echo "Branch $branch_name has to have a same-named remote branch"
echo "for this script to work."
echo
echo "(If you don't know what this means, you should change that"
echo "before relying on this script. You have been warned.)"
echo
exit 1
fi

# while at it, determine the remote to operate on
remote_name=$(git config --get branch.$branch_name.remote)

echo "git-sync: using $remote_name/$branch_name"

# check for intentionally unhandled file states
if [ ! -z "$(check_initial_file_state)" ] ; then
echo "git-sync: There are changed files you should probably handle manually."
git status
exit 1
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)" ]; then
autocommit_cmd=${ALL_AUTOCOMMIT_CMD}
else
autocommit_cmd=${DEFAULT_AUTOCOMMIT_CMD}
fi

echo "git-sync: Commiting local chages using ${autocommit_cmd}"
eval $autocommit_cmd

# after autocommit, we should be clean
rstate="$(git_repo_state)"
if [[ ! -z "$rstate" ]]; then
echo "git-sync: Auto-commit left uncommited changes. Cannot continue."
exit 1
fi
fi

# fetch remote to get to the current sync state
# TODO make fetching/pushing optional
echo "fetching from $remote_name"
git fetch $remote_name
if [ $? != 0 ] ; then
echo "git-sync: git fetch $remote_name returned non-zero. Cannot continue."
exit 3
fi

case "$(sync_state)" in
"noUpstream")
echo "git-sync: Strange state, you're on your own. Good luck."
exit 2
;;
"equal")
exit_assuming_sync
;;
"ahead")
echo "git-sync: Pushing changes"
git push
if [ $? == 0 ]; then
exit_assuming_sync
else
echo "git-sync: git push returned non-zero. Possibly a connection failure."
exit 3
fi
;;
"behind")
echo "git-sync: we are behind, fast-forwarding."
# TODO this should be with fetch's --dry-run
git pull --ff-only
if [ $? == 0 ]; then
exit_assuming_sync
else
echo "git pull --ff-only returned non-zero ($?). Exiting."
exit 2
fi
;;
"diverged")
echo "git-sync: We have diverged. Trying to rebase..."
git rebase $remote_name/$branch_name
if [[ $? == 0 && -z "$(git_repo_state)" && "ahead" == "$(sync_state)" ]] ; then
echo "git-sync: Rebasing went fine, pushing..."
git push
exit_assuming_sync
else
echo "git-sync: Rebasing failed, likely there are conflicting changes."
exit 1
fi
# TODO: save master, if rebasing fails, make a branch of old master
;;
esac

0 comments on commit 673fd51

Please sign in to comment.