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. | |
37ecca1d | 4 | # -- Matt McCutchen |
5b69859a MM |
5 | |
6 | # If I had to update the version in the --version message separately, I would forget. | |
40ea9b78 | 7 | PATCHSYNC_VERSION=2.4 |
5b69859a | 8 | |
37ecca1d MM |
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 | |
40ea9b78 MM |
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. | |
37ecca1d MM |
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 | |
67afd050 | 85 | |
40ea9b78 MM |
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 | |
37ecca1d MM |
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. | |
75ae3f77 | 101 | type cp2 >/dev/null 2>&1 || function cp2 { exec rsync -rltE --chmod=ugo=rwx "$@"; } |
37ecca1d MM |
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#/}" | |
40ea9b78 | 117 | pB="$(cd "$AtoB" && /bin/pwd)/" |
37ecca1d MM |
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 | |
40ea9b78 | 130 | ans="../$ans" |
37ecca1d MM |
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<. | |
40ea9b78 | 135 | [ $? == 0 ] |
37ecca1d MM |
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 | |
67afd050 MM |
152 | |
153 | staging="$1" | |
37ecca1d MM |
154 | if [ -r "$staging/settings" ]; then |
155 | echo "Using staging dir $staging" | |
156 | else | |
67afd050 MM |
157 | echo "Specify a staging directory containing a settings file!" 1>&2 |
158 | exit 1 | |
159 | fi | |
37ecca1d MM |
160 | cd "$staging" || { echo "Failed to enter staging dir!" 1>&2; exit 1; } |
161 | shift | |
67afd050 | 162 | |
75ae3f77 | 163 | . ./settings |
37ecca1d MM |
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; } | |
67afd050 | 166 | |
37ecca1d | 167 | whichtoupdate="$1" |
37ecca1d MM |
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 | |
67afd050 | 210 | else |
37ecca1d MM |
211 | # Branch and patch both changed. A message appears later. |
212 | whichtoupdate=conflict | |
67afd050 | 213 | fi |
37ecca1d | 214 | #echo "Synchronization will update $whichtoupdate." |
67afd050 MM |
215 | fi |
216 | ||
2a4c0430 MM |
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 | ||
37ecca1d MM |
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 | ||
37ecca1d MM |
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/ | |
40ea9b78 MM |
233 | (do_patch patch branch-new) |
234 | [ $? == 0 ] || { error=1; echo "Failed to prepare updated branch!" 1>&2; rm -rf branch-new; } | |
37ecca1d MM |
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/ | |
40ea9b78 MM |
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; } | |
37ecca1d MM |
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..." | |
40ea9b78 MM |
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; } | |
37ecca1d MM |
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 | |
67afd050 | 282 | echo "Copying out..." |
37ecca1d MM |
283 | ! [ -e branch-new ] || { |
284 | hash_dir branch-new/ >branch-new-hash | |
40ea9b78 MM |
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/ | |
37ecca1d MM |
287 | rm -rf branch-new |
288 | } | |
5b69859a | 289 | ! [ -e patch-new ] || cmp -s patch patch-new || { |
37ecca1d MM |
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 | |
67afd050 | 300 | done |
67afd050 | 301 | else |
37ecca1d MM |
302 | echo "Would copy out as follows:" |
303 | ! [ -e branch-new ] || { | |
304 | hash_dir branch-new/ >branch-new-hash | |
40ea9b78 MM |
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/ | |
37ecca1d MM |
307 | #rm -rf branch-new |
308 | } | |
5b69859a | 309 | ! [ -e patch-new ] || cmp -s patch patch-new || { |
37ecca1d MM |
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 | ||
75ae3f77 | 350 | # Set up arguments. |
37ecca1d MM |
351 | trunk="$1" |
352 | patch="$2" | |
353 | branch="$3" | |
354 | staging="$4" | |
355 | ||
75ae3f77 | 356 | # What exists? |
37ecca1d MM |
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. | |
40ea9b78 MM |
367 | [[ "$trunk" == /* ]] || trunk="$wdpp$trunk" |
368 | [[ "$patch" == /* ]] || patch="$wdpp$patch" | |
369 | [[ "$branch" == /* ]] || branch="$wdpp$branch" | |
37ecca1d MM |
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 | |
5b69859a | 425 | Patchsync version $PATCHSYNC_VERSION by Matt McCutchen |
37ecca1d MM |
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 |