2 # patchsync: Synchronizes a trunk, a branch, and a patch containing the
3 # differences between them.
7 # usage: patchsync [--dry-run] <staging> [branch | patch]
9 # Patchsync is invoked on a "staging directory", which holds some configuration
10 # (including the locations of the trunk, patch, and branch it is to synchronize)
11 # and some synchronization state. It determines whether each of the trunk,
12 # patch, and branch has changed since the last successful synchronization and
13 # updates the patch or branch as appropriate:
15 # Changed since last sync Patchsync's behavior
16 # -------------------------------------------------
18 # Trunk only Update branch
19 # Patch but not branch Update branch
20 # Branch but not patch Update patch
21 # Branch and patch Complain about conflict
23 # <staging>: path to the staging directory
25 # --dry-run: show what would happen without actually modifying the trunk, patch,
26 # branch, or synchronization state
28 # {branch | patch}: force patchsync to update the specified thing from the
29 # others instead of deciding automatically; you can use this argument to
30 # revert or to resolve a conflict
32 # CAVEAT: Patchsync might make a mess if the trunk, patch, or branch is
33 # modified in a way not hidden by the filters while patchsync is running!
35 # CAVEAT: Patchsync only notices creations, deletions, and modifications of
36 # regular files in the trunk and branch, not other changes like empty directory
37 # creations. If you make a change like that to the trunk, you can force
38 # patchsync to update the branch.
40 # Staging directory format: A staging directory contains the following items:
41 # "trunk", trunk directory or symlink to it
42 # "patch", patch regular file or symlink to it
43 # "branch", branch directory or symlink to it
44 # [Why symlinks? Expose as much as possible to tools like symlinks(8).]
45 # "settings", shell script defining the following shell functions:
46 # - do_diff <trunk> <branch> <write-patch>: diff the specified trunk and
47 # branch and write the patch to the specified file; define it to use
48 # your favorite diff format
49 # - example: exitoneok diff -urN $1 $2 \
50 # | sed -re 's/^(\+\+\+|---) ([^\t]+).*$/\1 \2/' \
51 # | exitoneok grep -v '^diff' >$3
52 # - do_patch <patch> <convert-trunk-to-branch>: apply the patch to the
53 # specified trunk; define it to understand your favorite diff format
54 # - example: patch --no-backup-if-mismatch -d $2/ -p1 <$1
55 # - Note: patchsync runs these functions under "pipefail", but the
56 # "set -e" it uses does not propagate into the functions. Patchsync
57 # provides an "exitoneok" function you can use to treat an exit code of
58 # 1 as 0. You might want to && successive commands together.
59 # - There are several possible ways to handle failed hunks. The simplest
60 # and safest is to make do_patch fail, but that's inconvenient for the
61 # user, who must investigate the *.rej files in the staging directory
62 # and either fix the patch or fix the branch and force updating the
63 # patch. One could make do_patch succeed, but if the user then modifies
64 # the branch, the failed hunks will merely be dropped from the patch,
65 # which is probably unacceptable. The clever way is to let do_patch
66 # succeed but make do_diff fail if any *.rej files exist in the branch.
67 # "filters" (optional): rsync filters to use when accessing the trunk and
68 # branch; hide filters apply to reading, protect filters to writing;
69 # hint: you probably want to hide and protect build outputs
71 # Other usage: patchsync --new <trunk> <patch> <branch> <staging>
72 # Mostly sets up a new staging directory for the given trunk, branch, and patch
73 # at the given location. You still have to provide settings, and filters if
75 # - If one of the patch or branch exists, the other will be calculated when
76 # you first synchronize.
77 # - If both exist, you will get a conflict when you first synchronize and you
78 # will need to specify which to update.
79 # - If neither exists, you get an empty patch and a branch identical to the trunk.
81 # Disable branch/.patchsync support because it's a bad idea in general, and the
82 # cyclic symlink confuses Eclipse in particular. -- Matt 2006.11.30
85 trap "echo 'Patchsync encountered an unexpected error! ABORTING!' 1>&2; exit 2;" ERR
89 # Make sure we have rsync.
90 type rsync >/dev/null 2>&1 || \
91 { echo "Patchsync requires rsync, but there's no rsync on your path!" 1>&2; exit 1; }
92 # If a cp2 is available, use it; otherwise define our own.
93 type cp2 >/dev/null 2>&1 || function cp2 { rsync -rltE --chmod=ugo=rwx "$@"; }
99 # wdpp_from <B> ==> the shortest relative prefix-path from directory B to the current directory
100 # (prefix-path means it ends in a slash unless it's `' which means '.')
101 # "patchsync" uses this to link-dest when copying the branch out.
102 # "patchsync --new" uses it to reverse the staging dir path when creating symlinks.
105 # Start with symlink-followed absolute prefix-paths without the initial slash.
106 # NOT bash builtin pwd; it tells us how we got here, not where we are
109 pB="$( (cd "$AtoB" && /bin/pwd) )/"
111 # Lop off the longest common prefix of components that we can.
112 # While first components are equal...
113 # (Empty correctly doesn't equal remaining)
114 while { [ -n "$pA" ] || [ -n "$pB" ]; } && [ "${pA%%/*}" == "${pB%%/*}" ]; do
120 # Translate remaining components of $pB to ../s
121 while [ -n "$pB" ]; do
125 # Double check; add dot to the end to enforce ending in a slash and handle empty ans
126 (cd "$AtoB" && [ "$ans." -ef /proc/self/fd/3 ]) 3<.
132 # Lop off the filename and binary indicator
133 sha1sum -b "$1" | sed -re 's/^([^ ]*).*$/\1/'
136 function patchsync_sync {
138 if [ "$1" == --dry-run ]; then
145 if [ -r "$staging/settings" ]; then
146 echo "Using staging dir $staging"
148 echo "Specify a staging directory containing a settings file!" 1>&2
151 cd "$staging" || { echo "Failed to enter staging dir!" 1>&2; exit 1; }
155 type do_diff >/dev/null 2>&1 || { echo "do_diff is not defined!" 1>&2; exit 1; }
156 type do_patch >/dev/null 2>&1 || { echo "do_patch is not defined!" 1>&2; exit 1; }
159 # patchsync --new doesn't need this any more except for identical-branch
160 #if [ -z "$whichtoupdate" ] && [ -s whichtoupdate ]; then
161 # # Hook for patchsync --new
162 # whichtoupdate="$(< whichtoupdate)"
163 # echo "Updating $whichtoupdate according to staging dir."
165 if [ -n "$whichtoupdate" ]; then
166 echo "Updating $whichtoupdate according to command line argument."
168 echo "Synchronizing."
172 ! [ -e filters ] || filteropts=("${filteropts[@]}" --filter='. filters')
173 # 'R *' or 'S *' disables filtering on the staging dir side.
175 COPYIN=(cp2 --del --filter='R *' "${filteropts[@]}")
176 COPYOUT=(cp2 --del --filter='S *' "${filteropts[@]}" --no-t --checksum) # be nice to mtimes
178 # hash_dir foo/ ==> a hash code covering all of the shown files in foo/
180 # Itemize the dir, extract filenames, hash the files, and hash the list of
182 "${COPYIN[@]}" -i -n $1 nonexistent/ \
183 | sed -n -e '/^>f/{ s/^[^ ]* //; p }' \
184 | (cd $1 && xargs --no-run-if-empty --delimiter='\n' sha1sum -b) \
185 | hash_file /dev/stdin
188 echo "Checking for changes..."
189 hash_dir trunk/ >trunk-new-hash
190 cmp trunk-{save,new}-hash &>/dev/null || { trunkch=1; echo "Trunk has changed"; }
191 hash_file patch >patch-new-hash
192 cmp patch-{save,new}-hash &>/dev/null || { patchch=1; echo "Patch has changed"; }
193 hash_dir branch/ >branch-new-hash
194 cmp branch-{save,new}-hash &>/dev/null || { branchch=1; echo "Branch has changed"; }
196 # If we're in synchronization mode, decide what to update.
197 if [ -z "$whichtoupdate" ] && [[ -n $trunkch || -n $branchch || -n $patchch ]]; then
198 if [ -e identical-branch-flag ] && ! [ $patchch ] && ! [ $branchch ]; then
199 # We still want to create an identical branch.
200 whichtoupdate=identical-branch
201 elif ! [ $branchch ]; then
202 # Trunk, patch, or both changed. Update branch.
204 elif ! [ $patchch ]; then
205 # Branch changed, and trunk may have also changed. Update patch.
208 # Branch and patch both changed. A message appears later.
209 whichtoupdate=conflict
211 #echo "Synchronization will update $whichtoupdate."
214 # Remove old copy-out files to be clean and to make sure we don't
215 # mistakenly copy them out this time.
216 rm -rf patch-new branch-new
218 if [ -n "$whichtoupdate" ]; then
220 # Always show what would happen if patch-new and branch-new were copied out.
221 # (If there was a problem creating one of them, patchsync would have just
222 # deleted it.) But only actually copy them out and update synchronization
226 function prepare_branch {
227 echo "Preparing updated branch..."
228 # No link-dest because we will modify and then link-dest when copying out
229 "${COPYIN[@]}" trunk/ branch-new/
230 do_patch patch branch-new || \
231 { error=1; echo "Failed to prepare updated branch!" 1>&2; rm -rf branch-new; }
234 function prepare_patch {
235 echo "Preparing updated patch..."
236 # Link-dest is fine because these are temporary read-only copies
237 "${COPYIN[@]}" --link-dest=../trunk/ trunk/ trunk-tmp/
238 "${COPYIN[@]}" --link-dest=../branch/ branch/ branch-tmp/
239 do_diff trunk-tmp branch-tmp patch-new || \
240 { error=1; echo "Failed to prepare updated patch!" 1>&2; rm -rf patch-new; }
241 rm -rf trunk-tmp branch-tmp
244 case $whichtoupdate in
246 echo "Creating identical branch..."
247 # No link-dest because we will link-dest when copying out
248 "${COPYIN[@]}" trunk/ branch-new/
249 echo "Creating empty patch..."
250 do_diff branch-new branch-new patch-new || \
251 { error=1; echo "Failed to create empty patch!" 1>&2; rm -rf patch-new; }
262 CONFLICT: both branch and patch changed!
263 Run patchsync <staging> {branch | patch} to
264 update the specified thing from the others.
265 I'll leave updated copies of both branch
266 and patch in the staging directory to help
267 you decide which way you want to update.
273 echo "Internal error, whichtoupdate should not be $whichtoupdate!" 1>&2
278 if ! [ $error ] && ! [ $dryrun ]; then
279 # Disable locking for now...
280 # ! [ -e lock ] || { echo "Staging dir is locked! Delete the file \`lock' if the other instance of patchsync is gone." 1>&2; exit 1; }
281 # echo "patchsync lock file pid $$ date $(date)" >lock
283 echo "Copying out..."
284 ! [ -e branch-new ] || {
285 hash_dir branch-new/ >branch-new-hash
286 "${COPYOUT[@]}" -i --link-dest="$(wdpp_from branch/)branch-new/" branch-new/ branch/
289 ! [ -e patch-new ] || cmp -s patch-work patch || {
290 hash_file patch-new >patch-new-hash
291 # Don't use rsync because we might have to write through a symlink.
293 cp --preserve=timestamps patch-new patch
297 echo "Remembering synchronized state for next time..."
298 for i in trunk patch branch; do
299 mv $i-new-hash $i-save-hash
304 echo "Would copy out as follows:"
305 ! [ -e branch-new ] || {
306 hash_dir branch-new/ >branch-new-hash
307 "${COPYOUT[@]}" -n -i --link-dest="$(wdpp_from branch/)branch-new/" branch-new/ branch/
310 ! [ -e patch-new ] || cmp -s patch-work patch || {
311 hash_file patch-new >patch-new-hash
312 # Don't use rsync because we might have to write through a symlink.
314 #cp --preserve=timestamps patch-new patch
317 echo "Would remember synchronized state for next time."
318 echo "I'm leaving \"new\" files in the staging dir so you can inspect them."
323 echo "Nothing changed."
324 rm -f {trunk,patch,branch}-new-hash
328 echo "Synchronization failed." 1>&2
331 echo "Synchronization finished."
332 if [ -e identical-branch-flag ]; then
333 if ! [ $dryrun ]; then
334 rm identical-branch-flag
335 echo "Removed identical-branch-flag."
337 echo "Would remove identical-branch-flag."
340 # Yay! Done patchsync_sync!
344 function patchsync_new {
346 echo "Expected 4 arguments after --new, got $#." 1>&2
347 echo "usage: patchsync --new <trunk> <patch> <branch> <staging>" 1>&2
351 # Set up arguments. Open templates because we will change directories.
357 # What exists? Whichtochange first?
358 ! [ -e "$staging" ] || { echo "Staging dir already exists!" 1>&2; exit 1; }
359 [ -d "$trunk" ] || { echo "Trunk does not exist!" 1>&2; exit 1; }
361 # Create staging dir.
363 wdpp="$(wdpp_from "$staging")"
365 echo "Created staging dir at $staging."
367 # Adjust paths appropriately.
370 branch="$wdpp$branch"
372 # Create links to areas
375 ln -s "$branch" branch
376 echo "Created links to areas."
378 # This approach is better than setting whichtochange because we'll notice
379 # if the user puts something into one of the areas we created before first
381 function create_patch {
383 hash_file patch >patch-save-hash
384 echo "Created empty patch."
386 function create_branch {
388 # Can't do hash_dir because ${COPYIN[@]} hasn't been set <== no filters
389 hash_file /dev/null >branch-save-hash
390 echo "Created empty branch."
393 if [ -e "$patch" ] && ! [ -e "$branch" ]; then
395 echo "Patch exists; branch will be calculated when you first synchronize."
396 elif [ -e "$branch" ] && ! [ -e "$patch" ]; then
398 echo "Branch exists; patch will be calculated when you first synchronize."
399 elif ! [ -e "$patch" ] && ! [ -e "$branch" ]; then
402 echo "Neither branch nor patch exists;"
403 echo "a branch identical to the trunk will be created when you first synchronize."
404 echo flag >identical-branch-flag
405 echo "Created identical-branch-flag to tell first run of patchsync about this."
407 echo "Both patch and branch exist."
408 echo "You will need to specify whether to overwrite the"
409 echo "patch or the branch when you first synchronize!"
412 # Write settings file.
414 # Define do_diff and do_patch here!
416 echo "Wrote settings file placeholder."
419 echo "Patchsync initialized."
420 echo "Now add your definitions of do_diff and do_patch to the settings file,"
421 echo "add a filter file if you wish, and perform the first sync."
424 function patchsync_help {
426 Patchsync version 2 by Matt McCutchen
427 usage: patchsync [--dry-run] <staging> [branch | patch]
428 patchsync --new <trunk> <patch> <branch> <staging>
429 Please read the top of the script for complete documentation.
437 patchsync_sync "$@" ;;
440 patchsync_new "$@" ;;
445 patchsync_sync "$@" ;;