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