Add mock configuration for building against the local dnf repository
[utils/utils.git] / patchsync
... / ...
CommitLineData
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.
7PATCHSYNC_VERSION=2.4
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" 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.
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# Error handling
87function handle_error {
88 exec >&2
89 echo "Patchsync encountered an unexpected error! Aborting!"
90 echo "The failed command was: $1"
91 exit 2
92}
93trap 'handle_error "$BASH_COMMAND"' ERR
94set -o errtrace
95set -o pipefail
96
97# Make sure we have rsync.
98type 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.
101type cp2 >/dev/null 2>&1 || function cp2 { exec rsync -rltE --chmod=ugo=rwx "$@"; }
102
103function 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.
111function 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#/}"
117 pB="$(cd "$AtoB" && /bin/pwd)/"
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
130 ans="../$ans"
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<.
135 [ $? == 0 ]
136 # Yay
137 echo "$ans"
138}
139
140function hash_file {
141 # Lop off the filename and binary indicator
142 sha1sum -b "$1" | sed -re 's/^([^ ]*).*$/\1/'
143}
144
145function patchsync_sync {
146
147if [ "$1" == --dry-run ]; then
148 echo "Dry run mode."
149 dryrun=1
150 shift
151fi
152
153staging="$1"
154if [ -r "$staging/settings" ]; then
155 echo "Using staging dir $staging"
156else
157 echo "Specify a staging directory containing a settings file!" 1>&2
158 exit 1
159fi
160cd "$staging" || { echo "Failed to enter staging dir!" 1>&2; exit 1; }
161shift
162
163. ./settings
164type do_diff >/dev/null 2>&1 || { echo "do_diff is not defined!" 1>&2; exit 1; }
165type do_patch >/dev/null 2>&1 || { echo "do_patch is not defined!" 1>&2; exit 1; }
166
167whichtoupdate="$1"
168if [ -n "$whichtoupdate" ]; then
169 echo "Updating $whichtoupdate according to command line argument."
170else
171 echo "Synchronizing."
172fi
173
174filteropts=()
175! [ -e filters ] || filteropts=("${filteropts[@]}" --filter='. filters')
176# 'R *' or 'S *' disables filtering on the staging dir side.
177
178COPYIN=(cp2 --del --filter='R *' "${filteropts[@]}")
179COPYOUT=(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/
182function 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
191echo "Checking for changes..."
192hash_dir trunk/ >trunk-new-hash
193cmp trunk-{save,new}-hash &>/dev/null || { trunkch=1; echo "Trunk has changed"; }
194hash_file patch >patch-new-hash
195cmp patch-{save,new}-hash &>/dev/null || { patchch=1; echo "Patch has changed"; }
196hash_dir branch/ >branch-new-hash
197cmp branch-{save,new}-hash &>/dev/null || { branchch=1; echo "Branch has changed"; }
198
199# If we're in synchronization mode, decide what to update.
200if [ -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
210 else
211 # Branch and patch both changed. A message appears later.
212 whichtoupdate=conflict
213 fi
214 #echo "Synchronization will update $whichtoupdate."
215fi
216
217# Remove old copy-out files to be clean and to make sure we don't
218# mistakenly copy them out this time.
219rm -rf patch-new branch-new
220
221if [ -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.
227error=
228
229function 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/
233 (do_patch patch branch-new)
234 [ $? == 0 ] || { error=1; echo "Failed to prepare updated branch!" 1>&2; rm -rf branch-new; }
235}
236
237function 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/
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; }
244 rm -rf trunk-tmp branch-tmp
245}
246
247case $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..."
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; }
255 ;;
256(branch)
257 prepare_branch
258 ;;
259(patch)
260 prepare_patch
261 ;;
262(conflict)
263 error=1
264 cat <<EOF 1>&2
265CONFLICT: both branch and patch changed!
266Run patchsync <staging> {branch | patch} to
267update the specified thing from the others.
268I'll leave updated copies of both branch
269and patch in the staging directory to help
270you decide which way you want to update.
271EOF
272 prepare_branch
273 prepare_patch
274 ;;
275(*)
276 echo "Internal error, whichtoupdate should not be $whichtoupdate!" 1>&2
277 exit 1
278 ;;
279esac
280
281if ! [ $error ] && ! [ $dryrun ]; then
282 echo "Copying out..."
283 ! [ -e branch-new ] || {
284 hash_dir branch-new/ >branch-new-hash
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/
287 rm -rf branch-new
288 }
289 ! [ -e patch-new ] || cmp -s patch patch-new || {
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
300 done
301else
302 echo "Would copy out as follows:"
303 ! [ -e branch-new ] || {
304 hash_dir branch-new/ >branch-new-hash
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/
307 #rm -rf branch-new
308 }
309 ! [ -e patch-new ] || cmp -s patch patch-new || {
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."
318fi
319
320else # whichtoupdate
321 # Easy case
322 echo "Nothing changed."
323 rm -f {trunk,patch,branch}-new-hash
324fi
325
326if [ $error ]; then
327 echo "Synchronization failed." 1>&2
328 exit 1
329else
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!
340fi
341}
342
343function 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.
351 trunk="$1"
352 patch="$2"
353 branch="$3"
354 staging="$4"
355
356 # What exists?
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" == /* ]] || trunk="$wdpp$trunk"
368 [[ "$patch" == /* ]] || patch="$wdpp$patch"
369 [[ "$branch" == /* ]] || 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!
414END
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
423function patchsync_help {
424 cat <<EOF
425Patchsync version $PATCHSYNC_VERSION by Matt McCutchen
426usage: patchsync [--dry-run] <staging> [branch | patch]
427 patchsync --new <trunk> <patch> <branch> <staging>
428Please read the top of the script for complete documentation.
429EOF
430}
431
432case "$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 "$@" ;;
445esac