Commit | Line | Data |
---|---|---|
67afd050 | 1 | #!/bin/bash |
37ecca1d MM |
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 | |
67afd050 MM |
83 | |
84 | set -e | |
37ecca1d MM |
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 | |
67afd050 MM |
143 | |
144 | staging="$1" | |
37ecca1d MM |
145 | if [ -r "$staging/settings" ]; then |
146 | echo "Using staging dir $staging" | |
147 | else | |
67afd050 MM |
148 | echo "Specify a staging directory containing a settings file!" 1>&2 |
149 | exit 1 | |
150 | fi | |
37ecca1d MM |
151 | cd "$staging" || { echo "Failed to enter staging dir!" 1>&2; exit 1; } |
152 | shift | |
67afd050 MM |
153 | |
154 | . settings | |
37ecca1d MM |
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; } | |
67afd050 | 157 | |
37ecca1d MM |
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 | |
67afd050 | 207 | else |
37ecca1d MM |
208 | # Branch and patch both changed. A message appears later. |
209 | whichtoupdate=conflict | |
67afd050 | 210 | fi |
37ecca1d | 211 | #echo "Synchronization will update $whichtoupdate." |
67afd050 MM |
212 | fi |
213 | ||
37ecca1d MM |
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 | |
67afd050 MM |
281 | |
282 | echo "Copying out..." | |
37ecca1d MM |
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 | |
67afd050 | 299 | done |
67afd050 | 300 | |
37ecca1d | 301 | # rm lock |
67afd050 | 302 | else |
37ecca1d MM |
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 | |
67afd050 | 324 | fi |
37ecca1d MM |
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! | |
67afd050 | 340 | fi |
37ecca1d MM |
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 | } | |
67afd050 | 431 | |
37ecca1d MM |
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 |