Import patchsync version 2.3
[utils/utils.git] / patchsync
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.3
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", but the
58 #         "set -e" it uses does not propagate into the functions.  Patchsync
59 #         provides an "exitoneok" function you can use to treat an exit code of
60 #         1 as 0.  You might want to && successive commands together.
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 set -e
87 trap "echo 'Patchsync encountered an unexpected error!  ABORTING!' 1>&2; exit 2;" ERR
88 set -o errtrace
89 set -o pipefail
90
91 # Make sure we have rsync.
92 type rsync >/dev/null 2>&1 || \
93         { echo "Patchsync requires rsync, but there's no rsync on your path!" 1>&2; exit 1; }
94 # If a cp2 is available, use it; otherwise define our own.
95 type cp2 >/dev/null 2>&1 || function cp2 { exec rsync -rltE --chmod=ugo=rwx "$@"; }
96
97 function exitoneok {
98         "$@" || [ $? == 1 ]
99 }
100
101 # wdpp_from <B> ==> the shortest relative prefix-path from directory B to the current directory
102 # (prefix-path means it ends in a slash unless it's `' which means '.')
103 # "patchsync" uses this to link-dest when copying the branch out.
104 # "patchsync --new" uses it to reverse the staging dir path when creating symlinks.
105 function wdpp_from {
106         AtoB="$1"
107         # Start with symlink-followed absolute prefix-paths without the initial slash.
108         # NOT bash builtin pwd; it tells us how we got here, not where we are
109         pA="$(/bin/pwd)/"
110         pA="${pA#/}"
111         pB="$( (cd "$AtoB" && /bin/pwd) )/"
112         pB="${pB#/}"
113         # Lop off the longest common prefix of components that we can.
114         # While first components are equal...
115         # (Empty correctly doesn't equal remaining)
116         while { [ -n "$pA" ] || [ -n "$pB" ]; } && [ "${pA%%/*}" == "${pB%%/*}" ]; do
117                 # Remove them.
118                 pA="${pA#*/}"
119                 pB="${pB#*/}"
120         done
121         ans="$pA"
122         # Translate remaining components of $pB to ../s
123         while [ -n "$pB" ]; do
124                 ans="$ans../"
125                 pB="${pB#*/}"
126         done
127         # Double check; add dot to the end to enforce ending in a slash and handle empty ans
128         (cd "$AtoB" && [ "$ans." -ef /proc/self/fd/3 ]) 3<.
129         # Yay
130         echo "$ans"
131 }
132
133 function hash_file {
134         # Lop off the filename and binary indicator
135         sha1sum -b "$1" | sed -re 's/^([^ ]*).*$/\1/'
136 }
137
138 function patchsync_sync {
139
140 if [ "$1" == --dry-run ]; then
141         echo "Dry run mode."
142         dryrun=1
143         shift
144 fi
145
146 staging="$1"
147 if [ -r "$staging/settings" ]; then
148         echo "Using staging dir $staging"
149 else
150         echo "Specify a staging directory containing a settings file!" 1>&2
151         exit 1
152 fi
153 cd "$staging" || { echo "Failed to enter staging dir!" 1>&2; exit 1; }
154 shift
155
156 . ./settings
157 type do_diff >/dev/null 2>&1 || { echo "do_diff is not defined!" 1>&2; exit 1; }
158 type do_patch >/dev/null 2>&1 || { echo "do_patch is not defined!" 1>&2; exit 1; }
159
160 whichtoupdate="$1"
161 # patchsync --new doesn't need this any more except for identical-branch
162 #if [ -z "$whichtoupdate" ] && [ -s whichtoupdate ]; then
163 #       # Hook for patchsync --new
164 #       whichtoupdate="$(< whichtoupdate)"
165 #       echo "Updating $whichtoupdate according to staging dir."
166 #el
167 if [ -n "$whichtoupdate" ]; then
168         echo "Updating $whichtoupdate according to command line argument."
169 else
170         echo "Synchronizing."
171 fi
172
173 filteropts=()
174 ! [ -e filters ] || filteropts=("${filteropts[@]}" --filter='. filters')
175 # 'R *' or 'S *' disables filtering on the staging dir side.
176
177 COPYIN=(cp2 --del --filter='R *' "${filteropts[@]}")
178 COPYOUT=(cp2 --del --filter='S *' "${filteropts[@]}" --no-t --checksum) # be nice to mtimes
179
180 # hash_dir foo/ ==> a hash code covering all of the shown files in foo/
181 function hash_dir {
182         # Itemize the dir, extract filenames, hash the files, and hash the list of
183         # hashes.
184         "${COPYIN[@]}" -i -n $1 nonexistent/ \
185                 | sed -n -e '/^>f/{ s/^[^ ]* //; p }' \
186                 | (cd $1 && xargs --no-run-if-empty --delimiter='\n' sha1sum -b) \
187                 | hash_file /dev/stdin
188 }
189
190 echo "Checking for changes..."
191 hash_dir trunk/ >trunk-new-hash
192 cmp trunk-{save,new}-hash &>/dev/null || { trunkch=1; echo "Trunk has changed"; }
193 hash_file patch >patch-new-hash
194 cmp patch-{save,new}-hash &>/dev/null || { patchch=1; echo "Patch has changed"; }
195 hash_dir branch/ >branch-new-hash
196 cmp branch-{save,new}-hash &>/dev/null || { branchch=1; echo "Branch has changed"; }
197
198 # If we're in synchronization mode, decide what to update.
199 if [ -z "$whichtoupdate" ] && [[ -n $trunkch || -n $branchch || -n $patchch ]]; then
200         if [ -e identical-branch-flag ] && ! [ $patchch ] && ! [ $branchch ]; then
201                 # We still want to create an identical branch.
202                 whichtoupdate=identical-branch
203         elif ! [ $branchch ]; then
204                 # Trunk, patch, or both changed.  Update branch.
205                 whichtoupdate=branch
206         elif ! [ $patchch ]; then
207                 # Branch changed, and trunk may have also changed.  Update patch.
208                 whichtoupdate=patch
209         else
210                 # Branch and patch both changed.  A message appears later.
211                 whichtoupdate=conflict
212         fi
213         #echo "Synchronization will update $whichtoupdate."
214 fi
215
216 # Remove old copy-out files to be clean and to make sure we don't
217 # mistakenly copy them out this time.
218 rm -rf patch-new branch-new
219
220 if [ -n "$whichtoupdate" ]; then
221
222 # Always show what would happen if patch-new and branch-new were copied out.
223 # (If there was a problem creating one of them, patchsync would have just
224 # deleted it.)  But only actually copy them out and update synchronization
225 # state if no error.
226 error=
227
228 function prepare_branch {
229         echo "Preparing updated branch..."
230         # No link-dest because we will modify and then link-dest when copying out
231         "${COPYIN[@]}" trunk/ branch-new/
232         do_patch patch branch-new || \
233                 { error=1; echo "Failed to prepare updated branch!" 1>&2; rm -rf branch-new; }
234 }
235
236 function prepare_patch {
237         echo "Preparing updated patch..."
238         # Link-dest is fine because these are temporary read-only copies
239         "${COPYIN[@]}" --link-dest=../trunk/ trunk/ trunk-tmp/
240         "${COPYIN[@]}" --link-dest=../branch/ branch/ branch-tmp/
241         do_diff trunk-tmp branch-tmp patch-new || \
242                 { error=1; echo "Failed to prepare updated patch!" 1>&2; rm -rf patch-new; }
243         rm -rf trunk-tmp branch-tmp
244 }
245
246 case $whichtoupdate in
247 (identical-branch)
248         echo "Creating identical branch..."
249         # No link-dest because we will link-dest when copying out
250         "${COPYIN[@]}" trunk/ branch-new/
251         echo "Creating empty patch..."
252         do_diff branch-new branch-new patch-new || \
253                 { error=1; echo "Failed to create empty patch!" 1>&2; rm -rf patch-new; }
254         ;;
255 (branch)
256         prepare_branch
257         ;;
258 (patch)
259         prepare_patch
260         ;;
261 (conflict)
262         error=1
263         cat <<EOF 1>&2
264 CONFLICT: both branch and patch changed!
265 Run patchsync <staging> {branch | patch} to
266 update the specified thing from the others.
267 I'll leave updated copies of both branch
268 and patch in the staging directory to help
269 you decide which way you want to update.
270 EOF
271         prepare_branch
272         prepare_patch
273         ;;
274 (*)
275         echo "Internal error, whichtoupdate should not be $whichtoupdate!" 1>&2
276         exit 1
277         ;;
278 esac
279
280 if ! [ $error ] && ! [ $dryrun ]; then
281         # Disable locking for now...
282         # ! [ -e lock ] || { echo "Staging dir is locked!  Delete the file \`lock' if the other instance of patchsync is gone." 1>&2; exit 1; }
283         # echo "patchsync lock file pid $$ date $(date)" >lock
284         
285         echo "Copying out..."
286         ! [ -e branch-new ] || {
287                 hash_dir branch-new/ >branch-new-hash
288                 "${COPYOUT[@]}" -i --link-dest="$(wdpp_from branch/)branch-new/" branch-new/ branch/
289                 rm -rf branch-new
290         }
291         ! [ -e patch-new ] || cmp -s patch patch-new || {
292                 hash_file patch-new >patch-new-hash
293                 # Don't use rsync because we might have to write through a symlink.
294                 echo "> patch"
295                 cp --preserve=timestamps patch-new patch
296                 rm -f patch-new
297         }
298         
299         echo "Remembering synchronized state for next time..."
300         for i in trunk patch branch; do
301                 mv $i-new-hash $i-save-hash
302         done
303         
304         # rm lock
305 else
306         echo "Would copy out as follows:"
307         ! [ -e branch-new ] || {
308                 hash_dir branch-new/ >branch-new-hash
309                 "${COPYOUT[@]}" -n -i --link-dest="$(wdpp_from branch/)branch-new/" branch-new/ branch/
310                 #rm -rf branch-new
311         }
312         ! [ -e patch-new ] || cmp -s patch patch-new || {
313                 hash_file patch-new >patch-new-hash
314                 # Don't use rsync because we might have to write through a symlink.
315                 echo "> patch"
316                 #cp --preserve=timestamps patch-new patch
317                 #rm -f patch-new
318         }
319         echo "Would remember synchronized state for next time."
320         echo "I'm leaving \"new\" files in the staging dir so you can inspect them."
321 fi
322
323 else # whichtoupdate
324         # Easy case
325         echo "Nothing changed."
326         rm -f {trunk,patch,branch}-new-hash
327 fi
328
329 if [ $error ]; then
330         echo "Synchronization failed." 1>&2
331         exit 1
332 else
333         echo "Synchronization finished."
334         if [ -e identical-branch-flag ]; then
335                 if ! [ $dryrun ]; then
336                         rm identical-branch-flag
337                         echo "Removed identical-branch-flag."
338                 else
339                         echo "Would remove identical-branch-flag."
340                 fi
341         fi
342         # Yay!  Done patchsync_sync!
343 fi
344 }
345
346 function patchsync_new {
347         if [ $# != 4 ]; then
348                 echo "Expected 4 arguments after --new, got $#." 1>&2
349                 echo "usage: patchsync --new <trunk> <patch> <branch> <staging>" 1>&2
350                 exit 1
351         fi
352         
353         # Set up arguments.
354         trunk="$1"
355         patch="$2"
356         branch="$3"
357         staging="$4"
358         
359         # What exists?
360         ! [ -e "$staging" ] || { echo "Staging dir already exists!" 1>&2; exit 1; }
361         [ -d "$trunk" ] || { echo "Trunk does not exist!" 1>&2; exit 1; }
362         
363         # Create staging dir.
364         mkdir "$staging"
365         wdpp="$(wdpp_from "$staging")"
366         cd "$staging"
367         echo "Created staging dir at $staging."
368         
369         # Adjust paths appropriately.
370         trunk="$wdpp$trunk"
371         patch="$wdpp$patch"
372         branch="$wdpp$branch"
373         
374         # Create links to areas
375         ln -s "$trunk" trunk
376         ln -s "$patch" patch
377         ln -s "$branch" branch
378         echo "Created links to areas."
379         
380         # This approach is better than setting whichtochange because we'll notice
381         # if the user puts something into one of the areas we created before first
382         # sync.
383         function create_patch {
384                 touch "$patch"
385                 hash_file patch >patch-save-hash
386                 echo "Created empty patch."
387         }
388         function create_branch {
389                 mkdir "$branch"
390                 # Can't do hash_dir because ${COPYIN[@]} hasn't been set <== no filters
391                 hash_file /dev/null >branch-save-hash
392                 echo "Created empty branch."
393         }
394         
395         if [ -e "$patch" ] && ! [ -e "$branch" ]; then
396                 create_branch
397                 echo "Patch exists; branch will be calculated when you first synchronize."
398         elif [ -e "$branch" ] && ! [ -e "$patch" ]; then
399                 create_patch
400                 echo "Branch exists; patch will be calculated when you first synchronize."
401         elif ! [ -e "$patch" ] && ! [ -e "$branch" ]; then
402                 create_patch
403                 create_branch
404                 echo "Neither branch nor patch exists;"
405                 echo "a branch identical to the trunk will be created when you first synchronize."
406                 echo flag >identical-branch-flag
407                 echo "Created identical-branch-flag to tell first run of patchsync about this."
408         else
409                 echo "Both patch and branch exist."
410                 echo "You will need to specify whether to overwrite the"
411                 echo "patch or the branch when you first synchronize!"
412         fi
413         
414         # Write settings file.
415         cat >settings <<END
416 # Define do_diff and do_patch here!
417 END
418         echo "Wrote settings file placeholder."
419         
420         echo ""
421         echo "Patchsync initialized."
422         echo "Now add your definitions of do_diff and do_patch to the settings file,"
423         echo "add a filter file if you wish, and perform the first sync."
424 }
425
426 function patchsync_help {
427         cat <<EOF
428 Patchsync version $PATCHSYNC_VERSION by Matt McCutchen
429 usage: patchsync [--dry-run] <staging> [branch | patch]
430        patchsync --new <trunk> <patch> <branch> <staging>
431 Please read the top of the script for complete documentation.
432 EOF
433 }
434
435 case "$1" in
436 (--help|--version)
437         patchsync_help ;;
438 (--dry-run)
439         patchsync_sync "$@" ;;
440 (--new)
441         shift
442         patchsync_new "$@" ;;
443 (''|--*)
444         patchsync_help 1>&2
445         exit 1 ;;
446 (*)
447         patchsync_sync "$@" ;;
448 esac