07e2f0fab2bb22f9f2f86cab1c94d552aef1c104
[utils/utils.git] / patchsync
1 #!/bin/bash
2 # patchsync: Synchronizes a trunk, a branch, and a patch containing the
3 # differences between them.
4 # Version 2
5 # -- Matt McCutchen
6 #
7 # usage: patchsync [--dry-run] <staging> [branch | patch]
8 #
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:
14
15 # Changed since last sync   Patchsync's behavior
16 # -------------------------------------------------
17 # Nothing                   Do nothing
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
22 #
23 # <staging>: path to the staging directory
24 #
25 # --dry-run: show what would happen without actually modifying the trunk, patch,
26 #   branch, or synchronization state
27 #
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
31 #
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!
34 #
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.
39 #
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
70 #
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
74 # you want them.
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.
80
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
83
84 set -e
85 trap "echo 'Patchsync encountered an unexpected error!  ABORTING!' 1>&2; exit 2;" ERR
86 set -o errtrace
87 set -o pipefail
88
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 "$@"; }
94
95 function exitoneok {
96         "$@" || [ $? == 1 ]
97 }
98
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.
103 function wdpp_from {
104         AtoB="$1"
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
107         pA="$(/bin/pwd)/"
108         pA="${pA#/}"
109         pB="$( (cd "$AtoB" && /bin/pwd) )/"
110         pB="${pB#/}"
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
115                 # Remove them.
116                 pA="${pA#*/}"
117                 pB="${pB#*/}"
118         done
119         ans="$pA"
120         # Translate remaining components of $pB to ../s
121         while [ -n "$pB" ]; do
122                 ans="$ans../"
123                 pB="${pB#*/}"
124         done
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<.
127         # Yay
128         echo "$ans"
129 }
130
131 function hash_file {
132         # Lop off the filename and binary indicator
133         sha1sum -b "$1" | sed -re 's/^([^ ]*).*$/\1/'
134 }
135
136 function patchsync_sync {
137
138 if [ "$1" == --dry-run ]; then
139         echo "Dry run mode."
140         dryrun=1
141         shift
142 fi
143
144 staging="$1"
145 if [ -r "$staging/settings" ]; then
146         echo "Using staging dir $staging"
147 else
148         echo "Specify a staging directory containing a settings file!" 1>&2
149         exit 1
150 fi
151 cd "$staging" || { echo "Failed to enter staging dir!" 1>&2; exit 1; }
152 shift
153
154 . settings
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; }
157
158 whichtoupdate="$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."
164 #el
165 if [ -n "$whichtoupdate" ]; then
166         echo "Updating $whichtoupdate according to command line argument."
167 else
168         echo "Synchronizing."
169 fi
170
171 filteropts=()
172 ! [ -e filters ] || filteropts=("${filteropts[@]}" --filter='. filters')
173 # 'R *' or 'S *' disables filtering on the staging dir side.
174
175 COPYIN=(cp2 --del --filter='R *' "${filteropts[@]}")
176 COPYOUT=(cp2 --del --filter='S *' "${filteropts[@]}" --no-t --checksum) # be nice to mtimes
177
178 # hash_dir foo/ ==> a hash code covering all of the shown files in foo/
179 function hash_dir {
180         # Itemize the dir, extract filenames, hash the files, and hash the list of
181         # hashes.
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
186 }
187
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"; }
195
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.
203                 whichtoupdate=branch
204         elif ! [ $patchch ]; then
205                 # Branch changed, and trunk may have also changed.  Update patch.
206                 whichtoupdate=patch
207         else
208                 # Branch and patch both changed.  A message appears later.
209                 whichtoupdate=conflict
210         fi
211         #echo "Synchronization will update $whichtoupdate."
212 fi
213
214 if [ -n "$whichtoupdate" ]; then
215
216 # Always show what would happen if patch-new and branch-new were copied out.
217 # (If there was a problem creating one of them, patchsync would have just
218 # deleted it.)  But only actually copy them out and update synchronization
219 # state if no error.
220 error=
221
222 # Don't let stuff from an old run confuse us.
223 rm -rf patch-new branch-new
224
225 function prepare_branch {
226         echo "Preparing updated branch..."
227         # No link-dest because we will modify and then link-dest when copying out
228         "${COPYIN[@]}" trunk/ branch-new/
229         do_patch patch branch-new || \
230                 { error=1; echo "Failed to prepare updated branch!" 1>&2; rm -rf branch-new; }
231 }
232
233 function prepare_patch {
234         echo "Preparing updated patch..."
235         # Link-dest is fine because these are temporary read-only copies
236         "${COPYIN[@]}" --link-dest=../trunk/ trunk/ trunk-tmp/
237         "${COPYIN[@]}" --link-dest=../branch/ branch/ branch-tmp/
238         do_diff trunk-tmp branch-tmp patch-new || \
239                 { error=1; echo "Failed to prepare updated patch!" 1>&2; rm -rf patch-new; }
240         rm -rf trunk-tmp branch-tmp
241 }
242
243 case $whichtoupdate in
244 (identical-branch)
245         echo "Creating identical branch..."
246         # No link-dest because we will link-dest when copying out
247         "${COPYIN[@]}" trunk/ branch-new/
248         echo "Creating empty patch..."
249         do_diff branch-new branch-new patch-new || \
250                 { error=1; echo "Failed to create empty patch!" 1>&2; rm -rf patch-new; }
251         ;;
252 (branch)
253         prepare_branch
254         ;;
255 (patch)
256         prepare_patch
257         ;;
258 (conflict)
259         error=1
260         cat <<EOF 1>&2
261 CONFLICT: both branch and patch changed!
262 Run patchsync <staging> {branch | patch} to
263 update the specified thing from the others.
264 I'll leave updated copies of both branch
265 and patch in the staging directory to help
266 you decide which way you want to update.
267 EOF
268         prepare_branch
269         prepare_patch
270         ;;
271 (*)
272         echo "Internal error, whichtoupdate should not be $whichtoupdate!" 1>&2
273         exit 1
274         ;;
275 esac
276
277 if ! [ $error ] && ! [ $dryrun ]; then
278         # Disable locking for now...
279         # ! [ -e lock ] || { echo "Staging dir is locked!  Delete the file \`lock' if the other instance of patchsync is gone." 1>&2; exit 1; }
280         # echo "patchsync lock file pid $$ date $(date)" >lock
281         
282         echo "Copying out..."
283         ! [ -e branch-new ] || {
284                 hash_dir branch-new/ >branch-new-hash
285                 "${COPYOUT[@]}" -i --link-dest="$(wdpp_from branch/)branch-new/" branch-new/ branch/
286                 rm -rf branch-new
287         }
288         ! [ -e patch-new ] || cmp -s patch-work patch || {
289                 hash_file patch-new >patch-new-hash
290                 # Don't use rsync because we might have to write through a symlink.
291                 echo "> patch"
292                 cp --preserve=timestamps patch-new patch
293                 rm -f patch-new
294         }
295         
296         echo "Remembering synchronized state for next time..."
297         for i in trunk patch branch; do
298                 mv $i-new-hash $i-save-hash
299         done
300         
301         # rm lock
302 else
303         echo "Would copy out as follows:"
304         ! [ -e branch-new ] || {
305                 hash_dir branch-new/ >branch-new-hash
306                 "${COPYOUT[@]}" -n -i --link-dest="$(wdpp_from branch/)branch-new/" branch-new/ branch/
307                 #rm -rf branch-new
308         }
309         ! [ -e patch-new ] || cmp -s patch-work patch || {
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.  Open templates because we will change directories.
351         trunk="$1"
352         patch="$2"
353         branch="$3"
354         staging="$4"
355         
356         # What exists?  Whichtochange first?
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="$wdpp$trunk"
368         patch="$wdpp$patch"
369         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 2 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