| 1 | #!/bin/bash |
| 2 | # patchsync: Synchronizes a trunk, a branch, and a patch containing the |
| 3 | # differences between them. |
| 4 | # -- Matt McCutchen |
| 5 | |
| 6 | # If I had to update the version in the --version message separately, I would forget. |
| 7 | PATCHSYNC_VERSION=2.4 |
| 8 | |
| 9 | # usage: patchsync [--dry-run] <staging> [branch | patch] |
| 10 | # |
| 11 | # Patchsync is invoked on a "staging directory", which holds some configuration |
| 12 | # (including the locations of the trunk, patch, and branch it is to synchronize) |
| 13 | # and some synchronization state. It determines whether each of the trunk, |
| 14 | # patch, and branch has changed since the last successful synchronization and |
| 15 | # updates the patch or branch as appropriate: |
| 16 | # |
| 17 | # Changed since last sync Patchsync's behavior |
| 18 | # ------------------------------------------------- |
| 19 | # Nothing Do nothing |
| 20 | # Trunk only Update branch |
| 21 | # Patch but not branch Update branch |
| 22 | # Branch but not patch Update patch |
| 23 | # Branch and patch Complain about conflict |
| 24 | # |
| 25 | # <staging>: path to the staging directory |
| 26 | # |
| 27 | # --dry-run: show what would happen without actually modifying the trunk, patch, |
| 28 | # branch, or synchronization state |
| 29 | # |
| 30 | # {branch | patch}: force patchsync to update the specified thing from the |
| 31 | # others instead of deciding automatically; you can use this argument to |
| 32 | # revert or to resolve a conflict |
| 33 | # |
| 34 | # CAVEAT: Patchsync might make a mess if the trunk, patch, or branch is |
| 35 | # modified in a way not hidden by the filters while patchsync is running! |
| 36 | # |
| 37 | # CAVEAT: Patchsync only notices creations, deletions, and modifications of |
| 38 | # regular files in the trunk and branch, not other changes like empty directory |
| 39 | # creations. If you make a change like that to the trunk, you can force |
| 40 | # patchsync to update the branch. |
| 41 | # |
| 42 | # Staging directory format: A staging directory contains the following items: |
| 43 | # "trunk", trunk directory or symlink to it |
| 44 | # "patch", patch regular file or symlink to it |
| 45 | # "branch", branch directory or symlink to it |
| 46 | # [Why symlinks? Expose as much as possible to tools like symlinks(8).] |
| 47 | # "settings", shell script defining the following shell functions: |
| 48 | # - do_diff <trunk> <branch> <write-patch>: diff the specified trunk and |
| 49 | # branch and write the patch to the specified file; define it to use |
| 50 | # your favorite diff format |
| 51 | # - example: exitoneok diff -urN $1 $2 \ |
| 52 | # | sed -re 's/^(\+\+\+|---) ([^\t]+).*$/\1 \2/' \ |
| 53 | # | exitoneok grep -v '^diff' >$3 |
| 54 | # - do_patch <patch> <convert-trunk-to-branch>: apply the patch to the |
| 55 | # specified trunk; define it to understand your favorite diff format |
| 56 | # - example: patch --no-backup-if-mismatch -d $2/ -p1 <$1 |
| 57 | # - Note: Patchsync runs these functions under "pipefail" and "set -e". |
| 58 | # Caution: "set -e" is idiosyncratic; you may wish to && together |
| 59 | # successive commands anyway. Patchsync provides an "exitoneok" |
| 60 | # function you can use to treat an exit code of 1 as 0. |
| 61 | # - There are several possible ways to handle failed hunks. The simplest |
| 62 | # and safest is to make do_patch fail, but that's inconvenient for the |
| 63 | # user, who must investigate the *.rej files in the staging directory |
| 64 | # and either fix the patch or fix the branch and force updating the |
| 65 | # patch. One could make do_patch succeed, but if the user then modifies |
| 66 | # the branch, the failed hunks will merely be dropped from the patch, |
| 67 | # which is probably unacceptable. The clever way is to let do_patch |
| 68 | # succeed but make do_diff fail if any *.rej files exist in the branch. |
| 69 | # "filters" (optional): rsync filters to use when accessing the trunk and |
| 70 | # branch; hide filters apply to reading, protect filters to writing; |
| 71 | # hint: you probably want to hide and protect build outputs |
| 72 | # |
| 73 | # Other usage: patchsync --new <trunk> <patch> <branch> <staging> |
| 74 | # Mostly sets up a new staging directory for the given trunk, branch, and patch |
| 75 | # at the given location. You still have to provide settings, and filters if |
| 76 | # you want them. |
| 77 | # - If one of the patch or branch exists, the other will be calculated when |
| 78 | # you first synchronize. |
| 79 | # - If both exist, you will get a conflict when you first synchronize and you |
| 80 | # will need to specify which to update. |
| 81 | # - If neither exists, you get an empty patch and a branch identical to the trunk. |
| 82 | |
| 83 | # Disable branch/.patchsync support because it's a bad idea in general, and the |
| 84 | # cyclic symlink confuses Eclipse in particular. -- Matt 2006.11.30 |
| 85 | |
| 86 | # Error handling |
| 87 | function handle_error { |
| 88 | exec >&2 |
| 89 | echo "Patchsync encountered an unexpected error! Aborting!" |
| 90 | echo "The failed command was: $1" |
| 91 | exit 2 |
| 92 | } |
| 93 | trap 'handle_error "$BASH_COMMAND"' ERR |
| 94 | set -o errtrace |
| 95 | set -o pipefail |
| 96 | |
| 97 | # Make sure we have rsync. |
| 98 | type rsync >/dev/null 2>&1 || \ |
| 99 | { echo "Patchsync requires rsync, but there's no rsync on your path!" 1>&2; exit 1; } |
| 100 | # If a cp2 is available, use it; otherwise define our own. |
| 101 | type cp2 >/dev/null 2>&1 || function cp2 { exec rsync -rltE --chmod=ugo=rwx "$@"; } |
| 102 | |
| 103 | function exitoneok { |
| 104 | "$@" || [ $? == 1 ] |
| 105 | } |
| 106 | |
| 107 | # wdpp_from <B> ==> the shortest relative prefix-path from directory B to the current directory |
| 108 | # (prefix-path means it ends in a slash unless it's `' which means '.') |
| 109 | # "patchsync" uses this to link-dest when copying the branch out. |
| 110 | # "patchsync --new" uses it to reverse the staging dir path when creating symlinks. |
| 111 | function wdpp_from { |
| 112 | AtoB="$1" |
| 113 | # Start with symlink-followed absolute prefix-paths without the initial slash. |
| 114 | # NOT bash builtin pwd; it tells us how we got here, not where we are |
| 115 | pA="$(/bin/pwd)/" |
| 116 | pA="${pA#/}" |
| 117 | pB="$(cd "$AtoB" && /bin/pwd)/" |
| 118 | pB="${pB#/}" |
| 119 | # Lop off the longest common prefix of components that we can. |
| 120 | # While first components are equal... |
| 121 | # (Empty correctly doesn't equal remaining) |
| 122 | while { [ -n "$pA" ] || [ -n "$pB" ]; } && [ "${pA%%/*}" == "${pB%%/*}" ]; do |
| 123 | # Remove them. |
| 124 | pA="${pA#*/}" |
| 125 | pB="${pB#*/}" |
| 126 | done |
| 127 | ans="$pA" |
| 128 | # Translate remaining components of $pB to ../s |
| 129 | while [ -n "$pB" ]; do |
| 130 | ans="../$ans" |
| 131 | pB="${pB#*/}" |
| 132 | done |
| 133 | # Double check; add dot to the end to enforce ending in a slash and handle empty ans |
| 134 | (cd "$AtoB" && [ "$ans." -ef /proc/self/fd/3 ]) 3<. |
| 135 | [ $? == 0 ] |
| 136 | # Yay |
| 137 | echo "$ans" |
| 138 | } |
| 139 | |
| 140 | function hash_file { |
| 141 | # Lop off the filename and binary indicator |
| 142 | sha1sum -b "$1" | sed -re 's/^([^ ]*).*$/\1/' |
| 143 | } |
| 144 | |
| 145 | function patchsync_sync { |
| 146 | |
| 147 | if [ "$1" == --dry-run ]; then |
| 148 | echo "Dry run mode." |
| 149 | dryrun=1 |
| 150 | shift |
| 151 | fi |
| 152 | |
| 153 | staging="$1" |
| 154 | if [ -r "$staging/settings" ]; then |
| 155 | echo "Using staging dir $staging" |
| 156 | else |
| 157 | echo "Specify a staging directory containing a settings file!" 1>&2 |
| 158 | exit 1 |
| 159 | fi |
| 160 | cd "$staging" || { echo "Failed to enter staging dir!" 1>&2; exit 1; } |
| 161 | shift |
| 162 | |
| 163 | . ./settings |
| 164 | type do_diff >/dev/null 2>&1 || { echo "do_diff is not defined!" 1>&2; exit 1; } |
| 165 | type do_patch >/dev/null 2>&1 || { echo "do_patch is not defined!" 1>&2; exit 1; } |
| 166 | |
| 167 | whichtoupdate="$1" |
| 168 | if [ -n "$whichtoupdate" ]; then |
| 169 | echo "Updating $whichtoupdate according to command line argument." |
| 170 | else |
| 171 | echo "Synchronizing." |
| 172 | fi |
| 173 | |
| 174 | filteropts=() |
| 175 | ! [ -e filters ] || filteropts=("${filteropts[@]}" --filter='. filters') |
| 176 | # 'R *' or 'S *' disables filtering on the staging dir side. |
| 177 | |
| 178 | COPYIN=(cp2 --del --filter='R *' "${filteropts[@]}") |
| 179 | COPYOUT=(cp2 --del --filter='S *' "${filteropts[@]}" --no-t --checksum) # be nice to mtimes |
| 180 | |
| 181 | # hash_dir foo/ ==> a hash code covering all of the shown files in foo/ |
| 182 | function hash_dir { |
| 183 | # Itemize the dir, extract filenames, hash the files, and hash the list of |
| 184 | # hashes. |
| 185 | "${COPYIN[@]}" -i -n $1 nonexistent/ \ |
| 186 | | sed -n -e '/^>f/{ s/^[^ ]* //; p }' \ |
| 187 | | (cd $1 && xargs --no-run-if-empty --delimiter='\n' sha1sum -b) \ |
| 188 | | hash_file /dev/stdin |
| 189 | } |
| 190 | |
| 191 | echo "Checking for changes..." |
| 192 | hash_dir trunk/ >trunk-new-hash |
| 193 | cmp trunk-{save,new}-hash &>/dev/null || { trunkch=1; echo "Trunk has changed"; } |
| 194 | hash_file patch >patch-new-hash |
| 195 | cmp patch-{save,new}-hash &>/dev/null || { patchch=1; echo "Patch has changed"; } |
| 196 | hash_dir branch/ >branch-new-hash |
| 197 | cmp branch-{save,new}-hash &>/dev/null || { branchch=1; echo "Branch has changed"; } |
| 198 | |
| 199 | # If we're in synchronization mode, decide what to update. |
| 200 | if [ -z "$whichtoupdate" ] && [[ -n $trunkch || -n $branchch || -n $patchch ]]; then |
| 201 | if [ -e identical-branch-flag ] && ! [ $patchch ] && ! [ $branchch ]; then |
| 202 | # We still want to create an identical branch. |
| 203 | whichtoupdate=identical-branch |
| 204 | elif ! [ $branchch ]; then |
| 205 | # Trunk, patch, or both changed. Update branch. |
| 206 | whichtoupdate=branch |
| 207 | elif ! [ $patchch ]; then |
| 208 | # Branch changed, and trunk may have also changed. Update patch. |
| 209 | whichtoupdate=patch |
| 210 | else |
| 211 | # Branch and patch both changed. A message appears later. |
| 212 | whichtoupdate=conflict |
| 213 | fi |
| 214 | #echo "Synchronization will update $whichtoupdate." |
| 215 | fi |
| 216 | |
| 217 | # Remove old copy-out files to be clean and to make sure we don't |
| 218 | # mistakenly copy them out this time. |
| 219 | rm -rf patch-new branch-new |
| 220 | |
| 221 | if [ -n "$whichtoupdate" ]; then |
| 222 | |
| 223 | # Always show what would happen if patch-new and branch-new were copied out. |
| 224 | # (If there was a problem creating one of them, patchsync would have just |
| 225 | # deleted it.) But only actually copy them out and update synchronization |
| 226 | # state if no error. |
| 227 | error= |
| 228 | |
| 229 | function prepare_branch { |
| 230 | echo "Preparing updated branch..." |
| 231 | # No link-dest because we will modify and then link-dest when copying out |
| 232 | "${COPYIN[@]}" trunk/ branch-new/ |
| 233 | (do_patch patch branch-new) |
| 234 | [ $? == 0 ] || { error=1; echo "Failed to prepare updated branch!" 1>&2; rm -rf branch-new; } |
| 235 | } |
| 236 | |
| 237 | function prepare_patch { |
| 238 | echo "Preparing updated patch..." |
| 239 | # Link-dest is fine because these are temporary read-only copies |
| 240 | "${COPYIN[@]}" --link-dest=../trunk/ trunk/ trunk-tmp/ |
| 241 | "${COPYIN[@]}" --link-dest=../branch/ branch/ branch-tmp/ |
| 242 | (do_diff trunk-tmp branch-tmp patch-new) |
| 243 | [ $? == 0 ] || { error=1; echo "Failed to prepare updated patch!" 1>&2; rm -rf patch-new; } |
| 244 | rm -rf trunk-tmp branch-tmp |
| 245 | } |
| 246 | |
| 247 | case $whichtoupdate in |
| 248 | (identical-branch) |
| 249 | echo "Creating identical branch..." |
| 250 | # No link-dest because we will link-dest when copying out |
| 251 | "${COPYIN[@]}" trunk/ branch-new/ |
| 252 | echo "Creating empty patch..." |
| 253 | (do_diff branch-new branch-new patch-new) |
| 254 | [ $? == 0 ] || { error=1; echo "Failed to create empty patch!" 1>&2; rm -rf patch-new; } |
| 255 | ;; |
| 256 | (branch) |
| 257 | prepare_branch |
| 258 | ;; |
| 259 | (patch) |
| 260 | prepare_patch |
| 261 | ;; |
| 262 | (conflict) |
| 263 | error=1 |
| 264 | cat <<EOF 1>&2 |
| 265 | CONFLICT: both branch and patch changed! |
| 266 | Run patchsync <staging> {branch | patch} to |
| 267 | update the specified thing from the others. |
| 268 | I'll leave updated copies of both branch |
| 269 | and patch in the staging directory to help |
| 270 | you decide which way you want to update. |
| 271 | EOF |
| 272 | prepare_branch |
| 273 | prepare_patch |
| 274 | ;; |
| 275 | (*) |
| 276 | echo "Internal error, whichtoupdate should not be $whichtoupdate!" 1>&2 |
| 277 | exit 1 |
| 278 | ;; |
| 279 | esac |
| 280 | |
| 281 | if ! [ $error ] && ! [ $dryrun ]; then |
| 282 | echo "Copying out..." |
| 283 | ! [ -e branch-new ] || { |
| 284 | hash_dir branch-new/ >branch-new-hash |
| 285 | linkdest="$(wdpp_from branch/)branch-new/" # Do separately so a failure in wdpp_from is noticed. |
| 286 | "${COPYOUT[@]}" -i --link-dest="$linkdest" branch-new/ branch/ |
| 287 | rm -rf branch-new |
| 288 | } |
| 289 | ! [ -e patch-new ] || cmp -s patch patch-new || { |
| 290 | hash_file patch-new >patch-new-hash |
| 291 | # Don't use rsync because we might have to write through a symlink. |
| 292 | echo "> patch" |
| 293 | cp --preserve=timestamps patch-new patch |
| 294 | rm -f patch-new |
| 295 | } |
| 296 | |
| 297 | echo "Remembering synchronized state for next time..." |
| 298 | for i in trunk patch branch; do |
| 299 | mv $i-new-hash $i-save-hash |
| 300 | done |
| 301 | else |
| 302 | echo "Would copy out as follows:" |
| 303 | ! [ -e branch-new ] || { |
| 304 | hash_dir branch-new/ >branch-new-hash |
| 305 | linkdest="$(wdpp_from branch/)branch-new/" # Do separately so a failure in wdpp_from is noticed. |
| 306 | "${COPYOUT[@]}" -n -i --link-dest="$linkdest" branch-new/ branch/ |
| 307 | #rm -rf branch-new |
| 308 | } |
| 309 | ! [ -e patch-new ] || cmp -s patch patch-new || { |
| 310 | hash_file patch-new >patch-new-hash |
| 311 | # Don't use rsync because we might have to write through a symlink. |
| 312 | echo "> patch" |
| 313 | #cp --preserve=timestamps patch-new patch |
| 314 | #rm -f patch-new |
| 315 | } |
| 316 | echo "Would remember synchronized state for next time." |
| 317 | echo "I'm leaving \"new\" files in the staging dir so you can inspect them." |
| 318 | fi |
| 319 | |
| 320 | else # whichtoupdate |
| 321 | # Easy case |
| 322 | echo "Nothing changed." |
| 323 | rm -f {trunk,patch,branch}-new-hash |
| 324 | fi |
| 325 | |
| 326 | if [ $error ]; then |
| 327 | echo "Synchronization failed." 1>&2 |
| 328 | exit 1 |
| 329 | else |
| 330 | echo "Synchronization finished." |
| 331 | if [ -e identical-branch-flag ]; then |
| 332 | if ! [ $dryrun ]; then |
| 333 | rm identical-branch-flag |
| 334 | echo "Removed identical-branch-flag." |
| 335 | else |
| 336 | echo "Would remove identical-branch-flag." |
| 337 | fi |
| 338 | fi |
| 339 | # Yay! Done patchsync_sync! |
| 340 | fi |
| 341 | } |
| 342 | |
| 343 | function patchsync_new { |
| 344 | if [ $# != 4 ]; then |
| 345 | echo "Expected 4 arguments after --new, got $#." 1>&2 |
| 346 | echo "usage: patchsync --new <trunk> <patch> <branch> <staging>" 1>&2 |
| 347 | exit 1 |
| 348 | fi |
| 349 | |
| 350 | # Set up arguments. |
| 351 | trunk="$1" |
| 352 | patch="$2" |
| 353 | branch="$3" |
| 354 | staging="$4" |
| 355 | |
| 356 | # What exists? |
| 357 | ! [ -e "$staging" ] || { echo "Staging dir already exists!" 1>&2; exit 1; } |
| 358 | [ -d "$trunk" ] || { echo "Trunk does not exist!" 1>&2; exit 1; } |
| 359 | |
| 360 | # Create staging dir. |
| 361 | mkdir "$staging" |
| 362 | wdpp="$(wdpp_from "$staging")" |
| 363 | cd "$staging" |
| 364 | echo "Created staging dir at $staging." |
| 365 | |
| 366 | # Adjust paths appropriately. |
| 367 | [[ "$trunk" == /* ]] || trunk="$wdpp$trunk" |
| 368 | [[ "$patch" == /* ]] || patch="$wdpp$patch" |
| 369 | [[ "$branch" == /* ]] || branch="$wdpp$branch" |
| 370 | |
| 371 | # Create links to areas |
| 372 | ln -s "$trunk" trunk |
| 373 | ln -s "$patch" patch |
| 374 | ln -s "$branch" branch |
| 375 | echo "Created links to areas." |
| 376 | |
| 377 | # This approach is better than setting whichtochange because we'll notice |
| 378 | # if the user puts something into one of the areas we created before first |
| 379 | # sync. |
| 380 | function create_patch { |
| 381 | touch "$patch" |
| 382 | hash_file patch >patch-save-hash |
| 383 | echo "Created empty patch." |
| 384 | } |
| 385 | function create_branch { |
| 386 | mkdir "$branch" |
| 387 | # Can't do hash_dir because ${COPYIN[@]} hasn't been set <== no filters |
| 388 | hash_file /dev/null >branch-save-hash |
| 389 | echo "Created empty branch." |
| 390 | } |
| 391 | |
| 392 | if [ -e "$patch" ] && ! [ -e "$branch" ]; then |
| 393 | create_branch |
| 394 | echo "Patch exists; branch will be calculated when you first synchronize." |
| 395 | elif [ -e "$branch" ] && ! [ -e "$patch" ]; then |
| 396 | create_patch |
| 397 | echo "Branch exists; patch will be calculated when you first synchronize." |
| 398 | elif ! [ -e "$patch" ] && ! [ -e "$branch" ]; then |
| 399 | create_patch |
| 400 | create_branch |
| 401 | echo "Neither branch nor patch exists;" |
| 402 | echo "a branch identical to the trunk will be created when you first synchronize." |
| 403 | echo flag >identical-branch-flag |
| 404 | echo "Created identical-branch-flag to tell first run of patchsync about this." |
| 405 | else |
| 406 | echo "Both patch and branch exist." |
| 407 | echo "You will need to specify whether to overwrite the" |
| 408 | echo "patch or the branch when you first synchronize!" |
| 409 | fi |
| 410 | |
| 411 | # Write settings file. |
| 412 | cat >settings <<END |
| 413 | # Define do_diff and do_patch here! |
| 414 | END |
| 415 | echo "Wrote settings file placeholder." |
| 416 | |
| 417 | echo "" |
| 418 | echo "Patchsync initialized." |
| 419 | echo "Now add your definitions of do_diff and do_patch to the settings file," |
| 420 | echo "add a filter file if you wish, and perform the first sync." |
| 421 | } |
| 422 | |
| 423 | function patchsync_help { |
| 424 | cat <<EOF |
| 425 | Patchsync version $PATCHSYNC_VERSION by Matt McCutchen |
| 426 | usage: patchsync [--dry-run] <staging> [branch | patch] |
| 427 | patchsync --new <trunk> <patch> <branch> <staging> |
| 428 | Please read the top of the script for complete documentation. |
| 429 | EOF |
| 430 | } |
| 431 | |
| 432 | case "$1" in |
| 433 | (--help|--version) |
| 434 | patchsync_help ;; |
| 435 | (--dry-run) |
| 436 | patchsync_sync "$@" ;; |
| 437 | (--new) |
| 438 | shift |
| 439 | patchsync_new "$@" ;; |
| 440 | (''|--*) |
| 441 | patchsync_help 1>&2 |
| 442 | exit 1 ;; |
| 443 | (*) |
| 444 | patchsync_sync "$@" ;; |
| 445 | esac |