#!/bin/bash # patchsync: Synchronizes a trunk, a branch, and a patch containing the # differences between them. # -- Matt McCutchen # If I had to update the version in the --version message separately, I would forget. PATCHSYNC_VERSION=2.4 # usage: patchsync [--dry-run] [branch | patch] # # Patchsync is invoked on a "staging directory", which holds some configuration # (including the locations of the trunk, patch, and branch it is to synchronize) # and some synchronization state. It determines whether each of the trunk, # patch, and branch has changed since the last successful synchronization and # updates the patch or branch as appropriate: # # Changed since last sync Patchsync's behavior # ------------------------------------------------- # Nothing Do nothing # Trunk only Update branch # Patch but not branch Update branch # Branch but not patch Update patch # Branch and patch Complain about conflict # # : path to the staging directory # # --dry-run: show what would happen without actually modifying the trunk, patch, # branch, or synchronization state # # {branch | patch}: force patchsync to update the specified thing from the # others instead of deciding automatically; you can use this argument to # revert or to resolve a conflict # # CAVEAT: Patchsync might make a mess if the trunk, patch, or branch is # modified in a way not hidden by the filters while patchsync is running! # # CAVEAT: Patchsync only notices creations, deletions, and modifications of # regular files in the trunk and branch, not other changes like empty directory # creations. If you make a change like that to the trunk, you can force # patchsync to update the branch. # # Staging directory format: A staging directory contains the following items: # "trunk", trunk directory or symlink to it # "patch", patch regular file or symlink to it # "branch", branch directory or symlink to it # [Why symlinks? Expose as much as possible to tools like symlinks(8).] # "settings", shell script defining the following shell functions: # - do_diff : diff the specified trunk and # branch and write the patch to the specified file; define it to use # your favorite diff format # - example: exitoneok diff -urN $1 $2 \ # | sed -re 's/^(\+\+\+|---) ([^\t]+).*$/\1 \2/' \ # | exitoneok grep -v '^diff' >$3 # - do_patch : apply the patch to the # specified trunk; define it to understand your favorite diff format # - example: patch --no-backup-if-mismatch -d $2/ -p1 <$1 # - Note: Patchsync runs these functions under "pipefail" and "set -e". # Caution: "set -e" is idiosyncratic; you may wish to && together # successive commands anyway. Patchsync provides an "exitoneok" # function you can use to treat an exit code of 1 as 0. # - There are several possible ways to handle failed hunks. The simplest # and safest is to make do_patch fail, but that's inconvenient for the # user, who must investigate the *.rej files in the staging directory # and either fix the patch or fix the branch and force updating the # patch. One could make do_patch succeed, but if the user then modifies # the branch, the failed hunks will merely be dropped from the patch, # which is probably unacceptable. The clever way is to let do_patch # succeed but make do_diff fail if any *.rej files exist in the branch. # "filters" (optional): rsync filters to use when accessing the trunk and # branch; hide filters apply to reading, protect filters to writing; # hint: you probably want to hide and protect build outputs # # Other usage: patchsync --new # Mostly sets up a new staging directory for the given trunk, branch, and patch # at the given location. You still have to provide settings, and filters if # you want them. # - If one of the patch or branch exists, the other will be calculated when # you first synchronize. # - If both exist, you will get a conflict when you first synchronize and you # will need to specify which to update. # - If neither exists, you get an empty patch and a branch identical to the trunk. # Disable branch/.patchsync support because it's a bad idea in general, and the # cyclic symlink confuses Eclipse in particular. -- Matt 2006.11.30 # Error handling function handle_error { exec >&2 echo "Patchsync encountered an unexpected error! Aborting!" echo "The failed command was: $1" exit 2 } trap 'handle_error "$BASH_COMMAND"' ERR set -o errtrace set -o pipefail # Make sure we have rsync. type rsync >/dev/null 2>&1 || \ { echo "Patchsync requires rsync, but there's no rsync on your path!" 1>&2; exit 1; } # If a cp2 is available, use it; otherwise define our own. type cp2 >/dev/null 2>&1 || function cp2 { exec rsync -rltE --chmod=ugo=rwx "$@"; } function exitoneok { "$@" || [ $? == 1 ] } # wdpp_from ==> the shortest relative prefix-path from directory B to the current directory # (prefix-path means it ends in a slash unless it's `' which means '.') # "patchsync" uses this to link-dest when copying the branch out. # "patchsync --new" uses it to reverse the staging dir path when creating symlinks. function wdpp_from { AtoB="$1" # Start with symlink-followed absolute prefix-paths without the initial slash. # NOT bash builtin pwd; it tells us how we got here, not where we are pA="$(/bin/pwd)/" pA="${pA#/}" pB="$(cd "$AtoB" && /bin/pwd)/" pB="${pB#/}" # Lop off the longest common prefix of components that we can. # While first components are equal... # (Empty correctly doesn't equal remaining) while { [ -n "$pA" ] || [ -n "$pB" ]; } && [ "${pA%%/*}" == "${pB%%/*}" ]; do # Remove them. pA="${pA#*/}" pB="${pB#*/}" done ans="$pA" # Translate remaining components of $pB to ../s while [ -n "$pB" ]; do ans="../$ans" pB="${pB#*/}" done # Double check; add dot to the end to enforce ending in a slash and handle empty ans (cd "$AtoB" && [ "$ans." -ef /proc/self/fd/3 ]) 3<. [ $? == 0 ] # Yay echo "$ans" } function hash_file { # Lop off the filename and binary indicator sha1sum -b "$1" | sed -re 's/^([^ ]*).*$/\1/' } function patchsync_sync { if [ "$1" == --dry-run ]; then echo "Dry run mode." dryrun=1 shift fi staging="$1" if [ -r "$staging/settings" ]; then echo "Using staging dir $staging" else echo "Specify a staging directory containing a settings file!" 1>&2 exit 1 fi cd "$staging" || { echo "Failed to enter staging dir!" 1>&2; exit 1; } shift . ./settings type do_diff >/dev/null 2>&1 || { echo "do_diff is not defined!" 1>&2; exit 1; } type do_patch >/dev/null 2>&1 || { echo "do_patch is not defined!" 1>&2; exit 1; } whichtoupdate="$1" if [ -n "$whichtoupdate" ]; then echo "Updating $whichtoupdate according to command line argument." else echo "Synchronizing." fi filteropts=() ! [ -e filters ] || filteropts=("${filteropts[@]}" --filter='. filters') # 'R *' or 'S *' disables filtering on the staging dir side. COPYIN=(cp2 --del --filter='R *' "${filteropts[@]}") COPYOUT=(cp2 --del --filter='S *' "${filteropts[@]}" --no-t --checksum) # be nice to mtimes # hash_dir foo/ ==> a hash code covering all of the shown files in foo/ function hash_dir { # Itemize the dir, extract filenames, hash the files, and hash the list of # hashes. "${COPYIN[@]}" -i -n $1 nonexistent/ \ | sed -n -e '/^>f/{ s/^[^ ]* //; p }' \ | (cd $1 && xargs --no-run-if-empty --delimiter='\n' sha1sum -b) \ | hash_file /dev/stdin } echo "Checking for changes..." hash_dir trunk/ >trunk-new-hash cmp trunk-{save,new}-hash &>/dev/null || { trunkch=1; echo "Trunk has changed"; } hash_file patch >patch-new-hash cmp patch-{save,new}-hash &>/dev/null || { patchch=1; echo "Patch has changed"; } hash_dir branch/ >branch-new-hash cmp branch-{save,new}-hash &>/dev/null || { branchch=1; echo "Branch has changed"; } # If we're in synchronization mode, decide what to update. if [ -z "$whichtoupdate" ] && [[ -n $trunkch || -n $branchch || -n $patchch ]]; then if [ -e identical-branch-flag ] && ! [ $patchch ] && ! [ $branchch ]; then # We still want to create an identical branch. whichtoupdate=identical-branch elif ! [ $branchch ]; then # Trunk, patch, or both changed. Update branch. whichtoupdate=branch elif ! [ $patchch ]; then # Branch changed, and trunk may have also changed. Update patch. whichtoupdate=patch else # Branch and patch both changed. A message appears later. whichtoupdate=conflict fi #echo "Synchronization will update $whichtoupdate." fi # Remove old copy-out files to be clean and to make sure we don't # mistakenly copy them out this time. rm -rf patch-new branch-new if [ -n "$whichtoupdate" ]; then # Always show what would happen if patch-new and branch-new were copied out. # (If there was a problem creating one of them, patchsync would have just # deleted it.) But only actually copy them out and update synchronization # state if no error. error= function prepare_branch { echo "Preparing updated branch..." # No link-dest because we will modify and then link-dest when copying out "${COPYIN[@]}" trunk/ branch-new/ (do_patch patch branch-new) [ $? == 0 ] || { error=1; echo "Failed to prepare updated branch!" 1>&2; rm -rf branch-new; } } function prepare_patch { echo "Preparing updated patch..." # Link-dest is fine because these are temporary read-only copies "${COPYIN[@]}" --link-dest=../trunk/ trunk/ trunk-tmp/ "${COPYIN[@]}" --link-dest=../branch/ branch/ branch-tmp/ (do_diff trunk-tmp branch-tmp patch-new) [ $? == 0 ] || { error=1; echo "Failed to prepare updated patch!" 1>&2; rm -rf patch-new; } rm -rf trunk-tmp branch-tmp } case $whichtoupdate in (identical-branch) echo "Creating identical branch..." # No link-dest because we will link-dest when copying out "${COPYIN[@]}" trunk/ branch-new/ echo "Creating empty patch..." (do_diff branch-new branch-new patch-new) [ $? == 0 ] || { error=1; echo "Failed to create empty patch!" 1>&2; rm -rf patch-new; } ;; (branch) prepare_branch ;; (patch) prepare_patch ;; (conflict) error=1 cat <&2 CONFLICT: both branch and patch changed! Run patchsync {branch | patch} to update the specified thing from the others. I'll leave updated copies of both branch and patch in the staging directory to help you decide which way you want to update. EOF prepare_branch prepare_patch ;; (*) echo "Internal error, whichtoupdate should not be $whichtoupdate!" 1>&2 exit 1 ;; esac if ! [ $error ] && ! [ $dryrun ]; then echo "Copying out..." ! [ -e branch-new ] || { hash_dir branch-new/ >branch-new-hash linkdest="$(wdpp_from branch/)branch-new/" # Do separately so a failure in wdpp_from is noticed. "${COPYOUT[@]}" -i --link-dest="$linkdest" branch-new/ branch/ rm -rf branch-new } ! [ -e patch-new ] || cmp -s patch patch-new || { hash_file patch-new >patch-new-hash # Don't use rsync because we might have to write through a symlink. echo "> patch" cp --preserve=timestamps patch-new patch rm -f patch-new } echo "Remembering synchronized state for next time..." for i in trunk patch branch; do mv $i-new-hash $i-save-hash done else echo "Would copy out as follows:" ! [ -e branch-new ] || { hash_dir branch-new/ >branch-new-hash linkdest="$(wdpp_from branch/)branch-new/" # Do separately so a failure in wdpp_from is noticed. "${COPYOUT[@]}" -n -i --link-dest="$linkdest" branch-new/ branch/ #rm -rf branch-new } ! [ -e patch-new ] || cmp -s patch patch-new || { hash_file patch-new >patch-new-hash # Don't use rsync because we might have to write through a symlink. echo "> patch" #cp --preserve=timestamps patch-new patch #rm -f patch-new } echo "Would remember synchronized state for next time." echo "I'm leaving \"new\" files in the staging dir so you can inspect them." fi else # whichtoupdate # Easy case echo "Nothing changed." rm -f {trunk,patch,branch}-new-hash fi if [ $error ]; then echo "Synchronization failed." 1>&2 exit 1 else echo "Synchronization finished." if [ -e identical-branch-flag ]; then if ! [ $dryrun ]; then rm identical-branch-flag echo "Removed identical-branch-flag." else echo "Would remove identical-branch-flag." fi fi # Yay! Done patchsync_sync! fi } function patchsync_new { if [ $# != 4 ]; then echo "Expected 4 arguments after --new, got $#." 1>&2 echo "usage: patchsync --new " 1>&2 exit 1 fi # Set up arguments. trunk="$1" patch="$2" branch="$3" staging="$4" # What exists? ! [ -e "$staging" ] || { echo "Staging dir already exists!" 1>&2; exit 1; } [ -d "$trunk" ] || { echo "Trunk does not exist!" 1>&2; exit 1; } # Create staging dir. mkdir "$staging" wdpp="$(wdpp_from "$staging")" cd "$staging" echo "Created staging dir at $staging." # Adjust paths appropriately. [[ "$trunk" == /* ]] || trunk="$wdpp$trunk" [[ "$patch" == /* ]] || patch="$wdpp$patch" [[ "$branch" == /* ]] || branch="$wdpp$branch" # Create links to areas ln -s "$trunk" trunk ln -s "$patch" patch ln -s "$branch" branch echo "Created links to areas." # This approach is better than setting whichtochange because we'll notice # if the user puts something into one of the areas we created before first # sync. function create_patch { touch "$patch" hash_file patch >patch-save-hash echo "Created empty patch." } function create_branch { mkdir "$branch" # Can't do hash_dir because ${COPYIN[@]} hasn't been set <== no filters hash_file /dev/null >branch-save-hash echo "Created empty branch." } if [ -e "$patch" ] && ! [ -e "$branch" ]; then create_branch echo "Patch exists; branch will be calculated when you first synchronize." elif [ -e "$branch" ] && ! [ -e "$patch" ]; then create_patch echo "Branch exists; patch will be calculated when you first synchronize." elif ! [ -e "$patch" ] && ! [ -e "$branch" ]; then create_patch create_branch echo "Neither branch nor patch exists;" echo "a branch identical to the trunk will be created when you first synchronize." echo flag >identical-branch-flag echo "Created identical-branch-flag to tell first run of patchsync about this." else echo "Both patch and branch exist." echo "You will need to specify whether to overwrite the" echo "patch or the branch when you first synchronize!" fi # Write settings file. cat >settings < [branch | patch] patchsync --new Please read the top of the script for complete documentation. EOF } case "$1" in (--help|--version) patchsync_help ;; (--dry-run) patchsync_sync "$@" ;; (--new) shift patchsync_new "$@" ;; (''|--*) patchsync_help 1>&2 exit 1 ;; (*) patchsync_sync "$@" ;; esac